docs: add N12R internal sandbox handoff workflow

This commit is contained in:
石头
2026-07-08 16:08:10 +08:00
parent d773571402
commit c54f7c447a
10 changed files with 1986 additions and 49 deletions

View 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