From 576d162f9ba286f553bfc765636b38b191d80526 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 26 Sep 2026 19:30:24 +0700 Subject: [PATCH] ci(provenance): reject restricted identifiers in public PR metadata --- CONTRIBUTING.md | 1 + scripts/test-source-clean-guard.ps1 | 61 +++++++++++++++++++++++++++++ scripts/verify-source-clean.ps1 | 22 +++++++++++ 3 files changed, 84 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46f7aea14..cb8068678 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,6 +64,7 @@ A pull request should: - update public documentation when behavior, maturity, safety, or claim boundaries change; - preserve the GPL community license and separate commercial-licensing wording; - document contribution authorship, origin, and required third-party rights; a DCO commit sign-off is not required. +- use vendor-neutral terminology in public PR titles, descriptions, comments and release notes, without changing recorded engineering measurements; For UI changes, include the tested Windows scaling level, resolution, keyboard workflow, and any accessibility impact. For protocol, reporting, GOOSE, SMV, file-transfer, SCL, or control changes, state whether validation used unit tests, deterministic fixtures, loopback, simulator, or an authorized laboratory IED. diff --git a/scripts/test-source-clean-guard.ps1 b/scripts/test-source-clean-guard.ps1 index 345bbdc5e..4577924fd 100644 --- a/scripts/test-source-clean-guard.ps1 +++ b/scripts/test-source-clean-guard.ps1 @@ -85,4 +85,65 @@ 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 +# Verify the GitHub PR event gate without placing any disallowed identifier +# literally in this repository, and without changing the existing file fixtures. +function Invoke-PrMetadataCase { + param( + [Parameter(Mandatory=$true)][string]$Field, + [Parameter(Mandatory=$true)][string]$Value, + [Parameter(Mandatory=$true)][bool]$MustReject + ) + $root = Join-Path ([IO.Path]::GetTempPath()) ("arsas-pr-metadata-" + [guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $root -Force | Out-Null + try { + & git -C $root init --quiet + if ($LASTEXITCODE -ne 0) { throw "Metadata fixture Git initialization failed." } + [IO.File]::WriteAllText( + (Join-Path $root "README.md"), "# Synthetic IEC 61850 metadata fixture", + [Text.UTF8Encoding]::new($false)) + & git -C $root add README.md + if ($LASTEXITCODE -ne 0) { throw "Metadata fixture Git staging failed." } + + $event = @{ pull_request = @{ title = "Synthetic ARSAS update"; body = "Independent engineering change" } } + $event.pull_request[$Field] = $Value + $eventPath = Join-Path $root "event.json" + [IO.File]::WriteAllText($eventPath, ($event | ConvertTo-Json -Depth 4), + [Text.UTF8Encoding]::new($false)) + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = "powershell.exe" + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.EnvironmentVariables["GITHUB_EVENT_NAME"] = "pull_request" + $startInfo.EnvironmentVariables["GITHUB_EVENT_PATH"] = $eventPath + $startInfo.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$scanner`" -RepositoryRoot `"$root`" -ScanOnly" + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + if (-not $process.Start()) { throw "PR metadata fixture scanner failed to start." } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $output = @($stdoutTask.GetAwaiter().GetResult(), $stderrTask.GetAwaiter().GetResult()) -join [Environment]::NewLine + $exitCode = $process.ExitCode + $process.Dispose() + if ($MustReject) { + if ($exitCode -eq 0 -or $output -notmatch ("Forbidden external identifier in PR " + $Field)) { + throw "Source-clean accepted a forbidden PR metadata fixture: $Field; exit=$exitCode; output=$output" + } + } + elseif ($exitCode -ne 0) { + throw "Source-clean rejected neutral PR metadata: $Field; exit=$exitCode; output=$output" + } + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Invoke-PrMetadataCase -Field "title" -Value $identifier -MustReject $true +Invoke-PrMetadataCase -Field "body" -Value ("Protocol evidence from " + $identifier) -MustReject $true +Invoke-PrMetadataCase -Field "title" -Value "Neutral engineering update" -MustReject $false Write-Host "Source-clean negative and positive fixture tests PASS." -ForegroundColor Green diff --git a/scripts/verify-source-clean.ps1 b/scripts/verify-source-clean.ps1 index db4d9cb8b..597872721 100644 --- a/scripts/verify-source-clean.ps1 +++ b/scripts/verify-source-clean.ps1 @@ -204,6 +204,28 @@ foreach ($relative in (Get-TrackedRelativePaths)) { } } +# A PR's public title and description are published before merge, so validate them +# through the same fingerprint matcher that protects Git-tracked paths and text. +if ($env:GITHUB_EVENT_NAME -eq "pull_request") { + if ([string]::IsNullOrWhiteSpace($env:GITHUB_EVENT_PATH) -or + -not (Test-Path -LiteralPath $env:GITHUB_EVENT_PATH -PathType Leaf)) { + $Problems.Add("Missing PR event metadata for public identifier verification") + } + else { + $eventPayload = Get-Content -LiteralPath $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json + if ($null -eq $eventPayload.pull_request) { + $Problems.Add("Missing pull_request object in PR event metadata") + } + else { + foreach ($field in @("title", "body")) { + if (Test-ContainsForbiddenIdentifier ([string]$eventPayload.pull_request.$field)) { + $Problems.Add("Forbidden external identifier in PR $field") + } + } + } + } +} + if ($Problems.Count -gt 0) { foreach ($problem in ($Problems | Sort-Object -Unique)) { Write-Host "ERROR: $problem" -ForegroundColor Red