PSGuerrilla.psm1
|
$ModuleRoot = $PSScriptRoot function Get-PSGuerrillaDataRoot { <# .SYNOPSIS Returns the per-user data root directory for PSGuerrilla on the current OS. .DESCRIPTION Windows : $env:APPDATA\PSGuerrilla macOS : ~/Library/Application Support/PSGuerrilla Linux : $XDG_CONFIG_HOME/PSGuerrilla, falling back to ~/.config/PSGuerrilla Previously the module hardcoded $env:APPDATA everywhere, which is $null on non-Windows — Join-Path silently returned a relative path and config/state ended up in the current working directory instead of a user-data location. #> [CmdletBinding()] param() # $IsWindows is automatic in PowerShell 6+. Be defensive in case anyone ever # imports this from Windows PowerShell 5.1 (where the var is undefined and # everything is Windows anyway). $onWindows = if (Test-Path variable:IsWindows) { $IsWindows } else { $true } if ($onWindows) { return Join-Path $env:APPDATA 'PSGuerrilla' } if ($IsMacOS) { return Join-Path $HOME 'Library/Application Support/PSGuerrilla' } $base = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' } return Join-Path $base 'PSGuerrilla' } # Helper used during module bootstrap to turn "10.0.0.0/16" into the # { Network = uint32; Mask = uint32 } pair that all the IP classification # code expects. Replaces three near-identical inline blocks that used to # live below. function ConvertTo-ParsedCidr { [CmdletBinding()] param([Parameter(Mandatory)][string]$Cidr) $parts = $Cidr -split '/' if ($parts.Count -ne 2) { return $null } try { $ipBytes = [System.Net.IPAddress]::Parse($parts[0]).GetAddressBytes() $prefix = [int]$parts[1] $ipUint = ([uint32]$ipBytes[0] -shl 24) -bor ([uint32]$ipBytes[1] -shl 16) -bor ([uint32]$ipBytes[2] -shl 8) -bor [uint32]$ipBytes[3] $mask = if ($prefix -eq 0) { [uint32]0 } else { [uint32]::MaxValue -shl (32 - $prefix) } return @{ Network = $ipUint -band $mask; Mask = $mask } } catch { return $null } } # Load data files into script-scoped variables $script:CloudIpRanges = Get-Content -Path (Join-Path $ModuleRoot 'Data/CloudIpRanges.json') -Raw | ConvertFrom-Json $script:KnownAttackerIps = Get-Content -Path (Join-Path $ModuleRoot 'Data/KnownAttackerIps.json') -Raw | ConvertFrom-Json $script:SuspiciousCountries = Get-Content -Path (Join-Path $ModuleRoot 'Data/SuspiciousCountries.json') -Raw | ConvertFrom-Json $script:VpnTorProxies = Get-Content -Path (Join-Path $ModuleRoot 'Data/VpnTorProxies.json') -Raw | ConvertFrom-Json # Pre-parse CIDR ranges into uint32 network/mask pairs for fast bitwise matching $script:ParsedProviderNetworks = [System.Collections.Generic.List[hashtable]]::new() $script:CloudProviderClasses = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $script:AttackerIpSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Parse the providers structure (v2.0.0 format with metadata) $providerData = if ($script:CloudIpRanges.providers) { $script:CloudIpRanges.providers } else { $script:CloudIpRanges } foreach ($providerName in ($providerData | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)) { [void]$script:CloudProviderClasses.Add($providerName) foreach ($cidr in $providerData.$providerName) { $parsed = ConvertTo-ParsedCidr -Cidr $cidr if ($parsed) { $parsed.Provider = $providerName $script:ParsedProviderNetworks.Add($parsed) } else { Write-Verbose "Skipping invalid CIDR: $cidr ($providerName)" } } } # Backward compat: ParsedAwsNetworks and ParsedCloudNetworks still available $script:ParsedAwsNetworks = [System.Collections.Generic.List[hashtable]]::new() $script:ParsedCloudNetworks = [System.Collections.Generic.List[hashtable]]::new() foreach ($entry in $script:ParsedProviderNetworks) { if ($entry.Provider -eq 'aws') { $script:ParsedAwsNetworks.Add($entry) } else { $script:ParsedCloudNetworks.Add($entry) } } # Parse VPN/Tor/Proxy CIDRs $script:ParsedVpnNetworks = [System.Collections.Generic.List[hashtable]]::new() $script:ParsedProxyNetworks = [System.Collections.Generic.List[hashtable]]::new() $script:TorExitNodes = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $script:VpnProviderNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) if ($script:VpnTorProxies) { foreach ($cidr in $script:VpnTorProxies.vpn_provider_cidrs) { $parsed = ConvertTo-ParsedCidr -Cidr $cidr if ($parsed) { $script:ParsedVpnNetworks.Add($parsed) } } foreach ($cidr in $script:VpnTorProxies.proxy_service_cidrs) { $parsed = ConvertTo-ParsedCidr -Cidr $cidr if ($parsed) { $script:ParsedProxyNetworks.Add($parsed) } } foreach ($ip in $script:VpnTorProxies.tor_exit_nodes) { [void]$script:TorExitNodes.Add($ip) } foreach ($name in $script:VpnTorProxies.vpn_provider_names) { [void]$script:VpnProviderNames.Add($name) } } foreach ($entry in $script:KnownAttackerIps.ips) { [void]$script:AttackerIpSet.Add($entry.address) } # Config path $script:ConfigPath = Join-Path (Get-PSGuerrillaDataRoot) 'config.json' # Dot-source private functions (recursive to pick up subdirectories) foreach ($file in Get-ChildItem -Path (Join-Path $ModuleRoot 'Private') -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue) { . $file.FullName } # Dot-source public functions foreach ($file in Get-ChildItem -Path (Join-Path $ModuleRoot 'Public') -Filter '*.ps1' -ErrorAction SilentlyContinue) { . $file.FullName } # IP classification cache (reset per module load) $script:IpClassCache = @{} $script:GeoCache = @{} # --- Color palette --- # Defined once in module scope so per-file color blocks all point at the same # palette. Change a shade here and every cmdlet picks it up automatically. $script:Palette = @{ Amber = $PSStyle.Foreground.FromRgb(0xC6, 0x7A, 0x1F) Khaki = $PSStyle.Foreground.FromRgb(0xB8, 0xA9, 0x7E) Gray = $PSStyle.Foreground.FromRgb(0x8B, 0x8B, 0x7A) Sage = $PSStyle.Foreground.FromRgb(0x6B, 0x8E, 0x6B) Parchment = $PSStyle.Foreground.FromRgb(0xF5, 0xF0, 0xE6) Gold = $PSStyle.Foreground.FromRgb(0xD4, 0xA8, 0x43) Red = $PSStyle.Foreground.FromRgb(0xCC, 0x55, 0x55) Reset = $PSStyle.Reset } # --- Spectre.Console capability detection --- Initialize-SpectreCapability # --- Config migration from PSRecon --- Initialize-ConfigMigration # --- Banner on import --- Write-GuerrillaBanner # --- Backward-compatibility aliases --- $aliasMap = @{ # PSRecon -> PSGuerrilla rename aliases 'Invoke-GoogleRecon' = 'Invoke-Recon' 'Get-ReconAlerts' = 'Get-DeadDrop' 'Send-ReconAlert' = 'Send-Signal' 'Send-ReconAlertSendGrid' = 'Send-SignalSendGrid' 'Send-ReconAlertMailgun' = 'Send-SignalMailgun' 'Send-ReconAlertTwilio' = 'Send-SignalTwilio' 'Set-ReconConfig' = 'Set-Safehouse' 'Get-ReconConfig' = 'Get-Safehouse' 'Register-ReconScheduledTask' = 'Register-Patrol' 'Unregister-ReconScheduledTask' = 'Unregister-Patrol' 'Get-ReconScheduledTask' = 'Get-Patrol' # Theater-disambiguating aliases — Invoke-Recon and Invoke-Reconnaissance # are easily confused (different theaters). These names make the intent # obvious at the call site. 'Invoke-WorkspaceRecon' = 'Invoke-Recon' # Google Workspace user-behavior recon 'Invoke-ADRecon' = 'Invoke-Reconnaissance' # Active Directory configuration audit 'Invoke-CloudRecon' = 'Invoke-Infiltration' # Entra ID / Azure / Intune / M365 audit } foreach ($old in $aliasMap.Keys) { $new = $aliasMap[$old] Set-Alias -Name $old -Value $new -Scope Script Export-ModuleMember -Alias $old } # Pester test hatch: Tests\Helpers\TestHelpers.psm1 sets PSGUERRILLA_TEST=1 before # Import-Module so existing tests can call private functions directly without # having to wrap every assertion in `InModuleScope PSGuerrilla { ... }`. New tests # should prefer InModuleScope and avoid relying on this. End-user code must NOT # set this variable — it intentionally widens the public API surface. if ($env:PSGUERRILLA_TEST -eq '1') { Export-ModuleMember -Function * } |