docs: add N12R internal sandbox handoff workflow
This commit is contained in:
179
scripts/test-validate-n12r-return-package.ps1
Normal file
179
scripts/test-validate-n12r-return-package.ps1
Normal file
@@ -0,0 +1,179 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
|
||||
$validator = Join-Path $repo "scripts\validate-n12r-return-package.ps1"
|
||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("n12r-validator-tests-" + [guid]::NewGuid().ToString("N"))
|
||||
|
||||
function Assert-True([bool]$Condition, [string]$Message) {
|
||||
if (-not $Condition) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Contains([string]$Haystack, [string]$Needle, [string]$Message) {
|
||||
if ($Haystack -notlike "*$Needle*") {
|
||||
throw "$Message`nExpected to contain: $Needle`nActual: $Haystack"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-Utf8Text([string]$Path, [string]$Content) {
|
||||
$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 $Content -Encoding UTF8
|
||||
}
|
||||
|
||||
function Write-Json([string]$Path, [object]$Value) {
|
||||
Write-Utf8Text -Path $Path -Content ($Value | ConvertTo-Json -Depth 32)
|
||||
}
|
||||
|
||||
function New-GoodPackage([string]$Path) {
|
||||
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||
foreach ($dir in @("metadata", "helper", "windows", "uia", "notes")) {
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Path $dir) | Out-Null
|
||||
}
|
||||
|
||||
Write-Utf8Text (Join-Path $Path "metadata\source-notes.txt") @"
|
||||
Capture source: internal-network test sandbox.
|
||||
Login method: human operator completed normal interactive sign-in.
|
||||
Boundary: read-only UI Automation capture only.
|
||||
"@
|
||||
Write-Utf8Text (Join-Path $Path "metadata\os-version.txt") "Microsoft Windows test sandbox"
|
||||
Write-Utf8Text (Join-Path $Path "metadata\process-list.txt") "IMPlatformClient 4242"
|
||||
Write-Utf8Text (Join-Path $Path "metadata\repo-status.txt") "## main...origin/main"
|
||||
Write-Utf8Text (Join-Path $Path "metadata\verification-summary.txt") "N12R package files present."
|
||||
|
||||
Write-Json (Join-Path $Path "helper\version.json") @{
|
||||
ok = $true
|
||||
data = @{
|
||||
helper_name = "ISphereWinHelper"
|
||||
helper_version = "0.1.0"
|
||||
}
|
||||
}
|
||||
Write-Json (Join-Path $Path "helper\self-check.json") @{
|
||||
ok = $true
|
||||
data = @{
|
||||
desktop_available = $true
|
||||
uia_available = $true
|
||||
}
|
||||
}
|
||||
Write-Json (Join-Path $Path "windows\scan-windows.json") @{
|
||||
ok = $true
|
||||
data = @{
|
||||
windows = @(
|
||||
@{
|
||||
hwnd = "0x123456"
|
||||
title = "iSphere - Test Sandbox"
|
||||
process_name = "IMPlatformClient"
|
||||
process_id = 4242
|
||||
class_name = "WindowsForms10.Window.8.app.0"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Write-Json (Join-Path $Path "windows\selected-window.json") @{
|
||||
hwnd = "0x123456"
|
||||
title = "iSphere - Test Sandbox"
|
||||
process_name = "IMPlatformClient"
|
||||
process_id = 4242
|
||||
}
|
||||
$uiaTree = @{
|
||||
ok = $true
|
||||
data = @{
|
||||
root = @{
|
||||
name = "iSphere - Test Sandbox"
|
||||
control_type = "Window"
|
||||
automation_id = "MainWindow"
|
||||
children = @(
|
||||
@{
|
||||
name = "Conversation list"
|
||||
control_type = "List"
|
||||
children = @()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Json (Join-Path $Path "uia\dump-uia-main.json") $uiaTree
|
||||
Write-Json (Join-Path $Path "uia\dump-uia-main-redacted.json") $uiaTree
|
||||
Write-Utf8Text (Join-Path $Path "notes\operator-notes.txt") "Read-only capture. No message/file action was performed."
|
||||
}
|
||||
|
||||
function Invoke-Validator([string]$PackageRoot, [string]$ReportPath) {
|
||||
$output = & powershell -NoProfile -ExecutionPolicy Bypass -File $validator -PackageRoot $PackageRoot -ReportPath $ReportPath 2>&1
|
||||
[pscustomobject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($output | Out-String)
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Test([string]$Name, [scriptblock]$Body) {
|
||||
Write-Host "## $Name"
|
||||
& $Body
|
||||
Write-Host "PASS $Name"
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
Run-Test "accepts a complete read-only N12R package" {
|
||||
$pkg = Join-Path $tempRoot "good"
|
||||
$report = Join-Path $tempRoot "good-report.md"
|
||||
New-GoodPackage $pkg
|
||||
$result = Invoke-Validator -PackageRoot $pkg -ReportPath $report
|
||||
Assert-True ($result.ExitCode -eq 0) "Expected validator success, got exit $($result.ExitCode): $($result.Output)"
|
||||
Assert-Contains $result.Output "Decision: ACCEPT" "Expected accept decision in output."
|
||||
Assert-True (Test-Path -LiteralPath $report) "Expected markdown report to be written outside package root."
|
||||
Assert-Contains (Get-Content -LiteralPath $report -Raw -Encoding UTF8) "Decision: ACCEPT" "Expected accept decision in report."
|
||||
}
|
||||
|
||||
Run-Test "rejects a package missing the redacted UIA dump" {
|
||||
$pkg = Join-Path $tempRoot "missing-redacted"
|
||||
$report = Join-Path $tempRoot "missing-redacted-report.md"
|
||||
New-GoodPackage $pkg
|
||||
Remove-Item -LiteralPath (Join-Path $pkg "uia\dump-uia-main-redacted.json") -Force
|
||||
$result = Invoke-Validator -PackageRoot $pkg -ReportPath $report
|
||||
Assert-True ($result.ExitCode -ne 0) "Expected validator failure for missing redacted UIA dump."
|
||||
Assert-Contains $result.Output "uia/dump-uia-main-redacted.json" "Expected missing required path in output."
|
||||
Assert-Contains $result.Output "Decision: REJECT" "Expected reject decision in output."
|
||||
}
|
||||
|
||||
Run-Test "rejects malformed required JSON" {
|
||||
$pkg = Join-Path $tempRoot "bad-json"
|
||||
$report = Join-Path $tempRoot "bad-json-report.md"
|
||||
New-GoodPackage $pkg
|
||||
Write-Utf8Text (Join-Path $pkg "helper\version.json") "{not-json"
|
||||
$result = Invoke-Validator -PackageRoot $pkg -ReportPath $report
|
||||
Assert-True ($result.ExitCode -ne 0) "Expected validator failure for malformed JSON."
|
||||
Assert-Contains $result.Output "helper/version.json" "Expected malformed JSON path in output."
|
||||
Assert-Contains $result.Output "JSON parse failed" "Expected JSON parse failure message."
|
||||
}
|
||||
|
||||
Run-Test "rejects obvious forbidden action evidence" {
|
||||
$pkg = Join-Path $tempRoot "forbidden-action"
|
||||
$report = Join-Path $tempRoot "forbidden-action-report.md"
|
||||
New-GoodPackage $pkg
|
||||
Write-Utf8Text (Join-Path $pkg "notes\operator-notes.txt") "operator accidentally ran send_message during capture"
|
||||
$result = Invoke-Validator -PackageRoot $pkg -ReportPath $report
|
||||
Assert-True ($result.ExitCode -ne 0) "Expected validator failure for forbidden action evidence."
|
||||
Assert-Contains $result.Output "send_message" "Expected forbidden action token in output."
|
||||
Assert-Contains $result.Output "Decision: REJECT" "Expected reject decision in output."
|
||||
}
|
||||
|
||||
Run-Test "rejects report paths inside the evidence package" {
|
||||
$pkg = Join-Path $tempRoot "report-inside-package"
|
||||
New-GoodPackage $pkg
|
||||
$report = Join-Path $pkg "validation-report.md"
|
||||
$result = Invoke-Validator -PackageRoot $pkg -ReportPath $report
|
||||
Assert-True ($result.ExitCode -ne 0) "Expected validator failure when report path is inside package root."
|
||||
Assert-Contains $result.Output "ReportPath must be outside PackageRoot" "Expected package immutability error."
|
||||
Assert-True (-not (Test-Path -LiteralPath $report)) "Validator must not write a report inside the package root."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $tempRoot) {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
537
scripts/validate-n12r-return-package.ps1
Normal file
537
scripts/validate-n12r-return-package.ps1
Normal file
@@ -0,0 +1,537 @@
|
||||
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
|
||||
Reference in New Issue
Block a user