Files
gstack/scripts/cso-scanner-matrix.ts
T
Garry TanandOpenAI Codex 4a3c6a8a3c v1.87.0.0 feat: add verified CSO audits and replayable repair bundles (#2852)
* feat(cso): add verified audits and replayable repair bundles

* fix(cso): harden qualification and setup boundaries

* fix(cso): assemble security canaries at runtime

* fix(cso): bound release proof and maintenance work

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): require complete evaluation reports

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): replay expired snapshots from supplied source

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): synchronize DNS cancellation assertion

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore(ship): exempt repository owner from liveness proof

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): make recheck retention overlap deterministic

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: bump version and changelog (v1.85.0.0)

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass native release gates

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.86.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): resolve rechecks by finding

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.87.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass macOS and Windows release gates

Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): harden native verification gates

* fix(cso): refine Windows native diagnostics

* test(cso): isolate Windows Git startup failure

* test(cso): stabilize Windows native diagnostics

* fix(cso): support hardened Git on Windows

* fix(cso): close final verification gaps

* test(cso): bound cold Docker fixture setup

* fix(cso): restore cross-platform free-suite gates

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-09-14 15:14:58 -07:00

135 lines
8.6 KiB
TypeScript

#!/usr/bin/env bun
/** Validate reviewed scanner inputs before any image is built or published. */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { SCANNER_IDS, ScannerId, scannerPlans } from '../lib/cso/scanners';
const PLATFORMS = ['linux/amd64', 'linux/arm64'] as const;
const IMAGE = /^(?:[a-z0-9.-]+(?::[0-9]+)?\/)?[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/;
const DIGEST = /^sha256:[a-f0-9]{64}$/;
const HASH = /^[a-f0-9]{64}$/;
const VERSION = /^[0-9][A-Za-z0-9.+_-]{0,100}$/;
const EXECUTABLE = /^\/(?:[A-Za-z0-9._+-]+\/)*[A-Za-z0-9._+-]+$/;
const REPOSITORY = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
const ID = /^[A-Za-z0-9][A-Za-z0-9._+/-]{0,100}$/;
const SIGNER_WORKFLOW = /^(?:github\.com\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/\.github\/workflows\/[A-Za-z0-9_.\/-]+\.ya?ml$/;
type Platform = typeof PLATFORMS[number];
type AssetDeclaration = {
semgrepRules?: { path: string; sha256: string };
advisoryDatabase?: { path: string; contentSha256: string; updatedAt: string; ecosystems: string[] };
};
export type ReviewedAttestedImage = {
image: string;
repository: string;
sourceCommit: string;
release: string;
signerWorkflow: string;
signerDigest: string;
provenanceStatementDigest: string;
sbomStatementDigest: string;
};
export interface ScannerBuildRow {
scanner: ScannerId;
platform: Platform;
arch: 'amd64' | 'arm64';
runner: 'ubuntu-24.04' | 'ubuntu-24.04-arm';
baseImage: string;
scannerExecutable: string;
version: string;
capabilities: string[];
assets: AssetDeclaration | null;
applicationExecutable: string;
sbomGenerator: ReviewedAttestedImage;
baseAttestation: ReviewedAttestedImage;
}
function object(value: unknown, code: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(code);
return value as Record<string, unknown>;
}
function exact(value: Record<string, unknown>, allowed: string[], code: string): void {
if (Object.keys(value).some(key => !allowed.includes(key))) throw new Error(code);
}
function safePath(value: unknown, prefix: string, code: string): string {
if (typeof value !== 'string' || !EXECUTABLE.test(value) || !value.startsWith(prefix) || value.split('/').includes('..')) throw new Error(code);
return value;
}
function digest(value: unknown, code: string): string {
if (typeof value !== 'string' || !DIGEST.test(value)) throw new Error(code);
return value;
}
function attestedImage(value:unknown,code:string):ReviewedAttestedImage{
const raw=object(value,code);exact(raw,['image','repository','sourceCommit','release','signerWorkflow','signerDigest','provenanceStatementDigest','sbomStatementDigest'],code);
if(typeof raw.image!=='string'||!IMAGE.test(raw.image)||typeof raw.repository!=='string'||!REPOSITORY.test(raw.repository)||
typeof raw.sourceCommit!=='string'||!/^[a-f0-9]{40}$/.test(raw.sourceCommit)||typeof raw.release!=='string'||!ID.test(raw.release)||
typeof raw.signerWorkflow!=='string'||!SIGNER_WORKFLOW.test(raw.signerWorkflow)||typeof raw.signerDigest!=='string'||!/^[a-f0-9]{40}$/.test(raw.signerDigest))throw new Error(code);
return{image:raw.image,repository:raw.repository,sourceCommit:raw.sourceCommit,release:raw.release,signerWorkflow:raw.signerWorkflow,signerDigest:raw.signerDigest,
provenanceStatementDigest:digest(raw.provenanceStatementDigest,code),sbomStatementDigest:digest(raw.sbomStatementDigest,code)};
}
function assets(value: unknown, scanner: ScannerId): AssetDeclaration | null {
if (value === undefined || value === null) {
if (['semgrep', 'osv', 'trivy'].includes(scanner)) throw new Error(`MISSING_OFFLINE_ASSET: ${scanner}`);
return null;
}
const raw = object(value, 'INVALID_SCANNER_ASSET'); exact(raw, ['semgrepRules', 'advisoryDatabase'], 'INVALID_SCANNER_ASSET');
if (scanner === 'semgrep') {
const rules = object(raw.semgrepRules, 'MISSING_SEMGREP_RULES'); exact(rules, ['path', 'sha256'], 'INVALID_SEMGREP_RULES');
if (raw.advisoryDatabase !== undefined) throw new Error('INVALID_SEMGREP_RULES');
const path = safePath(rules.path, '/policy/catalog/', 'INVALID_SEMGREP_RULES');
if (typeof rules.sha256 !== 'string' || !HASH.test(rules.sha256)) throw new Error('INVALID_SEMGREP_RULES');
return { semgrepRules: { path, sha256: rules.sha256 } };
}
if (scanner === 'osv' || scanner === 'trivy') {
const db = object(raw.advisoryDatabase, 'MISSING_ADVISORY_DATABASE'); exact(db, ['path', 'contentSha256', 'updatedAt', 'ecosystems'], 'INVALID_ADVISORY_DATABASE');
if (raw.semgrepRules !== undefined) throw new Error('INVALID_ADVISORY_DATABASE');
const path = safePath(db.path, '/opt/cso/scanner-data/', 'INVALID_ADVISORY_DATABASE');
if (typeof db.contentSha256 !== 'string' || !HASH.test(db.contentSha256) || typeof db.updatedAt !== 'string' || !Number.isFinite(Date.parse(db.updatedAt)) || !Array.isArray(db.ecosystems) || !db.ecosystems.length || new Set(db.ecosystems).size !== db.ecosystems.length || db.ecosystems.some(item => typeof item !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/.test(item))) throw new Error('INVALID_ADVISORY_DATABASE');
return { advisoryDatabase: { path, contentSha256: db.contentSha256, updatedAt: new Date(db.updatedAt).toISOString(), ecosystems: (db.ecosystems as string[]).slice().sort() } };
}
throw new Error(`UNEXPECTED_OFFLINE_ASSET: ${scanner}`);
}
/** Every scanner must have both native platforms; partial matrices cannot publish. */
export function scannerBuildMatrix(input: unknown): { include: ScannerBuildRow[] } {
const data = object(input, 'INVALID_SCANNER_BUILD_INPUTS');
exact(data, ['schemaVersion', 'helperAbi', 'state', 'sbomGenerator', 'profiles', 'instructions'], 'INVALID_SCANNER_BUILD_INPUTS');
if (data.schemaVersion !== 1 || data.helperAbi !== 3) throw new Error('INCOMPATIBLE_SCANNER_BUILD_INPUTS');
if (data.state !== 'reviewed') throw new Error('MISSING_REVIEWED_SCANNER_INPUTS: review immutable images, assets, versions, SBOMs, and provenance before staging.');
const sbomGenerator=attestedImage(data.sbomGenerator,'UNVERIFIED_SBOM_GENERATOR');
if (!Array.isArray(data.profiles) || data.profiles.length !== SCANNER_IDS.length) throw new Error('INCOMPLETE_SCANNER_MATRIX');
const rows: ScannerBuildRow[] = [], seen = new Set<ScannerId>();
for (const value of data.profiles) {
const profile = object(value, 'INVALID_SCANNER_PROFILE');
exact(profile, ['scanner', 'version', 'baseImages', 'executable', 'assets', 'applicationExecutable'], 'INVALID_SCANNER_PROFILE');
const scanner = profile.scanner as ScannerId;
if (!SCANNER_IDS.includes(scanner) || seen.has(scanner)) throw new Error('INVALID_OR_DUPLICATE_SCANNER');
seen.add(scanner);
if (typeof profile.version !== 'string' || !VERSION.test(profile.version)) throw new Error(`UNPINNED_SCANNER_VERSION: ${scanner}`);
if (typeof profile.executable !== 'string' || !EXECUTABLE.test(profile.executable)) throw new Error(`INVALID_SCANNER_EXECUTABLE: ${scanner}`);
const baseImages = object(profile.baseImages, `MISSING_SCANNER_IMAGES: ${scanner}`);
exact(baseImages, [...PLATFORMS], `INVALID_SCANNER_IMAGES: ${scanner}`);
const declaredAssets = assets(profile.assets, scanner);
const applicationExecutable = scanner === 'schemathesis' ? safePath(profile.applicationExecutable, '/', 'MISSING_SCHEMATHESIS_FIXTURE_RUNTIME') : '';
if (scanner !== 'schemathesis' && profile.applicationExecutable !== undefined) throw new Error(`UNEXPECTED_APPLICATION_EXECUTABLE: ${scanner}`);
const capabilities = scannerPlans({ snapshotRoot: '/source', offline: true, selected: [scanner] })[0].requiredFeatures.slice().sort();
for (const platform of PLATFORMS) {
const baseAttestation=attestedImage(baseImages[platform],`INVALID_UPSTREAM_EVIDENCE: ${scanner} ${platform}`),baseImage=baseAttestation.image;
const arch = platform === 'linux/amd64' ? 'amd64' : 'arm64';
rows.push({ scanner, platform, arch, runner: arch === 'amd64' ? 'ubuntu-24.04' : 'ubuntu-24.04-arm', baseImage, scannerExecutable: profile.executable, version: profile.version, capabilities, assets: declaredAssets, applicationExecutable, sbomGenerator, baseAttestation });
}
}
return { include: rows };
}
if (import.meta.main) {
try {
if (process.argv.length !== 2) throw new Error('No arguments accepted; scanner inputs come from the reviewed repository file.');
const file = resolve(import.meta.dir, '../lib/cso/scanner-images/build-inputs.json');
process.stdout.write(JSON.stringify(scannerBuildMatrix(JSON.parse(readFileSync(file, 'utf8')))) + '\n');
} catch (error) {
process.stderr.write((error instanceof Error ? error.message : 'INVALID_SCANNER_BUILD_INPUTS') + '\n');
process.exitCode = 1;
}
}