mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
v1.91.22.0 feat(cso): reviewed, attested scanner build inputs + fixes so scanner qualification can pass (#3035)
* v1.91.22.0 feat(cso): reviewed, attested scanner build inputs + scanner qualification fixes * Regenerate agents digest for v1.91.22.0
This commit is contained in:
1 parent
2db0b3adc8
commit
f97aefe654
14 files changed
+367
-26
No files matched your search
@@ -44,8 +44,8 @@ function write(name: string, body: string): void {
|
||||
}
|
||||
function fixture(): void {
|
||||
write('app.js', 'export const scannerQualification = true;\n');
|
||||
write('package.json', '{"name":"cso-scanner-qualification","version":"1.0.0"}\n');
|
||||
write('package-lock.json', '{"name":"cso-scanner-qualification","version":"1.0.0","lockfileVersion":3,"packages":{"":{"name":"cso-scanner-qualification","version":"1.0.0"}}}\n');
|
||||
write('package.json', '{"name":"cso-scanner-qualification","version":"1.0.0","dependencies":{"lodash":"4.17.20"}}\n');
|
||||
write('package-lock.json', '{"name":"cso-scanner-qualification","version":"1.0.0","lockfileVersion":3,"packages":{"":{"name":"cso-scanner-qualification","version":"1.0.0","dependencies":{"lodash":"4.17.20"}},"node_modules/lodash":{"version":"4.17.20","resolved":"https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz","integrity":"sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="}}}\n');
|
||||
write('.github/workflows/qualification.yml', 'name: qualification\non: [push]\npermissions: {}\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo qualification\n');
|
||||
write('server.py', [
|
||||
'from http.server import BaseHTTPRequestHandler, HTTPServer',
|
||||
@@ -80,7 +80,7 @@ beforeAll(async () => {
|
||||
if (staged.platform !== platform) throw new Error(`Scanner Docker qualification requires native ${platform}`);
|
||||
if (staged.scanner === 'schemathesis' && (typeof staged.applicationExecutable !== 'string' || !staged.applicationExecutable.startsWith('/'))) throw new Error('Schemathesis qualification requires its reviewed fixture runtime executable');
|
||||
root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cso-scanner-docker-'));
|
||||
source = secureDirectory(path.join(root, 'source')); fixture();
|
||||
source = secureDirectory(path.join(root, 'snapshot')); fixture();
|
||||
process.env.GSTACK_HOME = path.join(root, 'state');
|
||||
watchdog = path.resolve(import.meta.dir, '../bin/gstack-cso-watchdog');
|
||||
if (!fs.existsSync(watchdog)) throw new Error('Scanner Docker qualification requires the compiled watchdog');
|
||||
|
||||
@@ -129,9 +129,10 @@ describe('CSO scanner qualification workflow', () => {
|
||||
expect(raw.slice(publicPromotion, sourcePromotion)).toContain('--remove-after');
|
||||
for (const job of Object.values(workflow.jobs) as any[]) for (const step of job.steps) if (step.uses) expect(step.uses).toMatch(/@[a-f0-9]{40}$/);
|
||||
});
|
||||
test('commits no invented release input and normalizes scanner images to the constrained helper contract', () => {
|
||||
test('commits either the empty placeholder or a complete reviewed matrix, and normalizes scanner images to the constrained helper contract', () => {
|
||||
const inputs = JSON.parse(fs.readFileSync(path.join(ROOT, 'lib/cso/scanner-images/build-inputs.json'), 'utf8'));
|
||||
expect(inputs).toMatchObject({ state: 'pending', profiles: [], sbomGenerator: null });
|
||||
if (inputs.state === 'pending') expect(inputs).toMatchObject({ profiles: [], sbomGenerator: null });
|
||||
else expect(scannerBuildMatrix(inputs).include).toHaveLength(12);
|
||||
const dockerfile = fs.readFileSync(path.join(ROOT, 'lib/cso/scanner-images/scanner.Dockerfile'), 'utf8');
|
||||
for (const value of ['/opt/cso/entrypoint', '/opt/cso/bin/scanner', 'USER 10001:10001', 'test -x /bin/sleep', 'test -x /bin/cat']) expect(dockerfile).toContain(value);
|
||||
expect(fs.readFileSync(path.join(ROOT, 'lib/cso/scanner-executor.ts'), 'utf8')).toContain("capture(['/bin/cat', plan.outputPath])");
|
||||
|
||||
@@ -168,6 +168,21 @@ describe('CSO scanner candidate normalization', () => {
|
||||
expect(result.candidates[0]).not.toHaveProperty('reproduced');
|
||||
});
|
||||
|
||||
test('Schemathesis time-budget completion counts only when every selected operation ran', () => {
|
||||
const complete = parse('schemathesis', { schemathesis_version: '4.29.1', complete: true, stop_reason: 'max_time', operations: { selected: 1, tested: 1, errored: 0, skipped: 0 }, errors: [], failures: [] }, 0);
|
||||
expect(complete.status).toBe('complete');
|
||||
expect(complete.gaps).toEqual([]);
|
||||
const untested = parse('schemathesis', { schemathesis_version: '4.29.1', complete: true, stop_reason: 'max_time', operations: { selected: 2, tested: 1, errored: 0, skipped: 0 }, errors: [], failures: [] }, 0);
|
||||
expect(untested.status).toBe('partial');
|
||||
const interrupted = parse('schemathesis', { schemathesis_version: '4.29.1', complete: false, stop_reason: 'max_time', operations: { selected: 1, tested: 1, errored: 0, skipped: 0 }, errors: [], failures: [] }, 0);
|
||||
expect(interrupted.status).toBe('partial');
|
||||
});
|
||||
|
||||
test('Schemathesis time budget leaves room inside the execution deadline', () => {
|
||||
const plan = scannerPlans({ snapshotRoot: '/source', offline: true, selected: ['schemathesis'], deadlineSeconds: 120, schemaPath: '/policy/openapi.json', baseUrl: 'http://127.0.0.1:3000/', operationIds: ['listItems'] })[0];
|
||||
expect(plan.args[plan.args.indexOf('--max-time') + 1]).toBe('90');
|
||||
});
|
||||
|
||||
test('startup failure and zero exercised operations remain not covered', () => {
|
||||
const result = parse('schemathesis', { schemathesis_version: '4.0.0', complete: false, stop_reason: 'interrupted', operations: null, errors: [{ title: 'Schema load failed' }], failures: [] }, 1);
|
||||
expect(result.status).toBe('partial');
|
||||
|
||||
@@ -63,6 +63,9 @@ 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('splice redaction keeps structured scanner output and marks only located spans',async()=>{const dir=tmp(),r=await runProcess('/bin/sh',['-c',"printf '{\"url\":\"https://security.netapp.com/advisory/ntap-20210312-0006\",\"ok\":true}'; printf 'log line' >&2"],{cwd:dir,env:{PATH:'/usr/bin:/bin'},redaction:'splice'});expect(r.stdout).not.toContain('20210312-0006');expect(JSON.parse(r.stdout).ok).toBe(true);expect(r.stdout).toContain('<REDACTED-');expect(r.stderr).toBe('log line');});
|
||||
test('splice redaction still withholds a secret split across channels',async()=>{const dir=tmp();for(const script of ["printf 'ghp_abcdefghijklmnop'; printf 'qrstuvwxyz1234567890' >&2","printf g; sleep .02; printf hp_abcdefghijklmnop >&2; sleep .02; printf qrstuvwxyz1234567890"]){const r=await runProcess('/bin/sh',['-c',script],{cwd:dir,env:{PATH:'/usr/bin:/bin'},redaction:'splice'});expect(r.stdout+r.stderr).not.toContain('abcdefghijklmnop');expect(r.stdout).toBe('[sensitive process output redacted]');}});
|
||||
test('splice redaction replaces a whole-channel secret instead of leaking it',async()=>{const dir=tmp(),token=['ghp','abcdefghijklmnopqrstuvwxyz1234567890'].join('_'),r=await runProcess('/bin/sh',['-c',`printf 'token=${token} done'`],{cwd:dir,env:{PATH:'/usr/bin:/bin'},redaction:'splice'});expect(r.stdout).not.toContain('abcdefghijklmnop');expect(r.stdout).toContain('done');});
|
||||
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,reviewArtifactId:token,bundleId:token,sourceHash:hash,preparedManifestHash:hash}) as any;expect(trusted).toEqual({id:token,findingId:token,artifactId:token,reviewArtifactId: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'});});
|
||||
|
||||
Reference in new issue
Block a user