Merge pull request #52 from RI-SE/cursor/public-surface-truth #159
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| on: | |
| push: | |
| branches: [main, feature/**, feat/**] | |
| pull_request: | |
| jobs: | |
| required-check-drift: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Required-check context drift | |
| run: python3 scripts/check-required-contexts.py | |
| test: | |
| name: test (${{ matrix.os }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: "1.23.x" | |
| - name: Go test | |
| env: | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: go test ./... | |
| - name: Fuzz (short) | |
| if: runner.os != 'Windows' | |
| run: | | |
| go test ./internal/validate/ -fuzz=FuzzSafeJoin -fuzztime=5s | |
| go test ./internal/validate/ -fuzz=FuzzTextForbidRegex -fuzztime=5s | |
| go test ./internal/attest/ -fuzz=FuzzParseHPURLFragment -fuzztime=5s | |
| windows-smoke: | |
| runs-on: windows-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: "1.23.x" | |
| - name: Build windows exe | |
| run: | | |
| New-Item -ItemType Directory -Force -Path bin | Out-Null | |
| go build -o bin/curbpack.exe ./cmd/curbpack | |
| - name: Execute exe + spaced-path demo/init | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $bin = Join-Path $env:GITHUB_WORKSPACE "bin\curbpack.exe" | |
| & $bin doctor | |
| $space = Join-Path $env:TEMP "curbpack smoke $(Get-Random)" | |
| New-Item -ItemType Directory -Force -Path $space | Out-Null | |
| & $bin demo --out $space --keep | |
| $one = Join-Path $space "review-pack\buyer-onepager.html" | |
| if (-not (Test-Path $one)) { throw "missing onepager at $one" } | |
| $fix = Join-Path $env:TEMP "curb init $(Get-Random)" | |
| New-Item -ItemType Directory -Force -Path $fix | Out-Null | |
| Push-Location $fix | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| & $bin init --packs house-policy | |
| # Init stubs must stay red (scaffold overlap). Heal/init is not a greenlight. | |
| $checkOut = & $bin check 2>&1 | Out-String | |
| $checkCode = $LASTEXITCODE | |
| Write-Host $checkOut | |
| if ($checkCode -eq 0) { throw "check after house-policy init stubs must stay red (HOUSE-ANTI-PLACEHOLDER)" } | |
| if ($checkOut -notmatch 'HOUSE-ANTI-PLACEHOLDER') { throw "missing HOUSE-ANTI-PLACEHOLDER on red init check" } | |
| if ($checkOut -notmatch 'scaffold body overlap') { throw "missing scaffold body overlap on red init check" } | |
| $repairOut = & $bin doctor --repair 2>&1 | Out-String | |
| Write-Host $repairOut | |
| if ($LASTEXITCODE -ne 0) { throw "doctor --repair failed with exit $LASTEXITCODE" } | |
| if ($repairOut -notmatch 'repair:|Repair done|binary missing') { | |
| throw "doctor --repair did not exercise repair path (no expected status line)" | |
| } | |
| & $bin share --bundle | |
| if (-not (Test-Path "review-pack\evidence-bundle.html")) { throw "missing bundle" } | |
| Pop-Location | |
| exit 0 | |
| smoke: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: "1.23.x" | |
| - name: Build | |
| run: ./scripts/ci-build.sh | |
| - name: Doctor + demo sandbox | |
| run: | | |
| set -euo pipefail | |
| "$GITHUB_WORKSPACE/bin/curbpack" doctor | |
| DEMO=$(mktemp -d) | |
| "$GITHUB_WORKSPACE/bin/curbpack" demo --out "$DEMO" --keep | |
| test -f "$DEMO/review-pack/buyer-onepager.html" | |
| test -f "$DEMO/.curbpack.json" | |
| - name: Spaced-path demo/init smoke | |
| run: | | |
| set -euo pipefail | |
| SPACE=$(mktemp -d "/tmp/curbpack smoke.XXXXXX") | |
| "$GITHUB_WORKSPACE/bin/curbpack" demo --out "$SPACE" --keep | |
| test -f "$SPACE/review-pack/buyer-onepager.html" | |
| FIX=$(mktemp -d "/tmp/curb init.XXXXXX") | |
| cd "$FIX" | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| "$GITHUB_WORKSPACE/bin/curbpack" init --packs house-policy | |
| # Init stubs must stay red (scaffold overlap). Heal/init is not a greenlight. | |
| set +e | |
| CHECK_OUT=$("$GITHUB_WORKSPACE/bin/curbpack" check 2>&1) | |
| CHECK_CODE=$? | |
| set -e | |
| printf '%s\n' "$CHECK_OUT" | |
| test "$CHECK_CODE" -ne 0 | |
| printf '%s\n' "$CHECK_OUT" | grep -q 'HOUSE-ANTI-PLACEHOLDER' | |
| printf '%s\n' "$CHECK_OUT" | grep -q 'scaffold body overlap' | |
| # doctor --repair must fail the job on error (no || true) | |
| REPAIR_OUT=$("$GITHUB_WORKSPACE/bin/curbpack" doctor --repair 2>&1) | |
| echo "$REPAIR_OUT" | |
| echo "$REPAIR_OUT" | grep -Eqi 'repair:|Repair done|binary missing' | |
| - name: Offline smoke — CRA fixture | |
| run: | | |
| set -euo pipefail | |
| FIX=$(mktemp -d) | |
| cd "$FIX" | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| "$GITHUB_WORKSPACE/bin/curbpack" init --packs cra-baseline | |
| # Bind annex drafts to a resolvable product token (package.json name). | |
| # CRA-ANNEX-VII-RISK also require_tree_paths README.md (always present post-init/heal in product repos). | |
| printf '%s\n' '# contoso-gateway' > README.md | |
| printf '%s\n' '{"name":"contoso-gateway","version":"1.0.0","dependencies":{}}' > package.json | |
| # Fill annex stubs past placeholders / min sizes | |
| cat > docs/annex-vii/risk_assessment.md <<'EOF' | |
| # Risk Assessment | |
| ## Product Overview | |
| The contoso-gateway product forwards telemetry from clinical devices to a hospital EHR over mutually authenticated TLS. | |
| ## Identified Risks | |
| | Risk ID | Description | Severity | Mitigation | | |
| |---------|-------------|----------|------------| | |
| | R-001 | Credential stuffing on admin UI | High | MFA + lockout | | |
| ## Residual Risk Statement | |
| Residual risk is accepted by the product owner after mitigations above. | |
| EOF | |
| cat > docs/annex-vii/support_period.md <<'EOF' | |
| # Support Period | |
| ## End of Support | |
| Security updates for contoso-gateway are provided for five years from the general availability date of each major release. | |
| ## Rationale | |
| Aligned with expected clinical deployment lifetime and spare-parts availability. | |
| EOF | |
| cat > docs/annex-vii/user_manual_security.md <<'EOF' | |
| # User Manual — Security | |
| ## Secure Configuration | |
| Disable default accounts on contoso-gateway, enforce MFA, and restrict management interfaces to the hospital VLAN. | |
| ## Product Disposal | |
| Factory-reset the appliance, shred exported key material, and confirm cloud tenant deletion. | |
| EOF | |
| # CRA-ART14-PATH + CRA-ANTI-PLACEHOLDER: init stub lacks bind_repo_token | |
| # (contoso-gateway) and matches DefaultScaffoldBody. Same product-bound | |
| # prose as testdata/realish/cra-device/docs/incident/art14-path.md. | |
| mkdir -p docs/incident | |
| cat > docs/incident/art14-path.md <<'EOF' | |
| # Art 14 reporting path | |
| ## Reporting clock (CRA Art 14) | |
| For contoso-gateway, actively exploited or severe incidents are reported by the on-call owner using the in-repo rehearsal dated 2026-04-12. This is a file record for CRA Article 14 reporting (clock from 11 September 2026, including products already on the market). It is not a live Single Reporting Platform check and does not assert that EU Login works. | |
| ## Handling clock (not this file) | |
| Vulnerability handling and public security contact for contoso-gateway are documented separately. They sit on a later clock than Article 14 reporting and are not this rehearsal. | |
| ## Named owner | |
| Product security on-call for contoso-gateway owns the reporting path. Escalation is the engineering manager of record in SECURITY.md. | |
| ## Rehearsal dated artifact | |
| Last tabletop: 2026-04-12. Record: this file plus the incident mail template under docs/incident/ (in-repo). Not a live submission. | |
| EOF | |
| "$GITHUB_WORKSPACE/bin/curbpack" check | |
| "$GITHUB_WORKSPACE/bin/curbpack" prepare-release | |
| test -f review-pack/buyer-onepager.html | |
| test -f .github/curbpack/cache/latest_action_report.md | |
| - name: Offline smoke — house-policy fixture | |
| run: | | |
| set -euo pipefail | |
| FIX=$(mktemp -d) | |
| cd "$FIX" | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| "$GITHUB_WORKSPACE/bin/curbpack" init --packs house-policy --hooks --skill --ide | |
| test -x .git/hooks/pre-commit | |
| grep -q 'curbpack check' .git/hooks/pre-commit | |
| ! grep -q -- '--heal' .git/hooks/pre-commit | |
| # Fail-closed message (ASCII); -aF avoids locale/binary-file grep false negatives | |
| grep -aF 'refusing commit' .git/hooks/pre-commit | |
| # LF-only hooks | |
| ! grep -q $'\r' .git/hooks/pre-commit | |
| test -f .cursor/skills/curbpack/SKILL.md | |
| test -f .vscode/tasks.json | |
| # Init stubs must stay red (scaffold overlap). Heal/init is not a greenlight. | |
| set +e | |
| CHECK_OUT=$("$GITHUB_WORKSPACE/bin/curbpack" check 2>&1) | |
| CHECK_CODE=$? | |
| set -e | |
| printf '%s\n' "$CHECK_OUT" | |
| test "$CHECK_CODE" -ne 0 | |
| printf '%s\n' "$CHECK_OUT" | grep -q 'HOUSE-ANTI-PLACEHOLDER' | |
| printf '%s\n' "$CHECK_OUT" | grep -q 'scaffold body overlap' | |
| # bare curbpack defaults to check when inited — also honest-red on stubs | |
| set +e | |
| "$GITHUB_WORKSPACE/bin/curbpack" | |
| BARE_CODE=$? | |
| set -e | |
| test "$BARE_CODE" -ne 0 | |
| "$GITHUB_WORKSPACE/bin/curbpack" check --form-hints || true | |
| echo '{"name":"demo","dependencies":{"left-pad":"1.3.0"}}' > package.json | |
| "$GITHUB_WORKSPACE/bin/curbpack" prepare-release --allow-failing-gates | |
| test -f .github/curbpack/evidence/sbom.cdx.json | |
| python3 - <<'PY' | |
| import json | |
| d=json.load(open(".github/curbpack/evidence/sbom.cdx.json")) | |
| assert d["bomFormat"]=="CycloneDX" and d["specVersion"]=="1.5" | |
| v=json.load(open(".github/curbpack/evidence/vex-pending.json")) | |
| assert v["status"]=="draft_pending_attest" | |
| print("cyclonedx+vex ok") | |
| PY | |
| git add -A && git -c commit.gpgsign=false commit --no-verify -m "evidence" -q | |
| "$GITHUB_WORKSPACE/bin/curbpack" attest --allow-dirty | |
| test -f .github/curbpack/evidence/hpurl-pointer.json | |
| python3 - <<'PY' | |
| import json | |
| p=json.load(open(".github/curbpack/evidence/hpurl-pointer.json")) | |
| assert p.get("state_hash") and p["state_hash"] in p.get("hpurl","") | |
| print("hpurl pointer ok", p["state_hash"][:12]) | |
| PY | |
| # Hook dry-run: still fail-closed after attest (dump on miss) | |
| head -n 5 .git/hooks/pre-commit | |
| grep -aF 'refusing commit' .git/hooks/pre-commit || { | |
| echo "hook missing 'refusing commit' after attest:" >&2 | |
| wc -c .git/hooks/pre-commit >&2 | |
| cat -v .git/hooks/pre-commit >&2 | |
| exit 1 | |
| } | |
| H1=$(python3 -c 'import json;print(json.load(open(".github/curbpack/evidence/hpurl-pointer.json"))["state_hash"])') | |
| "$GITHUB_WORKSPACE/bin/curbpack" attest --allow-dirty >/dev/null | |
| H2=$(python3 -c 'import json;print(json.load(open(".github/curbpack/evidence/hpurl-pointer.json"))["state_hash"])') | |
| test "$H1" = "$H2" | |
| - name: Scan read-only smoke (uninitialized repo) | |
| run: | | |
| set -euo pipefail | |
| SCAN=$(mktemp -d) | |
| cd "$SCAN" | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| OUT=$("$GITHUB_WORKSPACE/bin/curbpack" scan 2>&1) | |
| printf '%s\n' "$OUT" | |
| printf '%s\n' "$OUT" | grep -q 'Art 14 reporting clock' | |
| printf '%s\n' "$OUT" | grep -q 'Packs: cra-baseline' | |
| printf '%s\n' "$OUT" | grep -q 'Read-only' | |
| test -z "$(git status --porcelain)" | |
| pin-guard: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Pin guard | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| PIN="v0.5.2" | |
| BASE="${{ github.event.pull_request.base.sha || github.event.before }}" | |
| HEAD="${{ github.sha }}" | |
| if [ -z "${BASE:-}" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then | |
| BASE="$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)" | |
| fi | |
| if ! git diff "$BASE" "$HEAD" -- . | grep -qE "[+-].*${PIN}"; then | |
| echo "pin unchanged — OK" | |
| exit 0 | |
| fi | |
| if git log "$BASE..$HEAD" --format=%B | grep -q 'Approve-Pin-Bump:'; then | |
| echo "pin bump approved via trailer — OK" | |
| exit 0 | |
| fi | |
| if gh issue list --label tabletop-evidence --state closed --limit 1 --json number --jq '.[0].number' 2>/dev/null | grep -qE '^[0-9]+$'; then | |
| echo "closed tabletop-evidence issue present — OK" | |
| exit 0 | |
| fi | |
| echo "pin literal changed without tabletop-evidence or Approve-Pin-Bump trailer" >&2 | |
| exit 1 | |
| redteam-pilot: | |
| # Stable check name for branch protection: merges to main require this green. | |
| name: redteam-pilot | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: "1.23.x" | |
| - name: Build | |
| run: ./scripts/ci-build.sh | |
| - name: Redteam pilot scoreboard (required) | |
| run: ./scripts/redteam-pilot.sh | |
| gauntlet: | |
| runs-on: ubuntu-latest | |
| needs: [test, redteam-pilot] | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version: "1.23.x" | |
| - name: Build | |
| run: ./scripts/ci-build.sh | |
| - name: Claim safety (docs + runtime) | |
| run: ./scripts/claim-safety.sh | |
| - name: Public links and social-card assets | |
| run: python3 scripts/check-public-assets.py | |
| - name: Release reference integrity | |
| run: ./scripts/release-ref-test.sh | |
| - name: Installer version tag validation | |
| run: ./scripts/install-version-test.sh | |
| - name: Manifest / release asset parity | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json, pathlib, re, sys | |
| root = pathlib.Path(".") | |
| man = json.loads((root/"scripts/install-manifest.json").read_text()) | |
| gate = json.loads((root/"scripts/release-gate.json").read_text()) | |
| assets = set(man["assets"]) | |
| assert "curbpack_windows_amd64.exe" in assets | |
| assert man["default_version"].startswith("v") | |
| assert gate.get("schema") == "curbpack-release-gate:1", gate | |
| assert gate["version"] == man["default_version"], ( | |
| f"manifest default_version {man['default_version']!r} must equal " | |
| f"release-gate version {gate['version']!r} before public pins advance" | |
| ) | |
| for k in ("assets_verified", "checksums_verified", "install_smoke_verified", "scan_write_free_verified"): | |
| assert gate.get(k) is True, f"release-gate {k} must be true" | |
| rel = (root/".github/workflows/release.yml").read_text() | |
| # release must build windows amd64 exe | |
| assert "windows/amd64" in rel or "windows_amd64" in rel, "release.yml missing windows amd64" | |
| assert "curbpack_windows_amd64.exe" in rel or 'windows/amd64' in rel | |
| sh = (root/"scripts/install.sh").read_text() | |
| assert "install-manifest.json" in sh or "default_version" in sh or "v0.5.2" in sh | |
| assert "cd /path/to/your/git/repo" in sh and "curbpack scan" in sh | |
| assert "curbpack demo" in sh | |
| ps1 = (root/"scripts/install.ps1").read_text() | |
| assert "curbpack_windows_amd64.exe" in ps1 | |
| assert "-Repair" in ps1 or "Repair" in ps1 | |
| assert "cd /path/to/your/git/repo" in ps1 and "curbpack scan" in ps1 | |
| # Action remains Linux/macOS only honesty (real assert — no or True) | |
| action = (root/"action.yml").read_text() | |
| assert "Linux/macOS" in action, "action.yml must state Linux/macOS runners" | |
| assert "not Windows runners" in action, "action.yml must state not Windows runners" | |
| print("manifest/release-gate/install parity ok", man["default_version"], sorted(assets)) | |
| PY | |
| - name: Heal smoke + baseline ratchet + dead-ends | |
| run: ./scripts/gauntlet-ratchet.sh | |
| - name: Install-from-release smoke (fail closed) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Pin to this tree's advertised default so advertise PRs (main still older) | |
| # and post-merge main-default smoke both assert the correct binary. | |
| export CURBPACK_VERSION="$(python3 -c 'import json; print(json.load(open("scripts/install-manifest.json"))["default_version"])')" | |
| ./scripts/release-smoke-install-scan.sh | |
| - name: Dead-end — corrupt pack env | |
| run: | | |
| set -euo pipefail | |
| BAD=$(mktemp -d) | |
| mkdir -p "$BAD/broken" | |
| echo '{not json' > "$BAD/broken/pack.json" | |
| FIX=$(mktemp -d) | |
| cd "$FIX" | |
| git init -q | |
| git config user.email "ci@curbpack.local" | |
| git config user.name "CI" | |
| git commit --allow-empty -m init -q | |
| set +e | |
| CURBPACK_PACKS_DIR="$BAD" "$GITHUB_WORKSPACE/bin/curbpack" init --packs broken | |
| code=$? | |
| set -e | |
| test "$code" -ne 0 | |
| - name: Chaos dead-ends | |
| run: ./scripts/chaos-deadends.sh "$GITHUB_WORKSPACE/bin/curbpack" |