Add mandatory parser fuzzing (K22-TEST-25) and public Product Best Practices participation (K22-REPO-12), with scope records, criterion evidence, and adoption templates. The existing required conformance job runs both PowerShell fuzz seeds on PR/main and longer campaigns on weekly schedules and release tags.
Fixes #18. Preserve Scorecard detection limits: native PowerShell property-based fuzzing is running, but is not a claim of a supported integration or a passing badge. EX-0007 covers standards enrollment only, reviewed 2026-10-17 and expiring 2026-11-10 UTC, while the provider blocks maintainer login.
BREAKING CHANGE: Public Product repositories must enroll and maintain an evidence-backed assessment; complex untrusted-input parsers must have required CI fuzz campaigns and documented scope. Publish these new MUSTs in a MAJOR release. Existing immutable v0.1.2 and reserved failed tags remain unchanged; this merge does not publish another release.
Validation: all nine required PR checks passed at aa40477a11, including both native fuzz seeds. Extended local campaigns passed 20000 cases and 320000 assertions. Checker/package regressions, Markdown/links, workflow lint/security, secret scans, configured analysis of new PowerShell code, and full authenticated conformance passed.
AI review: https://github.com/KofTwentyTwo/standards/pull/19#pullrequestreview-5480144854. No blocking code findings remain. Substantial AI assistance: OpenAI GPT-6 Codex, under explicit maintainer implementation and merge authorization. No independent human review or completed badge assessment is claimed.
Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
136 lines
6.9 KiB
PowerShell
136 lines
6.9 KiB
PowerShell
#Requires -Version 7.4
|
|
<#
|
|
.SYNOPSIS
|
|
Runs bounded, reproducible property tests against the conformance parsers.
|
|
.DESCRIPTION
|
|
Constructs workflows with independently known security properties, varying quotes,
|
|
indentation, comments, line endings, block scalars, and attacker expressions. Inputs
|
|
are data only: no generated script is executed and no GitHub access is needed.
|
|
A failure prints the seed, iteration, property, and synthetic input for replay.
|
|
.PARAMETER Seed
|
|
Seed for a deterministic sequence of generated cases.
|
|
.PARAMETER Iterations
|
|
Number of generated cases; bounded for pull-request CI.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[int] $Seed = 22023,
|
|
[ValidateRange(1, 100000)]
|
|
[int] $Iterations = 1000
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'Test-RepoConformance.ps1') -LoadHelpers
|
|
$random = [System.Random]::new($Seed)
|
|
$assertions = 0
|
|
$caseNumber = 0
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Fail with replay data when an independently generated security property is violated.
|
|
#>
|
|
function Assert-Property
|
|
{
|
|
param([bool] $Condition, [string] $Property, [string] $InputText)
|
|
if(-not $Condition)
|
|
{
|
|
$replay = [ordered]@{ seed = $Seed; iteration = $caseNumber; property = $Property; input = $InputText }
|
|
throw ($replay | ConvertTo-Json -Compress)
|
|
}
|
|
$script:assertions++
|
|
}
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Generate synthetic hexadecimal action refs using the injected deterministic PRNG.
|
|
#>
|
|
function Get-HexString
|
|
{
|
|
param([int] $Length)
|
|
$characters = for($position = 0; $position -lt $Length; $position++)
|
|
{
|
|
'0123456789abcdef'[$random.Next(16)]
|
|
}
|
|
return -join $characters
|
|
}
|
|
|
|
for($caseNumber = 1; $caseNumber -le $Iterations; $caseNumber++)
|
|
{
|
|
$newline = @("`n", "`r`n")[$random.Next(2)]
|
|
$indent = ' ' * $random.Next(2, 5)
|
|
$quote = @('', "'", '"')[$random.Next(3)]
|
|
$sha = Get-HexString 40
|
|
$uses = 'actions/checkout@' + $sha
|
|
$pinnedWorkflow = @(
|
|
'name: generated'
|
|
'on: pull_request'
|
|
'permissions: {}'
|
|
'jobs:'
|
|
($indent + 'check:')
|
|
($indent * 2 + 'runs-on: ubuntu-24.04')
|
|
($indent * 2 + 'steps:')
|
|
($indent * 3 + '- uses: ' + $quote + $uses + $quote + ' # pinned')
|
|
($indent * 3 + ' with:')
|
|
($indent * 3 + ' persist-credentials: false')
|
|
) -join $newline
|
|
$parsedUses = @(Get-WorkflowUses $pinnedWorkflow)
|
|
Assert-Property -Condition ($parsedUses.Count -eq 1) -Property 'one generated action is found' -InputText $pinnedWorkflow
|
|
Assert-Property -Condition ((Get-UsesPinState $parsedUses[0].Uses) -eq 'PINNED') -Property 'quoted full SHA remains pinned' -InputText $pinnedWorkflow
|
|
Assert-Property -Condition (@(Find-PersistedCheckout $pinnedWorkflow).Count -eq 0) -Property 'explicit credential disabling is found' -InputText $pinnedWorkflow
|
|
$unsafeCheckout = $pinnedWorkflow.Replace('persist-credentials: false', 'persist-credentials: true')
|
|
Assert-Property -Condition (@(Find-PersistedCheckout $unsafeCheckout).Count -eq 1) -Property 'quoted unsafe checkout is never missed' -InputText $unsafeCheckout
|
|
|
|
$badRef = @('v7', 'main', (Get-HexString 39), (Get-HexString 41))[$random.Next(4)]
|
|
Assert-Property -Condition ((Get-UsesPinState ('actions/checkout@' + $badRef)) -ne 'PINNED') -Property 'non-SHA refs never pass pinning' -InputText $badRef
|
|
|
|
$expression = @('github.event.pull_request.title', 'github.head_ref', 'inputs.payload')[$random.Next(3)]
|
|
$indicator = @('|', '|-', '|+', '>', '>-', '>+', '|2', '|2-', '>2+')[$random.Next(9)]
|
|
$expressionText = '${{ ' + $expression + ' }}'
|
|
$injectionWorkflow = @(
|
|
'permissions: {}'
|
|
'jobs:'
|
|
($indent + 'check:')
|
|
($indent * 2 + 'steps:')
|
|
($indent * 3 + '- run: ' + $indicator + ' # generated scalar')
|
|
($indent * 3 + ' echo "' + $expressionText + '"')
|
|
) -join $newline
|
|
$findings = @(Find-TemplateInjection $injectionWorkflow -IncludeInputs)
|
|
Assert-Property -Condition ($findings.Count -eq 1) -Property 'untrusted expression in a block scalar is found' -InputText $injectionWorkflow
|
|
Assert-Property -Condition ($findings[0].Expression -eq $expression) -Property 'finding preserves the controlled expression' -InputText $injectionWorkflow
|
|
$safeWorkflow = $injectionWorkflow.Replace($expressionText, '${{ github.sha }}')
|
|
Assert-Property -Condition (@(Find-TemplateInjection $safeWorkflow -IncludeInputs).Count -eq 0) -Property 'trusted expression is not reported' -InputText $safeWorkflow
|
|
$envWorkflow = @(
|
|
'jobs:'
|
|
($indent + 'check:')
|
|
($indent * 2 + 'steps:')
|
|
($indent * 3 + '- env:')
|
|
($indent * 3 + ' TITLE: ' + $expressionText)
|
|
($indent * 3 + ' run: echo "$TITLE"')
|
|
) -join $newline
|
|
Assert-Property -Condition (@(Find-TemplateInjection $envWorkflow -IncludeInputs).Count -eq 0) -Property 'expressions in env stay outside run bodies' -InputText $envWorkflow
|
|
|
|
$shortName = 'generated-' + $random.Next(100000).ToString([Globalization.CultureInfo]::InvariantCulture)
|
|
$requirement = 'K22-CI-' + $random.Next(10, 25).ToString('00', [Globalization.CultureInfo]::InvariantCulture)
|
|
$exception = '| [EX-0099](#ex-0099) | Open | ' + $requirement + ' | KofTwentyTwo/' + $shortName + ' | 2099-01-01 |'
|
|
$register = @(Read-ExceptionRegister $exception)
|
|
Assert-Property -Condition ($register.Count -eq 1) -Property 'generated exception row is parsed' -InputText $exception
|
|
Assert-Property -Condition ($null -ne (Find-Exception $register $requirement ('KofTwentyTwo/' + $shortName))) -Property 'exact exception scope matches' -InputText $exception
|
|
Assert-Property -Condition ($null -eq (Find-Exception $register $requirement ('KofTwentyTwo/' + $shortName + '-other'))) -Property 'partial repository names cannot gain an exception' -InputText $exception
|
|
Assert-Property -Condition ($null -eq (Find-Exception $register $requirement ('AnotherOwner/' + $shortName))) -Property 'explicit scope cannot authorize another owner' -InputText $exception
|
|
$closed = @(Read-ExceptionRegister $exception.Replace('| Open |', '| Closed |'))
|
|
Assert-Property -Condition ($null -eq (Find-Exception $closed $requirement ('KofTwentyTwo/' + $shortName))) -Property 'closed exceptions cannot authorize a failure' -InputText $exception
|
|
$expired = @(Read-ExceptionRegister $exception.Replace('2099-01-01', '2026-11-10'))
|
|
Assert-Property -Condition ($null -eq (Find-Exception $expired $requirement ('KofTwentyTwo/' + $shortName) -Today ([datetime] '2026-11-10'))) -Property 'expired exceptions cannot authorize a failure' -InputText $exception
|
|
|
|
$noise = -join (1..$random.Next(1, 128) | ForEach-Object { [char] $random.Next(32, 127) })
|
|
foreach($parser in 'Get-WorkflowUses', 'Get-TopLevelPermissions', 'Get-RunBlocks', 'Find-TemplateInjection', 'Find-PersistedCheckout', 'Read-ExceptionRegister')
|
|
{
|
|
& $parser $noise | Out-Null
|
|
}
|
|
Get-AssuranceCheck $noise @() @{} '' | Out-Null
|
|
$assertions++
|
|
}
|
|
|
|
Write-Output "PASS: conformance fuzz seed=$Seed iterations=$Iterations assertions=$assertions"
|