style(cso): format lib/cso TypeScript with pinned Prettier

Mechanical reformat only. Minified transpile output is byte-identical for
21 of 22 files; witness.ts differs only in three regex flag orders
(/mi -> /im), which JavaScript canonicalizes. Source-text assertions over
lib/cso now compare whitespace-insensitively with the same tokens.
This commit is contained in:
garrytan committed 2026-09-29 14:22:48 +00:00
1 parent dcaea52800
commit d93d61f7ba
27 files changed
+17942 -4164

No files matched your search

+3
View File
@@ -17,6 +17,7 @@
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.117",
"@anthropic-ai/sdk": "^0.78.0",
"prettier": "3.9.9",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
},
@@ -425,6 +426,8 @@
"playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
"prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 110,
"singleQuote": true,
"trailingComma": "all",
"semi": true
}
+421 -113
View File
@@ -6,160 +6,468 @@ import { CsoError, sha256 } from './contracts';
import { discardAtomicNoReplaceTemp, recoverAtomicNoReplaceJson, secureDirectory } from './state';
import { atomicWriteSync } from '../fs-atomic';
export const GROUP_LIMITS = { cpu: 2, memoryMiB: 4096, pids: 256, writableMiB: 2048, outputBytes: 1024 * 1024 } as const;
export const GROUP_LIMITS = {
cpu: 2,
memoryMiB: 4096,
pids: 256,
writableMiB: 2048,
outputBytes: 1024 * 1024,
} as const;
export const ROLE_LIMITS = {
anchor: {cpu:.05,memoryMiB:64,pids:8,writableMiB:16},
app: {cpu:.85,memoryMiB:2304,pids:96,writableMiB:1264},
verifier: {cpu:.55,memoryMiB:512,pids:32,writableMiB:256},
tests: {cpu:.55,memoryMiB:1280,pids:64,writableMiB:1024},
postgres: {cpu:.25,memoryMiB:1024,pids:96,writableMiB:512},
browser: {cpu:.30,memoryMiB:512,pids:16,writableMiB:256},
anchor: { cpu: 0.05, memoryMiB: 64, pids: 8, writableMiB: 16 },
app: { cpu: 0.85, memoryMiB: 2304, pids: 96, writableMiB: 1264 },
verifier: { cpu: 0.55, memoryMiB: 512, pids: 32, writableMiB: 256 },
tests: { cpu: 0.55, memoryMiB: 1280, pids: 64, writableMiB: 1024 },
postgres: { cpu: 0.25, memoryMiB: 1024, pids: 96, writableMiB: 512 },
browser: { cpu: 0.3, memoryMiB: 512, pids: 16, writableMiB: 256 },
} as const;
export type Role = keyof typeof ROLE_LIMITS;
export interface Lease { endpoint: string; slot: number; path: string; runId: string; ownerPid: number; expiresAt: number; token:string; supervised:boolean }
function alive(pid: number): boolean { try { process.kill(pid,0); return true; } catch { return false; } }
function processIdentity(pid:number):string|undefined{if(process.platform!=='linux')return;try{const raw=fs.readFileSync(`/proc/${pid}/stat`,'utf8'),tail=raw.slice(raw.lastIndexOf(')')+2).trim().split(/\s+/);return /^\d+$/.test(tail[19]??'')?`linux:${tail[19]}`:undefined;}catch{return;}}
function sameDirectory(left:fs.Stats,right:fs.Stats):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.uid===right.uid&&left.mode===right.mode;}
function sameFile(left:fs.Stats,right:fs.Stats):boolean{return left.dev===right.dev&&left.ino===right.ino&&left.uid===right.uid&&left.mode===right.mode&&left.nlink===right.nlink;}
function privateDirectory(path:string,label:string):fs.Stats{const stat=fs.lstatSync(path);if(!stat.isDirectory()||stat.isSymbolicLink()||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o077)!==0)throw new CsoError('UNSAFE_PATH',`${label} is not a private owned directory`);return stat;}
function privateFile(path:string,label:string):fs.Stats{const stat=fs.lstatSync(path);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1||(process.getuid&&stat.uid!==process.getuid())||(stat.mode&0o077)!==0||stat.size>1024*1024)throw new CsoError('UNSAFE_PATH',`${label} is not a private regular file`);return stat;}
type Claim={path:string;token:string;identity:fs.Stats;pid:number;processIdentity:string|null};
type ClaimOwner={pid:number;processIdentity:string|null;token:string;createdAt:number};
function validateClaimOwner(value:unknown,expectedToken?:string,publisherPid?:number):ClaimOwner{
if(!value||typeof value!=='object'||Array.isArray(value))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
const owner=value as Record<string,unknown>;
if(Object.keys(owner).sort().join(',')!=='createdAt,pid,processIdentity,token'||!Number.isSafeInteger(owner.pid)||Number(owner.pid)<=1||
typeof owner.token!=='string'||!/^[a-f0-9]{32}$/.test(owner.token)||(expectedToken!==undefined&&owner.token!==expectedToken)||
!Number.isFinite(owner.createdAt)||Number(owner.createdAt)<0||!(owner.processIdentity===null||(typeof owner.processIdentity==='string'&&/^linux:\d+$/.test(owner.processIdentity)))||
(publisherPid!==undefined&&Number(owner.pid)!==publisherPid))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
return{pid:Number(owner.pid),processIdentity:owner.processIdentity as string|null,token:owner.token,createdAt:Number(owner.createdAt)};
export interface Lease {
endpoint: string;
slot: number;
path: string;
runId: string;
ownerPid: number;
expiresAt: number;
token: string;
supervised: boolean;
}
function inspectClaim(path:string,expectedToken?:string):Claim{
const before=privateFile(path,'Reproduction recovery claim');if(before.size<=0||before.size>4096)throw new CsoError('UNSAFE_PATH','Reproduction recovery claim has an invalid size');
let owner:any;try{owner=JSON.parse(fs.readFileSync(path,'utf8'));}catch{throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');}
const after=privateFile(path,'Reproduction recovery claim');if(!sameFile(before,after))throw new CsoError('INCOMPATIBLE_INPUT','Reproduction recovery owner is invalid');
owner=validateClaimOwner(owner,expectedToken);
return{path,token:owner.token,identity:after,pid:owner.pid,processIdentity:owner.processIdentity};
function alive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function releaseClaim(claim:Claim):void{
const current=inspectClaim(claim.path,claim.token);if(!sameFile(current.identity,claim.identity))throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery ownership changed');
const final=privateFile(claim.path,'Reproduction recovery claim');if(!sameFile(final,claim.identity))throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery ownership changed');
function processIdentity(pid: number): string | undefined {
if (process.platform !== 'linux') return;
try {
const raw = fs.readFileSync(`/proc/${pid}/stat`, 'utf8'),
tail = raw
.slice(raw.lastIndexOf(')') + 2)
.trim()
.split(/\s+/);
return /^\d+$/.test(tail[19] ?? '') ? `linux:${tail[19]}` : undefined;
} catch {
return;
}
}
function sameDirectory(left: fs.Stats, right: fs.Stats): boolean {
return (
left.dev === right.dev && left.ino === right.ino && left.uid === right.uid && left.mode === right.mode
);
}
function sameFile(left: fs.Stats, right: fs.Stats): boolean {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.uid === right.uid &&
left.mode === right.mode &&
left.nlink === right.nlink
);
}
function privateDirectory(path: string, label: string): fs.Stats {
const stat = fs.lstatSync(path);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
(process.getuid && stat.uid !== process.getuid()) ||
(stat.mode & 0o077) !== 0
)
throw new CsoError('UNSAFE_PATH', `${label} is not a private owned directory`);
return stat;
}
function privateFile(path: string, label: string): fs.Stats {
const stat = fs.lstatSync(path);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.nlink !== 1 ||
(process.getuid && stat.uid !== process.getuid()) ||
(stat.mode & 0o077) !== 0 ||
stat.size > 1024 * 1024
)
throw new CsoError('UNSAFE_PATH', `${label} is not a private regular file`);
return stat;
}
type Claim = { path: string; token: string; identity: fs.Stats; pid: number; processIdentity: string | null };
type ClaimOwner = { pid: number; processIdentity: string | null; token: string; createdAt: number };
function validateClaimOwner(value: unknown, expectedToken?: string, publisherPid?: number): ClaimOwner {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new CsoError('INCOMPATIBLE_INPUT', 'Reproduction recovery owner is invalid');
const owner = value as Record<string, unknown>;
if (
Object.keys(owner).sort().join(',') !== 'createdAt,pid,processIdentity,token' ||
!Number.isSafeInteger(owner.pid) ||
Number(owner.pid) <= 1 ||
typeof owner.token !== 'string' ||
!/^[a-f0-9]{32}$/.test(owner.token) ||
(expectedToken !== undefined && owner.token !== expectedToken) ||
!Number.isFinite(owner.createdAt) ||
Number(owner.createdAt) < 0 ||
!(
owner.processIdentity === null ||
(typeof owner.processIdentity === 'string' && /^linux:\d+$/.test(owner.processIdentity))
) ||
(publisherPid !== undefined && Number(owner.pid) !== publisherPid)
)
throw new CsoError('INCOMPATIBLE_INPUT', 'Reproduction recovery owner is invalid');
return {
pid: Number(owner.pid),
processIdentity: owner.processIdentity as string | null,
token: owner.token,
createdAt: Number(owner.createdAt),
};
}
function inspectClaim(path: string, expectedToken?: string): Claim {
const before = privateFile(path, 'Reproduction recovery claim');
if (before.size <= 0 || before.size > 4096)
throw new CsoError('UNSAFE_PATH', 'Reproduction recovery claim has an invalid size');
let owner: any;
try {
owner = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch {
throw new CsoError('INCOMPATIBLE_INPUT', 'Reproduction recovery owner is invalid');
}
const after = privateFile(path, 'Reproduction recovery claim');
if (!sameFile(before, after))
throw new CsoError('INCOMPATIBLE_INPUT', 'Reproduction recovery owner is invalid');
owner = validateClaimOwner(owner, expectedToken);
return {
path,
token: owner.token,
identity: after,
pid: owner.pid,
processIdentity: owner.processIdentity,
};
}
function releaseClaim(claim: Claim): void {
const current = inspectClaim(claim.path, claim.token);
if (!sameFile(current.identity, claim.identity))
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction recovery ownership changed');
const final = privateFile(claim.path, 'Reproduction recovery claim');
if (!sameFile(final, claim.identity))
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction recovery ownership changed');
fs.unlinkSync(claim.path);
}
function acquireClaim(parent:string,expected:fs.Stats):Claim{
const path=join(parent,'.recovery'),assertParent=()=>{const current=privateDirectory(parent,'Reproduction lease slot');if(!sameDirectory(expected,current))throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction lease changed during recovery');};
const recoverPublications=()=>{
const pattern=/^\.recovery\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/;
for(const name of fs.readdirSync(parent)){
const match=name.match(pattern);if(!match)continue;
const publisherPid=Number(match[1]),temporary=join(parent,name),options={label:'Reproduction recovery claim',maxBytes:4096,
validate:(value:unknown,pid:number)=>{validateClaimOwner(value,undefined,pid);}};
assertParent();if(fs.existsSync(path))recoverAtomicNoReplaceJson(path,options);if(fs.existsSync(temporary))discardAtomicNoReplaceTemp(temporary,publisherPid,options);assertParent();
function acquireClaim(parent: string, expected: fs.Stats): Claim {
const path = join(parent, '.recovery'),
assertParent = () => {
const current = privateDirectory(parent, 'Reproduction lease slot');
if (!sameDirectory(expected, current))
throw new CsoError('INSUFFICIENT_CAPACITY', 'Reproduction lease changed during recovery');
};
const recoverPublications = () => {
const pattern = /^\.recovery\.tmp\.(\d{1,10})\.[a-f0-9]{8}$/;
for (const name of fs.readdirSync(parent)) {
const match = name.match(pattern);
if (!match) continue;
const publisherPid = Number(match[1]),
temporary = join(parent, name),
options = {
label: 'Reproduction recovery claim',
maxBytes: 4096,
validate: (value: unknown, pid: number) => {
validateClaimOwner(value, undefined, pid);
},
};
assertParent();
if (fs.existsSync(path)) recoverAtomicNoReplaceJson(path, options);
if (fs.existsSync(temporary)) discardAtomicNoReplaceTemp(temporary, publisherPid, options);
assertParent();
}
};
for(let attempt=0;attempt<64;attempt++){
assertParent();recoverPublications();const token=randomBytes(16).toString('hex');
try{
atomicWriteSync(path,JSON.stringify({pid:process.pid,processIdentity:processIdentity(process.pid)??null,token,createdAt:Date.now()})+'\n',{mode:0o600,noReplace:true});
const claim=inspectClaim(path,token);try{assertParent();}catch(error){try{releaseClaim(claim);}catch{}throw error;}return claim;
}catch(error:any){
if(error instanceof CsoError)throw error;
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction recovery claim could not be created');
for (let attempt = 0; attempt < 64; attempt++) {
assertParent();
recoverPublications();
const token = randomBytes(16).toString('hex');
try {
atomicWriteSync(
path,
JSON.stringify({
pid: process.pid,
processIdentity: processIdentity(process.pid) ?? null,
token,
createdAt: Date.now(),
}) + '\n',
{ mode: 0o600, noReplace: true },
);
const claim = inspectClaim(path, token);
try {
assertParent();
} catch (error) {
try {
releaseClaim(claim);
} catch {}
throw error;
}
return claim;
} catch (error: any) {
if (error instanceof CsoError) throw error;
if (error?.code !== 'EEXIST')
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction recovery claim could not be created');
}
assertParent();
const observed = inspectClaim(path),
isAlive = alive(observed.pid),
identity = isAlive ? processIdentity(observed.pid) : undefined;
if (
isAlive &&
!(
typeof observed.processIdentity === 'string' &&
identity !== undefined &&
identity !== observed.processIdentity
)
)
throw new CsoError('INSUFFICIENT_CAPACITY', 'Another helper is recovering the reproduction lease');
try {
releaseClaim(observed);
} catch (error) {
if (error instanceof CsoError && error.code === 'PERSISTENCE_FAILED') continue;
throw error;
}
assertParent();const observed=inspectClaim(path),isAlive=alive(observed.pid),identity=isAlive?processIdentity(observed.pid):undefined;
if(isAlive&&!(typeof observed.processIdentity==='string'&&identity!==undefined&&identity!==observed.processIdentity))throw new CsoError('INSUFFICIENT_CAPACITY','Another helper is recovering the reproduction lease');
try{releaseClaim(observed);}catch(error){if(error instanceof CsoError&&error.code==='PERSISTENCE_FAILED')continue;throw error;}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction recovery claim changed repeatedly');
throw new CsoError('INSUFFICIENT_CAPACITY', 'Reproduction recovery claim changed repeatedly');
}
/** One host-user pool shared by every workspace/state root on this machine. */
export function machinePoolRoot():string{
const uid=process.getuid?.()??userInfo().uid;
return secureDirectory(join(fs.realpathSync(tmpdir()),`gstack-cso-pool-${uid}`));
export function machinePoolRoot(): string {
const uid = process.getuid?.() ?? userInfo().uid;
return secureDirectory(join(fs.realpathSync(tmpdir()), `gstack-cso-pool-${uid}`));
}
function reclaimSlot(path:string,pool:string,slot:number,observed:fs.Stats,expectedToken?:string):boolean{
let claim:Claim;try{claim=acquireClaim(path,observed);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')return false;throw error;}
try{const current=privateDirectory(path,'Reproduction lease slot');if(!sameDirectory(observed,current)){releaseClaim(claim);return false;}if(expectedToken){privateFile(join(path,'lease.json'),'Reproduction lease');const lease=JSON.parse(fs.readFileSync(join(path,'lease.json'),'utf8'));if(lease.token!==expectedToken){releaseClaim(claim);return false;}}
const tomb=join(pool,`.slot-${slot}.stale-${process.pid}-${randomBytes(8).toString('hex')}`);fs.renameSync(path,tomb);const moved=privateDirectory(tomb,'Reproduction lease tomb');if(!sameDirectory(observed,moved))throw new CsoError('SNAPSHOT_RACE','Reproduction lease changed while quarantined');fs.mkdirSync(path,{mode:0o700});releaseClaim({...claim,path:join(tomb,'.recovery')});for(const name of fs.readdirSync(tomb)){if(!['lease.json','lease.token'].includes(name)&&!/^lease\.json\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))throw new CsoError('UNSAFE_PATH','Stale reproduction lease contains an unexpected object');privateFile(join(tomb,name),'Stale reproduction lease file');fs.unlinkSync(join(tomb,name));}fs.rmdirSync(tomb);return true;
}catch(error){if(error instanceof CsoError)throw error;return false;}
function reclaimSlot(
path: string,
pool: string,
slot: number,
observed: fs.Stats,
expectedToken?: string,
): boolean {
let claim: Claim;
try {
claim = acquireClaim(path, observed);
} catch (error) {
if (error instanceof CsoError && error.code === 'INSUFFICIENT_CAPACITY') return false;
throw error;
}
try {
const current = privateDirectory(path, 'Reproduction lease slot');
if (!sameDirectory(observed, current)) {
releaseClaim(claim);
return false;
}
if (expectedToken) {
privateFile(join(path, 'lease.json'), 'Reproduction lease');
const lease = JSON.parse(fs.readFileSync(join(path, 'lease.json'), 'utf8'));
if (lease.token !== expectedToken) {
releaseClaim(claim);
return false;
}
}
const tomb = join(pool, `.slot-${slot}.stale-${process.pid}-${randomBytes(8).toString('hex')}`);
fs.renameSync(path, tomb);
const moved = privateDirectory(tomb, 'Reproduction lease tomb');
if (!sameDirectory(observed, moved))
throw new CsoError('SNAPSHOT_RACE', 'Reproduction lease changed while quarantined');
fs.mkdirSync(path, { mode: 0o700 });
releaseClaim({ ...claim, path: join(tomb, '.recovery') });
for (const name of fs.readdirSync(tomb)) {
if (
!['lease.json', 'lease.token'].includes(name) &&
!/^lease\.json\.tmp\.\d+\.[a-f0-9]{8}$/.test(name) &&
!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)
)
throw new CsoError('UNSAFE_PATH', 'Stale reproduction lease contains an unexpected object');
privateFile(join(tomb, name), 'Stale reproduction lease file');
fs.unlinkSync(join(tomb, name));
}
fs.rmdirSync(tomb);
return true;
} catch (error) {
if (error instanceof CsoError) throw error;
return false;
}
}
function slotControl(pool:string,slot:number):{path:string;stat:fs.Stats}{
const path=join(pool,`.slot-${slot}.control`);
try{fs.mkdirSync(path,{mode:0o700});}catch(error:any){if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction slot control directory could not be created');}
const stat=privateDirectory(path,'Reproduction slot control directory');for(const name of fs.readdirSync(path))if(name!=='.recovery'&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))throw new CsoError('UNSAFE_PATH','Reproduction slot control directory contains an unexpected object');return{path,stat};
function slotControl(pool: string, slot: number): { path: string; stat: fs.Stats } {
const path = join(pool, `.slot-${slot}.control`);
try {
fs.mkdirSync(path, { mode: 0o700 });
} catch (error: any) {
if (error?.code !== 'EEXIST')
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction slot control directory could not be created');
}
const stat = privateDirectory(path, 'Reproduction slot control directory');
for (const name of fs.readdirSync(path))
if (name !== '.recovery' && !/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))
throw new CsoError('UNSAFE_PATH', 'Reproduction slot control directory contains an unexpected object');
return { path, stat };
}
function writeLease(lease:Lease):void{
const keys=Object.keys(lease).sort().join(','),expected='endpoint,expiresAt,ownerPid,path,runId,slot,supervised,token';
if(keys!==expected||!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(lease.endpoint)||![0,1].includes(lease.slot)||
!/^[A-Za-z0-9_.-]{1,100}$/.test(lease.runId)||lease.ownerPid!==process.pid||!Number.isSafeInteger(lease.expiresAt)||
!/^[a-f0-9]{32}$/.test(lease.token)||typeof lease.supervised!=='boolean')
throw new CsoError('PERSISTENCE_FAILED','Reproduction lease metadata is invalid');
const expectedPath=join(machinePoolRoot(),sha256(lease.endpoint).slice(0,24),`slot-${lease.slot}`);
if(lease.path!==expectedPath)throw new CsoError('PERSISTENCE_FAILED','Reproduction lease path is invalid');
function writeLease(lease: Lease): void {
const keys = Object.keys(lease).sort().join(','),
expected = 'endpoint,expiresAt,ownerPid,path,runId,slot,supervised,token';
if (
keys !== expected ||
!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(lease.endpoint) ||
![0, 1].includes(lease.slot) ||
!/^[A-Za-z0-9_.-]{1,100}$/.test(lease.runId) ||
lease.ownerPid !== process.pid ||
!Number.isSafeInteger(lease.expiresAt) ||
!/^[a-f0-9]{32}$/.test(lease.token) ||
typeof lease.supervised !== 'boolean'
)
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease metadata is invalid');
const expectedPath = join(machinePoolRoot(), sha256(lease.endpoint).slice(0, 24), `slot-${lease.slot}`);
if (lease.path !== expectedPath)
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease path is invalid');
// This exact helper-owned schema contains only control metadata. In
// particular, its random capability may resemble a wallet address and must
// remain byte-identical to lease.token; untrusted reports still use writeJson.
atomicWriteSync(join(lease.path,'lease.json'),JSON.stringify(lease)+'\n',{mode:0o600});
atomicWriteSync(join(lease.path, 'lease.json'), JSON.stringify(lease) + '\n', { mode: 0o600 });
}
export function admit(endpoint: string, runId: string, deadline: number): Lease {
if (!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(endpoint)) throw new CsoError('ISOLATION_FAILED','Only a pinned local Unix Docker endpoint is admitted on this host');
const pool = secureDirectory(join(machinePoolRoot(),sha256(endpoint).slice(0,24)));
for (let slot=0;slot<2;slot++) {
const path=join(pool,`slot-${slot}`),control=slotControl(pool,slot);let mutation:Claim;
try{mutation=acquireClaim(control.path,control.stat);}catch(error){if(error instanceof CsoError&&error.code==='INSUFFICIENT_CAPACITY')continue;throw error;}
try{
if (!/^unix:\/\/[/.A-Za-z0-9_-]+$/.test(endpoint))
throw new CsoError(
'ISOLATION_FAILED',
'Only a pinned local Unix Docker endpoint is admitted on this host',
);
const pool = secureDirectory(join(machinePoolRoot(), sha256(endpoint).slice(0, 24)));
for (let slot = 0; slot < 2; slot++) {
const path = join(pool, `slot-${slot}`),
control = slotControl(pool, slot);
let mutation: Claim;
try {
mutation = acquireClaim(control.path, control.stat);
} catch (error) {
if (error instanceof CsoError && error.code === 'INSUFFICIENT_CAPACITY') continue;
throw error;
}
try {
try {
fs.mkdirSync(path,{mode:0o700});
} catch(error:any) {
if(error?.code!=='EEXIST')throw new CsoError('PERSISTENCE_FAILED','Reproduction lease slot could not be created');
fs.mkdirSync(path, { mode: 0o700 });
} catch (error: any) {
if (error?.code !== 'EEXIST')
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease slot could not be created');
try {
const observed=privateDirectory(path,'Reproduction lease slot');
const old = JSON.parse(fs.readFileSync(join(path,'lease.json'),'utf8'));
const observed = privateDirectory(path, 'Reproduction lease slot');
const old = JSON.parse(fs.readFileSync(join(path, 'lease.json'), 'utf8'));
// A supervised lease is removed only after its watchdog or owner has
// confirmed exact-resource cleanup. This preserves the two-group cap
// through supervisor death and daemon outages.
if (old.supervised === true || (typeof old.ownerPid === 'number' && alive(old.ownerPid))) continue;
// Unsupervised stale slots cannot have created containers: supervision
// is acknowledged before the anchor create call.
if(!reclaimSlot(path,pool,slot,observed,typeof old.token==='string'?old.token:undefined))continue;
} catch(recoveryError) {
if(recoveryError instanceof CsoError)throw recoveryError;
if (!reclaimSlot(path, pool, slot, observed, typeof old.token === 'string' ? old.token : undefined))
continue;
} catch (recoveryError) {
if (recoveryError instanceof CsoError) throw recoveryError;
// No live initializer can publish into this path while this stable
// slot-control claim is held. Recover a crashed partial publication
// only after the compatibility grace period.
let stat:fs.Stats;try{stat=privateDirectory(path,'Reproduction lease slot');}catch(statError){if(statError instanceof CsoError)throw statError;continue;}
if(Date.now()-stat.mtimeMs<=5000)continue;
if(!reclaimSlot(path,pool,slot,stat))continue;
let stat: fs.Stats;
try {
stat = privateDirectory(path, 'Reproduction lease slot');
} catch (statError) {
if (statError instanceof CsoError) throw statError;
continue;
}
if (Date.now() - stat.mtimeMs <= 5000) continue;
if (!reclaimSlot(path, pool, slot, stat)) continue;
}
}
// Both authenticated records become visible as one logical publication
// when the stable slot-control claim is released.
const lease:Lease={endpoint,slot,path,runId,ownerPid:process.pid,expiresAt:deadline,token:randomBytes(16).toString('hex'),supervised:false};writeLease(lease);fs.writeFileSync(join(path,'lease.token'),lease.token+'\n',{mode:0o600,flag:'wx'});return lease;
}finally{releaseClaim(mutation);}
const lease: Lease = {
endpoint,
slot,
path,
runId,
ownerPid: process.pid,
expiresAt: deadline,
token: randomBytes(16).toString('hex'),
supervised: false,
};
writeLease(lease);
fs.writeFileSync(join(path, 'lease.token'), lease.token + '\n', { mode: 0o600, flag: 'wx' });
return lease;
} finally {
releaseClaim(mutation);
}
}
throw new CsoError('INSUFFICIENT_CAPACITY','Two reproduction groups are already admitted for this Docker endpoint');
throw new CsoError(
'INSUFFICIENT_CAPACITY',
'Two reproduction groups are already admitted for this Docker endpoint',
);
}
export function markSupervised(lease:Lease):void{
const current=JSON.parse(fs.readFileSync(join(lease.path,'lease.json'),'utf8'));
if(current.token!==lease.token||current.ownerPid!==lease.ownerPid)throw new CsoError('INSUFFICIENT_CAPACITY','Reproduction lease changed before watchdog supervision');
lease.supervised=true;writeLease(lease);
export function markSupervised(lease: Lease): void {
const current = JSON.parse(fs.readFileSync(join(lease.path, 'lease.json'), 'utf8'));
if (current.token !== lease.token || current.ownerPid !== lease.ownerPid)
throw new CsoError('INSUFFICIENT_CAPACITY', 'Reproduction lease changed before watchdog supervision');
lease.supervised = true;
writeLease(lease);
}
export function release(lease: Lease): void {
let observed:fs.Stats;try{observed=privateDirectory(lease.path,'Reproduction lease slot');}catch(error:any){if(error?.code==='ENOENT')throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease was already missing');throw error;}
const claim=acquireClaim(lease.path,observed);
try{
const currentStat=privateDirectory(lease.path,'Reproduction lease slot');if(!sameDirectory(observed,currentStat))throw new CsoError('PERSISTENCE_FAILED','Reproduction lease changed before exact release');
const names=fs.readdirSync(lease.path).filter(name=>name!=='.recovery'&&!/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name)).sort();if(names.join('\0')!=='lease.json\0lease.token')throw new CsoError('PERSISTENCE_FAILED','Reproduction lease contents changed before exact release');
privateFile(join(lease.path,'lease.json'),'Reproduction lease');privateFile(join(lease.path,'lease.token'),'Reproduction lease token');
const current=JSON.parse(fs.readFileSync(join(lease.path,'lease.json'),'utf8')),token=fs.readFileSync(join(lease.path,'lease.token'),'utf8').trim();
if(current.runId!==lease.runId||current.ownerPid!==lease.ownerPid||current.token!==lease.token||token!==lease.token)throw new CsoError('PERSISTENCE_FAILED','Reproduction lease ownership changed before exact release');
fs.unlinkSync(join(lease.path,'lease.token'));fs.unlinkSync(join(lease.path,'lease.json'));releaseClaim(claim);fs.rmdirSync(lease.path);
if(fs.existsSync(lease.path))throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease removal could not be proven');
}catch(error){try{if(fs.existsSync(claim.path))releaseClaim(claim);}catch{}if(error instanceof CsoError)throw error;throw new CsoError('PERSISTENCE_FAILED','Exact reproduction lease removal failed');}
let observed: fs.Stats;
try {
observed = privateDirectory(lease.path, 'Reproduction lease slot');
} catch (error: any) {
if (error?.code === 'ENOENT')
throw new CsoError('PERSISTENCE_FAILED', 'Exact reproduction lease was already missing');
throw error;
}
const claim = acquireClaim(lease.path, observed);
try {
const currentStat = privateDirectory(lease.path, 'Reproduction lease slot');
if (!sameDirectory(observed, currentStat))
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease changed before exact release');
const names = fs
.readdirSync(lease.path)
.filter((name) => name !== '.recovery' && !/^\.recovery\.tmp\.\d+\.[a-f0-9]{8}$/.test(name))
.sort();
if (names.join('\0') !== 'lease.json\0lease.token')
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease contents changed before exact release');
privateFile(join(lease.path, 'lease.json'), 'Reproduction lease');
privateFile(join(lease.path, 'lease.token'), 'Reproduction lease token');
const current = JSON.parse(fs.readFileSync(join(lease.path, 'lease.json'), 'utf8')),
token = fs.readFileSync(join(lease.path, 'lease.token'), 'utf8').trim();
if (
current.runId !== lease.runId ||
current.ownerPid !== lease.ownerPid ||
current.token !== lease.token ||
token !== lease.token
)
throw new CsoError('PERSISTENCE_FAILED', 'Reproduction lease ownership changed before exact release');
fs.unlinkSync(join(lease.path, 'lease.token'));
fs.unlinkSync(join(lease.path, 'lease.json'));
releaseClaim(claim);
fs.rmdirSync(lease.path);
if (fs.existsSync(lease.path))
throw new CsoError('PERSISTENCE_FAILED', 'Exact reproduction lease removal could not be proven');
} catch (error) {
try {
if (fs.existsSync(claim.path)) releaseClaim(claim);
} catch {}
if (error instanceof CsoError) throw error;
throw new CsoError('PERSISTENCE_FAILED', 'Exact reproduction lease removal failed');
}
}
export function total(roles: Role[]) {
const value = roles.reduce((a,r) => ({cpu:a.cpu+ROLE_LIMITS[r].cpu,memoryMiB:a.memoryMiB+ROLE_LIMITS[r].memoryMiB,pids:a.pids+ROLE_LIMITS[r].pids,writableMiB:a.writableMiB+ROLE_LIMITS[r].writableMiB}), {cpu:0,memoryMiB:0,pids:0,writableMiB:0});
if (value.cpu > GROUP_LIMITS.cpu || value.memoryMiB > GROUP_LIMITS.memoryMiB || value.pids > GROUP_LIMITS.pids || value.writableMiB > GROUP_LIMITS.writableMiB)
throw new CsoError('INSUFFICIENT_CAPACITY','Requested sidecars exceed the aggregate reproduction-group limit');
const value = roles.reduce(
(a, r) => ({
cpu: a.cpu + ROLE_LIMITS[r].cpu,
memoryMiB: a.memoryMiB + ROLE_LIMITS[r].memoryMiB,
pids: a.pids + ROLE_LIMITS[r].pids,
writableMiB: a.writableMiB + ROLE_LIMITS[r].writableMiB,
}),
{ cpu: 0, memoryMiB: 0, pids: 0, writableMiB: 0 },
);
if (
value.cpu > GROUP_LIMITS.cpu ||
value.memoryMiB > GROUP_LIMITS.memoryMiB ||
value.pids > GROUP_LIMITS.pids ||
value.writableMiB > GROUP_LIMITS.writableMiB
)
throw new CsoError(
'INSUFFICIENT_CAPACITY',
'Requested sidecars exceed the aggregate reproduction-group limit',
);
return value;
}
+54 -11
View File
@@ -2,15 +2,58 @@ import * as fs from 'node:fs';
import { CsoError } from './contracts';
/** Read one caller-supplied control file without following or blocking on a raced special file. */
export function readBoundedStable(path:string,max:number,label:string):Buffer{
let named:fs.Stats,fd:number|undefined;try{named=fs.lstatSync(path);}catch{throw new CsoError('MISSING_INPUT',`${label} does not exist`);}
if(named.isSymbolicLink()||!named.isFile()||named.nlink!==1||named.size>max)throw new CsoError('MISSING_INPUT',`${label} must be one bounded regular file`);
try{
fd=fs.openSync(path,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)|(fs.constants.O_NONBLOCK??0));const opened=fs.fstatSync(fd);
if(!opened.isFile()||opened.nlink!==1||opened.dev!==named.dev||opened.ino!==named.ino||opened.mode!==named.mode||opened.size!==named.size)throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be read`);
const data=Buffer.alloc(max+1);let bytes=0,count=0;while(bytes<data.length&&(count=fs.readSync(fd,data,bytes,data.length-bytes,null))>0)bytes+=count;
const after=fs.fstatSync(fd),current=fs.lstatSync(path);if(bytes>max)throw new CsoError('MISSING_INPUT',`${label} exceeds the ${max}-byte limit`);
if(!current.isFile()||current.isSymbolicLink()||current.nlink!==1||current.dev!==opened.dev||current.ino!==opened.ino||current.mode!==opened.mode||after.size!==opened.size||after.mtimeMs!==opened.mtimeMs||after.ctimeMs!==opened.ctimeMs)throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was read`);
return data.subarray(0,bytes);
}catch(error){if(error instanceof CsoError)throw error;const code=(error as NodeJS.ErrnoException).code;if(['ELOOP','ENOENT','ENOTDIR','ENXIO'].includes(code??''))throw new CsoError('SNAPSHOT_RACE',`${label} changed before it could be opened`);throw new CsoError('MISSING_INPUT',`${label} is missing or unreadable`);}finally{if(fd!==undefined)fs.closeSync(fd);}
export function readBoundedStable(path: string, max: number, label: string): Buffer {
let named: fs.Stats, fd: number | undefined;
try {
named = fs.lstatSync(path);
} catch {
throw new CsoError('MISSING_INPUT', `${label} does not exist`);
}
if (named.isSymbolicLink() || !named.isFile() || named.nlink !== 1 || named.size > max)
throw new CsoError('MISSING_INPUT', `${label} must be one bounded regular file`);
try {
fd = fs.openSync(
path,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0),
);
const opened = fs.fstatSync(fd);
if (
!opened.isFile() ||
opened.nlink !== 1 ||
opened.dev !== named.dev ||
opened.ino !== named.ino ||
opened.mode !== named.mode ||
opened.size !== named.size
)
throw new CsoError('SNAPSHOT_RACE', `${label} changed before it could be read`);
const data = Buffer.alloc(max + 1);
let bytes = 0,
count = 0;
while (bytes < data.length && (count = fs.readSync(fd, data, bytes, data.length - bytes, null)) > 0)
bytes += count;
const after = fs.fstatSync(fd),
current = fs.lstatSync(path);
if (bytes > max) throw new CsoError('MISSING_INPUT', `${label} exceeds the ${max}-byte limit`);
if (
!current.isFile() ||
current.isSymbolicLink() ||
current.nlink !== 1 ||
current.dev !== opened.dev ||
current.ino !== opened.ino ||
current.mode !== opened.mode ||
after.size !== opened.size ||
after.mtimeMs !== opened.mtimeMs ||
after.ctimeMs !== opened.ctimeMs
)
throw new CsoError('SNAPSHOT_RACE', `${label} changed while it was read`);
return data.subarray(0, bytes);
} catch (error) {
if (error instanceof CsoError) throw error;
const code = (error as NodeJS.ErrnoException).code;
if (['ELOOP', 'ENOENT', 'ENOTDIR', 'ENXIO'].includes(code ?? ''))
throw new CsoError('SNAPSHOT_RACE', `${label} changed before it could be opened`);
throw new CsoError('MISSING_INPUT', `${label} is missing or unreadable`);
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
}
+486 -169
View File
File diff suppressed because it is too large. Load diff
+2624 -398
View File
File diff suppressed because it is too large. Load diff
+856 -236
View File
File diff suppressed because it is too large. Load diff
+940 -230
View File
File diff suppressed because it is too large. Load diff
+87 -36
View File
@@ -1,49 +1,100 @@
/** Decode the two Git path tokens in a `diff --git` header. */
function token(source:string,offset:number):{value:string;next:number}|undefined{
if(source[offset]!=='"'){
const end=source.indexOf(' ',offset),next=end<0?source.length:end;
if(next===offset)return;
return{value:source.slice(offset,next),next};
function token(source: string, offset: number): { value: string; next: number } | undefined {
if (source[offset] !== '"') {
const end = source.indexOf(' ', offset),
next = end < 0 ? source.length : end;
if (next === offset) return;
return { value: source.slice(offset, next), next };
}
const bytes:number[]=[];let at=offset+1;
const append=(value:string)=>bytes.push(...new TextEncoder().encode(value));
while(at<source.length){
const value=source[at++];
if(value==='"')return{value:new TextDecoder('utf-8',{fatal:true}).decode(Uint8Array.from(bytes)),next:at};
if(value!=='\\'){append(value);continue;}
if(at>=source.length)return;
const escaped=source[at++],mapped:{[key:string]:string}={a:'\x07',b:'\b',f:'\f',n:'\n',r:'\r',t:'\t',v:'\v','\\':'\\','"':'"'};
if(mapped[escaped]!==undefined){append(mapped[escaped]);continue;}
if(/[0-7]/.test(escaped)&&/^[0-7]{2}/.test(source.slice(at,at+2))){bytes.push(Number.parseInt(escaped+source.slice(at,at+2),8));at+=2;continue;}
const bytes: number[] = [];
let at = offset + 1;
const append = (value: string) => bytes.push(...new TextEncoder().encode(value));
while (at < source.length) {
const value = source[at++];
if (value === '"')
return { value: new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(bytes)), next: at };
if (value !== '\\') {
append(value);
continue;
}
if (at >= source.length) return;
const escaped = source[at++],
mapped: { [key: string]: string } = {
a: '\x07',
b: '\b',
f: '\f',
n: '\n',
r: '\r',
t: '\t',
v: '\v',
'\\': '\\',
'"': '"',
};
if (mapped[escaped] !== undefined) {
append(mapped[escaped]);
continue;
}
if (/[0-7]/.test(escaped) && /^[0-7]{2}/.test(source.slice(at, at + 2))) {
bytes.push(Number.parseInt(escaped + source.slice(at, at + 2), 8));
at += 2;
continue;
}
return;
}
}
export function gitDiffHeaderPaths(line:string):[string,string]|undefined{
const prefix='diff --git ';if(!line.startsWith(prefix))return;
try{
const left=token(line,prefix.length);if(!left||line[left.next]!==' ')return;
const right=token(line,left.next+1);if(!right||right.next!==line.length)return;
return[left.value,right.value];
}catch{return;}
export function gitDiffHeaderPaths(line: string): [string, string] | undefined {
const prefix = 'diff --git ';
if (!line.startsWith(prefix)) return;
try {
const left = token(line, prefix.length);
if (!left || line[left.next] !== ' ') return;
const right = token(line, left.next + 1);
if (!right || right.next !== line.length) return;
return [left.value, right.value];
} catch {
return;
}
}
/** Return only exact path hunks, keeping one commit preamble per matching commit. */
export function historyForPath(raw:string,path:string):string|undefined{
const expected=new Set([`a/${path}`,`b/${path}`]),output:string[]=[],lines=raw.split('\n');
let preamble:string[]=[],section:string[]|undefined,include=false,preambleEmitted=false;
const flush=()=>{
if(section&&include){if(!preambleEmitted){output.push(...preamble);preambleEmitted=true;}output.push(...section);}
section=undefined;include=false;
};
for(const line of lines){
if(line.startsWith('commit ')){flush();preamble=[line];preambleEmitted=false;continue;}
if(line.startsWith('diff --git ')){
flush();section=[line];const paths=gitDiffHeaderPaths(line);include=Boolean(paths&&(expected.has(paths[0])||expected.has(paths[1])));continue;
export function historyForPath(raw: string, path: string): string | undefined {
const expected = new Set([`a/${path}`, `b/${path}`]),
output: string[] = [],
lines = raw.split('\n');
let preamble: string[] = [],
section: string[] | undefined,
include = false,
preambleEmitted = false;
const flush = () => {
if (section && include) {
if (!preambleEmitted) {
output.push(...preamble);
preambleEmitted = true;
}
output.push(...section);
}
if(section)section.push(line);else preamble.push(line);
section = undefined;
include = false;
};
for (const line of lines) {
if (line.startsWith('commit ')) {
flush();
preamble = [line];
preambleEmitted = false;
continue;
}
if (line.startsWith('diff --git ')) {
flush();
section = [line];
const paths = gitDiffHeaderPaths(line);
include = Boolean(paths && (expected.has(paths[0]) || expected.has(paths[1])));
continue;
}
if (section) section.push(line);
else preamble.push(line);
}
flush();
while(output.at(-1)==='')output.pop();
return output.length?output.join('\n'):undefined;
while (output.at(-1) === '') output.pop();
return output.length ? output.join('\n') : undefined;
}
+360 -122
View File
@@ -8,175 +8,413 @@ import { validateScannerCatalog, type ScannerCatalog } from './scanner-catalog';
import { secureDirectory } from './state';
export interface QualifiedCatalogImage {
kind:'runtime'|'scanner';
id:string;
image:string;
platform:RuntimePlatform;
kind: 'runtime' | 'scanner';
id: string;
image: string;
platform: RuntimePlatform;
}
export interface CatalogImageSession {
readonly docker:{endpoint:string;version:string;security:string[]};
present(entry:QualifiedCatalogImage,deadline?:number):Promise<boolean>;
pull(entry:QualifiedCatalogImage,deadline?:number):Promise<void>;
close():void;
readonly docker: { endpoint: string; version: string; security: string[] };
present(entry: QualifiedCatalogImage, deadline?: number): Promise<boolean>;
pull(entry: QualifiedCatalogImage, deadline?: number): Promise<void>;
close(): void;
}
/** Doctor performs concurrent, read-only checks inside its 30-second contract. */
export const CATALOG_IMAGE_INSPECTION_BUDGET_MS=30_000;
export const DEFAULT_CATALOG_IMAGE_BUDGET_MS=30_000;
export const MIN_CATALOG_IMAGE_BUDGET_SECONDS=5;
export const MAX_CATALOG_IMAGE_BUDGET_SECONDS=300;
export const MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS=60*60_000;
const CATALOG_IMAGE_ADMISSION_BUDGET_MS=30_000;
export interface CatalogImageProvisioningPolicy {perImageMs:number;aggregateMs:number;}
export const CATALOG_IMAGE_INSPECTION_BUDGET_MS = 30_000;
export const DEFAULT_CATALOG_IMAGE_BUDGET_MS = 30_000;
export const MIN_CATALOG_IMAGE_BUDGET_SECONDS = 5;
export const MAX_CATALOG_IMAGE_BUDGET_SECONDS = 300;
export const MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS = 60 * 60_000;
const CATALOG_IMAGE_ADMISSION_BUDGET_MS = 30_000;
export interface CatalogImageProvisioningPolicy {
perImageMs: number;
aggregateMs: number;
}
/**
* Give every declared native-platform image a bounded opportunity to download.
* The one-hour ceiling admits the current eleven-image catalog even at the
* maximum configurable five-minute allowance.
*/
export function catalogImageProvisioningPolicy(imageCount:number,requestedSeconds?:string):CatalogImageProvisioningPolicy{
if(!Number.isSafeInteger(imageCount)||imageCount<0)throw new CsoError('INVALID_ARGUMENT','Catalog image count is invalid');
let seconds=DEFAULT_CATALOG_IMAGE_BUDGET_MS/1000;
if(requestedSeconds!==undefined){
if(!/^[0-9]+$/.test(requestedSeconds))throw new CsoError('INVALID_ARGUMENT','--per-image-seconds requires a whole number');
seconds=Number(requestedSeconds);
if(seconds<MIN_CATALOG_IMAGE_BUDGET_SECONDS||seconds>MAX_CATALOG_IMAGE_BUDGET_SECONDS)throw new CsoError('INVALID_ARGUMENT',`--per-image-seconds must be ${MIN_CATALOG_IMAGE_BUDGET_SECONDS}..${MAX_CATALOG_IMAGE_BUDGET_SECONDS}`);
export function catalogImageProvisioningPolicy(
imageCount: number,
requestedSeconds?: string,
): CatalogImageProvisioningPolicy {
if (!Number.isSafeInteger(imageCount) || imageCount < 0)
throw new CsoError('INVALID_ARGUMENT', 'Catalog image count is invalid');
let seconds = DEFAULT_CATALOG_IMAGE_BUDGET_MS / 1000;
if (requestedSeconds !== undefined) {
if (!/^[0-9]+$/.test(requestedSeconds))
throw new CsoError('INVALID_ARGUMENT', '--per-image-seconds requires a whole number');
seconds = Number(requestedSeconds);
if (seconds < MIN_CATALOG_IMAGE_BUDGET_SECONDS || seconds > MAX_CATALOG_IMAGE_BUDGET_SECONDS)
throw new CsoError(
'INVALID_ARGUMENT',
`--per-image-seconds must be ${MIN_CATALOG_IMAGE_BUDGET_SECONDS}..${MAX_CATALOG_IMAGE_BUDGET_SECONDS}`,
);
}
const perImageMs=seconds*1000,aggregateMs=CATALOG_IMAGE_ADMISSION_BUDGET_MS+imageCount*perImageMs;
if(!Number.isSafeInteger(aggregateMs)||aggregateMs>MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS)throw new CsoError('INCOMPATIBLE_INPUT','Qualified image catalog exceeds the bounded setup preload capacity');
return{perImageMs,aggregateMs};
const perImageMs = seconds * 1000,
aggregateMs = CATALOG_IMAGE_ADMISSION_BUDGET_MS + imageCount * perImageMs;
if (!Number.isSafeInteger(aggregateMs) || aggregateMs > MAX_CATALOG_IMAGE_PROVISIONING_BUDGET_MS)
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Qualified image catalog exceeds the bounded setup preload capacity',
);
return { perImageMs, aggregateMs };
}
export type CatalogImageSessionFactory=(deadline:number)=>Promise<CatalogImageSession>;
export type CatalogImageSessionFactory = (deadline: number) => Promise<CatalogImageSession>;
export interface CatalogImageAvailability extends QualifiedCatalogImage {
status:'available'|'unavailable';
reason?:string;
status: 'available' | 'unavailable';
reason?: string;
}
export interface CatalogImageInspection {
docker:{status:'ready'|'missing';detail:unknown};
images:CatalogImageAvailability[];
docker: { status: 'ready' | 'missing'; detail: unknown };
images: CatalogImageAvailability[];
}
export interface CatalogImageProvisionResult {
schemaVersion:1;
status:'complete'|'partial'|'not_available';
downloads:true;
platform:RuntimePlatform;
requested:number;
inspected:number;
alreadyPresent:number;
downloaded:number;
deadlineReached:boolean;
unavailable:CatalogImageAvailability[];
summary:string;
schemaVersion: 1;
status: 'complete' | 'partial' | 'not_available';
downloads: true;
platform: RuntimePlatform;
requested: number;
inspected: number;
alreadyPresent: number;
downloaded: number;
deadlineReached: boolean;
unavailable: CatalogImageAvailability[];
summary: string;
}
export function qualifiedCatalogImages(runtimeCatalog:RuntimeCatalog,scannerCatalog:ScannerCatalog,platform:RuntimePlatform):QualifiedCatalogImage[]{
validateRuntimeCatalog(runtimeCatalog);validateScannerCatalog(scannerCatalog);
const entries:QualifiedCatalogImage[]=[
...runtimeCatalog.runtimes.filter(item=>item.platform===platform).map(item=>({kind:'runtime' as const,id:item.id,image:item.image,platform:item.platform})),
...scannerCatalog.scanners.filter(item=>item.platform===platform).map(item=>({kind:'scanner' as const,id:item.id,image:item.image,platform:item.platform})),
export function qualifiedCatalogImages(
runtimeCatalog: RuntimeCatalog,
scannerCatalog: ScannerCatalog,
platform: RuntimePlatform,
): QualifiedCatalogImage[] {
validateRuntimeCatalog(runtimeCatalog);
validateScannerCatalog(scannerCatalog);
const entries: QualifiedCatalogImage[] = [
...runtimeCatalog.runtimes
.filter((item) => item.platform === platform)
.map((item) => ({ kind: 'runtime' as const, id: item.id, image: item.image, platform: item.platform })),
...scannerCatalog.scanners
.filter((item) => item.platform === platform)
.map((item) => ({ kind: 'scanner' as const, id: item.id, image: item.image, platform: item.platform })),
];
const identities=new Set<string>();
for(const entry of entries){
const identity=`${entry.kind}:${entry.id}`;
if(identities.has(identity))throw new CsoError('INCOMPATIBLE_INPUT','Qualified image catalogs contain a duplicate identity');
const identities = new Set<string>();
for (const entry of entries) {
const identity = `${entry.kind}:${entry.id}`;
if (identities.has(identity))
throw new CsoError('INCOMPATIBLE_INPUT', 'Qualified image catalogs contain a duplicate identity');
identities.add(identity);
}
return entries.sort((left,right)=>`${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
return entries.sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
}
function controlledReason(error:unknown,fallback:string):string{
return error instanceof CsoError?error.message:fallback;
function controlledReason(error: unknown, fallback: string): string {
return error instanceof CsoError ? error.message : fallback;
}
export async function inspectCatalogImages(entries:QualifiedCatalogImage[],open:CatalogImageSessionFactory,deadline=Date.now()+CATALOG_IMAGE_INSPECTION_BUDGET_MS):Promise<CatalogImageInspection>{
let session:CatalogImageSession;
try{session=await open(deadline);}catch(error){
const detail=controlledReason(error,'Local Docker is unavailable for exact catalog image inspection');
return{docker:{status:'missing',detail},images:entries.map(entry=>({...entry,status:'unavailable',reason:detail}))};
export async function inspectCatalogImages(
entries: QualifiedCatalogImage[],
open: CatalogImageSessionFactory,
deadline = Date.now() + CATALOG_IMAGE_INSPECTION_BUDGET_MS,
): Promise<CatalogImageInspection> {
let session: CatalogImageSession;
try {
session = await open(deadline);
} catch (error) {
const detail = controlledReason(error, 'Local Docker is unavailable for exact catalog image inspection');
return {
docker: { status: 'missing', detail },
images: entries.map((entry) => ({ ...entry, status: 'unavailable', reason: detail })),
};
}
try{
try {
// Read-only daemon lookups run together so doctor remains within its
// 30-second contract even when a local Docker client is slow to fail.
const images=await Promise.all(entries.map(async(entry):Promise<CatalogImageAvailability>=>{
try{const present=await session.present(entry);if(Date.now()>=deadline)throw new CsoError('DEADLINE','Exact image inspection reached the aggregate image-provisioning deadline');return{...entry,status:present?'available':'unavailable',...(present?{}:{reason:'Exact qualified image is not present in the local Docker daemon'})};}
catch(error){return{...entry,status:'unavailable',reason:controlledReason(error,'Exact qualified image could not be inspected safely')};}
}));
return{docker:{status:'ready',detail:session.docker},images};
}finally{session.close();}
const images = await Promise.all(
entries.map(async (entry): Promise<CatalogImageAvailability> => {
try {
const present = await session.present(entry);
if (Date.now() >= deadline)
throw new CsoError(
'DEADLINE',
'Exact image inspection reached the aggregate image-provisioning deadline',
);
return {
...entry,
status: present ? 'available' : 'unavailable',
...(present ? {} : { reason: 'Exact qualified image is not present in the local Docker daemon' }),
};
} catch (error) {
return {
...entry,
status: 'unavailable',
reason: controlledReason(error, 'Exact qualified image could not be inspected safely'),
};
}
}),
);
return { docker: { status: 'ready', detail: session.docker }, images };
} finally {
session.close();
}
}
export async function provisionCatalogImages(entries:QualifiedCatalogImage[],platform:RuntimePlatform,open:CatalogImageSessionFactory,deadline=Date.now()+catalogImageProvisioningPolicy(entries.length).aggregateMs,perImageBudgetMs=DEFAULT_CATALOG_IMAGE_BUDGET_MS):Promise<CatalogImageProvisionResult>{
if(!entries.length)return{schemaVersion:1,status:'complete',downloads:true,platform,requested:0,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached:false,unavailable:[],summary:'No qualified CSO images are published for this platform; static audits remain available.'};
if(!Number.isSafeInteger(perImageBudgetMs)||perImageBudgetMs<1||perImageBudgetMs>MAX_CATALOG_IMAGE_BUDGET_SECONDS*1000)throw new CsoError('INVALID_ARGUMENT','Catalog per-image budget is invalid');
const deadlineReason='The bounded aggregate CSO image preload deadline was reached';
if(Date.now()>=deadline){const unavailable=entries.map(entry=>({...entry,status:'unavailable' as const,reason:deadlineReason}));return{schemaVersion:1,status:'partial',downloads:true,platform,requested:entries.length,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached:true,unavailable,summary:`Qualified CSO image preload partial: 0/${entries.length} available; ${deadlineReason.toLowerCase()}. Rerun setup to continue.`};}
let session:CatalogImageSession;
try{session=await open(deadline);}catch(error){
const reason=controlledReason(error,'Local Docker is unavailable for qualified image provisioning'),unavailable=entries.map(entry=>({...entry,status:'unavailable' as const,reason}));
const deadlineReached=error instanceof CsoError&&error.code==='DEADLINE';
return{schemaVersion:1,status:deadlineReached?'partial':'not_available',downloads:true,platform,requested:entries.length,inspected:0,alreadyPresent:0,downloaded:0,deadlineReached,unavailable,summary:deadlineReached?`Qualified CSO image preload partial: 0/${entries.length} available; ${reason}. Rerun setup to continue.`:`Qualified CSO images were not preloaded: ${reason}. Rerun setup after the prerequisite is available.`};
export async function provisionCatalogImages(
entries: QualifiedCatalogImage[],
platform: RuntimePlatform,
open: CatalogImageSessionFactory,
deadline = Date.now() + catalogImageProvisioningPolicy(entries.length).aggregateMs,
perImageBudgetMs = DEFAULT_CATALOG_IMAGE_BUDGET_MS,
): Promise<CatalogImageProvisionResult> {
if (!entries.length)
return {
schemaVersion: 1,
status: 'complete',
downloads: true,
platform,
requested: 0,
inspected: 0,
alreadyPresent: 0,
downloaded: 0,
deadlineReached: false,
unavailable: [],
summary: 'No qualified CSO images are published for this platform; static audits remain available.',
};
if (
!Number.isSafeInteger(perImageBudgetMs) ||
perImageBudgetMs < 1 ||
perImageBudgetMs > MAX_CATALOG_IMAGE_BUDGET_SECONDS * 1000
)
throw new CsoError('INVALID_ARGUMENT', 'Catalog per-image budget is invalid');
const deadlineReason = 'The bounded aggregate CSO image preload deadline was reached';
if (Date.now() >= deadline) {
const unavailable = entries.map((entry) => ({
...entry,
status: 'unavailable' as const,
reason: deadlineReason,
}));
return {
schemaVersion: 1,
status: 'partial',
downloads: true,
platform,
requested: entries.length,
inspected: 0,
alreadyPresent: 0,
downloaded: 0,
deadlineReached: true,
unavailable,
summary: `Qualified CSO image preload partial: 0/${entries.length} available; ${deadlineReason.toLowerCase()}. Rerun setup to continue.`,
};
}
let inspected=0,alreadyPresent=0,downloaded=0,pullBlocked='',deadlineReached=false,perImageTimeouts=0;const unavailable:CatalogImageAvailability[]=[];
try{
for(let index=0;index<entries.length;index++){
const entry=entries[index];
if(Date.now()>=deadline){deadlineReached=true;for(const remaining of entries.slice(index))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
const imageDeadline=Math.min(deadline,Date.now()+perImageBudgetMs),perImageReason=`The ${Math.ceil(perImageBudgetMs/1000)}-second per-image CSO preload deadline was reached`;
let present=false;
try{
present=await session.present(entry,imageDeadline);if(Date.now()>=imageDeadline)throw new CsoError('DEADLINE',imageDeadline===deadline?'Exact image inspection reached the aggregate image-provisioning deadline':perImageReason);inspected++;
if(present){alreadyPresent++;continue;}
}catch(error){
if(error instanceof CsoError&&error.code==='DEADLINE'){
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:error.message});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
perImageTimeouts++;unavailable.push({...entry,status:'unavailable',reason:perImageReason});continue;
let session: CatalogImageSession;
try {
session = await open(deadline);
} catch (error) {
const reason = controlledReason(error, 'Local Docker is unavailable for qualified image provisioning'),
unavailable = entries.map((entry) => ({ ...entry, status: 'unavailable' as const, reason }));
const deadlineReached = error instanceof CsoError && error.code === 'DEADLINE';
return {
schemaVersion: 1,
status: deadlineReached ? 'partial' : 'not_available',
downloads: true,
platform,
requested: entries.length,
inspected: 0,
alreadyPresent: 0,
downloaded: 0,
deadlineReached,
unavailable,
summary: deadlineReached
? `Qualified CSO image preload partial: 0/${entries.length} available; ${reason}. Rerun setup to continue.`
: `Qualified CSO images were not preloaded: ${reason}. Rerun setup after the prerequisite is available.`,
};
}
let inspected = 0,
alreadyPresent = 0,
downloaded = 0,
pullBlocked = '',
deadlineReached = false,
perImageTimeouts = 0;
const unavailable: CatalogImageAvailability[] = [];
try {
for (let index = 0; index < entries.length; index++) {
const entry = entries[index];
if (Date.now() >= deadline) {
deadlineReached = true;
for (const remaining of entries.slice(index))
unavailable.push({ ...remaining, status: 'unavailable', reason: deadlineReason });
break;
}
const imageDeadline = Math.min(deadline, Date.now() + perImageBudgetMs),
perImageReason = `The ${Math.ceil(perImageBudgetMs / 1000)}-second per-image CSO preload deadline was reached`;
let present = false;
try {
present = await session.present(entry, imageDeadline);
if (Date.now() >= imageDeadline)
throw new CsoError(
'DEADLINE',
imageDeadline === deadline
? 'Exact image inspection reached the aggregate image-provisioning deadline'
: perImageReason,
);
inspected++;
if (present) {
alreadyPresent++;
continue;
}
unavailable.push({...entry,status:'unavailable',reason:controlledReason(error,'Exact qualified image could not be inspected safely')});continue;
} catch (error) {
if (error instanceof CsoError && error.code === 'DEADLINE') {
if (Date.now() >= deadline) {
deadlineReached = true;
unavailable.push({ ...entry, status: 'unavailable', reason: error.message });
for (const remaining of entries.slice(index + 1))
unavailable.push({ ...remaining, status: 'unavailable', reason: deadlineReason });
break;
}
perImageTimeouts++;
unavailable.push({ ...entry, status: 'unavailable', reason: perImageReason });
continue;
}
unavailable.push({
...entry,
status: 'unavailable',
reason: controlledReason(error, 'Exact qualified image could not be inspected safely'),
});
continue;
}
// A registry failure blocks further network attempts, but read-only local
// inspection continues so the setup summary never calls a cached digest
// unavailable merely because it sorts after the failed pull.
if(pullBlocked){unavailable.push({...entry,status:'unavailable',reason:`Network provisioning stopped after an anonymous registry prerequisite failed: ${pullBlocked}`});continue;}
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:deadlineReason});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
try{await session.pull(entry,imageDeadline);if(Date.now()>=imageDeadline)throw new CsoError('DEADLINE',imageDeadline===deadline?'Qualified image pull reached the aggregate preload deadline':perImageReason);downloaded++;}
catch(error){
if(error instanceof CsoError&&error.code==='DEADLINE'){
if(Date.now()>=deadline){deadlineReached=true;unavailable.push({...entry,status:'unavailable',reason:error.message});for(const remaining of entries.slice(index+1))unavailable.push({...remaining,status:'unavailable',reason:deadlineReason});break;}
perImageTimeouts++;unavailable.push({...entry,status:'unavailable',reason:perImageReason});continue;
if (pullBlocked) {
unavailable.push({
...entry,
status: 'unavailable',
reason: `Network provisioning stopped after an anonymous registry prerequisite failed: ${pullBlocked}`,
});
continue;
}
if (Date.now() >= deadline) {
deadlineReached = true;
unavailable.push({ ...entry, status: 'unavailable', reason: deadlineReason });
for (const remaining of entries.slice(index + 1))
unavailable.push({ ...remaining, status: 'unavailable', reason: deadlineReason });
break;
}
try {
await session.pull(entry, imageDeadline);
if (Date.now() >= imageDeadline)
throw new CsoError(
'DEADLINE',
imageDeadline === deadline
? 'Qualified image pull reached the aggregate preload deadline'
: perImageReason,
);
downloaded++;
} catch (error) {
if (error instanceof CsoError && error.code === 'DEADLINE') {
if (Date.now() >= deadline) {
deadlineReached = true;
unavailable.push({ ...entry, status: 'unavailable', reason: error.message });
for (const remaining of entries.slice(index + 1))
unavailable.push({ ...remaining, status: 'unavailable', reason: deadlineReason });
break;
}
perImageTimeouts++;
unavailable.push({ ...entry, status: 'unavailable', reason: perImageReason });
continue;
}
pullBlocked=controlledReason(error,'Qualified image provisioning failed');unavailable.push({...entry,status:'unavailable',reason:pullBlocked});
pullBlocked = controlledReason(error, 'Qualified image provisioning failed');
unavailable.push({ ...entry, status: 'unavailable', reason: pullBlocked });
}
}
}finally{session.close();}
const status=deadlineReached?'partial':unavailable.length?(alreadyPresent||downloaded?'partial':'not_available'):'complete';
const summary=deadlineReached
?`Qualified CSO image preload partial: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; the bounded aggregate deadline was reached. Rerun setup to continue.`
:perImageTimeouts
?`Qualified CSO image preload ${status}: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${perImageTimeouts} exceeded the ${Math.ceil(perImageBudgetMs/1000)}-second per-image deadline. Increase GSTACK_CSO_IMAGE_PULL_TIMEOUT_SECONDS within 5..300 or rerun setup to continue.`
:unavailable.length
?`Qualified CSO image preload ${status}: ${alreadyPresent+downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${unavailable.length} require local Docker and anonymous public registry access. Rerun setup after the prerequisite is available.`
:`Qualified CSO images ready: ${entries.length} available (${downloaded} downloaded, ${alreadyPresent} already local).`;
return{schemaVersion:1,status,downloads:true,platform,requested:entries.length,inspected,alreadyPresent,downloaded,deadlineReached,unavailable,summary};
} finally {
session.close();
}
const status = deadlineReached
? 'partial'
: unavailable.length
? alreadyPresent || downloaded
? 'partial'
: 'not_available'
: 'complete';
const summary = deadlineReached
? `Qualified CSO image preload partial: ${alreadyPresent + downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; the bounded aggregate deadline was reached. Rerun setup to continue.`
: perImageTimeouts
? `Qualified CSO image preload ${status}: ${alreadyPresent + downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${perImageTimeouts} exceeded the ${Math.ceil(perImageBudgetMs / 1000)}-second per-image deadline. Increase GSTACK_CSO_IMAGE_PULL_TIMEOUT_SECONDS within 5..300 or rerun setup to continue.`
: unavailable.length
? `Qualified CSO image preload ${status}: ${alreadyPresent + downloaded}/${entries.length} available; inspected ${inspected}/${entries.length}; ${unavailable.length} require local Docker and anonymous public registry access. Rerun setup after the prerequisite is available.`
: `Qualified CSO images ready: ${entries.length} available (${downloaded} downloaded, ${alreadyPresent} already local).`;
return {
schemaVersion: 1,
status,
downloads: true,
platform,
requested: entries.length,
inspected,
alreadyPresent,
downloaded,
deadlineReached,
unavailable,
summary,
};
}
export async function openLocalCatalogImageSession(env:Record<string,string|undefined>=process.env,deadline=Date.now()+CATALOG_IMAGE_INSPECTION_BUDGET_MS):Promise<CatalogImageSession>{
let home='';
try{
home=secureDirectory(fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()),'gstack-cso-images-')));
export async function openLocalCatalogImageSession(
env: Record<string, string | undefined> = process.env,
deadline = Date.now() + CATALOG_IMAGE_INSPECTION_BUDGET_MS,
): Promise<CatalogImageSession> {
let home = '';
try {
home = secureDirectory(fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), 'gstack-cso-images-')));
// Endpoint discovery and the daemon probe must not borrow the download
// allowance. A slow or hostile local Docker endpoint gets the same bounded
// admission window in doctor and setup; successful pulls keep the caller's
// larger aggregate deadline below.
const admissionDeadline=Math.min(deadline,Date.now()+CATALOG_IMAGE_ADMISSION_BUDGET_MS);
const endpoint=await dockerEndpoint(home,env,admissionDeadline),config=secureDirectory(join(home,'docker-config'));
const admissionDeadline = Math.min(deadline, Date.now() + CATALOG_IMAGE_ADMISSION_BUDGET_MS);
const endpoint = await dockerEndpoint(home, env, admissionDeadline),
config = secureDirectory(join(home, 'docker-config'));
// dockerEnvironment pins both HOME and DOCKER_CONFIG here. An explicit
// empty auth map prevents inherited credential stores/helpers from being
// consulted during installation-time public pulls.
fs.writeFileSync(join(config,'config.json'),'{"auths":{}}\n',{encoding:'utf8',mode:0o600,flag:'wx'});
const probe=await dockerProbe(endpoint,home,admissionDeadline),docker={endpoint:endpoint.uri,...probe};
let closed=false;
return{
fs.writeFileSync(join(config, 'config.json'), '{"auths":{}}\n', {
encoding: 'utf8',
mode: 0o600,
flag: 'wx',
});
const probe = await dockerProbe(endpoint, home, admissionDeadline),
docker = { endpoint: endpoint.uri, ...probe };
let closed = false;
return {
docker,
present:(entry,operationDeadline=deadline)=>{if(closed)throw new CsoError('ISOLATION_FAILED','Catalog image session is closed');return dockerExactImagePresent(endpoint,home,entry.image,entry.platform,Math.min(deadline,operationDeadline));},
pull:(entry,operationDeadline=deadline)=>{if(closed)throw new CsoError('ISOLATION_FAILED','Catalog image session is closed');return dockerPullExactCatalogImage(endpoint,home,entry.image,entry.platform,Math.min(deadline,operationDeadline));},
close:()=>{if(closed)return;closed=true;fs.rmSync(home,{recursive:true,force:true});},
present: (entry, operationDeadline = deadline) => {
if (closed) throw new CsoError('ISOLATION_FAILED', 'Catalog image session is closed');
return dockerExactImagePresent(
endpoint,
home,
entry.image,
entry.platform,
Math.min(deadline, operationDeadline),
);
},
pull: (entry, operationDeadline = deadline) => {
if (closed) throw new CsoError('ISOLATION_FAILED', 'Catalog image session is closed');
return dockerPullExactCatalogImage(
endpoint,
home,
entry.image,
entry.platform,
Math.min(deadline, operationDeadline),
);
},
close: () => {
if (closed) return;
closed = true;
fs.rmSync(home, { recursive: true, force: true });
},
};
}catch(error){if(home)fs.rmSync(home,{recursive:true,force:true});throw error;}
} catch (error) {
if (home) fs.rmSync(home, { recursive: true, force: true });
throw error;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+875 -164
View File
File diff suppressed because it is too large. Load diff
+532 -149
View File
@@ -1,218 +1,601 @@
import { spawn } from 'node:child_process';
import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
import {
accessSync,
closeSync,
constants,
existsSync,
fstatSync,
lstatSync,
openSync,
readSync,
realpathSync,
statSync,
} from 'node:fs';
import { basename, dirname, join, isAbsolute, delimiter, resolve } from 'node:path';
import { redactFindingSpans } from '../redact-engine';
import { CsoError, MAX_OUTPUT } from './contracts';
const SOURCE_RUNTIME=/^bun(?:\.exe)?$/i.test(basename(process.execPath));
const WINDOWS_GIT=process.platform==='win32'?(process.env.GSTACK_CSO_TRUSTED_GIT||(SOURCE_RUNTIME?Bun.which('git')??'':'')):'';
const WINDOWS_SYSTEM=process.platform==='win32'?join(process.env.SystemRoot||'C:\\Windows','System32'):'';
export const TRUSTED_DIRECTORIES = process.platform === 'win32'
? [...new Set([WINDOWS_GIT?dirname(WINDOWS_GIT):'',WINDOWS_SYSTEM].filter(Boolean))]
: ['/usr/local/bin','/usr/bin','/bin','/opt/homebrew/bin','/usr/local/sbin','/usr/sbin','/sbin'];
const SOURCE_RUNTIME = /^bun(?:\.exe)?$/i.test(basename(process.execPath));
const WINDOWS_GIT =
process.platform === 'win32'
? process.env.GSTACK_CSO_TRUSTED_GIT || (SOURCE_RUNTIME ? (Bun.which('git') ?? '') : '')
: '';
const WINDOWS_SYSTEM =
process.platform === 'win32' ? join(process.env.SystemRoot || 'C:\\Windows', 'System32') : '';
export const TRUSTED_DIRECTORIES =
process.platform === 'win32'
? [...new Set([WINDOWS_GIT ? dirname(WINDOWS_GIT) : '', WINDOWS_SYSTEM].filter(Boolean))]
: ['/usr/local/bin', '/usr/bin', '/bin', '/opt/homebrew/bin', '/usr/local/sbin', '/usr/sbin', '/sbin'];
export const TRUSTED_PATH = TRUSTED_DIRECTORIES.join(delimiter);
export function executable(name: string): string {
// Never consult the audited repository's PATH or executable overrides.
if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw new CsoError('INVALID_ARGUMENT','Invalid executable name');
if(process.platform==='win32'&&name.toLowerCase()==='git'){
try{if(!WINDOWS_GIT||!isAbsolute(WINDOWS_GIT)||basename(WINDOWS_GIT).toLowerCase()!=='git.exe')throw new Error();const stat=statSync(WINDOWS_GIT);if(!stat.isFile())throw new Error();return realpathSync(WINDOWS_GIT);}catch{throw new CsoError('TOOL_UNAVAILABLE','git.exe is not the trusted executable bound during gstack setup');}
if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw new CsoError('INVALID_ARGUMENT', 'Invalid executable name');
if (process.platform === 'win32' && name.toLowerCase() === 'git') {
try {
if (!WINDOWS_GIT || !isAbsolute(WINDOWS_GIT) || basename(WINDOWS_GIT).toLowerCase() !== 'git.exe')
throw new Error();
const stat = statSync(WINDOWS_GIT);
if (!stat.isFile()) throw new Error();
return realpathSync(WINDOWS_GIT);
} catch {
throw new CsoError(
'TOOL_UNAVAILABLE',
'git.exe is not the trusted executable bound during gstack setup',
);
}
}
for (const directory of TRUSTED_DIRECTORIES) {
const candidates=process.platform==='win32'?[join(directory,`${name}.exe`),join(directory,`${name}.cmd`),join(directory,name)]:[join(directory,name)];
for(const p of candidates){
try { const stat=statSync(p);accessSync(p,constants.X_OK);if(stat.isFile()&&(process.platform==='win32'||(stat.mode&0o111)))return realpathSync(p); } catch {}
const candidates =
process.platform === 'win32'
? [join(directory, `${name}.exe`), join(directory, `${name}.cmd`), join(directory, name)]
: [join(directory, name)];
for (const p of candidates) {
try {
const stat = statSync(p);
accessSync(p, constants.X_OK);
if (stat.isFile() && (process.platform === 'win32' || stat.mode & 0o111)) return realpathSync(p);
} catch {}
}
}
throw new CsoError('TOOL_UNAVAILABLE', `${name} is not installed in a trusted system executable directory`);
}
export function childEnvironment(home: string): Record<string,string> {
return { PATH: TRUSTED_PATH, HOME: home, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC',
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform==='win32'?'NUL':'/dev/null', GIT_TERMINAL_PROMPT: '0',
GIT_OPTIONAL_LOCKS: '0', GIT_ATTR_NOSYSTEM: '1' };
export function childEnvironment(home: string): Record<string, string> {
return {
PATH: TRUSTED_PATH,
HOME: home,
LANG: 'C.UTF-8',
LC_ALL: 'C.UTF-8',
TZ: 'UTC',
GIT_CONFIG_NOSYSTEM: '1',
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
GIT_TERMINAL_PROMPT: '0',
GIT_OPTIONAL_LOCKS: '0',
GIT_ATTR_NOSYSTEM: '1',
};
}
export function redact(value: string): string {
// Scan the complete bounded stream, including across write/chunk boundaries.
const output = redactFindingSpans(value, { maxBytes: MAX_OUTPUT });
if (output === null) throw new CsoError('REDACTION_FAILED','Payload withheld because redaction could not safely locate every secret');
if (output === null)
throw new CsoError(
'REDACTION_FAILED',
'Payload withheld because redaction could not safely locate every secret',
);
return output;
}
const HASH_KEYS=new Set(['planSha256','planHash','originalHash','executionHash','snapshotHash','sourceHash','beforeSha256','afterSha256','patchHash','reviewedPatchHash','harnessHash','fixturesHash','policyHash','auditPolicyHash','originalSourceHash','transformationsHash','archivesHash','inputHash','beforeSourceHash','afterSourceHash','beforeDependencies','afterDependencies','beforeConfiguration','afterConfiguration','requestHash','startPlanHash','testPlanHash','preparationHash','preparedManifestHash','preparedDependencyHash','sourceProjectionHash','executionEnvironmentHash','databaseHash','receiptHash','dependencyClosureHash','closureHash','acquisitionReceiptHash','registryResponseSha256','sha256','versionOutputSha256','isolationPolicyHash','contentSha256','sbomDigest','provenanceDigest','dependencyHash','configurationHash','assertionHash','commandsHash','minimumPassingTestsHash','commandHash','outputHash','observationHash','witnessHash','keyId']);
function safeMetadata(value:string,key:string):boolean{
if(HASH_KEYS.has(key)&&/^[a-f0-9]{64}$/.test(value))return true;
if(['id','fingerprint','findingId','verificationId','reproductionAttemptId','artifactId','reviewArtifactId','bundleId','pathId'].includes(key)&&/^[a-f0-9]{32}$/.test(value))return true;
if(key==='path'&&/^@cso-path\/\/[a-f0-9]{32}$/.test(value))return true;
if(key==='repoId'&&/^[a-f0-9]{24}$/.test(value))return true;
if(key==='runId'&&/^\d{13}-[a-f0-9]{16}$/.test(value))return true;
if(key==='replayId'&&/^\d{13}-[a-f0-9]{16}$/.test(value))return true;
if(['baseCommit','headCommit'].includes(key)&&/^[a-f0-9]{40,64}$/.test(value))return true;
if(['createdAt','expiresAt','deadline','at','databaseUpdatedAt','qualifiedAt'].includes(key)&&/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(value))return true;
if(key==='nonce'&&/^[a-f0-9]{64}$/.test(value))return true;
if(key==='publicKey'&&/^[a-f0-9]{88}$/.test(value))return true;
if(key==='signature'&&/^[a-f0-9]{128}$/.test(value))return true;
if(key==='image'&&/^[a-z0-9./:_-]+@sha256:[a-f0-9]{64}$/.test(value))return true;
if(key==='integrity'&&/^(?:sha256|sha512)-[A-Za-z0-9+/]+={0,2}$/.test(value))return true;
const HASH_KEYS = new Set([
'planSha256',
'planHash',
'originalHash',
'executionHash',
'snapshotHash',
'sourceHash',
'beforeSha256',
'afterSha256',
'patchHash',
'reviewedPatchHash',
'harnessHash',
'fixturesHash',
'policyHash',
'auditPolicyHash',
'originalSourceHash',
'transformationsHash',
'archivesHash',
'inputHash',
'beforeSourceHash',
'afterSourceHash',
'beforeDependencies',
'afterDependencies',
'beforeConfiguration',
'afterConfiguration',
'requestHash',
'startPlanHash',
'testPlanHash',
'preparationHash',
'preparedManifestHash',
'preparedDependencyHash',
'sourceProjectionHash',
'executionEnvironmentHash',
'databaseHash',
'receiptHash',
'dependencyClosureHash',
'closureHash',
'acquisitionReceiptHash',
'registryResponseSha256',
'sha256',
'versionOutputSha256',
'isolationPolicyHash',
'contentSha256',
'sbomDigest',
'provenanceDigest',
'dependencyHash',
'configurationHash',
'assertionHash',
'commandsHash',
'minimumPassingTestsHash',
'commandHash',
'outputHash',
'observationHash',
'witnessHash',
'keyId',
]);
function safeMetadata(value: string, key: string): boolean {
if (HASH_KEYS.has(key) && /^[a-f0-9]{64}$/.test(value)) return true;
if (
[
'id',
'fingerprint',
'findingId',
'verificationId',
'reproductionAttemptId',
'artifactId',
'reviewArtifactId',
'bundleId',
'pathId',
].includes(key) &&
/^[a-f0-9]{32}$/.test(value)
)
return true;
if (key === 'path' && /^@cso-path\/\/[a-f0-9]{32}$/.test(value)) return true;
if (key === 'repoId' && /^[a-f0-9]{24}$/.test(value)) return true;
if (key === 'runId' && /^\d{13}-[a-f0-9]{16}$/.test(value)) return true;
if (key === 'replayId' && /^\d{13}-[a-f0-9]{16}$/.test(value)) return true;
if (['baseCommit', 'headCommit'].includes(key) && /^[a-f0-9]{40,64}$/.test(value)) return true;
if (
['createdAt', 'expiresAt', 'deadline', 'at', 'databaseUpdatedAt', 'qualifiedAt'].includes(key) &&
/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(value)
)
return true;
if (key === 'nonce' && /^[a-f0-9]{64}$/.test(value)) return true;
if (key === 'publicKey' && /^[a-f0-9]{88}$/.test(value)) return true;
if (key === 'signature' && /^[a-f0-9]{128}$/.test(value)) return true;
if (key === 'image' && /^[a-z0-9./:_-]+@sha256:[a-f0-9]{64}$/.test(value)) return true;
if (key === 'integrity' && /^(?:sha256|sha512)-[A-Za-z0-9+/]+={0,2}$/.test(value)) return true;
return false;
}
function sanitizeJson(value:unknown,key:string,seen:WeakSet<object>,trustedMetadata:boolean):unknown{
if(typeof value==='string'){
if(trustedMetadata&&safeMetadata(value,key))return value;
function sanitizeJson(value: unknown, key: string, seen: WeakSet<object>, trustedMetadata: boolean): unknown {
if (typeof value === 'string') {
if (trustedMetadata && safeMetadata(value, key)) return value;
return redact(value);
}
if(value===null||typeof value!=='object')return value;
if(seen.has(value as object))throw new CsoError('INVALID_SCHEMA','Cyclic JSON cannot be persisted');seen.add(value as object);
if(Array.isArray(value)){const out=value.map(v=>sanitizeJson(v,key,seen,trustedMetadata));seen.delete(value);return out;}
const out:Record<string,unknown>=Object.create(null);for(const [k,v] of Object.entries(value as Record<string,unknown>)){
if(['__proto__','prototype','constructor'].includes(k))throw new CsoError('INVALID_SCHEMA','Unsafe JSON property');out[k]=sanitizeJson(v,k,seen,trustedMetadata);
}seen.delete(value as object);return out;
if (value === null || typeof value !== 'object') return value;
if (seen.has(value as object)) throw new CsoError('INVALID_SCHEMA', 'Cyclic JSON cannot be persisted');
seen.add(value as object);
if (Array.isArray(value)) {
const out = value.map((v) => sanitizeJson(v, key, seen, trustedMetadata));
seen.delete(value);
return out;
}
const out: Record<string, unknown> = Object.create(null);
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
if (['__proto__', 'prototype', 'constructor'].includes(k))
throw new CsoError('INVALID_SCHEMA', 'Unsafe JSON property');
out[k] = sanitizeJson(v, k, seen, trustedMetadata);
}
seen.delete(value as object);
return out;
}
/** Redact untrusted JSON content. Key names never make an untrusted value exempt. */
export function sanitizeForJson(value:unknown):unknown{return sanitizeJson(value,'',new WeakSet<object>(),false);}
export function sanitizeForJson(value: unknown): unknown {
return sanitizeJson(value, '', new WeakSet<object>(), false);
}
/** Preserve only validated helper identifiers/hashes while redacting all content-bearing fields. */
export function sanitizeHelperForJson(value:unknown):unknown{return sanitizeJson(value,'',new WeakSet<object>(),true);}
export interface ProcessResult { code: number; stdout: string; stderr: string; timedOut: boolean; truncated: boolean; capturedBytes:number }
interface GitConfigIdentity { path:string; exists:boolean; dev?:number; ino?:number; mode?:number; size?:number; mtimeMs?:number; ctimeMs?:number; content?:string }
const GIT_CONFIG_LIMIT=1024*1024;
interface BoundedMetadataFile { dev:number;ino:number;mode:number;nlink:number;size:number;mtimeMs:number;ctimeMs:number;content:string }
function sameMetadataFile(left:BoundedMetadataFile|ReturnType<typeof lstatSync>,right:BoundedMetadataFile|ReturnType<typeof lstatSync>):boolean{
return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.size===right.size&&left.mtimeMs===right.mtimeMs&&left.ctimeMs===right.ctimeMs;
export function sanitizeHelperForJson(value: unknown): unknown {
return sanitizeJson(value, '', new WeakSet<object>(), true);
}
function boundedMetadataFile(path:string,maxBytes:number,label:string,optional=false):BoundedMetadataFile|undefined{
let before:ReturnType<typeof lstatSync>;
try{before=lstatSync(path);}catch(error:any){if(optional&&error?.code==='ENOENT')return;throw new CsoError(error?.code==='ENOENT'?'SNAPSHOT_RACE':'UNSAFE_PATH',`${label} is not a bounded regular file`);}
if(before.isSymbolicLink()||!before.isFile()||before.nlink!==1||before.size>maxBytes)throw new CsoError('UNSAFE_PATH',`${label} is not a bounded regular file`);
let fd:number|undefined;
try{
fd=openSync(path,constants.O_RDONLY|(constants.O_NOFOLLOW??0)|(constants.O_NONBLOCK??0));
const opened=fstatSync(fd);
if(!opened.isFile()||opened.nlink!==1||opened.size>maxBytes||!sameMetadataFile(before,opened))throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was opened`);
const buffer=Buffer.alloc(Math.min(maxBytes+1,opened.size+1));let bytes=0,count=0;
while(bytes<buffer.length&&(count=readSync(fd,buffer,bytes,buffer.length-bytes,null))>0)bytes+=count;
const final=fstatSync(fd),after=lstatSync(path);
if(bytes!==opened.size||!final.isFile()||!after.isFile()||after.isSymbolicLink()||!sameMetadataFile(opened,final)||!sameMetadataFile(opened,after))
throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was read`);
return{dev:opened.dev,ino:opened.ino,mode:opened.mode,nlink:opened.nlink,size:opened.size,mtimeMs:opened.mtimeMs,ctimeMs:opened.ctimeMs,content:buffer.subarray(0,bytes).toString('utf8')};
}catch(error:any){
if(error instanceof CsoError)throw error;
if(['ENOENT','ELOOP','ENXIO'].includes(error?.code))throw new CsoError('SNAPSHOT_RACE',`${label} changed while it was opened`);
throw new CsoError('UNSAFE_PATH',`${label} could not be read safely`);
}finally{if(fd!==undefined)try{closeSync(fd);}catch{}}
export interface ProcessResult {
code: number;
stdout: string;
stderr: string;
timedOut: boolean;
truncated: boolean;
capturedBytes: number;
}
function boundedConfig(path:string):GitConfigIdentity{
const file=boundedMetadataFile(path,GIT_CONFIG_LIMIT,'Repository Git configuration',true);
if(!file)return{path,exists:false};
const {content}=file;
interface GitConfigIdentity {
path: string;
exists: boolean;
dev?: number;
ino?: number;
mode?: number;
size?: number;
mtimeMs?: number;
ctimeMs?: number;
content?: string;
}
const GIT_CONFIG_LIMIT = 1024 * 1024;
interface BoundedMetadataFile {
dev: number;
ino: number;
mode: number;
nlink: number;
size: number;
mtimeMs: number;
ctimeMs: number;
content: string;
}
function sameMetadataFile(
left: BoundedMetadataFile | ReturnType<typeof lstatSync>,
right: BoundedMetadataFile | ReturnType<typeof lstatSync>,
): boolean {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.mode === right.mode &&
left.nlink === right.nlink &&
left.size === right.size &&
left.mtimeMs === right.mtimeMs &&
left.ctimeMs === right.ctimeMs
);
}
function boundedMetadataFile(
path: string,
maxBytes: number,
label: string,
optional = false,
): BoundedMetadataFile | undefined {
let before: ReturnType<typeof lstatSync>;
try {
before = lstatSync(path);
} catch (error: any) {
if (optional && error?.code === 'ENOENT') return;
throw new CsoError(
error?.code === 'ENOENT' ? 'SNAPSHOT_RACE' : 'UNSAFE_PATH',
`${label} is not a bounded regular file`,
);
}
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size > maxBytes)
throw new CsoError('UNSAFE_PATH', `${label} is not a bounded regular file`);
let fd: number | undefined;
try {
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
const opened = fstatSync(fd);
if (!opened.isFile() || opened.nlink !== 1 || opened.size > maxBytes || !sameMetadataFile(before, opened))
throw new CsoError('SNAPSHOT_RACE', `${label} changed while it was opened`);
const buffer = Buffer.alloc(Math.min(maxBytes + 1, opened.size + 1));
let bytes = 0,
count = 0;
while (bytes < buffer.length && (count = readSync(fd, buffer, bytes, buffer.length - bytes, null)) > 0)
bytes += count;
const final = fstatSync(fd),
after = lstatSync(path);
if (
bytes !== opened.size ||
!final.isFile() ||
!after.isFile() ||
after.isSymbolicLink() ||
!sameMetadataFile(opened, final) ||
!sameMetadataFile(opened, after)
)
throw new CsoError('SNAPSHOT_RACE', `${label} changed while it was read`);
return {
dev: opened.dev,
ino: opened.ino,
mode: opened.mode,
nlink: opened.nlink,
size: opened.size,
mtimeMs: opened.mtimeMs,
ctimeMs: opened.ctimeMs,
content: buffer.subarray(0, bytes).toString('utf8'),
};
} catch (error: any) {
if (error instanceof CsoError) throw error;
if (['ENOENT', 'ELOOP', 'ENXIO'].includes(error?.code))
throw new CsoError('SNAPSHOT_RACE', `${label} changed while it was opened`);
throw new CsoError('UNSAFE_PATH', `${label} could not be read safely`);
} finally {
if (fd !== undefined)
try {
closeSync(fd);
} catch {}
}
}
function boundedConfig(path: string): GitConfigIdentity {
const file = boundedMetadataFile(path, GIT_CONFIG_LIMIT, 'Repository Git configuration', true);
if (!file) return { path, exists: false };
const { content } = file;
// There is no process-wide "--no-includes" switch for ordinary Git
// commands. Reject include directives before spawning Git so repository
// configuration cannot pull policy or executable settings from elsewhere.
if(/^\s*\[\s*include(?:if)?(?=[\s."\]])/im.test(content))throw new CsoError('UNSAFE_PATH','Repository Git config includes are not allowed during a security snapshot');
return{path,exists:true,dev:file.dev,ino:file.ino,mode:file.mode,size:file.size,mtimeMs:file.mtimeMs,ctimeMs:file.ctimeMs,content};
if (/^\s*\[\s*include(?:if)?(?=[\s."\]])/im.test(content))
throw new CsoError(
'UNSAFE_PATH',
'Repository Git config includes are not allowed during a security snapshot',
);
return {
path,
exists: true,
dev: file.dev,
ino: file.ino,
mode: file.mode,
size: file.size,
mtimeMs: file.mtimeMs,
ctimeMs: file.ctimeMs,
content,
};
}
function gitDirectories(repo:string):{gitDir:string;commonDir:string}{
const marker=join(repo,'.git'),stat=lstatSync(marker);let gitDir:string;
if(stat.isDirectory()&&!stat.isSymbolicLink())gitDir=realpathSync(marker);
else if(stat.isFile()&&!stat.isSymbolicLink()&&stat.nlink===1&&stat.size<=8192){
const value=boundedMetadataFile(marker,8192,'Repository .git pointer')!.content,match=value.match(/^gitdir:\s*(.+?)\s*$/);
if(!match||value.includes('\0')||value.split(/\r?\n/).filter(Boolean).length!==1)throw new CsoError('UNSAFE_PATH','Repository .git pointer is invalid');
gitDir=realpathSync(resolve(dirname(marker),match[1]));
}else throw new CsoError('UNSAFE_PATH','Repository .git metadata is not a regular directory or worktree pointer');
const commonMarker=join(gitDir,'commondir'),commonFile=boundedMetadataFile(commonMarker,8192,'Repository common Git directory pointer',true);let commonDir=gitDir;
if(commonFile){
const value=commonFile.content.trim();
if(!value||value.includes('\0')||value.includes('\n')||value.includes('\r'))throw new CsoError('UNSAFE_PATH','Repository common Git directory pointer is invalid');
commonDir=realpathSync(resolve(gitDir,value));
function gitDirectories(repo: string): { gitDir: string; commonDir: string } {
const marker = join(repo, '.git'),
stat = lstatSync(marker);
let gitDir: string;
if (stat.isDirectory() && !stat.isSymbolicLink()) gitDir = realpathSync(marker);
else if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && stat.size <= 8192) {
const value = boundedMetadataFile(marker, 8192, 'Repository .git pointer')!.content,
match = value.match(/^gitdir:\s*(.+?)\s*$/);
if (!match || value.includes('\0') || value.split(/\r?\n/).filter(Boolean).length !== 1)
throw new CsoError('UNSAFE_PATH', 'Repository .git pointer is invalid');
gitDir = realpathSync(resolve(dirname(marker), match[1]));
} else
throw new CsoError(
'UNSAFE_PATH',
'Repository .git metadata is not a regular directory or worktree pointer',
);
const commonMarker = join(gitDir, 'commondir'),
commonFile = boundedMetadataFile(commonMarker, 8192, 'Repository common Git directory pointer', true);
let commonDir = gitDir;
if (commonFile) {
const value = commonFile.content.trim();
if (!value || value.includes('\0') || value.includes('\n') || value.includes('\r'))
throw new CsoError('UNSAFE_PATH', 'Repository common Git directory pointer is invalid');
commonDir = realpathSync(resolve(gitDir, value));
}
return{gitDir,commonDir};
return { gitDir, commonDir };
}
function gitConfigIdentities(repo:string):GitConfigIdentity[]{
const {gitDir,commonDir}=gitDirectories(repo);
function gitConfigIdentities(repo: string): GitConfigIdentity[] {
const { gitDir, commonDir } = gitDirectories(repo);
// extensions.worktreeConfig makes config.worktree active in both linked and
// main worktrees. Bind even its absence so it cannot appear after inspection
// and feed Git an unchecked include or executable setting.
return [join(commonDir,'config'),join(gitDir,'config.worktree')].map(boundedConfig);
return [join(commonDir, 'config'), join(gitDir, 'config.worktree')].map(boundedConfig);
}
function assertGitConfigIdentities(expected:GitConfigIdentity[]):void{
for(const item of expected){
const current=boundedConfig(item.path);
if(current.exists!==item.exists||current.dev!==item.dev||current.ino!==item.ino||current.mode!==item.mode||current.size!==item.size||current.mtimeMs!==item.mtimeMs||current.ctimeMs!==item.ctimeMs||current.content!==item.content)
throw new CsoError('SNAPSHOT_RACE','Repository Git configuration changed during a metadata operation');
function assertGitConfigIdentities(expected: GitConfigIdentity[]): void {
for (const item of expected) {
const current = boundedConfig(item.path);
if (
current.exists !== item.exists ||
current.dev !== item.dev ||
current.ino !== item.ino ||
current.mode !== item.mode ||
current.size !== item.size ||
current.mtimeMs !== item.mtimeMs ||
current.ctimeMs !== item.ctimeMs ||
current.content !== item.content
)
throw new CsoError('SNAPSHOT_RACE', 'Repository Git configuration changed during a metadata operation');
}
}
function hardenGit(file:string,args:string[]):{args:string[];configs?:GitConfigIdentity[]}{
if(!/^(?:git|git\.exe)$/i.test(basename(file)))return{args};
let trusted:string;try{trusted=executable('git');}catch{return{args};}
if(realpathSync(file)!==trusted)return{args};
const positions=args.flatMap((value,index)=>value==='-C'?[index]:[]);
if(positions.length!==1||positions[0]+1>=args.length)throw new CsoError('INVALID_ARGUMENT','CSO Git operations require exactly one audited working directory');
const position=positions[0],requested=args[position+1];
if(!isAbsolute(requested))throw new CsoError('INVALID_ARGUMENT','CSO Git operations require an absolute audited working directory');
const repo=realpathSync(requested),stat=statSync(repo);
if(!stat.isDirectory())throw new CsoError('MISSING_INPUT','Audited Git working directory is not a directory');
const configs=gitConfigIdentities(repo),nullPath=process.platform==='win32'?'NUL':'/dev/null',
function hardenGit(file: string, args: string[]): { args: string[]; configs?: GitConfigIdentity[] } {
if (!/^(?:git|git\.exe)$/i.test(basename(file))) return { args };
let trusted: string;
try {
trusted = executable('git');
} catch {
return { args };
}
if (realpathSync(file) !== trusted) return { args };
const positions = args.flatMap((value, index) => (value === '-C' ? [index] : []));
if (positions.length !== 1 || positions[0] + 1 >= args.length)
throw new CsoError(
'INVALID_ARGUMENT',
'CSO Git operations require exactly one audited working directory',
);
const position = positions[0],
requested = args[position + 1];
if (!isAbsolute(requested))
throw new CsoError(
'INVALID_ARGUMENT',
'CSO Git operations require an absolute audited working directory',
);
const repo = realpathSync(requested),
stat = statSync(repo);
if (!stat.isDirectory())
throw new CsoError('MISSING_INPUT', 'Audited Git working directory is not a directory');
const configs = gitConfigIdentities(repo),
nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null',
// Git for Windows accepts NUL for ordinary file-valued settings, but its
// config include machinery treats NUL as a failing include. Its MSYS path
// layer maps /dev/null correctly for this one directive.
includeNullPath=process.platform==='win32'?'/dev/null':nullPath;
const prefix=args.slice(0,position),command=args.slice(position+2);
return{configs,args:[...prefix,
'--no-replace-objects',
'-c','core.fsmonitor=false','-c',`core.hooksPath=${nullPath}`,'-c',`core.attributesFile=${nullPath}`,
'-c',`core.excludesFile=${nullPath}`,'-c','core.ignoreCase=false','-c','core.precomposeUnicode=false',
'-c','core.untrackedCache=false','-c',`include.path=${includeNullPath}`,'-c','core.pager=cat',
'-C',repo,`--work-tree=${repo}`,...command]};
includeNullPath = process.platform === 'win32' ? '/dev/null' : nullPath;
const prefix = args.slice(0, position),
command = args.slice(position + 2);
return {
configs,
args: [
...prefix,
'--no-replace-objects',
'-c',
'core.fsmonitor=false',
'-c',
`core.hooksPath=${nullPath}`,
'-c',
`core.attributesFile=${nullPath}`,
'-c',
`core.excludesFile=${nullPath}`,
'-c',
'core.ignoreCase=false',
'-c',
'core.precomposeUnicode=false',
'-c',
'core.untrackedCache=false',
'-c',
`include.path=${includeNullPath}`,
'-c',
'core.pager=cat',
'-C',
repo,
`--work-tree=${repo}`,
...command,
],
};
}
export async function runProcess(file: string, args: string[], opts: {
cwd: string; env: Record<string,string>; timeoutMs?: number; maxBytes?: number; input?: string;
raw?: boolean; // Only for inert Git framing or private helper/Docker control JSON that is validated before use. Never print or persist raw results.
}): Promise<ProcessResult> {
if (!isAbsolute(file) || !isAbsolute(opts.cwd) || !existsSync(opts.cwd)) throw new CsoError('INVALID_ARGUMENT','Children require absolute executables and an existing trusted working directory');
if (!args.every(a => typeof a === 'string' && !a.includes('\0'))) throw new CsoError('INVALID_ARGUMENT','Invalid child argument');
const hardened=hardenGit(file,args);args=hardened.args;
export async function runProcess(
file: string,
args: string[],
opts: {
cwd: string;
env: Record<string, string>;
timeoutMs?: number;
maxBytes?: number;
input?: string;
raw?: boolean; // Only for inert Git framing or private helper/Docker control JSON that is validated before use. Never print or persist raw results.
},
): Promise<ProcessResult> {
if (!isAbsolute(file) || !isAbsolute(opts.cwd) || !existsSync(opts.cwd))
throw new CsoError(
'INVALID_ARGUMENT',
'Children require absolute executables and an existing trusted working directory',
);
if (!args.every((a) => typeof a === 'string' && !a.includes('\0')))
throw new CsoError('INVALID_ARGUMENT', 'Invalid child argument');
const hardened = hardenGit(file, args);
args = hardened.args;
const cap = Math.min(opts.maxBytes ?? MAX_OUTPUT, MAX_OUTPUT);
return new Promise((resolve,reject) => {
const child = spawn(file,args,{cwd:opts.cwd,env:opts.env,stdio:['pipe','pipe','pipe'],detached:process.platform !== 'win32'});
const out: Buffer[] = [], err: Buffer[] = [], ordered:Buffer[]=[]; let bytes = 0, timedOut = false, truncated = false;
const kill = () => { try { if (process.platform !== 'win32' && child.pid) process.kill(-child.pid,'SIGKILL'); else child.kill('SIGKILL'); } catch {} };
const timer = setTimeout(() => { timedOut = true; kill(); }, Math.max(1,Math.min(opts.timeoutMs ?? 30_000,300_000)));
return new Promise((resolve, reject) => {
const child = spawn(file, args, {
cwd: opts.cwd,
env: opts.env,
stdio: ['pipe', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
});
const out: Buffer[] = [],
err: Buffer[] = [],
ordered: Buffer[] = [];
let bytes = 0,
timedOut = false,
truncated = false;
const kill = () => {
try {
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, 'SIGKILL');
else child.kill('SIGKILL');
} catch {}
};
const timer = setTimeout(
() => {
timedOut = true;
kill();
},
Math.max(1, Math.min(opts.timeoutMs ?? 30_000, 300_000)),
);
const capture = (target: Buffer[]) => (chunk: Buffer) => {
bytes += chunk.length;
if (bytes > cap) { truncated = true; kill(); return; }
target.push(chunk);ordered.push(chunk);
if (bytes > cap) {
truncated = true;
kill();
return;
}
target.push(chunk);
ordered.push(chunk);
};
child.stdout.on('data',capture(out)); child.stderr.on('data',capture(err));
child.on('error',() => { clearTimeout(timer); reject(new CsoError('TOOL_UNAVAILABLE','Trusted child process could not start')); });
child.on('close',code => {
child.stdout.on('data', capture(out));
child.stderr.on('data', capture(err));
child.on('error', () => {
clearTimeout(timer);
reject(new CsoError('TOOL_UNAVAILABLE', 'Trusted child process could not start'));
});
child.on('close', (code) => {
clearTimeout(timer);
try {
if(hardened.configs)assertGitConfigIdentities(hardened.configs);
if (hardened.configs) assertGitConfigIdentities(hardened.configs);
// Never expose a truncated tail: it might be the beginning of a secret.
const stdout = truncated ? '[output withheld: size limit]' : Buffer.concat(out).toString('utf8');
const stderr = truncated ? '' : Buffer.concat(err).toString('utf8');
if(opts.raw){resolve({code:code ?? -1,stdout,stderr,timedOut,truncated,capturedBytes:bytes});return;}
if (opts.raw) {
resolve({ code: code ?? -1, stdout, stderr, timedOut, truncated, capturedBytes: bytes });
return;
}
// A token may be split across stdout/stderr. Stream ordering is not
// recoverable here, so scan both concatenation orders and withhold both
// channels when either reveals a cross-stream sensitive span.
const forward=stdout+stderr,reverse=stderr+stdout,chronological=Buffer.concat(ordered).toString('utf8');
if([stdout,stderr,forward,reverse,chronological].some(value=>redact(value)!==value)){
resolve({code:code ?? -1,stdout:'[sensitive process output redacted]',stderr:'',timedOut,truncated,capturedBytes:bytes});return;
const forward = stdout + stderr,
reverse = stderr + stdout,
chronological = Buffer.concat(ordered).toString('utf8');
if ([stdout, stderr, forward, reverse, chronological].some((value) => redact(value) !== value)) {
resolve({
code: code ?? -1,
stdout: '[sensitive process output redacted]',
stderr: '',
timedOut,
truncated,
capturedBytes: bytes,
});
return;
}
resolve({code:code ?? -1,stdout,stderr,timedOut,truncated,capturedBytes:bytes});
} catch (e) { reject(e); }
resolve({ code: code ?? -1, stdout, stderr, timedOut, truncated, capturedBytes: bytes });
} catch (e) {
reject(e);
}
});
child.stdin.on('error',() => {}); child.stdin.end(opts.input);
child.stdin.on('error', () => {});
child.stdin.end(opts.input);
});
}
export async function git(repo: string, args: string[], home: string): Promise<string> {
const result = await runProcess(executable('git'),['--no-optional-locks','-C',repo,...args],
{cwd:home,env:childEnvironment(home),raw:true,timeoutMs:15_000});
const result = await runProcess(executable('git'), ['--no-optional-locks', '-C', repo, ...args], {
cwd: home,
env: childEnvironment(home),
raw: true,
timeoutMs: 15_000,
});
if (result.code || result.timedOut || result.truncated) {
// Git stderr and argv can contain repository paths, refs, and configured
// content. Name only the fixed helper-owned operation and bounded process
// outcome so native failures are actionable without exposing either.
const knownOperations=new Set(['rev-parse','symbolic-ref','ls-files','ls-tree','log','merge-base']),operation=args.find(value=>knownOperations.has(value))??'metadata',
phase=operation==='rev-parse'&&args.includes('--show-object-format')?'object-format':operation==='rev-parse'&&args.includes('--is-inside-work-tree')?'worktree-probe':operation,
reason=/not a git repository|outside repository/i.test(result.stderr)?'repository unavailable':/dubious ownership/i.test(result.stderr)?'repository ownership rejected':/(?:bad|invalid|unable to read).*config|config (?:error|file)/i.test(result.stderr)?'configuration rejected':/unknown option|unknown switch|unrecognized option|usage:/i.test(result.stderr)?'unsupported invocation':/(?:cannot|could not|unable to) (?:chdir|change directory)|no such file or directory/i.test(result.stderr)?'path unavailable':'request rejected',
outcome=result.timedOut?'timed out':result.truncated?'exceeded the output limit':`exited ${result.code}`;
throw new CsoError('MISSING_INPUT',`Could not read bounded Git metadata: ${phase} ${outcome} (${reason}); source may not be a Git repository`);
const knownOperations = new Set([
'rev-parse',
'symbolic-ref',
'ls-files',
'ls-tree',
'log',
'merge-base',
]),
operation = args.find((value) => knownOperations.has(value)) ?? 'metadata',
phase =
operation === 'rev-parse' && args.includes('--show-object-format')
? 'object-format'
: operation === 'rev-parse' && args.includes('--is-inside-work-tree')
? 'worktree-probe'
: operation,
reason = /not a git repository|outside repository/i.test(result.stderr)
? 'repository unavailable'
: /dubious ownership/i.test(result.stderr)
? 'repository ownership rejected'
: /(?:bad|invalid|unable to read).*config|config (?:error|file)/i.test(result.stderr)
? 'configuration rejected'
: /unknown option|unknown switch|unrecognized option|usage:/i.test(result.stderr)
? 'unsupported invocation'
: /(?:cannot|could not|unable to) (?:chdir|change directory)|no such file or directory/i.test(
result.stderr,
)
? 'path unavailable'
: 'request rejected',
outcome = result.timedOut
? 'timed out'
: result.truncated
? 'exceeded the output limit'
: `exited ${result.code}`;
throw new CsoError(
'MISSING_INPUT',
`Could not read bounded Git metadata: ${phase} ${outcome} (${reason}); source may not be a Git repository`,
);
}
return result.stdout;
}
+215 -59
View File
@@ -13,10 +13,23 @@ interface RuntimeQualificationProvenance {
provenanceDigest: string;
verifiedProvenance: true;
}
export type RuntimeQualification = RuntimeQualificationProvenance & (
| { kind: 'application'; containmentPassed: true; coldStartPassed: true; positiveNegativeAssertionsPassed: true; heldOutRepairPassed: true }
| { kind: 'postgresql'; containmentPassed: true; coldStartPassed: true; multiDatabasePassed: true; readinessPassed: true }
);
export type RuntimeQualification = RuntimeQualificationProvenance &
(
| {
kind: 'application';
containmentPassed: true;
coldStartPassed: true;
positiveNegativeAssertionsPassed: true;
heldOutRepairPassed: true;
}
| {
kind: 'postgresql';
containmentPassed: true;
coldStartPassed: true;
multiDatabasePassed: true;
readinessPassed: true;
}
);
export interface QualifiedRuntime {
id: string;
stack: CsoStack | 'postgresql';
@@ -76,46 +89,96 @@ function versionsKey(versions: Record<string, string>): string {
return JSON.stringify(Object.entries(versions).sort(([a], [b]) => a.localeCompare(b)));
}
function validateRuntimeIdentity(value: { id: string; stack: string; platform: string; versions: Record<string, string> }): void {
function validateRuntimeIdentity(value: {
id: string;
stack: string;
platform: string;
versions: Record<string, string>;
}): void {
if (typeof value.id !== 'string' || !ID.test(value.id)) throw new Error('INVALID_RUNTIME_ID');
if (!STACKS.includes(value.stack as typeof STACKS[number]) || !PLATFORMS.includes(value.platform as RuntimePlatform)) throw new Error('UNSUPPORTED_RUNTIME_PLATFORM');
if (!value.versions || typeof value.versions !== 'object' || Array.isArray(value.versions) || !Object.keys(value.versions).length ||
Object.values(value.versions).some(version => typeof version !== 'string' || !/^[0-9][a-zA-Z0-9.+_-]*$/.test(version))) throw new Error('UNPINNED_RUNTIME_VERSION');
if (Object.keys(value.versions).sort().join(',') !== [...REQUIRED[value.stack]].sort().join(',')) throw new Error('MISSING_RUNTIME_TOOL_VERSION');
if (['node', 'bun', 'python', 'rails'].includes(value.stack) && value.versions['cso-preparation'] !== '1.0.0') throw new Error('INCOMPATIBLE_PREPARATION_HELPER');
if (
!STACKS.includes(value.stack as (typeof STACKS)[number]) ||
!PLATFORMS.includes(value.platform as RuntimePlatform)
)
throw new Error('UNSUPPORTED_RUNTIME_PLATFORM');
if (
!value.versions ||
typeof value.versions !== 'object' ||
Array.isArray(value.versions) ||
!Object.keys(value.versions).length ||
Object.values(value.versions).some(
(version) => typeof version !== 'string' || !/^[0-9][a-zA-Z0-9.+_-]*$/.test(version),
)
)
throw new Error('UNPINNED_RUNTIME_VERSION');
if (Object.keys(value.versions).sort().join(',') !== [...REQUIRED[value.stack]].sort().join(','))
throw new Error('MISSING_RUNTIME_TOOL_VERSION');
if (
['node', 'bun', 'python', 'rails'].includes(value.stack) &&
value.versions['cso-preparation'] !== '1.0.0'
)
throw new Error('INCOMPATIBLE_PREPARATION_HELPER');
}
export function validateRuntimeCatalog(value: unknown): asserts value is RuntimeCatalog {
const catalog = value as RuntimeCatalog;
if (!catalog || catalog.schemaVersion !== 1 || catalog.helperAbi !== CSO_HELPER_ABI ||
typeof catalog.revision !== 'string' || !BUILD_REVISION.test(catalog.revision) || !Array.isArray(catalog.runtimes)) throw new Error('INCOMPATIBLE_RUNTIME_CATALOG');
if (catalog.previousRevision !== null && (typeof catalog.previousRevision !== 'string' || !BUILD_REVISION.test(catalog.previousRevision))) throw new Error('INVALID_RUNTIME_CATALOG');
if (
!catalog ||
catalog.schemaVersion !== 1 ||
catalog.helperAbi !== CSO_HELPER_ABI ||
typeof catalog.revision !== 'string' ||
!BUILD_REVISION.test(catalog.revision) ||
!Array.isArray(catalog.runtimes)
)
throw new Error('INCOMPATIBLE_RUNTIME_CATALOG');
if (
catalog.previousRevision !== null &&
(typeof catalog.previousRevision !== 'string' || !BUILD_REVISION.test(catalog.previousRevision))
)
throw new Error('INVALID_RUNTIME_CATALOG');
if (!Array.isArray(catalog.profiles) || catalog.profiles.length !== STACKS.length * PLATFORMS.length ||
typeof catalog.buildRevision !== 'string' || !BUILD_REVISION.test(catalog.buildRevision)) throw new Error('INVALID_REVIEWED_RUNTIME_PROFILES');
const profiles = new Map<string, ReviewedRuntimeProfile>(), profileIdentities = new Set<string>();
if (
!Array.isArray(catalog.profiles) ||
catalog.profiles.length !== STACKS.length * PLATFORMS.length ||
typeof catalog.buildRevision !== 'string' ||
!BUILD_REVISION.test(catalog.buildRevision)
)
throw new Error('INVALID_REVIEWED_RUNTIME_PROFILES');
const profiles = new Map<string, ReviewedRuntimeProfile>(),
profileIdentities = new Set<string>();
for (const profile of catalog.profiles) {
validateRuntimeIdentity(profile);
const identity = `${profile.stack}:${profile.platform}`;
if (profiles.has(profile.id) || profileIdentities.has(identity) || profile.state !== 'build_reviewed' ||
!Number.isFinite(Date.parse(profile.reviewedAt))) throw new Error('INVALID_REVIEWED_RUNTIME_PROFILE');
profiles.set(profile.id, profile); profileIdentities.add(identity);
}
for (const stack of STACKS) for (const platform of PLATFORMS) {
if (!profileIdentities.has(`${stack}:${platform}`)) throw new Error('INCOMPLETE_REVIEWED_RUNTIME_MATRIX');
if (
profiles.has(profile.id) ||
profileIdentities.has(identity) ||
profile.state !== 'build_reviewed' ||
!Number.isFinite(Date.parse(profile.reviewedAt))
)
throw new Error('INVALID_REVIEWED_RUNTIME_PROFILE');
profiles.set(profile.id, profile);
profileIdentities.add(identity);
}
for (const stack of STACKS)
for (const platform of PLATFORMS) {
if (!profileIdentities.has(`${stack}:${platform}`))
throw new Error('INCOMPLETE_REVIEWED_RUNTIME_MATRIX');
}
if (catalog.promotion !== undefined) {
if (!/^[a-f0-9]{40}$/.test(catalog.promotion.sourceCommit) ||
if (
!/^[a-f0-9]{40}$/.test(catalog.promotion.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(catalog.promotion.workflow) ||
!DIGEST.test(catalog.promotion.evidenceDigest) ||
!DIGEST.test(catalog.promotion.qualificationEvidenceDigest) ||
Object.keys(catalog.promotion).sort().join(',') !==
['evidenceDigest', 'qualificationEvidenceDigest', 'sourceCommit', 'workflow'].sort().join(',')) {
['evidenceDigest', 'qualificationEvidenceDigest', 'sourceCommit', 'workflow'].sort().join(',')
) {
throw new Error('INVALID_RUNTIME_PROMOTION');
}
}
if (catalog.runtimes.length !== 0 && catalog.runtimes.length !== STACKS.length * PLATFORMS.length) throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
if (catalog.runtimes.length !== 0 && catalog.runtimes.length !== STACKS.length * PLATFORMS.length)
throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
const ids = new Set<string>();
const runtimeIdentities = new Set<string>();
for (const runtime of catalog.runtimes) {
@@ -124,35 +187,94 @@ export function validateRuntimeCatalog(value: unknown): asserts value is Runtime
validateRuntimeIdentity(runtime);
const identity = `${runtime.stack}:${runtime.platform}`;
if (ids.has(runtime.id) || runtimeIdentities.has(identity)) throw new Error('INVALID_RUNTIME_ID');
ids.add(runtime.id); runtimeIdentities.add(identity);
ids.add(runtime.id);
runtimeIdentities.add(identity);
const arch = runtime.platform === 'linux/amd64' ? 'amd64' : 'arm64';
const expectedImage = new RegExp(`^ghcr\\.io/garrytan/gstack/cso-staging/${runtime.stack}-${arch}@sha256:[a-f0-9]{64}$`);
if (runtime.state !== 'qualified' || !IMAGE.test(runtime.image) || !expectedImage.test(runtime.image) || runtime.entrypoint !== '/opt/cso/entrypoint' ||
runtime.helperAbi !== CSO_HELPER_ABI || runtime.policyVersion !== 'cso-isolation-v1') throw new Error('UNQUALIFIED_RUNTIME');
const expectedImage = new RegExp(
`^ghcr\\.io/garrytan/gstack/cso-staging/${runtime.stack}-${arch}@sha256:[a-f0-9]{64}$`,
);
if (
runtime.state !== 'qualified' ||
!IMAGE.test(runtime.image) ||
!expectedImage.test(runtime.image) ||
runtime.entrypoint !== '/opt/cso/entrypoint' ||
runtime.helperAbi !== CSO_HELPER_ABI ||
runtime.policyVersion !== 'cso-isolation-v1'
)
throw new Error('UNQUALIFIED_RUNTIME');
const reviewed = profiles.get(runtime.id);
if (!reviewed || reviewed.stack !== runtime.stack || reviewed.platform !== runtime.platform ||
versionsKey(reviewed.versions) !== versionsKey(runtime.versions)) throw new Error('RUNTIME_BUILD_PROFILE_MISMATCH');
if (!qualification || !/^[a-f0-9]{40}$/.test(qualification.sourceCommit) ||
if (
!reviewed ||
reviewed.stack !== runtime.stack ||
reviewed.platform !== runtime.platform ||
versionsKey(reviewed.versions) !== versionsKey(runtime.versions)
)
throw new Error('RUNTIME_BUILD_PROFILE_MISMATCH');
if (
!qualification ||
!/^[a-f0-9]{40}$/.test(qualification.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(qualification.workflow) ||
!DIGEST.test(qualification.sbomDigest) || !DIGEST.test(qualification.provenanceDigest) || qualification.verifiedProvenance !== true ||
!Number.isFinite(Date.parse(runtime.qualifiedAt))) throw new Error('MISSING_RUNTIME_QUALIFICATION');
!DIGEST.test(qualification.sbomDigest) ||
!DIGEST.test(qualification.provenanceDigest) ||
qualification.verifiedProvenance !== true ||
!Number.isFinite(Date.parse(runtime.qualifiedAt))
)
throw new Error('MISSING_RUNTIME_QUALIFICATION');
const keys = Object.keys(qualification).sort();
const common = ['kind', 'sourceCommit', 'workflow', 'sbomDigest', 'provenanceDigest', 'verifiedProvenance'];
const common = [
'kind',
'sourceCommit',
'workflow',
'sbomDigest',
'provenanceDigest',
'verifiedProvenance',
];
if (['node', 'bun', 'python', 'rails'].includes(runtime.stack)) {
if (qualification.kind !== 'application' || qualification.containmentPassed !== true || qualification.coldStartPassed !== true ||
qualification.positiveNegativeAssertionsPassed !== true || qualification.heldOutRepairPassed !== true ||
keys.join(',') !== [...common, 'containmentPassed', 'coldStartPassed', 'positiveNegativeAssertionsPassed', 'heldOutRepairPassed'].sort().join(',')) throw new Error('MISSING_APPLICATION_QUALIFICATION');
if (
qualification.kind !== 'application' ||
qualification.containmentPassed !== true ||
qualification.coldStartPassed !== true ||
qualification.positiveNegativeAssertionsPassed !== true ||
qualification.heldOutRepairPassed !== true ||
keys.join(',') !==
[
...common,
'containmentPassed',
'coldStartPassed',
'positiveNegativeAssertionsPassed',
'heldOutRepairPassed',
]
.sort()
.join(',')
)
throw new Error('MISSING_APPLICATION_QUALIFICATION');
} else {
if (qualification.kind !== 'postgresql' || qualification.containmentPassed !== true || qualification.coldStartPassed !== true ||
qualification.multiDatabasePassed !== true || qualification.readinessPassed !== true ||
keys.join(',') !== [...common, 'containmentPassed', 'coldStartPassed', 'multiDatabasePassed', 'readinessPassed'].sort().join(',')) throw new Error('MISSING_POSTGRESQL_QUALIFICATION');
if (
qualification.kind !== 'postgresql' ||
qualification.containmentPassed !== true ||
qualification.coldStartPassed !== true ||
qualification.multiDatabasePassed !== true ||
qualification.readinessPassed !== true ||
keys.join(',') !==
[...common, 'containmentPassed', 'coldStartPassed', 'multiDatabasePassed', 'readinessPassed']
.sort()
.join(',')
)
throw new Error('MISSING_POSTGRESQL_QUALIFICATION');
}
}
if (catalog.runtimes.length > 0) {
for (const identity of profileIdentities) if (!runtimeIdentities.has(identity)) throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
for (const identity of profileIdentities)
if (!runtimeIdentities.has(identity)) throw new Error('INCOMPLETE_QUALIFIED_RUNTIME_MATRIX');
if (!catalog.promotion) throw new Error('MISSING_RUNTIME_PROMOTION');
if (catalog.runtimes.some(runtime => runtime.qualification.sourceCommit !== catalog.promotion!.sourceCommit ||
runtime.qualification.workflow !== catalog.promotion!.workflow)) throw new Error('RUNTIME_PROMOTION_MISMATCH');
if (
catalog.runtimes.some(
(runtime) =>
runtime.qualification.sourceCommit !== catalog.promotion!.sourceCommit ||
runtime.qualification.workflow !== catalog.promotion!.workflow,
)
)
throw new Error('RUNTIME_PROMOTION_MISMATCH');
if (catalog.promotion.evidenceDigest !== `sha256:${sha256(canonical(catalog.runtimes))}`) {
throw new Error('RUNTIME_PROMOTION_EVIDENCE_MISMATCH');
}
@@ -163,38 +285,72 @@ export const RUNTIME_CATALOG = committedCatalog as RuntimeCatalog;
validateRuntimeCatalog(RUNTIME_CATALOG);
export function assertRuntimeCompatible(plan: PreparationPlan, runtime: QualifiedRuntime): void {
if (plan.schemaVersion !== 1 || plan.status !== 'ready' || runtime.stack !== plan.stack) throw new CsoError('INCOMPATIBLE_INPUT', `Prepared ${plan.stack} source cannot run in ${runtime.stack} runtime ${runtime.id}`);
if (plan.schemaVersion !== 1 || plan.status !== 'ready' || runtime.stack !== plan.stack)
throw new CsoError(
'INCOMPATIBLE_INPUT',
`Prepared ${plan.stack} source cannot run in ${runtime.stack} runtime ${runtime.id}`,
);
for (const [declared, rawRange] of Object.entries(plan.runtimeRequirements)) {
if (!rawRange) continue;
let tool = declared, range = rawRange;
let tool = declared,
range = rawRange;
if (declared === 'packageManager') {
const match = rawRange.match(/^([a-z][a-z0-9_-]*)@(.+)$/i);
if (!match) throw new CsoError('PREREQUISITE', 'Package manager declaration must bind a named version range');
tool = match[1]; range = match[2];
if (!match)
throw new CsoError('PREREQUISITE', 'Package manager declaration must bind a named version range');
tool = match[1];
range = match[2];
}
const version = runtime.versions[tool];
if (!version) throw new CsoError('PREREQUISITE', `Qualified runtime ${runtime.id} does not declare a real ${tool} release`);
if (!version)
throw new CsoError(
'PREREQUISITE',
`Qualified runtime ${runtime.id} does not declare a real ${tool} release`,
);
let satisfies = false;
try { satisfies = Bun.semver.satisfies(version.replace(/^v/, ''), range); } catch {}
if (!satisfies) throw new CsoError('PREREQUISITE', `Qualified ${tool} ${version} does not satisfy source requirement ${range}`);
try {
satisfies = Bun.semver.satisfies(version.replace(/^v/, ''), range);
} catch {}
if (!satisfies)
throw new CsoError(
'PREREQUISITE',
`Qualified ${tool} ${version} does not satisfy source requirement ${range}`,
);
}
}
export function selectRuntime(profile: string, platform: RuntimePlatform, catalog: RuntimeCatalog = RUNTIME_CATALOG): QualifiedRuntime {
export function selectRuntime(
profile: string,
platform: RuntimePlatform,
catalog: RuntimeCatalog = RUNTIME_CATALOG,
): QualifiedRuntime {
validateRuntimeCatalog(catalog);
const matches = catalog.runtimes.filter(runtime => runtime.platform === platform && (runtime.id === profile || runtime.stack === profile));
const matches = catalog.runtimes.filter(
(runtime) => runtime.platform === platform && (runtime.id === profile || runtime.stack === profile),
);
if (matches.length === 0) {
const reviewed = catalog.profiles?.filter(item => item.platform === platform && (item.id === profile || item.stack === profile)) ?? [];
const detail = reviewed.length === 1 ? ` Reviewed build profile ${reviewed[0].id} is awaiting a qualified image promotion.` : '';
throw new Error(`MISSING_QUALIFIED_RUNTIME: ${profile} on ${platform}; build, qualify, and review a digest catalog before target execution.${detail}`);
const reviewed =
catalog.profiles?.filter(
(item) => item.platform === platform && (item.id === profile || item.stack === profile),
) ?? [];
const detail =
reviewed.length === 1
? ` Reviewed build profile ${reviewed[0].id} is awaiting a qualified image promotion.`
: '';
throw new Error(
`MISSING_QUALIFIED_RUNTIME: ${profile} on ${platform}; build, qualify, and review a digest catalog before target execution.${detail}`,
);
}
if (matches.length !== 1) throw new Error(`AMBIGUOUS_RUNTIME: select an exact qualified runtime id for ${profile}.`);
if (matches.length !== 1)
throw new Error(`AMBIGUOUS_RUNTIME: select an exact qualified runtime id for ${profile}.`);
return matches[0];
}
/** Rollback only pairs the previous catalog with a compatible helper; reports have their own schema. */
export function rollbackCatalog(current: RuntimeCatalog, previous: RuntimeCatalog): RuntimeCatalog {
validateRuntimeCatalog(current); validateRuntimeCatalog(previous);
if (current.previousRevision !== previous.revision || current.helperAbi !== previous.helperAbi) throw new Error('INCOMPATIBLE_RUNTIME_ROLLBACK');
validateRuntimeCatalog(current);
validateRuntimeCatalog(previous);
if (current.previousRevision !== previous.revision || current.helperAbi !== previous.helperAbi)
throw new Error('INCOMPATIBLE_RUNTIME_ROLLBACK');
return previous;
}
+162 -39
View File
@@ -54,8 +54,15 @@ const IMAGE = /^(?:[a-z0-9.-]+(?::[0-9]+)?\/)?[a-z0-9][a-z0-9._/-]*@sha256:[a-f0
const ID = /^[a-z0-9][a-z0-9._-]{0,100}$/;
const QUALIFICATION_WORKFLOW = /^https:\/\/github\.com\/garrytan\/gstack\/actions\/runs\/[0-9]+$/;
const PLATFORMS: RuntimePlatform[] = ['linux/amd64', 'linux/arm64'];
const path = (s: unknown, prefix: string): s is string => typeof s === 'string' && s.startsWith(prefix) && !/[\x00-\x20\\,]/.test(s) && !s.split('/').some(x => x === '..' || x === '.') && !s.includes('//');
function invalid(message: string): never { throw new CsoError('INCOMPATIBLE_INPUT', message); }
const path = (s: unknown, prefix: string): s is string =>
typeof s === 'string' &&
s.startsWith(prefix) &&
!/[\x00-\x20\\,]/.test(s) &&
!s.split('/').some((x) => x === '..' || x === '.') &&
!s.includes('//');
function invalid(message: string): never {
throw new CsoError('INCOMPATIBLE_INPUT', message);
}
function sameStrings(left: string[], right: string[]): boolean {
return canonical([...left].sort()) === canonical([...right].sort());
}
@@ -68,63 +75,179 @@ export function scannerVersionHash(stdout: string, stderr = ''): string {
* version to be one complete version token. A substring such as `1.2.3` in
* `11.2.3`, `1.2.30`, or `1.2.3-dev` is not qualification evidence.
*/
export function assertScannerVersionOutput(scanner:ScannerId,version:string,stdout:string,stderr=''):void{
if(!/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(version))invalid('Scanner version evidence has an invalid expected version');
const output=`${stdout}\n${stderr}`;
if(Buffer.byteLength(stdout)+Buffer.byteLength(stderr)>8192)invalid('Scanner version evidence exceeds the bounded output limit');
const escaped=version.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
const labels:Record<ScannerId,string>={gitleaks:'gitleaks',osv:'(?:osv|osv-scanner)',semgrep:'semgrep',zizmor:'zizmor',trivy:'trivy',schemathesis:'schemathesis'};
const primary=output.split(/\r?\n/).map(line=>line.trim()).find(Boolean)??'';
const exact=new RegExp(`^(?:v?${escaped}|${labels[scanner]},?\\s+(?:version\\s*:?\\s*)?v?${escaped}|version\\s*:\\s*v?${escaped})$`,'i');
if(!exact.test(primary))invalid('Scanner primary version output does not match the exact catalog version');
export function assertScannerVersionOutput(
scanner: ScannerId,
version: string,
stdout: string,
stderr = '',
): void {
if (!/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(version))
invalid('Scanner version evidence has an invalid expected version');
const output = `${stdout}\n${stderr}`;
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > 8192)
invalid('Scanner version evidence exceeds the bounded output limit');
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const labels: Record<ScannerId, string> = {
gitleaks: 'gitleaks',
osv: '(?:osv|osv-scanner)',
semgrep: 'semgrep',
zizmor: 'zizmor',
trivy: 'trivy',
schemathesis: 'schemathesis',
};
const primary =
output
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? '';
const exact = new RegExp(
`^(?:v?${escaped}|${labels[scanner]},?\\s+(?:version\\s*:?\\s*)?v?${escaped}|version\\s*:\\s*v?${escaped})$`,
'i',
);
if (!exact.test(primary))
invalid('Scanner primary version output does not match the exact catalog version');
}
export function validateQualifiedScanner(s: QualifiedScanner): void {
if (!SCANNER_IDS.includes(s.scanner) || !['linux/amd64', 'linux/arm64'].includes(s.platform)) invalid('Unsupported scanner or platform');
const arch = s.platform === 'linux/amd64' ? 'amd64' : 'arm64';
const expectedImage = new RegExp(`^ghcr\\.io/garrytan/gstack/cso-scanners/${s.scanner}-${arch}@sha256:[a-f0-9]{64}$`);
if (s.state !== 'qualified' || !IMAGE.test(s.image) || !expectedImage.test(s.image) || s.entrypoint !== '/opt/cso/entrypoint' || s.helperAbi !== ABI || s.isolationPolicyHash !== ISOLATION_POLICY_HASH) invalid('Scanner profile is not qualified for this helper isolation policy');
if (s.executable !== '/opt/cso/bin/scanner' || !/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(s.version) || !HASH.test(s.versionOutputSha256)) invalid('Scanner executable and version must be pinned');
if (!Array.isArray(s.capabilities) || !s.capabilities.length || s.capabilities.length > 100 || s.capabilities.some(x => typeof x !== 'string' || !x || x.length > 100)) invalid('Scanner capabilities must be reviewed');
const required = scannerPlans({ snapshotRoot: '/source', offline: true, selected: [s.scanner] })[0].requiredFeatures;
if (!sameStrings(s.capabilities, required)) invalid('Scanner capabilities do not match the helper adapter contract');
const rules = s.assets?.semgrepRules, db = s.assets?.advisoryDatabase;
if (rules && (s.scanner !== 'semgrep' || !path(rules.path, '/policy/catalog/') || !HASH.test(rules.sha256))) invalid('Invalid immutable Semgrep rules');
if (db && (!['osv', 'trivy'].includes(s.scanner) || !path(db.path, '/opt/cso/scanner-data/') || !HASH.test(db.contentSha256) || !Number.isFinite(Date.parse(db.updatedAt)) || !Array.isArray(db.ecosystems) || !db.ecosystems.length || db.ecosystems.some(x => typeof x !== 'string' || !x || x.length > 100))) invalid('Invalid immutable scanner database');
if (s.scanner === 'semgrep' && !rules) invalid('Qualified Semgrep profiles require an immutable rules bundle');
if (['osv', 'trivy'].includes(s.scanner) && !db) invalid(`Qualified ${s.scanner} profiles require an immutable offline database`);
const q = s.qualification;
if (!Number.isFinite(Date.parse(s.qualifiedAt)) || !q || !/^[a-f0-9]{40}$/.test(q.sourceCommit) || !QUALIFICATION_WORKFLOW.test(q.workflow) || !DIGEST.test(q.sbomDigest) || !DIGEST.test(q.provenanceDigest) || q.verifiedProvenance !== true || q.containmentPassed !== true || q.adapterContractPassed !== true || q.offlineAssetsPassed !== true) invalid('Missing trusted scanner qualification');
if (!SCANNER_IDS.includes(s.scanner) || !['linux/amd64', 'linux/arm64'].includes(s.platform))
invalid('Unsupported scanner or platform');
const arch = s.platform === 'linux/amd64' ? 'amd64' : 'arm64';
const expectedImage = new RegExp(
`^ghcr\\.io/garrytan/gstack/cso-scanners/${s.scanner}-${arch}@sha256:[a-f0-9]{64}$`,
);
if (
s.state !== 'qualified' ||
!IMAGE.test(s.image) ||
!expectedImage.test(s.image) ||
s.entrypoint !== '/opt/cso/entrypoint' ||
s.helperAbi !== ABI ||
s.isolationPolicyHash !== ISOLATION_POLICY_HASH
)
invalid('Scanner profile is not qualified for this helper isolation policy');
if (
s.executable !== '/opt/cso/bin/scanner' ||
!/^[0-9][A-Za-z0-9.+_-]{0,100}$/.test(s.version) ||
!HASH.test(s.versionOutputSha256)
)
invalid('Scanner executable and version must be pinned');
if (
!Array.isArray(s.capabilities) ||
!s.capabilities.length ||
s.capabilities.length > 100 ||
s.capabilities.some((x) => typeof x !== 'string' || !x || x.length > 100)
)
invalid('Scanner capabilities must be reviewed');
const required = scannerPlans({ snapshotRoot: '/source', offline: true, selected: [s.scanner] })[0]
.requiredFeatures;
if (!sameStrings(s.capabilities, required))
invalid('Scanner capabilities do not match the helper adapter contract');
const rules = s.assets?.semgrepRules,
db = s.assets?.advisoryDatabase;
if (rules && (s.scanner !== 'semgrep' || !path(rules.path, '/policy/catalog/') || !HASH.test(rules.sha256)))
invalid('Invalid immutable Semgrep rules');
if (
db &&
(!['osv', 'trivy'].includes(s.scanner) ||
!path(db.path, '/opt/cso/scanner-data/') ||
!HASH.test(db.contentSha256) ||
!Number.isFinite(Date.parse(db.updatedAt)) ||
!Array.isArray(db.ecosystems) ||
!db.ecosystems.length ||
db.ecosystems.some((x) => typeof x !== 'string' || !x || x.length > 100))
)
invalid('Invalid immutable scanner database');
if (s.scanner === 'semgrep' && !rules)
invalid('Qualified Semgrep profiles require an immutable rules bundle');
if (['osv', 'trivy'].includes(s.scanner) && !db)
invalid(`Qualified ${s.scanner} profiles require an immutable offline database`);
const q = s.qualification;
if (
!Number.isFinite(Date.parse(s.qualifiedAt)) ||
!q ||
!/^[a-f0-9]{40}$/.test(q.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(q.workflow) ||
!DIGEST.test(q.sbomDigest) ||
!DIGEST.test(q.provenanceDigest) ||
q.verifiedProvenance !== true ||
q.containmentPassed !== true ||
q.adapterContractPassed !== true ||
q.offlineAssetsPassed !== true
)
invalid('Missing trusted scanner qualification');
}
export function validateScannerCatalog(value: unknown): asserts value is ScannerCatalog {
const c = value as ScannerCatalog;
if (!c || c.schemaVersion !== 1 || c.helperAbi !== ABI || typeof c.revision !== 'string' || !ID.test(c.revision) || !Array.isArray(c.scanners) || ![0, SCANNER_IDS.length * PLATFORMS.length].includes(c.scanners.length)) invalid('Incompatible scanner catalog');
if (c.previousRevision !== undefined && c.previousRevision !== null && (typeof c.previousRevision !== 'string' || !ID.test(c.previousRevision) || c.previousRevision === c.revision)) invalid('Invalid previous scanner catalog revision');
if (c.promotion !== undefined && (!/^[a-f0-9]{40}$/.test(c.promotion.sourceCommit) || !QUALIFICATION_WORKFLOW.test(c.promotion.workflow) || !DIGEST.test(c.promotion.evidenceDigest))) invalid('Invalid scanner catalog promotion');
if (
!c ||
c.schemaVersion !== 1 ||
c.helperAbi !== ABI ||
typeof c.revision !== 'string' ||
!ID.test(c.revision) ||
!Array.isArray(c.scanners) ||
![0, SCANNER_IDS.length * PLATFORMS.length].includes(c.scanners.length)
)
invalid('Incompatible scanner catalog');
if (
c.previousRevision !== undefined &&
c.previousRevision !== null &&
(typeof c.previousRevision !== 'string' ||
!ID.test(c.previousRevision) ||
c.previousRevision === c.revision)
)
invalid('Invalid previous scanner catalog revision');
if (
c.promotion !== undefined &&
(!/^[a-f0-9]{40}$/.test(c.promotion.sourceCommit) ||
!QUALIFICATION_WORKFLOW.test(c.promotion.workflow) ||
!DIGEST.test(c.promotion.evidenceDigest))
)
invalid('Invalid scanner catalog promotion');
if (c.scanners.length === 0) {
if (c.promotion !== undefined) invalid('Empty scanner catalog cannot have a promotion');
return;
}
if (!c.promotion) invalid('Qualified scanner catalog requires trusted promotion evidence');
const ids = new Set<string>(), identities = new Set<string>();
const ids = new Set<string>(),
identities = new Set<string>();
for (const s of c.scanners) {
if (!s || typeof s.id !== 'string' || !ID.test(s.id) || ids.has(s.id)) invalid('Invalid or duplicate scanner profile');
if (!s || typeof s.id !== 'string' || !ID.test(s.id) || ids.has(s.id))
invalid('Invalid or duplicate scanner profile');
const identity = `${s.scanner}:${s.platform}`;
if (identities.has(identity)) invalid('Invalid or duplicate scanner profile');
ids.add(s.id); identities.add(identity);
ids.add(s.id);
identities.add(identity);
validateQualifiedScanner(s);
if (s.qualification.sourceCommit !== c.promotion.sourceCommit || s.qualification.workflow !== c.promotion.workflow) invalid('Scanner qualification does not match catalog promotion');
if (
s.qualification.sourceCommit !== c.promotion.sourceCommit ||
s.qualification.workflow !== c.promotion.workflow
)
invalid('Scanner qualification does not match catalog promotion');
}
for (const scanner of SCANNER_IDS) for (const platform of PLATFORMS) if (!identities.has(`${scanner}:${platform}`)) invalid('Incomplete qualified scanner matrix');
if (c.promotion.evidenceDigest !== `sha256:${sha256(canonical(c.scanners))}`) invalid('Scanner catalog promotion does not bind the qualified matrix');
for (const scanner of SCANNER_IDS)
for (const platform of PLATFORMS)
if (!identities.has(`${scanner}:${platform}`)) invalid('Incomplete qualified scanner matrix');
if (c.promotion.evidenceDigest !== `sha256:${sha256(canonical(c.scanners))}`)
invalid('Scanner catalog promotion does not bind the qualified matrix');
}
export const SCANNER_CATALOG = committedCatalog as unknown as ScannerCatalog;
// A malformed source-controlled catalog must break the helper build/startup;
// it can never degrade into an unreviewed executable fallback.
validateScannerCatalog(SCANNER_CATALOG);
export function selectScanner(scanner: ScannerId, platform: RuntimePlatform, profile?: string, catalog: ScannerCatalog = SCANNER_CATALOG): QualifiedScanner {
export function selectScanner(
scanner: ScannerId,
platform: RuntimePlatform,
profile?: string,
catalog: ScannerCatalog = SCANNER_CATALOG,
): QualifiedScanner {
validateScannerCatalog(catalog);
const matches = catalog.scanners.filter(s => s.scanner === scanner && s.platform === platform && (!profile || s.id === profile));
if (!matches.length) throw new CsoError('PREREQUISITE', `No qualified ${scanner} image for ${platform}${profile ? ` (${profile})` : ''}; qualify and review an immutable scanner catalog before execution`);
if (matches.length !== 1) throw new CsoError('PREREQUISITE', `Select an exact qualified ${scanner} profile for ${platform}`);
const matches = catalog.scanners.filter(
(s) => s.scanner === scanner && s.platform === platform && (!profile || s.id === profile),
);
if (!matches.length)
throw new CsoError(
'PREREQUISITE',
`No qualified ${scanner} image for ${platform}${profile ? ` (${profile})` : ''}; qualify and review an immutable scanner catalog before execution`,
);
if (matches.length !== 1)
throw new CsoError('PREREQUISITE', `Select an exact qualified ${scanner} profile for ${platform}`);
return matches[0];
}
+628 -144
View File
@@ -2,17 +2,63 @@
import * as fs from 'node:fs';
import { randomBytes } from 'node:crypto';
import { join } from 'node:path';
import { Command, CoverageRecord, CsoError, HttpAssertion, RunPolicy, SnapshotManifest, canonical, object, relativePath, sha256, snapshotPathHandleId, snapshotReference, string, strings, validateCommand, validateVerificationObservation, type ErrorCode } from './contracts';
import {
Command,
CoverageRecord,
CsoError,
HttpAssertion,
RunPolicy,
SnapshotManifest,
canonical,
object,
relativePath,
sha256,
snapshotPathHandleId,
snapshotReference,
string,
strings,
validateCommand,
validateVerificationObservation,
type ErrorCode,
} from './contracts';
import { DockerEndpoint, DockerGroup, dockerEndpoint } from './docker';
import { inspectPreparation, type CsoStack } from './preparation';
import { redact } from './process';
import { QualifiedRuntime, RUNTIME_CATALOG, RuntimeCatalog, RuntimePlatform, assertRuntimeCompatible, selectRuntime } from './runtime-catalog';
import { QualifiedScanner, SCANNER_CATALOG, ScannerCatalog, assertScannerVersionOutput, scannerVersionHash, selectScanner } from './scanner-catalog';
import { ScannerExecution, ScannerGap, ScannerId, ScannerOutcome, ScannerPlan, parseScannerOutput, scannerPlans } from './scanners';
import {
QualifiedRuntime,
RUNTIME_CATALOG,
RuntimeCatalog,
RuntimePlatform,
assertRuntimeCompatible,
selectRuntime,
} from './runtime-catalog';
import {
QualifiedScanner,
SCANNER_CATALOG,
ScannerCatalog,
assertScannerVersionOutput,
scannerVersionHash,
selectScanner,
} from './scanner-catalog';
import {
ScannerExecution,
ScannerGap,
ScannerId,
ScannerOutcome,
ScannerPlan,
parseScannerOutput,
scannerPlans,
} from './scanners';
import { assertSnapshot } from './snapshot';
import { hasPendingWatchdogCleanup, secureDirectory } from './state';
import { PublicArchiveCache, publicArchiveCacheRoot } from './cache';
import { admitPreparationRuntime, admitPreparationSidecar, PreparationExecutor, type PreparationSandboxRunner, type RailsDatabaseSelection } from './preparation-executor';
import {
admitPreparationRuntime,
admitPreparationSidecar,
PreparationExecutor,
type PreparationSandboxRunner,
type RailsDatabaseSelection,
} from './preparation-executor';
import type { PreparedDatabaseContract } from './preparation-executor';
import { DockerPreparationSandboxRunner } from './preparation-docker';
import { canonicalStartPlan, type CanonicalStartPlan } from './verification';
@@ -79,7 +125,9 @@ export interface ScannerRunner {
cleanup(): Promise<void>;
}
/** The trusted HTTP control probe is always the bounded verifier process. */
export function schemathesisControlRole(): 'verifier' { return 'verifier'; }
export function schemathesisControlRole(): 'verifier' {
return 'verifier';
}
export interface ScannerRunnerContext {
input: ScannerRunInput;
plan: ScannerPlan;
@@ -107,58 +155,121 @@ export interface ScannerRunDependencies {
}
function exact(v: Record<string, unknown>, allowed: string[], name: string): void {
for (const key of Object.keys(v)) if (!allowed.includes(key)) throw new CsoError('INVALID_SCHEMA', `Unexpected ${name} field: ${key}`);
for (const key of Object.keys(v))
if (!allowed.includes(key)) throw new CsoError('INVALID_SCHEMA', `Unexpected ${name} field: ${key}`);
}
function boundedInt(v: unknown, min: number, max: number, name: string): number {
if (!Number.isSafeInteger(v) || (v as number) < min || (v as number) > max) throw new CsoError('INVALID_SCHEMA', `${name} must be ${min}..${max}`);
if (!Number.isSafeInteger(v) || (v as number) < min || (v as number) > max)
throw new CsoError('INVALID_SCHEMA', `${name} must be ${min}..${max}`);
return v as number;
}
function control(value: unknown): HttpAssertion {
const v = object(value, 'API control'), expected = object(v.expected, 'API control expected');
const v = object(value, 'API control'),
expected = object(v.expected, 'API control expected');
exact(v, ['name', 'path', 'method', 'headers', 'body', 'expected'], 'API control');
exact(expected, ['status', 'includes', 'excludes'], 'API control expected');
const path = string(v.path, 'API control path', 4096);
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\]/.test(path)) throw new CsoError('INVALID_SCHEMA', 'API control path must remain on numeric loopback');
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(v.method)) throw new CsoError('INVALID_SCHEMA', 'Invalid API control method');
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\]/.test(path))
throw new CsoError('INVALID_SCHEMA', 'API control path must remain on numeric loopback');
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(v.method))
throw new CsoError('INVALID_SCHEMA', 'Invalid API control method');
const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(v.headers === undefined ? {} : object(v.headers, 'API control headers'))) {
if (!/^[A-Za-z0-9-]{1,100}$/.test(key) || typeof value !== 'string' || value.length > 8192 || /[\r\n]/.test(value)) throw new CsoError('INVALID_SCHEMA', 'Invalid API control header');
for (const [key, value] of Object.entries(
v.headers === undefined ? {} : object(v.headers, 'API control headers'),
)) {
if (
!/^[A-Za-z0-9-]{1,100}$/.test(key) ||
typeof value !== 'string' ||
value.length > 8192 ||
/[\r\n]/.test(value)
)
throw new CsoError('INVALID_SCHEMA', 'Invalid API control header');
headers[key] = value;
}
return { name: string(v.name, 'API control name', 200), path, method: v.method, headers,
return {
name: string(v.name, 'API control name', 200),
path,
method: v.method,
headers,
...(v.body === undefined ? {} : { body: string(v.body, 'API control body', 65536) }),
expected: { status: boundedInt(expected.status, 100, 599, 'API control status'),
...(expected.includes === undefined ? {} : { includes: string(expected.includes, 'API control includes', 8192) }),
...(expected.excludes === undefined ? {} : { excludes: string(expected.excludes, 'API control excludes', 8192) }) } };
expected: {
status: boundedInt(expected.status, 100, 599, 'API control status'),
...(expected.includes === undefined
? {}
: { includes: string(expected.includes, 'API control includes', 8192) }),
...(expected.excludes === undefined
? {}
: { excludes: string(expected.excludes, 'API control excludes', 8192) }),
},
};
}
/** Accept a bounded OpenAPI document, with internal references and selected path operations only. */
export function validateScannerRequest(value: unknown, id: ScannerId): ScannerRequest {
const v = object(value, 'scanner request');
exact(v, ['profile', 'api'], 'scanner request');
const request: ScannerRequest = v.profile === undefined ? {} : { profile: string(v.profile, 'scanner profile', 100) };
const request: ScannerRequest =
v.profile === undefined ? {} : { profile: string(v.profile, 'scanner profile', 100) };
if (v.api === undefined) return request;
if (id !== 'schemathesis') throw new CsoError('INVALID_SCHEMA', 'Only Schemathesis accepts application execution inputs');
if (id !== 'schemathesis')
throw new CsoError('INVALID_SCHEMA', 'Only Schemathesis accepts application execution inputs');
const api = object(v.api, 'API scan');
exact(api, ['runtimeProfile', 'port', 'start', 'control', 'boundaryFiles', 'schema', 'operationIds', 'seed', 'maxExamples'], 'API scan');
const schema = object(api.schema, 'OpenAPI schema'), operations = strings(api.operationIds, 'operation IDs');
if (operations.length < 1 || operations.length > 20 || new Set(operations).size !== operations.length || operations.some(x => x.length > 200 || /[\x00-\x1f]/.test(x))) throw new CsoError('INVALID_SCHEMA', 'Declare 1..20 unique bounded operation IDs');
if (typeof schema.openapi !== 'string' || !/^3\.[01]\.\d+$/.test(schema.openapi)) throw new CsoError('PREREQUISITE', 'Schemathesis requires a reviewed OpenAPI 3.0/3.1 JSON document');
if (Buffer.byteLength(JSON.stringify(schema)) > 262144) throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds 256 KiB');
exact(
api,
[
'runtimeProfile',
'port',
'start',
'control',
'boundaryFiles',
'schema',
'operationIds',
'seed',
'maxExamples',
],
'API scan',
);
const schema = object(api.schema, 'OpenAPI schema'),
operations = strings(api.operationIds, 'operation IDs');
if (
operations.length < 1 ||
operations.length > 20 ||
new Set(operations).size !== operations.length ||
operations.some((x) => x.length > 200 || /[\x00-\x1f]/.test(x))
)
throw new CsoError('INVALID_SCHEMA', 'Declare 1..20 unique bounded operation IDs');
if (typeof schema.openapi !== 'string' || !/^3\.[01]\.\d+$/.test(schema.openapi))
throw new CsoError('PREREQUISITE', 'Schemathesis requires a reviewed OpenAPI 3.0/3.1 JSON document');
if (Buffer.byteLength(JSON.stringify(schema)) > 262144)
throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds 256 KiB');
let nodes = 0;
const inspect = (x: unknown, depth: number): void => {
if (++nodes > 50_000 || depth > 32) throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds structural bounds');
if (++nodes > 50_000 || depth > 32)
throw new CsoError('INVALID_SCHEMA', 'OpenAPI schema exceeds structural bounds');
if (!x || typeof x !== 'object') return;
for (const [key, value] of Object.entries(x)) {
if (['__proto__', 'prototype', 'constructor', 'externalValue', 'callbacks', 'webhooks'].includes(key) || /hooks?/i.test(key)) throw new CsoError('PREREQUISITE', 'OpenAPI external examples, callbacks, webhooks, and hooks are not admitted');
if (key === '$ref' && (typeof value !== 'string' || !value.startsWith('#/'))) throw new CsoError('PREREQUISITE', 'OpenAPI references must be internal JSON pointers');
if (key === 'servers' && (!Array.isArray(value) || value.length)) throw new CsoError('PREREQUISITE', 'Remove server overrides from the reviewed API harness; its target is the isolated loopback application');
if (
['__proto__', 'prototype', 'constructor', 'externalValue', 'callbacks', 'webhooks'].includes(key) ||
/hooks?/i.test(key)
)
throw new CsoError(
'PREREQUISITE',
'OpenAPI external examples, callbacks, webhooks, and hooks are not admitted',
);
if (key === '$ref' && (typeof value !== 'string' || !value.startsWith('#/')))
throw new CsoError('PREREQUISITE', 'OpenAPI references must be internal JSON pointers');
if (key === 'servers' && (!Array.isArray(value) || value.length))
throw new CsoError(
'PREREQUISITE',
'Remove server overrides from the reviewed API harness; its target is the isolated loopback application',
);
inspect(value, depth + 1);
}
};
inspect(schema, 0);
const declared: string[] = [];
for (const [path, item] of Object.entries(object(schema.paths, 'OpenAPI paths'))) {
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\?#]/.test(path)) throw new CsoError('INVALID_SCHEMA', 'OpenAPI paths must be relative to the loopback target');
if (!path.startsWith('/') || path.startsWith('//') || /[\r\n\\?#]/.test(path))
throw new CsoError('INVALID_SCHEMA', 'OpenAPI paths must be relative to the loopback target');
const methods = object(item, 'OpenAPI path');
for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']) {
if (methods[method] === undefined) continue;
@@ -166,148 +277,413 @@ export function validateScannerRequest(value: unknown, id: ScannerId): ScannerRe
if (typeof op.operationId === 'string') declared.push(op.operationId);
}
}
if (operations.some(op => declared.filter(x => x === op).length !== 1)) throw new CsoError('INVALID_SCHEMA', 'Every selected operation must identify exactly one declared OpenAPI path operation');
if (operations.some((op) => declared.filter((x) => x === op).length !== 1))
throw new CsoError(
'INVALID_SCHEMA',
'Every selected operation must identify exactly one declared OpenAPI path operation',
);
const boundaries = strings(api.boundaryFiles, 'API boundary files').map(snapshotReference);
if (!boundaries.length || new Set(boundaries).size !== boundaries.length) throw new CsoError('INVALID_SCHEMA', 'API scan needs unique security-boundary source paths');
request.api = { runtimeProfile: string(api.runtimeProfile, 'API runtime profile', 100), port: boundedInt(api.port, 1024, 65535, 'API port'), start: validateCommand(api.start, 'API start'), control: control(api.control), boundaryFiles: boundaries, schema, operationIds: operations,
if (!boundaries.length || new Set(boundaries).size !== boundaries.length)
throw new CsoError('INVALID_SCHEMA', 'API scan needs unique security-boundary source paths');
request.api = {
runtimeProfile: string(api.runtimeProfile, 'API runtime profile', 100),
port: boundedInt(api.port, 1024, 65535, 'API port'),
start: validateCommand(api.start, 'API start'),
control: control(api.control),
boundaryFiles: boundaries,
schema,
operationIds: operations,
...(api.seed === undefined ? {} : { seed: boundedInt(api.seed, 1, 2147483647, 'API seed') }),
...(api.maxExamples === undefined ? {} : { maxExamples: boundedInt(api.maxExamples, 1, 100, 'API maxExamples') }) };
...(api.maxExamples === undefined
? {}
: { maxExamples: boundedInt(api.maxExamples, 1, 100, 'API maxExamples') }),
};
const raw = JSON.stringify(request);
if (redact(raw) !== raw) throw new CsoError('REDACTION_FAILED', 'Scanner harness contains secret-bearing material; use synthetic inputs');
if (redact(raw) !== raw)
throw new CsoError(
'REDACTION_FAILED',
'Scanner harness contains secret-bearing material; use synthetic inputs',
);
return request;
}
/** Resolve only helper-issued path references before any application command reaches containment. */
export function resolveScannerRequestPaths(manifest:SnapshotManifest,request:ScannerRequest):ScannerRequest{
if(!request.api)return request;
const resolve=(reference:string):string=>{const id=snapshotPathHandleId(reference);if(!id)return relativePath(reference);const entry=manifest.entries.find(item=>item.pathId===id);if(!entry)throw new CsoError('INVALID_SCHEMA',`API path handle is outside the retained snapshot: ${reference}`);return entry.path;};
const argument=(value:string):string=>{if(snapshotPathHandleId(value))return resolve(value);if(value.startsWith('./')&&snapshotPathHandleId(value.slice(2)))return `./${resolve(value.slice(2))}`;return value;};
return{...request,api:{...request.api,start:{...request.api.start,args:request.api.start.args.map(argument)},boundaryFiles:request.api.boundaryFiles.map(resolve)}};
export function resolveScannerRequestPaths(
manifest: SnapshotManifest,
request: ScannerRequest,
): ScannerRequest {
if (!request.api) return request;
const resolve = (reference: string): string => {
const id = snapshotPathHandleId(reference);
if (!id) return relativePath(reference);
const entry = manifest.entries.find((item) => item.pathId === id);
if (!entry)
throw new CsoError('INVALID_SCHEMA', `API path handle is outside the retained snapshot: ${reference}`);
return entry.path;
};
const argument = (value: string): string => {
if (snapshotPathHandleId(value)) return resolve(value);
if (value.startsWith('./') && snapshotPathHandleId(value.slice(2))) return `./${resolve(value.slice(2))}`;
return value;
};
return {
...request,
api: {
...request.api,
start: { ...request.api.start, args: request.api.start.args.map(argument) },
boundaryFiles: request.api.boundaryFiles.map(resolve),
},
};
}
export function scannerCoverage(outcome: ScannerOutcome, scope: string): CoverageRecord {
return { domain: `scanner:${outcome.tool}`, scope, status: outcome.status === 'complete' ? 'assessed' : outcome.status,
method: outcome.tool === 'sarif' ? 'bounded untrusted SARIF import' : 'qualified offline Docker scanner; candidate evidence only',
gaps: outcome.gaps.map(g => g.message), exclusions: outcome.exclusions,
return {
domain: `scanner:${outcome.tool}`,
scope,
status: outcome.status === 'complete' ? 'assessed' : outcome.status,
method:
outcome.tool === 'sarif'
? 'bounded untrusted SARIF import'
: 'qualified offline Docker scanner; candidate evidence only',
gaps: outcome.gaps.map((g) => g.message),
exclusions: outcome.exclusions,
evidence: [`${outcome.candidates.length} scanner candidates; plan ${outcome.planSha256}`],
tool: { name: outcome.tool, version: outcome.version ?? 'unavailable', freshness: outcome.databaseUpdatedAt ?? 'not reported', outcome: outcome.status } };
tool: {
name: outcome.tool,
version: outcome.version ?? 'unavailable',
freshness: outcome.databaseUpdatedAt ?? 'not reported',
outcome: outcome.status,
},
};
}
function failure(plan: ScannerPlan, error: unknown, version?: string): ScannerOutcome {
const e = error instanceof CsoError ? error : new CsoError('ISOLATION_FAILED', 'Scanner execution failed before bounded evidence was established');
const e =
error instanceof CsoError
? error
: new CsoError('ISOLATION_FAILED', 'Scanner execution failed before bounded evidence was established');
const codes: Record<ErrorCode, ScannerGap['code']> = {
INVALID_ARGUMENT: 'INVALID_OUTPUT', INVALID_SCHEMA: 'INVALID_OUTPUT', MISSING_INPUT: 'MISSING_INPUT', SNAPSHOT_RACE: 'SNAPSHOT_RACE',
UNSAFE_PATH: 'UNSAFE_PATH', REDACTION_FAILED: 'REDACTION_FAILED', PERSISTENCE_FAILED: 'PERSISTENCE_FAILED', TOOL_UNAVAILABLE: 'UNAVAILABLE',
TOOL_FAILED: 'TOOL_FAILED', ISOLATION_FAILED: 'ISOLATION_FAILED', INSUFFICIENT_CAPACITY: 'INSUFFICIENT_CAPACITY', DEADLINE: 'TIMEOUT',
CANCELLED: 'CANCELLED', PREREQUISITE: 'PREREQUISITE', INCOMPATIBLE_INPUT: 'PREREQUISITE', ASSERTION_FAILED: 'TOOL_FAILED',
INVALID_ARGUMENT: 'INVALID_OUTPUT',
INVALID_SCHEMA: 'INVALID_OUTPUT',
MISSING_INPUT: 'MISSING_INPUT',
SNAPSHOT_RACE: 'SNAPSHOT_RACE',
UNSAFE_PATH: 'UNSAFE_PATH',
REDACTION_FAILED: 'REDACTION_FAILED',
PERSISTENCE_FAILED: 'PERSISTENCE_FAILED',
TOOL_UNAVAILABLE: 'UNAVAILABLE',
TOOL_FAILED: 'TOOL_FAILED',
ISOLATION_FAILED: 'ISOLATION_FAILED',
INSUFFICIENT_CAPACITY: 'INSUFFICIENT_CAPACITY',
DEADLINE: 'TIMEOUT',
CANCELLED: 'CANCELLED',
PREREQUISITE: 'PREREQUISITE',
INCOMPATIBLE_INPUT: 'PREREQUISITE',
ASSERTION_FAILED: 'TOOL_FAILED',
};
const code = codes[e.code];
return { ...parseScannerOutput(plan, { stdout: '', exitCode: null, version }), status: 'not_assessed', candidates: [], gaps: [{ code, message: e.message }] };
return {
...parseScannerOutput(plan, { stdout: '', exitCode: null, version }),
status: 'not_assessed',
candidates: [],
gaps: [{ code, message: e.message }],
};
}
/** Empty catalogs and missing assets produce coverage gaps without opening Docker. */
export async function executeScanner(input: ScannerRunInput, dependencies: ScannerRunDependencies = {}): Promise<ScannerRunRecord> {
const identityRequest = validateScannerRequest(input.request ?? {}, input.id), catalog = dependencies.catalog ?? SCANNER_CATALOG;
export async function executeScanner(
input: ScannerRunInput,
dependencies: ScannerRunDependencies = {},
): Promise<ScannerRunRecord> {
const identityRequest = validateScannerRequest(input.request ?? {}, input.id),
catalog = dependencies.catalog ?? SCANNER_CATALOG;
const timeout = Math.min(300, Math.floor((input.executionDeadline - Date.now()) / 1000));
let profile: QualifiedScanner | undefined, runtime: QualifiedRuntime | undefined, observedVersion: string | undefined, versionHash: string | null = null;
let application: ScannerApplicationPreparation | undefined,request=identityRequest;
let plan = scannerPlans({ snapshotRoot: '/source', offline: input.policy.offline, selected: [input.id], deadlineSeconds: Math.max(1, timeout) })[0];
let profile: QualifiedScanner | undefined,
runtime: QualifiedRuntime | undefined,
observedVersion: string | undefined,
versionHash: string | null = null;
let application: ScannerApplicationPreparation | undefined,
request = identityRequest;
let plan = scannerPlans({
snapshotRoot: '/source',
offline: input.policy.offline,
selected: [input.id],
deadlineSeconds: Math.max(1, timeout),
})[0];
let outcome: ScannerOutcome, runner: ScannerRunner | undefined;
try {
if (timeout < 1) throw new CsoError('DEADLINE', 'No scanner time remains before the reporting reserve');
assertSnapshot(input.runDir, input.manifest);
request=resolveScannerRequestPaths(input.manifest,identityRequest);
if (input.id === 'schemathesis' && input.policy.mode !== 'comprehensive') throw new CsoError('PREREQUISITE', 'Schemathesis requires comprehensive mode; daily audits do not execute applications');
request = resolveScannerRequestPaths(input.manifest, identityRequest);
if (input.id === 'schemathesis' && input.policy.mode !== 'comprehensive')
throw new CsoError(
'PREREQUISITE',
'Schemathesis requires comprehensive mode; daily audits do not execute applications',
);
profile = selectScanner(input.id, input.platform, request.profile, catalog);
const api = request.api;
plan = scannerPlans({ snapshotRoot: '/source', offline: input.policy.offline, selected: [input.id], deadlineSeconds: timeout,
tools: { [input.id]: { available: true, version: profile.version, capabilities: profile.capabilities } },
semgrepRules: profile.assets?.semgrepRules?.path, advisoryCache: profile.assets?.advisoryDatabase?.path,
...(api ? { schemaPath: '/policy/openapi.json', baseUrl: `http://127.0.0.1:${api.port}/`, operationIds: api.operationIds, seed: api.seed, maxExamples: api.maxExamples } : {}) })[0];
plan = scannerPlans({
snapshotRoot: '/source',
offline: input.policy.offline,
selected: [input.id],
deadlineSeconds: timeout,
tools: {
[input.id]: { available: true, version: profile.version, capabilities: profile.capabilities },
},
semgrepRules: profile.assets?.semgrepRules?.path,
advisoryCache: profile.assets?.advisoryDatabase?.path,
...(api
? {
schemaPath: '/policy/openapi.json',
baseUrl: `http://127.0.0.1:${api.port}/`,
operationIds: api.operationIds,
seed: api.seed,
maxExamples: api.maxExamples,
}
: {}),
})[0];
if (plan.prerequisites.length) throw new CsoError('PREREQUISITE', plan.prerequisites.join('; '));
if (input.id === 'schemathesis') {
if (!api) throw new CsoError('PREREQUISITE', 'Schemathesis requires a reviewed API harness and legitimate control');
if (!api)
throw new CsoError(
'PREREQUISITE',
'Schemathesis requires a reviewed API harness and legitimate control',
);
for (const file of api.boundaryFiles) {
const entry = input.manifest.entries.find(e => e.path === file);
if (!entry || !entry.executionHash || entry.transformation) throw new CsoError('INCOMPATIBLE_INPUT', `API security boundary is missing or transformed: ${file}`);
const entry = input.manifest.entries.find((e) => e.path === file);
if (!entry || !entry.executionHash || entry.transformation)
throw new CsoError(
'INCOMPATIBLE_INPUT',
`API security boundary is missing or transformed: ${file}`,
);
}
try { runtime = selectRuntime(api.runtimeProfile, input.platform, dependencies.runtimes ?? RUNTIME_CATALOG); }
catch { throw new CsoError('PREREQUISITE', `Qualified application runtime is unavailable: ${api.runtimeProfile}`); }
if (!['node', 'bun', 'python', 'rails'].includes(runtime.stack)) throw new CsoError('INCOMPATIBLE_INPUT', 'Schemathesis requires a qualified application runtime');
const stack = runtime.stack as CsoStack, sourceRoot = join(input.runDir, 'snapshot');
try {
runtime = selectRuntime(api.runtimeProfile, input.platform, dependencies.runtimes ?? RUNTIME_CATALOG);
} catch {
throw new CsoError(
'PREREQUISITE',
`Qualified application runtime is unavailable: ${api.runtimeProfile}`,
);
}
if (!['node', 'bun', 'python', 'rails'].includes(runtime.stack))
throw new CsoError('INCOMPATIBLE_INPUT', 'Schemathesis requires a qualified application runtime');
const stack = runtime.stack as CsoStack,
sourceRoot = join(input.runDir, 'snapshot');
const preparation = inspectPreparation(sourceRoot, stack);
assertRuntimeCompatible(preparation, runtime);
const startPlan = canonicalStartPlan(sourceRoot, stack, api.port);
if (canonical(api.start) !== canonical(startPlan.command)) throw new CsoError('INVALID_SCHEMA', `API start must use the helper-derived ${startPlan.kind} command`);
for (const file of startPlan.entrypointFiles) if (!api.boundaryFiles.includes(file))
throw new CsoError('INVALID_SCHEMA', `API boundary files must include canonical startup input: ${file}`);
if (canonical(api.start) !== canonical(startPlan.command))
throw new CsoError(
'INVALID_SCHEMA',
`API start must use the helper-derived ${startPlan.kind} command`,
);
for (const file of startPlan.entrypointFiles)
if (!api.boundaryFiles.includes(file))
throw new CsoError(
'INVALID_SCHEMA',
`API boundary files must include canonical startup input: ${file}`,
);
application = await (dependencies.applicationPreparer ?? prepareDockerScannerApplication)({
input: { ...input, request }, runtime, stack, startPlan,
deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000), catalog: dependencies.runtimes ?? RUNTIME_CATALOG,
input: { ...input, request },
runtime,
stack,
startPlan,
deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000),
catalog: dependencies.runtimes ?? RUNTIME_CATALOG,
});
const preparedStart = canonicalStartPlan(application.sourceRoot, stack, api.port);
if (preparedStart.signature !== startPlan.signature || canonical(preparedStart.command) !== canonical(startPlan.command))
throw new CsoError('ISOLATION_FAILED', 'Offline API preparation changed the canonical application startup inputs');
if (
preparedStart.signature !== startPlan.signature ||
canonical(preparedStart.command) !== canonical(startPlan.command)
)
throw new CsoError(
'ISOLATION_FAILED',
'Offline API preparation changed the canonical application startup inputs',
);
}
runner = await (dependencies.runnerFactory ?? createDockerScannerRunner)({ input: { ...input, request }, plan, profile, runtime, application, deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000) });
runner = await (dependencies.runnerFactory ?? createDockerScannerRunner)({
input: { ...input, request },
plan,
profile,
runtime,
application,
deadline: Math.min(input.executionDeadline, Date.now() + timeout * 1000),
});
const version = await runner.version();
if (version.exitCode !== 0 || version.timedOut || version.truncated || version.unavailable || Buffer.byteLength(version.stdout) + Buffer.byteLength(version.stderr ?? '') > 8192) throw new CsoError('TOOL_UNAVAILABLE', 'Scanner version probe did not complete within the qualified sandbox');
assertScannerVersionOutput(profile.scanner,profile.version,version.stdout,version.stderr);
if (
version.exitCode !== 0 ||
version.timedOut ||
version.truncated ||
version.unavailable ||
Buffer.byteLength(version.stdout) + Buffer.byteLength(version.stderr ?? '') > 8192
)
throw new CsoError(
'TOOL_UNAVAILABLE',
'Scanner version probe did not complete within the qualified sandbox',
);
assertScannerVersionOutput(profile.scanner, profile.version, version.stdout, version.stderr);
versionHash = scannerVersionHash(version.stdout, version.stderr);
if (versionHash !== profile.versionOutputSha256) throw new CsoError('INCOMPATIBLE_INPUT', 'Scanner version output does not match its reviewed image profile');
if (versionHash !== profile.versionOutputSha256)
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Scanner version output does not match its reviewed image profile',
);
observedVersion = profile.version;
const execution = await runner.scan();
assertSnapshot(input.runDir, input.manifest);
outcome = parseScannerOutput(plan, { ...execution, version: profile.version, databaseUpdatedAt: profile.assets?.advisoryDatabase?.updatedAt });
} catch (error) { outcome = failure(plan, error, observedVersion); }
finally {
outcome = parseScannerOutput(plan, {
...execution,
version: profile.version,
databaseUpdatedAt: profile.assets?.advisoryDatabase?.updatedAt,
});
} catch (error) {
outcome = failure(plan, error, observedVersion);
} finally {
let cleanupError: unknown;
if (runner) try { await runner.cleanup(); } catch (error) { cleanupError = error; }
if (application) try { await application.cleanup(); } catch (error) { cleanupError ??= error; }
if (runner)
try {
await runner.cleanup();
} catch (error) {
cleanupError = error;
}
if (application)
try {
await application.cleanup();
} catch (error) {
cleanupError ??= error;
}
if (cleanupError) outcome = failure(plan, cleanupError, observedVersion);
}
return { outcome: outcome!, coverage: scannerCoverage(outcome!, input.policy.scope), provenance: {
scannerCatalog: catalog.revision, profile: profile?.id ?? null, image: profile?.image ?? null, platform: input.platform,
isolationPolicyHash: profile?.isolationPolicyHash ?? null, sourceHash: input.manifest.executionHash, requestHash: sha256(canonical(identityRequest)), versionOutputSha256: versionHash,
assets: profile?.assets ?? null, network: plan.network === 'loopback' ? 'isolated-loopback' : 'none', preparation: application?.proof ?? null } };
return {
outcome: outcome!,
coverage: scannerCoverage(outcome!, input.policy.scope),
provenance: {
scannerCatalog: catalog.revision,
profile: profile?.id ?? null,
image: profile?.image ?? null,
platform: input.platform,
isolationPolicyHash: profile?.isolationPolicyHash ?? null,
sourceHash: input.manifest.executionHash,
requestHash: sha256(canonical(identityRequest)),
versionOutputSha256: versionHash,
assets: profile?.assets ?? null,
network: plan.network === 'loopback' ? 'isolated-loopback' : 'none',
preparation: application?.proof ?? null,
},
};
}
export async function prepareDockerScannerApplication(context: Parameters<ScannerApplicationPreparer>[0],dependencies:{endpoint?:DockerEndpoint;runnerFactory?:(options:ConstructorParameters<typeof DockerPreparationSandboxRunner>[0])=>PreparationSandboxRunner;cacheRoot?:string}={}): Promise<ScannerApplicationPreparation> {
export async function prepareDockerScannerApplication(
context: Parameters<ScannerApplicationPreparer>[0],
dependencies: {
endpoint?: DockerEndpoint;
runnerFactory?: (
options: ConstructorParameters<typeof DockerPreparationSandboxRunner>[0],
) => PreparationSandboxRunner;
cacheRoot?: string;
} = {},
): Promise<ScannerApplicationPreparation> {
const { input, runtime, stack, deadline, catalog } = context;
const root = secureDirectory(join(input.runDir, 'supervision', `scanner-preparation-${randomBytes(12).toString('hex')}`));
let executor: PreparationExecutor | undefined, prepared: Awaited<ReturnType<PreparationExecutor['prepareOffline']>> | undefined;
const root = secureDirectory(
join(input.runDir, 'supervision', `scanner-preparation-${randomBytes(12).toString('hex')}`),
);
let executor: PreparationExecutor | undefined,
prepared: Awaited<ReturnType<PreparationExecutor['prepareOffline']>> | undefined;
try {
const plan = inspectPreparation(join(input.runDir, 'snapshot'), stack);
const admission = admitPreparationRuntime({ plan, platform: input.platform, profile: runtime.id, catalog });
const endpoint = dependencies.endpoint??await dockerEndpoint(root),runnerOptions={ endpoint, watchdogPath: input.watchdogPath,
runRoot: root, controlRoot: secureDirectory(join(root, 'execution')), admission },runner=dependencies.runnerFactory?dependencies.runnerFactory(runnerOptions):new DockerPreparationSandboxRunner(runnerOptions);
executor = new PreparationExecutor({ cache: new PublicArchiveCache({ root: dependencies.cacheRoot??publicArchiveCacheRoot(), stagingRoot: secureDirectory(join(root, 'staging')) }),
runner, materializationRoot: secureDirectory(join(root, 'materializations')) });
const closure = await executor.acquire({ plan, admission, snapshot: join(input.runDir, 'snapshot'), deadline, offline: input.policy.offline });
let database:RailsDatabaseSelection|undefined;
if(stack==='rails'){
if(!plan.database?.selected)throw new CsoError('PREREQUISITE','Rails API preparation could not select one locked database adapter');
database=plan.database.selected==='postgresql'
?{adapter:'postgresql',sidecar:admitPreparationSidecar({platform:input.platform,catalog})}:{adapter:'sqlite'};
const admission = admitPreparationRuntime({
plan,
platform: input.platform,
profile: runtime.id,
catalog,
});
const endpoint = dependencies.endpoint ?? (await dockerEndpoint(root)),
runnerOptions = {
endpoint,
watchdogPath: input.watchdogPath,
runRoot: root,
controlRoot: secureDirectory(join(root, 'execution')),
admission,
},
runner = dependencies.runnerFactory
? dependencies.runnerFactory(runnerOptions)
: new DockerPreparationSandboxRunner(runnerOptions);
executor = new PreparationExecutor({
cache: new PublicArchiveCache({
root: dependencies.cacheRoot ?? publicArchiveCacheRoot(),
stagingRoot: secureDirectory(join(root, 'staging')),
}),
runner,
materializationRoot: secureDirectory(join(root, 'materializations')),
});
const closure = await executor.acquire({
plan,
admission,
snapshot: join(input.runDir, 'snapshot'),
deadline,
offline: input.policy.offline,
});
let database: RailsDatabaseSelection | undefined;
if (stack === 'rails') {
if (!plan.database?.selected)
throw new CsoError(
'PREREQUISITE',
'Rails API preparation could not select one locked database adapter',
);
database =
plan.database.selected === 'postgresql'
? { adapter: 'postgresql', sidecar: admitPreparationSidecar({ platform: input.platform, catalog }) }
: { adapter: 'sqlite' };
}
prepared = await executor.prepareOffline({ plan, admission, snapshot: join(input.runDir, 'snapshot'), closure, deadline, database });
const proof = { dependencyClosureHash: prepared.dependencyClosureHash, preparedManifestHash: prepared.preparedManifestHash,
sourceProjectionHash: prepared.sourceProjectionHash, receiptHash: prepared.receiptHash,
executionEnvironmentHash: sha256(canonical(prepared.executionEnvironment)), databaseHash: prepared.databaseHash };
prepared = await executor.prepareOffline({
plan,
admission,
snapshot: join(input.runDir, 'snapshot'),
closure,
deadline,
database,
});
const proof = {
dependencyClosureHash: prepared.dependencyClosureHash,
preparedManifestHash: prepared.preparedManifestHash,
sourceProjectionHash: prepared.sourceProjectionHash,
receiptHash: prepared.receiptHash,
executionEnvironmentHash: sha256(canonical(prepared.executionEnvironment)),
databaseHash: prepared.databaseHash,
};
let cleaned = false;
return { sourceRoot: prepared.preparedRoot, environment: prepared.executionEnvironment, database: prepared.database, proof, cleanup: async () => {
if (cleaned) return; cleaned = true;
await executor!.dispose(prepared!);
fs.rmSync(root, { recursive: true, force: false });
} };
return {
sourceRoot: prepared.preparedRoot,
environment: prepared.executionEnvironment,
database: prepared.database,
proof,
cleanup: async () => {
if (cleaned) return;
cleaned = true;
await executor!.dispose(prepared!);
fs.rmSync(root, { recursive: true, force: false });
},
};
} catch (error) {
let cleanupError:unknown;
if (prepared && executor) try { await executor.dispose(prepared); } catch (failed) { cleanupError=failed; }
let cleanupError: unknown;
if (prepared && executor)
try {
await executor.dispose(prepared);
} catch (failed) {
cleanupError = failed;
}
// A failed Docker/retained-copy cleanup deliberately hands ownership to a
// detached watchdog. Its journals and label-sweep scratch files live below
// this root, so only remove the tree after every watchdog acknowledged.
let pending=true;try{pending=hasPendingWatchdogCleanup(input.runDir);}catch(failed){cleanupError??=failed;}
if(!cleanupError&&!pending)try { fs.rmSync(root, { recursive: true, force: false }); } catch {}
if(cleanupError)throw cleanupError;
let pending = true;
try {
pending = hasPendingWatchdogCleanup(input.runDir);
} catch (failed) {
cleanupError ??= failed;
}
if (!cleanupError && !pending)
try {
fs.rmSync(root, { recursive: true, force: false });
} catch {}
if (cleanupError) throw cleanupError;
throw error;
}
}
@@ -320,8 +696,10 @@ export async function createDockerScannerRunner(context: ScannerRunnerContext):
const policyDir = secureDirectory(join(controlDir, 'policy'));
const files: Array<{ host: string; container: string }> = [];
const writePolicy = (container: string, content: string): void => {
if (redact(content) !== content) throw new CsoError('REDACTION_FAILED', 'Scanner policy contains secret-bearing material');
const host = join(policyDir, String(files.length)); fs.writeFileSync(host, content, { mode: 0o600, flag: 'wx' });
if (redact(content) !== content)
throw new CsoError('REDACTION_FAILED', 'Scanner policy contains secret-bearing material');
const host = join(policyDir, String(files.length));
fs.writeFileSync(host, content, { mode: 0o600, flag: 'wx' });
files.push({ host, container });
};
let group: DockerGroup | undefined;
@@ -329,12 +707,27 @@ export async function createDockerScannerRunner(context: ScannerRunnerContext):
for (const file of plan.trustedFiles) writePolicy(file.path, file.content);
if (input.request?.api) writePolicy('/policy/openapi.json', JSON.stringify(input.request.api.schema));
const endpoint: DockerEndpoint = await dockerEndpoint(controlDir);
group = await DockerGroup.create(endpoint, attempt, controlDir, deadline, profile.image, input.watchdogPath);
const createScanner=()=>group!.createContainer({ role: runtime ? 'verifier' : 'app', image: profile.image, source: join(input.runDir, 'snapshot'), command: ['/bin/sleep', '2147483647'], env: plan.env, readonlyFiles: files });
group = await DockerGroup.create(
endpoint,
attempt,
controlDir,
deadline,
profile.image,
input.watchdogPath,
);
const createScanner = () =>
group!.createContainer({
role: runtime ? 'verifier' : 'app',
image: profile.image,
source: join(input.runDir, 'snapshot'),
command: ['/bin/sleep', '2147483647'],
env: plan.env,
readonlyFiles: files,
});
let scanner = await createScanner();
await group.start(scanner);
const capture = async (command: string[]): Promise<ScannerExecution> => {
if(!scanner)throw new CsoError('ISOLATION_FAILED','Scanner container is unavailable');
if (!scanner) throw new CsoError('ISOLATION_FAILED', 'Scanner container is unavailable');
const result = await group!.execCapture(scanner, command, { workdir: '/work', env: plan.env });
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.code };
};
@@ -343,41 +736,132 @@ export async function createDockerScannerRunner(context: ScannerRunnerContext):
scan: async () => {
const api = input.request?.api;
if (api && runtime) {
if (!application) throw new CsoError('ISOLATION_FAILED', 'Schemathesis application was not materialized through offline preparation');
const env={ ...application.environment, PORT: String(api.port), HOST: '127.0.0.1', NODE_ENV: 'test', RAILS_ENV: 'test', RACK_ENV: 'test', PYTHONUNBUFFERED: '1', CI: '1', SECRET_KEY_BASE: 'cso-synthetic-test-key' };
const rails=runtime.stack==='rails';
if(rails){await group!.removeContainer(scanner);scanner='';}
if(application.database?.adapter==='postgresql'){
const databaseFile=join(policyDir,'postgresql.databases'),names=application.database.connections.map(name=>`cso_${name}`);
if(!names.length||names.some(name=>!/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))throw new CsoError('INCOMPATIBLE_INPUT','Prepared PostgreSQL connection names are invalid');
fs.writeFileSync(databaseFile,names.join('\n')+'\n',{mode:0o444,flag:'wx'});
const postgres=await group!.createContainer({role:'postgres',image:application.database.sidecar.image,command:['/opt/cso/run-postgresql','/policy/postgresql.databases'],postgresDatabasePolicy:databaseFile});await group!.start(postgres);
let ready=false;for(let attempt=0;attempt<100&&!ready;attempt++){const checked=await group!.execCapture(postgres,['/opt/cso/postgresql-ready','/policy/postgresql.databases']);ready=checked.code===0;if(!ready)await new Promise(resolveWait=>setTimeout(resolveWait,50));}
if(!ready)throw new CsoError('TOOL_FAILED','Disposable PostgreSQL did not become ready for Rails API scanning');
if (!application)
throw new CsoError(
'ISOLATION_FAILED',
'Schemathesis application was not materialized through offline preparation',
);
const env = {
...application.environment,
PORT: String(api.port),
HOST: '127.0.0.1',
NODE_ENV: 'test',
RAILS_ENV: 'test',
RACK_ENV: 'test',
PYTHONUNBUFFERED: '1',
CI: '1',
SECRET_KEY_BASE: 'cso-synthetic-test-key',
};
const rails = runtime.stack === 'rails';
if (rails) {
await group!.removeContainer(scanner);
scanner = '';
}
const app = await group!.createContainer({ role: 'app', image: runtime.image, source: application.sourceRoot, env,
command:rails?['/opt/cso/run-app','/bin/sleep','2147483647']:['/opt/cso/run-app', api.start.executable, ...api.start.args] });
if (application.database?.adapter === 'postgresql') {
const databaseFile = join(policyDir, 'postgresql.databases'),
names = application.database.connections.map((name) => `cso_${name}`);
if (!names.length || names.some((name) => !/^cso_[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(name)))
throw new CsoError('INCOMPATIBLE_INPUT', 'Prepared PostgreSQL connection names are invalid');
fs.writeFileSync(databaseFile, names.join('\n') + '\n', { mode: 0o444, flag: 'wx' });
const postgres = await group!.createContainer({
role: 'postgres',
image: application.database.sidecar.image,
command: ['/opt/cso/run-postgresql', '/policy/postgresql.databases'],
postgresDatabasePolicy: databaseFile,
});
await group!.start(postgres);
let ready = false;
for (let attempt = 0; attempt < 100 && !ready; attempt++) {
const checked = await group!.execCapture(postgres, [
'/opt/cso/postgresql-ready',
'/policy/postgresql.databases',
]);
ready = checked.code === 0;
if (!ready) await new Promise((resolveWait) => setTimeout(resolveWait, 50));
}
if (!ready)
throw new CsoError(
'TOOL_FAILED',
'Disposable PostgreSQL did not become ready for Rails API scanning',
);
}
const app = await group!.createContainer({
role: 'app',
image: runtime.image,
source: application.sourceRoot,
env,
command: rails
? ['/opt/cso/run-app', '/bin/sleep', '2147483647']
: ['/opt/cso/run-app', api.start.executable, ...api.start.args],
});
await group!.start(app);
if(rails){const clean=['/usr/bin/env','-i',...Object.entries(env).sort(([a],[b])=>a.localeCompare(b)).map(([key,value])=>`${key}=${value}`),'/usr/local/bin/bundle','exec','rails','db:prepare'];const prepared=await group!.execCapture(app,clean,{workdir:'/work'});if(prepared.code!==0)throw new CsoError('TOOL_FAILED','Rails API database preparation failed');await group!.execDetached(app,[api.start.executable,...api.start.args]);}
const security = { ...api.control, vulnerable: { status: api.control.expected.status === 599 ? 598 : 599 } };
if (rails) {
const clean = [
'/usr/bin/env',
'-i',
...Object.entries(env)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`),
'/usr/local/bin/bundle',
'exec',
'rails',
'db:prepare',
];
const prepared = await group!.execCapture(app, clean, { workdir: '/work' });
if (prepared.code !== 0)
throw new CsoError('TOOL_FAILED', 'Rails API database preparation failed');
await group!.execDetached(app, [api.start.executable, ...api.start.args]);
}
const security = {
...api.control,
vulnerable: { status: api.control.expected.status === 599 ? 598 : 599 },
};
const controlFile = join(policyDir, 'control.json');
fs.writeFileSync(controlFile, JSON.stringify({ phase: 'after', port: api.port, legitimate: [api.control], security }), { mode: 0o600, flag: 'wx' });
const probe = await group!.createContainer({ role: schemathesisControlRole(), image: runtime.image, command: ['/opt/cso/verifier', '/policy/control.json'], readonlyFiles: [{ host: controlFile, container: '/policy/control.json' }] });
const observed = await group!.startAttach(probe); await group!.removeContainer(probe);
fs.writeFileSync(
controlFile,
JSON.stringify({ phase: 'after', port: api.port, legitimate: [api.control], security }),
{ mode: 0o600, flag: 'wx' },
);
const probe = await group!.createContainer({
role: schemathesisControlRole(),
image: runtime.image,
command: ['/opt/cso/verifier', '/policy/control.json'],
readonlyFiles: [{ host: controlFile, container: '/policy/control.json' }],
});
const observed = await group!.startAttach(probe);
await group!.removeContainer(probe);
let valid = false;
try { const v = validateVerificationObservation(JSON.parse(observed.output)); valid = observed.code === 0 && v.booted && v.legitimate && v.security === 'pass'; } catch {}
if (!valid) throw new CsoError('PREREQUISITE', 'API application boot or legitimate control failed; no Schemathesis requests were sent');
if(rails){scanner=await createScanner();await group!.start(scanner);}
try {
const v = validateVerificationObservation(JSON.parse(observed.output));
valid = observed.code === 0 && v.booted && v.legitimate && v.security === 'pass';
} catch {}
if (!valid)
throw new CsoError(
'PREREQUISITE',
'API application boot or legitimate control failed; no Schemathesis requests were sent',
);
if (rails) {
scanner = await createScanner();
await group!.start(scanner);
}
}
const execution = await capture([profile.executable, ...plan.args]);
if (plan.outputPath) {
const report = await capture(['/bin/cat', plan.outputPath]);
if (report.exitCode !== 0) throw new CsoError('PREREQUISITE', 'Scanner did not produce its required bounded report file');
return { ...execution, stdout: report.stdout, stderr: [execution.stderr, report.stderr].filter(Boolean).join('\n') };
if (report.exitCode !== 0)
throw new CsoError('PREREQUISITE', 'Scanner did not produce its required bounded report file');
return {
...execution,
stdout: report.stdout,
stderr: [execution.stderr, report.stderr].filter(Boolean).join('\n'),
};
}
return execution;
},
cleanup: async () => { await group!.cleanup(); fs.rmSync(policyDir, { recursive: true, force: true }); },
cleanup: async () => {
await group!.cleanup();
fs.rmSync(policyDir, { recursive: true, force: true });
},
};
} catch (error) {
if (group) await group.cleanup();
+581 -126
View File
@@ -8,8 +8,9 @@ import { posix } from 'node:path';
import { redactFindingSpans } from '../redact-engine';
export const SCANNER_IDS = ['gitleaks', 'osv', 'semgrep', 'zizmor', 'trivy', 'schemathesis'] as const;
export type ScannerId = typeof SCANNER_IDS[number];
export type ScannerFormat = 'gitleaks-json' | 'osv-json' | 'semgrep-json' | 'sarif' | 'trivy-json' | 'schemathesis-json';
export type ScannerId = (typeof SCANNER_IDS)[number];
export type ScannerFormat =
'gitleaks-json' | 'osv-json' | 'semgrep-json' | 'sarif' | 'trivy-json' | 'schemathesis-json';
export const MAX_SCANNER_OUTPUT_BYTES = 1_048_576;
const MAX_CANDIDATES = 5_000;
@@ -63,7 +64,13 @@ export interface ScannerCandidate {
reportedSeverity: 'critical' | 'high' | 'medium' | 'low' | 'info' | 'unknown';
location?: { path: string; line?: number; column?: number };
advisoryIds: string[];
dependency?: { name: string; version?: string; ecosystem?: string; reachability: 'unknown'; exposure: 'unknown' };
dependency?: {
name: string;
version?: string;
ecosystem?: string;
reachability: 'unknown';
exposure: 'unknown';
};
operation?: string;
suppressed: boolean;
evidence: 'scanner-candidate';
@@ -71,10 +78,25 @@ export interface ScannerCandidate {
}
export interface ScannerGap {
code: 'UNAVAILABLE' | 'PREREQUISITE' | 'TIMEOUT' | 'OUTPUT_LIMIT' | 'INVALID_OUTPUT' | 'TOOL_FAILED' |
'REDACTION_FAILED' | 'ISOLATION_FAILED' | 'PERSISTENCE_FAILED' | 'SNAPSHOT_RACE' | 'CANCELLED' |
'INSUFFICIENT_CAPACITY' | 'UNSAFE_PATH' | 'MISSING_INPUT' | 'INCOMPATIBLE_INPUT' |
'UNSAFE_LOCATION' | 'SKIPPED_INPUT' | 'UNKNOWN_FRESHNESS';
code:
| 'UNAVAILABLE'
| 'PREREQUISITE'
| 'TIMEOUT'
| 'OUTPUT_LIMIT'
| 'INVALID_OUTPUT'
| 'TOOL_FAILED'
| 'REDACTION_FAILED'
| 'ISOLATION_FAILED'
| 'PERSISTENCE_FAILED'
| 'SNAPSHOT_RACE'
| 'CANCELLED'
| 'INSUFFICIENT_CAPACITY'
| 'UNSAFE_PATH'
| 'MISSING_INPUT'
| 'INCOMPATIBLE_INPUT'
| 'UNSAFE_LOCATION'
| 'SKIPPED_INPUT'
| 'UNKNOWN_FRESHNESS';
message: string;
}
@@ -109,34 +131,64 @@ export interface ScannerExecution {
const SOURCES: Record<ScannerId, string[]> = {
gitleaks: ['https://github.com/gitleaks/gitleaks/blob/master/README.md'],
osv: ['https://google.github.io/osv-scanner/usage/scan-source/', 'https://google.github.io/osv-scanner/usage/offline-mode/'],
osv: [
'https://google.github.io/osv-scanner/usage/scan-source/',
'https://google.github.io/osv-scanner/usage/offline-mode/',
],
semgrep: ['https://docs.semgrep.dev/cli-reference'],
zizmor: ['https://docs.zizmor.sh/usage/', 'https://docs.zizmor.sh/quickstart/'],
trivy: ['https://trivy.dev/docs/dev/docs/advanced/telemetry/', 'https://trivy.dev/docs/latest/guide/advanced/air-gap/'],
schemathesis: ['https://schemathesis.readthedocs.io/en/stable/reference/cli/', 'https://github.com/schemathesis/schemathesis/blob/master/src/schemathesis/cli/json_report.py'],
trivy: [
'https://trivy.dev/docs/dev/docs/advanced/telemetry/',
'https://trivy.dev/docs/latest/guide/advanced/air-gap/',
],
schemathesis: [
'https://schemathesis.readthedocs.io/en/stable/reference/cli/',
'https://github.com/schemathesis/schemathesis/blob/master/src/schemathesis/cli/json_report.py',
],
};
function absolutePath(value: string, name: string): string {
if (value === '/' || !value.startsWith('/') || value.startsWith('//') || /[\x00-\x1f\\]/.test(value) || value.split('/').includes('..')) {
if (
value === '/' ||
!value.startsWith('/') ||
value.startsWith('//') ||
/[\x00-\x1f\\]/.test(value) ||
value.split('/').includes('..')
) {
throw new Error(`${name} must be an absolute sandbox path without traversal`);
}
return posix.normalize(value);
}
function positiveInteger(value: number, max: number, name: string): number {
if (!Number.isSafeInteger(value) || value < 1 || value > max) throw new Error(`${name} must be between 1 and ${max}`);
if (!Number.isSafeInteger(value) || value < 1 || value > max)
throw new Error(`${name} must be between 1 and ${max}`);
return value;
}
/** Numeric loopback only: no DNS, URL credentials, redirected targets, or remote schemas. */
export function validateScannerBaseUrl(raw: string): string {
let url: URL;
try { url = new URL(raw); } catch { throw new Error('Schemathesis requires a numeric loopback HTTP URL'); }
if (!['http:', 'https:'].includes(url.protocol) || !['127.0.0.1', '[::1]'].includes(url.hostname) || url.username || url.password || url.hash || url.search) {
throw new Error('Schemathesis requires a numeric loopback HTTP URL without credentials, query, or fragment');
try {
url = new URL(raw);
} catch {
throw new Error('Schemathesis requires a numeric loopback HTTP URL');
}
if (
!['http:', 'https:'].includes(url.protocol) ||
!['127.0.0.1', '[::1]'].includes(url.hostname) ||
url.username ||
url.password ||
url.hash ||
url.search
) {
throw new Error(
'Schemathesis requires a numeric loopback HTTP URL without credentials, query, or fragment',
);
}
// URL canonicalization accepts integer, hex, and shorthand IPv4. Reject these spellings.
if (!/^https?:\/\/(127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/.test(raw)) throw new Error('Schemathesis requires canonical numeric loopback');
if (!/^https?:\/\/(127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/.test(raw))
throw new Error('Schemathesis requires canonical numeric loopback');
return url.href;
}
@@ -148,99 +200,288 @@ export function validateScannerBaseUrl(raw: string): string {
export function scannerPlans(opts: ScannerOptions): ScannerPlan[] {
const root = absolutePath(opts.snapshotRoot, 'snapshotRoot');
const policy = absolutePath(opts.policyRoot ?? '/policy', 'policyRoot');
if (policy === root || policy.startsWith(`${root}/`) || root.startsWith(`${policy}/`)) throw new Error('policyRoot must be separate from source');
if (policy === root || policy.startsWith(`${root}/`) || root.startsWith(`${policy}/`))
throw new Error('policyRoot must be separate from source');
const cache = opts.advisoryCache ? absolutePath(opts.advisoryCache, 'advisoryCache') : undefined;
if (cache && (cache === root || cache.startsWith(`${root}/`))) throw new Error('advisoryCache must be separate from source');
if (cache && (cache === root || cache.startsWith(`${root}/`)))
throw new Error('advisoryCache must be separate from source');
const timeout = positiveInteger(opts.deadlineSeconds ?? 120, 300, 'deadlineSeconds');
const selected = opts.selected ?? [...SCANNER_IDS];
if (new Set(selected).size !== selected.length || selected.some(id => !SCANNER_IDS.includes(id))) throw new Error('Invalid or duplicate scanner selection');
return selected.map(id => {
if (new Set(selected).size !== selected.length || selected.some((id) => !SCANNER_IDS.includes(id)))
throw new Error('Invalid or duplicate scanner selection');
return selected.map((id) => {
const plan: ScannerPlan = {
id, executableName: id === 'osv' ? 'osv-scanner' : id, args: [], versionArgs: ['--version'], requiredFeatures: [],
format: 'sarif', execution: 'sandbox', network: 'none', cwd: '/work', sourceRoot: root,
env: { HOME: '/work/home', TMPDIR: '/tmp', LANG: 'C.UTF-8', NO_COLOR: '1' }, trustedFiles: [], prerequisites: [],
timeoutSeconds: timeout, maxOutputBytes: MAX_SCANNER_OUTPUT_BYTES,
coverage: { domain: id, scope: [root], exclusions: ['Snapshot transformations apply; inspect the snapshot manifest.'] },
provenanceSources: SOURCES[id], documentationInspectedAt: '2026-09-09',
id,
executableName: id === 'osv' ? 'osv-scanner' : id,
args: [],
versionArgs: ['--version'],
requiredFeatures: [],
format: 'sarif',
execution: 'sandbox',
network: 'none',
cwd: '/work',
sourceRoot: root,
env: { HOME: '/work/home', TMPDIR: '/tmp', LANG: 'C.UTF-8', NO_COLOR: '1' },
trustedFiles: [],
prerequisites: [],
timeoutSeconds: timeout,
maxOutputBytes: MAX_SCANNER_OUTPUT_BYTES,
coverage: {
domain: id,
scope: [root],
exclusions: ['Snapshot transformations apply; inspect the snapshot manifest.'],
},
provenanceSources: SOURCES[id],
documentationInspectedAt: '2026-09-09',
};
if (opts.tools?.[id]?.available === false) plan.prerequisites.push(`Install a reviewed ${plan.executableName} executable in the scanner image.`);
if (opts.tools?.[id]?.available === false)
plan.prerequisites.push(`Install a reviewed ${plan.executableName} executable in the scanner image.`);
switch (id) {
case 'gitleaks': {
const target = opts.gitHistory ? absolutePath(opts.gitHistory, 'gitHistory') : root;
plan.format = 'gitleaks-json';
plan.coverage.domain = 'secrets';
plan.coverage.scope = [target];
plan.trustedFiles.push({ path: `${policy}/gitleaks.toml`, content: '[extend]\nuseDefault = true\n' }, { path: `${policy}/gitleaksignore`, content: '' });
plan.args = [opts.gitHistory ? 'git' : 'dir', '--redact=100', '--no-banner', '--no-color', '--ignore-gitleaks-allow', '--gitleaks-ignore-path', `${policy}/gitleaksignore`, '--config', `${policy}/gitleaks.toml`, '--report-format=json', '--report-path=-', '--exit-code=10', '--timeout', String(timeout), target];
if (opts.gitHistory) plan.prerequisites.push('History input must be a sanitized Git object store with trusted config and no hooks, filters, alternates, or external helpers.');
plan.trustedFiles.push(
{ path: `${policy}/gitleaks.toml`, content: '[extend]\nuseDefault = true\n' },
{ path: `${policy}/gitleaksignore`, content: '' },
);
plan.args = [
opts.gitHistory ? 'git' : 'dir',
'--redact=100',
'--no-banner',
'--no-color',
'--ignore-gitleaks-allow',
'--gitleaks-ignore-path',
`${policy}/gitleaksignore`,
'--config',
`${policy}/gitleaks.toml`,
'--report-format=json',
'--report-path=-',
'--exit-code=10',
'--timeout',
String(timeout),
target,
];
if (opts.gitHistory)
plan.prerequisites.push(
'History input must be a sanitized Git object store with trusted config and no hooks, filters, alternates, or external helpers.',
);
else plan.coverage.exclusions.push('Historical revisions are not scanned by this directory pass.');
plan.requiredFeatures = ['dir', '--redact', '--ignore-gitleaks-allow'];
break;
}
case 'osv':
plan.format = 'osv-json'; plan.coverage.domain = 'dependencies';
plan.format = 'osv-json';
plan.coverage.domain = 'dependencies';
plan.trustedFiles.push({ path: `${policy}/osv-scanner.toml`, content: '' });
plan.args = ['scan', 'source', '--format=json', '--offline', '--no-call-analysis=all', '--config', `${policy}/osv-scanner.toml`, '--recursive', root];
plan.args = [
'scan',
'source',
'--format=json',
'--offline',
'--no-call-analysis=all',
'--config',
`${policy}/osv-scanner.toml`,
'--recursive',
root,
];
plan.requiredFeatures = ['scan source', '--offline', '--no-call-analysis'];
if (cache) plan.env.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY = cache;
else plan.prerequisites.push('Provide verified offline OSV databases for every assessed ecosystem.');
plan.coverage.exclusions.push('Call analysis is disabled; dependency reachability remains unknown until independently investigated.');
plan.coverage.exclusions.push(
'Call analysis is disabled; dependency reachability remains unknown until independently investigated.',
);
break;
case 'semgrep': {
plan.format = 'semgrep-json'; plan.coverage.domain = 'code';
const rules = opts.semgrepRules ? absolutePath(opts.semgrepRules, 'semgrepRules') : `${policy}/semgrep.yml`;
if (!rules.startsWith(`${policy}/`)) throw new Error('Semgrep rules must be below the trusted policyRoot');
if (!opts.semgrepRules) plan.prerequisites.push('Provide a reviewed, pinned local Semgrep ruleset; registry aliases and repo rules are not accepted.');
plan.args = ['scan', '--json', '--config', rules, '--metrics=off', '--disable-version-check', '--disable-nosem', '--no-git-ignore', '--no-secrets-validation', '--oss-only', '--no-autofix', '--timeout=10', '--timeout-threshold=3', '--jobs=1', root];
plan.env.SEMGREP_SEND_METRICS = 'off'; plan.env.SEMGREP_ENABLE_VERSION_CHECK = '0'; plan.env.SEMGREP_APP_TOKEN = '';
plan.requiredFeatures = ['scan', '--metrics', '--disable-version-check', '--no-secrets-validation', '--oss-only'];
plan.coverage.exclusions.push('Semgrep language support, built-in file selection, and .semgrepignore rules can exclude inputs; independently inspect these exclusions.');
plan.format = 'semgrep-json';
plan.coverage.domain = 'code';
const rules = opts.semgrepRules
? absolutePath(opts.semgrepRules, 'semgrepRules')
: `${policy}/semgrep.yml`;
if (!rules.startsWith(`${policy}/`))
throw new Error('Semgrep rules must be below the trusted policyRoot');
if (!opts.semgrepRules)
plan.prerequisites.push(
'Provide a reviewed, pinned local Semgrep ruleset; registry aliases and repo rules are not accepted.',
);
plan.args = [
'scan',
'--json',
'--config',
rules,
'--metrics=off',
'--disable-version-check',
'--disable-nosem',
'--no-git-ignore',
'--no-secrets-validation',
'--oss-only',
'--no-autofix',
'--timeout=10',
'--timeout-threshold=3',
'--jobs=1',
root,
];
plan.env.SEMGREP_SEND_METRICS = 'off';
plan.env.SEMGREP_ENABLE_VERSION_CHECK = '0';
plan.env.SEMGREP_APP_TOKEN = '';
plan.requiredFeatures = [
'scan',
'--metrics',
'--disable-version-check',
'--no-secrets-validation',
'--oss-only',
];
plan.coverage.exclusions.push(
'Semgrep language support, built-in file selection, and .semgrepignore rules can exclude inputs; independently inspect these exclusions.',
);
break;
}
case 'zizmor':
plan.coverage.domain = 'github-actions';
plan.args = ['--offline', '--no-config', '--no-ignores', '--no-exit-codes', '--no-progress', '--color=never', '--format=sarif', root];
plan.env.ZIZMOR_OFFLINE = '1'; plan.requiredFeatures = ['--offline', '--no-config', '--no-ignores'];
plan.coverage.exclusions.push('Online GitHub audits and remote reusable action inspection require separate assessment.');
plan.args = [
'--offline',
'--no-config',
'--no-ignores',
'--no-exit-codes',
'--no-progress',
'--color=never',
'--format=sarif',
root,
];
plan.env.ZIZMOR_OFFLINE = '1';
plan.requiredFeatures = ['--offline', '--no-config', '--no-ignores'];
plan.coverage.exclusions.push(
'Online GitHub audits and remote reusable action inspection require separate assessment.',
);
break;
case 'trivy':
plan.format = 'trivy-json'; plan.coverage.domain = 'dependencies-and-infrastructure';
plan.trustedFiles.push({ path: `${policy}/trivy.yaml`, content: '{}\n' }, { path: `${policy}/trivyignore`, content: '' });
plan.args = ['fs', '--format=json', '--config', `${policy}/trivy.yaml`, '--ignorefile', `${policy}/trivyignore`, '--scanners=vuln,misconfig,secret', '--cache-backend=memory', '--disable-telemetry', '--offline-scan', '--skip-db-update', '--skip-java-db-update', '--skip-check-update', '--skip-version-check', '--skip-vex-repo-update', '--timeout', `${timeout}s`, ...(cache ? ['--cache-dir', cache] : []), root];
plan.format = 'trivy-json';
plan.coverage.domain = 'dependencies-and-infrastructure';
plan.trustedFiles.push(
{ path: `${policy}/trivy.yaml`, content: '{}\n' },
{ path: `${policy}/trivyignore`, content: '' },
);
plan.args = [
'fs',
'--format=json',
'--config',
`${policy}/trivy.yaml`,
'--ignorefile',
`${policy}/trivyignore`,
'--scanners=vuln,misconfig,secret',
'--cache-backend=memory',
'--disable-telemetry',
'--offline-scan',
'--skip-db-update',
'--skip-java-db-update',
'--skip-check-update',
'--skip-version-check',
'--skip-vex-repo-update',
'--timeout',
`${timeout}s`,
...(cache ? ['--cache-dir', cache] : []),
root,
];
plan.env.TRIVY_DISABLE_TELEMETRY = 'true';
plan.requiredFeatures = ['--cache-backend', '--disable-telemetry', '--offline-scan', '--skip-db-update', '--skip-java-db-update', '--skip-check-update', '--skip-version-check', '--skip-vex-repo-update'];
if (!cache) plan.prerequisites.push('Provide verified offline Trivy vulnerability, Java, and misconfiguration databases as needed.');
plan.requiredFeatures = [
'--cache-backend',
'--disable-telemetry',
'--offline-scan',
'--skip-db-update',
'--skip-java-db-update',
'--skip-check-update',
'--skip-version-check',
'--skip-vex-repo-update',
];
if (!cache)
plan.prerequisites.push(
'Provide verified offline Trivy vulnerability, Java, and misconfiguration databases as needed.',
);
break;
case 'schemathesis': {
plan.format = 'schemathesis-json'; plan.network = 'loopback'; plan.coverage.domain = 'api-runtime';
plan.format = 'schemathesis-json';
plan.network = 'loopback';
plan.coverage.domain = 'api-runtime';
plan.outputPath = '/work/schemathesis.json';
// The upstream image enables a Python hook module and coverage plugin by
// default. Qualified CSO scans use only the reviewed schema/config.
plan.env.SCHEMATHESIS_HOOKS = ''; plan.env.SCHEMATHESIS_COVERAGE = 'false';
plan.env.SCHEMATHESIS_HOOKS = '';
plan.env.SCHEMATHESIS_COVERAGE = 'false';
plan.trustedFiles.push({ path: `${policy}/schemathesis.toml`, content: '' });
const schema = opts.schemaPath ? absolutePath(opts.schemaPath, 'schemaPath') : `${policy}/openapi.json`;
if (!schema.startsWith(`${policy}/`)) throw new Error('Schemathesis schema must be below trusted policyRoot');
if (!opts.schemaPath) plan.prerequisites.push('Provide a reviewed local schema with resolved local references, no remote references, and no hook imports.');
const schema = opts.schemaPath
? absolutePath(opts.schemaPath, 'schemaPath')
: `${policy}/openapi.json`;
if (!schema.startsWith(`${policy}/`))
throw new Error('Schemathesis schema must be below trusted policyRoot');
if (!opts.schemaPath)
plan.prerequisites.push(
'Provide a reviewed local schema with resolved local references, no remote references, and no hook imports.',
);
const base = opts.baseUrl ? validateScannerBaseUrl(opts.baseUrl) : 'http://127.0.0.1:3000/';
if (!opts.baseUrl) plan.prerequisites.push('Start the application and a legitimate control in the admitted loopback namespace.');
if (!opts.baseUrl)
plan.prerequisites.push(
'Start the application and a legitimate control in the admitted loopback namespace.',
);
const seed = positiveInteger(opts.seed ?? 1, 2_147_483_647, 'seed');
const examples = positiveInteger(opts.maxExamples ?? 20, 100, 'maxExamples');
const operations = opts.operationIds ?? [];
if (operations.length === 0 || operations.length > 20) plan.prerequisites.push('Declare between 1 and 20 reviewed operation IDs to bound the API assessment.');
if (operations.some(op => !op || op.length > 200 || /[\x00-\x1f]/.test(op))) throw new Error('Invalid Schemathesis operation ID');
plan.args = ['--config-file', `${policy}/schemathesis.toml`, '--no-color', 'run', schema, '--url', base, '--workers=1', '--phases=fuzzing', '--max-examples', String(examples), '--max-failures=10', '--max-time', String(timeout), '--seed', String(seed), '--request-timeout=5', '--request-retries=0', '--max-redirects=0', '--rate-limit=10/s', '--output-sanitize=true', '--generation-database=none', '--report-json-path', plan.outputPath, ...operations.flatMap(op => ['--include-operation-id', op])];
plan.requiredFeatures = ['--report-json-path', '--max-time', '--seed', '--max-redirects', '--include-operation-id'];
plan.coverage.scope = operations.map(op => `operation:${op}`);
plan.coverage.exclusions.push('Only declared operations and generated examples are exercised; API failures are candidates, not security proofs.');
if (operations.length === 0 || operations.length > 20)
plan.prerequisites.push(
'Declare between 1 and 20 reviewed operation IDs to bound the API assessment.',
);
if (operations.some((op) => !op || op.length > 200 || /[\x00-\x1f]/.test(op)))
throw new Error('Invalid Schemathesis operation ID');
plan.args = [
'--config-file',
`${policy}/schemathesis.toml`,
'--no-color',
'run',
schema,
'--url',
base,
'--workers=1',
'--phases=fuzzing',
'--max-examples',
String(examples),
'--max-failures=10',
'--max-time',
String(timeout),
'--seed',
String(seed),
'--request-timeout=5',
'--request-retries=0',
'--max-redirects=0',
'--rate-limit=10/s',
'--output-sanitize=true',
'--generation-database=none',
'--report-json-path',
plan.outputPath,
...operations.flatMap((op) => ['--include-operation-id', op]),
];
plan.requiredFeatures = [
'--report-json-path',
'--max-time',
'--seed',
'--max-redirects',
'--include-operation-id',
];
plan.coverage.scope = operations.map((op) => `operation:${op}`);
plan.coverage.exclusions.push(
'Only declared operations and generated examples are exercised; API failures are candidates, not security proofs.',
);
break;
}
}
const capabilities = opts.tools?.[id]?.capabilities;
if (capabilities) for (const required of plan.requiredFeatures) {
if (!capabilities.includes(required)) plan.prerequisites.push(`${plan.executableName} lacks required capability ${required}.`);
}
if (capabilities)
for (const required of plan.requiredFeatures) {
if (!capabilities.includes(required))
plan.prerequisites.push(`${plan.executableName} lacks required capability ${required}.`);
}
const version = opts.tools?.[id]?.version;
if (id === 'osv' && version && !/\b(?:v)?2\./.test(version)) plan.prerequisites.push('OSV-Scanner major version 2 is required.');
if (id === 'osv' && version && !/\b(?:v)?2\./.test(version))
plan.prerequisites.push('OSV-Scanner major version 2 is required.');
return plan;
});
}
@@ -258,7 +499,9 @@ function str(value: unknown): string {
if (typeof value !== 'string' || value.length > 16_384) throw new Error('Expected bounded string');
return value;
}
function optionalString(value: unknown): string | undefined { return value === undefined || value === null ? undefined : str(value); }
function optionalString(value: unknown): string | undefined {
return value === undefined || value === null ? undefined : str(value);
}
function integer(value: unknown): number | undefined {
if (value === undefined) return undefined;
if (!Number.isSafeInteger(value) || (value as number) < 1) throw new Error('Invalid source coordinate');
@@ -266,20 +509,32 @@ function integer(value: unknown): number | undefined {
}
function severity(value: unknown): ScannerCandidate['reportedSeverity'] {
const normalized = typeof value === 'string' ? value.toLowerCase() : '';
if (['critical', 'high', 'medium', 'low', 'info'].includes(normalized)) return normalized as ScannerCandidate['reportedSeverity'];
return ({ error: 'high', warning: 'medium', note: 'info', informational: 'info', unknown: 'unknown' } as const)[normalized] ?? 'unknown';
if (['critical', 'high', 'medium', 'low', 'info'].includes(normalized))
return normalized as ScannerCandidate['reportedSeverity'];
return (
({ error: 'high', warning: 'medium', note: 'info', informational: 'info', unknown: 'unknown' } as const)[
normalized
] ?? 'unknown'
);
}
/** No path is opened by this module. Normalization refuses URI/traversal escapes. */
export function scannerLocation(raw: string, sourceRoot: string): string {
let decoded: string;
try { decoded = decodeURIComponent(raw); } catch { throw new Error('Unsafe location'); }
if (/[\x00-\x1f\x7f]/.test(decoded) || /%[\da-f]{2}/i.test(decoded) || decoded.includes('\\')) throw new Error('Unsafe location');
try {
decoded = decodeURIComponent(raw);
} catch {
throw new Error('Unsafe location');
}
if (/[\x00-\x1f\x7f]/.test(decoded) || /%[\da-f]{2}/i.test(decoded) || decoded.includes('\\'))
throw new Error('Unsafe location');
if (decoded.startsWith('file:')) {
const url = new URL(decoded);
if (url.hostname || url.username || url.password || url.search || url.hash) throw new Error('Unsafe file URI');
if (url.hostname || url.username || url.password || url.search || url.hash)
throw new Error('Unsafe file URI');
decoded = decodeURIComponent(url.pathname);
} else if (/^[a-z][a-z\d+.-]*:/i.test(decoded) || decoded.startsWith('//')) throw new Error('Unsafe location');
} else if (/^[a-z][a-z\d+.-]*:/i.test(decoded) || decoded.startsWith('//'))
throw new Error('Unsafe location');
if (decoded.split('/').includes('..')) throw new Error('Unsafe location');
const root = absolutePath(sourceRoot, 'sourceRoot');
const absolute = decoded.startsWith('/') ? posix.normalize(decoded) : posix.join(root, decoded);
@@ -314,32 +569,66 @@ function decodedDocument(raw: string): unknown {
return document;
}
function candidate(tool: ScannerCandidate['tool'], fields: Omit<ScannerCandidate, 'id' | 'tool' | 'evidence' | 'trust' | 'suppressed'> & { suppressed?: boolean }): ScannerCandidate {
const identity = [tool, fields.ruleId, fields.location?.path ?? fields.operation ?? '', fields.location?.line ?? '', ...fields.advisoryIds.slice().sort()];
function candidate(
tool: ScannerCandidate['tool'],
fields: Omit<ScannerCandidate, 'id' | 'tool' | 'evidence' | 'trust' | 'suppressed'> & {
suppressed?: boolean;
},
): ScannerCandidate {
const identity = [
tool,
fields.ruleId,
fields.location?.path ?? fields.operation ?? '',
fields.location?.line ?? '',
...fields.advisoryIds.slice().sort(),
];
const id = createHash('sha256').update(JSON.stringify(identity)).digest('hex');
return { ...fields, id, tool, suppressed: fields.suppressed ?? false, evidence: 'scanner-candidate', trust: 'untrusted' };
return {
...fields,
id,
tool,
suppressed: fields.suppressed ?? false,
evidence: 'scanner-candidate',
trust: 'untrusted',
};
}
function location(path: unknown, line: unknown, column: unknown, root: string): ScannerCandidate['location'] {
return { path: scannerLocation(str(path), root), line: integer(line), column: integer(column) };
}
function parseSarif(document: unknown, tool: ScannerCandidate['tool'], root: string, add: (value: ScannerCandidate) => void, gap: (code: ScannerGap['code'], message: string) => void): void {
function parseSarif(
document: unknown,
tool: ScannerCandidate['tool'],
root: string,
add: (value: ScannerCandidate) => void,
gap: (code: ScannerGap['code'], message: string) => void,
): void {
const sarif = obj(document);
if (sarif.version !== '2.1.0') throw new Error('SARIF 2.1.0 required');
const runs = arr(sarif.runs);
if (!runs.length) { gap('SKIPPED_INPUT', 'SARIF contains no assessment runs.'); return; }
if (!runs.length) {
gap('SKIPPED_INPUT', 'SARIF contains no assessment runs.');
return;
}
for (const input of runs) {
const run = obj(input); const driver = obj(obj(run.tool).driver);
const run = obj(input);
const driver = obj(obj(run.tool).driver);
str(driver.name);
if (run.externalPropertyFileReferences !== undefined) {
const refs = obj(run.externalPropertyFileReferences);
if (refs.results !== undefined && arr(refs.results).length) gap('SKIPPED_INPUT', 'External SARIF result files were not fetched or assessed.');
if (refs.results !== undefined && arr(refs.results).length)
gap('SKIPPED_INPUT', 'External SARIF result files were not fetched or assessed.');
}
for (const invocation of run.invocations === undefined ? [] : arr(run.invocations)) {
const inv = obj(invocation);
if (inv.executionSuccessful === false) gap('TOOL_FAILED', 'SARIF records an unsuccessful tool invocation.');
if (Array.isArray(inv.toolExecutionNotifications) && inv.toolExecutionNotifications.some(n => obj(n).level === 'error')) gap('TOOL_FAILED', 'SARIF records tool execution errors.');
if (inv.executionSuccessful === false)
gap('TOOL_FAILED', 'SARIF records an unsuccessful tool invocation.');
if (
Array.isArray(inv.toolExecutionNotifications) &&
inv.toolExecutionNotifications.some((n) => obj(n).level === 'error')
)
gap('TOOL_FAILED', 'SARIF records tool execution errors.');
}
const rules = driver.rules === undefined ? [] : arr(driver.rules);
const results = arr(run.results);
@@ -349,7 +638,10 @@ function parseSarif(document: unknown, tool: ScannerCandidate['tool'], root: str
// SARIF also represents passing checks and informational inventory.
if (['pass', 'notApplicable', 'informational'].includes(String(result.kind))) continue;
const ruleIndex = result.ruleIndex;
const rule = Number.isSafeInteger(ruleIndex) && (ruleIndex as number) >= 0 && rules[ruleIndex as number] ? obj(rules[ruleIndex as number]) : undefined;
const rule =
Number.isSafeInteger(ruleIndex) && (ruleIndex as number) >= 0 && rules[ruleIndex as number]
? obj(rules[ruleIndex as number])
: undefined;
const ruleId = str(result.ruleId ?? rule?.id);
const message = obj(result.message);
let loc: ScannerCandidate['location'];
@@ -358,7 +650,8 @@ function parseSarif(document: unknown, tool: ScannerCandidate['tool'], root: str
let artifact = obj(physical.artifactLocation);
if (artifact.uri === undefined && Number.isSafeInteger(artifact.index)) {
const index = artifact.index as number;
if (index < 0 || !Array.isArray(run.artifacts) || !run.artifacts[index]) throw new Error('Invalid artifact index');
if (index < 0 || !Array.isArray(run.artifacts) || !run.artifacts[index])
throw new Error('Invalid artifact index');
artifact = obj(obj(run.artifacts[index]).location);
}
let uri = str(artifact.uri);
@@ -373,21 +666,58 @@ function parseSarif(document: unknown, tool: ScannerCandidate['tool'], root: str
loc = location(uri, region.startLine, region.startColumn, root);
}
const properties = result.properties === undefined ? {} : obj(result.properties);
const aliases = properties.tags === undefined ? [] : arr(properties.tags).filter(v => typeof v === 'string' && /^(CVE-|GHSA-|OSV-)/.test(v));
add(candidate(tool, { ruleId, message: str(message.text ?? message.markdown ?? message.id), location: loc, reportedSeverity: severity(result.level ?? (rule?.defaultConfiguration as Obj | undefined)?.level), advisoryIds: aliases as string[], suppressed: Array.isArray(result.suppressions) && result.suppressions.length > 0 }));
} catch (error) { gap(error instanceof Error && /[Ll]ocation|URI|source root/.test(error.message) ? 'UNSAFE_LOCATION' : 'INVALID_OUTPUT', 'A SARIF result could not be safely normalized.'); }
const aliases =
properties.tags === undefined
? []
: arr(properties.tags).filter((v) => typeof v === 'string' && /^(CVE-|GHSA-|OSV-)/.test(v));
add(
candidate(tool, {
ruleId,
message: str(message.text ?? message.markdown ?? message.id),
location: loc,
reportedSeverity: severity(
result.level ?? (rule?.defaultConfiguration as Obj | undefined)?.level,
),
advisoryIds: aliases as string[],
suppressed: Array.isArray(result.suppressions) && result.suppressions.length > 0,
}),
);
} catch (error) {
gap(
error instanceof Error && /[Ll]ocation|URI|source root/.test(error.message)
? 'UNSAFE_LOCATION'
: 'INVALID_OUTPUT',
'A SARIF result could not be safely normalized.',
);
}
}
}
}
function parseResults(plan: ScannerPlan, document: unknown, add: (value: ScannerCandidate) => void, gap: (code: ScannerGap['code'], message: string) => void): void {
function parseResults(
plan: ScannerPlan,
document: unknown,
add: (value: ScannerCandidate) => void,
gap: (code: ScannerGap['code'], message: string) => void,
): void {
const root = plan.sourceRoot;
if (plan.format === 'sarif') { parseSarif(document, plan.id, root, add, gap); return; }
if (plan.format === 'sarif') {
parseSarif(document, plan.id, root, add, gap);
return;
}
if (plan.format === 'gitleaks-json') {
for (const value of arr(document)) {
const row = obj(value);
// Never retain Match, Secret, Line, commit message, author, or scanner fingerprint.
add(candidate(plan.id, { ruleId: str(row.RuleID), message: str(row.Description), reportedSeverity: 'unknown', location: location(row.File, row.StartLine, row.StartColumn, root), advisoryIds: [] }));
add(
candidate(plan.id, {
ruleId: str(row.RuleID),
message: str(row.Description),
reportedSeverity: 'unknown',
location: location(row.File, row.StartLine, row.StartColumn, root),
advisoryIds: [],
}),
);
}
return;
}
@@ -395,38 +725,93 @@ function parseResults(plan: ScannerPlan, document: unknown, add: (value: Scanner
switch (plan.format) {
case 'semgrep-json':
for (const value of arr(doc.results)) {
const row = obj(value), extra = obj(row.extra), start = obj(row.start);
add(candidate(plan.id, { ruleId: str(row.check_id), message: str(extra.message), reportedSeverity: severity(extra.severity), location: location(row.path, start.line, start.col, root), advisoryIds: [], suppressed: extra.is_ignored === true }));
const row = obj(value),
extra = obj(row.extra),
start = obj(row.start);
add(
candidate(plan.id, {
ruleId: str(row.check_id),
message: str(extra.message),
reportedSeverity: severity(extra.severity),
location: location(row.path, start.line, start.col, root),
advisoryIds: [],
suppressed: extra.is_ignored === true,
}),
);
}
if (arr(doc.errors).length) gap('TOOL_FAILED', 'Semgrep reported parser, rule, or execution errors; inspect affected coverage.');
if (arr(doc.errors).length)
gap('TOOL_FAILED', 'Semgrep reported parser, rule, or execution errors; inspect affected coverage.');
if (!arr(obj(doc.paths).scanned).length) gap('SKIPPED_INPUT', 'Semgrep did not scan any source files.');
if (Array.isArray(obj(doc.paths).skipped) && (obj(doc.paths).skipped as unknown[]).length) gap('SKIPPED_INPUT', 'Semgrep skipped source files.');
if (Array.isArray(obj(doc.paths).skipped) && (obj(doc.paths).skipped as unknown[]).length)
gap('SKIPPED_INPUT', 'Semgrep skipped source files.');
return;
case 'osv-json':
for (const value of arr(doc.results)) {
const result = obj(value), source = obj(result.source);
const result = obj(value),
source = obj(result.source);
for (const entry of arr(result.packages)) {
const pkg = obj(entry), detail = obj(pkg.package);
const pkg = obj(entry),
detail = obj(pkg.package);
for (const input of arr(pkg.vulnerabilities)) {
const vuln = obj(input), id = str(vuln.id);
const vuln = obj(input),
id = str(vuln.id);
const aliases = vuln.aliases === undefined ? [] : arr(vuln.aliases).map(str);
add(candidate(plan.id, { ruleId: id, message: optionalString(vuln.summary) ?? id, reportedSeverity: 'unknown', location: location(source.path, undefined, undefined, root), advisoryIds: [...new Set([id, ...aliases])], dependency: { name: str(detail.name), version: optionalString(detail.version), ecosystem: optionalString(detail.ecosystem), reachability: 'unknown', exposure: 'unknown' } }));
add(
candidate(plan.id, {
ruleId: id,
message: optionalString(vuln.summary) ?? id,
reportedSeverity: 'unknown',
location: location(source.path, undefined, undefined, root),
advisoryIds: [...new Set([id, ...aliases])],
dependency: {
name: str(detail.name),
version: optionalString(detail.version),
ecosystem: optionalString(detail.ecosystem),
reachability: 'unknown',
exposure: 'unknown',
},
}),
);
}
}
}
return;
case 'trivy-json':
if (doc.SchemaVersion !== 2) throw new Error('Trivy schema version 2 required');
if (doc.Results === undefined && (typeof doc.ArtifactName !== 'string' || doc.ArtifactType !== 'filesystem')) throw new Error('Missing Trivy assessment metadata');
if (
doc.Results === undefined &&
(typeof doc.ArtifactName !== 'string' || doc.ArtifactType !== 'filesystem')
)
throw new Error('Missing Trivy assessment metadata');
for (const value of arr(doc.Results ?? [])) {
const result = obj(value);
for (const key of ['Vulnerabilities', 'Misconfigurations', 'Secrets'] as const) {
for (const input of result[key] === undefined ? [] : arr(result[key])) {
const row = obj(input), id = str(row.VulnerabilityID ?? row.ID ?? row.RuleID);
const row = obj(input),
id = str(row.VulnerabilityID ?? row.ID ?? row.RuleID);
const cause = row.CauseMetadata === undefined ? {} : obj(row.CauseMetadata);
// Some filesystem package scanners add " (type)" after their target.
const target = str(result.Target).replace(/ \([a-zA-Z0-9_. -]+\)$/, '');
add(candidate(plan.id, { ruleId: id, message: optionalString(row.Title) ?? optionalString(row.Description) ?? id, reportedSeverity: severity(row.Severity), location: location(target, cause.StartLine ?? row.StartLine, undefined, root), advisoryIds: row.VulnerabilityID ? [id] : [], ...(key === 'Vulnerabilities' ? { dependency: { name: str(row.PkgName), version: optionalString(row.InstalledVersion), ecosystem: optionalString(result.Type), reachability: 'unknown' as const, exposure: 'unknown' as const } } : {}) }));
add(
candidate(plan.id, {
ruleId: id,
message: optionalString(row.Title) ?? optionalString(row.Description) ?? id,
reportedSeverity: severity(row.Severity),
location: location(target, cause.StartLine ?? row.StartLine, undefined, root),
advisoryIds: row.VulnerabilityID ? [id] : [],
...(key === 'Vulnerabilities'
? {
dependency: {
name: str(row.PkgName),
version: optionalString(row.InstalledVersion),
ecosystem: optionalString(result.Type),
reachability: 'unknown' as const,
exposure: 'unknown' as const,
},
}
: {}),
}),
);
}
}
}
@@ -434,13 +819,31 @@ function parseResults(plan: ScannerPlan, document: unknown, add: (value: Scanner
case 'schemathesis-json': {
str(doc.schemathesis_version);
const operations = doc.operations === null ? null : obj(doc.operations);
if (doc.complete !== true || doc.stop_reason !== 'completed') gap('SKIPPED_INPUT', 'Schemathesis did not finish its declared operation assessment.');
if (!operations || typeof operations.tested !== 'number' || operations.tested === 0) gap('SKIPPED_INPUT', 'Schemathesis exercised no operations.');
if (operations && (Number(operations.errored) > 0 || Number(operations.skipped) > 0 || Number(operations.tested) < Number(operations.selected))) gap('SKIPPED_INPUT', 'Schemathesis skipped or failed to exercise selected operations.');
if (arr(doc.errors).length) gap('TOOL_FAILED', 'Schemathesis reported setup or test-generation errors.');
if (doc.complete !== true || doc.stop_reason !== 'completed')
gap('SKIPPED_INPUT', 'Schemathesis did not finish its declared operation assessment.');
if (!operations || typeof operations.tested !== 'number' || operations.tested === 0)
gap('SKIPPED_INPUT', 'Schemathesis exercised no operations.');
if (
operations &&
(Number(operations.errored) > 0 ||
Number(operations.skipped) > 0 ||
Number(operations.tested) < Number(operations.selected))
)
gap('SKIPPED_INPUT', 'Schemathesis skipped or failed to exercise selected operations.');
if (arr(doc.errors).length)
gap('TOOL_FAILED', 'Schemathesis reported setup or test-generation errors.');
for (const value of arr(doc.failures)) {
const row = obj(value);
for (const op of arr(row.operations)) add(candidate(plan.id, { ruleId: str(row.type), message: str(row.title), reportedSeverity: severity(row.severity), advisoryIds: [], operation: str(op) }));
for (const op of arr(row.operations))
add(
candidate(plan.id, {
ruleId: str(row.type),
message: str(row.title),
reportedSeverity: severity(row.severity),
advisoryIds: [],
operation: str(op),
}),
);
}
return;
}
@@ -450,16 +853,38 @@ function parseResults(plan: ScannerPlan, document: unknown, add: (value: Scanner
/** Failed or malformed tools never become an empty-clean assessment. */
export function parseScannerOutput(plan: ScannerPlan, execution: ScannerExecution): ScannerOutcome {
const outcome: ScannerOutcome = {
tool: plan.id, version: null, status: 'not_assessed', candidates: [], gaps: [], scope: plan.coverage.scope.slice(), exclusions: plan.coverage.exclusions.slice(),
databaseUpdatedAt: null, exitCode: execution.exitCode, evidence: 'scanner-candidate', provenanceSources: plan.provenanceSources.slice(),
planSha256: createHash('sha256').update(JSON.stringify(plan)).digest('hex'), documentationInspectedAt: plan.documentationInspectedAt,
tool: plan.id,
version: null,
status: 'not_assessed',
candidates: [],
gaps: [],
scope: plan.coverage.scope.slice(),
exclusions: plan.coverage.exclusions.slice(),
databaseUpdatedAt: null,
exitCode: execution.exitCode,
evidence: 'scanner-candidate',
provenanceSources: plan.provenanceSources.slice(),
planSha256: createHash('sha256').update(JSON.stringify(plan)).digest('hex'),
documentationInspectedAt: plan.documentationInspectedAt,
};
const gap = (code: ScannerGap['code'], message: string) => { if (!outcome.gaps.some(g => g.code === code && g.message === message)) outcome.gaps.push({ code, message }); };
if (execution.unavailable) { gap('UNAVAILABLE', `${plan.id} was unavailable; this scanner assessment did not run.`); return outcome; }
if (plan.prerequisites.length) { for (const value of plan.prerequisites) gap('PREREQUISITE', value); return outcome; }
const gap = (code: ScannerGap['code'], message: string) => {
if (!outcome.gaps.some((g) => g.code === code && g.message === message))
outcome.gaps.push({ code, message });
};
if (execution.unavailable) {
gap('UNAVAILABLE', `${plan.id} was unavailable; this scanner assessment did not run.`);
return outcome;
}
if (plan.prerequisites.length) {
for (const value of plan.prerequisites) gap('PREREQUISITE', value);
return outcome;
}
if (execution.timedOut) gap('TIMEOUT', 'Scanner exceeded its execution deadline.');
const outputBytes = Buffer.byteLength(execution.stdout) + Buffer.byteLength(execution.stderr ?? '');
if (execution.truncated || outputBytes > Math.min(plan.maxOutputBytes, MAX_SCANNER_OUTPUT_BYTES)) { gap('OUTPUT_LIMIT', 'Scanner output exceeded the capture limit; payload withheld.'); return outcome; }
if (execution.truncated || outputBytes > Math.min(plan.maxOutputBytes, MAX_SCANNER_OUTPUT_BYTES)) {
gap('OUTPUT_LIMIT', 'Scanner output exceeded the capture limit; payload withheld.');
return outcome;
}
try {
if (execution.version) {
const safe = redactFindingSpans(execution.version);
@@ -467,31 +892,61 @@ export function parseScannerOutput(plan: ScannerPlan, execution: ScannerExecutio
outcome.version = safe.slice(0, 200).replace(/[\x00-\x1f\x7f]/g, '');
}
if (redactFindingSpans(execution.stderr ?? '') === null) throw new RedactionFailure();
if (/\b(?:error|fatal|panic|failed to|unable to|no offline version)\b/i.test(execution.stderr ?? '')) gap('TOOL_FAILED', 'Scanner diagnostic output reported a failure; the JSON result does not establish complete coverage.');
if (/\b(?:error|fatal|panic|failed to|unable to|no offline version)\b/i.test(execution.stderr ?? ''))
gap(
'TOOL_FAILED',
'Scanner diagnostic output reported a failure; the JSON result does not establish complete coverage.',
);
const doc = decodedDocument(execution.stdout);
const seen = new Set<string>();
parseResults(plan, doc, item => {
if (outcome.candidates.length >= MAX_CANDIDATES) throw new Error('Candidate limit exceeded');
if (!seen.has(item.id)) { seen.add(item.id); outcome.candidates.push(item); }
}, gap);
parseResults(
plan,
doc,
(item) => {
if (outcome.candidates.length >= MAX_CANDIDATES) throw new Error('Candidate limit exceeded');
if (!seen.has(item.id)) {
seen.add(item.id);
outcome.candidates.push(item);
}
},
gap,
);
outcome.status = 'complete';
} catch (error) {
if (error instanceof RedactionFailure) { outcome.candidates = []; gap('REDACTION_FAILED', 'Scanner payload could not be safely redacted and was withheld.'); }
else gap('INVALID_OUTPUT', 'Scanner report is malformed, unsupported, or exceeds structural limits.');
if (error instanceof RedactionFailure) {
outcome.candidates = [];
gap('REDACTION_FAILED', 'Scanner payload could not be safely redacted and was withheld.');
} else gap('INVALID_OUTPUT', 'Scanner report is malformed, unsupported, or exceeds structural limits.');
}
const successCodes = plan.id === 'gitleaks' ? [0, 10] : ['osv', 'schemathesis'].includes(plan.id) ? [0, 1] : [0];
if (execution.exitCode === null || !successCodes.includes(execution.exitCode)) gap('TOOL_FAILED', 'Scanner did not exit with a recognized assessment status.');
if ((plan.id === 'gitleaks' && execution.exitCode === 10 || plan.id === 'osv' && execution.exitCode === 1) && !outcome.candidates.length) gap('INVALID_OUTPUT', 'Scanner finding exit status disagrees with its empty report.');
const successCodes =
plan.id === 'gitleaks' ? [0, 10] : ['osv', 'schemathesis'].includes(plan.id) ? [0, 1] : [0];
if (execution.exitCode === null || !successCodes.includes(execution.exitCode))
gap('TOOL_FAILED', 'Scanner did not exit with a recognized assessment status.');
if (
((plan.id === 'gitleaks' && execution.exitCode === 10) ||
(plan.id === 'osv' && execution.exitCode === 1)) &&
!outcome.candidates.length
)
gap('INVALID_OUTPUT', 'Scanner finding exit status disagrees with its empty report.');
if (['osv', 'trivy'].includes(plan.id)) {
if (execution.databaseUpdatedAt && /^\d{4}-\d\d-\d\dT/.test(execution.databaseUpdatedAt) && Number.isFinite(Date.parse(execution.databaseUpdatedAt))) outcome.databaseUpdatedAt = execution.databaseUpdatedAt;
if (
execution.databaseUpdatedAt &&
/^\d{4}-\d\d-\d\dT/.test(execution.databaseUpdatedAt) &&
Number.isFinite(Date.parse(execution.databaseUpdatedAt))
)
outcome.databaseUpdatedAt = execution.databaseUpdatedAt;
else gap('UNKNOWN_FRESHNESS', 'The advisory database freshness is unknown.');
}
if (outcome.gaps.length) outcome.status = outcome.status === 'complete' || outcome.candidates.length ? 'partial' : 'not_assessed';
if (outcome.gaps.length)
outcome.status = outcome.status === 'complete' || outcome.candidates.length ? 'partial' : 'not_assessed';
return outcome;
}
/** Import CodeQL or other SARIF as read-only candidates; never trust its verdict. */
export function importSarif(raw: string, opts: { sourceRoot: string; version?: string; scope?: string[] }): ScannerOutcome {
export function importSarif(
raw: string,
opts: { sourceRoot: string; version?: string; scope?: string[] },
): ScannerOutcome {
const root = absolutePath(opts.sourceRoot, 'sourceRoot');
const plan = scannerPlans({ snapshotRoot: root, offline: true, selected: ['zizmor'] })[0];
plan.coverage.scope = opts.scope ?? [root];
@@ -499,6 +954,6 @@ export function importSarif(raw: string, opts: { sourceRoot: string; version?: s
plan.coverage.exclusions = ['Imported scanner scope and suppressions require independent validation.'];
const outcome = parseScannerOutput(plan, { stdout: raw, exitCode: 0, version: opts.version });
outcome.tool = 'sarif';
outcome.candidates = outcome.candidates.map(item => candidate('sarif', item));
outcome.candidates = outcome.candidates.map((item) => candidate('sarif', item));
return outcome;
}
+902 -192
View File
File diff suppressed because it is too large. Load diff
+2338 -655
View File
File diff suppressed because it is too large. Load diff
+2117 -301
View File
File diff suppressed because it is too large. Load diff
+153 -24
View File
@@ -3,30 +3,159 @@ import * as fs from 'node:fs';
import { connect } from 'node:net';
import { HttpAssertion, VerificationObservation, object } from './contracts';
interface Config { phase:'before'|'after'; port:number; legitimate:HttpAssertion[]; security:HttpAssertion }
function matches(status:number,body:string,oracle:HttpAssertion['expected']):boolean{return status===oracle.status&&(oracle.includes===undefined||body.includes(oracle.includes))&&(oracle.excludes===undefined||!body.includes(oracle.excludes));}
export async function boundedResponseBody(response:Response,limit=65536):Promise<string>{
if(!Number.isSafeInteger(limit)||limit<1)throw new Error('invalid response limit');
const declared=response.headers.get('content-length');
if(declared!==null&&(/^\d+$/.test(declared)?Number(declared)>limit:true)){await response.body?.cancel();throw new Error('response too large');}
if(!response.body)return'';
const reader=response.body.getReader(),chunks:Uint8Array[]=[];let total=0;
try{
for(;;){const next=await reader.read();if(next.done)break;if(!next.value)continue;total+=next.value.byteLength;if(total>limit){await reader.cancel();throw new Error('response too large');}chunks.push(next.value);}
}finally{reader.releaseLock();}
const bytes=new Uint8Array(total);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.byteLength;}return new TextDecoder().decode(bytes);
interface Config {
phase: 'before' | 'after';
port: number;
legitimate: HttpAssertion[];
security: HttpAssertion;
}
async function request(a:HttpAssertion,port:number):Promise<{status:number;body:string}>{
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),5000);
try{const response=await fetch(`http://127.0.0.1:${port}${a.path}`,{method:a.method,headers:a.headers,body:['GET'].includes(a.method)?undefined:a.body,redirect:'manual',signal:controller.signal});return{status:response.status,body:await boundedResponseBody(response)};}finally{clearTimeout(timer);}
function matches(status: number, body: string, oracle: HttpAssertion['expected']): boolean {
return (
status === oracle.status &&
(oracle.includes === undefined || body.includes(oracle.includes)) &&
(oracle.excludes === undefined || !body.includes(oracle.excludes))
);
}
async function ready(port:number):Promise<boolean>{return await new Promise(resolve=>{const socket=connect({host:'127.0.0.1',port}),done=(value:boolean)=>{socket.removeAllListeners();socket.destroy();resolve(value);},timer=setTimeout(()=>done(false),500);socket.once('connect',()=>{clearTimeout(timer);done(true);});socket.once('error',()=>{clearTimeout(timer);done(false);});});}
async function main(){
const file=process.argv[2];if(!file||!file.startsWith('/policy/'))throw new Error('trusted policy path required');const raw=fs.readFileSync(file,'utf8');if(Buffer.byteLength(raw)>1024*1024)throw new Error('policy too large');const v=object(JSON.parse(raw),'verifier policy') as any;
if(!['before','after'].includes(v.phase)||!Number.isInteger(v.port)||v.port<1024||v.port>65535||!Array.isArray(v.legitimate)||!v.security)throw new Error('invalid verifier policy');const config=v as Config;
let booted=false;for(let attempt=0;attempt<60;attempt++){if(await ready(config.port)){booted=true;break;}await Bun.sleep(250);}
let legitimate=false,security:VerificationObservation['security']='inconclusive',summary='application did not answer a legitimate control';
if(booted){try{legitimate=(await Promise.all(config.legitimate.map(async a=>{const r=await request(a,config.port);return matches(r.status,r.body,a.expected);}))).every(Boolean);const r=await request(config.security,config.port),fixed=matches(r.status,r.body,config.security.expected),vulnerable=matches(r.status,r.body,config.security.vulnerable!);security=config.phase==='before'?(vulnerable&&!fixed?'intended_failure':fixed&&!vulnerable?'pass':'inconclusive'):(fixed&&!vulnerable?'pass':'inconclusive');summary=`boot=true legitimate=${legitimate} security=${security}`;}catch{summary='bounded verifier request failed';}}
process.stdout.write(JSON.stringify({booted,legitimate,security,existingTests:false,output:summary,inputHash:''})+'\n');
export async function boundedResponseBody(response: Response, limit = 65536): Promise<string> {
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('invalid response limit');
const declared = response.headers.get('content-length');
if (declared !== null && (/^\d+$/.test(declared) ? Number(declared) > limit : true)) {
await response.body?.cancel();
throw new Error('response too large');
}
if (!response.body) return '';
const reader = response.body.getReader(),
chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const next = await reader.read();
if (next.done) break;
if (!next.value) continue;
total += next.value.byteLength;
if (total > limit) {
await reader.cancel();
throw new Error('response too large');
}
chunks.push(next.value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
if(import.meta.main)main().catch(()=>{process.stdout.write(JSON.stringify({booted:false,legitimate:false,security:'inconclusive',existingTests:false,output:'verifier setup failed',inputHash:''})+'\n');process.exitCode=1;});
async function request(a: HttpAssertion, port: number): Promise<{ status: number; body: string }> {
const controller = new AbortController(),
timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`http://127.0.0.1:${port}${a.path}`, {
method: a.method,
headers: a.headers,
body: ['GET'].includes(a.method) ? undefined : a.body,
redirect: 'manual',
signal: controller.signal,
});
return { status: response.status, body: await boundedResponseBody(response) };
} finally {
clearTimeout(timer);
}
}
async function ready(port: number): Promise<boolean> {
return await new Promise((resolve) => {
const socket = connect({ host: '127.0.0.1', port }),
done = (value: boolean) => {
socket.removeAllListeners();
socket.destroy();
resolve(value);
},
timer = setTimeout(() => done(false), 500);
socket.once('connect', () => {
clearTimeout(timer);
done(true);
});
socket.once('error', () => {
clearTimeout(timer);
done(false);
});
});
}
async function main() {
const file = process.argv[2];
if (!file || !file.startsWith('/policy/')) throw new Error('trusted policy path required');
const raw = fs.readFileSync(file, 'utf8');
if (Buffer.byteLength(raw) > 1024 * 1024) throw new Error('policy too large');
const v = object(JSON.parse(raw), 'verifier policy') as any;
if (
!['before', 'after'].includes(v.phase) ||
!Number.isInteger(v.port) ||
v.port < 1024 ||
v.port > 65535 ||
!Array.isArray(v.legitimate) ||
!v.security
)
throw new Error('invalid verifier policy');
const config = v as Config;
let booted = false;
for (let attempt = 0; attempt < 60; attempt++) {
if (await ready(config.port)) {
booted = true;
break;
}
await Bun.sleep(250);
}
let legitimate = false,
security: VerificationObservation['security'] = 'inconclusive',
summary = 'application did not answer a legitimate control';
if (booted) {
try {
legitimate = (
await Promise.all(
config.legitimate.map(async (a) => {
const r = await request(a, config.port);
return matches(r.status, r.body, a.expected);
}),
)
).every(Boolean);
const r = await request(config.security, config.port),
fixed = matches(r.status, r.body, config.security.expected),
vulnerable = matches(r.status, r.body, config.security.vulnerable!);
security =
config.phase === 'before'
? vulnerable && !fixed
? 'intended_failure'
: fixed && !vulnerable
? 'pass'
: 'inconclusive'
: fixed && !vulnerable
? 'pass'
: 'inconclusive';
summary = `boot=true legitimate=${legitimate} security=${security}`;
} catch {
summary = 'bounded verifier request failed';
}
}
process.stdout.write(
JSON.stringify({ booted, legitimate, security, existingTests: false, output: summary, inputHash: '' }) +
'\n',
);
}
if (import.meta.main)
main().catch(() => {
process.stdout.write(
JSON.stringify({
booted: false,
legitimate: false,
security: 'inconclusive',
existingTests: false,
output: 'verifier setup failed',
inputHash: '',
}) + '\n',
);
process.exitCode = 1;
});
+680 -103
View File
@@ -1,136 +1,713 @@
import { generateKeyPairSync, createPrivateKey, createPublicKey, randomBytes, sign, verify } from 'node:crypto';
import {
generateKeyPairSync,
createPrivateKey,
createPublicKey,
randomBytes,
sign,
verify,
} from 'node:crypto';
import { lstatSync, realpathSync } from 'node:fs';
import { basename, dirname } from 'node:path';
import {
AssertionWitnessBinding, AssertionWitnessReceipt, Command, CsoError, MAX_OUTPUT,
VerificationObservation, canonical, object, oneOf, sha256, string, validateCommand,
AssertionWitnessBinding,
AssertionWitnessReceipt,
Command,
CsoError,
MAX_OUTPUT,
VerificationObservation,
canonical,
object,
oneOf,
sha256,
string,
validateCommand,
validateVerificationObservation,
} from './contracts';
import { runProcess } from './process';
export interface WitnessTestExecution { command:Command; code:number; output:string; minimumPassingTests:number }
export interface WitnessedVerificationResult { observation:VerificationObservation; witness:AssertionWitnessReceipt }
export interface WitnessTestExecution {
command: Command;
code: number;
output: string;
minimumPassingTests: number;
}
export interface WitnessedVerificationResult {
observation: VerificationObservation;
witness: AssertionWitnessReceipt;
}
export interface AssertionWitnessHandle {
readonly binding:AssertionWitnessBinding;
attest(observation:VerificationObservation,executions:WitnessTestExecution[]):Promise<AssertionWitnessReceipt>;
validate(receipt:unknown,observation:VerificationObservation,now?:number):AssertionWitnessReceipt;
readonly binding: AssertionWitnessBinding;
attest(
observation: VerificationObservation,
executions: WitnessTestExecution[],
): Promise<AssertionWitnessReceipt>;
validate(receipt: unknown, observation: VerificationObservation, now?: number): AssertionWitnessReceipt;
}
const HASH=/^[a-f0-9]{64}$/;
const PUBLIC_KEY=/^[a-f0-9]{88}$/;
const SIGNATURE=/^[a-f0-9]{128}$/;
const PROTOCOL='gstack-cso-assertion-witness-v1' as const;
const MAX_RECEIPT_AGE=300_000;
const exact=(value:Record<string,any>,allowed:readonly string[],name:string)=>{for(const key of Object.keys(value))if(!allowed.includes(key))throw new CsoError('INVALID_SCHEMA',`Unexpected ${name} field: ${key}`);};
const hash=(value:unknown,name:string):string=>{if(typeof value!=='string'||!HASH.test(value))throw new CsoError('INVALID_SCHEMA',`${name} must be a sha256 hash`);return value;};
const timestamp=(value:unknown,name:string):string=>{const result=string(value,name,64),ms=Date.parse(result);if(!Number.isFinite(ms)||new Date(ms).toISOString()!==result)throw new CsoError('INVALID_SCHEMA',`${name} must be a canonical UTC timestamp`);return result;};
const HASH = /^[a-f0-9]{64}$/;
const PUBLIC_KEY = /^[a-f0-9]{88}$/;
const SIGNATURE = /^[a-f0-9]{128}$/;
const PROTOCOL = 'gstack-cso-assertion-witness-v1' as const;
const MAX_RECEIPT_AGE = 300_000;
const exact = (value: Record<string, any>, allowed: readonly string[], name: string) => {
for (const key of Object.keys(value))
if (!allowed.includes(key)) throw new CsoError('INVALID_SCHEMA', `Unexpected ${name} field: ${key}`);
};
const hash = (value: unknown, name: string): string => {
if (typeof value !== 'string' || !HASH.test(value))
throw new CsoError('INVALID_SCHEMA', `${name} must be a sha256 hash`);
return value;
};
const timestamp = (value: unknown, name: string): string => {
const result = string(value, name, 64),
ms = Date.parse(result);
if (!Number.isFinite(ms) || new Date(ms).toISOString() !== result)
throw new CsoError('INVALID_SCHEMA', `${name} must be a canonical UTC timestamp`);
return result;
};
export function validateAssertionWitnessBinding(value:unknown):AssertionWitnessBinding{
const v=object(value,'assertion witness binding'),runtime=object(v.runtime,'assertion witness runtime'),runner=object(v.runner,'assertion witness runner');
exact(v,['schemaVersion','protocol','nonce','phase','issuedAt','expiresAt','runId','findingId','policyHash','auditPolicyHash','runtime','runner','sourceHash','dependencyHash','configurationHash','requestHash','patchHash','harnessHash','assertionHash','fixturesHash'],'assertion witness binding');
exact(runtime,['image','verifierImage','platform','profile'],'assertion witness runtime');exact(runner,['testToolchain','startPlanHash','testPlanHash','commandsHash','minimumPassingTestsHash'],'assertion witness runner');
if(v.schemaVersion!==1||v.protocol!==PROTOCOL)throw new CsoError('INVALID_SCHEMA','Unsupported assertion witness protocol');
const issuedAt=timestamp(v.issuedAt,'assertion witness issuedAt'),expiresAt=timestamp(v.expiresAt,'assertion witness expiresAt'),duration=Date.parse(expiresAt)-Date.parse(issuedAt);
if(duration<=0||duration>MAX_RECEIPT_AGE)throw new CsoError('INVALID_SCHEMA','Assertion witness lifetime exceeds the bounded attempt policy');
if(typeof v.nonce!=='string'||!HASH.test(v.nonce))throw new CsoError('INVALID_SCHEMA','Assertion witness nonce must be 32 random bytes');
const findingId=string(v.findingId,'assertion witness findingId',64);if(!/^[a-f0-9]{32}$/.test(findingId))throw new CsoError('INVALID_SCHEMA','Assertion witness findingId is invalid');
return{schemaVersion:1,protocol:PROTOCOL,nonce:v.nonce,phase:oneOf(v.phase,['before','after'],'assertion witness phase'),issuedAt,expiresAt,runId:string(v.runId,'assertion witness runId',200),findingId,policyHash:hash(v.policyHash,'assertion witness policyHash'),auditPolicyHash:hash(v.auditPolicyHash,'assertion witness auditPolicyHash'),runtime:{image:string(runtime.image,'assertion witness runtime image',500),verifierImage:string(runtime.verifierImage,'assertion witness verifier image',500),platform:string(runtime.platform,'assertion witness runtime platform',100),profile:string(runtime.profile,'assertion witness runtime profile',100)},runner:{testToolchain:oneOf(runner.testToolchain,['runtime','project'],'assertion witness test toolchain'),startPlanHash:hash(runner.startPlanHash,'assertion witness start plan'),testPlanHash:hash(runner.testPlanHash,'assertion witness test plan'),commandsHash:hash(runner.commandsHash,'assertion witness commands'),minimumPassingTestsHash:hash(runner.minimumPassingTestsHash,'assertion witness execution floors')},sourceHash:hash(v.sourceHash,'assertion witness sourceHash'),dependencyHash:hash(v.dependencyHash,'assertion witness dependencyHash'),configurationHash:hash(v.configurationHash,'assertion witness configurationHash'),requestHash:hash(v.requestHash,'assertion witness requestHash'),patchHash:hash(v.patchHash,'assertion witness patchHash'),harnessHash:hash(v.harnessHash,'assertion witness harnessHash'),assertionHash:hash(v.assertionHash,'assertion witness assertionHash'),fixturesHash:hash(v.fixturesHash,'assertion witness fixturesHash')};
export function validateAssertionWitnessBinding(value: unknown): AssertionWitnessBinding {
const v = object(value, 'assertion witness binding'),
runtime = object(v.runtime, 'assertion witness runtime'),
runner = object(v.runner, 'assertion witness runner');
exact(
v,
[
'schemaVersion',
'protocol',
'nonce',
'phase',
'issuedAt',
'expiresAt',
'runId',
'findingId',
'policyHash',
'auditPolicyHash',
'runtime',
'runner',
'sourceHash',
'dependencyHash',
'configurationHash',
'requestHash',
'patchHash',
'harnessHash',
'assertionHash',
'fixturesHash',
],
'assertion witness binding',
);
exact(runtime, ['image', 'verifierImage', 'platform', 'profile'], 'assertion witness runtime');
exact(
runner,
['testToolchain', 'startPlanHash', 'testPlanHash', 'commandsHash', 'minimumPassingTestsHash'],
'assertion witness runner',
);
if (v.schemaVersion !== 1 || v.protocol !== PROTOCOL)
throw new CsoError('INVALID_SCHEMA', 'Unsupported assertion witness protocol');
const issuedAt = timestamp(v.issuedAt, 'assertion witness issuedAt'),
expiresAt = timestamp(v.expiresAt, 'assertion witness expiresAt'),
duration = Date.parse(expiresAt) - Date.parse(issuedAt);
if (duration <= 0 || duration > MAX_RECEIPT_AGE)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness lifetime exceeds the bounded attempt policy');
if (typeof v.nonce !== 'string' || !HASH.test(v.nonce))
throw new CsoError('INVALID_SCHEMA', 'Assertion witness nonce must be 32 random bytes');
const findingId = string(v.findingId, 'assertion witness findingId', 64);
if (!/^[a-f0-9]{32}$/.test(findingId))
throw new CsoError('INVALID_SCHEMA', 'Assertion witness findingId is invalid');
return {
schemaVersion: 1,
protocol: PROTOCOL,
nonce: v.nonce,
phase: oneOf(v.phase, ['before', 'after'], 'assertion witness phase'),
issuedAt,
expiresAt,
runId: string(v.runId, 'assertion witness runId', 200),
findingId,
policyHash: hash(v.policyHash, 'assertion witness policyHash'),
auditPolicyHash: hash(v.auditPolicyHash, 'assertion witness auditPolicyHash'),
runtime: {
image: string(runtime.image, 'assertion witness runtime image', 500),
verifierImage: string(runtime.verifierImage, 'assertion witness verifier image', 500),
platform: string(runtime.platform, 'assertion witness runtime platform', 100),
profile: string(runtime.profile, 'assertion witness runtime profile', 100),
},
runner: {
testToolchain: oneOf(runner.testToolchain, ['runtime', 'project'], 'assertion witness test toolchain'),
startPlanHash: hash(runner.startPlanHash, 'assertion witness start plan'),
testPlanHash: hash(runner.testPlanHash, 'assertion witness test plan'),
commandsHash: hash(runner.commandsHash, 'assertion witness commands'),
minimumPassingTestsHash: hash(runner.minimumPassingTestsHash, 'assertion witness execution floors'),
},
sourceHash: hash(v.sourceHash, 'assertion witness sourceHash'),
dependencyHash: hash(v.dependencyHash, 'assertion witness dependencyHash'),
configurationHash: hash(v.configurationHash, 'assertion witness configurationHash'),
requestHash: hash(v.requestHash, 'assertion witness requestHash'),
patchHash: hash(v.patchHash, 'assertion witness patchHash'),
harnessHash: hash(v.harnessHash, 'assertion witness harnessHash'),
assertionHash: hash(v.assertionHash, 'assertion witness assertionHash'),
fixturesHash: hash(v.fixturesHash, 'assertion witness fixturesHash'),
};
}
function receiptUnsigned(receipt:AssertionWitnessReceipt):Omit<AssertionWitnessReceipt,'signature'>{const {signature:_,...unsigned}=receipt;return unsigned;}
function observationForReceipt(observation:VerificationObservation,binding:AssertionWitnessBinding,diagnosticTestsPassed:boolean):VerificationObservation{
const checked=validateVerificationObservation(observation);
return{...checked,existingTests:diagnosticTestsPassed,inputHash:binding.harnessHash};
function receiptUnsigned(receipt: AssertionWitnessReceipt): Omit<AssertionWitnessReceipt, 'signature'> {
const { signature: _, ...unsigned } = receipt;
return unsigned;
}
export function witnessObservationHash(observation:VerificationObservation):string{return sha256(canonical(validateVerificationObservation(observation)));}
export function validateStoredAssertionWitnessReceipt(value:unknown):AssertionWitnessReceipt{
const v=object(value,'assertion witness receipt'),binding=validateAssertionWitnessBinding(v.binding);
exact(v,['schemaVersion','binding','keyId','publicKey','observationHash','externalAssertionsPassed','diagnosticTestsPassed','executions','signature'],'assertion witness receipt');
if(v.schemaVersion!==1||typeof v.keyId!=='string'||!HASH.test(v.keyId)||typeof v.publicKey!=='string'||!PUBLIC_KEY.test(v.publicKey)||typeof v.signature!=='string'||!SIGNATURE.test(v.signature))throw new CsoError('INVALID_SCHEMA','Assertion witness cryptographic metadata is invalid');
if(sha256(Buffer.from(v.publicKey,'hex'))!==v.keyId)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness key identity does not match its public key');
if(typeof v.observationHash!=='string'||!HASH.test(v.observationHash)||typeof v.externalAssertionsPassed!=='boolean'||typeof v.diagnosticTestsPassed!=='boolean'||!Array.isArray(v.executions)||!v.executions.length||v.executions.length>100)throw new CsoError('INVALID_SCHEMA','Assertion witness outcomes are malformed');
const executions=v.executions.map((raw:any,index:number)=>{const item=object(raw,`assertion witness execution ${index}`);exact(item,['commandHash','exitCode','outputHash','minimumPassingTests','executedTests','passingTests','reportedPassed'],`assertion witness execution ${index}`);if(!Number.isSafeInteger(item.exitCode)||item.exitCode<-1||item.exitCode>255||!Number.isSafeInteger(item.minimumPassingTests)||item.minimumPassingTests<1||!Number.isSafeInteger(item.executedTests)||item.executedTests<0||!Number.isSafeInteger(item.passingTests)||item.passingTests<0||item.passingTests>item.executedTests||typeof item.reportedPassed!=='boolean'||(item.reportedPassed&&item.passingTests<item.minimumPassingTests))throw new CsoError('INVALID_SCHEMA','Assertion witness execution outcome is malformed');return{commandHash:hash(item.commandHash,'assertion witness commandHash'),exitCode:item.exitCode,outputHash:hash(item.outputHash,'assertion witness outputHash'),minimumPassingTests:item.minimumPassingTests,executedTests:item.executedTests,passingTests:item.passingTests,reportedPassed:item.reportedPassed};});
if(v.diagnosticTestsPassed!==executions.every(item=>item.reportedPassed))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness diagnostic summary does not match its executions');
const receipt:AssertionWitnessReceipt={schemaVersion:1,binding,keyId:v.keyId,publicKey:v.publicKey,observationHash:v.observationHash,externalAssertionsPassed:v.externalAssertionsPassed,diagnosticTestsPassed:v.diagnosticTestsPassed,executions,signature:v.signature};
let valid=false;try{valid=verify(null,Buffer.from(canonical(receiptUnsigned(receipt))),createPublicKey({key:Buffer.from(receipt.publicKey,'hex'),format:'der',type:'spki'}),Buffer.from(receipt.signature,'hex'));}catch{}
if(!valid)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness signature is invalid');return receipt;
function observationForReceipt(
observation: VerificationObservation,
binding: AssertionWitnessBinding,
diagnosticTestsPassed: boolean,
): VerificationObservation {
const checked = validateVerificationObservation(observation);
return { ...checked, existingTests: diagnosticTestsPassed, inputHash: binding.harnessHash };
}
export function witnessObservationHash(observation: VerificationObservation): string {
return sha256(canonical(validateVerificationObservation(observation)));
}
export function validateAssertionWitnessReceipt(value:unknown,expected:AssertionWitnessBinding,expectedPublicKey:string,observation:VerificationObservation,now=Date.now()):AssertionWitnessReceipt{
const receipt=validateStoredAssertionWitnessReceipt(value),binding=validateAssertionWitnessBinding(expected);
if(canonical(receipt.binding)!==canonical(binding)||receipt.publicKey!==expectedPublicKey)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt does not bind this verification challenge');
if(now<Date.parse(binding.issuedAt)||now>Date.parse(binding.expiresAt))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt is stale');
const normalized=observationForReceipt(observation,binding,receipt.diagnosticTestsPassed),external=normalized.booted&&normalized.legitimate&&normalized.security!=='inconclusive';
if(receipt.observationHash!==witnessObservationHash(normalized)||receipt.externalAssertionsPassed!==external)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness receipt does not bind the external verifier observation');
export function validateStoredAssertionWitnessReceipt(value: unknown): AssertionWitnessReceipt {
const v = object(value, 'assertion witness receipt'),
binding = validateAssertionWitnessBinding(v.binding);
exact(
v,
[
'schemaVersion',
'binding',
'keyId',
'publicKey',
'observationHash',
'externalAssertionsPassed',
'diagnosticTestsPassed',
'executions',
'signature',
],
'assertion witness receipt',
);
if (
v.schemaVersion !== 1 ||
typeof v.keyId !== 'string' ||
!HASH.test(v.keyId) ||
typeof v.publicKey !== 'string' ||
!PUBLIC_KEY.test(v.publicKey) ||
typeof v.signature !== 'string' ||
!SIGNATURE.test(v.signature)
)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness cryptographic metadata is invalid');
if (sha256(Buffer.from(v.publicKey, 'hex')) !== v.keyId)
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness key identity does not match its public key');
if (
typeof v.observationHash !== 'string' ||
!HASH.test(v.observationHash) ||
typeof v.externalAssertionsPassed !== 'boolean' ||
typeof v.diagnosticTestsPassed !== 'boolean' ||
!Array.isArray(v.executions) ||
!v.executions.length ||
v.executions.length > 100
)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness outcomes are malformed');
const executions = v.executions.map((raw: any, index: number) => {
const item = object(raw, `assertion witness execution ${index}`);
exact(
item,
[
'commandHash',
'exitCode',
'outputHash',
'minimumPassingTests',
'executedTests',
'passingTests',
'reportedPassed',
],
`assertion witness execution ${index}`,
);
if (
!Number.isSafeInteger(item.exitCode) ||
item.exitCode < -1 ||
item.exitCode > 255 ||
!Number.isSafeInteger(item.minimumPassingTests) ||
item.minimumPassingTests < 1 ||
!Number.isSafeInteger(item.executedTests) ||
item.executedTests < 0 ||
!Number.isSafeInteger(item.passingTests) ||
item.passingTests < 0 ||
item.passingTests > item.executedTests ||
typeof item.reportedPassed !== 'boolean' ||
(item.reportedPassed && item.passingTests < item.minimumPassingTests)
)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness execution outcome is malformed');
return {
commandHash: hash(item.commandHash, 'assertion witness commandHash'),
exitCode: item.exitCode,
outputHash: hash(item.outputHash, 'assertion witness outputHash'),
minimumPassingTests: item.minimumPassingTests,
executedTests: item.executedTests,
passingTests: item.passingTests,
reportedPassed: item.reportedPassed,
};
});
if (v.diagnosticTestsPassed !== executions.every((item) => item.reportedPassed))
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Assertion witness diagnostic summary does not match its executions',
);
const receipt: AssertionWitnessReceipt = {
schemaVersion: 1,
binding,
keyId: v.keyId,
publicKey: v.publicKey,
observationHash: v.observationHash,
externalAssertionsPassed: v.externalAssertionsPassed,
diagnosticTestsPassed: v.diagnosticTestsPassed,
executions,
signature: v.signature,
};
let valid = false;
try {
valid = verify(
null,
Buffer.from(canonical(receiptUnsigned(receipt))),
createPublicKey({ key: Buffer.from(receipt.publicKey, 'hex'), format: 'der', type: 'spki' }),
Buffer.from(receipt.signature, 'hex'),
);
} catch {}
if (!valid) throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness signature is invalid');
return receipt;
}
export function assertionWitnessSemanticValue(receipt:AssertionWitnessReceipt):unknown{
const checked=validateStoredAssertionWitnessReceipt(receipt),{nonce:_,issuedAt:__,expiresAt:___,...stable}=checked.binding;
return{binding:stable,observationHash:checked.observationHash,externalAssertionsPassed:checked.externalAssertionsPassed,diagnosticTestsPassed:checked.diagnosticTestsPassed,executions:checked.executions};
}
export function assertionWitnessPairHash(pair:{before:AssertionWitnessReceipt;after:AssertionWitnessReceipt}):string{
return sha256(canonical({before:assertionWitnessSemanticValue(pair.before),after:assertionWitnessSemanticValue(pair.after)}));
export function validateAssertionWitnessReceipt(
value: unknown,
expected: AssertionWitnessBinding,
expectedPublicKey: string,
observation: VerificationObservation,
now = Date.now(),
): AssertionWitnessReceipt {
const receipt = validateStoredAssertionWitnessReceipt(value),
binding = validateAssertionWitnessBinding(expected);
if (canonical(receipt.binding) !== canonical(binding) || receipt.publicKey !== expectedPublicKey)
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Assertion witness receipt does not bind this verification challenge',
);
if (now < Date.parse(binding.issuedAt) || now > Date.parse(binding.expiresAt))
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness receipt is stale');
const normalized = observationForReceipt(observation, binding, receipt.diagnosticTestsPassed),
external = normalized.booted && normalized.legitimate && normalized.security !== 'inconclusive';
if (
receipt.observationHash !== witnessObservationHash(normalized) ||
receipt.externalAssertionsPassed !== external
)
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Assertion witness receipt does not bind the external verifier observation',
);
return receipt;
}
function witnessReplayValue(receipt:AssertionWitnessReceipt):unknown{
const checked=validateStoredAssertionWitnessReceipt(receipt),{nonce:_,issuedAt:__,expiresAt:___,...binding}=checked.binding;
return{binding,externalAssertionsPassed:checked.externalAssertionsPassed,diagnosticTestsPassed:checked.diagnosticTestsPassed,
executions:checked.executions.map(({outputHash:_,...execution})=>execution)};
export function assertionWitnessSemanticValue(receipt: AssertionWitnessReceipt): unknown {
const checked = validateStoredAssertionWitnessReceipt(receipt),
{ nonce: _, issuedAt: __, expiresAt: ___, ...stable } = checked.binding;
return {
binding: stable,
observationHash: checked.observationHash,
externalAssertionsPassed: checked.externalAssertionsPassed,
diagnosticTestsPassed: checked.diagnosticTestsPassed,
executions: checked.executions,
};
}
export function assertionWitnessReplayHash(pair:{before:AssertionWitnessReceipt;after:AssertionWitnessReceipt}):string{
return sha256(canonical({before:witnessReplayValue(pair.before),after:witnessReplayValue(pair.after)}));
export function assertionWitnessPairHash(pair: {
before: AssertionWitnessReceipt;
after: AssertionWitnessReceipt;
}): string {
return sha256(
canonical({
before: assertionWitnessSemanticValue(pair.before),
after: assertionWitnessSemanticValue(pair.after),
}),
);
}
interface TestExecutionSummary {executedTests:number;passingTests:number;reportedPassed:boolean}
function testExecutionSummary(command:Command,code:number,output:string,minimumPassingTests=1):TestExecutionSummary{
const failed={executedTests:0,passingTests:0,reportedPassed:false};
if(!Number.isInteger(minimumPassingTests)||minimumPassingTests<1||code!==0||!output||output.includes('[sensitive process output redacted]'))return failed;
const clean=output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g,''),args=command.args,name=basename(command.executable),json=()=>{const end=clean.lastIndexOf('}');if(end<0)return undefined;for(let start=clean.lastIndexOf('{',end);start>=0;start=clean.lastIndexOf('{',start-1)){try{const value=JSON.parse(clean.slice(start,end+1));if(value&&typeof value==='object')return value;}catch{}}};
let executedTests=0,passingTests=0,valid=false;
if(name==='node'&&args.includes('--test')&&args.includes('--test-reporter=tap')){const paths=args.filter(arg=>arg.startsWith('./')).map(arg=>arg.slice(2)),registered=[...clean.matchAll(/^# Subtest:\s+(.+?)\s*$/gm)].map(match=>match[1]),isPathWrapper=(label:string)=>paths.some(path=>label===path||label.endsWith(`/${path}`));executedTests=Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);passingTests=Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);valid=registered.some(label=>!isPathWrapper(label))&&!registered.some(isPathWrapper)&&executedTests>=passingTests&&/^# fail\s+0\s*$/m.test(clean)&&/^# cancelled\s+0\s*$/m.test(clean);}
else if(name==='bun'&&args.includes('test')){passingTests=Number(clean.match(/^\s*(\d+)\s+pass(?:es)?\s*$/mi)?.[1]);executedTests=Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);valid=executedTests>=passingTests&&/^\s*0\s+fail(?:ures?)?\s*$/mi.test(clean);}
else if(name==='jest'&&args.includes('--json')){const value=json();passingTests=Number(value?.numPassedTests);executedTests=Number(value?.numTotalTests);valid=value?.success===true&&value?.numFailedTests===0&&value?.numRuntimeErrorTestSuites===0&&executedTests>=passingTests;}
else if(name==='vitest'&&args.includes('--reporter=verbose')){const match=clean.match(/^\s*Tests\s+.*?(\d+)\s+passed.*?\((\d+)\)\s*$/mi);passingTests=Number(match?.[1]);executedTests=Number(match?.[2]);valid=executedTests>=passingTests&&!/\b\d+\s+failed\b/i.test(match?.[0]??'');}
else if(name==='mocha'&&args.includes('json')){const stats=json()?.stats;passingTests=Number(stats?.passes);executedTests=Number(stats?.tests);valid=stats?.failures===0&&Number.isSafeInteger(stats?.pending)&&executedTests===passingTests+stats.pending;}
else if(name==='ava'&&args.includes('--tap')){executedTests=Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);passingTests=Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);valid=executedTests>=passingTests&&/^# fail\s+0\s*$/m.test(clean);}
else if(name==='python'&&args.some(arg=>arg.includes('import pytest;')&&arg.includes('pytest.main'))){passingTests=Number(clean.match(/(?:^|\s)(\d+)\s+passed\b/i)?.[1]);const skipped=Number(clean.match(/(?:^|\s)(\d+)\s+skipped\b/i)?.[1]??0);executedTests=passingTests+skipped;valid=true;}
else if(name==='python'&&args.some(arg=>arg.includes('import os,sys,unittest;')&&arg.includes('unittest.main'))){executedTests=Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);const skipped=Number(clean.match(/\bskipped=(\d+)\b/i)?.[1]??0);passingTests=executedTests-skipped;valid=Number.isSafeInteger(skipped);}
else if(name==='bundle'&&args[0]==='exec'&&args[1]==='rspec'&&args.includes('json')){const summary=json()?.summary,pending=Number(summary?.pending_count??0);executedTests=Number(summary?.example_count);passingTests=executedTests-pending;valid=Number.isSafeInteger(pending)&&summary?.failure_count===0&&(summary?.errors_outside_of_examples_count??0)===0;}
else if(name==='bundle'&&args[0]==='exec'&&args[1]==='rails'&&args[2]==='test'&&args.includes('--no-color')){const match=clean.match(/\b(\d+)\s+runs?\s*,\s*(\d+)\s+assertions?\s*,\s*0\s+failures?\s*,\s*0\s+errors?\s*,\s*(\d+)\s+skips?\b/i),skipped=Number(match?.[3]);executedTests=Number(match?.[1]);passingTests=executedTests-skipped;valid=Number.isSafeInteger(skipped);}
const countsValid=Number.isSafeInteger(executedTests)&&executedTests>=0&&Number.isSafeInteger(passingTests)&&passingTests>=0&&executedTests>=passingTests;
return countsValid?{executedTests,passingTests,reportedPassed:valid&&passingTests>=minimumPassingTests}:failed;
function witnessReplayValue(receipt: AssertionWitnessReceipt): unknown {
const checked = validateStoredAssertionWitnessReceipt(receipt),
{ nonce: _, issuedAt: __, expiresAt: ___, ...binding } = checked.binding;
return {
binding,
externalAssertionsPassed: checked.externalAssertionsPassed,
diagnosticTestsPassed: checked.diagnosticTestsPassed,
executions: checked.executions.map(({ outputHash: _, ...execution }) => execution),
};
}
export function testExecutionPassed(command:Command,code:number,output:string,minimumPassingTests=1):boolean{
return testExecutionSummary(command,code,output,minimumPassingTests).reportedPassed;
export function assertionWitnessReplayHash(pair: {
before: AssertionWitnessReceipt;
after: AssertionWitnessReceipt;
}): string {
return sha256(
canonical({ before: witnessReplayValue(pair.before), after: witnessReplayValue(pair.after) }),
);
}
interface ChildRequest {privateKey:string;publicKey:string;binding:AssertionWitnessBinding;observation:VerificationObservation;executions:WitnessTestExecution[]}
async function readChildInput():Promise<string>{const chunks:Buffer[]=[];let bytes=0;for await(const value of process.stdin){const chunk=Buffer.from(value);bytes+=chunk.length;if(bytes>2*MAX_OUTPUT)throw new CsoError('INVALID_SCHEMA','Assertion witness request exceeds the bounded input limit');chunks.push(chunk);}return Buffer.concat(chunks).toString('utf8');}
function createReceipt(input:unknown):AssertionWitnessReceipt{
const v=object(input,'assertion witness child request');exact(v,['privateKey','publicKey','binding','observation','executions'],'assertion witness child request');const binding=validateAssertionWitnessBinding(v.binding);
if(Date.now()<Date.parse(binding.issuedAt)||Date.now()>Date.parse(binding.expiresAt))throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness challenge is stale');
if(typeof v.privateKey!=='string'||v.privateKey.length>4096||typeof v.publicKey!=='string'||!PUBLIC_KEY.test(v.publicKey))throw new CsoError('INVALID_SCHEMA','Assertion witness signing input is invalid');
let privateKey;try{privateKey=createPrivateKey(v.privateKey);const derived=createPublicKey(privateKey).export({format:'der',type:'spki'}).toString('hex');if(derived!==v.publicKey)throw new Error();}catch{throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness signing authority does not match the challenge');}
const rawObservation=validateVerificationObservation(v.observation);if(!Array.isArray(v.executions)||!v.executions.length||v.executions.length>100)throw new CsoError('INVALID_SCHEMA','Assertion witness needs one or more canonical test executions');
let outputBytes=0;const rawExecutions:WitnessTestExecution[]=v.executions.map((raw:any,index:number)=>{const item=object(raw,`witness execution ${index}`);exact(item,['command','code','output','minimumPassingTests'],`witness execution ${index}`);const command=validateCommand(item.command,`witness execution ${index}.command`);if(!Number.isSafeInteger(item.code)||item.code<-1||item.code>255||typeof item.output!=='string'||item.output.includes('\0')||!Number.isSafeInteger(item.minimumPassingTests)||item.minimumPassingTests<1)throw new CsoError('INVALID_SCHEMA','Assertion witness test execution is malformed');outputBytes+=Buffer.byteLength(item.output);if(outputBytes>MAX_OUTPUT)throw new CsoError('INVALID_SCHEMA','Assertion witness test output exceeds the group capture limit');return{command,code:item.code,output:item.output,minimumPassingTests:item.minimumPassingTests};});
if(sha256(canonical(rawExecutions.map(item=>item.command)))!==binding.runner.commandsHash||sha256(canonical(rawExecutions.map(item=>item.minimumPassingTests)))!==binding.runner.minimumPassingTestsHash)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness executions do not match the helper-derived runner');
const executions=rawExecutions.map(item=>{const summary=testExecutionSummary(item.command,item.code,item.output,item.minimumPassingTests);return{commandHash:sha256(canonical(item.command)),exitCode:item.code,outputHash:sha256(item.output),minimumPassingTests:item.minimumPassingTests,...summary};}),diagnosticTestsPassed=executions.every(item=>item.reportedPassed),observation=observationForReceipt(rawObservation,binding,diagnosticTestsPassed),externalAssertionsPassed=observation.booted&&observation.legitimate&&observation.security!=='inconclusive';
const unsigned:Omit<AssertionWitnessReceipt,'signature'>={schemaVersion:1,binding,keyId:sha256(Buffer.from(v.publicKey,'hex')),publicKey:v.publicKey,observationHash:witnessObservationHash(observation),externalAssertionsPassed,diagnosticTestsPassed,executions};
return{...unsigned,signature:sign(null,Buffer.from(canonical(unsigned)),privateKey).toString('hex')};
interface TestExecutionSummary {
executedTests: number;
passingTests: number;
reportedPassed: boolean;
}
function testExecutionSummary(
command: Command,
code: number,
output: string,
minimumPassingTests = 1,
): TestExecutionSummary {
const failed = { executedTests: 0, passingTests: 0, reportedPassed: false };
if (
!Number.isInteger(minimumPassingTests) ||
minimumPassingTests < 1 ||
code !== 0 ||
!output ||
output.includes('[sensitive process output redacted]')
)
return failed;
const clean = output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, ''),
args = command.args,
name = basename(command.executable),
json = () => {
const end = clean.lastIndexOf('}');
if (end < 0) return undefined;
for (let start = clean.lastIndexOf('{', end); start >= 0; start = clean.lastIndexOf('{', start - 1)) {
try {
const value = JSON.parse(clean.slice(start, end + 1));
if (value && typeof value === 'object') return value;
} catch {}
}
};
let executedTests = 0,
passingTests = 0,
valid = false;
if (name === 'node' && args.includes('--test') && args.includes('--test-reporter=tap')) {
const paths = args.filter((arg) => arg.startsWith('./')).map((arg) => arg.slice(2)),
registered = [...clean.matchAll(/^# Subtest:\s+(.+?)\s*$/gm)].map((match) => match[1]),
isPathWrapper = (label: string) => paths.some((path) => label === path || label.endsWith(`/${path}`));
executedTests = Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);
passingTests = Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);
valid =
registered.some((label) => !isPathWrapper(label)) &&
!registered.some(isPathWrapper) &&
executedTests >= passingTests &&
/^# fail\s+0\s*$/m.test(clean) &&
/^# cancelled\s+0\s*$/m.test(clean);
} else if (name === 'bun' && args.includes('test')) {
passingTests = Number(clean.match(/^\s*(\d+)\s+pass(?:es)?\s*$/im)?.[1]);
executedTests = Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);
valid = executedTests >= passingTests && /^\s*0\s+fail(?:ures?)?\s*$/im.test(clean);
} else if (name === 'jest' && args.includes('--json')) {
const value = json();
passingTests = Number(value?.numPassedTests);
executedTests = Number(value?.numTotalTests);
valid =
value?.success === true &&
value?.numFailedTests === 0 &&
value?.numRuntimeErrorTestSuites === 0 &&
executedTests >= passingTests;
} else if (name === 'vitest' && args.includes('--reporter=verbose')) {
const match = clean.match(/^\s*Tests\s+.*?(\d+)\s+passed.*?\((\d+)\)\s*$/im);
passingTests = Number(match?.[1]);
executedTests = Number(match?.[2]);
valid = executedTests >= passingTests && !/\b\d+\s+failed\b/i.test(match?.[0] ?? '');
} else if (name === 'mocha' && args.includes('json')) {
const stats = json()?.stats;
passingTests = Number(stats?.passes);
executedTests = Number(stats?.tests);
valid =
stats?.failures === 0 &&
Number.isSafeInteger(stats?.pending) &&
executedTests === passingTests + stats.pending;
} else if (name === 'ava' && args.includes('--tap')) {
executedTests = Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);
passingTests = Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);
valid = executedTests >= passingTests && /^# fail\s+0\s*$/m.test(clean);
} else if (
name === 'python' &&
args.some((arg) => arg.includes('import pytest;') && arg.includes('pytest.main'))
) {
passingTests = Number(clean.match(/(?:^|\s)(\d+)\s+passed\b/i)?.[1]);
const skipped = Number(clean.match(/(?:^|\s)(\d+)\s+skipped\b/i)?.[1] ?? 0);
executedTests = passingTests + skipped;
valid = true;
} else if (
name === 'python' &&
args.some((arg) => arg.includes('import os,sys,unittest;') && arg.includes('unittest.main'))
) {
executedTests = Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);
const skipped = Number(clean.match(/\bskipped=(\d+)\b/i)?.[1] ?? 0);
passingTests = executedTests - skipped;
valid = Number.isSafeInteger(skipped);
} else if (name === 'bundle' && args[0] === 'exec' && args[1] === 'rspec' && args.includes('json')) {
const summary = json()?.summary,
pending = Number(summary?.pending_count ?? 0);
executedTests = Number(summary?.example_count);
passingTests = executedTests - pending;
valid =
Number.isSafeInteger(pending) &&
summary?.failure_count === 0 &&
(summary?.errors_outside_of_examples_count ?? 0) === 0;
} else if (
name === 'bundle' &&
args[0] === 'exec' &&
args[1] === 'rails' &&
args[2] === 'test' &&
args.includes('--no-color')
) {
const match = clean.match(
/\b(\d+)\s+runs?\s*,\s*(\d+)\s+assertions?\s*,\s*0\s+failures?\s*,\s*0\s+errors?\s*,\s*(\d+)\s+skips?\b/i,
),
skipped = Number(match?.[3]);
executedTests = Number(match?.[1]);
passingTests = executedTests - skipped;
valid = Number.isSafeInteger(skipped);
}
const countsValid =
Number.isSafeInteger(executedTests) &&
executedTests >= 0 &&
Number.isSafeInteger(passingTests) &&
passingTests >= 0 &&
executedTests >= passingTests;
return countsValid
? { executedTests, passingTests, reportedPassed: valid && passingTests >= minimumPassingTests }
: failed;
}
export function testExecutionPassed(
command: Command,
code: number,
output: string,
minimumPassingTests = 1,
): boolean {
return testExecutionSummary(command, code, output, minimumPassingTests).reportedPassed;
}
export async function runAssertionWitnessChild():Promise<void>{const receipt=createReceipt(JSON.parse(await readChildInput()));process.stdout.write(JSON.stringify(receipt)+'\n');}
interface ChildRequest {
privateKey: string;
publicKey: string;
binding: AssertionWitnessBinding;
observation: VerificationObservation;
executions: WitnessTestExecution[];
}
async function readChildInput(): Promise<string> {
const chunks: Buffer[] = [];
let bytes = 0;
for await (const value of process.stdin) {
const chunk = Buffer.from(value);
bytes += chunk.length;
if (bytes > 2 * MAX_OUTPUT)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness request exceeds the bounded input limit');
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('utf8');
}
function createReceipt(input: unknown): AssertionWitnessReceipt {
const v = object(input, 'assertion witness child request');
exact(
v,
['privateKey', 'publicKey', 'binding', 'observation', 'executions'],
'assertion witness child request',
);
const binding = validateAssertionWitnessBinding(v.binding);
if (Date.now() < Date.parse(binding.issuedAt) || Date.now() > Date.parse(binding.expiresAt))
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness challenge is stale');
if (
typeof v.privateKey !== 'string' ||
v.privateKey.length > 4096 ||
typeof v.publicKey !== 'string' ||
!PUBLIC_KEY.test(v.publicKey)
)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness signing input is invalid');
let privateKey;
try {
privateKey = createPrivateKey(v.privateKey);
const derived = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).toString('hex');
if (derived !== v.publicKey) throw new Error();
} catch {
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Assertion witness signing authority does not match the challenge',
);
}
const rawObservation = validateVerificationObservation(v.observation);
if (!Array.isArray(v.executions) || !v.executions.length || v.executions.length > 100)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness needs one or more canonical test executions');
let outputBytes = 0;
const rawExecutions: WitnessTestExecution[] = v.executions.map((raw: any, index: number) => {
const item = object(raw, `witness execution ${index}`);
exact(item, ['command', 'code', 'output', 'minimumPassingTests'], `witness execution ${index}`);
const command = validateCommand(item.command, `witness execution ${index}.command`);
if (
!Number.isSafeInteger(item.code) ||
item.code < -1 ||
item.code > 255 ||
typeof item.output !== 'string' ||
item.output.includes('\0') ||
!Number.isSafeInteger(item.minimumPassingTests) ||
item.minimumPassingTests < 1
)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness test execution is malformed');
outputBytes += Buffer.byteLength(item.output);
if (outputBytes > MAX_OUTPUT)
throw new CsoError('INVALID_SCHEMA', 'Assertion witness test output exceeds the group capture limit');
return { command, code: item.code, output: item.output, minimumPassingTests: item.minimumPassingTests };
});
if (
sha256(canonical(rawExecutions.map((item) => item.command))) !== binding.runner.commandsHash ||
sha256(canonical(rawExecutions.map((item) => item.minimumPassingTests))) !==
binding.runner.minimumPassingTestsHash
)
throw new CsoError(
'INCOMPATIBLE_INPUT',
'Assertion witness executions do not match the helper-derived runner',
);
const executions = rawExecutions.map((item) => {
const summary = testExecutionSummary(item.command, item.code, item.output, item.minimumPassingTests);
return {
commandHash: sha256(canonical(item.command)),
exitCode: item.code,
outputHash: sha256(item.output),
minimumPassingTests: item.minimumPassingTests,
...summary,
};
}),
diagnosticTestsPassed = executions.every((item) => item.reportedPassed),
observation = observationForReceipt(rawObservation, binding, diagnosticTestsPassed),
externalAssertionsPassed =
observation.booted && observation.legitimate && observation.security !== 'inconclusive';
const unsigned: Omit<AssertionWitnessReceipt, 'signature'> = {
schemaVersion: 1,
binding,
keyId: sha256(Buffer.from(v.publicKey, 'hex')),
publicKey: v.publicKey,
observationHash: witnessObservationHash(observation),
externalAssertionsPassed,
diagnosticTestsPassed,
executions,
};
return { ...unsigned, signature: sign(null, Buffer.from(canonical(unsigned)), privateKey).toString('hex') };
}
export class AssertionWitnessSession{
private privateKey:string;readonly publicKey:string;readonly keyId:string;private nonces=new Set<string>();
constructor(private workDirectory:string,private deadline:number){const stat=lstatSync(workDirectory),real=realpathSync(workDirectory),resolved=lstatSync(real);if(!stat.isDirectory()||stat.isSymbolicLink()||!resolved.isDirectory()||resolved.isSymbolicLink()||stat.dev!==resolved.dev||stat.ino!==resolved.ino||(process.getuid&&resolved.uid!==process.getuid())||(resolved.mode&0o022)!==0)throw new CsoError('UNSAFE_PATH','Assertion witness working directory must be private and owned');this.workDirectory=real;const pair=generateKeyPairSync('ed25519');this.privateKey=pair.privateKey.export({format:'pem',type:'pkcs8'}).toString();this.publicKey=pair.publicKey.export({format:'der',type:'spki'}).toString('hex');this.keyId=sha256(Buffer.from(this.publicKey,'hex'));}
handle(stable:Omit<AssertionWitnessBinding,'schemaVersion'|'protocol'|'nonce'|'issuedAt'|'expiresAt'>):AssertionWitnessHandle{
const now=Date.now(),expires=Math.min(this.deadline,now+MAX_RECEIPT_AGE);if(expires<=now)throw new CsoError('DEADLINE','No time remains for an authenticated assertion witness');let nonce='';do{nonce=randomBytes(32).toString('hex');}while(this.nonces.has(nonce));this.nonces.add(nonce);
const binding=validateAssertionWitnessBinding({schemaVersion:1,protocol:PROTOCOL,nonce,issuedAt:new Date(now).toISOString(),expiresAt:new Date(expires).toISOString(),...stable});let consumed=false;
return{binding,attest:async(observation,executions)=>{if(consumed)throw new CsoError('INCOMPATIBLE_INPUT','Assertion witness challenge was already consumed');consumed=true;const input=JSON.stringify({privateKey:this.privateKey,publicKey:this.publicKey,binding,observation,executions} satisfies ChildRequest);if(Buffer.byteLength(input)>2*MAX_OUTPUT)throw new CsoError('REDACTION_FAILED','Assertion witness input exceeds the bounded helper channel');const bun=/^bun(?:\.exe)?$/i.test(basename(process.execPath)),file=bun?process.execPath:join(dirname(process.execPath),process.platform==='win32'?'gstack-cso-launcher.exe':'gstack-cso-launcher'),args=bun?[import.meta.path,'--child']:['__cso-assertion-witness'],env=process.platform==='win32'?{PATH:dirname(process.execPath),SYSTEMROOT:process.env.SYSTEMROOT??'C:\\Windows',WINDIR:process.env.WINDIR??'C:\\Windows'}:{PATH:'/usr/bin:/bin',LANG:'C.UTF-8',LC_ALL:'C.UTF-8',TZ:'UTC'},result=await runProcess(file,args,{cwd:this.workDirectory,env,timeoutMs:Math.max(1,expires-Date.now()),maxBytes:128*1024,input,raw:true});if(result.timedOut)throw new CsoError('DEADLINE','Assertion witness exceeded the verification deadline');if(result.truncated||result.code!==0)throw new CsoError('TOOL_FAILED','Authenticated assertion witness did not return a bounded receipt');let receipt:unknown;try{receipt=JSON.parse(result.stdout);}catch{throw new CsoError('TOOL_FAILED','Authenticated assertion witness returned invalid output');}return validateAssertionWitnessReceipt(receipt,binding,this.publicKey,observation);},validate:(receipt,observation,current=Date.now())=>validateAssertionWitnessReceipt(receipt,binding,this.publicKey,observation,current)};
export async function runAssertionWitnessChild(): Promise<void> {
const receipt = createReceipt(JSON.parse(await readChildInput()));
process.stdout.write(JSON.stringify(receipt) + '\n');
}
export class AssertionWitnessSession {
private privateKey: string;
readonly publicKey: string;
readonly keyId: string;
private nonces = new Set<string>();
constructor(
private workDirectory: string,
private deadline: number,
) {
const stat = lstatSync(workDirectory),
real = realpathSync(workDirectory),
resolved = lstatSync(real);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
!resolved.isDirectory() ||
resolved.isSymbolicLink() ||
stat.dev !== resolved.dev ||
stat.ino !== resolved.ino ||
(process.getuid && resolved.uid !== process.getuid()) ||
(resolved.mode & 0o022) !== 0
)
throw new CsoError('UNSAFE_PATH', 'Assertion witness working directory must be private and owned');
this.workDirectory = real;
const pair = generateKeyPairSync('ed25519');
this.privateKey = pair.privateKey.export({ format: 'pem', type: 'pkcs8' }).toString();
this.publicKey = pair.publicKey.export({ format: 'der', type: 'spki' }).toString('hex');
this.keyId = sha256(Buffer.from(this.publicKey, 'hex'));
}
handle(
stable: Omit<AssertionWitnessBinding, 'schemaVersion' | 'protocol' | 'nonce' | 'issuedAt' | 'expiresAt'>,
): AssertionWitnessHandle {
const now = Date.now(),
expires = Math.min(this.deadline, now + MAX_RECEIPT_AGE);
if (expires <= now)
throw new CsoError('DEADLINE', 'No time remains for an authenticated assertion witness');
let nonce = '';
do {
nonce = randomBytes(32).toString('hex');
} while (this.nonces.has(nonce));
this.nonces.add(nonce);
const binding = validateAssertionWitnessBinding({
schemaVersion: 1,
protocol: PROTOCOL,
nonce,
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(expires).toISOString(),
...stable,
});
let consumed = false;
return {
binding,
attest: async (observation, executions) => {
if (consumed)
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness challenge was already consumed');
consumed = true;
const input = JSON.stringify({
privateKey: this.privateKey,
publicKey: this.publicKey,
binding,
observation,
executions,
} satisfies ChildRequest);
if (Buffer.byteLength(input) > 2 * MAX_OUTPUT)
throw new CsoError(
'REDACTION_FAILED',
'Assertion witness input exceeds the bounded helper channel',
);
const bun = /^bun(?:\.exe)?$/i.test(basename(process.execPath)),
file = bun
? process.execPath
: join(
dirname(process.execPath),
process.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher',
),
args = bun ? [import.meta.path, '--child'] : ['__cso-assertion-witness'],
env =
process.platform === 'win32'
? {
PATH: dirname(process.execPath),
SYSTEMROOT: process.env.SYSTEMROOT ?? 'C:\\Windows',
WINDIR: process.env.WINDIR ?? 'C:\\Windows',
}
: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC' },
result = await runProcess(file, args, {
cwd: this.workDirectory,
env,
timeoutMs: Math.max(1, expires - Date.now()),
maxBytes: 128 * 1024,
input,
raw: true,
});
if (result.timedOut)
throw new CsoError('DEADLINE', 'Assertion witness exceeded the verification deadline');
if (result.truncated || result.code !== 0)
throw new CsoError(
'TOOL_FAILED',
'Authenticated assertion witness did not return a bounded receipt',
);
let receipt: unknown;
try {
receipt = JSON.parse(result.stdout);
} catch {
throw new CsoError('TOOL_FAILED', 'Authenticated assertion witness returned invalid output');
}
return validateAssertionWitnessReceipt(receipt, binding, this.publicKey, observation);
},
validate: (receipt, observation, current = Date.now()) =>
validateAssertionWitnessReceipt(receipt, binding, this.publicKey, observation, current),
};
}
}
if(import.meta.main&&process.argv.at(-1)==='--child')runAssertionWitnessChild().catch(()=>{process.stderr.write('assertion witness failed\n');process.exitCode=1;});
if (import.meta.main && process.argv.at(-1) === '--child')
runAssertionWitnessChild().catch(() => {
process.stderr.write('assertion witness failed\n');
process.exitCode = 1;
});
+3
View File
@@ -11,6 +11,8 @@
"scripts": {
"build": "bash scripts/build.sh",
"build:cso": "bash scripts/build-cso.sh",
"format:cso": "prettier --write 'lib/cso/*.ts'",
"format:cso:check": "prettier --check 'lib/cso/*.ts'",
"test:cso:docker": "bun test --max-concurrency 1 test/cso-docker-integration.test.ts test/cso-node-lifecycle-integration.test.ts test/cso-stack-cold-integration.test.ts",
"test:cso:macos": "bun test test/cso-macos-launcher.test.ts test/cso-registry-socket.test.ts",
"test:cso:windows": "bun test test/cso-windows-launcher.test.ts",
@@ -88,6 +90,7 @@
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.117",
"@anthropic-ai/sdk": "^0.78.0",
"prettier": "3.9.9",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
+2 -2
View File
@@ -118,9 +118,9 @@ describe('CSO scanner qualification workflow', () => {
expect(anonymousStage.env.GH_TOKEN).toBe('${{ github.token }}');
for(const value of ['--signer-workflow "$signer_workflow"','--signer-digest "$signer_digest"','--source-digest "$source_commit"','cso-attestation-evidence.ts digest','provenanceStatementDigest','sbomStatementDigest'])expect(raw).toContain(value);
expect(raw).not.toContain('cso-scanner-staging');
const docker = fs.readFileSync(path.join(ROOT, 'lib/cso/docker.ts'), 'utf8');
const docker = fs.readFileSync(path.join(ROOT, 'lib/cso/docker.ts'), 'utf8').replace(/\s+/g, '');
for (const flag of ["'--pull=never'", "'--read-only'", "'--cap-drop','ALL'", "'no-new-privileges:true'", "'seccomp=builtin'", "'--log-driver=none'", "'--network'"]) expect(docker).toContain(flag);
expect(docker).toContain("['rm','--force','--volumes',id]"); expect(docker).toContain('Pinned runtime image declares writable volumes');
expect(docker).toContain("['rm','--force','--volumes',id]"); expect(docker).toContain('Pinnedruntimeimagedeclareswritablevolumes');
expect(raw).toContain('cso-scanner-catalog.ts assemble'); expect(raw).toContain('cso-scanner-catalog.ts validate-transition lib/cso/scanner-images/catalog.json promotion/catalog-proposal.json'); expect(raw).toContain('gh pr create --base main');
expect(raw).toContain('branch="cso-scanner-catalog-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"'); expect(raw).not.toContain('branch="cso-scanner-catalog-$GITHUB_RUN_ID"');
const publicPromotion = raw.indexOf('Recheck public visibility and anonymous pulls before promotion');
+1 -1
View File
@@ -135,7 +135,7 @@ describe('CSO native Windows build contract', () => {
expect(msvc).toContain('GSTACK_CSO_GIT_PATH');
expect(msvc).toContain('/FI$binding');
expect(msvc).toContain('if ($LASTEXITCODE -ne 0)');
const processSource=fs.readFileSync(path.join(ROOT,'lib','cso','process.ts'),'utf8');
const processSource=fs.readFileSync(path.join(ROOT,'lib','cso','process.ts'),'utf8').replace(/\s+/g,'');
expect(processSource).toContain("includeNullPath=process.platform==='win32'?'/dev/null':nullPath");
const launcherSource = fs.readFileSync(path.join(ROOT, 'lib/cso/launcher-windows.c'), 'utf8');
expect(launcherSource).toContain('.gstack-cso-generation.lock');