From 25aa9064396b32ff5478cabfb7261e2316635883 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 5 Oct 2026 14:23:37 +0000 Subject: [PATCH] v1.91.27.0 fix(cso): package-level signed SBOMs and staged probes in the verifier container --- .github/workflows/cso-runtime-images.yml | 5 ++-- .github/workflows/cso-scanner-images.yml | 5 ++-- CHANGELOG.md | 9 ++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- package.json | 2 +- scripts/cso-sbom-packages-check.jq | 7 +++++ scripts/cso-sbom-packages.jq | 8 +++++ test/cso-docker-integration.test.ts | 12 ++++---- test/cso-sbom-packages.test.ts | 37 ++++++++++++++++++++++++ 10 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 scripts/cso-sbom-packages-check.jq create mode 100644 scripts/cso-sbom-packages.jq create mode 100644 test/cso-sbom-packages.test.ts diff --git a/.github/workflows/cso-runtime-images.yml b/.github/workflows/cso-runtime-images.yml index 6505a1910..e9c3259f0 100644 --- a/.github/workflows/cso-runtime-images.yml +++ b/.github/workflows/cso-runtime-images.yml @@ -209,8 +209,9 @@ jobs: CSO_IMAGE: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} run: | set -eu - docker buildx imagetools inspect "$CSO_IMAGE" --format '{{ json .SBOM.SPDX }}' > sbom.spdx.json - jq -e '.spdxVersion == "SPDX-2.3" and (.packages | type == "array")' sbom.spdx.json + docker buildx imagetools inspect "$CSO_IMAGE" --format '{{ json .SBOM.SPDX }}' > sbom.full.spdx.json + jq -c -f scripts/cso-sbom-packages.jq sbom.full.spdx.json > sbom.spdx.json + jq -e -f scripts/cso-sbom-packages-check.jq sbom.spdx.json >/dev/null - name: Sign build provenance uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 with: diff --git a/.github/workflows/cso-scanner-images.yml b/.github/workflows/cso-scanner-images.yml index 2d7a172e2..11613d586 100644 --- a/.github/workflows/cso-scanner-images.yml +++ b/.github/workflows/cso-scanner-images.yml @@ -145,8 +145,9 @@ jobs: CSO_IMAGE: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} run: | set -eu - docker buildx imagetools inspect "$CSO_IMAGE" --format '{{ json .SBOM.SPDX }}' > sbom.spdx.json - jq -e '.spdxVersion == "SPDX-2.3" and (.packages | type == "array")' sbom.spdx.json + docker buildx imagetools inspect "$CSO_IMAGE" --format '{{ json .SBOM.SPDX }}' > sbom.full.spdx.json + jq -c -f scripts/cso-sbom-packages.jq sbom.full.spdx.json > sbom.spdx.json + jq -e -f scripts/cso-sbom-packages-check.jq sbom.spdx.json >/dev/null - name: Sign build provenance uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c917e58..15725e068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.91.27.0] - 2026-10-05 + +**The first protected-main runtime staging run can finish.** + +The first `cso-runtime-images.yml` dispatch on main staged all ten runtime images but four rows stopped before qualification. Both causes are fixed: + +- **Rails SBOMs fit the attestation limit.** BuildKit's Rails SBOM lists about 19,600 files (20 MB), past `actions/attest`'s 16 MiB cap. The runtime and scanner release workflows now sign a package-level SPDX 2.3 document (`scripts/cso-sbom-packages.jq`: packages and package relationships kept, file entries and file relationships dropped, about 0.8 MB for Rails) and reject any document whose relationships name unknown elements. +- **The staged-image test probes tools in a real role container.** It ran `npm --version` and the other version probes inside the group's network anchor, which is limited to 8 processes; Node's worker threads exceed that and abort (exit 134). The probes now run in the verifier container that the same test already starts. Production code never executes in the anchor. + ## [1.91.25.0] - 2026-10-05 **gstack can grade a private, two-architecture `/cso` release run, and the runtime release gates check more for themselves.** diff --git a/VERSION b/VERSION index aae93d7ed..816ffb756 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.25.0 +1.91.27.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index d816a5c03..551c1eec7 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.25.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.27.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/package.json b/package.json index 2df04d323..1c8758aed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.25", + "version": "1.91.27", "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/scripts/cso-sbom-packages-check.jq b/scripts/cso-sbom-packages-check.jq new file mode 100644 index 000000000..ef5ec58c3 --- /dev/null +++ b/scripts/cso-sbom-packages-check.jq @@ -0,0 +1,7 @@ +# Accept only a package-level SPDX 2.3 document whose relationships name known elements. +.spdxVersion == "SPDX-2.3" +and (.packages | type == "array" and length > 0) +and (has("files") | not) +and (([.SPDXID, .packages[].SPDXID] | map({key: ., value: true}) | from_entries) as $known + | all(.relationships[]; ($known[.spdxElementId] // false) + and (($known[.relatedSpdxElement] // false) or (.relatedSpdxElement | test("^(NONE|NOASSERTION)$|^DocumentRef-"))))) diff --git a/scripts/cso-sbom-packages.jq b/scripts/cso-sbom-packages.jq new file mode 100644 index 000000000..cd0ce2266 --- /dev/null +++ b/scripts/cso-sbom-packages.jq @@ -0,0 +1,8 @@ +# Reduce a BuildKit SPDX 2.3 SBOM to its package inventory before signing it. +# File entries (tens of thousands for Ruby or Python images) push the signed +# statement past actions/attest's 16 MiB limit; packages, their versions and +# package relationships are what CSO's release evidence relies on. +((.files // []) | map({key: .SPDXID, value: true}) | from_entries) as $files +| del(.files) +| .packages |= map(del(.hasFiles)) +| .relationships |= map(select(($files[.spdxElementId] // false | not) and ($files[.relatedSpdxElement] // false | not))) diff --git a/test/cso-docker-integration.test.ts b/test/cso-docker-integration.test.ts index b09abea62..dfa8a6591 100644 --- a/test/cso-docker-integration.test.ts +++ b/test/cso-docker-integration.test.ts @@ -65,8 +65,13 @@ suite('CSO Docker containment integration',()=>{ fs.writeFileSync(negativePath, JSON.stringify({...policy, legitimate: [{...policy.legitimate[0], expected: {status: 201, includes: 'CONTROL_OK'}}]}), {mode: 0o600}); const group = await DockerGroup.create(endpoint, `staged-${Date.now()}`, dir, Date.now() + 90_000, staged, watchdog); try { + const verifier = await group.createContainer({ + role: 'verifier', image: staged, command: ['/bin/sleep', '2147483647'], + readonlyFiles: [{host: positivePath, container: '/policy/positive.json'}, {host: negativePath, container: '/policy/broken-control.json'}], + }); + await group.start(verifier); for (const [tool, version] of Object.entries(versions)) { - const result = await group.execAttach(group.anchor, stack[tool]); + const result = await group.execAttach(verifier, stack[tool]); expect(result.code).toBe(0); const expected = `${prefixes[tool]}${version}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); expect(result.output.trim()).toMatch(new RegExp(`^${expected}(?:$|\\s)`)); @@ -80,11 +85,6 @@ suite('CSO Docker containment integration',()=>{ } const server = await group.createContainer({role: 'app', image, command: ['/http-server']}); await group.start(server); - const verifier = await group.createContainer({ - role: 'verifier', image: staged, command: ['/bin/sleep', '2147483647'], - readonlyFiles: [{host: positivePath, container: '/policy/positive.json'}, {host: negativePath, container: '/policy/broken-control.json'}], - }); - await group.start(verifier); const positive = await group.execAttach(verifier, ['/opt/cso/verifier', '/policy/positive.json']); expect(positive.code).toBe(0); expect(JSON.parse(positive.output)).toMatchObject({booted: true, legitimate: true, security: 'pass'}); diff --git a/test/cso-sbom-packages.test.ts b/test/cso-sbom-packages.test.ts new file mode 100644 index 000000000..24fc6ef94 --- /dev/null +++ b/test/cso-sbom-packages.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import * as path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const jq = Bun.which('jq'); +const run = (filter: string, input: unknown, check = false) => spawnSync(jq!, [check ? '-e' : '-c', '-f', path.join(ROOT, 'scripts', filter)], { input: JSON.stringify(input), encoding: 'utf8', timeout: 10_000 }); +const document = { + spdxVersion: 'SPDX-2.3', SPDXID: 'SPDXRef-DOCUMENT', name: 'image', + packages: [{ SPDXID: 'SPDXRef-Package-rails', name: 'rails', versionInfo: '8.0.2', hasFiles: ['SPDXRef-File-a'] }, { SPDXID: 'SPDXRef-Package-rack', name: 'rack', versionInfo: '3.1.0' }], + files: [{ SPDXID: 'SPDXRef-File-a', fileName: '/usr/local/bundle/gems/rails/a.rb' }, { SPDXID: 'SPDXRef-File-b', fileName: '/b' }], + relationships: [ + { spdxElementId: 'SPDXRef-DOCUMENT', relationshipType: 'DESCRIBES', relatedSpdxElement: 'SPDXRef-Package-rails' }, + { spdxElementId: 'SPDXRef-Package-rails', relationshipType: 'DEPENDS_ON', relatedSpdxElement: 'SPDXRef-Package-rack' }, + { spdxElementId: 'SPDXRef-Package-rails', relationshipType: 'CONTAINS', relatedSpdxElement: 'SPDXRef-File-a' }, + { spdxElementId: 'SPDXRef-File-b', relationshipType: 'OTHER', relatedSpdxElement: 'SPDXRef-Package-rack' }, + ], +}; + +describe.skipIf(!jq)('CSO signed SBOMs carry the package inventory only', () => { + test('file entries and every relationship that names a file are removed; packages and package edges stay', () => { + const result = run('cso-sbom-packages.jq', document); + expect(result.status, result.stderr).toBe(0); + const compact = JSON.parse(result.stdout); + expect(compact.files).toBeUndefined(); + expect(compact.packages.map((p: any) => [p.name, p.versionInfo, 'hasFiles' in p])).toEqual([['rails', '8.0.2', false], ['rack', '3.1.0', false]]); + expect(compact.relationships.map((r: any) => r.relationshipType)).toEqual(['DESCRIBES', 'DEPENDS_ON']); + expect(run('cso-sbom-packages-check.jq', compact, true).status).toBe(0); + }); + test('the check rejects file-bearing, empty, wrong-version, or dangling documents', () => { + expect(run('cso-sbom-packages-check.jq', document, true).status).not.toBe(0); + const compact = JSON.parse(run('cso-sbom-packages.jq', document).stdout); + expect(run('cso-sbom-packages-check.jq', { ...compact, packages: [] }, true).status).not.toBe(0); + expect(run('cso-sbom-packages-check.jq', { ...compact, spdxVersion: 'SPDX-2.2' }, true).status).not.toBe(0); + expect(run('cso-sbom-packages-check.jq', { ...compact, relationships: [...compact.relationships, { spdxElementId: 'SPDXRef-Package-rails', relationshipType: 'CONTAINS', relatedSpdxElement: 'SPDXRef-File-gone' }] }, true).status).not.toBe(0); + }); +});