From 0f8047237ec1c4362437fcb90e1e2dc0d2a97d8c Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Tue, 2 Jun 2026 23:09:06 +0530 Subject: [PATCH 1/8] improve windows reports --- lib/windows/Common.ps1 | 103 ++++++++++++++++++ scripts/windows/endpoint/Invoke-WinTriage.ps1 | 26 ++++- .../endpoint/Test-WinServiceAnomaly.ps1 | 15 ++- .../windows/forensic/New-WinEventTimeline.ps1 | 23 +++- .../windows/forensic/Test-WinLogTampering.ps1 | 23 +++- .../hardening/Test-WinDefenderStatus.ps1 | 19 +++- .../hardening/Test-WinPrivilegeSurface.ps1 | 29 ++++- .../network/Get-WinNetworkExposure.ps1 | 25 ++++- .../network/Test-WinFirewallExposure.ps1 | 23 +++- .../persistence/Find-WinPersistence.ps1 | 22 +++- .../persistence/Test-WinScheduledTasks.ps1 | 16 ++- 11 files changed, 308 insertions(+), 16 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index 0af5da7..e27458e 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -70,6 +70,109 @@ function Save-OpsForgeSummary { Set-Content -Encoding UTF8 -Path (Join-Path $OutputDirectory 'summary.txt') -Value $lines } +function Save-OpsForgeReport { + param( + [Parameter(Mandatory = $true)][string]$OutputDirectory, + [Parameter(Mandatory = $true)][string]$Title, + [AllowEmptyCollection()][object[]]$Findings = @(), + [hashtable]$Stats = @{}, + [string[]]$EvidenceFiles = @(), + [string[]]$Limitations = @(), + [string[]]$NextSteps = @(), + [string]$CollectionMode = 'read-only' + ) + + $findingList = @($Findings) + $severityOrder = @('critical','high','medium','low','info') + $severityRank = @{ + critical = 0 + high = 1 + medium = 2 + low = 3 + info = 4 + } + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $lines = New-Object System.Collections.Generic.List[string] + + $lines.Add("# $Title") + $lines.Add('') + $lines.Add("- Host: $(Get-OpsForgeHostName)") + $lines.Add("- Generated: $timestamp") + $lines.Add("- Collection mode: $CollectionMode") + $lines.Add("- Output: $OutputDirectory") + $lines.Add('') + $lines.Add('## Finding Count') + $lines.Add('') + $lines.Add("- Total: $($findingList.Count)") + foreach ($severity in $severityOrder) { + $count = @($findingList | Where-Object { $_.severity -eq $severity }).Count + $lines.Add("- ${severity}: $count") + } + + if ($Stats.Count -gt 0) { + $lines.Add('') + $lines.Add('## Collected') + $lines.Add('') + foreach ($key in ($Stats.Keys | Sort-Object)) { + $lines.Add("- ${key}: $($Stats[$key])") + } + } + + $lines.Add('') + $lines.Add('## Top Findings') + $lines.Add('') + $topFindings = $findingList | + Sort-Object @{ Expression = { $severityRank[[string]$_.severity] } }, title | + Select-Object -First 10 + if (@($topFindings).Count -eq 0) { + $lines.Add('No findings recorded.') + } else { + foreach ($finding in $topFindings) { + $severity = ([string]$finding.severity).ToUpperInvariant() + $lines.Add("- [$severity] $($finding.title) - $($finding.evidence)") + } + } + + $lines.Add('') + $lines.Add('## Evidence Files') + $lines.Add('') + if (@($EvidenceFiles).Count -eq 0) { + $lines.Add('- raw\') + $lines.Add('- findings.json') + $lines.Add('- summary.txt') + } else { + foreach ($file in $EvidenceFiles) { + $lines.Add("- $file") + } + } + + $lines.Add('') + $lines.Add('## Collection Limitations') + $lines.Add('') + if (@($Limitations).Count -eq 0) { + $lines.Add('No explicit limitations recorded. Some data can still be partial without admin rights.') + } else { + foreach ($limitation in $Limitations) { + $lines.Add("- $limitation") + } + } + + $lines.Add('') + $lines.Add('## Next Steps') + $lines.Add('') + if (@($NextSteps).Count -eq 0) { + $lines.Add('- Review high and critical findings first.') + $lines.Add('- Check raw evidence before making changes.') + $lines.Add('- Treat missing data as partial collection, not proof of absence.') + } else { + foreach ($step in $NextSteps) { + $lines.Add("- $step") + } + } + + Set-Content -Encoding UTF8 -Path (Join-Path $OutputDirectory 'report.md') -Value $lines +} + function Test-OpsForgeUserWritablePath { param([AllowNull()][string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } diff --git a/scripts/windows/endpoint/Invoke-WinTriage.ps1 b/scripts/windows/endpoint/Invoke-WinTriage.ps1 index 392a1bf..137c979 100644 --- a/scripts/windows/endpoint/Invoke-WinTriage.ps1 +++ b/scripts/windows/endpoint/Invoke-WinTriage.ps1 @@ -92,7 +92,29 @@ Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -$reportLines = @('# Windows Triage Collector', '', "- Host: $env:COMPUTERNAME", "- Findings: $($findings.Count)", '', 'Raw evidence is stored under `raw\`.') -Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') -Value $reportLines +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Triage Collector' ` + -Findings $findings.ToArray() ` + -Stats @{ + Processes = @($processes).Count + Services = @(Get-CimInstance Win32_Service).Count + ScheduledTasks = @(Get-ScheduledTask).Count + } ` + -EvidenceFiles @( + 'raw\processes.json', + 'raw\services.json', + 'raw\scheduled-tasks.json', + 'raw\network-connections.json', + 'raw\firewall-profiles.json' + ) ` + -Limitations @( + 'Some process paths and signatures may be unavailable without admin rights.', + 'Event log and Defender data depend on local policy and installed components.' + ) ` + -NextSteps @( + 'Review suspicious process, service, task, Defender, firewall, and RDP findings.', + 'Use the raw JSON files to confirm command lines, paths, and owners.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows triage collector' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 b/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 index 51d4f2b..61b469f 100644 --- a/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 +++ b/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 @@ -36,7 +36,18 @@ foreach ($svc in $services) { } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -@('# Windows Service Anomaly Auditor','',"Services collected: $(@($services).Count)","Findings: $($findings.Count)") | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Service Anomaly Auditor' ` + -Findings $findings.ToArray() ` + -Stats @{ Services = @($services).Count } ` + -EvidenceFiles @('raw\services.json') ` + -Limitations @( + 'Service creation time and file ACL review are not always available from Win32_Service alone.' + ) ` + -NextSteps @( + 'Review high severity service paths first.', + 'Check binary signatures and directory permissions for flagged services.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows service anomaly auditor' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet - diff --git a/scripts/windows/forensic/New-WinEventTimeline.ps1 b/scripts/windows/forensic/New-WinEventTimeline.ps1 index d52bbf8..e96acf9 100644 --- a/scripts/windows/forensic/New-WinEventTimeline.ps1 +++ b/scripts/windows/forensic/New-WinEventTimeline.ps1 @@ -54,7 +54,28 @@ $ordered | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Path @('# Windows Event Timeline','','| timestamp | source | event_type | severity | summary |','|---|---|---:|---|---|') + ( $ordered | Select-Object -First 500 | ForEach-Object { "| $($_.timestamp) | $($_.source) | $($_.event_type) | $($_.severity) | $($_.summary -replace '\|','/') |" } ) | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'timeline.md') -Copy-Item -Force -Path (Join-Path $OutDir 'timeline.md') -Destination (Join-Path $OutDir 'report.md') Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Event Timeline Builder' ` + -Findings $findings.ToArray() ` + -Stats @{ + TimelineEvents = @($ordered).Count + LogsRequested = @($logs).Count + } ` + -EvidenceFiles @( + 'timeline.csv', + 'timeline.md', + 'raw\timeline.json', + 'raw\event-read-errors.txt' + ) ` + -Limitations @( + 'Some event logs may be missing, disabled, or unreadable without enough privilege.', + 'Process creation and script block events depend on audit policy being enabled.' + ) ` + -NextSteps @( + 'Review high severity account, service install, task creation, and log-clear events.', + 'Use timeline.csv for sorting and timeline.md for quick reading.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows event timeline builder' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/forensic/Test-WinLogTampering.ps1 b/scripts/windows/forensic/Test-WinLogTampering.ps1 index 05c9714..1846a16 100644 --- a/scripts/windows/forensic/Test-WinLogTampering.ps1 +++ b/scripts/windows/forensic/Test-WinLogTampering.ps1 @@ -65,6 +65,27 @@ try { } catch { } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -@('# Windows Log Tampering Detector','',"Lookback days: $LookbackDays","Findings: $($findings.Count)") | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Log Tampering Detector' ` + -Findings $findings.ToArray() ` + -Stats @{ + LookbackDays = $LookbackDays + EventsCollected = @($events).Count + } ` + -EvidenceFiles @( + 'raw\tampering-events.json', + 'raw\audit-policy.txt', + 'raw\audit-policy-error.txt', + 'raw\security-services.json' + ) ` + -Limitations @( + 'Large event gaps need deeper review than this first-pass check.', + 'Audit policy and event log access can be restricted by local privilege.' + ) ` + -NextSteps @( + 'Review log clear, audit policy, Defender config, and service stop findings.', + 'Correlate timestamps with admin activity and endpoint telemetry.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows log tampering detector' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/hardening/Test-WinDefenderStatus.ps1 b/scripts/windows/hardening/Test-WinDefenderStatus.ps1 index acb14e7..1388c5e 100644 --- a/scripts/windows/hardening/Test-WinDefenderStatus.ps1 +++ b/scripts/windows/hardening/Test-WinDefenderStatus.ps1 @@ -43,6 +43,23 @@ try { } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -@('# Windows Defender Status Auditor','',"Findings: $($findings.Count)") | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Defender Status Auditor' ` + -Findings $findings.ToArray() ` + -EvidenceFiles @( + 'raw\defender-status.json', + 'raw\defender-preferences.json', + 'raw\threat-history.json', + 'raw\defender-error.txt' + ) ` + -Limitations @( + 'Defender cmdlets may be unavailable when Defender is removed or managed differently.', + 'Some settings require admin rights or current Defender platform support.' + ) ` + -NextSteps @( + 'Review disabled protection, stale signatures, and user-writable exclusions first.', + 'Confirm whether any exclusion is expected before removing it.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows Defender status auditor' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 b/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 index 716f8d4..a04aa16 100644 --- a/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 +++ b/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 @@ -66,7 +66,32 @@ try { Get-Service WinRM,TermService -ErrorAction SilentlyContinue | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\remote-services.json') Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -@('# Windows Local Privilege Surface Audit','',"Findings: $($findings.Count)",'Raw evidence is in `raw\`.') | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'privilege-surface-report.md') -Copy-Item -Force -Path (Join-Path $OutDir 'privilege-surface-report.md') -Destination (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Local Privilege Surface Audit' ` + -Findings $findings.ToArray() ` + -Stats @{ + LocalAdministrators = @($admins).Count + Services = @($services).Count + ScheduledTasks = @($tasks).Count + } ` + -EvidenceFiles @( + 'raw\local-admins.json', + 'raw\rdp-users.json', + 'raw\backup-operators.json', + 'raw\services.json', + 'raw\scheduled-tasks.json', + 'raw\uac.json', + 'raw\remote-services.json' + ) ` + -Limitations @( + 'Group membership visibility can differ on domain-joined or policy-managed hosts.', + 'Privilege risk depends on local ACLs and domain policy that this script does not change.' + ) ` + -NextSteps @( + 'Review local admins, Backup Operators, RDP users, UAC state, and privileged task findings.', + 'Confirm privileged service and task paths before changing memberships or configs.' + ) +Copy-Item -Force -Path (Join-Path $OutDir 'report.md') -Destination (Join-Path $OutDir 'privilege-surface-report.md') Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows local privilege surface audit' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/network/Get-WinNetworkExposure.ps1 b/scripts/windows/network/Get-WinNetworkExposure.ps1 index ba506dc..cb17501 100644 --- a/scripts/windows/network/Get-WinNetworkExposure.ps1 +++ b/scripts/windows/network/Get-WinNetworkExposure.ps1 @@ -58,7 +58,28 @@ foreach ($record in $records) { } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -$reportLines = @('# Windows Network Exposure Mapper', '', "- Host: $env:COMPUTERNAME", "- Listening sockets: $(@($records).Count)", "- Findings: $($findings.Count)", '', 'Raw data is stored under `raw\`.') -Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') -Value $reportLines +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Network Exposure Mapper' ` + -Findings $findings.ToArray() ` + -Stats @{ + ListeningSockets = @($records).Count + TcpConnections = @($connections).Count + } ` + -EvidenceFiles @( + 'raw\listening-tcp.json', + 'raw\tcp-connections.json', + 'raw\dns-cache.json', + 'raw\net-adapters.json', + 'raw\routes.json' + ) ` + -Limitations @( + 'Process paths can be missing for protected processes without enough privilege.', + 'Firewall rule mapping is handled by the firewall auditor, not this mapper.' + ) ` + -NextSteps @( + 'Review admin ports listening on all interfaces.', + 'Check unknown process paths and user-writable listener binaries.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows network exposure mapper' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/network/Test-WinFirewallExposure.ps1 b/scripts/windows/network/Test-WinFirewallExposure.ps1 index b862bbc..c300788 100644 --- a/scripts/windows/network/Test-WinFirewallExposure.ps1 +++ b/scripts/windows/network/Test-WinFirewallExposure.ps1 @@ -50,7 +50,26 @@ foreach ($rule in $filters) { Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir Copy-Item -Force -Path (Join-Path $OutDir 'findings.json') -Destination (Join-Path $OutDir 'firewall-findings.json') -@('# Windows Firewall Exposure Auditor','',"Findings: $($findings.Count)",'Raw firewall data is in `raw\`.') | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'firewall-report.md') -Copy-Item -Force -Path (Join-Path $OutDir 'firewall-report.md') -Destination (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Firewall Exposure Auditor' ` + -Findings $findings.ToArray() ` + -Stats @{ + FirewallProfiles = @($profiles).Count + InboundAllowRules = @($filters).Count + } ` + -EvidenceFiles @( + 'raw\firewall-profiles.json', + 'raw\inbound-allow-rules.json', + 'firewall-findings.json' + ) ` + -Limitations @( + 'This catches broad exposure patterns; it does not fully prove rule shadowing.' + ) ` + -NextSteps @( + 'Restrict broad inbound rules for RDP, SMB, WinRM, SSH, and Any/Any allows.', + 'Compare exposed ports with business need and source ranges.' + ) +Copy-Item -Force -Path (Join-Path $OutDir 'report.md') -Destination (Join-Path $OutDir 'firewall-report.md') Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows firewall exposure auditor' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/persistence/Find-WinPersistence.ps1 b/scripts/windows/persistence/Find-WinPersistence.ps1 index 535d2b4..8673816 100644 --- a/scripts/windows/persistence/Find-WinPersistence.ps1 +++ b/scripts/windows/persistence/Find-WinPersistence.ps1 @@ -124,6 +124,26 @@ try { $autoruns.ToArray() | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.json') Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -@('# Windows Persistence Hunter','','Persistence records are stored in `raw\autoruns.json`.',"Findings: $($findings.Count)") | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Persistence Hunter' ` + -Findings $findings.ToArray() ` + -Stats @{ + AutorunRecords = @($autoruns).Count + PowerShellProfiles = @($profiles).Count + } ` + -EvidenceFiles @( + 'raw\autoruns.json', + 'raw\wmi-event-consumers.json', + 'raw\wmi-event-consumers.error.txt' + ) ` + -Limitations @( + 'Registry, WMI, and scheduled task visibility can be partial without admin rights.', + 'PowerShell profile paths vary between Windows PowerShell and PowerShell 7.' + ) ` + -NextSteps @( + 'Start with AppData, Temp, encoded PowerShell, hidden task, and LOLBin findings.', + 'Export suspicious scheduled tasks and preserve referenced binaries before cleanup.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows persistence hunter' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet diff --git a/scripts/windows/persistence/Test-WinScheduledTasks.ps1 b/scripts/windows/persistence/Test-WinScheduledTasks.ps1 index 3b11fe2..827ee80 100644 --- a/scripts/windows/persistence/Test-WinScheduledTasks.ps1 +++ b/scripts/windows/persistence/Test-WinScheduledTasks.ps1 @@ -69,7 +69,19 @@ foreach ($task in $tasks) { } Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -$reportLines = @('# Windows Scheduled Task Auditor', '', "- Host: $env:COMPUTERNAME", "- Tasks collected: $(@($tasks).Count)", "- Findings: $($findings.Count)", '', 'Raw task data: `raw\scheduled-tasks.json`') -Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') -Value $reportLines +Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Scheduled Task Auditor' ` + -Findings $findings.ToArray() ` + -Stats @{ ScheduledTasks = @($tasks).Count } ` + -EvidenceFiles @('raw\scheduled-tasks.json') ` + -Limitations @( + 'Some task metadata may be missing when task info cannot be read.', + 'Task creation time is not exposed cleanly by every scheduled task API.' + ) ` + -NextSteps @( + 'Review encoded PowerShell, user-writable paths, hidden tasks, and logon triggers.', + 'Export suspicious task XML before disabling anything.' + ) Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows scheduled task auditor' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet From fbf6f6c2334a0e5f9d11916cd2b215a0eadb8bc6 Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Tue, 2 Jun 2026 23:13:18 +0530 Subject: [PATCH 2/8] fix windows triage report stats --- scripts/windows/endpoint/Invoke-WinTriage.ps1 | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/windows/endpoint/Invoke-WinTriage.ps1 b/scripts/windows/endpoint/Invoke-WinTriage.ps1 index 137c979..f285c08 100644 --- a/scripts/windows/endpoint/Invoke-WinTriage.ps1 +++ b/scripts/windows/endpoint/Invoke-WinTriage.ps1 @@ -50,7 +50,14 @@ $historyPaths = @( ) | Select-Object -Unique $historyPaths | Where-Object { Test-Path $_ } | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\powershell-history-paths.txt') -Get-Process | ForEach-Object { +$runningProcesses = Get-Process +$services = Get-CimInstance Win32_Service +$scheduledTasks = Get-ScheduledTask +$firewallProfiles = Get-NetFirewallProfile +$rdpListeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $_.LocalPort -eq 3389 -and $_.LocalAddress -in '0.0.0.0','::' } + +$runningProcesses | ForEach-Object { $path = $null try { $path = $_.Path } catch { } if (Test-OpsForgeUserWritablePath $path) { @@ -63,13 +70,13 @@ Get-Process | ForEach-Object { } } -Get-CimInstance Win32_Service | ForEach-Object { +$services | ForEach-Object { if ($_.PathName -match '(?i)\\Users\\|\\AppData\\|\\Temp\\|\\Windows\\Temp\\|powershell.*(-enc|-encodedcommand)') { $findings.Add((New-OpsForgeFinding "WIN-TRIAGE-SERVICE-$([Math]::Abs($_.Name.GetHashCode()))" 'Service binary path is suspicious' 'high' 'endpoint' "$($_.Name) $($_.PathName)" 'Validate service creation source and binary signature.')) } } -Get-ScheduledTask | ForEach-Object { +$scheduledTasks | ForEach-Object { $action = ($_.Actions | ForEach-Object { Get-OpsForgeTaskActionText $_ }) -join '; ' if ($action -match '(?i)powershell.*(-enc|-encodedcommand)|\\AppData\\|\\Temp\\|\\Users\\Public\\') { $findings.Add((New-OpsForgeFinding "WIN-TRIAGE-TASK-$([Math]::Abs(($_.TaskPath + $_.TaskName).GetHashCode()))" 'Suspicious scheduled task action' 'high' 'endpoint' "$($_.TaskPath)$($_.TaskName): $action" 'Export task XML and verify task author, action, and trigger.')) @@ -83,11 +90,11 @@ try { } } catch { } -Get-NetFirewallProfile | Where-Object { -not $_.Enabled } | ForEach-Object { +$firewallProfiles | Where-Object { -not $_.Enabled } | ForEach-Object { $findings.Add((New-OpsForgeFinding "WIN-TRIAGE-FW-$($_.Name)" 'Windows firewall profile is disabled' 'high' 'network' "$($_.Name) profile disabled" 'Re-enable firewall profile or document compensating controls.')) } -Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -eq 3389 -and $_.LocalAddress -in '0.0.0.0','::' } | ForEach-Object { +$rdpListeners | ForEach-Object { $findings.Add((New-OpsForgeFinding 'WIN-TRIAGE-RDP-EXPOSED' 'RDP listens on all interfaces' 'high' 'network' "$($_.LocalAddress):$($_.LocalPort)" 'Restrict RDP exposure with firewall policy and validate remote access requirements.')) } @@ -97,9 +104,9 @@ Save-OpsForgeReport ` -Title 'Windows Triage Collector' ` -Findings $findings.ToArray() ` -Stats @{ - Processes = @($processes).Count - Services = @(Get-CimInstance Win32_Service).Count - ScheduledTasks = @(Get-ScheduledTask).Count + Processes = @($runningProcesses).Count + Services = @($services).Count + ScheduledTasks = @($scheduledTasks).Count } ` -EvidenceFiles @( 'raw\processes.json', From 2782ee11616281a983bd2cb29fab2a3d512b0fb9 Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:04:24 +0530 Subject: [PATCH 3/8] fix windows report writer --- lib/windows/Common.ps1 | 89 ++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index e27458e..3fe3b49 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -92,81 +92,86 @@ function Save-OpsForgeReport { info = 4 } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - $lines = New-Object System.Collections.Generic.List[string] - - $lines.Add("# $Title") - $lines.Add('') - $lines.Add("- Host: $(Get-OpsForgeHostName)") - $lines.Add("- Generated: $timestamp") - $lines.Add("- Collection mode: $CollectionMode") - $lines.Add("- Output: $OutputDirectory") - $lines.Add('') - $lines.Add('## Finding Count') - $lines.Add('') - $lines.Add("- Total: $($findingList.Count)") + $lines = @() + + $lines += "# $Title" + $lines += '' + $lines += "- Host: $(Get-OpsForgeHostName)" + $lines += "- Generated: $timestamp" + $lines += "- Collection mode: $CollectionMode" + $lines += "- Output: $OutputDirectory" + $lines += '' + $lines += '## Finding Count' + $lines += '' + $lines += "- Total: $($findingList.Count)" foreach ($severity in $severityOrder) { $count = @($findingList | Where-Object { $_.severity -eq $severity }).Count - $lines.Add("- ${severity}: $count") + $lines += "- ${severity}: $count" } if ($Stats.Count -gt 0) { - $lines.Add('') - $lines.Add('## Collected') - $lines.Add('') + $lines += '' + $lines += '## Collected' + $lines += '' foreach ($key in ($Stats.Keys | Sort-Object)) { - $lines.Add("- ${key}: $($Stats[$key])") + $lines += "- ${key}: $($Stats[$key])" } } - $lines.Add('') - $lines.Add('## Top Findings') - $lines.Add('') + $lines += '' + $lines += '## Top Findings' + $lines += '' $topFindings = $findingList | - Sort-Object @{ Expression = { $severityRank[[string]$_.severity] } }, title | + Sort-Object @{ + Expression = { + $severity = [string]$_.severity + if ($severityRank.ContainsKey($severity)) { $severityRank[$severity] } else { 99 } + } + }, @{ Expression = { [string]$_.title } } | Select-Object -First 10 if (@($topFindings).Count -eq 0) { - $lines.Add('No findings recorded.') + $lines += 'No findings recorded.' } else { foreach ($finding in $topFindings) { $severity = ([string]$finding.severity).ToUpperInvariant() - $lines.Add("- [$severity] $($finding.title) - $($finding.evidence)") + $lines += "- [$severity] $($finding.title) - $($finding.evidence)" } } - $lines.Add('') - $lines.Add('## Evidence Files') - $lines.Add('') + $lines += '' + $lines += '## Evidence Files' + $lines += '' if (@($EvidenceFiles).Count -eq 0) { - $lines.Add('- raw\') - $lines.Add('- findings.json') - $lines.Add('- summary.txt') + $lines += '- raw\' + $lines += '- findings.json' + $lines += '- summary.txt' } else { foreach ($file in $EvidenceFiles) { - $lines.Add("- $file") + $lines += "- $file" } } - $lines.Add('') - $lines.Add('## Collection Limitations') - $lines.Add('') + $lines += '' + $lines += '## Collection Limitations' + $lines += '' if (@($Limitations).Count -eq 0) { - $lines.Add('No explicit limitations recorded. Some data can still be partial without admin rights.') + $lines += 'No explicit limitations recorded. Some data can still be partial without admin rights.' } else { foreach ($limitation in $Limitations) { - $lines.Add("- $limitation") + $lines += "- $limitation" } } - $lines.Add('') - $lines.Add('## Next Steps') - $lines.Add('') + $lines += '' + $lines += '## Next Steps' + $lines += '' if (@($NextSteps).Count -eq 0) { - $lines.Add('- Review high and critical findings first.') - $lines.Add('- Check raw evidence before making changes.') - $lines.Add('- Treat missing data as partial collection, not proof of absence.') + $lines += '- Review high and critical findings first.' + $lines += '- Check raw evidence before making changes.' + $lines += '- Treat missing data as partial collection, not proof of absence.' } else { foreach ($step in $NextSteps) { - $lines.Add("- $step") + $lines += "- $step" } } From f9ed648c24cbb195ef5498fc19cd12f1e2e99d2e Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:08:54 +0530 Subject: [PATCH 4/8] harden windows report findings --- lib/windows/Common.ps1 | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index 3fe3b49..102a232 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -121,20 +121,27 @@ function Save-OpsForgeReport { $lines += '' $lines += '## Top Findings' $lines += '' - $topFindings = $findingList | - Sort-Object @{ - Expression = { - $severity = [string]$_.severity - if ($severityRank.ContainsKey($severity)) { $severityRank[$severity] } else { 99 } - } - }, @{ Expression = { [string]$_.title } } | - Select-Object -First 10 + $rankedFindings = foreach ($finding in $findingList) { + $severity = [string]$finding.severity + $rank = 99 + if ($severity -and $severityRank.ContainsKey($severity)) { + $rank = $severityRank[$severity] + } + [pscustomobject]@{ + Rank = $rank + Severity = $severity + Title = [string]$finding.title + Evidence = [string]$finding.evidence + } + } + $topFindings = @($rankedFindings | Sort-Object Rank, Title | Select-Object -First 10) if (@($topFindings).Count -eq 0) { $lines += 'No findings recorded.' } else { foreach ($finding in $topFindings) { - $severity = ([string]$finding.severity).ToUpperInvariant() - $lines += "- [$severity] $($finding.title) - $($finding.evidence)" + $severity = ([string]$finding.Severity).ToUpperInvariant() + if (-not $severity) { $severity = 'INFO' } + $lines += "- [$severity] $($finding.Title) - $($finding.Evidence)" } } From afd7c346e5f1466ed3e0644f1d393909dded7492 Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:13:20 +0530 Subject: [PATCH 5/8] fix windows report args --- lib/windows/Common.ps1 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index 102a232..c5833a3 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -75,14 +75,18 @@ function Save-OpsForgeReport { [Parameter(Mandatory = $true)][string]$OutputDirectory, [Parameter(Mandatory = $true)][string]$Title, [AllowEmptyCollection()][object[]]$Findings = @(), - [hashtable]$Stats = @{}, - [string[]]$EvidenceFiles = @(), - [string[]]$Limitations = @(), - [string[]]$NextSteps = @(), + [object]$Stats = $null, + [object[]]$EvidenceFiles = @(), + [object[]]$Limitations = @(), + [object[]]$NextSteps = @(), [string]$CollectionMode = 'read-only' ) $findingList = @($Findings) + $statMap = @{} + if ($Stats -is [hashtable]) { + $statMap = $Stats + } $severityOrder = @('critical','high','medium','low','info') $severityRank = @{ critical = 0 @@ -109,12 +113,12 @@ function Save-OpsForgeReport { $lines += "- ${severity}: $count" } - if ($Stats.Count -gt 0) { + if ($statMap.Count -gt 0) { $lines += '' $lines += '## Collected' $lines += '' - foreach ($key in ($Stats.Keys | Sort-Object)) { - $lines += "- ${key}: $($Stats[$key])" + foreach ($key in ($statMap.Keys | Sort-Object)) { + $lines += "- ${key}: $($statMap[$key])" } } From 3d02d7220a9e110ed605e69be7eec3909f2669df Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:18:38 +0530 Subject: [PATCH 6/8] fix windows persistence report --- .../persistence/Find-WinPersistence.ps1 | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/scripts/windows/persistence/Find-WinPersistence.ps1 b/scripts/windows/persistence/Find-WinPersistence.ps1 index 8673816..13ee03a 100644 --- a/scripts/windows/persistence/Find-WinPersistence.ps1 +++ b/scripts/windows/persistence/Find-WinPersistence.ps1 @@ -123,27 +123,63 @@ try { } $autoruns.ToArray() | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.json') -Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir -Save-OpsForgeReport ` - -OutputDirectory $OutDir ` - -Title 'Windows Persistence Hunter' ` - -Findings $findings.ToArray() ` - -Stats @{ - AutorunRecords = @($autoruns).Count - PowerShellProfiles = @($profiles).Count - } ` - -EvidenceFiles @( - 'raw\autoruns.json', - 'raw\wmi-event-consumers.json', - 'raw\wmi-event-consumers.error.txt' - ) ` - -Limitations @( - 'Registry, WMI, and scheduled task visibility can be partial without admin rights.', - 'PowerShell profile paths vary between Windows PowerShell and PowerShell 7.' - ) ` - -NextSteps @( - 'Start with AppData, Temp, encoded PowerShell, hidden task, and LOLBin findings.', - 'Export suspicious scheduled tasks and preserve referenced binaries before cleanup.' - ) + +try { + Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir +} catch { + $message = "Unable to write findings cleanly: $($_.Exception.Message)" + $message | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\findings-write-error.txt') + '[]' | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'findings.json') + '[]' | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'normalized\findings.json') +} + +try { + Save-OpsForgeReport ` + -OutputDirectory $OutDir ` + -Title 'Windows Persistence Hunter' ` + -Findings $findings.ToArray() ` + -Stats @{ + AutorunRecords = @($autoruns).Count + PowerShellProfiles = @($profiles).Count + } ` + -EvidenceFiles @( + 'raw\autoruns.json', + 'raw\wmi-event-consumers.json', + 'raw\wmi-event-consumers.error.txt' + ) ` + -Limitations @( + 'Registry, WMI, and scheduled task visibility can be partial without admin rights.', + 'PowerShell profile paths vary between Windows PowerShell and PowerShell 7.' + ) ` + -NextSteps @( + 'Start with AppData, Temp, encoded PowerShell, hidden task, and LOLBin findings.', + 'Export suspicious scheduled tasks and preserve referenced binaries before cleanup.' + ) +} catch { + $message = "Unable to write full report: $($_.Exception.Message)" + $message | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\report-write-error.txt') + @( + '# Windows Persistence Hunter', + '', + "- Host: $env:COMPUTERNAME", + "- Findings: $($findings.Count)", + "- Autorun records: $(@($autoruns).Count)", + '', + '## Evidence Files', + '', + '- raw\autoruns.json', + '- findings.json', + '', + '## Collection Limitations', + '', + "- $message", + '- Registry, WMI, and scheduled task visibility can be partial without admin rights.', + '', + '## Next Steps', + '', + '- Review findings.json and raw\autoruns.json.', + '- Preserve suspicious referenced files before cleanup.' + ) | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'report.md') +} Save-OpsForgeSummary -OutputDirectory $OutDir -Title 'Windows persistence hunter' -FindingCount $findings.Count Write-OpsForgeInfo -Message "Output written to $OutDir" -Quiet:$Quiet From cdcecc5c07d370acf40523443866059de55dd23b Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:23:20 +0530 Subject: [PATCH 7/8] fix windows persistence raw output --- .../windows/persistence/Find-WinPersistence.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/windows/persistence/Find-WinPersistence.ps1 b/scripts/windows/persistence/Find-WinPersistence.ps1 index 13ee03a..dc89237 100644 --- a/scripts/windows/persistence/Find-WinPersistence.ps1 +++ b/scripts/windows/persistence/Find-WinPersistence.ps1 @@ -117,12 +117,24 @@ foreach ($key in $specialKeys) { } try { - Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\wmi-event-consumers.json') + Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer | + Select-Object Name, CreatorSID, CommandLineTemplate, ExecutablePath, ScriptingEngine, ScriptText | + ConvertTo-Json -Depth 3 | + Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\wmi-event-consumers.json') } catch { "Unable to read WMI event consumers: $($_.Exception.Message)" | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\wmi-event-consumers.error.txt') } -$autoruns.ToArray() | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.json') +try { + $autoruns.ToArray() | + Select-Object Source, Name, Command | + ConvertTo-Json -Depth 3 | + Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.json') +} catch { + "Unable to serialize autoruns: $($_.Exception.Message)" | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.error.txt') + @($autoruns.ToArray() | ForEach-Object { "$($_.Source)`t$($_.Name)`t$($_.Command)" }) | + Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\autoruns.tsv') +} try { Save-OpsForgeFindings -Findings $findings.ToArray() -OutputDirectory $OutDir From 14552d33678b314e2989b6cad469dd27f6c101b8 Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Wed, 3 Jun 2026 01:31:20 +0530 Subject: [PATCH 8/8] fix windows persistence values --- lib/windows/Common.ps1 | 25 ++++++++++ .../persistence/Find-WinPersistence.ps1 | 50 ++++++++++++------- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index c5833a3..05d1719 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -200,6 +200,31 @@ function Get-OpsForgeSafeFileName { return ($Name -replace '[\\/:*?"<>| ]', '_') } +function ConvertTo-OpsForgeText { + param([AllowNull()][object]$Value) + + if ($null -eq $Value) { + return '' + } + + if ($Value -is [array]) { + return (@($Value) | ForEach-Object { [string]$_ }) -join '; ' + } + + return [string]$Value +} + +function Get-OpsForgeIdSeed { + param([AllowNull()][object]$Value) + + $text = ConvertTo-OpsForgeText $Value + $hash = [int64]$text.GetHashCode() + if ($hash -lt 0) { + $hash = -$hash + } + return $hash +} + function Get-OpsForgeTaskActionText { param([object]$Action) if ($null -eq $Action) { return '' } diff --git a/scripts/windows/persistence/Find-WinPersistence.ps1 b/scripts/windows/persistence/Find-WinPersistence.ps1 index dc89237..4c2567f 100644 --- a/scripts/windows/persistence/Find-WinPersistence.ps1 +++ b/scripts/windows/persistence/Find-WinPersistence.ps1 @@ -18,16 +18,26 @@ $findings = New-Object System.Collections.Generic.List[object] $autoruns = New-Object System.Collections.Generic.List[object] function Add-CommandFinding { - param([string]$Source, [string]$Name, [string]$Command) - $seed = [Math]::Abs(("$Source-$Name-$Command").GetHashCode()) - if ($Command -match '(?i)\\AppData\\|\\Temp\\|\\Users\\Public\\|\\Windows\\Temp\\') { - $findings.Add((New-OpsForgeFinding "WIN-PERSIST-USERPATH-$seed" 'Persistence entry points to user-writable path' 'high' 'persistence' "$Source $Name $Command" 'Validate the referenced binary and remove unauthorized autorun entries.')) + param( + [AllowNull()][object]$Source, + [AllowNull()][object]$Name, + [AllowNull()][object]$Command + ) + + $sourceText = ConvertTo-OpsForgeText $Source + $nameText = ConvertTo-OpsForgeText $Name + $commandText = ConvertTo-OpsForgeText $Command + $evidence = "$sourceText $nameText $commandText".Trim() + $seed = Get-OpsForgeIdSeed $evidence + + if ($commandText -match '(?i)\\AppData\\|\\Temp\\|\\Users\\Public\\|\\Windows\\Temp\\') { + $findings.Add((New-OpsForgeFinding "WIN-PERSIST-USERPATH-$seed" 'Persistence entry points to user-writable path' 'high' 'persistence' $evidence 'Validate the referenced binary and remove unauthorized autorun entries.')) } - if ($Command -match '(?i)powershell.*(-enc|-encodedcommand)') { - $findings.Add((New-OpsForgeFinding "WIN-PERSIST-ENC-$seed" 'Persistence entry uses encoded PowerShell' 'high' 'persistence' "$Source $Name $Command" 'Decode the command, preserve evidence, and disable unauthorized persistence.')) + if ($commandText -match '(?i)powershell.*(-enc|-encodedcommand)') { + $findings.Add((New-OpsForgeFinding "WIN-PERSIST-ENC-$seed" 'Persistence entry uses encoded PowerShell' 'high' 'persistence' $evidence 'Decode the command, preserve evidence, and disable unauthorized persistence.')) } - if ($Command -match '(?i)(rundll32|regsvr32|mshta|wscript|cscript|certutil|bitsadmin|wmic)\.exe') { - $findings.Add((New-OpsForgeFinding "WIN-PERSIST-LOLBIN-$seed" 'Persistence entry uses a living-off-the-land binary' 'medium' 'persistence' "$Source $Name $Command" 'Confirm expected business use and inspect arguments for remote payload loading.')) + if ($commandText -match '(?i)(rundll32|regsvr32|mshta|wscript|cscript|certutil|bitsadmin|wmic)\.exe') { + $findings.Add((New-OpsForgeFinding "WIN-PERSIST-LOLBIN-$seed" 'Persistence entry uses a living-off-the-land binary' 'medium' 'persistence' $evidence 'Confirm expected business use and inspect arguments for remote payload loading.')) } } @@ -41,7 +51,7 @@ foreach ($key in $runKeys) { if (Test-Path $key) { $props = Get-ItemProperty -Path $key foreach ($prop in $props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' }) { - $record = [pscustomobject]@{ Source = $key; Name = $prop.Name; Command = [string]$prop.Value } + $record = [pscustomobject]@{ Source = $key; Name = $prop.Name; Command = (ConvertTo-OpsForgeText $prop.Value) } $autoruns.Add($record) Add-CommandFinding -Source $record.Source -Name $record.Name -Command $record.Command } @@ -50,20 +60,23 @@ foreach ($key in $runKeys) { $services = Get-CimInstance Win32_Service foreach ($svc in $services) { - $autoruns.Add([pscustomobject]@{ Source = 'Service'; Name = $svc.Name; Command = $svc.PathName }) - if ($svc.PathName -match '(?i)\\AppData\\|\\Temp\\|\\Users\\Public\\|powershell.*(-enc|-encodedcommand)') { - Add-CommandFinding -Source 'Service' -Name $svc.Name -Command $svc.PathName - $findings.Add((New-OpsForgeFinding "WIN-PERSIST-SERVICE-$([Math]::Abs($svc.Name.GetHashCode()))" 'Service has suspicious persistence path or arguments' 'high' 'persistence' "$($svc.Name) $($svc.PathName)" 'Validate service creation time, binary signature, and owner.')) + $serviceName = ConvertTo-OpsForgeText $svc.Name + $servicePath = ConvertTo-OpsForgeText $svc.PathName + $autoruns.Add([pscustomobject]@{ Source = 'Service'; Name = $serviceName; Command = $servicePath }) + if ($servicePath -match '(?i)\\AppData\\|\\Temp\\|\\Users\\Public\\|powershell.*(-enc|-encodedcommand)') { + Add-CommandFinding -Source 'Service' -Name $serviceName -Command $servicePath + $findings.Add((New-OpsForgeFinding "WIN-PERSIST-SERVICE-$(Get-OpsForgeIdSeed $serviceName)" 'Service has suspicious persistence path or arguments' 'high' 'persistence' "$serviceName $servicePath" 'Validate service creation time, binary signature, and owner.')) } } Get-ScheduledTask | ForEach-Object { $action = ($_.Actions | ForEach-Object { Get-OpsForgeTaskActionText $_ }) -join '; ' - $autoruns.Add([pscustomobject]@{ Source = 'ScheduledTask'; Name = "$($_.TaskPath)$($_.TaskName)"; Command = $action }) + $taskName = "$(ConvertTo-OpsForgeText $_.TaskPath)$(ConvertTo-OpsForgeText $_.TaskName)" + $autoruns.Add([pscustomobject]@{ Source = 'ScheduledTask'; Name = $taskName; Command = $action }) if ($_.Settings.Hidden) { - $findings.Add((New-OpsForgeFinding "WIN-PERSIST-HIDDEN-TASK-$([Math]::Abs(($_.TaskPath + $_.TaskName).GetHashCode()))" 'Hidden scheduled task' 'medium' 'persistence' "$($_.TaskPath)$($_.TaskName)" 'Confirm task legitimacy and export XML for review.')) + $findings.Add((New-OpsForgeFinding "WIN-PERSIST-HIDDEN-TASK-$(Get-OpsForgeIdSeed $taskName)" 'Hidden scheduled task' 'medium' 'persistence' $taskName 'Confirm task legitimacy and export XML for review.')) } - Add-CommandFinding -Source 'ScheduledTask' -Name "$($_.TaskPath)$($_.TaskName)" -Command $action + Add-CommandFinding -Source 'ScheduledTask' -Name $taskName -Command $action } $startupFolders = @( @@ -109,8 +122,9 @@ foreach ($key in $specialKeys) { Get-ChildItem -Path $key -ErrorAction SilentlyContinue | ForEach-Object { $props = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue $props.PSObject.Properties | Where-Object { $_.Name -match 'Debugger|Shell|Userinit|AppInit_DLLs' } | ForEach-Object { - $autoruns.Add([pscustomobject]@{ Source = $_.Name; Name = $key; Command = [string]$_.Value }) - Add-CommandFinding -Source $_.Name -Name $key -Command ([string]$_.Value) + $value = ConvertTo-OpsForgeText $_.Value + $autoruns.Add([pscustomobject]@{ Source = $_.Name; Name = $key; Command = $value }) + Add-CommandFinding -Source $_.Name -Name $key -Command $value } } }