Refactor application structure and simplify implementation
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user