Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions bin/test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions lib/windows/Common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions scripts/windows/endpoint/Invoke-WinTriage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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.'))
}
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/endpoint/Test-WinServiceAnomaly.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/forensic/New-WinEventTimeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/forensic/Test-WinLogTampering.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/hardening/Test-WinDefenderStatus.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions scripts/windows/hardening/Test-WinPrivilegeSurface.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.'))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/network/Get-WinNetworkExposure.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/network/Test-WinFirewallExposure.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/persistence/Test-WinScheduledTasks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
}
Expand Down
Loading