param( [Parameter(Mandatory = $true)] [string]$PackageRoot, [string]$ReportPath ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $results = New-Object System.Collections.Generic.List[object] $parsedJson = @{} $rawFiles = @{} $requiredFiles = @( "metadata/source-notes.txt", "helper/version.json", "helper/self-check.json", "windows/scan-windows.json", "windows/selected-window.json", "uia/dump-uia-main.json", "uia/dump-uia-main-redacted.json", "notes/operator-notes.txt" ) $optionalFiles = @( "metadata/os-version.txt", "metadata/process-list.txt", "metadata/repo-status.txt", "metadata/verification-summary.txt" ) $jsonFiles = @( "helper/version.json", "helper/self-check.json", "windows/scan-windows.json", "windows/selected-window.json", "uia/dump-uia-main.json", "uia/dump-uia-main-redacted.json" ) $forbiddenTokens = @( "send_message", "search_contact", "upload_file", "download_file", "receive_file", "open_conversation", "auto_login", "autologin", "file_transfer", "read_memory", "memory_dump", "inject", "hook" ) function Add-Result([string]$Status, [string]$Category, [string]$Message) { $script:results.Add([pscustomobject]@{ Status = $Status Category = $Category Message = $Message }) | Out-Null } function Add-Pass([string]$Category, [string]$Message) { Add-Result "PASS" $Category $Message } function Add-Warn([string]$Category, [string]$Message) { Add-Result "WARN" $Category $Message } function Add-Fail([string]$Category, [string]$Message) { Add-Result "FAIL" $Category $Message } function Resolve-AbsolutePath([string]$Path) { if ([System.IO.Path]::IsPathRooted($Path)) { return [System.IO.Path]::GetFullPath($Path) } return [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $Path)) } function Ensure-TrailingSeparator([string]$Path) { $trimmed = $Path.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) return $trimmed + [System.IO.Path]::DirectorySeparatorChar } function Join-PackagePath([string]$Root, [string]$RelativePath) { return Join-Path $Root ($RelativePath -replace '/', [System.IO.Path]::DirectorySeparatorChar) } function Read-TextFile([string]$Root, [string]$RelativePath) { $path = Join-PackagePath $Root $RelativePath if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null } try { $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 $script:rawFiles[$RelativePath] = $text return $text } catch { Add-Fail "read" "$RelativePath could not be read as UTF-8 text: $($_.Exception.Message)" return $null } } function Read-JsonFile([string]$Root, [string]$RelativePath) { $text = Read-TextFile $Root $RelativePath if ($null -eq $text) { return $null } try { $json = $text | ConvertFrom-Json -ErrorAction Stop $script:parsedJson[$RelativePath] = $json Add-Pass "json" "$RelativePath parsed as JSON" return $json } catch { Add-Fail "json" "$RelativePath JSON parse failed: $($_.Exception.Message)" return $null } } function Get-PropertyValue([object]$Object, [string[]]$Names) { if ($null -eq $Object) { return $null } foreach ($name in $Names) { $prop = $Object.PSObject.Properties | Where-Object { $_.Name -ieq $name } | Select-Object -First 1 if ($null -ne $prop) { return $prop.Value } } return $null } function Has-Property([object]$Object, [string]$Name) { if ($null -eq $Object) { return $false } return $null -ne ($Object.PSObject.Properties | Where-Object { $_.Name -ieq $Name } | Select-Object -First 1) } function Test-OkField([object]$Object) { if ($null -eq $Object) { return $false } if (-not (Has-Property $Object "ok")) { return $false } return [bool](Get-PropertyValue $Object @("ok")) } function Convert-ToArray([object]$Value) { if ($null -eq $Value) { return @() } if ($Value -is [System.Array]) { return @($Value) } return @($Value) } function Get-NestedValue([object]$Object, [string[]]$Path) { $current = $Object foreach ($segment in $Path) { if ($null -eq $current) { return $null } $current = Get-PropertyValue $current @($segment) } return $current } function Get-UiRoot([object]$Json) { foreach ($path in @( @("data", "root"), @("data", "tree"), @("root"), @("tree") )) { $candidate = Get-NestedValue $Json $path if ($null -ne $candidate) { return $candidate } } return $Json } function Count-UiNodes([object]$Node) { if ($null -eq $Node) { return 0 } if (($Node -is [System.Collections.IEnumerable]) -and -not ($Node -is [string]) -and -not ($Node -is [pscustomobject])) { $sum = 0 foreach ($item in $Node) { $sum += Count-UiNodes $item } return $sum } $count = 1 $children = Get-PropertyValue $Node @("children", "Children") foreach ($child in (Convert-ToArray $children)) { $count += Count-UiNodes $child } return $count } function Format-Value([object]$Value) { if ($null -eq $Value) { return "" } return [string]$Value } function Test-ForbiddenEvidence([string]$RelativePath, [string]$Text) { if ($null -eq $Text) { return } foreach ($token in $script:forbiddenTokens) { if ($Text -match [regex]::Escape($token)) { Add-Fail "boundary" "$RelativePath contains forbidden action token: $token" } } $opPattern = '"(op|tool|action|operation)"\s*:\s*"(send|search|upload|download|receive|auto_login|autologin|open_conversation|file_transfer|read_memory|inject|hook)[^"]*"' if ($Text -match $opPattern) { Add-Fail "boundary" "$RelativePath contains forbidden operation evidence: $($Matches[0])" } } function Write-Report([string]$Path, [object[]]$Rows, [string]$Decision, [int]$PassCount, [int]$WarnCount, [int]$FailCount) { $lines = New-Object System.Collections.Generic.List[string] $lines.Add("# N12R Return Package Validation Report") | Out-Null $lines.Add("") | Out-Null $lines.Add("- Decision: $Decision") | Out-Null $lines.Add("- PASS: $PassCount") | Out-Null $lines.Add("- WARN: $WarnCount") | Out-Null $lines.Add("- FAIL: $FailCount") | Out-Null $lines.Add("") | Out-Null $lines.Add("| Status | Category | Message |") | Out-Null $lines.Add("|---|---|---|") | Out-Null foreach ($row in $Rows) { $message = ([string]$row.Message).Replace("|", "\|") $category = ([string]$row.Category).Replace("|", "\|") $lines.Add("| $($row.Status) | $category | $message |") | Out-Null } $parent = Split-Path -Parent $Path if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } Set-Content -LiteralPath $Path -Value $lines -Encoding UTF8 } $packageFullPath = $null $reportFullPath = $null $canWriteReport = $false try { $packageFullPath = Resolve-AbsolutePath $PackageRoot if (-not (Test-Path -LiteralPath $packageFullPath -PathType Container)) { Add-Fail "package" "PackageRoot does not exist or is not a directory: $packageFullPath" } else { $packageFullPath = (Resolve-Path -LiteralPath $packageFullPath).Path Add-Pass "package" "PackageRoot exists: $packageFullPath" } if ($ReportPath) { $reportFullPath = Resolve-AbsolutePath $ReportPath if ($null -ne $packageFullPath) { $packagePrefix = Ensure-TrailingSeparator $packageFullPath if ($reportFullPath.StartsWith($packagePrefix, [System.StringComparison]::OrdinalIgnoreCase) -or $reportFullPath.Equals($packageFullPath, [System.StringComparison]::OrdinalIgnoreCase)) { Add-Fail "package" "ReportPath must be outside PackageRoot to keep the returned evidence package immutable: $reportFullPath" } else { $canWriteReport = $true Add-Pass "package" "ReportPath is outside PackageRoot: $reportFullPath" } } } if ((Test-Path -LiteralPath $packageFullPath -PathType Container)) { foreach ($relative in $requiredFiles) { $path = Join-PackagePath $packageFullPath $relative if (Test-Path -LiteralPath $path -PathType Leaf) { Add-Pass "required-files" "$relative exists" } else { Add-Fail "required-files" "$relative is missing" } } foreach ($relative in $optionalFiles) { $path = Join-PackagePath $packageFullPath $relative if (Test-Path -LiteralPath $path -PathType Leaf) { Add-Pass "optional-files" "$relative exists" } else { Add-Warn "optional-files" "$relative is not present" } } foreach ($relative in $jsonFiles) { $path = Join-PackagePath $packageFullPath $relative if (Test-Path -LiteralPath $path -PathType Leaf) { [void](Read-JsonFile $packageFullPath $relative) } } foreach ($relative in ($requiredFiles + $optionalFiles)) { $path = Join-PackagePath $packageFullPath $relative if (Test-Path -LiteralPath $path -PathType Leaf) { $text = if ($rawFiles.ContainsKey($relative)) { $rawFiles[$relative] } else { Read-TextFile $packageFullPath $relative } Test-ForbiddenEvidence $relative $text } } $sourceNotes = Read-TextFile $packageFullPath "metadata/source-notes.txt" if ($null -ne $sourceNotes) { if ($sourceNotes -match '(?i)(internal|intranet|内网)') { Add-Pass "metadata" "metadata/source-notes.txt states internal/intranet context" } else { Add-Fail "metadata" "metadata/source-notes.txt must state this came from an internal-network test sandbox" } if ($sourceNotes -match '(?i)(sandbox|沙盒)') { Add-Pass "metadata" "metadata/source-notes.txt states sandbox context" } else { Add-Fail "metadata" "metadata/source-notes.txt must state sandbox context" } if ($sourceNotes -match '(?i)(human|manual|operator|interactive|人工|手工)') { Add-Pass "metadata" "metadata/source-notes.txt states human/manual operation context" } else { Add-Warn "metadata" "metadata/source-notes.txt does not clearly state human/manual operation context" } } $version = if ($parsedJson.ContainsKey("helper/version.json")) { $parsedJson["helper/version.json"] } else { $null } if ($null -ne $version) { if (Test-OkField $version) { Add-Pass "helper" "helper/version.json ok=true" } else { Add-Fail "helper" "helper/version.json must have ok=true" } $versionData = Get-PropertyValue $version @("data") $helperName = Get-PropertyValue $versionData @("helper_name", "helperName", "name") if ($helperName -eq "ISphereWinHelper") { Add-Pass "helper" "helper/version.json reports ISphereWinHelper" } else { Add-Fail "helper" "helper/version.json must report helper_name=ISphereWinHelper" } } $selfCheck = if ($parsedJson.ContainsKey("helper/self-check.json")) { $parsedJson["helper/self-check.json"] } else { $null } if ($null -ne $selfCheck) { if (Test-OkField $selfCheck) { Add-Pass "helper" "helper/self-check.json ok=true" } else { Add-Fail "helper" "helper/self-check.json must have ok=true" } $selfData = Get-PropertyValue $selfCheck @("data") $uiaAvailable = Get-PropertyValue $selfData @("uia_available", "uiaAvailable", "uia") if ([bool]$uiaAvailable) { Add-Pass "helper" "helper/self-check.json indicates UIA availability" } else { Add-Fail "helper" "helper/self-check.json must indicate UIA availability" } } $scan = if ($parsedJson.ContainsKey("windows/scan-windows.json")) { $parsedJson["windows/scan-windows.json"] } else { $null } $scanWindows = @() if ($null -ne $scan) { if (Test-OkField $scan) { Add-Pass "windows" "windows/scan-windows.json ok=true" } else { Add-Fail "windows" "windows/scan-windows.json must have ok=true" } $scanWindows = @(Convert-ToArray (Get-NestedValue $scan @("data", "windows"))) if ($scanWindows.Count -eq 0) { $scanWindows = @(Convert-ToArray (Get-PropertyValue $scan @("windows"))) } if ($scanWindows.Count -gt 0) { Add-Pass "windows" "windows/scan-windows.json contains $($scanWindows.Count) window row(s)" } else { Add-Fail "windows" "windows/scan-windows.json must contain data.windows" } $candidateCount = 0 foreach ($window in $scanWindows) { $title = Format-Value (Get-PropertyValue $window @("title", "name")) $process = Format-Value (Get-PropertyValue $window @("process_name", "processName", "process", "exe")) $className = Format-Value (Get-PropertyValue $window @("class_name", "className")) $haystack = "$title $process $className" if ($haystack -match '(?i)(isphere|IMPlatformClient|IMPlatform)') { $candidateCount += 1 } } if ($candidateCount -gt 0) { Add-Pass "windows" "windows/scan-windows.json includes $candidateCount iSphere/IMPlatform candidate window(s)" } else { Add-Fail "windows" "windows/scan-windows.json must include an iSphere/IMPlatform candidate window" } } $selected = if ($parsedJson.ContainsKey("windows/selected-window.json")) { $parsedJson["windows/selected-window.json"] } else { $null } if ($null -ne $selected) { $selectedHwnd = Format-Value (Get-PropertyValue $selected @("hwnd", "handle")) $selectedTitle = Format-Value (Get-PropertyValue $selected @("title", "name")) $selectedProcess = Format-Value (Get-PropertyValue $selected @("process_name", "processName", "process", "exe")) if ($selectedHwnd) { Add-Pass "windows" "windows/selected-window.json records hwnd=$selectedHwnd" } else { Add-Fail "windows" "windows/selected-window.json must record hwnd" } if ($selectedTitle -or $selectedProcess) { Add-Pass "windows" "windows/selected-window.json records title/process metadata" } else { Add-Fail "windows" "windows/selected-window.json must record title or process metadata" } $matchFound = $false foreach ($window in $scanWindows) { $windowHwnd = Format-Value (Get-PropertyValue $window @("hwnd", "handle")) if ($selectedHwnd -and $windowHwnd -and $selectedHwnd -ieq $windowHwnd) { $matchFound = $true break } } if ($scanWindows.Count -gt 0) { if ($matchFound) { Add-Pass "windows" "selected hwnd appears in scan-windows" } else { Add-Warn "windows" "selected hwnd was not found in scan-windows" } } } $uiaMain = if ($parsedJson.ContainsKey("uia/dump-uia-main.json")) { $parsedJson["uia/dump-uia-main.json"] } else { $null } if ($null -ne $uiaMain) { if ((Has-Property $uiaMain "ok") -and -not (Test-OkField $uiaMain)) { Add-Fail "uia" "uia/dump-uia-main.json has ok=false" } else { Add-Pass "uia" "uia/dump-uia-main.json is a successful or root-level UIA response" } $root = Get-UiRoot $uiaMain $nodeCount = Count-UiNodes $root if ($nodeCount -gt 0) { Add-Pass "uia" "uia/dump-uia-main.json contains UIA root/tree with $nodeCount node(s)" } else { Add-Fail "uia" "uia/dump-uia-main.json must contain a UIA root/tree" } } $uiaRedacted = if ($parsedJson.ContainsKey("uia/dump-uia-main-redacted.json")) { $parsedJson["uia/dump-uia-main-redacted.json"] } else { $null } if ($null -ne $uiaRedacted) { $root = Get-UiRoot $uiaRedacted $nodeCount = Count-UiNodes $root if ($nodeCount -gt 0) { Add-Pass "uia" "uia/dump-uia-main-redacted.json contains parseable UIA root/tree with $nodeCount node(s)" } else { Add-Fail "uia" "uia/dump-uia-main-redacted.json must contain a UIA root/tree" } if ($rawFiles.ContainsKey("uia/dump-uia-main.json") -and $rawFiles.ContainsKey("uia/dump-uia-main-redacted.json")) { if ($rawFiles["uia/dump-uia-main.json"] -eq $rawFiles["uia/dump-uia-main-redacted.json"]) { Add-Warn "uia" "redacted UIA dump is byte-identical to main dump; manually confirm no sensitive text is present" } else { Add-Pass "uia" "redacted UIA dump differs from main dump" } } } } } catch { Add-Fail "validator" "Unexpected validator error: $($_.Exception.Message)" } $passCount = @($results | Where-Object { $_.Status -eq "PASS" }).Count $warnCount = @($results | Where-Object { $_.Status -eq "WARN" }).Count $failCount = @($results | Where-Object { $_.Status -eq "FAIL" }).Count $decision = if ($failCount -eq 0) { "ACCEPT" } else { "REJECT" } foreach ($row in $results) { Write-Host ("[{0}] {1}: {2}" -f $row.Status, $row.Category, $row.Message) } Write-Host ("Summary: PASS={0} WARN={1} FAIL={2}" -f $passCount, $warnCount, $failCount) Write-Host "Decision: $decision" if ($ReportPath -and $canWriteReport -and $null -ne $reportFullPath) { try { Write-Report -Path $reportFullPath -Rows ([object[]]$results.ToArray()) -Decision $decision -PassCount $passCount -WarnCount $warnCount -FailCount $failCount Write-Host "Report: $reportFullPath" } catch { Write-Host "[FAIL] report: could not write report: $($_.Exception.Message)" exit 1 } } if ($failCount -gt 0) { exit 1 } exit 0