mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
fix(cso): resolve rechecks by finding
Co-Authored-By: OpenAI Codex <noreply@openai.com>
This commit is contained in:
1 parent
24cd6a3119
commit
589e3a3a95
4 files changed
+33
-5
No files matched your search
+1
-1
@@ -71,7 +71,7 @@ gstack-cso start --repo <repo> [scope/diff/base/budget/offline flags] [--compreh
|
||||
gstack-cso doctor --repo <repo>
|
||||
gstack-cso resume <run>
|
||||
gstack-cso replay <bundle> [--source <matching-source>]
|
||||
gstack-cso recheck <finding> --run <original-run> --repo <repo>
|
||||
gstack-cso recheck <finding> --repo <repo> [--run <original-run>]
|
||||
gstack-cso inspect <run>
|
||||
gstack-cso read <run> <path-or-handle>
|
||||
gstack-cso history <run> [path-or-handle]
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ gstack-cso start --repo <repo> [scope/diff/base/budget/offline flags] [--compreh
|
||||
gstack-cso doctor --repo <repo>
|
||||
gstack-cso resume <run>
|
||||
gstack-cso replay <bundle> [--source <matching-source>]
|
||||
gstack-cso recheck <finding> --run <original-run> --repo <repo>
|
||||
gstack-cso recheck <finding> --repo <repo> [--run <original-run>]
|
||||
gstack-cso inspect <run>
|
||||
gstack-cso read <run> <path-or-handle>
|
||||
gstack-cso history <run> [path-or-handle]
|
||||
|
||||
+22
-3
@@ -70,7 +70,7 @@ Usage:
|
||||
gstack-cso patch-hash REQUEST.json
|
||||
gstack-cso finish RUN
|
||||
gstack-cso replay BUNDLE_ID [--source MATCHING_SOURCE]
|
||||
gstack-cso recheck FINDING --run RUN --repo PATH
|
||||
gstack-cso recheck FINDING --repo PATH [--run RUN]
|
||||
gstack-cso import-v2 REPORT.json
|
||||
gstack-cso schema
|
||||
|
||||
@@ -363,10 +363,29 @@ async function scanner(args:string[],sarif=false){
|
||||
});
|
||||
}
|
||||
function scannerOutcome(args:string[]){const {dir}=run(args);if(args.length!==1||!/^[a-z0-9-]{1,40}-[a-f0-9]{16}-[a-f0-9]{16}$/.test(args[0]))throw new CsoError('INVALID_ARGUMENT','scanner-outcome requires one immutable scanner artifact ID');return readJson(join(dir,'scanner-outcomes',`${args[0]}.json`));}
|
||||
async function recheck(args:string[],dependencies:CsoCliDependencies){const startedAt=new Date();if(!args.length)throw new CsoError('INVALID_ARGUMENT','recheck requires a finding ID');retention(startedAt.getTime(),{deadlineMs:startedAt.getTime()+RETENTION_MAINTENANCE_MS,maxEntries:RETENTION_MAX_ENTRIES});const findingId=args.shift()!,runId=need(args,'--run'),originalDir=runDirectory(runId);return await withLock(originalDir,async()=>{const original=loadReport(originalDir),finding=original.findings.find(f=>f.id===findingId);if(!finding)throw new CsoError('MISSING_INPUT','Original finding does not exist');if(original.status!=='finished')throw new CsoError('INVALID_SCHEMA','Recheck requires a finished original audit');
|
||||
function recheckOriginalDirectory(repo:string,findingId:string,requestedRun?:string):{dir:string;runId:string}{
|
||||
const currentRepoId=repoId(repo),root=privateRoot(),repoDir=join(root,currentRepoId),runPattern=/^\d{13}-[a-f0-9]{16}$/;
|
||||
if(requestedRun){
|
||||
if(!runPattern.test(requestedRun))throw new CsoError('INVALID_ARGUMENT','Run identifier must be the ID returned by start');
|
||||
const dir=join(repoDir,requestedRun);if(!fs.existsSync(dir))throw new CsoError('MISSING_INPUT','Original run was not found for the current repository or has expired');
|
||||
return{dir:secureDirectory(dir),runId:requestedRun};
|
||||
}
|
||||
if(!fs.existsSync(repoDir))throw new CsoError('MISSING_INPUT','No finished original audit contains this finding in the current repository');
|
||||
const matches:{dir:string;runId:string}[]=[],directory=fs.opendirSync(secureDirectory(repoDir));let visited=0;
|
||||
try{let entry:fs.Dirent|null;while((entry=directory.readSync())!==null){
|
||||
if(++visited>REPLAY_LOOKUP_MAX_ENTRIES)throw new CsoError('INSUFFICIENT_CAPACITY',`Recheck lookup exceeded ${REPLAY_LOOKUP_MAX_ENTRIES} private state entries`);
|
||||
if(!entry.isDirectory()||!runPattern.test(entry.name))continue;const dir=join(repoDir,entry.name),reportPath=join(dir,'report.json');if(!fs.existsSync(reportPath))continue;const report=loadReport(dir);
|
||||
if(report.runId!==entry.name||report.repoId!==currentRepoId)throw new CsoError('INCOMPATIBLE_INPUT','Retained original audit identity does not match its repository state path');
|
||||
if(report.status==='finished'&&!report.parent&&report.findings.some(f=>f.id===findingId))matches.push({dir,runId:entry.name});
|
||||
}}finally{directory.closeSync();}
|
||||
if(!matches.length)throw new CsoError('MISSING_INPUT','No finished original audit contains this finding in the current repository');
|
||||
if(matches.length>1)throw new CsoError('INVALID_ARGUMENT',`Finding matches ${matches.length} finished original audits; use --run RUN to select one`);
|
||||
return matches[0];
|
||||
}
|
||||
async function recheck(args:string[],dependencies:CsoCliDependencies){const startedAt=new Date();if(!args.length)throw new CsoError('INVALID_ARGUMENT','recheck requires a finding ID');const findingId=args.shift()!;if(!/^[a-f0-9]{32}$/.test(findingId))throw new CsoError('INVALID_ARGUMENT','Finding identifier must be the 32-character ID reported by CSO');const repo=callerPath(need(args,'--repo')),requestedRun=args.includes('--run')?need(args,'--run'):undefined;if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown recheck argument: ${args[0]}`);if(!fs.existsSync(repo)||!fs.statSync(repo).isDirectory())throw new CsoError('MISSING_INPUT','Repository directory does not exist');assertStateOutside(repo);retention(startedAt.getTime(),{deadlineMs:startedAt.getTime()+RETENTION_MAINTENANCE_MS,maxEntries:RETENTION_MAX_ENTRIES});const selected=recheckOriginalDirectory(repo,findingId,requestedRun),runId=selected.runId,originalDir=selected.dir;return await withLock(originalDir,async()=>{const original=loadReport(originalDir);if(original.runId!==runId||original.repoId!==repoId(repo))throw new CsoError('INCOMPATIBLE_INPUT','Retained original audit identity does not match its repository state path');const finding=original.findings.find(f=>f.id===findingId);if(!finding)throw new CsoError('MISSING_INPUT','Original finding does not exist');if(original.status!=='finished'||original.parent)throw new CsoError('INVALID_SCHEMA','Recheck requires a finished original audit');
|
||||
// Keep the original immutable while the fresh snapshot is captured and
|
||||
// until its child lineage report has been durably published.
|
||||
const repo=callerPath(need(args,'--repo'));if(args.length)throw new CsoError('INVALID_ARGUMENT',`Unknown recheck argument: ${args[0]}`);if(repoId(repo)!==original.repoId)throw new CsoError('INCOMPATIBLE_INPUT','Recheck must use the original audited repository');const oldManifest=readJson(join(originalDir,'snapshot.json'));
|
||||
const oldManifest=readJson(join(originalDir,'snapshot.json'));
|
||||
const preserveBase=original.policy.diff||Boolean(original.source.baseCommit),report=await start(['--repo',repo,...(original.policy.mode==='comprehensive'?['--comprehensive']:[]),...(original.policy.diff?['--diff']:[]),...(preserveBase?['--base',original.policy.base]:[]),'--budget',String(original.policy.budgetSeconds),...(original.policy.offline?['--offline']:[]),...(original.policy.scope==='default'?[]:original.policy.scope.startsWith('domain:')?['--scope',original.policy.scope.slice(7)]:[`--${original.policy.scope}`])],dependencies,{runId,findingId,kind:'recheck'},oldManifest.headCommit,startedAt);
|
||||
return {runId:report.runId,parent:report.parent};});}
|
||||
|
||||
|
||||
@@ -17,10 +17,19 @@ function recheckEvidence(source='src/users.ts'){return[
|
||||
{kind:'caller',path:source,line:1,observation:'Fresh caller trace reaches the authorization predicate'},
|
||||
{kind:'security_boundary',path:source,line:1,observation:'Fresh boundary trace confirms cross-tenant access is denied'},
|
||||
];}
|
||||
function finishedOriginal(name:string,candidate:any){
|
||||
const run=JSON.parse(command(['start','--repo',repo,'--scope','auth','--offline']).stdout),initial=JSON.parse(fs.readFileSync(reportPath(run),'utf8'));
|
||||
expect(command(['submit',run.runId,writeInput(`${name}.json`,completeEvidence(initial,[candidate]))]).status).toBe(0);
|
||||
expect(command(['finish',run.runId]).status).toBe(0);
|
||||
return{run,findingId:JSON.parse(fs.readFileSync(reportPath(run),'utf8')).findings[0].id};
|
||||
}
|
||||
beforeAll(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-cli-recheck-'));repo=path.join(root,'repo');state=path.join(root,'state');fs.mkdirSync(repo);git('init','-q');git('config','user.email','fixture@example.test');git('config','user.name','Fixture');fs.mkdirSync(path.join(repo,'src'));fs.writeFileSync(path.join(repo,'src','users.ts'),'export const tenantQuery = (id:string) => db.user.findUnique({where:{id}})\n');fs.writeFileSync(path.join(repo,'src','other.ts'),'export const unrelated = true\n');git('add','src/users.ts','src/other.ts');git('commit','-qm','fixture');});
|
||||
afterAll(()=>fs.rmSync(root,{recursive:true,force:true}));
|
||||
|
||||
describe('CSO recheck persistence',()=>{
|
||||
test('resolves a unique finished original audit from the current repository',()=>{const candidate={...finding,rootCause:'Unique fixture query omits its tenant predicate',location:{...finding.location,symbol:'uniqueTenantQuery'}},original=finishedOriginal('unique-original',candidate),rechecked=command(['recheck',original.findingId,'--repo',repo]);expect(rechecked.status).toBe(0);expect(rechecked.stderr).toBe('');const child=JSON.parse(rechecked.stdout);expect(child.parent).toEqual({runId:original.run.runId,findingId:original.findingId,kind:'recheck'});},30_000);
|
||||
test('reports a missing original finding without requiring an internal run ID',()=>{const missing=command(['recheck','0'.repeat(32),'--repo',repo]);expect(missing.status).not.toBe(0);expect(missing.stderr).toContain('MISSING_INPUT');expect(missing.stderr).toContain('No finished original audit contains this finding');},30_000);
|
||||
test('rejects ambiguous originals and accepts --run as an explicit disambiguator',()=>{const candidate={...finding,rootCause:'Ambiguous fixture query omits its tenant predicate',location:{...finding.location,symbol:'ambiguousTenantQuery'}},first=finishedOriginal('ambiguous-original-first',candidate),second=finishedOriginal('ambiguous-original-second',candidate);expect(second.findingId).toBe(first.findingId);const ambiguous=command(['recheck',first.findingId,'--repo',repo]);expect(ambiguous.status).not.toBe(0);expect(ambiguous.stderr).toContain('INVALID_ARGUMENT');expect(ambiguous.stderr).toContain('--run RUN');const selected=command(['recheck',first.findingId,'--repo',repo,'--run',second.run.runId]);expect(selected.status).toBe(0);expect(JSON.parse(selected.stdout).parent.runId).toBe(second.run.runId);},30_000);
|
||||
test('requires an immutable finished original audit',()=>{const run=JSON.parse(command(['start','--repo',repo,'--scope','auth','--offline']).stdout),report=JSON.parse(fs.readFileSync(reportPath(run),'utf8'));expect(command(['submit',run.runId,writeInput('unfinished.json',completeEvidence(report,[finding]))]).status).toBe(0);const id=JSON.parse(fs.readFileSync(reportPath(run),'utf8')).findings[0].id,recheck=command(['recheck',id,'--run',run.runId,'--repo',repo]);expect(recheck.status).not.toBe(0);expect(recheck.stderr).toContain('finished original audit');},30_000);
|
||||
test('resolved closure rejects free-form or unrelated boundary evidence',()=>{const original=JSON.parse(command(['start','--repo',repo,'--scope','auth','--offline']).stdout),originalPath=reportPath(original),initial=JSON.parse(fs.readFileSync(originalPath,'utf8'));expect(command(['submit',original.runId,writeInput('bound-original.json',completeEvidence(initial,[finding]))]).status).toBe(0);expect(command(['finish',original.runId]).status).toBe(0);const findingId=JSON.parse(fs.readFileSync(originalPath,'utf8')).findings[0].id,started=JSON.parse(command(['recheck',findingId,'--run',original.runId,'--repo',repo]).stdout);
|
||||
const freeForm=command(['submit',started.runId,writeInput('free-form-claim.json',{recheck:{findingId,outcome:'resolved',evidence:['caller words','boundary words'],rootCause:finding.rootCause}})]);expect(freeForm.status).not.toBe(0);expect(freeForm.stderr).toContain('recheck evidence[0]');
|
||||
|
||||
Reference in new issue
Block a user