diff --git a/CHANGELOG.md b/CHANGELOG.md index e402f0b4..7d93d9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer]( ### Added +- Source provenance policy SSOT + SelfCheck (C06 L59): `docs/ops/source-provenance.md`, `scripts/source-provenance-check.ps1 -SelfCheck`, `branch-protection-check.ps1 -PolicyOnly` hermetic hook, CONTRIBUTING cross-link (signed commits + CODEOWNERS + human org gates; live Settings remain NOT_VERIFIABLE_IN_REPO). + - SLSA L3 environment isolation SelfCheck (C06 L53): `scripts/slsa-isolation-check.ps1 -SelfCheck`, isolated container rebuild evidence row in `hermetic-builds.md`, `repro-check.ps1 -PolicyOnly` isolation hook, soft CI in `hermetic.yml` (not a full L3 attestation). - ADR 0006: explicit no MCP host/server / pin list (C06 L57) + `mcp-scope` SelfCheck. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea413182..bb3063b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -201,5 +201,22 @@ Verify locally: pwsh -NoProfile -File scripts/commit-signing-check.ps1 -Ref HEAD -Count 5 ``` +## Source provenance (signed commits + CODEOWNERS) + +SessionLedger's **source provenance** policy SSOT covers cryptographic commit +signatures, [`CODEOWNERS`](CODEOWNERS) review expectations, and human org gates +(branch protection, maintainer 2FA) that cannot be verified from checkout alone. +See [`docs/ops/source-provenance.md`](docs/ops/source-provenance.md). + +Hermetic policy smoke (no GitHub API): + +```powershell +pwsh -NoProfile -File scripts/source-provenance-check.ps1 -SelfCheck +pwsh -NoProfile -File scripts/branch-protection-check.ps1 -PolicyOnly +``` + +Live branch protection remains a maintainer Settings control; the scripts above +document anchors and do not claim org Settings are enforced from the tree. + ## Governance This repository follows governance guidelines defined in ~/.claude/CLAUDE.md at a high level. diff --git a/WAVE37_LANE.md b/WAVE37_LANE.md index bf708a37..f89cbfb2 100644 --- a/WAVE37_LANE.md +++ b/WAVE37_LANE.md @@ -1,25 +1,25 @@ -# Wave-37 lane: w37-miri-hard — C00 L7 blocking Miri permutation CI +# Wave-37 lane: w37-source-prov — C06 L59 source-code provenance policy -**Branch:** `feat/sl-w37-miri-hard` -**Worktree:** `C:\Users\koosh\SessionLedger-wtrees\w37-miri-hard` -**Cluster / pillar:** C00 L7 -**Wave-36 overlap:** #296 loom permutation blocking +**Branch:** `feat/sl-w37-source-prov` +**Worktree:** `C:\Users\koosh\SessionLedger-wtrees\w37-source-prov` +**Cluster / pillar:** C06 L59 ## Gap -`miri-smoke.yml` is nightly + `continue-on-error`. Add PR-blocking Miri job for -`race_model` (and optional `loom_model` subset) with policy doc anchors. +L59 score 2: signed merges present but no in-repo signed-commit policy SSOT. +Extend branch-protection + contributor policy with machine-verifiable anchors. ## Acceptance criteria -1. Add `scripts/miri-permutation-check.ps1 -SelfCheck` documenting policy. -2. Add `.github/workflows/miri-permutation.yml` blocking on PR/push (ubuntu, race_model). -3. Keep existing `miri-smoke.yml` soft nightly or document split. -4. Update `docs/ops/concurrency-safety.md`. +1. Add `docs/ops/source-provenance.md` SSOT (signed commits, CODEOWNERS, human org gates). +2. Add `scripts/source-provenance-check.ps1 -SelfCheck`. +3. Extend `scripts/branch-protection-check.ps1` policy hooks if needed. +4. Update `CONTRIBUTING.md` cross-link. 5. CHANGELOG bullet. **Do not edit** audit scorecard/traceability files. ## Verify ```powershell -pwsh ./scripts/miri-permutation-check.ps1 -SelfCheck +pwsh ./scripts/source-provenance-check.ps1 -SelfCheck +pwsh ./scripts/branch-protection-check.ps1 -PolicyOnly ``` diff --git a/docs/ops/branch-protection.md b/docs/ops/branch-protection.md index 35c264bc..957e0c02 100644 --- a/docs/ops/branch-protection.md +++ b/docs/ops/branch-protection.md @@ -63,8 +63,22 @@ unless maintainers pass `-Strict` manually. without elevated permissions; treat CI as advisory evidence that the script runs, not as proof that Settings are correct. +## PolicyOnly (hermetic hook) + +[`scripts/branch-protection-check.ps1`](../../scripts/branch-protection-check.ps1) +`-PolicyOnly` validates in-repo source-provenance policy anchors (signed commits, +CODEOWNERS, human org gate rows) without calling the GitHub API. Use it in CI or +local smoke when tokens are unavailable: + +```powershell +pwsh ./scripts/branch-protection-check.ps1 -PolicyOnly +``` + +See [`source-provenance.md`](source-provenance.md) (C06 L59 SSOT). + ## Related +- [Source provenance policy](source-provenance.md) — signed commits + CODEOWNERS + org gates SSOT - [Commit signing ops](commit-signing.md) — tip signature check + soft checklist - [ADR 0004](../adr/0004-commit-signing-policy.md) — signed-commit policy - [SECURITY.md](../../SECURITY.md) — supply-chain index diff --git a/docs/ops/source-provenance.md b/docs/ops/source-provenance.md new file mode 100644 index 00000000..673364bf --- /dev/null +++ b/docs/ops/source-provenance.md @@ -0,0 +1,108 @@ +# Source code provenance policy + +Status: **C06 L59** — SSOT for **signed commits**, [`CODEOWNERS`](../../CODEOWNERS) +review gates, and **human org controls** (branch protection, maintainer 2FA) that +bind who may change SessionLedger source. This page records policy and +machine-verifiable anchors; it does **not** claim that GitHub org Settings are +provable from a git checkout alone. + +Related: [`commit-signing.md`](commit-signing.md), [`branch-protection.md`](branch-protection.md), +[`CONTRIBUTING.md`](../../CONTRIBUTING.md), [`SECURITY.md`](../../SECURITY.md), +[`docs/adr/0004-commit-signing-policy.md`](../adr/0004-commit-signing-policy.md). + +## Policy layers + +| Layer | Requirement | In-repo evidence | +|-------|-------------|------------------| +| DCO | `Signed-off-by:` on each commit | [`CONTRIBUTING.md`](../../CONTRIBUTING.md) | +| GPG / SSH | Cryptographic signature on each commit reaching `main` | [`commit-signing.md`](commit-signing.md), ADR 0004 | +| CODEOWNERS | Review from listed owners on owned paths | [`CODEOWNERS`](../../CODEOWNERS) | +| Branch discipline | Feature branches + PRs; no direct push to `main` | [`CONTRIBUTING.md`](../../CONTRIBUTING.md) | +| Branch protection | Require signed commits + PR before merge on `main` | Human org gate (see below) | +| Maintainer 2FA | Org/account 2FA for Settings / merge access | Human org gate (see below) | + +Contributors must configure `commit.gpgsign` (GPG or SSH) **before** pushing. +DCO sign-off is complementary — it is **not** a substitute for GPG/SSH signatures. + +## CODEOWNERS review gates + +[`CODEOWNERS`](../../CODEOWNERS) assigns default ownership for the tree and +crate-specific paths. GitHub uses CODEOWNERS to request reviews from listed +handles when matching files change. + +Maintainers should enable **Require review from Code Owners** on `main` when the +repository settings allow it. That toggle is a **human org gate** — this +repository documents the expectation but cannot assert the toggle from checkout. + +## Signed commits (contributor + maintainer) + +Operator setup, tip verification, and CI evidence live in +[`commit-signing.md`](commit-signing.md). Local checks: + +```powershell +pwsh ./scripts/commit-signing-check.ps1 -Ref HEAD -Count 5 +pwsh ./scripts/branch-protection-check.ps1 -PolicyOnly +``` + +Branch protection machine-verify (best-effort `gh api`, soft-skip without admin +scope): [`branch-protection.md`](branch-protection.md) + +[`scripts/branch-protection-check.ps1`](../../scripts/branch-protection-check.ps1). + +## Human org gates (Settings) + +These controls are **required policy** but **not** machine-verifiable from +repository contents: + +| Control | Why not in-tree? | Maintainer action | +|---------|------------------|-------------------| +| **Require signed commits** on `main` | GitHub branch protection API needs admin scope | Settings → Branches → `main` | +| **Require a pull request before merging** | Same | Same | +| **Require review from Code Owners** | Same | Same (when CODEOWNERS is active) | +| **Maintainer 2FA / hardware keys** | GitHub does not expose per-user 2FA to this repo | Org/account Settings (human attestation) | + +Record the date branch protection was enabled in an internal ops note. Do **not** +commit screenshots of GitHub Settings or 2FA enrollment. + +## What this repository can verify + +| Control | Verifiable in-tree? | Evidence | +|---------|---------------------|----------| +| Signed-commit policy SSOT | **Yes** | This page + ADR 0004 | +| CODEOWNERS file present | **Yes** | [`CODEOWNERS`](../../CODEOWNERS) | +| Recent `main` tip signature | Partial | `scripts/commit-signing-check.ps1` | +| Branch protection doc anchors | **Yes** | `scripts/branch-protection-check.ps1 -PolicyOnly` | +| GitHub branch protection live state | Partial (best-effort API) | `scripts/branch-protection-check.ps1` (no `-PolicyOnly`) | +| Org 2FA / hardware-key enrollment | **No** | Human attestation only | + +## Evidence checklist + +| Gate | Status | Evidence / prerequisite | +|------|--------|-------------------------| +| Source provenance policy documented | **done** | This page | +| Source provenance SelfCheck | **done** | `scripts/source-provenance-check.ps1 -SelfCheck` | +| Branch protection PolicyOnly hook | **done** | `scripts/branch-protection-check.ps1 -PolicyOnly` | +| CONTRIBUTING.md cross-link | **done** | [`CONTRIBUTING.md`](../../CONTRIBUTING.md) § Source provenance | +| CODEOWNERS present | **done** | [`CODEOWNERS`](../../CODEOWNERS) | +| GitHub **Require signed commits** live proof | **NOT_VERIFIABLE_IN_REPO** | Human org gate — enable in Settings; optional `gh api` via branch-protection-check without `-PolicyOnly` | +| Maintainer 2FA live proof | **NOT_VERIFIABLE_IN_REPO** | Human org gate — org/account 2FA attestation out-of-band | + +## SelfCheck (machine proof) + +Docs + path anchors only — no network, no GitHub API, no false org-gate claims: + +```powershell +pwsh ./scripts/source-provenance-check.ps1 -SelfCheck +``` + +The script asserts: + +- This page documents **signed commits**, **CODEOWNERS**, and **human org gates** +- Cross-links to `CONTRIBUTING.md`, `commit-signing.md`, and `branch-protection.md` +- The evidence checklist includes **NOT_VERIFIABLE_IN_REPO** human org rows +- `scripts/branch-protection-check.ps1` exposes `-PolicyOnly` for hermetic policy hooks + +## Related + +- [`branch-protection.md`](branch-protection.md) — PR + signed-commit machine verify +- [`commit-signing.md`](commit-signing.md) — contributor GPG/SSH setup + CI tip check +- [`docs/adr/0004-commit-signing-policy.md`](../adr/0004-commit-signing-policy.md) — decision record diff --git a/scripts/branch-protection-check.ps1 b/scripts/branch-protection-check.ps1 index 2e1a616f..e7edf6dc 100644 --- a/scripts/branch-protection-check.ps1 +++ b/scripts/branch-protection-check.ps1 @@ -4,10 +4,60 @@ param( [string]$Repo = "KooshaPari/SessionLedger", # When set, exit 1 only if the API succeeds and required controls are missing. # Without -Strict, missing token / 404 / insufficient scope always soft-exit 0. - [switch]$Strict + [switch]$Strict, + # Hermetic policy anchors only (C06 L59 source-provenance hook). No gh / no network. + [switch]$PolicyOnly ) $ErrorActionPreference = "Stop" +$RepoRoot = Split-Path -Parent $PSScriptRoot + +function Assert-SourceProvenancePolicy { + $sourceProvDocPath = Join-Path $RepoRoot "docs/ops/source-provenance.md" + $branchProtectionDocPath = Join-Path $RepoRoot "docs/ops/branch-protection.md" + $codeownersPath = Join-Path $RepoRoot "CODEOWNERS" + + foreach ($pair in @( + @{ Path = $sourceProvDocPath; Label = "source provenance policy doc" }, + @{ Path = $branchProtectionDocPath; Label = "branch protection doc" }, + @{ Path = $codeownersPath; Label = "CODEOWNERS" } + )) { + if (-not (Test-Path -LiteralPath $pair.Path -PathType Leaf)) { + throw "Missing $($pair.Label) at '$($pair.Path)'." + } + } + + $sourceProvDoc = Get-Content -LiteralPath $sourceProvDocPath -Raw + $branchProtectionDoc = Get-Content -LiteralPath $branchProtectionDocPath -Raw + + if ($sourceProvDoc -notmatch 'Require signed commits') { + throw "docs/ops/source-provenance.md must document Require signed commits." + } + if ($sourceProvDoc -notmatch 'scripts/branch-protection-check\.ps1') { + throw "docs/ops/source-provenance.md must reference scripts/branch-protection-check.ps1." + } + if ($sourceProvDoc -notmatch '-PolicyOnly') { + throw "docs/ops/source-provenance.md must document branch-protection-check.ps1 -PolicyOnly." + } + if ($sourceProvDoc -notmatch 'NOT_VERIFIABLE_IN_REPO') { + throw "docs/ops/source-provenance.md must include NOT_VERIFIABLE_IN_REPO human org rows." + } + if ($branchProtectionDoc -notmatch 'source-provenance\.md') { + throw "docs/ops/branch-protection.md must cross-link docs/ops/source-provenance.md." + } + if ($branchProtectionDoc -notmatch 'Require signed commits') { + throw "docs/ops/branch-protection.md must document Require signed commits." + } + + Write-Host "Source provenance policy OK (docs + CODEOWNERS anchors; no live Settings claim)." +} + +if ($PolicyOnly) { + Write-Host "Branch protection check (PolicyOnly — C06 L59 hermetic policy hook)" + Assert-SourceProvenancePolicy + Write-Host "PolicyOnly: skipping GitHub API branch protection query." + exit 0 +} function Write-Skip { param([string]$Reason) diff --git a/scripts/source-provenance-check.ps1 b/scripts/source-provenance-check.ps1 new file mode 100644 index 00000000..e4da5810 --- /dev/null +++ b/scripts/source-provenance-check.ps1 @@ -0,0 +1,153 @@ +<# +.SYNOPSIS + Machine-check the C06 L59 source provenance policy doc anchors. + +.DESCRIPTION + Verifies docs/ops/source-provenance.md documents signed commits, CODEOWNERS + review gates, human org gates (branch protection, maintainer 2FA), and + NOT_VERIFIABLE_IN_REPO evidence rows. Cross-checks CONTRIBUTING.md, + commit-signing.md, branch-protection.md, CODEOWNERS, and the branch-protection + PolicyOnly hook. + Hermetic: no network, no GitHub API, no false org-gate enforcement claims. + +.PARAMETER SelfCheck + Explicit docs/path smoke (CI unit proof). Same checks as the default path. + +.EXAMPLE + pwsh ./scripts/source-provenance-check.ps1 -SelfCheck +#> +[CmdletBinding()] +param( + [switch]$SelfCheck +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$docPath = Join-Path $repoRoot "docs/ops/source-provenance.md" +$commitSigningPath = Join-Path $repoRoot "docs/ops/commit-signing.md" +$branchProtectionDocPath = Join-Path $repoRoot "docs/ops/branch-protection.md" +$contributingPath = Join-Path $repoRoot "CONTRIBUTING.md" +$codeownersPath = Join-Path $repoRoot "CODEOWNERS" +$adrPath = Join-Path $repoRoot "docs/adr/0004-commit-signing-policy.md" +$branchProtectionScriptPath = Join-Path $repoRoot "scripts/branch-protection-check.ps1" +$selfPath = Join-Path $repoRoot "scripts/source-provenance-check.ps1" + +function Assert-File { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Label + ) + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "Missing $Label at '$Path'." + } +} + +function Write-Check { + param([string]$Label, [bool]$Ok) + $mark = if ($Ok) { "PASS" } else { "FAIL" } + Write-Host " [$mark] $Label" + return $Ok +} + +function Test-DocContains { + param( + [Parameter(Mandatory = $true)][string]$Doc, + [Parameter(Mandatory = $true)][string]$Needle, + [Parameter(Mandatory = $true)][string]$Label, + [string]$Context = "docs/ops/source-provenance.md" + ) + $ok = $Doc.Contains($Needle) + [void](Write-Check -Label $Label -Ok $ok) + if (-not $ok) { + throw "$Context missing required anchor: '$Needle'" + } +} + +Write-Host "Source provenance policy check (C06 L59)" +if ($SelfCheck) { + Write-Host "Mode: SelfCheck (docs + evidence paths; no network / no GitHub API)" +} + +Assert-File -Path $docPath -Label "source provenance policy doc" +Assert-File -Path $commitSigningPath -Label "commit signing doc" +Assert-File -Path $branchProtectionDocPath -Label "branch protection doc" +Assert-File -Path $contributingPath -Label "CONTRIBUTING.md" +Assert-File -Path $codeownersPath -Label "CODEOWNERS" +Assert-File -Path $adrPath -Label "ADR 0004" +Assert-File -Path $branchProtectionScriptPath -Label "branch protection check script" +Assert-File -Path $selfPath -Label "source provenance check script" + +$doc = Get-Content -LiteralPath $docPath -Raw +$commitSigning = Get-Content -LiteralPath $commitSigningPath -Raw +$branchProtectionDoc = Get-Content -LiteralPath $branchProtectionDocPath -Raw +$contributing = Get-Content -LiteralPath $contributingPath -Raw +$codeowners = Get-Content -LiteralPath $codeownersPath -Raw +$branchProtectionScript = Get-Content -LiteralPath $branchProtectionScriptPath -Raw + +Write-Host "Source provenance doc anchors:" +Test-DocContains -Doc $doc -Needle "Source code provenance policy" ` + -Label "doc heading" +Test-DocContains -Doc $doc -Needle "**C06 L59**" ` + -Label "C06 L59 status" +Test-DocContains -Doc $doc -Needle "## Policy layers" ` + -Label "policy layers heading" +Test-DocContains -Doc $doc -Needle "GPG / SSH" ` + -Label "signed commits policy" +Test-DocContains -Doc $doc -Needle "## CODEOWNERS review gates" ` + -Label "CODEOWNERS heading" +Test-DocContains -Doc $doc -Needle "## Human org gates (Settings)" ` + -Label "human org gates heading" +Test-DocContains -Doc $doc -Needle "cannot assert the toggle from checkout" ` + -Label "no false CODEOWNERS toggle claim" +Test-DocContains -Doc $doc -Needle "NOT_VERIFIABLE_IN_REPO" ` + -Label "NOT_VERIFIABLE_IN_REPO human rows" +Test-DocContains -Doc $doc -Needle "Human org gate" ` + -Label "human org gate label" +Test-DocContains -Doc $doc -Needle "scripts/source-provenance-check.ps1" ` + -Label "SelfCheck script reference" +Test-DocContains -Doc $doc -Needle "-SelfCheck" ` + -Label "SelfCheck invocation" +Test-DocContains -Doc $doc -Needle "Source provenance SelfCheck | **done**" ` + -Label "SelfCheck gate marked done" +Test-DocContains -Doc $doc -Needle "branch-protection-check.ps1 -PolicyOnly" ` + -Label "PolicyOnly hook reference" +Test-DocContains -Doc $doc -Needle "Branch protection PolicyOnly hook | **done**" ` + -Label "PolicyOnly gate marked done" + +Write-Host "CODEOWNERS anchors:" +if ($codeowners -notmatch '@\w') { + throw "CODEOWNERS must list at least one owner handle." +} +[void](Write-Check -Label "CODEOWNERS owner handle present" -Ok $true) + +Write-Host "Branch protection PolicyOnly hook:" +Test-DocContains -Doc $branchProtectionScript -Needle "PolicyOnly" ` + -Label "branch-protection-check.ps1 defines PolicyOnly" ` + -Context "scripts/branch-protection-check.ps1" + +Write-Host "Cross-link anchors:" +Test-DocContains -Doc $contributing -Needle "docs/ops/source-provenance.md" ` + -Label "CONTRIBUTING.md -> source-provenance" -Context "CONTRIBUTING.md" +Test-DocContains -Doc $contributing -Needle "source-provenance-check.ps1" ` + -Label "CONTRIBUTING.md SelfCheck script" -Context "CONTRIBUTING.md" +Test-DocContains -Doc $commitSigning -Needle "branch-protection-check.ps1" ` + -Label "commit-signing.md branch-protection script" -Context "docs/ops/commit-signing.md" +Test-DocContains -Doc $branchProtectionDoc -Needle "source-provenance.md" ` + -Label "branch-protection.md -> source-provenance" -Context "docs/ops/branch-protection.md" + +$summary = @" +## Source provenance policy SelfCheck + +SelfCheck passed: ``docs/ops/source-provenance.md`` policy anchors (signed commits, +CODEOWNERS, human org gates), NOT_VERIFIABLE_IN_REPO rows, and CONTRIBUTING.md +cross-links. Does not claim GitHub Settings enforcement from checkout. +"@ + +if ($env:GITHUB_STEP_SUMMARY) { + $summary | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 +} + +Write-Host "Source provenance SelfCheck passed (C06 L59 policy SSOT; NOT_VERIFIABLE_IN_REPO human org rows; no false Settings claim)." +exit 0