mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
v1.91.27.0 fix(cso): package-level signed SBOMs and staged probes in the verifier container
This commit is contained in:
1 parent
857466ff8b
commit
25aa906439
10 files changed
+76
-13
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
@@ -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",
|
||||
|
||||
@@ -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-")))))
|
||||
@@ -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)))
|
||||
@@ -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'});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user