Detection/Get-SysmonAnalytics.ps1

function Get-SysmonAnalytics {
    <#
    .SYNOPSIS
        Sysmon-native threat analytics across key Sysmon event types
    .DESCRIPTION
        Analyzes the Microsoft-Windows-Sysmon/Operational log for suspicious activity:
        process-creation chains and encoded/obfuscated command lines (Event ID 1),
        network connections from unexpected processes and beaconing patterns (ID 3),
        suspicious image loads into critical system processes (ID 7),
        lsass.exe process-access attempts with read-capable masks (ID 10),
        executable drops into startup/temp locations (ID 11),
        persistence-oriented registry writes (IDs 12/13), and
        anomalous DNS queries (ID 22).
        Every finding carries a MITRE ATT&CK technique ID. Behavioral checks are
        heuristics and are labeled as such - validate findings manually before
        treating them as confirmed malicious. If Sysmon is not installed, the
        function returns gracefully with Status 'NotAvailable'.
    .PARAMETER Hours
        Number of hours to look back in the Sysmon log (default: 24)
    .PARAMETER EventTypes
        Sysmon event categories to analyze. Valid values: ProcessCreation,
        NetworkConnection, ImageLoad, ProcessAccess, FileCreate, RegistryEvent,
        DnsQuery. Defaults to all categories
    .PARAMETER MaxEvents
        Maximum number of Sysmon events to load across all categories (default: 5000).
        Guards against excessive memory usage on busy systems; when the cap is hit,
        remaining categories are skipped and the cap is reported in the summary
    .EXAMPLE
        Get-SysmonAnalytics
        Get-SysmonAnalytics -Hours 48 -EventTypes ProcessCreation, NetworkConnection
    .OUTPUTS
        PSCustomObject with Findings, Summary, and Recommendations
    #>


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

        [Parameter()]
        [ValidateSet('ProcessCreation', 'NetworkConnection', 'ImageLoad', 'ProcessAccess', 'FileCreate', 'RegistryEvent', 'DnsQuery')]
        [string[]]$EventTypes = @('ProcessCreation', 'NetworkConnection', 'ImageLoad', 'ProcessAccess', 'FileCreate', 'RegistryEvent', 'DnsQuery'),

        [Parameter()]
        [int]$MaxEvents = 5000
    )

    begin {
        Write-Host "Analyzing Sysmon telemetry (last $Hours hours)..." -ForegroundColor Cyan

        $sysmonLog = 'Microsoft-Windows-Sysmon/Operational'
        $startTime = (Get-Date).AddHours(-$Hours)

        $results = [PSCustomObject]@{
            ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            TimeRange = "$Hours hours"
            Status = 'OK'
            Findings = @()
            Summary = [PSCustomObject]@{
                TotalFindings = 0
                Critical = 0
                High = 0
                Medium = 0
                Low = 0
                Info = 0
                SysmonPresent = $false
                EventsScanned = 0
                MaxEventsReached = $false
            }
            Recommendations = @()
        }

        $eventIdMap = @{
            ProcessCreation   = @(1)
            NetworkConnection = @(3)
            ImageLoad         = @(7)
            ProcessAccess     = @(10)
            FileCreate        = @(11)
            RegistryEvent     = @(12, 13)
            DnsQuery          = @(22)
        }

        # Null-safe EventData field extraction (Sysmon schema varies by version;
        # missing Data nodes return $null instead of throwing)
        $getEventValue = {
            param($EventData, [string]$Name)
            if ($null -eq $EventData) { return $null }
            $node = $EventData | Where-Object { $_.Name -eq $Name }
            if ($null -ne $node) { return $node.InnerText }
            return $null
        }

        $getFileName = {
            param([string]$Path)
            if ([string]::IsNullOrEmpty($Path)) { return '' }
            if ($Path -match '([^\\/]+)$') { return $Matches[1] }
            return $Path
        }

        $isPrivateIP = {
            param([string]$IP)
            if ([string]::IsNullOrEmpty($IP) -or $IP -eq '-') { return $true }
            if ($IP -match '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.|169\.254\.|::1$|fe80:|fc00:|fd)') { return $true }
            return $false
        }

        # Shannon entropy helper for DGA-style label detection (heuristic)
        $getEntropy = {
            param([string]$Text)
            if ([string]::IsNullOrEmpty($Text)) { return 0.0 }
            $freq = @{}
            foreach ($char in $Text.ToCharArray()) {
                $key = [string]$char
                if ($freq.ContainsKey($key)) { $freq[$key]++ } else { $freq[$key] = 1 }
            }
            $length = $Text.Length
            $entropy = 0.0
            foreach ($k in $freq.Keys) {
                $p = $freq[$k] / $length
                $entropy -= $p * [math]::Log($p, 2)
            }
            return [math]::Round($entropy, 2)
        }

        # Granted-access mask classification aligned with Get-CredentialAccessEvents:
        # PROCESS_VM_READ (0x0010) or broad rights are suspicious for lsass handles
        $normalAccessMasks = @('0x1000', '0x1400')
        $suspiciousAccessMasks = @('0x1010', '0x101a', '0x103a', '0x143a', '0x1f0fff', '0x1fffff')

        $officeParents = @('winword.exe', 'excel.exe', 'powerpnt.exe', 'outlook.exe', 'mspub.exe', 'visio.exe', 'msaccess.exe')
        $scriptChildren = @('cmd.exe', 'powershell.exe', 'pwsh.exe', 'wscript.exe', 'cscript.exe', 'mshta.exe', 'rundll32.exe', 'regsvr32.exe')
        $networkAbuseProcs = @('powershell.exe', 'pwsh.exe', 'rundll32.exe', 'regsvr32.exe', 'mshta.exe', 'wscript.exe', 'cscript.exe', 'certutil.exe', 'bitsadmin.exe', 'cmd.exe', 'hh.exe')
        $criticalSystemProcs = @('lsass.exe', 'winlogon.exe', 'csrss.exe', 'services.exe', 'smss.exe', 'wininit.exe')
        $userWritablePathPattern = '(\\Temp\\|\\AppData\\|\\Users\\Public\\|\\Downloads\\|\\Windows\\Tasks\\)'
        $commonPorts = @(80, 443, 53, 22, 21, 25, 110, 143, 993, 995, 3389, 445, 139, 135, 587, 465, 853)

        # Processes that legitimately issue large volumes of DNS queries (browsers,
        # system services, common collaboration tools) - used by the Event 22
        # process-mismatch heuristic
        $dnsExpectedProcs = @(
            'svchost.exe', 'chrome.exe', 'firefox.exe', 'msedge.exe', 'iexplore.exe', 'brave.exe', 'opera.exe',
            'outlook.exe', 'teams.exe', 'slack.exe', 'zoom.exe', 'onedrive.exe', 'dropbox.exe', 'code.exe',
            'wmiprvse.exe', 'dns.exe', 'lsass.exe', 'smartscreen.exe', 'searchui.exe', 'backgroundtaskhost.exe',
            'dllhost.exe', 'msmpeng.exe', 'system', 'spoolsv.exe', 'officeclicktorun.exe'
        )

        # Known-abused TLDs (heuristic signal only - many legitimate sites use these)
        $suspiciousTldPattern = '\.(top|xyz|club|online|site|buzz|tk|ml|ga|cf|gq|loan|work|click|link|pw|quest|icu|cam|rest|monster)\.?$'

        # Encoded / obfuscated command-line indicators (matching the pattern style
        # used across the module's script-block analytics)
        $encodedCommandPatterns = @(
            [PSCustomObject]@{ Name = 'Base64-encoded command argument';  Pattern = '(?i)-e(nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{40,}'; Severity = 'High';   Technique = 'T1027' }
            [PSCustomObject]@{ Name = 'Runtime decoding primitive';        Pattern = '(?i)frombase64string|fromcharcode|\[char\]\s*\d+'; Severity = 'High';   Technique = 'T1027' }
            [PSCustomObject]@{ Name = 'Download cradle';                   Pattern = '(?i)downloadstring|downloadfile|invoke-webrequest|\biwr\b|start-bitstransfer|certutil\s+.*-urlcache|bitsadmin\s+.*/transfer'; Severity = 'High'; Technique = 'T1105' }
            [PSCustomObject]@{ Name = 'Dynamic evaluation (IEX)';          Pattern = '(?i)\biex\b|invoke-expression'; Severity = 'Medium'; Technique = 'T1059.001' }
            [PSCustomObject]@{ Name = 'Hidden window execution';           Pattern = '(?i)-w(indowstyle)?\s+hidden'; Severity = 'Medium'; Technique = 'T1564.003' }
            [PSCustomObject]@{ Name = 'Execution-policy bypass';           Pattern = '(?i)-ep\s+bypass|-executionpolicy\s+bypass'; Severity = 'Medium'; Technique = 'T1059.001' }
        )

        $addFinding = {
            param(
                [Parameter(Mandatory)]$Results,
                [Parameter(Mandatory)][string]$EventType,
                [Parameter(Mandatory)][string]$Technique,
                [Parameter(Mandatory)][string]$Severity,
                [Parameter(Mandatory)]$Evidence,
                $Timestamp
            )
            $Results.Findings += [PSCustomObject]@{
                EventType = $EventType
                Technique = $Technique
                Severity = $Severity
                Evidence = $Evidence
                Timestamp = $Timestamp
            }
            switch ($Severity) {
                'Critical' { $Results.Summary.Critical++ }
                'High'     { $Results.Summary.High++ }
                'Medium'   { $Results.Summary.Medium++ }
                'Low'      { $Results.Summary.Low++ }
                default    { $Results.Summary.Info++ }
            }
        }
    }

    process {
        # Sysmon presence gate - return gracefully when the log does not exist
        try {
            $null = Get-WinEvent -ListLog $sysmonLog -ErrorAction Stop
            $results.Summary.SysmonPresent = $true
        }
        catch {
            $results.Summary.SysmonPresent = $false
            $results.Status = 'NotAvailable'
            $results.Recommendations += 'Sysmon not installed; enable for deep visibility - deploy Sysmon with a community-hardened configuration (e.g. SwiftOnSecurity/sysmon-config or Olaf Hartong''s sysmon-modular) to gain process, network, image-load, registry, and DNS telemetry'
        }

        if (-not $results.Summary.SysmonPresent) {
            return
        }

        # Load events for the selected categories, honoring the MaxEvents budget
        $eventsByType = @{}
        foreach ($eventType in $EventTypes) {
            $remaining = $MaxEvents - $results.Summary.EventsScanned
            if ($remaining -le 0) {
                $results.Summary.MaxEventsReached = $true
                Write-Warning "MaxEvents cap ($MaxEvents) reached - skipping remaining event categories: $($EventTypes | Where-Object { -not $eventsByType.ContainsKey($_) })"
                break
            }
            try {
                $categoryEvents = @(Get-WinEvent -FilterHashtable @{
                    LogName = $sysmonLog
                    ID = $eventIdMap[$eventType]
                    StartTime = $startTime
                } -MaxEvents $remaining -ErrorAction SilentlyContinue)
                $eventsByType[$eventType] = $categoryEvents
                $results.Summary.EventsScanned += @($categoryEvents).Count
            }
            catch {
                Write-Warning "Could not query Sysmon events for $eventType`: $_"
                $eventsByType[$eventType] = @()
                $results.Recommendations += "Sysmon $eventType events were not queryable - verify the Sysmon service is running and the configuration enables this event type"
            }
        }
        if ($results.Summary.EventsScanned -ge $MaxEvents) {
            $results.Summary.MaxEventsReached = $true
        }

        # 1. Process creation (Event ID 1): parent-child chains, encoded command lines,
        # unsigned binaries in user-writable paths spawning children
        if ($eventsByType.ContainsKey('ProcessCreation')) {
            try {
                Write-Verbose "Analyzing process creation events (ID 1)..."
                $processEvents = @($eventsByType['ProcessCreation'])

                $parentImages = @{}
                foreach ($event in $processEvents) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data
                    $parentImage = & $getEventValue $eventData 'ParentImage'
                    if ($parentImage) { $parentImages[$parentImage.ToLower()] = $true }
                }

                foreach ($event in $processEvents) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $commandLine = & $getEventValue $eventData 'CommandLine'
                    $parentImage = & $getEventValue $eventData 'ParentImage'
                    $parentCommandLine = & $getEventValue $eventData 'ParentCommandLine'
                    $user = & $getEventValue $eventData 'User'
                    $signed = & $getEventValue $eventData 'Signed'
                    $signatureStatus = & $getEventValue $eventData 'SignatureStatus'
                    $imageName = (& $getFileName $image).ToLower()
                    $parentName = (& $getFileName $parentImage).ToLower()
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    # Office application spawning a scripting engine (T1204.002)
                    if ($officeParents -contains $parentName -and $scriptChildren -contains $imageName) {
                        & $addFinding $results 'ProcessCreation' 'T1204.002' 'High' ([PSCustomObject]@{
                            Indicator = 'Office application spawned a scripting engine'
                            ParentImage = $parentImage
                            Image = $image
                            CommandLine = $commandLine
                            User = $user
                            Note = 'Classic malicious-document execution chain - inspect the parent document'
                        }) $timestamp
                    }

                    # services.exe spawning unusual children (T1569.002)
                    if ($parentName -eq 'services.exe' -and $imageName -notin @('svchost.exe', 'msiexec.exe', 'tiworker.exe', 'trustedinstaller.exe', 'wmiprvse.exe', 'sppsvc.exe', 'sihost.exe')) {
                        & $addFinding $results 'ProcessCreation' 'T1569.002' 'Medium' ([PSCustomObject]@{
                            Indicator = 'services.exe spawned an unusual child process'
                            ParentImage = $parentImage
                            ParentCommandLine = $parentCommandLine
                            Image = $image
                            CommandLine = $commandLine
                            User = $user
                            Note = 'May indicate malicious service execution - verify the child against installed services'
                        }) $timestamp
                    }

                    # Encoded / obfuscated command lines on script-capable hosts
                    if ($commandLine -and $scriptChildren -contains $imageName) {
                        foreach ($indicator in $encodedCommandPatterns) {
                            if ($commandLine -match $indicator.Pattern) {
                                & $addFinding $results 'ProcessCreation' $indicator.Technique $indicator.Severity ([PSCustomObject]@{
                                    Indicator = $indicator.Name
                                    Image = $image
                                    ParentImage = $parentImage
                                    CommandLine = $commandLine
                                    User = $user
                                    Note = 'Obfuscation indicators are heuristics - confirm intent by decoding the command line'
                                }) $timestamp
                            }
                        }
                    }

                    # Binaries in user-writable paths that spawn children; signature
                    # fields are schema-dependent, so absence downgrades to heuristic
                    if ($image -and $image -match $userWritablePathPattern -and $parentImages.ContainsKey($image.ToLower())) {
                        $signatureNote = 'Signature fields absent from event schema - unsigned status inferred heuristically from the path'
                        $severity = 'Medium'
                        $isUnsigned = $false
                        if ($signed -and $signed -match '^(?i:false)$') { $isUnsigned = $true }
                        if ($signatureStatus -and $signatureStatus -match '^(?i:unavailable|invalid|notsigned)$') { $isUnsigned = $true }
                        if ($isUnsigned) {
                            $signatureNote = "Unsigned binary (Signed=$signed, SignatureStatus=$signatureStatus)"
                            $severity = 'High'
                        }
                        & $addFinding $results 'ProcessCreation' 'T1036' $severity ([PSCustomObject]@{
                            Indicator = 'Binary in user-writable path spawning child processes'
                            Image = $image
                            CommandLine = $commandLine
                            User = $user
                            SignatureNote = $signatureNote
                            Note = 'HEURISTIC when signature fields are absent - legitimate updaters can share this shape'
                        }) $timestamp
                    }
                }
            }
            catch {
                Write-Warning "Process creation (ID 1) analysis failed: $_"
            }
        }

        # 2. Network connections (Event ID 3): script-capable processes reaching
        # external IPs, and rare-port beaconing (same dest ip+port > 10x in window)
        if ($eventsByType.ContainsKey('NetworkConnection')) {
            try {
                Write-Verbose "Analyzing network connection events (ID 3)..."
                $destinationCounts = @{}

                foreach ($event in @($eventsByType['NetworkConnection'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $destIP = & $getEventValue $eventData 'DestinationIp'
                    $destPort = & $getEventValue $eventData 'DestinationPort'
                    $destHost = & $getEventValue $eventData 'DestinationHostname'
                    $user = & $getEventValue $eventData 'User'
                    $imageName = (& $getFileName $image).ToLower()
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    if (-not $destIP -or $destIP -eq '-') { continue }

                    $portNumber = 0
                    if ($destPort) { try { $portNumber = [int]$destPort } catch { $portNumber = 0 } }
                    $destKey = "${destIP}:$portNumber"
                    if (-not $destinationCounts.ContainsKey($destKey)) {
                        $destinationCounts[$destKey] = [PSCustomObject]@{ Count = 0; Images = @(); LastSeen = $timestamp; Port = $portNumber; DestinationHostname = $destHost }
                    }
                    $destinationCounts[$destKey].Count++
                    if ($image -and $destinationCounts[$destKey].Images -notcontains $image) {
                        $destinationCounts[$destKey].Images += $image
                    }
                    if ($null -ne $timestamp -and ($null -eq $destinationCounts[$destKey].LastSeen -or $timestamp -gt $destinationCounts[$destKey].LastSeen)) {
                        $destinationCounts[$destKey].LastSeen = $timestamp
                    }

                    # Script-capable process connecting to a non-RFC1918 address
                    if ($networkAbuseProcs -contains $imageName -and -not (& $isPrivateIP $destIP)) {
                        $severity = 'Medium'
                        if ($imageName -in @('powershell.exe', 'pwsh.exe', 'rundll32.exe', 'regsvr32.exe')) { $severity = 'High' }
                        & $addFinding $results 'NetworkConnection' 'T1071' $severity ([PSCustomObject]@{
                            Indicator = 'Script-capable or proxy-execution binary made an outbound connection to an external address'
                            Image = $image
                            DestinationIp = $destIP
                            DestinationPort = $portNumber
                            DestinationHostname = $destHost
                            User = $user
                            Note = 'HEURISTIC - legitimate admin scripts also match; correlate with the process command line (Event ID 1)'
                        }) $timestamp
                    }
                }

                # Beaconing: same destination ip+port more than 10 times in the window
                foreach ($destKey in $destinationCounts.Keys) {
                    $destInfo = $destinationCounts[$destKey]
                    if ($destInfo.Count -gt 10) {
                        $technique = 'T1071.001'
                        if ($destInfo.Port -gt 0 -and $commonPorts -notcontains $destInfo.Port) { $technique = 'T1571' }
                        & $addFinding $results 'NetworkConnection' $technique 'Medium' ([PSCustomObject]@{
                            Indicator = "Beaconing pattern: $($destInfo.Count) connections to $destKey within the analysis window"
                            Destination = $destKey
                            DestinationHostname = $destInfo.DestinationHostname
                            ConnectionCount = $destInfo.Count
                            Images = ($destInfo.Images -join ', ')
                            Note = 'HEURISTIC threshold (>10 connections) - software update agents can produce similar cadence'
                        }) $destInfo.LastSeen
                    }
                }
            }
            catch {
                Write-Warning "Network connection (ID 3) analysis failed: $_"
            }
        }

        # 3. Image loads (Event ID 7): unsigned DLLs into lsass/winlogon (T1547.008 style),
        # DLLs from temp/appdata into system processes (T1574)
        if ($eventsByType.ContainsKey('ImageLoad')) {
            try {
                Write-Verbose "Analyzing image load events (ID 7)..."

                foreach ($event in @($eventsByType['ImageLoad'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $imageLoaded = & $getEventValue $eventData 'ImageLoaded'
                    $signed = & $getEventValue $eventData 'Signed'
                    $signatureStatus = & $getEventValue $eventData 'SignatureStatus'
                    $imageName = (& $getFileName $image).ToLower()
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    if (-not $imageLoaded) { continue }

                    $isUnsigned = $false
                    if ($signed -and $signed -match '^(?i:false)$') { $isUnsigned = $true }
                    if ($signatureStatus -and $signatureStatus -match '^(?i:unavailable|invalid|notsigned)$') { $isUnsigned = $true }

                    # Unsigned module loaded into lsass.exe / winlogon.exe
                    if ($imageName -in @('lsass.exe', 'winlogon.exe') -and ($isUnsigned -or $imageLoaded -notmatch '\\Windows\\System32\\')) {
                        & $addFinding $results 'ImageLoad' 'T1547.008' 'High' ([PSCustomObject]@{
                            Indicator = 'Unsigned or non-System32 module loaded into a critical logon process'
                            Image = $image
                            ImageLoaded = $imageLoaded
                            Signed = $signed
                            SignatureStatus = $signatureStatus
                            Note = 'LSA authentication-package / SSP style load - verify the DLL against a known-good baseline'
                        }) $timestamp
                        continue
                    }

                    # DLL loaded from temp/appdata into a system process
                    if ($criticalSystemProcs -contains $imageName -and $imageLoaded -match $userWritablePathPattern) {
                        & $addFinding $results 'ImageLoad' 'T1574.002' 'High' ([PSCustomObject]@{
                            Indicator = 'DLL loaded from a user-writable directory into a system process'
                            Image = $image
                            ImageLoaded = $imageLoaded
                            Signed = $signed
                            SignatureStatus = $signatureStatus
                            Note = 'Consistent with DLL side-loading / search-order hijacking into a privileged process'
                        }) $timestamp
                    }
                }
            }
            catch {
                Write-Warning "Image load (ID 7) analysis failed: $_"
            }
        }

        # 4. Process access (Event ID 10): lsass.exe targets with VM_READ-capable masks
        # (mask classification aligned with Get-CredentialAccessEvents)
        if ($eventsByType.ContainsKey('ProcessAccess')) {
            try {
                Write-Verbose "Analyzing process access events (ID 10)..."

                foreach ($event in @($eventsByType['ProcessAccess'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $targetImage = & $getEventValue $eventData 'TargetImage'
                    if (-not $targetImage -or $targetImage -notmatch 'lsass\.exe$') { continue }

                    $sourceImage = & $getEventValue $eventData 'SourceImage'
                    $sourceUser = & $getEventValue $eventData 'SourceUser'
                    $grantedAccess = & $getEventValue $eventData 'GrantedAccess'
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    $classification = 'Review'
                    $severity = 'Medium'
                    $maskValue = $null
                    if ($grantedAccess) {
                        try { $maskValue = [Convert]::ToInt64($grantedAccess, 16) } catch { $maskValue = $null }
                    }
                    if ($null -ne $maskValue) {
                        if (($maskValue -band 0x10) -ne 0 -or $suspiciousAccessMasks -contains $grantedAccess.ToLower()) {
                            $classification = 'Suspicious'
                            $severity = 'High'
                        }
                        elseif ($normalAccessMasks -contains $grantedAccess.ToLower()) {
                            $classification = 'Normal'
                            $severity = 'Info'
                        }
                    }

                    if ($classification -eq 'Normal') { continue }

                    & $addFinding $results 'ProcessAccess' 'T1003.001' $severity ([PSCustomObject]@{
                        Indicator = "Process opened lsass.exe with granted access $grantedAccess ($classification)"
                        SourceImage = $sourceImage
                        SourceUser = $sourceUser
                        TargetImage = $targetImage
                        GrantedAccess = $grantedAccess
                        Note = 'PROCESS_VM_READ (0x10) or broad rights against lsass may indicate credential dumping (T1003.001)'
                    }) $timestamp
                }
            }
            catch {
                Write-Warning "Process access (ID 10) analysis failed: $_"
            }
        }

        # 5. File create (Event ID 11): executables dropped into Startup/Prefetch/Temp
        # by non-installer parents (T1547.001 / T1105)
        if ($eventsByType.ContainsKey('FileCreate')) {
            try {
                Write-Verbose "Analyzing file create events (ID 11)..."
                $installerParents = @('msiexec.exe', 'setup.exe', 'tiworker.exe', 'trustedinstaller.exe', 'wuauclt.exe', 'musnotification.exe', 'officeclicktorun.exe', 'msedgeupdate.exe', 'googleupdate.exe', 'svchost.exe')
                $executableExtensionPattern = '\.(exe|dll|scr|bat|cmd|ps1|vbs|js|jse|hta|msi)$'

                foreach ($event in @($eventsByType['FileCreate'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $targetFilename = & $getEventValue $eventData 'TargetFilename'
                    $imageName = (& $getFileName $image).ToLower()
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    if (-not $targetFilename -or $targetFilename -notmatch $executableExtensionPattern) { continue }
                    if ($targetFilename -notmatch '(\\Start Menu\\Programs\\Startup\\|\\Prefetch\\|\\Temp\\|\\Windows\\Tasks\\)') { continue }
                    if ($installerParents -contains $imageName) { continue }

                    $technique = 'T1105'
                    if ($targetFilename -match '\\Start Menu\\Programs\\Startup\\') { $technique = 'T1547.001' }

                    & $addFinding $results 'FileCreate' $technique 'Medium' ([PSCustomObject]@{
                        Indicator = 'Executable dropped into a startup, prefetch, or temp location by a non-installer process'
                        Image = $image
                        TargetFilename = $targetFilename
                        Note = 'HEURISTIC - browsers and build tools also write executables to temp paths; verify the file hash'
                    }) $timestamp
                }
            }
            catch {
                Write-Warning "File create (ID 11) analysis failed: $_"
            }
        }

        # 6. Registry events (IDs 12/13): Run-key writes, IFEO debugger sets, LSA
        # package changes (patterns consistent with Find-PersistenceMechanisms)
        if ($eventsByType.ContainsKey('RegistryEvent')) {
            try {
                Write-Verbose "Analyzing registry events (IDs 12/13)..."

                foreach ($event in @($eventsByType['RegistryEvent'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $targetObject = & $getEventValue $eventData 'TargetObject'
                    $details = & $getEventValue $eventData 'Details'
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    if (-not $targetObject) { continue }

                    # Run / RunOnce key writes (same key families as Find-PersistenceMechanisms)
                    if ($targetObject -match '(?i)\\(Software|SOFTWARE)\\(Microsoft\\)?Windows\\CurrentVersion\\Run(Once|Services|ServicesOnce)?(\\|$)') {
                        & $addFinding $results 'RegistryEvent' 'T1547.001' 'High' ([PSCustomObject]@{
                            Indicator = 'Write to a Run/RunOnce autostart registry key'
                            Image = $image
                            TargetObject = $targetObject
                            Details = $details
                            Note = 'New or modified autostart entry - compare with the persistence baseline from Find-PersistenceMechanisms'
                        }) $timestamp
                        continue
                    }

                    # IFEO debugger hijack
                    if ($targetObject -match '(?i)\\Image File Execution Options\\[^\\]+\\Debugger$') {
                        & $addFinding $results 'RegistryEvent' 'T1546.012' 'High' ([PSCustomObject]@{
                            Indicator = 'Image File Execution Options debugger value set'
                            Image = $image
                            TargetObject = $targetObject
                            Details = $details
                            Note = 'IFEO debuggers redirect execution of the named binary - a classic persistence / privilege-escalation primitive'
                        }) $timestamp
                        continue
                    }

                    # LSA authentication / security package changes
                    if ($targetObject -match '(?i)\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\(Authentication Packages|Security Packages|Notification Packages)$' -or
                        ($targetObject -match '(?i)\\Control\\Lsa$' -and $details -match '(?i)Authentication Packages|Security Packages|Notification Packages')) {
                        & $addFinding $results 'RegistryEvent' 'T1547.005' 'High' ([PSCustomObject]@{
                            Indicator = 'LSA authentication/security package configuration modified'
                            Image = $image
                            TargetObject = $targetObject
                            Details = $details
                            Note = 'Custom SSP/AP DLLs load into lsass at boot and can capture credentials - validate the package list against msv1_0/scecli/rassfm defaults'
                        }) $timestamp
                    }
                }
            }
            catch {
                Write-Warning "Registry event (ID 12/13) analysis failed: $_"
            }
        }

        # 7. DNS queries (Event ID 22): DGA-ish high-entropy labels, known-abused TLDs,
        # queries issued by unexpected processes (all heuristics)
        if ($eventsByType.ContainsKey('DnsQuery')) {
            try {
                Write-Verbose "Analyzing DNS query events (ID 22)..."

                foreach ($event in @($eventsByType['DnsQuery'])) {
                    if ($null -eq $event) { continue }
                    $eventData = ([xml]$event.ToXml()).Event.EventData.Data

                    $image = & $getEventValue $eventData 'Image'
                    $queryName = & $getEventValue $eventData 'QueryName'
                    $queryResults = & $getEventValue $eventData 'QueryResults'
                    $imageName = (& $getFileName $image).ToLower()
                    $timestamp = $null
                    if ($null -ne $event -and $null -ne ($event.PSObject.Properties['TimeCreated']) -and $null -ne $event.TimeCreated) {
                        $timestamp = $event.TimeCreated
                    }

                    if (-not $queryName) { continue }

                    # DGA-style label: long label with high Shannon entropy (heuristic)
                    $labels = $queryName.TrimEnd('.') -split '\.'
                    foreach ($label in $labels) {
                        if ($label.Length -ge 20) {
                            $entropy = & $getEntropy $label
                            if ($entropy -gt 3.8 -or $label.Length -ge 30) {
                                & $addFinding $results 'DnsQuery' 'T1568.002' 'Medium' ([PSCustomObject]@{
                                    Indicator = "High-entropy DNS label (length $($label.Length), entropy $entropy) - DGA-style heuristic"
                                    Image = $image
                                    QueryName = $queryName
                                    QueryResults = $queryResults
                                    Note = 'HEURISTIC - CDN and telemetry domains can also produce long high-entropy labels'
                                }) $timestamp
                            }
                            break
                        }
                    }

                    # Known-abused TLD (heuristic)
                    if ($queryName -match $suspiciousTldPattern) {
                        & $addFinding $results 'DnsQuery' 'T1071.004' 'Low' ([PSCustomObject]@{
                            Indicator = 'Query for a domain on a frequently abused TLD'
                            Image = $image
                            QueryName = $queryName
                            QueryResults = $queryResults
                            Note = 'HEURISTIC - these TLDs also host legitimate services; combine with other signals before blocking'
                        }) $timestamp
                    }

                    # Query issued by a process that does not normally resolve DNS
                    if ($imageName -and $dnsExpectedProcs -notcontains $imageName -and $image -notmatch '(?i)\\Windows\\System32\\|\\Program Files') {
                        & $addFinding $results 'DnsQuery' 'T1071.004' 'Low' ([PSCustomObject]@{
                            Indicator = 'DNS query issued by an unexpected process (not a browser/system service and outside System32/Program Files)'
                            Image = $image
                            QueryName = $queryName
                            QueryResults = $queryResults
                            Note = 'HEURISTIC - portable applications legitimately resolve DNS; prioritize when combined with DGA or TLD signals'
                        }) $timestamp
                    }
                }
            }
            catch {
                Write-Warning "DNS query (ID 22) analysis failed: $_"
            }
        }

        $results.Summary.TotalFindings = @($results.Findings).Count

        # Recommendations based on findings
        if ($results.Summary.MaxEventsReached) {
            $results.Recommendations += "MaxEvents cap ($MaxEvents) was reached - re-run with a higher -MaxEvents or a smaller -Hours window for complete coverage"
        }
        if ($results.Summary.High -gt 0 -or $results.Summary.Critical -gt 0) {
            $results.Recommendations += 'High-severity Sysmon findings present - isolate affected hosts, preserve the Sysmon log, and validate each finding against the referenced MITRE technique before remediation'
        }
        if (@($results.Findings | Where-Object { $_.Technique -eq 'T1003.001' }).Count -gt 0) {
            $results.Recommendations += 'lsass.exe access with read-capable masks detected (T1003.001) - treat as potential credential theft; capture memory and reset exposed credentials'
        }
        if (@($results.Findings | Where-Object { $_.Technique -match '^T1547|^T1546' }).Count -gt 0) {
            $results.Recommendations += 'Persistence-oriented writes detected - cross-check with Find-PersistenceMechanisms to confirm the mechanism is active and remove unauthorized autostart entries'
        }
        if ($results.Summary.TotalFindings -eq 0) {
            $results.Recommendations += 'No suspicious Sysmon activity detected in the window - maintain Sysmon with a hardened configuration and forward the Operational log to a SIEM for long-term correlation'
        }
    }

    end {
        Write-Host "`nSysmon Analytics Complete!" -ForegroundColor Green
        Write-Host "`n=== Summary ===" -ForegroundColor Cyan
        Write-Host "Sysmon present: $($results.Summary.SysmonPresent)" -ForegroundColor Yellow
        Write-Host "Events scanned: $($results.Summary.EventsScanned)" -ForegroundColor Yellow
        Write-Host "Total findings: $($results.Summary.TotalFindings)" -ForegroundColor $(if ($results.Summary.TotalFindings -gt 0) { 'Red' } else { 'Green' })
        Write-Host "High/Critical: $($results.Summary.High + $results.Summary.Critical)" -ForegroundColor $(if (($results.Summary.High + $results.Summary.Critical) -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 Yellow

        if (@($results.Recommendations).Count -gt 0) {
            Write-Host "`n=== Recommendations ===" -ForegroundColor Cyan
            foreach ($recommendation in $results.Recommendations) {
                Write-Host "- $recommendation" -ForegroundColor Yellow
            }
        }

        return $results
    }
}