Guerrilla.psm1

$script:ModuleRoot = $PSScriptRoot

function Get-GuerrillaDataRoot {
    <#
    .SYNOPSIS
        Returns the per-user data root directory for Guerrilla on the current OS.
    .DESCRIPTION
        Windows : $env:APPDATA\Guerrilla
        macOS : ~/Library/Application Support/Guerrilla
        Linux : $XDG_CONFIG_HOME/Guerrilla, falling back to ~/.config/Guerrilla

        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 }

    $newRoot = if ($onWindows) {
        Join-Path $env:APPDATA 'Guerrilla'
    } elseif ($IsMacOS) {
        Join-Path $HOME 'Library/Application Support/Guerrilla'
    } else {
        $base = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
        Join-Path $base 'Guerrilla'
    }

    # Back-compat: the module was renamed PSGuerrilla -> Guerrilla. If this install
    # predates the rename, carry the old per-user data (reports, config, patrol state)
    # forward exactly once — when the new root does not yet exist but the old one does.
    # Idempotent: after the first migration the new root exists and this is skipped.
    if (-not (Test-Path $newRoot)) {
        $oldRoot = Join-Path (Split-Path $newRoot -Parent) 'PSGuerrilla'
        if ((Test-Path $oldRoot) -and @(Get-ChildItem -LiteralPath $oldRoot -Force -ErrorAction SilentlyContinue).Count -gt 0) {
            try {
                Copy-Item -LiteralPath $oldRoot -Destination $newRoot -Recurse -Force -ErrorAction Stop
                Write-Verbose "Migrated Guerrilla data from $oldRoot to $newRoot (post-rename)."
            } catch {
                Write-Verbose "Guerrilla data migration from $oldRoot failed: $($_.Exception.Message)"
            }
        }
    }
    return $newRoot
}

# Config path
$script:ConfigPath = Join-Path (Get-GuerrillaDataRoot) 'config.json'

# Dot-source internal engine helpers and the platform checks (recursive over the
# source tree). internal/ holds the collectors and engine; checks/ holds the
# per-platform Invoke-*Checks files. Both are loaded before public so the exported
# cmdlets can call them.
foreach ($dir in 'internal', 'checks') {
    foreach ($file in Get-ChildItem -Path (Join-Path $ModuleRoot $dir) -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue) {
        . $file.FullName
    }
}

# Dot-source public functions (the exported cmdlets)
foreach ($file in Get-ChildItem -Path (Join-Path $ModuleRoot 'public') -Filter '*.ps1' -Recurse -ErrorAction SilentlyContinue) {
    . $file.FullName
}

# Test-mode flag (reset per module load). When true, the console timestamp helpers
# (Write-ProgressLine / Write-OperationHeader) render zeroed times so -TestMode demo
# output is deterministic. The audit cmdlets set it per run (self-healing).
$script:GuerrillaTestMode = $false

# SID-resolution caches/tables (reset per module load). Resolve-ADSid references all
# three; without these the AD ACL/DCSync/GPO-delegation collectors throw
# "You cannot call a method on a null-valued expression" and silently return empty.
$script:SidCache = @{}

$script:WellKnownSids = @{
    'S-1-0-0'      = 'NULL';                       'S-1-1-0'      = 'Everyone'
    'S-1-2-0'      = 'Local';                      'S-1-3-0'      = 'Creator Owner'
    'S-1-3-1'      = 'Creator Group';              'S-1-5-2'      = 'Network'
    'S-1-5-4'      = 'Interactive';                'S-1-5-6'      = 'Service'
    'S-1-5-7'      = 'Anonymous';                  'S-1-5-9'      = 'Enterprise Domain Controllers'
    'S-1-5-10'     = 'Principal Self';             'S-1-5-11'     = 'Authenticated Users'
    'S-1-5-13'     = 'Terminal Server Users';      'S-1-5-18'     = 'SYSTEM'
    'S-1-5-19'     = 'Local Service';              'S-1-5-20'     = 'Network Service'
    'S-1-5-32-544' = 'Administrators';             'S-1-5-32-545' = 'Users'
    'S-1-5-32-546' = 'Guests';                     'S-1-5-32-548' = 'Account Operators'
    'S-1-5-32-549' = 'Server Operators';           'S-1-5-32-550' = 'Print Operators'
    'S-1-5-32-551' = 'Backup Operators';           'S-1-5-32-554' = 'Pre-Windows 2000 Compatible Access'
    'S-1-5-32-555' = 'Remote Desktop Users';       'S-1-5-32-557' = 'Incoming Forest Trust Builders'
    'S-1-5-32-562' = 'Distributed COM Users';      'S-1-5-32-568' = 'IIS_IUSRS'
    'S-1-5-32-569' = 'Cryptographic Operators';    'S-1-5-32-573' = 'Event Log Readers'
    'S-1-5-32-578' = 'Hyper-V Administrators';     'S-1-5-32-579' = 'Access Control Assistance Operators'
    'S-1-5-32-580' = 'Remote Management Users'
}

# Domain-RELATIVE RIDs (last sub-authority of a domain SID, S-1-5-21-...-RID)
$script:WellKnownRids = @{
    500 = 'Administrator';  501 = 'Guest';                     502 = 'krbtgt'
    512 = 'Domain Admins';  513 = 'Domain Users';              514 = 'Domain Guests'
    515 = 'Domain Computers'; 516 = 'Domain Controllers';      517 = 'Cert Publishers'
    518 = 'Schema Admins';  519 = 'Enterprise Admins';         520 = 'Group Policy Creator Owners'
    521 = 'Read-only Domain Controllers'; 522 = 'Cloneable Domain Controllers'
    525 = 'Protected Users'; 526 = 'Key Admins';               527 = 'Enterprise Key Admins'
    553 = 'RAS and IAS Servers'
}

# --- 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 ---
# The behavioral monitoring subsystem was removed (see
# docs/proposals/effective-state-and-audit-log-inference.md); only aliases
# targeting surviving assessment cmdlets remain.
$aliasMap = @{
    # PSRecon -> Guerrilla rename aliases
    'Set-ReconConfig'              = 'Set-Safehouse'
    'Get-ReconConfig'              = 'Get-Safehouse'

    # Platform-disambiguating aliases: the intent is obvious at the call site.
    'Invoke-ADRecon'               = 'Invoke-ADAudit'  # Active Directory configuration audit
    'Invoke-CloudRecon'            = 'Invoke-EntraAudit'    # 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 Guerrilla { ... }`. 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 *
}