Detection/Find-DefenseEvasion.ps1

function Find-DefenseEvasion {
    <#
    .SYNOPSIS
        Detects defense-evasion and security-tampering activity
    .DESCRIPTION
        Scans for indicators that attackers attempted to blind or weaken host defenses:
        AMSI bypass patterns in PowerShell script-block logging (4104), ETW tampering,
        Windows Defender tampering (exclusions, disabled real-time monitoring, 5001/5007
        events), event log tampering (1102/104/4719), and optional heuristic log-gap
        analysis across major logs
    .PARAMETER Hours
        Number of hours to look back in event logs (default: 24)
    .PARAMETER IncludeLogGapAnalysis
        Perform heuristic analysis of time gaps between consecutive events in major logs
    .EXAMPLE
        Find-DefenseEvasion
        Find-DefenseEvasion -Hours 48 -IncludeLogGapAnalysis
    .OUTPUTS
        PSCustomObject with EvasionFindings, Summary, Recommendations, and per-check status
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$Hours = 24,

        [Parameter()]
        [switch]$IncludeLogGapAnalysis
    )

    begin {
        Write-Host "Scanning for defense-evasion and tampering activity (last $Hours hours)..." -ForegroundColor Cyan

        $results = [PSCustomObject]@{
            ScanDate         = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            ComputerName     = $env:COMPUTERNAME
            TimeRange        = "$Hours hours"
            EvasionFindings  = @()
            Summary          = [PSCustomObject]@{
                TotalFindings = 0
                Critical      = 0
                High          = 0
                Medium        = 0
                Low           = 0
                Info          = 0
                ChecksRun     = 0
            }
            CheckStatus      = [PSCustomObject]@{
                ScriptBlockLogQuery = 'Unknown'
                AmsiProviderCheck   = 'Unknown'
                AmsiPatternScan     = 'Unknown'
                EtwPatternScan      = 'Unknown'
                PowerShellLoggingGpo= 'Unknown'
                DefenderPreferences = 'Unknown'
                DefenderStatus      = 'Unknown'
                DefenderEvents      = 'Unknown'
                SecurityLogCleared  = 'Unknown'
                SystemLogCleared    = 'Unknown'
                AuditPolicyChanged  = 'Unknown'
                LogGapAnalysis      = 'NotRequested'
            }
            Recommendations  = @()
        }

        $startTime = (Get-Date).AddHours(-$Hours)

        function Get-EventDataValue {
            param(
                [Parameter()]
                $WinEvent,

                [Parameter()]
                [string]$Name
            )
            try {
                if ($null -eq $WinEvent) { return $null }
                $xml = [xml]$WinEvent.ToXml()
                if ($null -eq $xml -or $null -eq $xml.Event -or $null -eq $xml.Event.EventData) { return $null }
                $data = $xml.Event.EventData.Data
                if ($null -eq $data) { return $null }
                $item = @($data | Where-Object { $null -ne $_.Name -and $_.Name -eq $Name } | Select-Object -First 1)
                if ($item.Count -eq 0) { return $null }
                $node = $item[0]
                if ($node.PSObject.Properties['#text']) { return $node.'#text' }
                return [string]$node
            }
            catch {
                return $null
            }
        }

        function Get-EventMessage {
            param(
                [Parameter()]
                $WinEvent
            )
            if ($null -eq $WinEvent) { return $null }
            if ($WinEvent.PSObject.Properties['Message'] -and $WinEvent.Message) { return [string]$WinEvent.Message }
            return $null
        }

        function Get-EventTimestamp {
            param(
                [Parameter()]
                $WinEvent
            )
            if ($null -eq $WinEvent) { return $null }
            if ($WinEvent.PSObject.Properties['TimeCreated']) { return $WinEvent.TimeCreated }
            return $null
        }

        function Get-MessageSnippet {
            param(
                [Parameter()]
                [string]$Message,

                [Parameter()]
                [int]$MaxLength = 200
            )
            if ([string]::IsNullOrEmpty($Message)) { return '' }
            if ($Message.Length -gt $MaxLength) {
                return ($Message.Substring(0, $MaxLength) + '...')
            }
            return $Message
        }

        # AMSI bypass indicator patterns (T1562.001 - Impair Defenses: Disable or Modify Tools)
        $amsiPatterns = @(
            [PSCustomObject]@{ Name = 'AmsiScanBuffer patch/reference';    Pattern = 'AmsiScanBuffer';                          Combo = $null;                                              Severity = 'High' }
            [PSCustomObject]@{ Name = 'amsiInitFailed forced';             Pattern = 'amsiInitFailed';                          Combo = $null;                                              Severity = 'High' }
            [PSCustomObject]@{ Name = 'amsi.dll memory patch';             Pattern = 'amsi\.dll';                               Combo = '(?i)(patch|VirtualProtect|WriteProcessMemory|Marshal)'; Severity = 'High' }
            [PSCustomObject]@{ Name = 'Reflection-based amsiUtils tamper'; Pattern = '\[Ref\]\.Assembly\.GetType';              Combo = '(?i)amsiUtils';                                    Severity = 'High' }
            [PSCustomObject]@{ Name = 'Marshal::Copy near AMSI';           Pattern = 'Marshal::Copy';                           Combo = '(?i)amsi';                                         Severity = 'Medium' }
        )

        # ETW tamper indicator patterns (T1562.006 - Impair Defenses: Indicator Blocking)
        $etwPatterns = @(
            [PSCustomObject]@{ Name = 'EtwEventWrite patch';               Pattern = 'EtwEventWrite';                           Combo = '(?i)(patch|VirtualProtect|Marshal|GetProcAddress)'; Severity = 'High' }
            [PSCustomObject]@{ Name = 'Generic ETW patch reference';       Pattern = '(?i)etw';                                 Combo = '(?i)patch';                                        Severity = 'Medium' }
        )

        $scriptBlockEvents = $null
    }

    process {
        try {
            # Shared query: PowerShell script-block logging events (4104)
            try {
                $scriptBlockEvents = @(Get-WinEvent -FilterHashtable @{
                    LogName   = 'Microsoft-Windows-PowerShell/Operational'
                    ID        = 4104
                    StartTime = $startTime
                } -ErrorAction SilentlyContinue)
                $results.CheckStatus.ScriptBlockLogQuery = 'OK'
                $results.Summary.ChecksRun++
            }
            catch {
                Write-Warning "Could not query PowerShell Operational log (4104): $_"
                $results.CheckStatus.ScriptBlockLogQuery = 'Unknown'
            }

            # 1a. AMSI provider registration (informational baseline)
            Write-Verbose "Checking AMSI provider registration..."
            try {
                $results.Summary.ChecksRun++
                $amsiProviderPath = 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers'
                $providerKeyExists = $false
                try { $providerKeyExists = Test-Path $amsiProviderPath -ErrorAction SilentlyContinue } catch { $providerKeyExists = $false }

                if ($providerKeyExists) {
                    $providers = @(Get-ChildItem -Path $amsiProviderPath -ErrorAction SilentlyContinue)
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = 'Info'
                        Evidence  = "AMSI provider registration present with $($providers.Count) provider(s) - AMSI appears registered (informational baseline)"
                        Timestamp = $null
                    }
                }
                else {
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = 'Info'
                        Evidence  = 'AMSI provider registry key not found - AMSI providers absent or AMSI unavailable on this host (informational baseline)'
                        Timestamp = $null
                    }
                }
                $results.CheckStatus.AmsiProviderCheck = 'OK'
            }
            catch {
                Write-Warning "Could not check AMSI provider registration: $_"
                $results.CheckStatus.AmsiProviderCheck = 'Unknown'
            }

            # 1b. AMSI bypass patterns in script-block logs
            Write-Verbose "Scanning script blocks for AMSI bypass patterns..."
            try {
                $results.Summary.ChecksRun++
                if ($results.CheckStatus.ScriptBlockLogQuery -eq 'OK') {
                    foreach ($evt in $scriptBlockEvents) {
                        $msg = Get-EventMessage -WinEvent $evt
                        if ([string]::IsNullOrEmpty($msg)) { continue }

                        foreach ($indicator in $amsiPatterns) {
                            $matched = $false
                            if ($msg -match $indicator.Pattern) {
                                if ($null -eq $indicator.Combo -or $msg -match $indicator.Combo) {
                                    $matched = $true
                                }
                            }
                            if ($matched) {
                                $results.EvasionFindings += [PSCustomObject]@{
                                    Technique = 'T1562.001'
                                    Severity  = $indicator.Severity
                                    Evidence  = "AMSI bypass indicator '$($indicator.Name)' in PowerShell script block (4104): $(Get-MessageSnippet -Message $msg)"
                                    Timestamp = Get-EventTimestamp -WinEvent $evt
                                }
                                break
                            }
                        }
                    }
                    $results.CheckStatus.AmsiPatternScan = 'OK'
                }
                else {
                    $results.CheckStatus.AmsiPatternScan = 'Unknown'
                }
            }
            catch {
                Write-Warning "Error scanning script blocks for AMSI bypass patterns: $_"
                $results.CheckStatus.AmsiPatternScan = 'Unknown'
            }

            # 2a. ETW tamper patterns in script-block logs
            Write-Verbose "Scanning script blocks for ETW tamper patterns..."
            try {
                $results.Summary.ChecksRun++
                if ($results.CheckStatus.ScriptBlockLogQuery -eq 'OK') {
                    foreach ($evt in $scriptBlockEvents) {
                        $msg = Get-EventMessage -WinEvent $evt
                        if ([string]::IsNullOrEmpty($msg)) { continue }

                        foreach ($indicator in $etwPatterns) {
                            $matched = $false
                            if ($msg -match $indicator.Pattern) {
                                if ($null -eq $indicator.Combo -or $msg -match $indicator.Combo) {
                                    $matched = $true
                                }
                            }
                            if ($matched) {
                                $results.EvasionFindings += [PSCustomObject]@{
                                    Technique = 'T1562.006'
                                    Severity  = $indicator.Severity
                                    Evidence  = "ETW tamper indicator '$($indicator.Name)' in PowerShell script block (4104): $(Get-MessageSnippet -Message $msg)"
                                    Timestamp = Get-EventTimestamp -WinEvent $evt
                                }
                                break
                            }
                        }
                    }
                    $results.CheckStatus.EtwPatternScan = 'OK'
                }
                else {
                    $results.CheckStatus.EtwPatternScan = 'Unknown'
                }
            }
            catch {
                Write-Warning "Error scanning script blocks for ETW tamper patterns: $_"
                $results.CheckStatus.EtwPatternScan = 'Unknown'
            }

            # 2b. PowerShell script-block logging GPO state (current-state check; cannot establish when it changed)
            Write-Verbose "Checking PowerShell logging GPO state..."
            try {
                $results.Summary.ChecksRun++
                $gpoPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
                $gpoExists = $false
                try { $gpoExists = Test-Path $gpoPath -ErrorAction SilentlyContinue } catch { $gpoExists = $false }

                if ($gpoExists) {
                    $gpo = Get-ItemProperty -Path $gpoPath -ErrorAction SilentlyContinue
                    $sblEnabled = $null
                    if ($null -ne $gpo -and $gpo.PSObject.Properties['EnableScriptBlockLogging']) {
                        $sblEnabled = $gpo.EnableScriptBlockLogging
                    }
                    if ($null -ne $sblEnabled -and 0 -eq [int]$sblEnabled) {
                        $results.EvasionFindings += [PSCustomObject]@{
                            Technique = 'T1562.002'
                            Severity  = 'Medium'
                            Evidence  = 'PowerShell script-block logging explicitly disabled via GPO (EnableScriptBlockLogging=0) - current-state check, change time unknown'
                            Timestamp = $null
                        }
                    }
                    elseif ($null -ne $sblEnabled -and 1 -eq [int]$sblEnabled) {
                        $results.EvasionFindings += [PSCustomObject]@{
                            Technique = 'T1562.002'
                            Severity  = 'Info'
                            Evidence  = 'PowerShell script-block logging enabled via GPO (informational baseline)'
                            Timestamp = $null
                        }
                    }
                }
                else {
                    Write-Verbose "PowerShell logging GPO key not present (default state)"
                }
                $results.CheckStatus.PowerShellLoggingGpo = 'OK'
            }
            catch {
                Write-Warning "Could not check PowerShell logging GPO state: $_"
                $results.CheckStatus.PowerShellLoggingGpo = 'Unknown'
            }

            # 3a. Defender preference tampering
            Write-Verbose "Checking Windows Defender preferences..."
            try {
                $results.Summary.ChecksRun++
                $mpPref = Get-MpPreference -ErrorAction Stop
                $results.CheckStatus.DefenderPreferences = 'OK'

                $exclusionPaths = @()
                $exclusionProcesses = @()
                $exclusionExtensions = @()
                if ($null -ne $mpPref) {
                    if ($mpPref.PSObject.Properties['ExclusionPath'] -and $null -ne $mpPref.ExclusionPath) { $exclusionPaths = @($mpPref.ExclusionPath) }
                    if ($mpPref.PSObject.Properties['ExclusionProcess'] -and $null -ne $mpPref.ExclusionProcess) { $exclusionProcesses = @($mpPref.ExclusionProcess) }
                    if ($mpPref.PSObject.Properties['ExclusionExtension'] -and $null -ne $mpPref.ExclusionExtension) { $exclusionExtensions = @($mpPref.ExclusionExtension) }
                }

                $totalExclusions = $exclusionPaths.Count + $exclusionProcesses.Count + $exclusionExtensions.Count
                if ($totalExclusions -gt 0) {
                    $exclSeverity = 'Medium'
                    if ($totalExclusions -gt 3) { $exclSeverity = 'High' }

                    $evidenceParts = @()
                    if ($exclusionPaths.Count -gt 0) { $evidenceParts += "Paths: $($exclusionPaths -join ', ')" }
                    if ($exclusionProcesses.Count -gt 0) { $evidenceParts += "Processes: $($exclusionProcesses -join ', ')" }
                    if ($exclusionExtensions.Count -gt 0) { $evidenceParts += "Extensions: $($exclusionExtensions -join ', ')" }

                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = $exclSeverity
                        Evidence  = "Windows Defender exclusions configured ($totalExclusions total) - $($evidenceParts -join '; ')"
                        Timestamp = $null
                    }
                }

                if ($null -ne $mpPref -and $mpPref.PSObject.Properties['DisableRealtimeMonitoring'] -and $mpPref.DisableRealtimeMonitoring -eq $true) {
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = 'High'
                        Evidence  = 'DisableRealtimeMonitoring is set to true in Defender preferences'
                        Timestamp = $null
                    }
                }
                if ($null -ne $mpPref -and $mpPref.PSObject.Properties['DisableBehaviorMonitoring'] -and $mpPref.DisableBehaviorMonitoring -eq $true) {
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = 'High'
                        Evidence  = 'DisableBehaviorMonitoring is set to true in Defender preferences'
                        Timestamp = $null
                    }
                }
            }
            catch {
                Write-Warning "Could not query Windows Defender preferences: $_"
                $results.CheckStatus.DefenderPreferences = 'Unknown'
            }

            # 3b. Defender real-time protection status
            Write-Verbose "Checking Windows Defender real-time protection status..."
            try {
                $results.Summary.ChecksRun++
                $mpStatus = Get-MpComputerStatus -ErrorAction Stop
                if ($null -ne $mpStatus -and $mpStatus.PSObject.Properties['RealTimeProtectionEnabled'] -and $mpStatus.RealTimeProtectionEnabled -eq $false) {
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.001'
                        Severity  = 'High'
                        Evidence  = 'Windows Defender real-time protection is currently disabled'
                        Timestamp = $null
                    }
                }
                $results.CheckStatus.DefenderStatus = 'OK'
            }
            catch {
                Write-Warning "Could not query Windows Defender computer status: $_"
                $results.CheckStatus.DefenderStatus = 'Unknown'
            }

            # 3c. Defender Operational events 5001 (real-time protection disabled) / 5007 (configuration changed)
            Write-Verbose "Checking Defender Operational log for tampering events..."
            try {
                $results.Summary.ChecksRun++
                $defenderEvents = @(Get-WinEvent -FilterHashtable @{
                    LogName   = 'Microsoft-Windows-Windows Defender/Operational'
                    ID        = 5001, 5007
                    StartTime = $startTime
                } -ErrorAction SilentlyContinue)

                foreach ($evt in $defenderEvents) {
                    $msg = Get-EventMessage -WinEvent $evt
                    $eventId = $null
                    if ($null -ne $evt -and $evt.PSObject.Properties['Id']) { $eventId = $evt.Id }

                    if (5001 -eq $eventId) {
                        $results.EvasionFindings += [PSCustomObject]@{
                            Technique = 'T1562.001'
                            Severity  = 'High'
                            Evidence  = "Defender event 5001 - real-time protection disabled: $(Get-MessageSnippet -Message $msg)"
                            Timestamp = Get-EventTimestamp -WinEvent $evt
                        }
                    }
                    elseif (5007 -eq $eventId) {
                        $sev = 'Medium'
                        $desc = 'Defender configuration changed'
                        $exclusionPath = $null
                        if ($null -ne $evt.PSObject.Properties['Properties'] -and $null -ne $evt.Properties) {
                            foreach ($prop in $evt.Properties) {
                                if ($null -ne $prop -and $null -ne $prop.Value -and
                                    $prop.Value -is [string] -and $prop.Value -match '(?i)\\Exclusions\\') {
                                    $exclusionPath = $prop.Value
                                    break
                                }
                            }
                        }
                        if ($null -ne $exclusionPath) {
                            $sev = 'High'
                            $desc = 'Defender exclusion changed'
                        }
                        $results.EvasionFindings += [PSCustomObject]@{
                            Technique = 'T1562.001'
                            Severity  = $sev
                            Evidence  = "Defender event 5007 - ${desc}: $(Get-MessageSnippet -Message $msg)"
                            Timestamp = Get-EventTimestamp -WinEvent $evt
                        }
                    }
                }
                $results.CheckStatus.DefenderEvents = 'OK'
            }
            catch {
                Write-Warning "Could not query Defender Operational log: $_"
                $results.CheckStatus.DefenderEvents = 'Unknown'
            }

            # 4a. Security log cleared (1102)
            Write-Verbose "Checking for Security log cleared events (1102)..."
            try {
                $results.Summary.ChecksRun++
                $clearedSecurity = @(Get-WinEvent -FilterHashtable @{
                    LogName   = 'Security'
                    ID        = 1102
                    StartTime = $startTime
                } -ErrorAction SilentlyContinue)

                foreach ($evt in $clearedSecurity) {
                    $subjectUser = Get-EventDataValue -WinEvent $evt -Name 'SubjectUserName'
                    $subjectDomain = Get-EventDataValue -WinEvent $evt -Name 'SubjectDomainName'
                    $who = 'Unknown'
                    if (-not [string]::IsNullOrEmpty($subjectUser)) {
                        if (-not [string]::IsNullOrEmpty($subjectDomain)) { $who = "$subjectDomain\$subjectUser" }
                        else { $who = $subjectUser }
                    }
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1070.001'
                        Severity  = 'High'
                        Evidence  = "Security event log was cleared (event 1102) by '$who'"
                        Timestamp = Get-EventTimestamp -WinEvent $evt
                    }
                }
                $results.CheckStatus.SecurityLogCleared = 'OK'
            }
            catch {
                Write-Warning "Could not query Security log for 1102 events: $_"
                $results.CheckStatus.SecurityLogCleared = 'Unknown'
            }

            # 4b. System log cleared (104)
            Write-Verbose "Checking for System log cleared events (104)..."
            try {
                $results.Summary.ChecksRun++
                $clearedSystem = @(Get-WinEvent -FilterHashtable @{
                    LogName   = 'System'
                    ID        = 104
                    StartTime = $startTime
                } -ErrorAction SilentlyContinue)

                foreach ($evt in $clearedSystem) {
                    $subjectUser = Get-EventDataValue -WinEvent $evt -Name 'SubjectUserName'
                    $subjectDomain = Get-EventDataValue -WinEvent $evt -Name 'SubjectDomainName'
                    $who = 'Unknown'
                    if (-not [string]::IsNullOrEmpty($subjectUser)) {
                        if (-not [string]::IsNullOrEmpty($subjectDomain)) { $who = "$subjectDomain\$subjectUser" }
                        else { $who = $subjectUser }
                    }
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1070.001'
                        Severity  = 'High'
                        Evidence  = "System event log was cleared (event 104) by '$who'"
                        Timestamp = Get-EventTimestamp -WinEvent $evt
                    }
                }
                $results.CheckStatus.SystemLogCleared = 'OK'
            }
            catch {
                Write-Warning "Could not query System log for 104 events: $_"
                $results.CheckStatus.SystemLogCleared = 'Unknown'
            }

            # 4c. Audit policy changed (4719)
            Write-Verbose "Checking for audit policy change events (4719)..."
            try {
                $results.Summary.ChecksRun++
                $auditChanges = @(Get-WinEvent -FilterHashtable @{
                    LogName   = 'Security'
                    ID        = 4719
                    StartTime = $startTime
                } -ErrorAction SilentlyContinue)

                foreach ($evt in $auditChanges) {
                    $subjectUser = Get-EventDataValue -WinEvent $evt -Name 'SubjectUserName'
                    $subjectDomain = Get-EventDataValue -WinEvent $evt -Name 'SubjectDomainName'
                    $category = Get-EventDataValue -WinEvent $evt -Name 'CategoryId'
                    $who = 'Unknown'
                    if (-not [string]::IsNullOrEmpty($subjectUser)) {
                        if (-not [string]::IsNullOrEmpty($subjectDomain)) { $who = "$subjectDomain\$subjectUser" }
                        else { $who = $subjectUser }
                    }
                    $catText = 'Unknown category'
                    if (-not [string]::IsNullOrEmpty($category)) { $catText = "category $category" }
                    $results.EvasionFindings += [PSCustomObject]@{
                        Technique = 'T1562.002'
                        Severity  = 'High'
                        Evidence  = "System audit policy was changed (event 4719, $catText) by '$who'"
                        Timestamp = Get-EventTimestamp -WinEvent $evt
                    }
                }
                $results.CheckStatus.AuditPolicyChanged = 'OK'
            }
            catch {
                Write-Warning "Could not query Security log for 4719 events: $_"
                $results.CheckStatus.AuditPolicyChanged = 'Unknown'
            }

            # 5. Log-gap analysis (heuristic)
            if ($IncludeLogGapAnalysis) {
                Write-Verbose "Performing heuristic log-gap analysis..."
                $gapLogs = @('Security', 'System', 'Microsoft-Windows-PowerShell/Operational')
                $gapStatus = 'OK'

                foreach ($logName in $gapLogs) {
                    try {
                        $results.Summary.ChecksRun++
                        $logEvents = @(Get-WinEvent -FilterHashtable @{
                            LogName   = $logName
                            StartTime = $startTime
                        } -ErrorAction SilentlyContinue)

                        $timedEvents = @($logEvents | Where-Object {
                            $null -ne $_ -and $_.PSObject.Properties['TimeCreated'] -and $null -ne $_.TimeCreated
                        })
                        if ($timedEvents.Count -lt 5) {
                            Write-Verbose "Not enough events in '$logName' for gap analysis ($($timedEvents.Count) events)"
                            continue
                        }

                        $sorted = @($timedEvents | Sort-Object -Property TimeCreated)

                        $maxGapHours = 0.0
                        $gapStart = $null
                        $gapEnd = $null
                        $gapIndex = -1
                        for ($i = 1; $i -lt $sorted.Count; $i++) {
                            $gapHours = ($sorted[$i].TimeCreated - $sorted[$i - 1].TimeCreated).TotalHours
                            if ($gapHours -gt $maxGapHours) {
                                $maxGapHours = $gapHours
                                $gapStart = $sorted[$i - 1].TimeCreated
                                $gapEnd = $sorted[$i].TimeCreated
                                $gapIndex = $i
                            }
                        }

                        $activeBefore = ($gapIndex -ge 2)
                        $activeAfter = (($sorted.Count - $gapIndex) -ge 2)

                        if ($maxGapHours -gt 2 -and $activeBefore -and $activeAfter) {
                            $results.EvasionFindings += [PSCustomObject]@{
                                Technique = 'T1070'
                                Severity  = 'Low'
                                Evidence  = "Suspicious gap (heuristic): largest gap of $([math]::Round($maxGapHours, 2)) hours with no events in '$logName' between $gapStart and $gapEnd during an otherwise-active period - may indicate log tampering or service downtime"
                                Timestamp = $gapEnd
                            }
                        }
                    }
                    catch {
                        Write-Warning "Could not perform gap analysis on '$logName': $_"
                        $gapStatus = 'Unknown'
                    }
                }
                $results.CheckStatus.LogGapAnalysis = $gapStatus
            }
        }
        catch {
            Write-Error "Error during defense-evasion scan: $_"
            throw
        }
    }

    end {
        # Summarize findings by severity
        $allFindings = @($results.EvasionFindings)
        $results.Summary.TotalFindings = $allFindings.Count
        $results.Summary.Critical = @($allFindings | Where-Object { $_.Severity -eq 'Critical' }).Count
        $results.Summary.High     = @($allFindings | Where-Object { $_.Severity -eq 'High' }).Count
        $results.Summary.Medium   = @($allFindings | Where-Object { $_.Severity -eq 'Medium' }).Count
        $results.Summary.Low      = @($allFindings | Where-Object { $_.Severity -eq 'Low' }).Count
        $results.Summary.Info     = @($allFindings | Where-Object { $_.Severity -eq 'Info' }).Count

        # Build recommendations
        $recommendations = @(
            'Enable and centrally forward PowerShell script-block logging (4104), Defender Operational, and Security/System logs to a tamper-resistant SIEM'
            'Enable Windows Defender Tamper Protection to prevent unauthorized changes to exclusions and real-time monitoring'
            'Alert on event IDs 1102 (Security log cleared), 104 (System log cleared), and 4719 (audit policy changed) as high-priority detections'
        )

        if (@($allFindings | Where-Object { $_.Technique -eq 'T1562.001' -and $_.Severity -in @('High', 'Medium') }).Count -gt 0) {
            $recommendations += 'Investigate AMSI/Defender tampering findings: review the flagged script blocks and validate every Defender exclusion against change records'
        }
        if (@($allFindings | Where-Object { $_.Technique -eq 'T1562.006' }).Count -gt 0) {
            $recommendations += 'Investigate ETW tampering indicators and consider deploying ETW-based tamper detection (e.g., monitoring for EtwEventWrite patching)'
        }
        if (@($allFindings | Where-Object { $_.Technique -eq 'T1070.001' }).Count -gt 0) {
            $recommendations += 'Treat cleared event logs as a potential intrusion in progress: identify the clearing account, review activity around the clear time, and verify log-forwarding coverage'
        }
        if (@($allFindings | Where-Object { $_.Technique -eq 'T1562.002' }).Count -gt 0) {
            $recommendations += 'Review audit policy and PowerShell logging GPO changes; re-enable logging and investigate who disabled it'
        }
        if (@($allFindings | Where-Object { $_.Technique -eq 'T1070' }).Count -gt 0) {
            $recommendations += 'Log-gap findings are heuristic only: corroborate gaps with service uptime and SIEM ingestion health before treating them as tampering'
        }
        $results.Recommendations = $recommendations

        Write-Host "`nDefense-evasion scan complete!" -ForegroundColor Green
        Write-Host "Total findings: $($results.Summary.TotalFindings)" -ForegroundColor $(if ($results.Summary.TotalFindings -gt 0) { 'Yellow' } else { 'Green' })
        Write-Host " High: $($results.Summary.High)" -ForegroundColor $(if ($results.Summary.High -gt 0) { 'Red' } else { 'Green' })
        Write-Host " Medium: $($results.Summary.Medium)" -ForegroundColor $(if ($results.Summary.Medium -gt 0) { 'Yellow' } else { 'Green' })
        Write-Host " Low: $($results.Summary.Low)" -ForegroundColor Cyan
        Write-Host " Info: $($results.Summary.Info)" -ForegroundColor Cyan
        Write-Host "Checks run: $($results.Summary.ChecksRun)" -ForegroundColor Cyan

        return $results
    }
}