mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 18:05:31 +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>
159 lines
7.8 KiB
TypeScript
159 lines
7.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
/** Trusted CI input validation. Qualification evidence is handled separately. */
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
export const CSO_RUNTIME_PLATFORMS = ['linux/amd64', 'linux/arm64'] as const;
|
|
export const CSO_RUNTIME_STACKS = ['node', 'bun', 'python', 'rails', 'postgresql'] as const;
|
|
export type CsoRuntimeBuildPlatform = typeof CSO_RUNTIME_PLATFORMS[number];
|
|
export type CsoRuntimeBuildStack = typeof CSO_RUNTIME_STACKS[number];
|
|
|
|
const IMAGE = /^[a-z0-9.-]+(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/;
|
|
const TAGGED_IMAGE = /^[a-z0-9.-]+(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
const REVISION = /^[a-z0-9][a-z0-9._-]{0,100}$/;
|
|
const REQUIRED_VERSIONS: Record<CsoRuntimeBuildStack, string[]> = {
|
|
node: ['node', 'npm', 'cso-preparation'],
|
|
// oven/bun exposes a Bun-backed `node` fallback, not a real Node release.
|
|
// Recording a Node version here would make package engine admission unsound.
|
|
bun: ['bun', 'cso-preparation'],
|
|
python: ['python', 'uv', 'cso-preparation'],
|
|
rails: ['ruby', 'bundler', 'cso-preparation'],
|
|
postgresql: ['postgresql'],
|
|
};
|
|
|
|
export interface ImageBuildRow {
|
|
inputRevision: string;
|
|
profileId: string;
|
|
runtimeId: string;
|
|
stack: CsoRuntimeBuildStack;
|
|
platform: CsoRuntimeBuildPlatform;
|
|
arch: 'amd64' | 'arm64';
|
|
runner: 'ubuntu-24.04' | 'ubuntu-24.04-arm';
|
|
baseSource: string;
|
|
baseIndexImage: string;
|
|
baseImage: string;
|
|
uvSource: string;
|
|
uvIndexImage: string;
|
|
uvImage: string;
|
|
sbomGeneratorSource: string;
|
|
sbomGeneratorIndexImage: string;
|
|
sbomGeneratorImage: string;
|
|
versions: Record<string, string>;
|
|
}
|
|
|
|
function record(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function repositoryOfPinned(image: string): string {
|
|
return image.slice(0, image.lastIndexOf('@'));
|
|
}
|
|
|
|
function repositoryOfTag(image: string): string {
|
|
const slash = image.lastIndexOf('/');
|
|
const colon = image.lastIndexOf(':');
|
|
if (colon <= slash) throw new Error('INVALID_SOURCE_REFERENCE');
|
|
return image.slice(0, colon);
|
|
}
|
|
|
|
function assertPinnedFamily(source: unknown, indexImage: unknown, images: unknown, label: string): Record<CsoRuntimeBuildPlatform, string> {
|
|
if (typeof source !== 'string' || !TAGGED_IMAGE.test(source)) throw new Error(`INVALID_${label}_SOURCE`);
|
|
if (typeof indexImage !== 'string' || !IMAGE.test(indexImage)) throw new Error(`UNPINNED_${label}_INDEX`);
|
|
if (!record(images)) throw new Error(`MISSING_${label}_IMAGES`);
|
|
const repository = repositoryOfTag(source);
|
|
if (repositoryOfPinned(indexImage) !== repository) throw new Error(`MISMATCHED_${label}_REPOSITORY`);
|
|
const pinned = {} as Record<CsoRuntimeBuildPlatform, string>;
|
|
for (const platform of CSO_RUNTIME_PLATFORMS) {
|
|
const image = images[platform];
|
|
if (typeof image !== 'string' || !IMAGE.test(image)) throw new Error(`UNPINNED_${label}_IMAGE: ${platform}`);
|
|
if (repositoryOfPinned(image) !== repository) throw new Error(`MISMATCHED_${label}_REPOSITORY`);
|
|
pinned[platform] = image;
|
|
}
|
|
if (pinned['linux/amd64'] === pinned['linux/arm64']) throw new Error(`DUPLICATE_${label}_PLATFORM_MANIFEST`);
|
|
return pinned;
|
|
}
|
|
|
|
/** Every declared stack must have both native platform manifests. */
|
|
export function imageBuildMatrix(input: unknown): { include: ImageBuildRow[] } {
|
|
if (!record(input)) throw new Error('INVALID_BUILD_INPUTS');
|
|
if (input.schemaVersion !== 1 || input.helperAbi !== 3) throw new Error('INCOMPATIBLE_BUILD_INPUTS');
|
|
if (input.state !== 'reviewed') throw new Error('MISSING_REVIEWED_BUILD_INPUTS: review pinned images and exact tool versions before staging publication.');
|
|
if (typeof input.revision !== 'string' || !REVISION.test(input.revision)) throw new Error('INVALID_BUILD_INPUT_REVISION');
|
|
if (typeof input.reviewedAt !== 'string' || !Number.isFinite(Date.parse(input.reviewedAt))) throw new Error('MISSING_BUILD_INPUT_REVIEW');
|
|
if (typeof input.reviewMethod !== 'string' || input.reviewMethod.length < 40 || input.reviewMethod.length > 500) throw new Error('MISSING_BUILD_INPUT_REVIEW');
|
|
if (!record(input.sbomGenerator)) throw new Error('UNPINNED_SBOM_GENERATOR');
|
|
const sbomImages = assertPinnedFamily(
|
|
input.sbomGenerator.source,
|
|
input.sbomGenerator.indexImage,
|
|
input.sbomGenerator.images,
|
|
'SBOM_GENERATOR',
|
|
);
|
|
if (!Array.isArray(input.profiles) || input.profiles.length !== CSO_RUNTIME_STACKS.length) throw new Error('INCOMPLETE_STACK_MATRIX');
|
|
|
|
const rows: ImageBuildRow[] = [];
|
|
const seenStacks = new Set<string>();
|
|
const seenProfileIds = new Set<string>();
|
|
for (const raw of input.profiles) {
|
|
if (!record(raw)) throw new Error('INVALID_PROFILE');
|
|
const { id, stack, source, indexImage, baseImages, versions } = raw;
|
|
if (typeof id !== 'string' || !REVISION.test(id) || seenProfileIds.has(id)) throw new Error('INVALID_PROFILE_ID');
|
|
seenProfileIds.add(id);
|
|
if (typeof stack !== 'string' || !CSO_RUNTIME_STACKS.includes(stack as CsoRuntimeBuildStack) || seenStacks.has(stack)) throw new Error('INVALID_STACK');
|
|
seenStacks.add(stack);
|
|
const typedStack = stack as CsoRuntimeBuildStack;
|
|
const bases = assertPinnedFamily(source, indexImage, baseImages, 'BASE');
|
|
if (!record(versions)) throw new Error('MISSING_TOOL_VERSIONS');
|
|
const exactVersion = (value: unknown) => typeof value === 'string' &&
|
|
(typedStack === 'postgresql'
|
|
? /^\d+\.\d+(?:\.\d+)?(?:[.+_-][A-Za-z0-9.-]+)?$/
|
|
: /^\d+\.\d+\.\d+(?:[.+_-][A-Za-z0-9.-]+)?$/).test(value);
|
|
const expectedKeys = [...REQUIRED_VERSIONS[typedStack]].sort();
|
|
const suppliedKeys = Object.keys(versions).sort();
|
|
if (expectedKeys.join(',') !== suppliedKeys.join(',') || expectedKeys.some(key => !exactVersion(versions[key]))) throw new Error('UNPINNED_TOOL_VERSION');
|
|
if (typedStack !== 'postgresql' && versions['cso-preparation'] !== '1.0.0') throw new Error('INCOMPATIBLE_PREPARATION_HELPER');
|
|
|
|
let uvImages = {} as Record<CsoRuntimeBuildPlatform, string>;
|
|
if (typedStack === 'python') uvImages = assertPinnedFamily(raw.uvSource, raw.uvIndexImage, raw.uvImages, 'UV');
|
|
else if (raw.uvSource !== undefined || raw.uvIndexImage !== undefined || raw.uvImages !== undefined) throw new Error('UNEXPECTED_UV_IMAGE');
|
|
|
|
for (const platform of CSO_RUNTIME_PLATFORMS) {
|
|
const arch = platform === 'linux/amd64' ? 'amd64' : 'arm64';
|
|
rows.push({
|
|
inputRevision: input.revision,
|
|
profileId: id,
|
|
runtimeId: `${id}-${arch}`,
|
|
stack: typedStack,
|
|
platform,
|
|
arch,
|
|
runner: arch === 'amd64' ? 'ubuntu-24.04' : 'ubuntu-24.04-arm',
|
|
baseSource: source as string,
|
|
baseIndexImage: indexImage as string,
|
|
baseImage: bases[platform],
|
|
uvSource: typedStack === 'python' ? raw.uvSource as string : '',
|
|
uvIndexImage: typedStack === 'python' ? raw.uvIndexImage as string : '',
|
|
uvImage: typedStack === 'python' ? uvImages[platform] : '',
|
|
sbomGeneratorSource: input.sbomGenerator.source as string,
|
|
sbomGeneratorIndexImage: input.sbomGenerator.indexImage as string,
|
|
sbomGeneratorImage: sbomImages[platform],
|
|
versions: versions as Record<string, string>,
|
|
});
|
|
}
|
|
}
|
|
return { include: rows };
|
|
}
|
|
|
|
export function committedImageBuildMatrix(): { include: ImageBuildRow[] } {
|
|
const file = resolve(import.meta.dir, '../lib/cso/images/build-inputs.json');
|
|
return imageBuildMatrix(JSON.parse(readFileSync(file, 'utf8')));
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
try {
|
|
if (process.argv.length !== 2) throw new Error('No arguments accepted; build inputs come from the reviewed repository file.');
|
|
process.stdout.write(JSON.stringify(committedImageBuildMatrix()) + '\n');
|
|
} catch (error) {
|
|
process.stderr.write((error instanceof Error ? error.message : 'INVALID_BUILD_INPUTS') + '\n');
|
|
process.exitCode = 1;
|
|
}
|
|
}
|