212 lines
7.6 KiB
PowerShell
212 lines
7.6 KiB
PowerShell
param(
|
|
[ValidateSet("PlanOnly", "Capture")]
|
|
[string]$Mode = "PlanOnly",
|
|
[string]$DataRoot = "C:\ProgramData\ProxyWarden",
|
|
[string]$AppRoot = "C:\Program Files\ProxyWarden",
|
|
[string]$ProxiFyreRoot = "C:\Program Files\ProxyWarden\components\ProxiFyre",
|
|
[string]$SingBoxRoot = "C:\Program Files\ProxyWarden\components\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
|
|
}
|
|
|
|
function Get-InternalStateEvidence {
|
|
param([string]$Root)
|
|
|
|
$categories = [ordered]@{
|
|
cutoverJournal = ".proxywarden-cutover"
|
|
cutoverQuarantine = ".proxywarden-quarantine"
|
|
packageStaging = ".proxywarden-package-staging"
|
|
privilegedJobs = ".proxywarden-privileged-jobs"
|
|
serviceLogs = ".proxywarden-service-logs"
|
|
singBoxCleanupTombstone = ".proxywarden-sing-box-cleanup"
|
|
}
|
|
|
|
$result = @()
|
|
foreach ($entry in $categories.GetEnumerator()) {
|
|
$path = Join-Path $Root $entry.Value
|
|
$item = Get-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
|
|
$result += [ordered]@{
|
|
category = $entry.Key
|
|
present = $null -ne $item
|
|
itemType = if ($null -eq $item) { $null } elseif ($item.PSIsContainer) { "directory" } else { "file" }
|
|
}
|
|
}
|
|
return $result
|
|
}
|
|
|
|
try {
|
|
$quotedServiceFixture = '"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe" -service'
|
|
$quotedExecutable = Get-ServiceExecutablePath -PathName $quotedServiceFixture
|
|
if (-not (Test-PathUnderRoot -Path $quotedExecutable -Root "C:\Program Files\ProxyWarden\components\sing-box")) {
|
|
throw "Quoted service PathName ownership self-test failed."
|
|
}
|
|
|
|
$plan = [ordered]@{
|
|
mode = $Mode
|
|
serviceNames = @("ProxiFyreService", "ProxyWardenSingBox")
|
|
foreignServiceName = $ForeignServiceName
|
|
roots = [ordered]@{
|
|
app = [IO.Path]::GetFullPath($AppRoot)
|
|
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", "internal-state-presence-only")
|
|
}
|
|
|
|
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)
|
|
internalState = @(Get-InternalStateEvidence -Root $AppRoot)
|
|
}
|
|
$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
|
|
internalStateCategoryCount = @($report.internalState).Count
|
|
}
|
|
} catch {
|
|
New-Result -Success $false -Action "audit-windows-smoke.$($Mode.ToLowerInvariant())" -Changed $false -Message $_.Exception.Message -Details @{}
|
|
exit 1
|
|
}
|