#Requires -Version 7.0 [CmdletBinding(SupportsShouldProcess)] param([string]$RepositoryRoot=(Split-Path -Parent $PSScriptRoot),[ValidateSet('All','Git','Directory','Preflight')][string]$Mode='All',[string]$GitleaksPath='gitleaks',[string]$ExpectedGitleaksSha256,[string]$ReportDirectory) $ErrorActionPreference='Stop' if(-not $PSCmdlet.ShouldProcess($RepositoryRoot,'Verify immutable receipt/config identities before generating temporary native exceptions and scanning')){return} $root=(Resolve-Path -LiteralPath $RepositoryRoot).ProviderPath $target='assets/docs/receipts/hv/20261101T160215-operator-context-publication-correction-wiki.json' $expectedConfig='D9A61C941A3C99BD431AF67846738320A80EDF0B150B73138047CECBBBE06F7E' $expectedRuntime='5811CC9B22387392AFDDE27DCBC7CF58DF8F06F3FC2EFDCA5C87726406481D30' $knownReceiptHashes=@('41EAD0CD1B6824414845576593066268D589E36D58CA69F4BEB574FE9046B54B','89DD3E2CB6165168BF7C412F4D4DABDDE44B8B0461D8853FFF190EF5245B6EDE') $prefixRecords='[{"kind":"original","digest":"8c90ae56b38e5d979d3a83c3cda0b2024c007743d0521708bf5dcdf786b07df6","length":35},{"kind":"redacted","digest":"155783b39f69a5a17377436a2a6362116e696f347dcba9f6fc02290bdfd1ad72","length":49},{"kind":"redacted","digest":"480ca6f2e794643d82c80ab0ebed0b18d1763aa9d2a49272d65cd45389fedc44","length":26},{"kind":"redacted","digest":"863e5dee0c2767b76b244576dbb9f0561fb5aadd086819897bac5b07b084d46b","length":62}]'|ConvertFrom-Json if(-not $ReportDirectory){$ReportDirectory=Join-Path ([IO.Path]::GetTempPath()) ('wordpairs-wiki-privacy-'+[guid]::NewGuid().ToString())} $reportRoot=[IO.Path]::GetFullPath($ReportDirectory) if($reportRoot -ceq $root -or $reportRoot.StartsWith($root+[IO.Path]::DirectorySeparatorChar,[StringComparison]::Ordinal)){throw 'Reports and temporary configurations must remain outside the publication checkout.'} [void][IO.Directory]::CreateDirectory($reportRoot) $reportPath=Join-Path $reportRoot 'guard-receipt.json' $result=[ordered]@{schema='wordpairs-wiki-immutable-receipt-native-guard/v1';passed=$false;mode=$Mode;preflightPassed=$false;historicalReceiptBlobs=0;nativeScanStarted=$false;runtimeConfigGenerated=$false;configSha256=$null;runtimeConfigSha256=$null;currentReceiptSha256=$null;gitleaksVersion=$null;gitleaksSha256=$null;gitExitCode=$null;directoryExitCode=$null;gitFindings=$null;directoryFindings=$null;failure=$null;protectedFilesOpened=0} function Get-GuardDigest {param([byte[]]$Bytes)return [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($Bytes))} function Read-GuardBlob { param([string]$Oid) $start=[Diagnostics.ProcessStartInfo]::new();$start.FileName='git';$start.UseShellExecute=$false;$start.RedirectStandardOutput=$true;$start.RedirectStandardError=$true foreach($arg in @('-C',$root,'cat-file','blob',$Oid)){[void]$start.ArgumentList.Add($arg)} $process=[Diagnostics.Process]::new();$process.StartInfo=$start;$buffer=[IO.MemoryStream]::new() try{[void]$process.Start();$copy=$process.StandardOutput.BaseStream.CopyToAsync($buffer);$errors=$process.StandardError.ReadToEndAsync();$process.WaitForExit();$null=$copy.GetAwaiter().GetResult();$null=$errors.GetAwaiter().GetResult();if($process.ExitCode -ne 0){throw 'Historical receipt blob could not be verified.'};return ,$buffer.ToArray()}finally{$process.Dispose();$buffer.Dispose()} } function Test-GuardPath { param([string]$Relative) $credentialPattern='(?:^|/)(?:\.ssh|\.creds|\.aws)(?:/|$)|(?:^|/)(?:cred-[^/]+|credentials?(?:\.[^/]+)?|id_(?:rsa|dsa|ecdsa|ed25519)[^/]*|[^/]+\.(?:pwd|key|pfx|p12)|AGENTS-README-FIRST\.yaml|\.netrc|\.npmrc|\.pypirc|\.previewinfo|\.rscinfo)$|(?:^|/)\.next/cache/|(?:^|/)\.env(?:\.(?:local|production|staging|development|test))?(?:\.local)?$' if([IO.Path]::IsPathRooted($Relative) -or $Relative -match $credentialPattern -or @($Relative.Split('/')|Where-Object{$_ -eq '..'}).Count){throw 'Protected or escaping tracked path rejected before read.'} $cursor=$root foreach($part in $Relative.Split('/')){$cursor=Join-Path $cursor $part;if([IO.File]::GetAttributes($cursor) -band [IO.FileAttributes]::ReparsePoint){throw 'Tracked symlink rejected before reading its target.'}} } try{ $gitRoot=(& git -C $root rev-parse --show-toplevel).Trim();if($LASTEXITCODE -ne 0 -or [IO.Path]::GetFullPath($gitRoot) -cne [IO.Path]::GetFullPath($root)){throw 'A repository root is required.'} $headBefore=(& git -C $root rev-parse HEAD).Trim();if($LASTEXITCODE -ne 0){throw 'Repository HEAD cannot be pinned.'} $refsBefore=(& git -C $root for-each-ref --format='%(refname) %(objectname)') -join "`n";if($LASTEXITCODE -ne 0){throw 'Repository refs cannot be pinned.'} Test-GuardPath -Relative '.gitleaks.toml' $configPath=Join-Path $root '.gitleaks.toml';$configBytes=[IO.File]::ReadAllBytes($configPath) $result.configSha256=Get-GuardDigest -Bytes $configBytes if($result.configSha256 -cne $expectedConfig){throw 'Unrecognized configuration identity; native exceptions were not generated.'} Test-GuardPath -Relative $target $currentBytes=[IO.File]::ReadAllBytes((Join-Path $root $target));$result.currentReceiptSha256=Get-GuardDigest -Bytes $currentBytes if($result.currentReceiptSha256 -cnotin $knownReceiptHashes){throw 'Unrecognized current receipt identity; native exceptions were not generated.'} $treeIds=@(& git -C $root log --all --format=%T | Sort-Object -Unique);if($LASTEXITCODE -ne 0){throw 'Complete historical tree inventory failed.'} $blobSet=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach($tree in $treeIds){ $entries=@((& git --literal-pathspecs -C $root ls-tree -r --full-tree -z $tree -- $target) -split [char]0|Where-Object{$_});if($LASTEXITCODE -ne 0){throw 'Historical target tree inventory failed.'} foreach($entry in $entries){$parts=$entry.Split([char]9,2);if($parts.Count -ne 2 -or $parts[1] -cne $target){continue};$metadata=$parts[0].Split(' ');if($metadata[1] -cne 'blob' -or $metadata[0] -notin @('100644','100755')){throw 'Historical receipt is not a regular file.'};[void]$blobSet.Add($metadata[2])} } foreach($oid in $blobSet){$hash=Get-GuardDigest -Bytes (Read-GuardBlob -Oid $oid);if($hash -cnotin $knownReceiptHashes){throw 'Unrecognized historical receipt identity; native exceptions were not generated.'}} $result.historicalReceiptBlobs=$blobSet.Count if($blobSet.Count -eq 0){throw 'No immutable historical receipt was available to verify.'} $result.preflightPassed=$true if($Mode -ceq 'Preflight'){$result.passed=$true}else{ $native=(Get-Command -Name $GitleaksPath -CommandType Application -ErrorAction Stop).Source $result.gitleaksVersion=(& $native version).Trim();if($LASTEXITCODE -ne 0 -or $result.gitleaksVersion -cne '8.30.1'){throw 'This bounded native qualification requires gitleaks 8.30.1.'} $result.gitleaksSha256=(Get-FileHash -LiteralPath $native -Algorithm SHA256).Hash if($ExpectedGitleaksSha256 -and $result.gitleaksSha256 -cne $ExpectedGitleaksSha256){throw 'Native scanner checksum differs from the requested platform binary.'} $patterns=@(foreach($record in $prefixRecords){$field=$record.kind+'TokenSha256';$prefix=$record.digest.Substring(0,$record.length);$span=$field+'": "'+$prefix;'^'+[regex]::Escape($span)+'$'}) $extra=@('[[allowlists]]','description = "Verified native fragment spans; generated only after immutable full receipt identities pass"','targetRules = ["generic-api-key"]','condition = "AND"',"paths = ['''__GUARDED_PATH__''']",'regexTarget = "match"','regexes = [')+@($patterns|ForEach-Object{" '''"+$_+"''',"})+@(']') $runtimeTemplate=[Text.UTF8Encoding]::new($false,$true).GetString($configBytes).TrimEnd()+"`n`n"+($extra -join "`n")+"`n" $result.runtimeConfigSha256=Get-GuardDigest -Bytes ([Text.UTF8Encoding]::new($false).GetBytes($runtimeTemplate)) if($result.runtimeConfigSha256 -cne $expectedRuntime){throw 'Temporary native template does not match its qualified deterministic identity.'} $result.runtimeConfigModeSha256s=[ordered]@{} foreach($scanMode in @('Git','Directory')){ if($Mode -cne 'All' -and $Mode -cne $scanMode){continue} $report=Join-Path $reportRoot ($scanMode.ToLowerInvariant()+'-native.redacted.json') $boundTarget=if($scanMode -ceq 'Git'){$target}else{[IO.Path]::GetFullPath((Join-Path (Join-Path $reportRoot 'tracked-publication') $target)).Replace('\','/')} $runtimeText=$runtimeTemplate.Replace('__GUARDED_PATH__',('^'+[regex]::Escape($boundTarget)+'$')) $runtimePath=Join-Path $reportRoot ($scanMode.ToLowerInvariant()+'-qualified-native.toml') $runtimeBytes=[Text.UTF8Encoding]::new($false).GetBytes($runtimeText) [IO.File]::WriteAllBytes($runtimePath,$runtimeBytes);$result.runtimeConfigModeSha256s[$scanMode]=Get-GuardDigest -Bytes $runtimeBytes;$result.runtimeConfigGenerated=$true if($scanMode -ceq 'Git'){$arguments=@('git',$root,'--log-opts=--all')}else{ $tracked=@((& git -C $root ls-files -z) -split [char]0|Where-Object{$_});if($LASTEXITCODE -ne 0){throw 'Tracked source projection inventory failed.'} foreach($relative in $tracked){Test-GuardPath -Relative $relative} $projection=Join-Path $reportRoot 'tracked-publication';[void][IO.Directory]::CreateDirectory($projection) foreach($relative in $tracked){$destination=Join-Path $projection $relative;[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination));if($relative -ceq $target){[IO.File]::WriteAllBytes($destination,$currentBytes)}else{[IO.File]::Copy((Join-Path $root $relative),$destination,$true)}} $arguments=@('dir',$projection) } $arguments+=@('--config',$runtimePath,'--redact=100','--no-banner','--no-color','--max-archive-depth','10','--report-format','json','--report-path',$report) $result.nativeScanStarted=$true $log=@(& $native @arguments 2>&1);$exit=$LASTEXITCODE;[IO.File]::WriteAllText((Join-Path $reportRoot ($scanMode.ToLowerInvariant()+'.log')),($log -join "`n"),[Text.UTF8Encoding]::new($false)) if($exit -notin @(0,1)){throw 'Native scanner execution failed.'} $findings=@(Get-Content -LiteralPath $report -Raw|ConvertFrom-Json -Depth 100) if($scanMode -ceq 'Git'){$result.gitExitCode=$exit;$result.gitFindings=$findings.Count}else{$result.directoryExitCode=$exit;$result.directoryFindings=$findings.Count} if($exit -ne 0 -or $findings.Count){throw 'Native scanner reported unsuppressed findings.'} } if((Get-FileHash -LiteralPath $configPath -Algorithm SHA256).Hash -cne $expectedConfig -or (Get-FileHash -LiteralPath (Join-Path $root $target) -Algorithm SHA256).Hash -cne $result.currentReceiptSha256){throw 'Pinned current inputs changed during native scans.'} $headAfter=(& git -C $root rev-parse HEAD).Trim();$refsAfter=(& git -C $root for-each-ref --format='%(refname) %(objectname)') -join "`n" if($headAfter -cne $headBefore -or $refsAfter -cne $refsBefore){throw 'Repository refs changed during native scans.'} $result.passed=$true } }catch{$result.failure=$_.Exception.Message}finally{ $result.checkedAt=[DateTimeOffset]::Now.ToString('o') $result|ConvertTo-Json -Depth 30|Set-Content -LiteralPath $reportPath -Encoding utf8 } $result|ConvertTo-Json -Depth 30 if(-not $result.passed){throw 'Guarded wiki native validation failed; review the bounded receipt.'}