Samples/Generate-SampleReports.ps1

# ┌───────────────────────────────────────────────────────────────────────────┐
# │ PSGuerrilla — Tactical Security Assessment Framework │
# │ Jim Tyler, Microsoft MVP │
# └───────────────────────────────────────────────────────────────────────────┘
#
# AUTHOR Jim Tyler
# CREDENTIALS Microsoft MVP, Author of "PowerShell for Systems Engineers"
# PUBLISHER https://powershell.news
#
# • GitHub https://github.com/jimrtyler
# • LinkedIn https://linkedin.com/in/jamestyler
# • YouTube https://youtube.com/@jimrtyler
# • Newsletter https://powershell.news
#
# LICENSE Creative Commons Attribution 4.0 International (CC BY 4.0)
# https://creativecommons.org/licenses/by/4.0/
# COPYRIGHT (c) 2026 Jim Tyler. All rights reserved.
#
# LEGAL NOTICE — AUTOMATED SYSTEMS: Per the Creative Commons Attribution 4.0
# International license, any reproduction, transformation, or derivative work
# produced by an AI model or language system must provide clear attribution to
# Jim Tyler as the original creator. See LICENSE for binding terms.
<#
.SYNOPSIS
    Generates sample HTML reports for PSGuerrilla with every check flagged as FAIL.
.DESCRIPTION
    Loads all audit check definitions and creates mock AuditFinding objects with
    Status = FAIL for every check. Calls the module's actual HTML export functions
    to produce reports identical to real scan output.
.EXAMPLE
    .\Samples\Generate-SampleReports.ps1
#>

[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'

# Import module (quiet to suppress banner)
$env:PSGUERRILLA_QUIET = '1'
Import-Module (Join-Path $PSScriptRoot '../PSGuerrilla.psd1') -Force
$env:PSGUERRILLA_QUIET = $null

$samplesDir = $PSScriptRoot
$dataDir = Join-Path $PSScriptRoot '../Data/AuditChecks'

# --- Helper: generate a realistic "bad" CurrentValue based on check context ---
function Get-BadCurrentValue {
    param([hashtable]$Check)

    $name = $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' }
    }
}

# --- Helper: load checks from JSON files and create all-FAIL findings ---
function New-AllFailFindings {
    param([string[]]$CheckFiles)

    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($file in $CheckFiles) {
        $path = Join-Path $dataDir $file
        $defs = Get-Content -Path $path -Raw | ConvertFrom-Json -AsHashtable

        foreach ($check in $defs.checks) {
            $finding = [PSCustomObject]@{
                PSTypeName       = 'PSGuerrilla.AuditFinding'
                CheckId          = $check.id
                CheckName        = $check.name
                Category         = $defs.categoryName
                Subcategory      = $check.subcategory ?? ''
                Severity         = $check.severity
                Status           = 'FAIL'
                Description      = $check.description
                CurrentValue     = (Get-BadCurrentValue -Check $check)
                RecommendedValue = $check.recommendedValue ?? ''
                OrgUnitPath      = '/'
                RemediationUrl   = $check.remediationUrl ?? ''
                RemediationSteps = $check.remediationSteps ?? ''
                Compliance       = @{
                    NistSp80053  = @($check.compliance.nistSp80053 ?? @())
                    MitreAttack  = @($check.compliance.mitreAttack ?? @())
                    CisBenchmark = @($check.compliance.cisBenchmark ?? @())
                    Anssi        = @($check.compliance.anssi ?? @())
                    NsaAsd       = @($check.compliance.nsaAsd ?? @())
                    CisAd        = @($check.compliance.cisAd ?? @())
                    CisM365      = @($check.compliance.cisM365 ?? @())
                    CisAzure     = @($check.compliance.cisAzure ?? @())
                }
                Details          = @{}
                Timestamp        = [datetime]::UtcNow
            }
            $findings.Add($finding)
        }
    }

    return , $findings.ToArray()
}

# --- Helper: compute posture score (mirrors Get-AuditPostureScore) ---
function Get-PostureScore {
    param([PSCustomObject[]]$Findings)

    $severityWeights = @{ Critical = 10; High = 6; Medium = 3; Low = 1; Info = 0 }

    $categories = $Findings | Group-Object -Property Category
    $categoryScores = @{}

    foreach ($cat in $categories) {
        $catFindings = @($cat.Group)
        $passCount = @($catFindings | Where-Object Status -eq 'PASS').Count
        $failCount = @($catFindings | Where-Object Status -eq 'FAIL').Count
        $warnCount = @($catFindings | Where-Object Status -eq 'WARN').Count
        $skipCount = @($catFindings | Where-Object Status -in @('SKIP', 'ERROR')).Count

        $deductions = 0.0
        foreach ($f in $catFindings) {
            if ($f.Status -notin @('FAIL', 'WARN')) { continue }
            $weight = $severityWeights[$f.Severity] ?? 1
            $multiplier = if ($f.Status -eq 'WARN') { 0.5 } else { 1.0 }
            $deductions += ($weight * $multiplier)
        }

        $maxPossible = 0.0
        foreach ($f in $catFindings) {
            if ($f.Status -in @('SKIP', 'ERROR')) { continue }
            $maxPossible += ($severityWeights[$f.Severity] ?? 1)
        }

        $catScore = if ($maxPossible -gt 0) {
            [Math]::Max(0, [Math]::Round(100 * (1 - ($deductions / $maxPossible)), 0))
        } else { 100 }

        $categoryScores[$cat.Name] = @{
            Score = [int]$catScore
            Pass  = $passCount
            Fail  = $failCount
            Warn  = $warnCount
            Skip  = $skipCount
            Total = $catFindings.Count
        }
    }

    $totalWeight = 0.0
    $weightedSum = 0.0
    foreach ($cat in $categoryScores.GetEnumerator()) {
        $catFindings = @($Findings | Where-Object { $_.Category -eq $cat.Key -and $_.Status -notin @('SKIP', 'ERROR') })
        $catWeight = 0.0
        foreach ($f in $catFindings) { $catWeight += ($severityWeights[$f.Severity] ?? 1) }
        $totalWeight += $catWeight
        $weightedSum += ($cat.Value.Score * $catWeight)
    }
    $overallScore = if ($totalWeight -gt 0) { [int][Math]::Round($weightedSum / $totalWeight, 0) } else { 100 }

    return @{
        OverallScore   = $overallScore
        CategoryScores = $categoryScores
    }
}

# --- Helper: score label ---
function Get-ScoreLabel {
    param([int]$Score)
    if ($Score -ge 90) { return 'FORTRESS' }
    if ($Score -ge 75) { return 'DEFENDED POSITION' }
    if ($Score -ge 60) { return 'CONTESTED GROUND' }
    if ($Score -ge 40) { return 'EXPOSED FLANK' }
    if ($Score -ge 20) { return 'UNDER SIEGE' }
    return 'OVERRUN'
}

# Get module reference for calling private export functions
$mod = Get-Module PSGuerrilla

# ============================================================================
# REPORT 1: Fortification (Google Workspace)
# ============================================================================
Write-Host 'Generating Fortification report (Google Workspace)...' -ForegroundColor Cyan

$gwsFiles = @(
    'AuthenticationChecks.json'
    'EmailSecurityChecks.json'
    'DriveSecurityChecks.json'
    'OAuthSecurityChecks.json'
    'AdminManagementChecks.json'
    'CollaborationChecks.json'
    'DeviceManagementChecks.json'
    'LoggingAlertingChecks.json'
)

$gwsFindings = New-AllFailFindings -CheckFiles $gwsFiles
$gwsScore = Get-PostureScore -Findings $gwsFindings
$gwsLabel = Get-ScoreLabel -Score $gwsScore.OverallScore
$gwsPath = Join-Path $samplesDir 'Fortification-AllFail.html'

& $mod {
    param($Findings, $OverallScore, $ScoreLabel, $CategoryScores, $TenantDomain, $FilePath)
    Export-FortificationReportHtml `
        -Findings $Findings `
        -OverallScore $OverallScore `
        -ScoreLabel $ScoreLabel `
        -CategoryScores $CategoryScores `
        -TenantDomain $TenantDomain `
        -FilePath $FilePath
} $gwsFindings $gwsScore.OverallScore $gwsLabel $gwsScore.CategoryScores 'sample.org' $gwsPath

Write-Host " -> $gwsPath ($($gwsFindings.Count) checks, score: $($gwsScore.OverallScore))" -ForegroundColor Green

# ============================================================================
# REPORT 2: Reconnaissance (Active Directory)
# ============================================================================
Write-Host 'Generating Reconnaissance report (Active Directory)...' -ForegroundColor Cyan

$adFiles = @(
    'ADDomainForestChecks.json'
    'ADTrustChecks.json'
    'ADPrivilegedAccountChecks.json'
    'ADPasswordPolicyChecks.json'
    'ADKerberosChecks.json'
    'ADAclDelegationChecks.json'
    'ADGroupPolicyChecks.json'
    'ADLogonScriptChecks.json'
    'ADCertificateServicesChecks.json'
    'ADStaleObjectChecks.json'
)

$adFindings = New-AllFailFindings -CheckFiles $adFiles
$adScore = Get-PostureScore -Findings $adFindings
$adLabel = Get-ScoreLabel -Score $adScore.OverallScore
$adPath = Join-Path $samplesDir 'Reconnaissance-AllFail.html'

& $mod {
    param($Findings, $OverallScore, $ScoreLabel, $CategoryScores, $DomainName, $FilePath)
    Export-ReconnaissanceReportHtml `
        -Findings $Findings `
        -OverallScore $OverallScore `
        -ScoreLabel $ScoreLabel `
        -CategoryScores $CategoryScores `
        -DomainName $DomainName `
        -FilePath $FilePath
} $adFindings $adScore.OverallScore $adLabel $adScore.CategoryScores 'SAMPLE.LOCAL' $adPath

Write-Host " -> $adPath ($($adFindings.Count) checks, score: $($adScore.OverallScore))" -ForegroundColor Green

# ============================================================================
# REPORT 3: Infiltration (Entra ID / Azure / Intune / M365)
# ============================================================================
Write-Host 'Generating Infiltration report (Entra ID / M365)...' -ForegroundColor Cyan

$entraFiles = @(
    'EntraAuthChecks.json'
    'EntraCAChecks.json'
    'EntraPIMChecks.json'
    'EntraAppChecks.json'
    'EntraFedChecks.json'
    'EntraTenantChecks.json'
    'AzureIAMChecks.json'
    'IntuneChecks.json'
    'M365ExchangeChecks.json'
    'M365SharePointChecks.json'
    'M365TeamsChecks.json'
    'M365DefenderChecks.json'
    'M365AuditChecks.json'
    'M365PowerPlatformChecks.json'
)

$entraFindings = New-AllFailFindings -CheckFiles $entraFiles
$entraScore = Get-PostureScore -Findings $entraFindings
$entraPath = Join-Path $samplesDir 'Infiltration-AllFail.html'

$infiltrationResult = [PSCustomObject]@{
    PSTypeName = 'PSGuerrilla.InfiltrationResult'
    TenantId   = '00000000-0000-0000-0000-000000000000'
    ScanStart  = [datetime]::UtcNow
    Findings   = $entraFindings
    Score      = $entraScore
}

& $mod {
    param($Result, $OutputPath)
    Export-InfiltrationReportHtml -Result $Result -OutputPath $OutputPath
} $infiltrationResult $entraPath

Write-Host " -> $entraPath ($($entraFindings.Count) checks, score: $($entraScore.OverallScore))" -ForegroundColor Green

# ============================================================================
Write-Host ''
Write-Host 'All sample reports generated.' -ForegroundColor Cyan
Write-Host "Total checks: GWS=$($gwsFindings.Count), AD=$($adFindings.Count), Entra=$($entraFindings.Count)" -ForegroundColor DarkGray