mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 01:45:29 +02:00
* feat(cso): add verified audits and replayable repair bundles * fix(cso): harden qualification and setup boundaries * fix(cso): assemble security canaries at runtime * fix(cso): bound release proof and maintenance work Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): require complete evaluation reports Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): replay expired snapshots from supplied source Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): synchronize DNS cancellation assertion Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore(ship): exempt repository owner from liveness proof Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): make recheck retention overlap deterministic Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: bump version and changelog (v1.85.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass native release gates Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.86.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): resolve rechecks by finding Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.87.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass macOS and Windows release gates Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): harden native verification gates * fix(cso): refine Windows native diagnostics * test(cso): isolate Windows Git startup failure * test(cso): stabilize Windows native diagnostics * fix(cso): support hardened Git on Windows * fix(cso): close final verification gaps * test(cso): bound cold Docker fixture setup * fix(cso): restore cross-platform free-suite gates --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
85 lines
7.0 KiB
TypeScript
85 lines
7.0 KiB
TypeScript
import { afterEach, describe, expect, spyOn, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { capture } from '../lib/cso/snapshot';
|
|
|
|
const roots:string[]=[];
|
|
afterEach(()=>{for(const root of roots.splice(0))fs.rmSync(root,{recursive:true,force:true});});
|
|
|
|
function trustedGit():string{
|
|
const executable=Bun.which('git');
|
|
if(!executable)throw new Error('git is required for the snapshot disappearance fixture');
|
|
return executable;
|
|
}
|
|
function gitEnvironment(home:string):NodeJS.ProcessEnv{
|
|
return{...process.env,HOME:home,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_TERMINAL_PROMPT:'0'};
|
|
}
|
|
type FileIdentity={path:string;dev:bigint;ino:bigint};
|
|
function canonicalFile(file:string):string{
|
|
const resolved=fs.realpathSync.native(file);
|
|
return process.platform==='win32'?resolved.replace(/^\\\\\?\\UNC\\/i,'\\\\').replace(/^\\\\\?\\/,'').toLowerCase():resolved;
|
|
}
|
|
function fileIdentity(file:string):FileIdentity{const stat=fs.statSync(file,{bigint:true});return{path:canonicalFile(file),dev:stat.dev,ino:stat.ino};}
|
|
function isFileIdentity(candidate:unknown,expected:FileIdentity):boolean{
|
|
if(path.basename(String(candidate)).toLowerCase()!=='tracked.txt')return false;
|
|
try{const file=String(candidate),current=fs.statSync(file,{bigint:true});return canonicalFile(file)===expected.path&¤t.dev===expected.dev&¤t.ino===expected.ino;}catch{return false;}
|
|
}
|
|
|
|
function fixture(){
|
|
const root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-snapshot-disappearance-'));roots.push(root);
|
|
const repo=path.join(root,'repo'),runDir=path.join(root,'run');
|
|
fs.mkdirSync(repo);fs.mkdirSync(runDir,{mode:0o700});
|
|
const git=(...args:string[])=>{const result=spawnSync(trustedGit(),['-C',repo,...args],{encoding:'utf8',env:gitEnvironment(root),timeout:30_000});if(result.status)throw new Error(result.stderr);return result.stdout.trim();};
|
|
git('init','-q');git('config','user.email','fixture@example.test');git('config','user.name','Fixture');
|
|
const tracked=path.join(repo,'tracked.txt');fs.writeFileSync(tracked,'security-relevant source\n');git('add','tracked.txt');git('commit','-qm','base');
|
|
// Match the file itself instead of one pathname spelling. Windows may hand
|
|
// capture() an 8.3, long, or \\?\-namespaced spelling for this same inode.
|
|
return{repo,runDir,tracked,trackedIdentity:fileIdentity(tracked),parked:path.join(repo,'.tracked.txt.parked')};
|
|
}
|
|
|
|
describe('CSO snapshot source-disappearance races',()=>{
|
|
// Bun on Windows does not patch the node:fs binding imported by snapshot.ts,
|
|
// so these syscall-choreography fixtures cannot activate there. The native
|
|
// Windows launcher suite instead mutates real source after capture history is
|
|
// recorded and requires start to fail closed without a snapshot or report.
|
|
test.skipIf(process.platform==='win32')('rejects a tracked path that disappears at its capture lstat and is restored before final membership validation',async()=>{
|
|
const {repo,runDir,tracked,trackedIdentity,parked}=fixture(),lstat=fs.lstatSync;
|
|
let trackedLstats=0,injected=false,failure:unknown;
|
|
const patched=spyOn(fs,'lstatSync').mockImplementation(((candidate:any,options?:any)=>{
|
|
if(isFileIdentity(candidate,trackedIdentity)&&++trackedLstats===2){
|
|
// The first lstat is rejectSpecialFiles' directory walk. The second is
|
|
// capture's per-entry existence check. Move the tracked file for that
|
|
// exact syscall, then restore it before any final Git membership check.
|
|
injected=true;fs.renameSync(tracked,parked);
|
|
try{return options===undefined?lstat(candidate):lstat(candidate,options);}
|
|
finally{fs.renameSync(parked,tracked);}
|
|
}
|
|
return options===undefined?lstat(candidate):lstat(candidate,options);
|
|
}) as typeof fs.lstatSync);
|
|
try{await capture(repo,runDir);}catch(error){failure=error;}finally{patched.mockRestore();}
|
|
expect(injected).toBe(true);
|
|
expect(fs.readFileSync(tracked,'utf8')).toBe('security-relevant source\n');
|
|
expect(failure).toMatchObject({code:'SNAPSHOT_RACE'});
|
|
});
|
|
test.skipIf(process.platform==='win32')('rejects a nonignored source file introduced during the final content validation',async()=>{
|
|
const {repo,runDir,tracked,trackedIdentity}=fixture(),late=path.join(path.dirname(tracked),'late-vulnerable.js'),lstat=fs.lstatSync;let injected=false,failure:unknown;
|
|
const patched=spyOn(fs,'lstatSync').mockImplementation(((candidate:any,options?:any)=>{
|
|
if(!injected&&isFileIdentity(candidate,trackedIdentity)&&fs.existsSync(path.join(runDir,'history-status.json'))){injected=true;fs.writeFileSync(late,'export const vulnerable = true\n');}
|
|
return options===undefined?lstat(candidate):lstat(candidate,options);
|
|
}) as typeof fs.lstatSync);
|
|
try{await capture(repo,runDir);}catch(error){failure=error;}finally{patched.mockRestore();}
|
|
expect(injected).toBe(true);expect(fs.readFileSync(late,'utf8')).toContain('vulnerable');expect(failure).toMatchObject({code:'SNAPSHOT_RACE'});expect(fs.existsSync(path.join(runDir,'snapshot.json'))).toBe(false);
|
|
});
|
|
test.skipIf(process.platform==='win32')('binds the audited repository root while source is copied',async()=>{
|
|
const root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-snapshot-root-swap-'));roots.push(root);const repo=path.join(root,'repo'),parked=path.join(root,'parked'),decoy=path.join(root,'decoy'),gitDir=path.join(root,'gitdir'),runDir=path.join(root,'run'),tracked=path.join(repo,'tracked.txt');fs.mkdirSync(repo);fs.mkdirSync(runDir,{mode:0o700});
|
|
const run=(...args:string[])=>{const result=spawnSync(trustedGit(),args,{encoding:'utf8',env:gitEnvironment(root),timeout:30_000});if(result.status)throw new Error(result.stderr);};
|
|
run('init','-q',`--separate-git-dir=${gitDir}`,repo);run('-C',repo,'config','user.email','fixture@example.test');run('-C',repo,'config','user.name','Fixture');fs.writeFileSync(tracked,'secure original source\n');run('-C',repo,'add','tracked.txt');run('-C',repo,'commit','-qm','base');fs.mkdirSync(decoy);fs.writeFileSync(path.join(decoy,'.git'),`gitdir: ${gitDir}\n`);fs.writeFileSync(path.join(decoy,'tracked.txt'),'vulnerable decoy source\n');
|
|
const lstat=fs.lstatSync;let seen=0,injected=false,failure:unknown;
|
|
const patched=spyOn(fs,'lstatSync').mockImplementation(((candidate:any,options?:any)=>{if(path.resolve(String(candidate))===tracked&&++seen===2){injected=true;fs.renameSync(repo,parked);fs.renameSync(decoy,repo);}return options===undefined?lstat(candidate):lstat(candidate,options);}) as typeof fs.lstatSync);
|
|
try{await capture(repo,runDir);}catch(error){failure=error;}finally{patched.mockRestore();if(injected){fs.renameSync(repo,decoy);fs.renameSync(parked,repo);}}
|
|
expect(injected).toBe(true);expect(failure).toMatchObject({code:'SNAPSHOT_RACE'});expect(fs.readFileSync(path.join(repo,'tracked.txt'),'utf8')).toBe('secure original source\n');expect(fs.existsSync(path.join(runDir,'snapshot.json'))).toBe(false);
|
|
});
|
|
});
|