Files
ProxyWarden/scripts/audit-windows-smoke.ps1
T

183 lines
6.5 KiB
PowerShell

param(
[ValidateSet("PlanOnly", "Capture")]
[string]$Mode = "PlanOnly",
[string]$DataRoot = "C:\ProgramData\ProxyWarden",
[string]$ProxiFyreRoot = "C:\Tools\ProxiFyre",
[string]$SingBoxRoot = "C:\Program Files\ProxyWarden\sing-box",
[string]$ForeignServiceName = "",
[string]$OutputPath = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function New-Result {
param(
[bool]$Success,
[string]$Action,
[bool]$Changed,
[string]$Message,
[hashtable]$Details
)
[ordered]@{
success = $Success
action = $Action
changed = $Changed
message = $Message
details = $Details
} | ConvertTo-Json -Depth 8
}
function Get-ServiceEvidence {
param([string[]]$Names)
$result = @()
foreach ($name in $Names | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) {
$escaped = $name.Replace("'", "''")
$service = Get-CimInstance Win32_Service -Filter "Name='$escaped'" -ErrorAction SilentlyContinue
if ($null -eq $service) {
$result += [ordered]@{ name = $name; found = $false }
continue
}
$result += [ordered]@{
name = $service.Name
found = $true
state = $service.State
startMode = $service.StartMode
pathName = $service.PathName
processId = [int]$service.ProcessId
}
}
return $result
}
function Test-PathUnderRoot {
param([string]$Path, [string]$Root)
if ([string]::IsNullOrWhiteSpace($Path) -or [string]::IsNullOrWhiteSpace($Root)) { return $false }
$fullPath = [IO.Path]::GetFullPath($Path).TrimEnd('\')
$fullRoot = [IO.Path]::GetFullPath($Root).TrimEnd('\')
return $fullPath.Equals($fullRoot, [StringComparison]::OrdinalIgnoreCase) -or
$fullPath.StartsWith("$fullRoot\", [StringComparison]::OrdinalIgnoreCase)
}
function Get-ServiceExecutablePath {
param([string]$PathName)
if ([string]::IsNullOrWhiteSpace($PathName)) { return "" }
$trimmed = $PathName.Trim()
if ($trimmed.StartsWith('"')) {
$closingQuote = $trimmed.IndexOf('"', 1)
if ($closingQuote -gt 1) { return $trimmed.Substring(1, $closingQuote - 1) }
}
return ($trimmed -split '\s+', 2)[0]
}
function Get-FileEvidence {
param([string]$Root)
if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return @() }
return @(
Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue |
Select-Object @{N="path";E={$_.FullName}}, @{N="length";E={$_.Length}}, @{N="lastWriteTimeUtc";E={$_.LastWriteTimeUtc.ToString("o")}}
)
}
function Get-SecretFindingCategories {
param([string]$Root)
if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return @() }
$patterns = [ordered]@{
urlUserInfo = '://[^/\s"'']+@'
credentialQuery = '(?i)[?&](token|key|auth|password|passwd|secret)=[^&\s"'']+'
socksCredentials = '(?i)socks5://[^/\s:@]+:[^/\s@]+@'
hwidHeader = '(?i)x-hwid[^\r\n]*[0-9a-f]{8}-[0-9a-f-]{27,}'
}
$findings = @()
$files = Get-ChildItem -LiteralPath $Root -Recurse -File -Include *.json,*.log,*.txt -ErrorAction SilentlyContinue
foreach ($file in $files) {
$content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction SilentlyContinue
if ($null -eq $content) { continue }
foreach ($entry in $patterns.GetEnumerator()) {
if ($content -match $entry.Value) {
$findings += [ordered]@{ path = $file.FullName; category = $entry.Key }
}
}
}
return $findings
}
try {
$quotedServiceFixture = '"C:\Program Files\ProxyWarden\sing-box\ProxyWardenSingBox.exe" -service'
$quotedExecutable = Get-ServiceExecutablePath -PathName $quotedServiceFixture
if (-not (Test-PathUnderRoot -Path $quotedExecutable -Root "C:\Program Files\ProxyWarden\sing-box")) {
throw "Quoted service PathName ownership self-test failed."
}
$plan = [ordered]@{
mode = $Mode
serviceNames = @("ProxiFyreService", "ProxyWardenSingBox")
foreignServiceName = $ForeignServiceName
roots = [ordered]@{
data = [IO.Path]::GetFullPath($DataRoot)
proxifyre = [IO.Path]::GetFullPath($ProxiFyreRoot)
singbox = [IO.Path]::GetFullPath($SingBoxRoot)
}
checks = @("service-state-and-path", "managed-root-membership", "file-metadata", "secret-category-scan")
}
if ($Mode -eq "PlanOnly") {
New-Result -Success $true -Action "audit-windows-smoke.plan" -Changed $false -Message "Windows smoke evidence plan is ready." -Details $plan
exit 0
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $PWD ("audit-windows-smoke-{0}.json" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
}
$outputFullPath = [IO.Path]::GetFullPath($OutputPath)
$outputDirectory = Split-Path -Parent $outputFullPath
if ([string]::IsNullOrWhiteSpace($outputDirectory)) { throw "OutputPath must include a writable directory." }
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
$serviceNames = @("ProxiFyreService", "ProxyWardenSingBox", $ForeignServiceName)
$services = @(Get-ServiceEvidence -Names $serviceNames)
$ownership = @(
$services | Where-Object found | ForEach-Object {
$expectedRoot = switch ($_.name) {
"ProxiFyreService" { $ProxiFyreRoot }
"ProxyWardenSingBox" { $SingBoxRoot }
default { "" }
}
[ordered]@{
name = $_.name
expectedManagedRoot = if ($expectedRoot) { [IO.Path]::GetFullPath($expectedRoot) } else { $null }
pathUnderExpectedRoot = if ($expectedRoot) { Test-PathUnderRoot -Path (Get-ServiceExecutablePath -PathName $_.pathName) -Root $expectedRoot } else { $false }
}
}
)
$report = [ordered]@{
capturedAt = (Get-Date).ToUniversalTime().ToString("o")
computerName = $env:COMPUTERNAME
os = (Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, OSArchitecture)
services = $services
ownership = $ownership
files = @(Get-FileEvidence -Root $DataRoot)
secretFindingCategories = @(Get-SecretFindingCategories -Root $DataRoot)
}
$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $outputFullPath -Encoding UTF8
New-Result -Success $true -Action "audit-windows-smoke.capture" -Changed $true -Message "Read-only Windows smoke evidence captured." -Details @{
outputPath = $outputFullPath
serviceCount = @($services | Where-Object found).Count
fileCount = @($report.files).Count
secretFindingCount = @($report.secretFindingCategories).Count
}
} catch {
New-Result -Success $false -Action "audit-windows-smoke.$($Mode.ToLowerInvariant())" -Changed $false -Message $_.Exception.Message -Details @{}
exit 1
}