Private/Audit/Get-GuerrillaSimulatedFindings.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution

# Test-mode support. Builds a full set of all-FAIL audit findings for a theater
# straight from the shipped check definitions, with no live connection or data
# collection. Used by the -TestMode switch on the scan cmdlets so an operator can
# preview a report (and verify branding / themes) without touching a real tenant.
# Findings are real PSGuerrilla.AuditFinding objects, so every downstream feature
# (scoring, branding, report styles, affected-accounts lists) works unchanged.

# Produce a realistic "bad" CurrentValue for a check based on its name/severity.
function Get-GuerrillaBadValue {
    [CmdletBinding()]
    param([hashtable]$Check)

    $name = ([string]$Check.name).ToLower()

    if ($name -match 'enabl|audit|log')       { return 'Disabled' }
    if ($name -match 'mfa|multi.factor')       { return 'Not enforced' }
    if ($name -match 'encrypt')                { return 'Disabled' }
    if ($name -match 'password.*length')       { return '4 characters' }
    if ($name -match 'password.*age')          { return 'Never expires' }
    if ($name -match 'password.*complex')      { return 'Not required' }
    if ($name -match 'password.*history')      { return '0 passwords remembered' }
    if ($name -match 'lockout')                { return 'No lockout configured' }
    if ($name -match 'expir')                  { return 'Never' }
    if ($name -match 'shar|external')          { return 'Anyone (no restrictions)' }
    if ($name -match 'forward')                { return 'Allowed to external' }
    if ($name -match 'guest|anonymous')        { return 'Unrestricted' }
    if ($name -match 'admin|privilege')        { return 'Excessive permissions found' }
    if ($name -match 'stale|inactive|orphan')  { return 'Multiple found' }
    if ($name -match 'sign|smb|ldap|ntlm')     { return 'Not required' }
    if ($name -match 'delegation')             { return 'Unconstrained' }
    if ($name -match 'kerberos|spn')           { return 'Weak encryption (RC4)' }
    if ($name -match 'cert|ca |adcs|esc\d')    { return 'Vulnerable configuration' }
    if ($name -match 'gpo|group policy')       { return 'Misconfigured' }
    if ($name -match 'trust')                  { return 'SID filtering disabled' }
    if ($name -match 'compliance|policy')      { return 'Non-compliant' }
    if ($name -match 'conditional access')     { return 'Not configured' }
    if ($name -match 'pim|role')               { return 'Permanent assignments found' }
    if ($name -match 'app|oauth|consent')      { return 'Unreviewed permissions' }
    if ($name -match 'federation')             { return 'Insecure configuration' }
    if ($name -match 'intune|endpoint|device') { return 'Not enrolled' }
    if ($name -match 'defender|threat')        { return 'Disabled' }
    if ($name -match 'retention')              { return '0 days' }
    if ($name -match 'dkim|dmarc|spf')         { return 'Not configured' }
    if ($name -match 'transport|rule')         { return 'Insecure rules found' }

    switch ($Check.severity) {
        'Critical' { return 'Not configured (critical risk)' }
        'High'     { return 'Disabled' }
        'Medium'   { return 'Default (insecure)' }
        default    { return 'Not configured' }
    }
}

function Get-GuerrillaSimulatedFindings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('ActiveDirectory', 'GoogleWorkspace', 'EntraM365')]
        [string]$Theater
    )

    # Resolve the check-definition base names that make up each theater's full set.
    $defNames = switch ($Theater) {
        'GoogleWorkspace' {
            @('AuthenticationChecks', 'EmailSecurityChecks', 'DriveSecurityChecks', 'OAuthSecurityChecks',
              'AdminManagementChecks', 'CollaborationChecks', 'DeviceManagementChecks', 'LoggingAlertingChecks')
        }
        'EntraM365' {
            @('EntraAuthChecks', 'EntraCAChecks', 'EntraPIMChecks', 'EntraAppChecks', 'EntraFedChecks',
              'EntraTenantChecks', 'AzureIAMChecks', 'IntuneChecks', 'M365ExchangeChecks', 'M365SharePointChecks',
              'M365TeamsChecks', 'M365DefenderChecks', 'M365AuditChecks', 'M365PowerPlatformChecks')
        }
        'ActiveDirectory' {
            # Discover every AD check file (case-sensitive 'AD'/'TierZero' prefix so the
            # Google Workspace AdminManagementChecks.json is not captured).
            $dataDir = if ($script:ModuleRoot) { Join-Path $script:ModuleRoot 'Data/AuditChecks' }
                       else { Join-Path $PSScriptRoot '../../Data/AuditChecks' }
            @(Get-ChildItem -Path $dataDir -Filter '*.json' |
                Where-Object { $_.Name -cmatch '^(AD[A-Z]|TierZero)' } |
                ForEach-Object { $_.BaseName } |
                Sort-Object)
        }
    }

    # Fake accounts to populate the "affected accounts" list for checks that declare
    # an affectedLabel — so the affected-accounts feature is visible in a test report.
    $sampleAccounts = @(
        'jsmith@sample.org', 'akumar@sample.org', 'mchen@sample.org', 'rlopez@sample.org',
        'tokafor@sample.org', 'dwilson@sample.org', 'bnguyen@sample.org', 'pgarcia@sample.org'
    )

    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($name in $defNames) {
        $defs = Get-AuditCategoryDefinitions -Category $name
        foreach ($check in $defs.checks) {
            $details = @{}
            if ($check.affectedLabel) {
                $idNum = [int]([regex]::Match([string]$check.id, '\d+').Value)
                $count = ($idNum % 5) + 3
                $details.AffectedItems = @($sampleAccounts | Select-Object -First $count)
                $details.AffectedLabel = $check.affectedLabel
            }
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'FAIL' `
                -CurrentValue (Get-GuerrillaBadValue -Check $check) -Details $details))
        }
    }

    return @($findings)
}