src/Public/Collect/Invoke-FotonSecurityCollection.ps1

# Invoke-FotonSecurityCollection
# Live facts only. Called from Invoke-FotonTriage when that exists.
# Endpoint collect. Rebuild dist/Foton-Collect.ps1 after edits.

function Invoke-FotonSecurityCollection {
    Write-FotonStatus 'Collecting security posture, AV state, logging and hardening configuration' 'Step'

    Invoke-FotonCollection -Category 'Security' -Name 'defender_status' -Description 'Microsoft Defender engine, signature, and protection state' -Script {
        $s = Get-MpComputerStatus -ErrorAction Stop
        [PSCustomObject]@{
            AMServiceEnabled            = $s.AMServiceEnabled
            AntispywareEnabled          = $s.AntispywareEnabled
            AntivirusEnabled            = $s.AntivirusEnabled
            RealTimeProtectionEnabled   = $s.RealTimeProtectionEnabled
            BehaviorMonitorEnabled      = $s.BehaviorMonitorEnabled
            IoavProtectionEnabled       = $s.IoavProtectionEnabled
            OnAccessProtectionEnabled   = $s.OnAccessProtectionEnabled
            NISEnabled                  = $s.NISEnabled
            TamperProtectionSource      = $s.TamperProtectionSource
            IsTamperProtected           = $s.IsTamperProtected
            AntivirusSignatureVersion   = $s.AntivirusSignatureVersion
            AntivirusSignatureLastUpdated = [string]$s.AntivirusSignatureLastUpdated
            AntivirusSignatureAgeDays   = $s.AntivirusSignatureAge
            QuickScanAge                = $s.QuickScanAge
            FullScanAge                 = $s.FullScanAge
            LastQuickScanTime           = [string]$s.QuickScanEndTime
            AMEngineVersion             = $s.AMEngineVersion
            AMProductVersion            = $s.AMProductVersion
        }
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'defender_preferences_and_exclusions' -Description 'Defender preferences, including exclusion paths/processes/extensions' -Script {
        $p = Get-MpPreference -ErrorAction Stop
        [PSCustomObject]@{
            ExclusionPath                 = @($p.ExclusionPath)
            ExclusionProcess              = @($p.ExclusionProcess)
            ExclusionExtension            = @($p.ExclusionExtension)
            ExclusionIpAddress            = @($p.ExclusionIpAddress)
            DisableRealtimeMonitoring     = $p.DisableRealtimeMonitoring
            DisableBehaviorMonitoring     = $p.DisableBehaviorMonitoring
            DisableScriptScanning         = $p.DisableScriptScanning
            DisableArchiveScanning        = $p.DisableArchiveScanning
            DisableIOAVProtection         = $p.DisableIOAVProtection
            DisableIntrusionPreventionSystem = $p.DisableIntrusionPreventionSystem
            DisableBlockAtFirstSeen       = $p.DisableBlockAtFirstSeen
            MAPSReporting                 = [string]$p.MAPSReporting
            SubmitSamplesConsent          = [string]$p.SubmitSamplesConsent
            PUAProtection                 = [string]$p.PUAProtection
            EnableControlledFolderAccess  = [string]$p.EnableControlledFolderAccess
            AttackSurfaceReductionRules_Ids = @($p.AttackSurfaceReductionRules_Ids)
            AttackSurfaceReductionRules_Actions = @($p.AttackSurfaceReductionRules_Actions)
        }
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'defender_detections' -Format 'Csv' -Description 'Defender threat detection history' -Script {
        $rows = @()
        try {
            $threats = Get-MpThreatDetection -ErrorAction Stop
            foreach ($t in $threats) {
                $rows += [PSCustomObject]@{
                    DetectionID       = $t.DetectionID
                    ThreatID          = $t.ThreatID
                    InitialDetectionTimeUtc = if ($t.InitialDetectionTime) { $t.InitialDetectionTime.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss') } else { '' }
                    LastThreatStatusChangeTime = [string]$t.LastThreatStatusChangeTime
                    ThreatStatus      = [string]$t.ThreatStatusID
                    CleaningAction    = [string]$t.CleaningActionID
                    Resources         = ($t.Resources -join ' | ')
                    ProcessName       = $t.ProcessName
                    DomainUser        = $t.DomainUser
                }
            }
        }
        catch { }
        try {
            foreach ($t in (Get-MpThreat -ErrorAction Stop)) {
                $rows += [PSCustomObject]@{
                    DetectionID       = 'MpThreat'
                    ThreatID          = $t.ThreatID
                    InitialDetectionTimeUtc = ''
                    LastThreatStatusChangeTime = ''
                    ThreatStatus      = $t.ThreatName
                    CleaningAction    = [string]$t.SeverityID
                    Resources         = ($t.Resources -join ' | ')
                    ProcessName       = ''
                    DomainUser        = ''
                }
            }
        }
        catch { }
        $rows
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'installed_av_products' -Format 'Csv' -Description 'Registered AV products from Security Center' -Script {
        Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName AntiVirusProduct -ErrorAction Stop |
            Select-Object displayName, productState, pathToSignedProductExe, pathToSignedReportingExe, timestamp
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'hardening_registry_settings' -Format 'Csv' -Description 'UAC, LSA, WDigest, SMB, and credential protection settings' -Script {
        $rows = @()
        foreach ($c in $script:FotonSession.HardeningRegistryChecks) {
            $val = Get-FotonRegValue -Path $c.Path -Name $c.Name
            $rows += [PSCustomObject]@{
                Setting      = $c.Name
                Key          = $c.Path
                Value        = if ($null -eq $val) { '(not set)' } else { [string]$val }
                RecommendedValue = $c.Expected
                Meaning      = $c.Note
                MatchesRecommended = ($null -ne $val -and [string]$val -eq $c.Expected)
            }
        }
        $rows
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'audit_policy' -Format 'Txt' -Description 'Advanced audit policy configuration (log coverage)' -RequiresElevation -Script {
        & auditpol.exe /get /category:* 2>&1 | Out-String
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'smb_and_winrm_config' -Format 'Txt' -Description 'SMB server config, SMBv1 feature state, WinRM listeners' -Script {
        $out = @()
        $out += '--- Get-SmbServerConfiguration ---'
        try { $out += (Get-SmbServerConfiguration -ErrorAction Stop | Format-List | Out-String -Width 300) } catch { $out += "Error: $($_.Exception.Message)" }
        $out += '--- SMB1 optional feature ---'
        try { $out += (Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop | Select-Object FeatureName, State | Out-String) } catch { $out += 'SMB1 feature query unavailable' }
        $out += '--- winrm enumerate winrm/config/listener ---'
        $winrmCmd = Join-Path $env:SystemRoot 'System32\winrm.cmd'
        if (-not (Test-Path -LiteralPath $winrmCmd)) {
            $winrmCmd = Join-Path $env:SystemRoot 'System32\winrm.exe'
        }
        if (Test-Path -LiteralPath $winrmCmd) {
            $out += (& $winrmCmd enumerate winrm/config/listener 2>&1 | Out-String)
        }
        else {
            $out += 'winrm client not present on PATH or System32 (common on some Windows editions). WinRM service status still recorded below.'
        }
        $out += '--- WinRM service ---'
        $out += (Get-Service WinRM -ErrorAction SilentlyContinue | Select-Object Name, Status, StartType | Out-String)
        $out += '--- PSRemoting trusted hosts (registry) ---'
        # Read the registry value, not the WSMan: drive. With WinRM stopped, the WSMan
        # provider prompts to START the service: the collector blocks on input and a
        # "Y" changes the host it is supposed to observe read-only.
        $th = Get-FotonRegValue 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client' 'trusted_hosts'
        if ($null -eq $th -or [string]$th -eq '') { $out += 'trusted_hosts: (not set)' } else { $out += ('trusted_hosts: {0}' -f $th) }
        $out
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'powershell_environment' -Description 'PowerShell versions, policy, and logging configuration' -Script {
        $psv2 = 'Unknown'
        try { $psv2 = [string](Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2 -ErrorAction Stop).State } catch { }
        [PSCustomObject]@{
            CurrentVersion        = $PSVersionTable.PSVersion.ToString()
            ExecutionPolicyList   = (Get-ExecutionPolicy -List | Out-String)
            PSv2FeatureState      = $psv2
            ScriptBlockLogging    = (Get-FotonRegValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -Name 'EnableScriptBlockLogging')
            ModuleLogging         = (Get-FotonRegValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' -Name 'EnableModuleLogging')
            Transcription         = (Get-FotonRegValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Name 'EnableTranscripting')
            TranscriptOutputDir   = (Get-FotonRegValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Name 'OutputDirectory')
            LanguageMode          = [string]$ExecutionContext.SessionState.LanguageMode
        }
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'non_microsoft_root_certificates' -Format 'Csv' -Description 'Root/intermediate CAs not issued by Microsoft (rogue CA check)' -Script {
        $rows = @()
        foreach ($store in @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\CA', 'Cert:\LocalMachine\TrustedPublisher')) {
            try {
                foreach ($c in (Get-ChildItem -Path $store -ErrorAction Stop)) {
                    $rows += [PSCustomObject]@{
                        Store        = $store
                        Subject      = $c.Subject
                        Issuer       = $c.Issuer
                        Thumbprint   = $c.Thumbprint
                        NotBefore    = $c.NotBefore.ToString('yyyy-MM-dd')
                        NotAfter     = $c.NotAfter.ToString('yyyy-MM-dd')
                        SelfSigned   = ($c.Subject -eq $c.Issuer)
                        FriendlyName = $c.FriendlyName
                    }
                }
            }
            catch { }
        }
        $rows | Sort-Object Store, Subject
    } | Out-Null

    Invoke-FotonCollection -Category 'Security' -Name 'applocker_wdac_and_policy' -Format 'Txt' -Description 'Application control policy and applied group policy summary' -Script {
        $out = @()
        $out += '--- AppLocker effective policy ---'
        try { $out += (Get-AppLockerPolicy -Effective -Xml -ErrorAction Stop) } catch { $out += "AppLocker policy unavailable: $($_.Exception.Message)" }
        $out += '--- Device Guard / WDAC status ---'
        try {
            $out += (Get-CimInstance -Namespace 'root\Microsoft\Windows\DeviceGuard' -ClassName Win32_DeviceGuard -ErrorAction Stop |
                    Select-Object SecurityServicesConfigured, SecurityServicesRunning, VirtualizationBasedSecurityStatus, CodeIntegrityPolicyEnforcementStatus | Out-String)
        }
        catch { $out += 'DeviceGuard WMI class unavailable' }
        $out += '--- gpresult /r ---'
        $out += (& gpresult.exe /r 2>&1 | Out-String)
        $out
    } | Out-Null

    # Deep only: full gpresult HTML (elevated). Complements the /r text above.
    if ($script:FotonSession.Deep) {
        Invoke-FotonCollection -Category 'Security' -Name 'gpresult_html' -Format 'Csv' -Description 'Deep: gpresult /h HTML report into the package (requires elevation).' -RequiresElevation -Script {
            $dest = Join-Path $script:FotonSession.Dirs['Security'] 'gpresult.html'
            $rel = Join-Path $script:FotonSession.Categories['Security'] 'gpresult.html'
            $row = [PSCustomObject]@{
                SourcePath   = 'gpresult.exe /h'
                DestRelative = $rel
                SizeBytes    = 0
                SHA256       = ''
                Status       = 'CopyError'
                Note         = ''
            }
            try {
                $destDir = Split-Path -Parent $dest
                if ($destDir -and -not (Test-Path -LiteralPath $destDir)) {
                    New-Item -Path $destDir -ItemType Directory -Force | Out-Null
                }
                $null = & gpresult.exe /h $dest /f 2>&1
                if (Test-Path -LiteralPath $dest -PathType Leaf) {
                    $item = Get-Item -LiteralPath $dest -Force
                    $row.SizeBytes = $item.Length
                    $row.SHA256 = Get-FotonSafeHash -Path $dest
                    $row.Status = 'Copied'
                    $row.Note = 'gpresult /h written into package. Applied GPO HTML, not Intune MDM report.'
                }
                else {
                    $row.Status = 'Missing'
                    $row.Note = 'gpresult produced no HTML. Visibility gap (policy, elevation, or gpresult failure).'
                }
            }
            catch {
                $row.Status = 'CopyError'
                $row.Note = $_.Exception.Message
            }
            @($row)
        } | Out-Null
    }
}