v1.91.27.0 fix(cso): package-level signed SBOMs and staged probes in the verifier container

This commit is contained in:
garrytan committed 2026-10-05 14:23:37 +00:00
1 parent 857466ff8b
commit 25aa906439
10 files changed
+76 -13

No files matched your search

+3 -2
View File
@@ -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:
+3 -2
View File
@@ -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:
+9
View File
@@ -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.**
+1 -1
View File
@@ -1 +1 @@
1.91.25.0
1.91.27.0
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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",
+7
View File
@@ -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-")))))
+8
View File
@@ -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)))
+6 -6
View File
@@ -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'});
+37
View File
@@ -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);
});
});