Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ jobs:
shell: powershell
run: .\ArIED61850Tester\scripts\verify-source-clean.ps1

- name: Verify source-clean rejection fixtures
shell: powershell
run: .\ArIED61850Tester\scripts\test-source-clean-guard.ps1

- name: Verify premium UX, GOOSE, SMV, SAS and release invariants
shell: powershell
run: |
Expand Down
84 changes: 84 additions & 0 deletions scripts/test-source-clean-guard.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Ari Sulistiono
# SPDX-License-Identifier: GPL-3.0-or-later
<#
Tests the complete source-clean scanner against temporary Git-tracked fixtures.
Construct the known test identifier from code points so this test file itself
does not need an exemption from the same clean-room gate.
#>
[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"
$scanner = Join-Path $PSScriptRoot "verify-source-clean.ps1"
$identifier = -join (@(73, 69, 68, 83, 99, 111, 117, 116) | ForEach-Object { [char]$_ })
$cases = @(
@{ Path = "Services/Fixture.cs"; Text = "public sealed class ${identifier}Fixture {}"; Expected = "text" },
@{ Path = "docs/reference.md"; Text = "# $identifier"; Expected = "text" },
@{ Path = "evidence/fixture.json"; Text = "{`"reference`": `"$identifier`"}"; Expected = "text" },
@{ Path = ".github/workflows/smart-discovery-post-merge-production.yml"; Text = "name: $identifier"; Expected = "text" },
@{ Path = "tests/ARSAS.Tests/SyntheticFixture.cs"; Text = "// $identifier"; Expected = "text" },
@{ Path = "docs/${identifier}-fixture.md"; Text = "# independently generated fixture"; Expected = "path" }
)

function Invoke-Case {
param(
[Parameter(Mandatory=$true)][string]$RelativePath,
[Parameter(Mandatory=$true)][string]$Content,
[Parameter(Mandatory=$true)][bool]$MustReject,
[string]$Expected = "text"
)
$root = Join-Path ([IO.Path]::GetTempPath()) ("arsas-clean-room-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $root -Force | Out-Null
try {
& git -C $root init --quiet
if ($LASTEXITCODE -ne 0) { throw "Fixture Git initialization failed." }
$file = Join-Path $root ($RelativePath.Replace('/', [IO.Path]::DirectorySeparatorChar))
New-Item -ItemType Directory -Path (Split-Path $file) -Force | Out-Null
[IO.File]::WriteAllText($file, $Content, [Text.UTF8Encoding]::new($false))
& git -C $root add --all
if ($LASTEXITCODE -ne 0) { throw "Fixture Git staging failed." }

$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = "powershell.exe"
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true

$quotedScanner = '"' + $scanner.Replace('"', '\"') + '"'
$quotedRoot = '"' + $root.Replace('"', '\"') + '"'
$startInfo.Arguments = "-NoProfile -ExecutionPolicy Bypass -File $quotedScanner -RepositoryRoot $quotedRoot -ScanOnly"

$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $startInfo
if (-not $process.Start()) {
throw "Source-clean fixture scanner failed to start."
}

$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$process.WaitForExit()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()
$exitCode = $process.ExitCode
$output = @($stdout, $stderr) -join [Environment]::NewLine
$process.Dispose()
if ($MustReject) {
if ($exitCode -eq 0 -or $output -notmatch ("Forbidden external identifier in " + $Expected)) {
throw "Source-clean unexpectedly accepted a forbidden $Expected fixture: $RelativePath; exit=$exitCode; output=$output"
}
}
elseif ($exitCode -ne 0) {
throw "Source-clean rejected a neutral fixture: $RelativePath; exit=$exitCode; output=$output"
}
}
finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}
}

foreach ($case in $cases) {
Invoke-Case -RelativePath $case.Path -Content $case.Text -MustReject $true -Expected $case.Expected
}
Invoke-Case -RelativePath "docs/synthetic-reference.md" -Content "# ARSAS independent IEC 61850 synthetic evidence" -MustReject $false
Write-Host "Source-clean negative and positive fixture tests PASS." -ForegroundColor Green
75 changes: 48 additions & 27 deletions scripts/verify-source-clean.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@
publish or repeat unrelated product and company names.
#>
[CmdletBinding()]
param()
param(
[string]$RepositoryRoot,
[switch]$ScanOnly
)

$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$RepoRoot = if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
(Resolve-Path (Join-Path $PSScriptRoot "..")).Path
} else {
(Resolve-Path -LiteralPath $RepositoryRoot).Path
}

$ForbiddenFilePatterns = @(
"LICENSE-APACHE-2.0",
Expand Down Expand Up @@ -56,34 +63,31 @@ $TextExtensions = @(
".props", ".targets", ".sln", ".slnx", ".txt"
)

# These are first-party convergence authorities. They intentionally contain the
# external interoperability label so the acceptance contract remains discoverable.
$ApprovedConvergenceIdentifierPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
@(
".github/workflows/smart-discovery-post-merge-production.yml",
".github/workflows/smart-discovery-mainline-readiness.yml",
".github/workflows/scl-interoperability-r7.yml",
"tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs"
) | ForEach-Object { [void]$ApprovedConvergenceIdentifierPaths.Add($_) }

# No tracked path receives a whole-file external-identifier exemption. Historical
# comparison evidence is linked by immutable commit rather than copied into active files.
Comment on lines +66 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan the guard script before skipping its policy literals

When scripts/verify-source-clean.ps1 itself contains a prohibited plain-text identifier, the gate still accepts it because the loop reaches if ($relative -eq "scripts/verify-source-clean.ps1") { continue } before reading its content. This contradicts the newly stated no-whole-file-exemption invariant, and the fixture suite does not expose it because the scanner executable lives outside each temporary repository. Move the self-skip after the external-identifier check (skipping only the embedded policy-pattern checks) and add a fixture using this reserved path.

AGENTS.md reference: AGENTS.md:L172-L176

Useful? React with 👍 / 👎.

$Problems = New-Object System.Collections.Generic.List[string]

function Normalize-RelativePath {
param([Parameter(Mandatory=$true)][string]$Path)
return $Path.Replace('\', '/').TrimStart('/')
}

function Get-Sha256Hex {
$Sha256 = [System.Security.Cryptography.SHA256]::Create()
$IdentifierCandidateCache = [System.Collections.Generic.Dictionary[string,bool]]::new([System.StringComparer]::Ordinal)

function Test-ForbiddenIdentifierCandidate {
param([Parameter(Mandatory=$true)][string]$Value)

$algorithm = [System.Security.Cryptography.SHA256]::Create()
try {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
return -join ($algorithm.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
}
finally {
$algorithm.Dispose()
if (-not $CandidateLengths.Contains($Value.Length)) { return $false }
if ($IdentifierCandidateCache.ContainsKey($Value)) {
return $IdentifierCandidateCache[$Value]
}

$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$hash = -join ($Sha256.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
$isForbidden = $ForbiddenIdentifierHashes.Contains($hash)
$IdentifierCandidateCache[$Value] = $isForbidden
return $isForbidden
}

function Test-ContainsForbiddenIdentifier {
Expand All @@ -93,11 +97,26 @@ function Test-ContainsForbiddenIdentifier {
$words = @([regex]::Matches($Text.ToLowerInvariant(), '[a-z0-9]+') | ForEach-Object { $_.Value })

for ($index = 0; $index -lt $words.Count; $index++) {
$word = $words[$index]

# Detect identifiers embedded in source/path tokens such as TypeNameSuffix.
# This closes the common case where a prohibited product name is attached
# to a class, fixture, job, or filename rather than separated by punctuation.
foreach ($length in $CandidateLengths) {
if ($word.Length -lt $length) { continue }
for ($offset = 0; $offset -le ($word.Length - $length); $offset++) {
$fragment = $word.Substring($offset, $length)
if (Test-ForbiddenIdentifierCandidate $fragment) {
return $true
}
}
}

$candidate = ""
for ($count = 1; $count -le 4 -and ($index + $count - 1) -lt $words.Count; $count++) {
$candidate += $words[$index + $count - 1]
if ($candidate.Length -gt 22) { break }
if ($CandidateLengths.Contains($candidate.Length) -and $ForbiddenIdentifierHashes.Contains((Get-Sha256Hex $candidate))) {
if (Test-ForbiddenIdentifierCandidate $candidate) {
return $true
}
}
Expand Down Expand Up @@ -135,16 +154,15 @@ foreach ($relative in (Get-TrackedRelativePaths)) {
}
}

$identifierScanExempt = $ApprovedConvergenceIdentifierPaths.Contains($relative)
if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $relative)) {
if (Test-ContainsForbiddenIdentifier $relative) {
$Problems.Add("Forbidden external identifier in path: $relative")
}

if ($relative -eq "scripts/verify-source-clean.ps1") { continue }
if ($TextExtensions -notcontains [IO.Path]::GetExtension($relative).ToLowerInvariant()) { continue }

$content = Get-Content -LiteralPath $fullPath -Raw -ErrorAction SilentlyContinue
if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $content)) {
if (Test-ContainsForbiddenIdentifier $content) {
$Problems.Add("Forbidden external identifier in text: $relative")
}

Expand All @@ -162,7 +180,10 @@ if ($Problems.Count -gt 0) {
throw "ARSAS source tree failed clean-room validation with $($Problems.Count) problem(s)."
}

& (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1")
& (Join-Path $PSScriptRoot "verify-auto-update.ps1")
if (-not $ScanOnly) {
& (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1")
& (Join-Path $PSScriptRoot "verify-auto-update.ps1")
}

Write-Host "All Git-tracked ARSAS content passed source, website, external-IP, current-license, binding, and updater checks." -ForegroundColor Green
$Sha256.Dispose()
Write-Host "All Git-tracked ARSAS content passed source and external-identifier checks." -ForegroundColor Green
17 changes: 10 additions & 7 deletions tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,15 +385,18 @@ public void InteroperabilityReferenceContract_PhysicalRetestPassedAndMergeReady(
}

[Fact]
public void SourceClean_DoesNotExemptNeutralReferenceEvidence()
public void SourceClean_GuardsEveryTrackedFileWithoutWholeFileExceptions()
{
var source = File.ReadAllText(FindRepoFile("scripts/verify-source-clean.ps1"));

Assert.Contains("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal);
Assert.DoesNotContain("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md", source, StringComparison.Ordinal);
Assert.DoesNotContain("evidence/interoperability-reference-target.json", source, StringComparison.Ordinal);
Assert.Contains("CanonicalLiveSclExportRegressionTests.cs", source, StringComparison.Ordinal);
Assert.Contains("identifierScanExempt", source, StringComparison.Ordinal);
var build = File.ReadAllText(FindRepoFile(".github/workflows/build.yml"));

Assert.DoesNotContain("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal);
Assert.DoesNotContain("identifierScanExempt", source, StringComparison.Ordinal);
Assert.Contains("if (Test-ContainsForbiddenIdentifier $relative)", source, StringComparison.Ordinal);
Assert.Contains("if (Test-ContainsForbiddenIdentifier $content)", source, StringComparison.Ordinal);
Assert.Contains("test-source-clean-guard.ps1", build, StringComparison.Ordinal);
Assert.Contains("RepositoryRoot", source, StringComparison.Ordinal);
Assert.Contains("ScanOnly", source, StringComparison.Ordinal);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public void ActiveReferenceAndOriginalHistoricalProvenance_AreBothDiscoverable()
Assert.Contains("externalReferenceCapture", workflow, StringComparison.Ordinal);
Assert.Contains("evidence/interoperability-reference-target.json", workflow, StringComparison.Ordinal);
Assert.Contains("evidence/interoperability-reference-target.json", documentation, StringComparison.Ordinal);
Assert.Contains("ApprovedConvergenceIdentifierPaths", sourceClean, StringComparison.Ordinal);
Assert.DoesNotContain("ApprovedConvergenceIdentifierPaths", sourceClean, StringComparison.Ordinal);
Assert.Contains("No tracked path receives a whole-file external-identifier exemption", sourceClean, StringComparison.Ordinal);
}

private static string FindRepositoryFile(string path)
Expand Down
Loading