From 012bfa5c021fc33dee902ea32bae00417c1b0a97 Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Fri, 24 Jul 2026 00:48:00 +0530 Subject: [PATCH 1/2] add stable finding id check --- bin/test.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bin/test.ps1 b/bin/test.ps1 index d654dfe..e1b0b9c 100644 --- a/bin/test.ps1 +++ b/bin/test.ps1 @@ -140,6 +140,18 @@ function Test-Static { if ($content -notmatch '\[string\]\$OutputPath') { Fail-Test "missing OutputPath parameter: $($file.FullName)" } if ($content -notmatch '\[switch\]\$Quiet') { Fail-Test "missing Quiet parameter: $($file.FullName)" } } + + . (Join-Path $Root 'lib\windows\Common.ps1') + $seed = Get-OpsForgeIdSeed 'opsforge' + if ($seed -ne '05115ad96d12923e') { + Fail-Test "finding ID seed is not stable: $seed" + } + + $idFiles = @($files) + @(Get-Item (Join-Path $Root 'lib\windows\Common.ps1')) + $unstableHashes = $idFiles | Select-String -SimpleMatch '.GetHashCode(' + if ($unstableHashes) { + Fail-Test "finding IDs still use GetHashCode: $($unstableHashes.Path -join ', ')" + } } function Test-WrapperTargets { From 58e255ecd7ad9389c618c56b5ba55af3a84454df Mon Sep 17 00:00:00 2001 From: iamb4uc Date: Fri, 24 Jul 2026 00:51:28 +0530 Subject: [PATCH 2/2] fix stable windows finding ids --- lib/windows/Common.ps1 | 10 ++++++---- scripts/windows/endpoint/Invoke-WinTriage.ps1 | 6 +++--- scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 | 2 +- scripts/windows/forensic/New-WinEventTimeline.ps1 | 2 +- scripts/windows/forensic/Test-WinLogTampering.ps1 | 2 +- scripts/windows/hardening/Test-WinDefenderStatus.ps1 | 2 +- scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 | 6 +++--- scripts/windows/network/Get-WinNetworkExposure.ps1 | 2 +- scripts/windows/network/Test-WinFirewallExposure.ps1 | 2 +- scripts/windows/persistence/Test-WinScheduledTasks.ps1 | 2 +- 10 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/windows/Common.ps1 b/lib/windows/Common.ps1 index 2552b47..3f0b999 100644 --- a/lib/windows/Common.ps1 +++ b/lib/windows/Common.ps1 @@ -307,11 +307,13 @@ function Get-OpsForgeIdSeed { param([AllowNull()][object]$Value) $text = ConvertTo-OpsForgeText $Value - $hash = [int64]$text.GetHashCode() - if ($hash -lt 0) { - $hash = -$hash + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($text)) + } finally { + $sha256.Dispose() } - return $hash + return [BitConverter]::ToString($hash, 0, 8).Replace('-', '').ToLowerInvariant() } function Get-OpsForgeTaskActionText { diff --git a/scripts/windows/endpoint/Invoke-WinTriage.ps1 b/scripts/windows/endpoint/Invoke-WinTriage.ps1 index 9dad1ba..c2e2a3f 100644 --- a/scripts/windows/endpoint/Invoke-WinTriage.ps1 +++ b/scripts/windows/endpoint/Invoke-WinTriage.ps1 @@ -61,7 +61,7 @@ $runningProcesses | ForEach-Object { $path = $null try { $path = $_.Path } catch { } if (Test-OpsForgeUserWritablePath $path) { - $seed = [Math]::Abs(("$($_.Id)-$path").GetHashCode()) + $seed = Get-OpsForgeIdSeed "$($_.Id)-$path" $signed = $null try { $signed = Get-AuthenticodeSignature -FilePath $path -ErrorAction Stop } catch { } if (-not $signed -or $signed.Status -ne 'Valid') { @@ -72,14 +72,14 @@ $runningProcesses | 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.')) + $findings.Add((New-OpsForgeFinding "WIN-TRIAGE-SERVICE-$(Get-OpsForgeIdSeed $_.Name)" 'Service binary path is suspicious' 'high' 'endpoint' "$($_.Name) $($_.PathName)" 'Validate service creation source and binary signature.')) } } $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.')) + $findings.Add((New-OpsForgeFinding "WIN-TRIAGE-TASK-$(Get-OpsForgeIdSeed ($_.TaskPath + $_.TaskName))" 'Suspicious scheduled task action' 'high' 'endpoint' "$($_.TaskPath)$($_.TaskName): $action" 'Export task XML and verify task author, action, and trigger.')) } } diff --git a/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 b/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 index 2aab86d..862aa6b 100644 --- a/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 +++ b/scripts/windows/endpoint/Test-WinServiceAnomaly.ps1 @@ -20,7 +20,7 @@ $services | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 -Path (Join-Pat foreach ($svc in $services) { $path = [string]$svc.PathName - $seed = [Math]::Abs(($svc.Name + $path).GetHashCode()) + $seed = Get-OpsForgeIdSeed ($svc.Name + $path) if ($path -match '^[A-Za-z]:\\[^"].*\s+.*\.exe') { $findings.Add((New-OpsForgeFinding "WIN-SVC-UNQUOTED-$seed" 'Service has unquoted executable path with spaces' 'medium' 'endpoint' "$($svc.Name): $path" 'Quote the service ImagePath and validate directory ACLs.')) } diff --git a/scripts/windows/forensic/New-WinEventTimeline.ps1 b/scripts/windows/forensic/New-WinEventTimeline.ps1 index 07bdf59..677a318 100644 --- a/scripts/windows/forensic/New-WinEventTimeline.ps1 +++ b/scripts/windows/forensic/New-WinEventTimeline.ps1 @@ -39,7 +39,7 @@ foreach ($log in $logs) { severity = $severity }) if ($event.Id -in 1102,4720,4728,4732,7045,4698) { - $seed = [Math]::Abs(("$log-$($event.RecordId)-$($event.Id)").GetHashCode()) + $seed = Get-OpsForgeIdSeed "$log-$($event.RecordId)-$($event.Id)" $findings.Add((New-OpsForgeFinding "WIN-EVENT-$seed" "Important security event $($event.Id)" $severity 'forensic' "$log record=$($event.RecordId) time=$($event.TimeCreated)" 'Review the event details and correlate with change tickets and endpoint activity.')) } } diff --git a/scripts/windows/forensic/Test-WinLogTampering.ps1 b/scripts/windows/forensic/Test-WinLogTampering.ps1 index 2a3ddb8..36420d7 100644 --- a/scripts/windows/forensic/Test-WinLogTampering.ps1 +++ b/scripts/windows/forensic/Test-WinLogTampering.ps1 @@ -20,7 +20,7 @@ $start = (Get-Date).AddDays(-1 * $LookbackDays) function Add-EventFinding { param([string]$IdPrefix, [string]$Title, [string]$Severity, [object]$Event) - $seed = [Math]::Abs(("$IdPrefix-$($Event.RecordId)-$($Event.TimeCreated)").GetHashCode()) + $seed = Get-OpsForgeIdSeed "$IdPrefix-$($Event.RecordId)-$($Event.TimeCreated)" $findings.Add((New-OpsForgeFinding "$IdPrefix-$seed" $Title $Severity 'forensic' "$($Event.LogName) id=$($Event.Id) time=$($Event.TimeCreated) record=$($Event.RecordId)" 'Correlate with administrative activity, EDR telemetry, and change tickets.')) } diff --git a/scripts/windows/hardening/Test-WinDefenderStatus.ps1 b/scripts/windows/hardening/Test-WinDefenderStatus.ps1 index 1388c5e..0357a95 100644 --- a/scripts/windows/hardening/Test-WinDefenderStatus.ps1 +++ b/scripts/windows/hardening/Test-WinDefenderStatus.ps1 @@ -34,7 +34,7 @@ try { } foreach ($exclusion in @($prefs.ExclusionPath) + @($prefs.ExclusionProcess) + @($prefs.ExclusionExtension)) { if ($exclusion -match '(?i)\\Users\\|\\AppData\\|\\Temp\\|\\ProgramData\\') { - $findings.Add((New-OpsForgeFinding "WIN-DEFENDER-EXCLUSION-$([Math]::Abs($exclusion.GetHashCode()))" 'Suspicious Defender exclusion' 'high' 'hardening' $exclusion 'Remove broad or user-writable exclusions unless formally approved.')) + $findings.Add((New-OpsForgeFinding "WIN-DEFENDER-EXCLUSION-$(Get-OpsForgeIdSeed $exclusion)" 'Suspicious Defender exclusion' 'high' 'hardening' $exclusion 'Remove broad or user-writable exclusions unless formally approved.')) } } } catch { diff --git a/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 b/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 index 524e7b1..862cb0e 100644 --- a/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 +++ b/scripts/windows/hardening/Test-WinPrivilegeSurface.ps1 @@ -30,7 +30,7 @@ $tasks | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -Path (Join-Path $ foreach ($member in @($admins)) { if ($member.ObjectClass -eq 'User' -and $member.Name -notmatch '\\Administrator$') { - $findings.Add((New-OpsForgeFinding "WIN-PRIV-ADMIN-$([Math]::Abs($member.Name.GetHashCode()))" 'Non-default local administrator present' 'medium' 'hardening' $member.Name 'Validate local administrator membership against access policy.')) + $findings.Add((New-OpsForgeFinding "WIN-PRIV-ADMIN-$(Get-OpsForgeIdSeed $member.Name)" 'Non-default local administrator present' 'medium' 'hardening' $member.Name 'Validate local administrator membership against access policy.')) } } if (@($rdp).Count -gt 0) { @@ -43,14 +43,14 @@ if (@($backup).Count -gt 0) { } foreach ($svc in $services | Where-Object { $_.StartName -eq 'LocalSystem' }) { if ($svc.PathName -match '(?i)\\Users\\|\\ProgramData\\|\\Temp\\') { - $findings.Add((New-OpsForgeFinding "WIN-PRIV-SYSTEM-SVC-$([Math]::Abs($svc.Name.GetHashCode()))" 'LocalSystem service references writable-looking path' 'high' 'hardening' "$($svc.Name): $($svc.PathName)" 'Harden ACLs and verify service binary ownership.')) + $findings.Add((New-OpsForgeFinding "WIN-PRIV-SYSTEM-SVC-$(Get-OpsForgeIdSeed $svc.Name)" 'LocalSystem service references writable-looking path' 'high' 'hardening' "$($svc.Name): $($svc.PathName)" 'Harden ACLs and verify service binary ownership.')) } } foreach ($task in $tasks) { if ($task.Principal.UserId -match 'SYSTEM|Administrators') { $action = ($task.Actions | ForEach-Object { Get-OpsForgeTaskActionText $_ }) -join '; ' if ($action -match '(?i)\\Users\\|\\AppData\\|\\Temp\\') { - $findings.Add((New-OpsForgeFinding "WIN-PRIV-ADMIN-TASK-$([Math]::Abs(($task.TaskPath + $task.TaskName).GetHashCode()))" 'Privileged scheduled task executes writable-looking path' 'high' 'hardening' "$($task.TaskPath)$($task.TaskName): $action" 'Validate task path and remove unauthorized privileged automation.')) + $findings.Add((New-OpsForgeFinding "WIN-PRIV-ADMIN-TASK-$(Get-OpsForgeIdSeed ($task.TaskPath + $task.TaskName))" 'Privileged scheduled task executes writable-looking path' 'high' 'hardening' "$($task.TaskPath)$($task.TaskName): $action" 'Validate task path and remove unauthorized privileged automation.')) } } } diff --git a/scripts/windows/network/Get-WinNetworkExposure.ps1 b/scripts/windows/network/Get-WinNetworkExposure.ps1 index dab8896..5dae196 100644 --- a/scripts/windows/network/Get-WinNetworkExposure.ps1 +++ b/scripts/windows/network/Get-WinNetworkExposure.ps1 @@ -45,7 +45,7 @@ Get-NetAdapter -ErrorAction SilentlyContinue | ConvertTo-Json -Depth 4 | Set-Con Get-NetRoute -ErrorAction SilentlyContinue | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 -Path (Join-Path $OutDir 'raw\routes.json') foreach ($record in $records) { - $seed = [Math]::Abs(("$($record.LocalAddress):$($record.LocalPort):$($record.OwningProcess)").GetHashCode()) + $seed = Get-OpsForgeIdSeed "$($record.LocalAddress):$($record.LocalPort):$($record.OwningProcess)" if ($record.LocalPort -in 22, 3389, 5985, 5986, 445, 135, 139 -and $record.LocalAddress -in '0.0.0.0','::') { $findings.Add((New-OpsForgeFinding "WIN-NET-ADMIN-$seed" 'Administrative service listens on all interfaces' 'high' 'network' "$($record.LocalAddress):$($record.LocalPort) $($record.ProcessName)" 'Confirm exposure is intended and restricted by firewall policy.')) } diff --git a/scripts/windows/network/Test-WinFirewallExposure.ps1 b/scripts/windows/network/Test-WinFirewallExposure.ps1 index 4b38d98..bbc517b 100644 --- a/scripts/windows/network/Test-WinFirewallExposure.ps1 +++ b/scripts/windows/network/Test-WinFirewallExposure.ps1 @@ -39,7 +39,7 @@ foreach ($profile in $profiles) { } } foreach ($rule in $filters) { - $seed = [Math]::Abs(($rule.Name + $rule.LocalPort + $rule.RemoteAddress).GetHashCode()) + $seed = Get-OpsForgeIdSeed ($rule.Name + $rule.LocalPort + $rule.RemoteAddress) if ($rule.RemoteAddress -match 'Any|0\.0\.0\.0/0|\*' -and $rule.LocalPort -match '3389|445|5985|5986|22') { $findings.Add((New-OpsForgeFinding "WIN-FW-ADMIN-$seed" 'Administrative port allowed from broad source' 'high' 'network' "$($rule.DisplayName) port=$($rule.LocalPort) remote=$($rule.RemoteAddress)" 'Restrict administrative services to trusted source ranges.')) } diff --git a/scripts/windows/persistence/Test-WinScheduledTasks.ps1 b/scripts/windows/persistence/Test-WinScheduledTasks.ps1 index 2ab3f44..df42000 100644 --- a/scripts/windows/persistence/Test-WinScheduledTasks.ps1 +++ b/scripts/windows/persistence/Test-WinScheduledTasks.ps1 @@ -58,7 +58,7 @@ $tasks | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -Path $rawPath foreach ($task in $tasks) { $action = [string]$task.Actions - $idSeed = [Math]::Abs(($task.TaskPath + $task.TaskName + $action).GetHashCode()) + $idSeed = Get-OpsForgeIdSeed ($task.TaskPath + $task.TaskName + $action) if ($action -match '(?i)powershell.*(-enc|-encodedcommand)') { $findings.Add((New-OpsForgeFinding "WIN-TASK-ENC-$idSeed" 'Scheduled task runs encoded PowerShell' 'high' 'persistence' "$($task.TaskPath)$($task.TaskName): $action" 'Inspect the task XML, validate owner, and disable unauthorized tasks.')) }