From 4a98e407e60e6581748b3835e387a870cff55265 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:26:19 +0000 Subject: [PATCH] v1.91.181.0 Promote qualified CSO runtime catalog (#3343) * Promote qualified CSO runtime catalog * changelog: qualified CSO runtime catalog * test(cso): stop pinning the committed runtime catalog to empty * v1.91.181.0 Promote qualified CSO runtime catalog --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: garrytan --- CHANGELOG.md | 10 + VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- lib/cso/runtime-catalog.json | 347 +++++++++++++++++++++++++- package.json | 2 +- test/cso-audit-paths.test.ts | 3 +- test/cso-cli-lifecycle.test.ts | 13 +- test/cso-preparation-executor.test.ts | 6 +- test/cso-preparation.test.ts | 11 +- test/cso-runtime-promotion.test.ts | 5 +- test/cso-scanner-executor.test.ts | 4 +- test/helpers/cso-runtime-catalog.ts | 21 ++ 12 files changed, 393 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bf2582a..37ae50b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.91.181.0] - 2026-10-23 + +**/cso can run its contained runtime checks.** Before this release the runtime catalog qualified nothing, so /cso always fell back to static assessment. It now qualifies ten pinned images: Node 24.4.0, Bun 1.3.10, Python 3.13.4 with uv 0.8.0, Rails on Ruby 3.4.4, and PostgreSQL 17.2, each for linux/amd64 and linux/arm64. With Docker installed, comprehensive /cso boots the target app inside the matching image and tests findings and repairs against it. The images passed the private evaluator's release qualification (run-5 statements, re-delivered under Garry's approval). The ingress then checked them in qualify run 39674065531 at 514fbfd. + +### Added +- **`lib/cso/runtime-catalog.json`** is now catalog `cso-v3-39674065531-133cb360842d`, which replaces `cso-v3-build-reviewed-2026-09-10`. Promote run 39841502986 generated it from qualification run 39674065531's authenticated statements and attested it. GitHub Actions committed the attested bytes (`sha256:078e0bf6…`). Each runtime's image is pinned to a public `ghcr.io/garrytan/gstack/cso-staging/…@sha256` digest, and each entry records its source commit (133cb36), SBOM and provenance digests, and requalification triggers (`preparationSha256` `c721b585…`). + +### For contributors +- **The /cso free tests no longer assume the committed runtime catalog is empty.** Six tests pinned `runtimes: []` and broke as soon as a promotion landed. Tests that need an unqualified catalog now build one with `unqualifiedRuntimeCatalog()`. The replay tests expect whichever fail-closed rejection the committed catalog produces, so the suite passes with the catalog both before and after a promotion. + ## [1.91.180.0] - 2026-10-22 **The /cso docs say what the qualified catalogs now allow.** AGENTS.md, the README and `docs/cso-release-qualification.md` still said that no qualified runtime or scanner image existed, so /cso ran static assessment only. The scanner catalog has been qualified since #3042. The runtime catalog becomes qualified with the runtime promotion PR that ships alongside this one. diff --git a/VERSION b/VERSION index e59dce263..1662717f2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.180.0 +1.91.181.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index fc5723e69..14e8a9dc3 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.180.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.181.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/lib/cso/runtime-catalog.json b/lib/cso/runtime-catalog.json index b80298cfc..b9adbc692 100644 --- a/lib/cso/runtime-catalog.json +++ b/lib/cso/runtime-catalog.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "revision": "cso-v3-build-reviewed-2026-09-10", - "previousRevision": null, + "revision": "cso-v3-39674065531-133cb360842d", + "previousRevision": "cso-v3-build-reviewed-2026-09-10", "helperAbi": 3, "buildRevision": "cso-runtime-inputs-2026-09-10", "profiles": [ @@ -10,7 +10,11 @@ "stack": "node", "platform": "linux/amd64", "state": "build_reviewed", - "versions": { "node": "24.4.0", "npm": "11.4.2", "cso-preparation": "1.0.0" }, + "versions": { + "node": "24.4.0", + "npm": "11.4.2", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -18,7 +22,11 @@ "stack": "node", "platform": "linux/arm64", "state": "build_reviewed", - "versions": { "node": "24.4.0", "npm": "11.4.2", "cso-preparation": "1.0.0" }, + "versions": { + "node": "24.4.0", + "npm": "11.4.2", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -26,7 +34,10 @@ "stack": "bun", "platform": "linux/amd64", "state": "build_reviewed", - "versions": { "bun": "1.3.10", "cso-preparation": "1.0.0" }, + "versions": { + "bun": "1.3.10", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -34,7 +45,10 @@ "stack": "bun", "platform": "linux/arm64", "state": "build_reviewed", - "versions": { "bun": "1.3.10", "cso-preparation": "1.0.0" }, + "versions": { + "bun": "1.3.10", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -42,7 +56,11 @@ "stack": "python", "platform": "linux/amd64", "state": "build_reviewed", - "versions": { "python": "3.13.4", "uv": "0.8.0", "cso-preparation": "1.0.0" }, + "versions": { + "python": "3.13.4", + "uv": "0.8.0", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -50,7 +68,11 @@ "stack": "python", "platform": "linux/arm64", "state": "build_reviewed", - "versions": { "python": "3.13.4", "uv": "0.8.0", "cso-preparation": "1.0.0" }, + "versions": { + "python": "3.13.4", + "uv": "0.8.0", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -58,7 +80,11 @@ "stack": "rails", "platform": "linux/amd64", "state": "build_reviewed", - "versions": { "ruby": "3.4.4", "bundler": "2.6.7", "cso-preparation": "1.0.0" }, + "versions": { + "ruby": "3.4.4", + "bundler": "2.6.7", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -66,7 +92,11 @@ "stack": "rails", "platform": "linux/arm64", "state": "build_reviewed", - "versions": { "ruby": "3.4.4", "bundler": "2.6.7", "cso-preparation": "1.0.0" }, + "versions": { + "ruby": "3.4.4", + "bundler": "2.6.7", + "cso-preparation": "1.0.0" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -74,7 +104,9 @@ "stack": "postgresql", "platform": "linux/amd64", "state": "build_reviewed", - "versions": { "postgresql": "17.2" }, + "versions": { + "postgresql": "17.2" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" }, { @@ -82,9 +114,298 @@ "stack": "postgresql", "platform": "linux/arm64", "state": "build_reviewed", - "versions": { "postgresql": "17.2" }, + "versions": { + "postgresql": "17.2" + }, "reviewedAt": "2026-09-10T00:00:00.000Z" } ], - "runtimes": [] + "promotion": { + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "evidenceDigest": "sha256:3e7b150d2baa737c49743a5ca04c126a1a03e40e3f761c3820076949d8d886df", + "qualificationEvidenceDigest": "sha256:0ddc6d6876ae8b6c630faf576e8fbbb95323bd193998e37c4a83e42dafe1bc9c", + "requalification": { + "helperAbi": 3, + "isolationPolicyHash": "2ae25762ae556094cff083d1e9f0eb4884f22f5918920609c7b05a57c8e208fc", + "preparationSha256": "c721b5851c661dda35218994f714043472ad09bc339c4c3b5c7cb6961f69b898", + "buildInputsRevision": "cso-runtime-inputs-2026-09-10" + } + }, + "runtimes": [ + { + "id": "node-24.4.0-amd64", + "stack": "node", + "platform": "linux/amd64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/node-amd64@sha256:f7a52b8916cc78a846650c52b7847839b475fa6e81c5f3a7fd9b57e6b2915444", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "cso-preparation": "1.0.0", + "node": "24.4.0", + "npm": "11.4.2" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:1f33ea574738bf16ae2433f5a03a731654c2a808404e1547a6a598f3e955a973", + "provenanceDigest": "sha256:9e7d120e174eba06610ba853c8f89837f3e9d29547e9b63fa961531edee43239", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "node-24.4.0-arm64", + "stack": "node", + "platform": "linux/arm64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/node-arm64@sha256:a21c1aae3279793f546c09106528e3e05189e90f46c74a1dfd4bd294cd37f7ba", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "cso-preparation": "1.0.0", + "node": "24.4.0", + "npm": "11.4.2" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:6e13ed8415018576ab6cca8d962f6f3ea547c7c26ec01d645aee2910f9a2fa28", + "provenanceDigest": "sha256:39e5fa6d84ca0b22206d3c2c37827df490e2e21122f6a25dc03d117ce1e6e238", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "bun-1.3.10-amd64", + "stack": "bun", + "platform": "linux/amd64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/bun-amd64@sha256:49809f7c933954c2468c6dda731d1cef0d38ad1eb6b29d341f715fe09073aa89", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "bun": "1.3.10", + "cso-preparation": "1.0.0" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:b20227dfa4d520ebc7c0a448535e1bf0defcfc8aa5ffaca01157db246f5573a5", + "provenanceDigest": "sha256:bb111e4982d61b72d6e564d7a915440d6c61c0cb8e081810bfef33cefdc40323", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "bun-1.3.10-arm64", + "stack": "bun", + "platform": "linux/arm64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/bun-arm64@sha256:3194bbf094cdd0ee8a92b43f782f3ab1cad195dafc3eb9dcea66e4c2d2795733", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "bun": "1.3.10", + "cso-preparation": "1.0.0" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:f4e3a814c9121d98016bf19c590b4821741aec3904917e14bbf7f40d26a03253", + "provenanceDigest": "sha256:003a5e1a85412907b9c7ce9198ebe6a09bda8d457ecff60ae6ccc91d78ad0775", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "python-3.13.4-uv-0.8.0-amd64", + "stack": "python", + "platform": "linux/amd64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/python-amd64@sha256:93d17316a0bd53721904ef1d4b3259421248f1ee73b8ace14f4e323090f6e107", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "cso-preparation": "1.0.0", + "python": "3.13.4", + "uv": "0.8.0" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:0ae2eef050f0d4e68b4702fe29e2e8e325f4fa83500d9accc86f10a4825de947", + "provenanceDigest": "sha256:47b91e8706c764b4b1ca82dc96a90b131cf15882fca41b9fa2e45824bf2a5699", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "python-3.13.4-uv-0.8.0-arm64", + "stack": "python", + "platform": "linux/arm64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/python-arm64@sha256:69fbc13c7395c0f10fd02ebb648a41d6bcd20b801a8369a4073433a64720eafb", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "cso-preparation": "1.0.0", + "python": "3.13.4", + "uv": "0.8.0" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:c7da1793dda06b71b7705dbb1b4ca7f44d994a843aacea7e7b9df3210df4f29c", + "provenanceDigest": "sha256:5cb118f89b3422d1e4e009f0e004ea27e39f7b19d5a5d7090513da5b2c1f7abe", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "rails-ruby-3.4.4-amd64", + "stack": "rails", + "platform": "linux/amd64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/rails-amd64@sha256:11e35903d9d53a9965fd5cb03d898275db41512b152443a12c433a36e6e1ee69", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "bundler": "2.6.7", + "cso-preparation": "1.0.0", + "ruby": "3.4.4" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:74da76f47df2ba2f2bee0b829fb4e1c220dc859c211e5c51d2e17e2b6d3c9cb2", + "provenanceDigest": "sha256:183ec989bc6a22f36348177a46cb81aea7156cb6625af82b2f6d2672eade768c", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "rails-ruby-3.4.4-arm64", + "stack": "rails", + "platform": "linux/arm64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/rails-arm64@sha256:2210c1aec0a8226cd2aafa9a6238f95fc07a2c840129e85aff6d70d6cec6ddce", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "bundler": "2.6.7", + "cso-preparation": "1.0.0", + "ruby": "3.4.4" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "application", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:3a115e5a520e0baf0828600f03bcd068709e8a48fa6dd1b11e677c9ef9a1c9d9", + "provenanceDigest": "sha256:6e64dd8ccec1dc673baff97364bc4d7264580d351cee0513671904d870ce0525", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "positiveNegativeAssertionsPassed": true, + "heldOutRepairPassed": true + } + }, + { + "id": "postgresql-17.2-amd64", + "stack": "postgresql", + "platform": "linux/amd64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/postgresql-amd64@sha256:006442609a277af34656366320866e3d6eb26e4c9241e2313c4856519628eab4", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "postgresql": "17.2" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "postgresql", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:5b7699536fdeb0f473eee0952e4e9f1b37050c099b9b5076eb1781d854977d6f", + "provenanceDigest": "sha256:337bb19b9aa7c6be3f6658f8f0ec4b84238ba69f7be006184f129b44e76b9b71", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "multiDatabasePassed": true, + "readinessPassed": true + } + }, + { + "id": "postgresql-17.2-arm64", + "stack": "postgresql", + "platform": "linux/arm64", + "state": "qualified", + "image": "ghcr.io/garrytan/gstack/cso-staging/postgresql-arm64@sha256:9df877dabc0ea9d05beb7947c10dc3000617c1e5162e95b83dd9618ad3d16f7a", + "entrypoint": "/opt/cso/entrypoint", + "helperAbi": 3, + "versions": { + "postgresql": "17.2" + }, + "policyVersion": "cso-isolation-v1", + "qualifiedAt": "2026-10-21T20:57:28.000Z", + "qualification": { + "kind": "postgresql", + "sourceCommit": "133cb360842d0a7cf88fc1d10dcb624f2b7b841f", + "workflow": "https://github.com/garrytan/gstack/actions/runs/39674065531", + "sbomDigest": "sha256:2b319e83d0b74def00dea625e24c886a71f7fea828327eb6cb4dfd8f7e777aa8", + "provenanceDigest": "sha256:cf27a88b3b1351aec9182e2b3509bd7e83b6046d1e9f1ea8d46054d30acbeeaf", + "verifiedProvenance": true, + "containmentPassed": true, + "coldStartPassed": true, + "multiDatabasePassed": true, + "readinessPassed": true + } + } + ] } diff --git a/package.json b/package.json index 727b2b972..bdf731bc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.180", + "version": "1.91.181", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/cso-audit-paths.test.ts b/test/cso-audit-paths.test.ts index c4636487c..33dba7464 100644 --- a/test/cso-audit-paths.test.ts +++ b/test/cso-audit-paths.test.ts @@ -11,6 +11,7 @@ import { newRun, readJson, saveReport } from '../lib/cso/state'; import { MAX_OUTPUT, canonical, sha256, type VerificationObservation } from '../lib/cso/contracts'; import { ISOLATION_POLICY_HASH } from '../lib/cso/docker'; import { canonicalStartPlan, canonicalTestPlan, patchHash, verifyRepair } from '../lib/cso/verification'; +import { replayRuntimeRejection } from './helpers/cso-runtime-catalog'; const ROOT = path.resolve(import.meta.dir, '..'); const launcher = path.join(ROOT, 'bin', process.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher'); @@ -342,6 +343,6 @@ describe('CSO --paths audit scope', () => { const supplied = cso(repo, ['replay', verified.bundle.id, '--source', repo]); expect(supplied.status).not.toBe(0); expect(supplied.stderr).not.toContain('does not match the bundle input hashes'); - expect(supplied.stderr).toContain('MISSING_QUALIFIED_RUNTIME'); + expect(supplied.stderr).toContain(replayRuntimeRejection(request.runtimeProfile, `linux/${arch}`).message); }, 60_000); }); diff --git a/test/cso-cli-lifecycle.test.ts b/test/cso-cli-lifecycle.test.ts index d3d502f34..8b54b0d66 100644 --- a/test/cso-cli-lifecycle.test.ts +++ b/test/cso-cli-lifecycle.test.ts @@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process'; import { canonical, sha256, type VerificationObservation } from '../lib/cso/contracts'; import { ISOLATION_POLICY_HASH } from '../lib/cso/docker'; import { saveReport, withLock } from '../lib/cso/state'; +import { replayRuntimeRejection } from './helpers/cso-runtime-catalog'; import { canonicalStartPlan, canonicalTestPlan, patchHash, verifyRepair } from '../lib/cso/verification'; const ROOT=path.resolve(import.meta.dir,'..'),launcher=path.join(ROOT,'bin',process.platform==='win32'?'gstack-cso-launcher.exe':'gstack-cso-launcher'); @@ -64,23 +65,23 @@ describe('CSO interrupted-run and replay commands',()=>{ const finding={title:'Unprotected record read',rootCause:'Record lookup omits the caller authorization predicate',location:{path:'app.js',line:1,symbol:'recordLookup'},advisoryIds:[],severity:'high',confidence:'high',confidenceRationale:'The caller-to-record trace directly establishes the missing authorization predicate',evidence:'supported',attackerControl:'Authenticated caller chooses the record identifier',impact:'Another tenant record is returned',scenario:'A tenant supplies another tenant record identifier and receives the record',trace:['GET /records/:id','recordLookup','record response'],references:['app.js:1','OWASP API1:2023'],recommendation:'Bind the lookup to the authenticated caller before returning the record',challenge:{reviewer:'independent-reviewer',independent:true,mode:'independent_agent',callers:'Authenticated route forwards the record identifier',controls:'Authentication exists without record authorization',counterevidence:'Opaque identifiers reduce guessing but do not authorize known identifiers',conclusion:'The record lookup lacks a caller authorization predicate'}}; expect(command(['submit',run.runId,writeInput('replay-finding.json',{findings:[finding]})]).status).toBe(0);const reportPath=path.join(dir,'report.json'),report=JSON.parse(fs.readFileSync(reportPath,'utf8')),findingId=report.findings[0].id,start=canonicalStartPlan(snapshot,'node',3456),tests=canonicalTestPlan(snapshot,'node'); const request:any={findingId,runtimeProfile:`node-24.4.0-${process.arch==='arm64'?'arm64':'amd64'}`,port:3456,start:start.command,legitimate:[{name:'legitimate control',path:'/control',method:'GET',expected:{status:200,includes:'CONTROL_OK'}}],security:{name:'unauthorized record is denied',path:'/records/other',method:'GET',expected:{status:403,includes:'DENIED'},vulnerable:{status:200,includes:'RECORD'}},existingTests:tests.commands,fixtures:{},boundaryFiles:['app.js'],testFiles:tests.files,changes:[{path:'app.js',beforeSha256:sha256(fs.readFileSync(path.join(snapshot,'app.js'))),after:'module.exports = "fixed"\n',effect:'source'}],review:{reviewer:'independent-reviewer',independent:true,rootCauseRepaired:true,featurePreserved:true,boundaryMocks:false,rationale:'The caller predicate is added while the legitimate control and existing test remain unchanged.',reviewedPatchHash:''}};request.review.reviewedPatchHash=patchHash(request); - const platform=process.arch==='arm64'?'linux/arm64':'linux/amd64',runtime:any={id:request.runtimeProfile,stack:'node',platform,image:`ghcr.io/garrytan/gstack/cso-staging/node-${process.arch==='arm64'?'arm64':'amd64'}@sha256:${'b'.repeat(64)}`},executor={observe:async(_source:string,phase:'before'|'after',received:any,_runtime:any,_verifier:any,_work:string,_control:string,_execution:any,evidence:any,witness:any)=>{const observation:VerificationObservation={booted:true,legitimate:true,security:phase==='before'?'intended_failure':'pass',existingTests:false,output:`external ${phase} assertions passed`,inputHash:''},receipt=await witness.attest(observation,[{command:received.existingTests[0],code:0,output:tap,minimumPassingTests:evidence.minimumPassingTests[0]}]);return{observation:{...observation,existingTests:receipt.diagnosticTestsPassed,inputHash:witness.binding.harnessHash},witness:receipt};}}; + const platform=process.arch==='arm64'?'linux/arm64':'linux/amd64',rejection=replayRuntimeRejection(request.runtimeProfile,platform),runtime:any={id:request.runtimeProfile,stack:'node',platform,image:`ghcr.io/garrytan/gstack/cso-staging/node-${process.arch==='arm64'?'arm64':'amd64'}@sha256:${'b'.repeat(64)}`},executor={observe:async(_source:string,phase:'before'|'after',received:any,_runtime:any,_verifier:any,_work:string,_control:string,_execution:any,evidence:any,witness:any)=>{const observation:VerificationObservation={booted:true,legitimate:true,security:phase==='before'?'intended_failure':'pass',existingTests:false,output:`external ${phase} assertions passed`,inputHash:''},receipt=await witness.attest(observation,[{command:received.existingTests[0],code:0,output:tap,minimumPassingTests:evidence.minimumPassingTests[0]}]);return{observation:{...observation,existingTests:receipt.diagnosticTestsPassed,inputHash:witness.binding.harnessHash},witness:receipt};}}; const verified=await verifyRepair({runId:run.runId,runDir:dir,manifest,rawRequest:request,runtime,verifier:runtime,policyHash:ISOLATION_POLICY_HASH,auditPolicyHash:sha256(canonical(report.policy)),archives:[],executor}); report.findings[0].reproduction='reproduced';report.findings[0].repair='runtime_tested';report.findings[0].verificationId=verified.bundle.id;report.findings[0].verificationAssurance={assertions:'authenticated_out_of_process',testCompletion:'self_reported',review:'self_attested'};saveReport(dir,report); const leased=withLock(dir,()=>command(['replay',verified.bundle.id])) as ReturnType;expect(leased.status).not.toBe(0);expect(leased.stderr).toContain('INSUFFICIENT_CAPACITY');expect(leased.stderr).toContain('Another helper is updating this run'); - const retained=command(['replay',verified.bundle.id]);expect(retained.status).not.toBe(0);expect(retained.stderr).toContain('PREREQUISITE');expect(retained.stderr).toContain('MISSING_QUALIFIED_RUNTIME'); + const retained=command(['replay',verified.bundle.id]);expect(retained.status).not.toBe(0);expect(retained.stderr).toContain(rejection.code);expect(retained.stderr).toContain(rejection.message); const manifestPath=path.join(dir,'snapshot.json'),expiredManifest={...manifest,expiresAt:new Date(Date.now()-1_000).toISOString()};fs.writeFileSync(manifestPath,`${JSON.stringify(expiredManifest,null,2)}\n`); const expiredPresent=command(['replay',verified.bundle.id]);expect(expiredPresent.status).not.toBe(0);expect(expiredPresent.stderr).toContain('MISSING_INPUT');expect(expiredPresent.stderr).toContain('Retained source expired'); - const suppliedExpired=command(['replay',verified.bundle.id,'--source',repo]);expect(suppliedExpired.status).not.toBe(0);expect(suppliedExpired.stderr).toContain('PREREQUISITE');expect(suppliedExpired.stderr).toContain('MISSING_QUALIFIED_RUNTIME');expect(suppliedExpired.stderr).not.toContain('Retained source expired'); + const suppliedExpired=command(['replay',verified.bundle.id,'--source',repo]);expect(suppliedExpired.status).not.toBe(0);expect(suppliedExpired.stderr).toContain(rejection.code);expect(suppliedExpired.stderr).toContain(rejection.message);expect(suppliedExpired.stderr).not.toContain('Retained source expired'); const noncanonicalExpiry=new Date(Date.now()+86400_000).toISOString().replace(/\.\d{3}Z$/,'Z');fs.writeFileSync(manifestPath,`${JSON.stringify({...manifest,expiresAt:noncanonicalExpiry},null,2)}\n`); - const invalidExpiry=command(['replay',verified.bundle.id,'--source',repo]);expect(invalidExpiry.status).not.toBe(0);expect(invalidExpiry.stderr).toContain('INCOMPATIBLE_INPUT');expect(invalidExpiry.stderr).toContain('Retained snapshot expiry is invalid');expect(invalidExpiry.stderr).not.toContain('MISSING_QUALIFIED_RUNTIME'); + const invalidExpiry=command(['replay',verified.bundle.id,'--source',repo]);expect(invalidExpiry.status).not.toBe(0);expect(invalidExpiry.stderr).toContain('INCOMPATIBLE_INPUT');expect(invalidExpiry.stderr).toContain('Retained snapshot expiry is invalid');expect(invalidExpiry.stderr).not.toContain(rejection.message); fs.writeFileSync(manifestPath,`${JSON.stringify({...manifest,expiresAt:new Date(Date.now()+86400_000).toISOString()},null,2)}\n`);fs.writeFileSync(path.join(snapshot,'app.js'),'module.exports = "tampered"\n'); - const corruptRetained=command(['replay',verified.bundle.id,'--source',repo]);expect(corruptRetained.status).not.toBe(0);expect(corruptRetained.stderr).toContain('INCOMPATIBLE_INPUT');expect(corruptRetained.stderr).toContain('Retained snapshot changed');expect(corruptRetained.stderr).not.toContain('MISSING_QUALIFIED_RUNTIME'); + const corruptRetained=command(['replay',verified.bundle.id,'--source',repo]);expect(corruptRetained.status).not.toBe(0);expect(corruptRetained.stderr).toContain('INCOMPATIBLE_INPUT');expect(corruptRetained.stderr).toContain('Retained snapshot changed');expect(corruptRetained.stderr).not.toContain(rejection.message); fs.rmSync(snapshot,{recursive:true,force:true});fs.rmSync(path.join(dir,'readable'),{recursive:true,force:true}); const expired=command(['replay',verified.bundle.id]);expect(expired.status).not.toBe(0);expect(expired.stderr).toContain('MISSING_INPUT');expect(expired.stderr).toContain('Retained source expired'); fs.writeFileSync(path.join(repo,'app.js'),'module.exports = "changed"\n');const changed=command(['replay',verified.bundle.id,'--source',repo]);expect(changed.status).not.toBe(0);expect(changed.stderr).toContain('INCOMPATIBLE_INPUT');expect(changed.stderr).toContain('does not match the bundle input hashes'); - fs.writeFileSync(path.join(repo,'app.js'),'module.exports = "vulnerable"\n');const supplied=command(['replay',verified.bundle.id,'--source',repo]);expect(supplied.status).not.toBe(0);expect(supplied.stderr).toContain('PREREQUISITE');expect(supplied.stderr).toContain('MISSING_QUALIFIED_RUNTIME');expect(supplied.stderr).not.toContain('does not match the bundle input hashes'); + fs.writeFileSync(path.join(repo,'app.js'),'module.exports = "vulnerable"\n');const supplied=command(['replay',verified.bundle.id,'--source',repo]);expect(supplied.status).not.toBe(0);expect(supplied.stderr).toContain(rejection.code);expect(supplied.stderr).toContain(rejection.message);expect(supplied.stderr).not.toContain('does not match the bundle input hashes'); },30_000); }); diff --git a/test/cso-preparation-executor.test.ts b/test/cso-preparation-executor.test.ts index 5aa47d25e..5123df45b 100644 --- a/test/cso-preparation-executor.test.ts +++ b/test/cso-preparation-executor.test.ts @@ -13,7 +13,7 @@ import { type PreparationSandboxRunner, type OfflinePreparationRequest, preparedIdentityEntry } from '../lib/cso/preparation-executor'; import { CSO_HELPER_ABI } from '../lib/cso/runtime-catalog'; import { DockerPreparationSandboxRunner, isBlockedRegistryAddress, RegistryEgressBroker } from '../lib/cso/preparation-docker'; -import { completeRuntimeCatalogFixture, qualifiedRuntimeFixture } from './helpers/cso-runtime-catalog'; +import { completeRuntimeCatalogFixture, qualifiedRuntimeFixture, unqualifiedRuntimeCatalog } from './helpers/cso-runtime-catalog'; const roots: string[] = []; const digest = 'a'.repeat(64); @@ -138,9 +138,9 @@ function errorCode(error: unknown): string | undefined { return error instanceof afterEach(() => { for (const value of roots.splice(0)) fs.rmSync(value, { recursive: true, force: true }); }); describe('CSO constrained dependency preparation executor', () => { - test('the unqualified default catalog and forged admissions fail closed', async () => { + test('an unqualified catalog and forged admissions fail closed', async () => { const source = snapshot('node'), plan = inspectPreparation(source, 'node'), runner = new FakeRunner(), cache = cacheFixture(); - expect(() => admitPreparationRuntime({ plan, platform: 'linux/amd64' })).toThrow('MISSING_QUALIFIED_RUNTIME'); + expect(() => admitPreparationRuntime({ plan, platform: 'linux/amd64', catalog: unqualifiedRuntimeCatalog() })).toThrow('MISSING_QUALIFIED_RUNTIME'); const executor = new PreparationExecutor({ cache, runner }); try { await executor.acquire({ plan, admission: { schemaVersion: 1, catalogRevision: 'forged', runtime: runtime('node') }, diff --git a/test/cso-preparation.test.ts b/test/cso-preparation.test.ts index f9ca4271a..6addab47f 100644 --- a/test/cso-preparation.test.ts +++ b/test/cso-preparation.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { inspectPreparation, railsTestConfiguration } from '../lib/cso/preparation'; import { assertRuntimeCompatible, RUNTIME_CATALOG, rollbackCatalog, selectRuntime, validateRuntimeCatalog } from '../lib/cso/runtime-catalog'; -import { completeRuntimeCatalogFixture, qualifiedRuntimeFixture } from './helpers/cso-runtime-catalog'; +import { completeRuntimeCatalogFixture, qualifiedRuntimeFixture, unqualifiedRuntimeCatalog } from './helpers/cso-runtime-catalog'; const roots: string[] = []; const hash = 'a'.repeat(64); @@ -231,10 +231,13 @@ describe('CSO inert Rails preparation', () => { describe('CSO runtime catalog admission', () => { const runtime = () => qualifiedRuntimeFixture('node'); const catalog = () => completeRuntimeCatalogFixture('test-1'); - test('default catalog cannot execute unqualified images', () => { + test('default catalog admits only digest-pinned reviewed profiles and unqualified profiles cannot execute', () => { expect(RUNTIME_CATALOG.profiles).toHaveLength(10); - expect(RUNTIME_CATALOG.runtimes).toEqual([]); - expect(() => selectRuntime('node', 'linux/amd64')).toThrow('Reviewed build profile node-24.4.0-amd64 is awaiting a qualified image promotion'); + for (const runtime of RUNTIME_CATALOG.runtimes) { + expect(RUNTIME_CATALOG.profiles.filter(profile => profile.id === runtime.id && profile.stack === runtime.stack && profile.platform === runtime.platform)).toHaveLength(1); + expect(runtime.image).toMatch(/@sha256:[a-f0-9]{64}$/); + } + expect(() => selectRuntime('node', 'linux/amd64', unqualifiedRuntimeCatalog())).toThrow('Reviewed build profile node-24.4.0-amd64 is awaiting a qualified image promotion'); }); test('selects exact qualified digest and platform', () => { expect(selectRuntime('node', 'linux/amd64', catalog()).image).toContain('@sha256:'); diff --git a/test/cso-runtime-promotion.test.ts b/test/cso-runtime-promotion.test.ts index 74383245d..212e34a66 100644 --- a/test/cso-runtime-promotion.test.ts +++ b/test/cso-runtime-promotion.test.ts @@ -69,7 +69,10 @@ describe('CSO runtime catalog promotion', () => { const matrix = imageBuildMatrix(buildInputs); expect(matrix.include).toHaveLength(10); expect(committedCatalog.profiles).toHaveLength(10); - expect(committedCatalog.runtimes).toEqual([]); + for (const runtime of committedCatalog.runtimes) { + expect(committedCatalog.profiles.filter(profile => profile.id === runtime.id && profile.stack === runtime.stack && profile.platform === runtime.platform)).toHaveLength(1); + expect(runtime.image).toMatch(/@sha256:[a-f0-9]{64}$/); + } expect(committedCatalog.buildRevision).toBe(buildInputs.revision); expect(matrix.include.find(row => row.stack === 'bun')!.versions).not.toHaveProperty('node'); validateRuntimeCatalog(committedCatalog); diff --git a/test/cso-scanner-executor.test.ts b/test/cso-scanner-executor.test.ts index 6024462d8..bae145946 100644 --- a/test/cso-scanner-executor.test.ts +++ b/test/cso-scanner-executor.test.ts @@ -10,7 +10,7 @@ import { total } from '../lib/cso/admission'; import { SCANNER_IDS, ScannerExecution, ScannerId, scannerPlans } from '../lib/cso/scanners'; import { type PreparationSandboxRunner } from '../lib/cso/preparation-executor'; import { canonicalStartPlan } from '../lib/cso/verification'; -import { completeRuntimeCatalogFixture } from './helpers/cso-runtime-catalog'; +import { completeRuntimeCatalogFixture, unqualifiedRuntimeCatalog } from './helpers/cso-runtime-catalog'; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); }); @@ -186,7 +186,7 @@ describe('Schemathesis application admission', () => { expect(calls).toBe(0); expect(out.outcome.gaps[0].message).toContain('boundary is missing or transformed'); }); test('no runtime profile means a prerequisite, not a scanner success', async () => { - const out = await executeScanner(input('schemathesis'), { catalog: catalog('schemathesis') }); + const out = await executeScanner(input('schemathesis'), { catalog: catalog('schemathesis'), runtimes: unqualifiedRuntimeCatalog() }); expect(out.outcome.status).toBe('not_assessed'); expect(out.outcome.gaps[0].message).toContain('runtime is unavailable'); }); test.each(['remote-ref', 'server', 'hook', 'callback', 'operation', 'unbounded', 'source-command'])('rejects hostile or ambiguous %s harness input', kind => { diff --git a/test/helpers/cso-runtime-catalog.ts b/test/helpers/cso-runtime-catalog.ts index f004896b7..1e55877d1 100644 --- a/test/helpers/cso-runtime-catalog.ts +++ b/test/helpers/cso-runtime-catalog.ts @@ -5,6 +5,8 @@ import { canonical, sha256 } from '../../lib/cso/contracts'; import { ISOLATION_POLICY_HASH } from '../../lib/cso/docker'; import { CSO_HELPER_ABI, + RUNTIME_CATALOG, + selectRuntime, type QualifiedRuntime, type RuntimeCatalog, type RuntimePlatform, @@ -125,3 +127,22 @@ export function installStagedRuntime(catalog: RuntimeCatalog, runtime: Qualified catalog.promotion.evidenceDigest = `sha256:${sha256(canonical(catalog.runtimes))}`; return installed; } + +/** The given catalog with its promotion removed: every reviewed profile is still awaiting a qualified image. */ +export function unqualifiedRuntimeCatalog(base: RuntimeCatalog = RUNTIME_CATALOG): RuntimeCatalog { + const { promotion: _promotion, ...reviewed } = base; + return { ...reviewed, profiles: reviewed.profiles.map(profile => ({ ...profile, versions: { ...profile.versions } })), runtimes: [] }; +} + +/** + * The fail-closed rejection replay reaches at runtime admission for a fixture + * bundle whose image digest no qualified catalog entry carries. + */ +export function replayRuntimeRejection(profile: string, platform: RuntimePlatform): { code: string; message: string } { + try { + selectRuntime(profile, platform); + } catch (error) { + return { code: 'PREREQUISITE', message: String((error as Error).message).split(':')[0] }; + } + return { code: 'INCOMPATIBLE_INPUT', message: 'Qualified runtime digest does not match the bundle' }; +}