mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 09:55:29 +02:00
* 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>
135 lines
8.6 KiB
TypeScript
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;
|
|
}
|
|
}
|