From f4821d59749cd6b6ebd6c68bc0574dceefb60e61 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 11 Sep 2026 05:30:56 +0000 Subject: [PATCH] fix(cso): assemble security canaries at runtime --- test/cso-cli.test.ts | 5 +++-- test/cso-contracts.test.ts | 3 ++- test/cso-distribution.test.ts | 3 ++- test/cso-preparation.test.ts | 3 ++- test/cso-scanners.test.ts | 4 ++-- test/cso-snapshot-state.test.ts | 6 +++--- test/skill-e2e-cso.test.ts | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/test/cso-cli.test.ts b/test/cso-cli.test.ts index 83c52ff47..343970994 100644 --- a/test/cso-cli.test.ts +++ b/test/cso-cli.test.ts @@ -5,9 +5,10 @@ import { fingerprint, sha256 } from '../lib/cso/contracts'; import { patchHash } from '../lib/cso/verification'; import { redact } from '../lib/cso/process'; const ROOT=path.resolve(import.meta.dir,'..'),launcher=path.join(ROOT,'bin',process.platform==='win32'?'gstack-cso-launcher.exe':'gstack-cso-launcher');let root='',repo='',state=''; +const CREDENTIAL_CANARY=['ghp_','abcdefghijklmnopqrstuvwxyz1234567890'].join(''); function command(args:string[],extra:Record={},file=launcher){const r=spawnSync(file,args,{cwd:repo,encoding:'utf8',env:{HOME:root,GSTACK_HOME:state,PATH:`${repo}/hostile-bin:/usr/bin:/bin`,BUN_OPTIONS:`--preload=${repo}/preload.ts`,BUN_BE_BUN:'1',NODE_OPTIONS:`--require=${repo}/preload.cjs`,RUBYOPT:`-r${repo}/preload.rb`,PYTHONPATH:repo,LD_PRELOAD:path.join(repo,'missing.so'),BASH_ENV:path.join(repo,'bashenv'),...extra},timeout:30_000});return r;} function git(...args:string[]){const r=spawnSync('/usr/bin/git',['-C',repo,...args],{encoding:'utf8',env:{HOME:root,PATH:'/usr/bin:/bin'},timeout:30_000});if(r.status)throw new Error(r.stderr);return r.stdout;} -beforeAll(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-cli-'));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','control.test.ts'),'export const control = true\n');fs.writeFileSync(path.join(repo,'.env'),'TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890\n');git('add','src/users.ts','src/control.test.ts','.env');git('commit','-qm','fixture');fs.mkdirSync(path.join(repo,'hostile-bin'));for(const tool of ['git','dirname'])fs.writeFileSync(path.join(repo,'hostile-bin',tool),`#!/bin/sh\ntouch '${path.join(repo,'path-shim-ran')}'\nexit 99\n`,{mode:0o755});for(const f of ['preload.ts','preload.cjs','preload.rb','bashenv'])fs.writeFileSync(path.join(repo,f),`require('fs').writeFileSync('${path.join(repo,'autoload-ran')}','yes')\n`);}); +beforeAll(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),'cso-cli-'));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','control.test.ts'),'export const control = true\n');fs.writeFileSync(path.join(repo,'.env'),`TOKEN=${CREDENTIAL_CANARY}\n`);git('add','src/users.ts','src/control.test.ts','.env');git('commit','-qm','fixture');fs.mkdirSync(path.join(repo,'hostile-bin'));for(const tool of ['git','dirname'])fs.writeFileSync(path.join(repo,'hostile-bin',tool),`#!/bin/sh\ntouch '${path.join(repo,'path-shim-ran')}'\nexit 99\n`,{mode:0o755});for(const f of ['preload.ts','preload.cjs','preload.rb','bashenv'])fs.writeFileSync(path.join(repo,f),`require('fs').writeFileSync('${path.join(repo,'autoload-ran')}','yes')\n`);}); afterAll(()=>fs.rmSync(root,{recursive:true,force:true})); describe('compiled CSO command workflow',()=>{ @@ -22,7 +23,7 @@ describe('compiled CSO command workflow',()=>{ fs.mkdirSync(path.join(walletRepo,'src'),{recursive:true}); const runGit=(...args:string[])=>{const result=spawnSync('/usr/bin/git',['-C',walletRepo,...args],{encoding:'utf8',env:{HOME:root,PATH:'/usr/bin:/bin'},timeout:30_000});if(result.status)throw new Error(result.stderr);}; runGit('init','-q');runGit('config','user.email','fixture@example.test');runGit('config','user.name','Fixture'); - fs.writeFileSync(path.join(walletRepo,sourcePath),'export const value = true\n// TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890\n');fs.writeFileSync(path.join(walletRepo,'control.test.js'),'test(\"control\",()=>{})\n');runGit('add','.');runGit('commit','-qm','wallet-shaped path'); + fs.writeFileSync(path.join(walletRepo,sourcePath),`export const value = true\n// TOKEN=${CREDENTIAL_CANARY}\n`);fs.writeFileSync(path.join(walletRepo,'control.test.js'),'test(\"control\",()=>{})\n');runGit('add','.');runGit('commit','-qm','wallet-shaped path'); const started=command(['start','--repo',walletRepo,'--comprehensive','--offline']);expect(started.status).toBe(0); const run=JSON.parse(started.stdout),dir=path.join(state,'security','cso',run.repoId,run.runId),privateManifest=JSON.parse(fs.readFileSync(path.join(dir,'snapshot.json'),'utf8')); const privateEntry=privateManifest.entries.find((item:any)=>item.path===sourcePath);expect(privateEntry.path).toBe(sourcePath);expect(privateEntry.pathId).toMatch(/^[a-f0-9]{32}$/); diff --git a/test/cso-contracts.test.ts b/test/cso-contracts.test.ts index 4dbcbe2af..d19995a5c 100644 --- a/test/cso-contracts.test.ts +++ b/test/cso-contracts.test.ts @@ -10,6 +10,7 @@ import { admit, machinePoolRoot, markSupervised, release } from '../lib/cso/admi import { sanitizeHelperForJson } from '../lib/cso/process'; const dirs:string[]=[];const tmp=()=>{const p=fs.mkdtempSync(path.join(os.tmpdir(),'cso-contract-'));dirs.push(p);return p;};afterEach(()=>{for(const p of dirs.splice(0))fs.rmSync(p,{recursive:true,force:true});}); +const CREDENTIAL_CANARY=['ghp_','abcdefghijklmnopqrstuvwxyz1234567890'].join(''); const finding:any={title:'Cross-tenant read',rootCause:'Tenant query omits caller tenant predicate',location:{path:'src/users.ts',line:4,symbol:'getUser'},advisoryIds:[],severity:'high',confidence:'high',confidenceRationale:'The captured caller-to-sink trace and control review directly support the finding',evidence:'supported',attackerControl:'Authenticated user selects another object ID',impact:'Reads another tenant record',scenario:'A tenant supplies a known record ID owned by a second tenant and receives that record',trace:['GET /users/:id','getUser','db.user.findUnique'],references:['src/users.ts:4','OWASP API1:2023'],recommendation:'Bind every record lookup to the authenticated tenant identifier and reject a missing tenant context',challenge:{reviewer:'reviewer-2',independent:true,mode:'independent_agent',callers:'Route is public to authenticated tenants',controls:'Session middleware authenticates but does not bind tenant',counterevidence:'UUID is unguessable only by convention',conclusion:'Authorization invariant is absent'}}; const request=(before:string,after:string)=>({findingId:'a'.repeat(32),runtimeProfile:'node',port:3456,start:{executable:'/usr/local/bin/node',args:['app.js']},legitimate:[{name:'health',path:'/health',method:'GET',expected:{status:200,includes:'ok'}}],security:{name:'tenant isolation',path:'/user?id=2',method:'GET',expected:{status:403},vulnerable:{status:200,includes:'tenant-b'}},existingTests:[{executable:'/usr/local/bin/node',args:['test.js']}],testFiles:['test.js'],fixtures:{'tenant.json':'{}'},boundaryFiles:['app.js'],changes:[{path:'app.js',beforeSha256:sha256(before),after,effect:'source'}],review:{reviewer:'independent-reviewer',independent:true,rootCauseRepaired:true,featurePreserved:true,boundaryMocks:false,rationale:'Predicate now binds the caller tenant and health behavior is preserved',reviewedPatchHash:''}}); // Structural signed fixture for bundle-reader mutation tests. Production @@ -183,7 +184,7 @@ describe('tested repair certificate gate',()=>{ test('overlapping security oracles and no-op repairs cannot be certified',()=>{const overlapping:any=request('vulnerable\n','fixed\n');overlapping.review.reviewedPatchHash='pending';overlapping.security.vulnerable={...overlapping.security.expected};expect(()=>validateVerificationRequest(overlapping)).toThrow('mutually exclusive');const noop:any=request('vulnerable\n','vulnerable\n');noop.review.reviewedPatchHash='pending';expect(()=>validateVerificationRequest(noop)).toThrow('material patch');}); test('review is a separate content-bound but explicitly self-attested artifact',()=>{const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);const req=validateVerificationRequest(raw),artifact=makeReviewArtifact('run',req,'producer-session');raw.review.artifactId=artifact.id;const bound=validateVerificationRequest(raw);expect(validateReviewArtifact(artifact,'run',bound)).toMatchObject({producer:'producer-session',assurance:'self_attested'});expect(()=>makeReviewArtifact('run',req,req.review.reviewer)).toThrow('distinct');expect(()=>validateReviewArtifact({...artifact,rationale:'changed'},'run',bound)).toThrow('does not bind');}); test('executor observations have a strict helper-owned shape',()=>{expect(()=>validateVerificationObservation({booted:true,legitimate:true,security:'pass',existingTests:true,output:'ok',inputHash:'',tested:true})).toThrow('Unexpected');expect(()=>validateVerificationObservation({booted:'yes',legitimate:true,security:'pass',existingTests:true,output:'ok',inputHash:''})).toThrow('booleans');}); - test('secret-bearing fixtures cannot enter a replayable verification bundle',async()=>{const runDir=tmp(),snapshot=path.join(runDir,'snapshot');fs.mkdirSync(snapshot);fs.writeFileSync(path.join(snapshot,'app.js'),'vulnerable\n');const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);raw.fixtures={'secret.txt':'ghp_abcdefghijklmnopqrstuvwxyz1234567890'};const manifest:any={entries:[{path:'app.js',transformation:undefined}]};const runtime:any={id:'node',image:'runtime@sha256:'+'a'.repeat(64),platform:'linux/amd64'};expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:{observe:async()=>{throw new Error('must not execute');}},persist:false})).rejects.toMatchObject({code:'REDACTION_FAILED'});}); + test('secret-bearing fixtures cannot enter a replayable verification bundle',async()=>{const runDir=tmp(),snapshot=path.join(runDir,'snapshot');fs.mkdirSync(snapshot);fs.writeFileSync(path.join(snapshot,'app.js'),'vulnerable\n');const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);raw.fixtures={'secret.txt':CREDENTIAL_CANARY};const manifest:any={entries:[{path:'app.js',transformation:undefined}]};const runtime:any={id:'node',image:'runtime@sha256:'+'a'.repeat(64),platform:'linux/amd64'};expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:{observe:async()=>{throw new Error('must not execute');}},persist:false})).rejects.toMatchObject({code:'REDACTION_FAILED'});}); test('declared patches require exact pristine preimages',()=>{const root=tmp(),before=path.join(root,'before'),after=path.join(root,'after');fs.mkdirSync(before);fs.writeFileSync(path.join(before,'app.js'),'changed\n');const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);expect(()=>preparePatchedSource(before,after,validateVerificationRequest(raw))).toThrow('preimage');}); test('helper derives dependency and configuration patch effects',()=>{expect(fileEffect('package.json')).toBe('dependency');expect(fileEffect('apps/x/pyproject.toml')).toBe('dependency');expect(fileEffect('config/environments/test.rb')).toBe('configuration');const root=tmp(),before=path.join(root,'before'),after=path.join(root,'after');fs.mkdirSync(before);fs.writeFileSync(path.join(before,'package.json'),'{}');const raw:any=request('vulnerable\n','fixed\n');raw.changes=[{path:'package.json',beforeSha256:sha256('{}'),after:'{"dependencies":{}}',effect:'configuration'}];raw.boundaryFiles=['package.json'];raw.review.reviewedPatchHash=patchHash(raw);expect(()=>preparePatchedSource(before,after,validateVerificationRequest(raw))).toThrow('must be declared as dependency');}); test('verification evidence stays outside both source trees and temporary copies are deleted on errors',async()=>{const runDir=tmp(),snapshot=path.join(runDir,'snapshot');fs.mkdirSync(snapshot);fs.writeFileSync(path.join(snapshot,'app.js'),'vulnerable\n',{mode:0o755});const testBody='test("ok",()=>{})\n';fs.writeFileSync(path.join(snapshot,'app.test.js'),testBody);const packageJson='{"scripts":{"test":"node --test"}}';fs.writeFileSync(path.join(snapshot,'package.json'),packageJson);const raw:any=request('vulnerable\n','fixed\n');raw.findingId='1abcdefabcdefabcdefabcdefabcdefa';raw.existingTests=[{executable:'/usr/local/bin/node',args:['--test','--test-reporter=tap','./app.test.js']}];raw.testFiles=['app.test.js'];raw.changes.push({path:'security/policy.js',beforeSha256:null,after:'export const enforced = true\n',effect:'source'});raw.review.reviewedPatchHash=patchHash(raw);const manifest:any={version:3,createdAt:'2026-01-01T00:00:00Z',expiresAt:'2026-01-08T00:00:00Z',root:'/repo',headCommit:'a'.repeat(40),originalHash:'b'.repeat(64),executionHash:treeHash(snapshot),entries:[{path:'app.js',originalHash:sha256('vulnerable\n'),executionHash:sha256('vulnerable\n'),bytes:11,mode:0o755},{path:'app.test.js',originalHash:sha256(testBody),executionHash:sha256(testBody),bytes:Buffer.byteLength(testBody),mode:0o600},{path:'package.json',originalHash:sha256(packageJson),executionHash:sha256(packageJson),bytes:packageJson.length,mode:0o600}]};const runtime:any={id:'node',stack:'node',image:'runtime@sha256:'+'a'.repeat(64),platform:'linux/amd64'};const executor:any={observe:async(source:string,phase:string,_request:any,_runtime:any,_verifier:any,work:string)=>{expect(work.startsWith(source+path.sep)).toBe(false);fs.mkdirSync(path.join(work,phase),{recursive:true});fs.writeFileSync(path.join(work,phase,'policy.json'),'evidence');return{booted:true,legitimate:true,security:phase==='before'?'intended_failure':'pass',existingTests:true,output:'ok',inputHash:''};}};let unattested:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor,persist:false});}catch(error){unattested=error;}expect(unattested).toMatchObject({code:'PREREQUISITE',attempt:{artifactKind:'repair_candidate',findingId:raw.findingId,reproduction:'reproduced',repair:'proposed',bundleIssued:false,testCompletionAssurance:'self_reported'}});expect(unattested.message).toContain('out-of-process external assertion witness');expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);expect(fs.statSync(path.join(snapshot,'app.js')).mode&0o777).toBe(0o755);const failing:any={observe:async()=>{throw new Error('stop');}};expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:failing,persist:false})).rejects.toThrow('stop');expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);const afterFailure:any={observe:async(_source:string,phase:string)=>{if(phase==='after')throw new CsoError('TOOL_FAILED','after setup failed');return{booted:true,legitimate:true,security:'intended_failure',existingTests:true,output:'reproduced safely',inputHash:''};}};let failed:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:afterFailure});}catch(error){failed=error;}expect(failed).toMatchObject({code:'TOOL_FAILED',attempt:{findingId:raw.findingId,reproduction:'reproduced',repair:'failed',bundleIssued:false}});const attempt=failed.attempt,artifact=JSON.parse(fs.readFileSync(path.join(runDir,'verification-attempts',`${attempt.id}.json`),'utf8'));expect(artifact).toMatchObject({id:attempt.id,findingId:raw.findingId,reproduction:'reproduced',repair:'failed',bundleIssued:false});expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);const assertionFailure:any={observe:async(_source:string,phase:string)=>({booted:true,legitimate:true,security:'intended_failure',existingTests:true,output:phase==='before'?'reproduced safely':'repair remained vulnerable',inputHash:''})};let notRepaired:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:assertionFailure});}catch(error){notRepaired=error;}expect(notRepaired).toMatchObject({code:'ASSERTION_FAILED',attempt:{reproduction:'reproduced',repair:'failed',bundleIssued:false,after:{security:'intended_failure'}}});expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification-attempts'))).toHaveLength(2);const brokenStartup:any={observe:async()=>({booted:false,legitimate:false,security:'pass',existingTests:false,output:'application never started',inputHash:''})};let blocked:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:brokenStartup});}catch(error){blocked=error;}expect(blocked).toMatchObject({attempt:{reproduction:'blocked',repair:'failed',bundleIssued:false}});expect(fs.readdirSync(path.join(runDir,'verification-attempts'))).toHaveLength(3);fs.writeFileSync(path.join(snapshot,'unmanifested.txt'),'changed after snapshot identity\n');let executed=false;const mustNotExecute:any={observe:async()=>{executed=true;return{booted:true,legitimate:true,security:'pass',existingTests:true,output:'',inputHash:''};}};await expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:mustNotExecute,persist:false})).rejects.toMatchObject({code:'INCOMPATIBLE_INPUT'});expect(executed).toBe(false);}); diff --git a/test/cso-distribution.test.ts b/test/cso-distribution.test.ts index 40a5a7256..47d2825c5 100644 --- a/test/cso-distribution.test.ts +++ b/test/cso-distribution.test.ts @@ -10,6 +10,7 @@ const ROOT = resolve(import.meta.dir, '..'); const temps: string[] = []; afterEach(() => { for (const dir of temps.splice(0)) rmSync(dir, { recursive: true, force: true }); }); const quote = (s: string) => "'" + s.replaceAll("'", "'\\''") + "'"; +const CREDENTIAL_IMAGE = ['https://user:', 'pass@example.test/node'].join(''); function buildFixture() { const dir = mkdtempSync(join(tmpdir(), 'gstack-cso-build-')); temps.push(dir); @@ -257,7 +258,7 @@ describe('CSO runtime staging gates', () => { expect(() => imageBuildMatrix(inputs)).toThrow('INCOMPLETE_STACK_MATRIX'); }); - test.each(['node:latest', 'docker.io/library/node:24', 'https://user:pass@example.test/node', 'docker.io/library/node@sha256:bad', 'docker.io/library/node@sha256:' + 'A'.repeat(64), 'docker.io/library/node@sha256:' + 'a'.repeat(64) + '\nBAD=value'])('untrusted base input %s cannot enter the workflow matrix', image => { + test.each(['node:latest', 'docker.io/library/node:24', CREDENTIAL_IMAGE, 'docker.io/library/node@sha256:bad', 'docker.io/library/node@sha256:' + 'A'.repeat(64), 'docker.io/library/node@sha256:' + 'a'.repeat(64) + '\nBAD=value'])('untrusted base input %s cannot enter the workflow matrix', image => { const inputs = reviewedInputs(); inputs.profiles[0].baseImages['linux/amd64'] = image; expect(() => imageBuildMatrix(inputs)).toThrow('UNPINNED_BASE_IMAGE'); }); diff --git a/test/cso-preparation.test.ts b/test/cso-preparation.test.ts index ad6182926..1f663b970 100644 --- a/test/cso-preparation.test.ts +++ b/test/cso-preparation.test.ts @@ -9,6 +9,7 @@ import { completeRuntimeCatalogFixture, qualifiedRuntimeFixture } from './helper const roots: string[] = []; const hash = 'a'.repeat(64); const sri = `sha512-${Buffer.alloc(64, 1).toString('base64')}`; +const CREDENTIAL_ARCHIVE_URL = ['https://user:', 'secret@registry.npmjs.org/a.tgz'].join(''); function fixture(files: Record): string { const root = mkdtempSync(join(tmpdir(), 'cso-preparation-')); roots.push(root); for (const [path, contents] of Object.entries(files)) { @@ -58,7 +59,7 @@ describe('CSO inert Node preparation', () => { const plan = inspectPreparation(fixture(nodeFiles(1))); expect(plan.prerequisites[0].code).toBe('UNSUPPORTED_LOCK'); expect(plan.acquisition).toEqual([]); expect(plan.metadata).toEqual([]); }); - for (const url of ['https://user:secret@registry.npmjs.org/a.tgz', 'http://registry.npmjs.org/a.tgz', 'https://registry.npmjs.org.evil.invalid/a.tgz', 'https://127.0.0.1/a.tgz', 'git+ssh://github.com/a/b', 'https://registry.npmjs.org/a.tgz?token=secret']) test(`rejects non-public archive ${url.split('@').at(-1)}`, () => { + for (const url of [CREDENTIAL_ARCHIVE_URL, 'http://registry.npmjs.org/a.tgz', 'https://registry.npmjs.org.evil.invalid/a.tgz', 'https://127.0.0.1/a.tgz', 'git+ssh://github.com/a/b', 'https://registry.npmjs.org/a.tgz?token=secret']) test(`rejects non-public archive ${url.split('@').at(-1)}`, () => { const files = nodeFiles(); files['package-lock.json'].packages['node_modules/cookie'].resolved = url; const plan = inspectPreparation(fixture(files)); expect(plan.status).toBe('prerequisites'); expect(plan.acquisition).toEqual([]); }); diff --git a/test/cso-scanners.test.ts b/test/cso-scanners.test.ts index 3e22afe23..8d98f2d41 100644 --- a/test/cso-scanners.test.ts +++ b/test/cso-scanners.test.ts @@ -94,7 +94,7 @@ describe('CSO scanner execution plans', () => { expect(() => scannerPlans({ snapshotRoot: '/src', offline: true, schemaPath: 'https://example.test/api.json' })).toThrow(); }); - test.each(['http://example.com', 'http://localhost:3000', 'http://169.254.169.254/', 'http://127.1/', 'http://2130706433/', 'http://0x7f000001/', 'http://127.0.0.1.evil.test/', 'http://user:pass@127.0.0.1/', 'file:///tmp/app', 'http://[::ffff:127.0.0.1]/', 'http://127.0.0.1/?x=secret'])('rejects non-canonical or credential-bearing target %s', url => { + test.each(['http://example.com', 'http://localhost:3000', 'http://169.254.169.254/', 'http://127.1/', 'http://2130706433/', 'http://0x7f000001/', 'http://127.0.0.1.evil.test/', ['http://user:', 'pass@127.0.0.1/'].join(''), 'file:///tmp/app', 'http://[::ffff:127.0.0.1]/', 'http://127.0.0.1/?x=secret'])('rejects non-canonical or credential-bearing target %s', url => { expect(() => validateScannerBaseUrl(url)).toThrow(); }); @@ -228,7 +228,7 @@ describe('CSO scanner failure and hostile-input handling', () => { }); test('marker-only private keys withhold the entire decoded document', () => { - const data = [{ RuleID: 'key', Description: '-----BEGIN PRIVATE KEY-----\nsecretbody\n-----END PRIVATE KEY-----', File: 'config' }]; + const data = [{ RuleID: 'key', Description: ['-----BEGIN ', 'PRIVATE KEY-----\nsecretbody\n-----END ', 'PRIVATE KEY-----'].join(''), File: 'config' }]; const result = parse('gitleaks', data, 10); expect(result.status).toBe('not_assessed'); expect(result.gaps[0].code).toBe('REDACTION_FAILED'); diff --git a/test/cso-snapshot-state.test.ts b/test/cso-snapshot-state.test.ts index ac05fa72c..ee88ffc3f 100644 --- a/test/cso-snapshot-state.test.ts +++ b/test/cso-snapshot-state.test.ts @@ -24,7 +24,7 @@ describe('CSO dirty snapshot boundary',()=>{ test('does not invoke configured clean filters, hooks, fsmonitor, or PATH shims',async()=>{const p=repo(),marker=path.join(p,'marker');fs.writeFileSync(path.join(p,'.gitattributes'),'*.txt filter=hostile\n');git(p,'config','filter.hostile.clean',`/bin/sh -c 'touch ${marker}; cat'`);git(p,'config','core.fsmonitor',`/bin/sh -c 'touch ${marker}'`);const fake=path.join(p,'bin');fs.mkdirSync(fake);fs.writeFileSync(path.join(fake,'git'),`#!/bin/sh\ntouch '${marker}'\nexit 99\n`,{mode:0o755});const old=process.env.PATH;process.env.PATH=`${fake}:${old}`;try{await capture(p,newRun(p).dir,'HEAD');}finally{process.env.PATH=old;}expect(fs.existsSync(marker)).toBe(false);}); test('rejects symlinks and hard links as execution inputs',async()=>{const p=repo();fs.symlinkSync('/etc/passwd',path.join(p,'escape'));expect(capture(p,newRun(p).dir)).rejects.toThrow();fs.unlinkSync(path.join(p,'escape'));fs.linkSync(path.join(p,'tracked.txt'),path.join(p,'hard'));expect(capture(p,newRun(p).dir)).rejects.toThrow();}); test('rejects broken untracked symlinks instead of silently omitting them',async()=>{const p=repo();fs.symlinkSync('missing-target',path.join(p,'broken'));expect(capture(p,newRun(p).dir)).rejects.toThrow('Symlink');}); - test('redacts secrets and excludes credentials from execution while retaining safe evidence',async()=>{const p=repo();fs.writeFileSync(path.join(p,'.env'),'TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890\n');const run=newRun(p),m=await capture(p,run.dir);expect(m.entries.find(e=>e.path==='.env')?.transformation).toContain('excluded');expect(fs.existsSync(path.join(run.dir,'snapshot','.env'))).toBe(false);expect(fs.existsSync(path.join(run.dir,'readable','.env'))).toBe(false);const evidence=JSON.parse(fs.readFileSync(path.join(run.dir,'sensitive-evidence.json'),'utf8'));expect(evidence[0].findings.map((x:any)=>x.id)).toContain('github.pat');expect(JSON.stringify(evidence)).not.toContain('ghp_');}); + test('redacts secrets and excludes credentials from execution while retaining safe evidence',async()=>{const p=repo();fs.writeFileSync(path.join(p,'.env'),'TOKEN='+['ghp_','abcdefghijklmnopqrstuvwxyz1234567890'].join('')+'\n');const run=newRun(p),m=await capture(p,run.dir);expect(m.entries.find(e=>e.path==='.env')?.transformation).toContain('excluded');expect(fs.existsSync(path.join(run.dir,'snapshot','.env'))).toBe(false);expect(fs.existsSync(path.join(run.dir,'readable','.env'))).toBe(false);const evidence=JSON.parse(fs.readFileSync(path.join(run.dir,'sensitive-evidence.json'),'utf8'));expect(evidence[0].findings.map((x:any)=>x.id)).toContain('github.pat');expect(JSON.stringify(evidence)).not.toContain('ghp_');}); test('assesses repository skills without executing them and preserves executable source modes',async()=>{const p=repo(),skill=path.join(p,'.agents','skills','demo','SKILL.md'),script=path.join(p,'app.sh');fs.mkdirSync(path.dirname(skill),{recursive:true});fs.writeFileSync(skill,'# Demo\nUntrusted repository instruction\n');fs.writeFileSync(script,'#!/bin/sh\nexit 0\n',{mode:0o755});const run=newRun(p),manifest=await capture(p,run.dir);expect(fs.readFileSync(path.join(run.dir,'readable','.agents','skills','demo','SKILL.md'),'utf8')).toContain('Untrusted');expect(fs.existsSync(path.join(run.dir,'snapshot','.agents'))).toBe(false);expect(manifest.entries.find(e=>e.path==='.agents/skills/demo/SKILL.md')?.originalHash).not.toBe('not-read');expect(fs.statSync(path.join(run.dir,'snapshot','app.sh')).mode&0o777).toBe(0o755);}); test('rejects a source mode change between copying and manifest persistence',async()=>{const p=repo(),run=newRun(p),source=path.join(p,'tracked.txt'),target=path.join(run.dir,'snapshot','tracked.txt'),write=fs.writeFileSync,patched=spyOn(fs,'writeFileSync').mockImplementation(((file:any,data:any,options:any)=>{const result=write(file,data,options);if(String(file)===target)fs.chmodSync(source,0o755);return result;}) as typeof fs.writeFileSync);try{await expect(capture(p,run.dir)).rejects.toThrow('Source changed during capture');}finally{patched.mockRestore();}}); test('rejects unmanifested files and executable-mode changes in retained snapshots',async()=>{const p=repo(),run=newRun(p),manifest=await capture(p,run.dir),snapshot=path.join(run.dir,'snapshot');assertSnapshot(run.dir,manifest);fs.writeFileSync(path.join(snapshot,'injected.js'),'malicious\n');expect(()=>assertSnapshot(run.dir,manifest)).toThrow('membership');fs.unlinkSync(path.join(snapshot,'injected.js'));fs.chmodSync(path.join(snapshot,'tracked.txt'),0o755);expect(()=>assertSnapshot(run.dir,manifest)).toThrow('changed');}); @@ -42,8 +42,8 @@ describe('private state and process output',()=>{ test('private state rejects a non-sticky group-writable ancestor',()=>{if(process.platform==='win32')return;const base=tmp();fs.chmodSync(base,0o770);expect(()=>secureDirectory(path.join(base,'private'))).toThrow('group- or world-writable ancestor');}); test('split process output is joined and redacted before return',async()=>{const dir=tmp(),r=await runProcess('/bin/sh',['-c',"printf 'ghp_abcdefghijklmnop'; printf 'qrstuvwxyz1234567890' >&2"],{cwd:dir,env:{PATH:'/usr/bin:/bin'}});expect(r.stdout+r.stderr).not.toContain('ghp_');expect(r.stdout.toLowerCase()).toContain('redacted');}); test('alternating stdout and stderr chunks cannot split a secret marker',async()=>{const dir=tmp(),r=await runProcess('/bin/sh',['-c',"printf g; sleep .02; printf hp_abcdefghijklmnop >&2; sleep .02; printf qrstuvwxyz1234567890"],{cwd:dir,env:{PATH:'/usr/bin:/bin'}});expect(r.stdout+r.stderr).not.toContain('abcdefghijklmnop');expect(r.stdout.toLowerCase()).toContain('redacted');}); - test('marker-only private keys withhold the whole payload',async()=>{const dir=tmp();expect(runProcess('/bin/sh',['-c',"printf '%s' '-----BEGIN PRIVATE KEY-----\\nraw\\n-----END PRIVATE KEY-----'"],{cwd:dir,env:{PATH:'/usr/bin:/bin'}})).rejects.toMatchObject({code:'REDACTION_FAILED'});}); - test('untrusted hash-like keys do not bypass structured redaction',()=>{const dir=tmp(),file=path.join(dir,'value.json');writeJson(file,{payloadHash:'ghp_abcdefghijklmnopqrstuvwxyz1234567890'});expect(fs.readFileSync(file,'utf8')).not.toContain('ghp_');}); + test('marker-only private keys withhold the whole payload',async()=>{const dir=tmp();expect(runProcess('/bin/sh',['-c',"printf '%s' \"$1\"",'sh',['-----BEGIN ','PRIVATE KEY-----\nraw\n-----END ','PRIVATE KEY-----'].join('')],{cwd:dir,env:{PATH:'/usr/bin:/bin'}})).rejects.toMatchObject({code:'REDACTION_FAILED'});}); + test('untrusted hash-like keys do not bypass structured redaction',()=>{const dir=tmp(),file=path.join(dir,'value.json');writeJson(file,{payloadHash:['ghp_','abcdefghijklmnopqrstuvwxyz1234567890'].join('')});expect(fs.readFileSync(file,'utf8')).not.toContain('ghp_');}); test('only schema-bound helper metadata bypasses content redaction',()=>{const token='1abcdefabcdefabcdefabcdefabcdefa',hash='a'.repeat(64),untrusted=sanitizeForJson({id:token,findingId:token,sourceHash:hash}) as any;expect(untrusted.id).not.toBe(token);expect(untrusted.findingId).not.toBe(token);const trusted=sanitizeHelperForJson({id:token,findingId:token,artifactId:token,bundleId:token,sourceHash:hash,preparedManifestHash:hash}) as any;expect(trusted).toEqual({id:token,findingId:token,artifactId:token,bundleId:token,sourceHash:hash,preparedManifestHash:hash});const dir=tmp(),file=path.join(dir,'claim.json');writeHelperJson(file,{findingId:token,outcome:'open'});expect(readJson(file)).toEqual({findingId:token,outcome:'open'});}); test('oversized report growth is rejected before replacing the last readable report',()=>{const dir=tmp(),report:any={schemaVersion:3,runId:'1'.repeat(13)+'-'+'a'.repeat(16),repoId:'b'.repeat(24),createdAt:new Date().toISOString(),deadline:new Date(Date.now()+60_000).toISOString(),status:'running',completeness:'not assessed',policy:{mode:'daily',scope:'default',diff:false,base:'origin/main',offline:true,budgetSeconds:600,maxWorkers:3,maxRepairs:3},source:{root:'/source',snapshotHash:'c'.repeat(64),originalHash:'d'.repeat(64)},application:{actors:[],assets:[],entrypoints:[],tenantBoundaries:[],sensitiveOperations:[],invariants:[]},coverage:[],findings:[],gaps:[],events:[]};saveReport(dir,report);const original=fs.readFileSync(path.join(dir,'report.json'),'utf8');report.events=Array.from({length:4000},(_,i)=>({at:new Date().toISOString(),kind:'evidence',message:`${i}:`+'x'.repeat(300)}));expect(()=>saveReport(dir,report)).toThrow('previous artifact was preserved');expect(fs.readFileSync(path.join(dir,'report.json'),'utf8')).toBe(original);expect(loadReport(dir).status).toBe('running');}); test('a fully published dead legacy owner migrates to the immutable lease protocol',()=>{const dir=tmp(),lock=path.join(dir,'.mutation-lock');fs.mkdirSync(lock);fs.writeFileSync(path.join(lock,'owner.json'),JSON.stringify({pid:2147483647,token:'old',createdAt:0,expiresAt:0}));expect(withLock(dir,()=>42)).toBe(42);expect(fs.lstatSync(lock).isFile()).toBe(true);expect(JSON.parse(fs.readFileSync(lock,'utf8')).protocol).toBe('immutable-lease-set-v3');expect(fs.readdirSync(path.join(dir,'.mutation-lock-leases'))).toEqual([]);}); diff --git a/test/skill-e2e-cso.test.ts b/test/skill-e2e-cso.test.ts index e588ef3f8..ec286bf3e 100644 --- a/test/skill-e2e-cso.test.ts +++ b/test/skill-e2e-cso.test.ts @@ -12,7 +12,7 @@ const evalCollector = createEvalCollector('e2e-cso'); afterAll(() => finalizeEvalCollector(evalCollector)); // Synthetic credential-shaped canary: its bytes must never appear in agent output or saved reports. -const CANARY = 'ghp_f7K3mV9nT2qR8sL4xP6dW0bH5jY1cZ3aF9uE'; +const CANARY = ['ghp_', 'f7K3mV9nT2qR8sL4xP6dW0bH5jY1cZ3aF9uE'].join(''); interface Fixture { repo: string; state: string; original: Map; } function git(repo: string, ...args: string[]): string { const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 5000 });