mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 09:55:29 +02:00
v1.87.0.0 feat: add verified CSO audits and replayable repair bundles (#2852)
* 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>
This commit is contained in:
co-authored by
OpenAI Codex
parent
9f81911136
commit
4a3c6a8a3c
+220
-271
@@ -1,289 +1,238 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { CAPTURE_MS, CAPTURE_LONG_MS } from './helpers/eval-budgets';
|
||||
import { test, expect, afterAll } from 'bun:test';
|
||||
import { CAPTURE_LONG_MS } from './helpers/eval-budgets';
|
||||
import { runSkillTest } from './helpers/session-runner';
|
||||
import {
|
||||
ROOT, runId, evalsEnabled,
|
||||
describeIfSelected, logCost, recordE2E,
|
||||
createEvalCollector, finalizeEvalCollector,
|
||||
} from './helpers/e2e-helpers';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { ROOT, runId, describeIfSelected, logCost, recordE2E, createEvalCollector, finalizeEvalCollector } from './helpers/e2e-helpers';
|
||||
import { validateCoverage, validateFinding, completeness, type RunReportV3 } from '../lib/cso/contracts';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
const evalCollector = createEvalCollector('e2e-cso');
|
||||
// runSkillTest can drain stderr for 5s after its unchanged CLI deadline.
|
||||
// Let cleanup and failure recording finish before Bun starts a retry.
|
||||
const CAPTURE_CLEANUP_MS = 6_000;
|
||||
let captureSequence = 0;
|
||||
afterAll(() => finalizeEvalCollector(evalCollector));
|
||||
|
||||
afterAll(() => {
|
||||
finalizeEvalCollector(evalCollector);
|
||||
});
|
||||
|
||||
// --- CSO v2 E2E Tests ---
|
||||
|
||||
describeIfSelected('CSO v2 — full audit', ['cso-full-audit'], () => {
|
||||
let csoDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
csoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cso-'));
|
||||
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
spawnSync(cmd, args, { cwd: csoDir, stdio: 'pipe', timeout: 5000 });
|
||||
|
||||
run('git', ['init', '-b', 'main']);
|
||||
run('git', ['config', 'user.email', 'test@test.com']);
|
||||
run('git', ['config', 'user.name', 'Test']);
|
||||
|
||||
// Create a minimal app with a planted vulnerability
|
||||
fs.writeFileSync(path.join(csoDir, 'package.json'), JSON.stringify({
|
||||
name: 'cso-test-app',
|
||||
version: '1.0.0',
|
||||
dependencies: { express: '4.18.0' },
|
||||
}, null, 2));
|
||||
|
||||
// Planted vuln: hardcoded API key
|
||||
fs.writeFileSync(path.join(csoDir, 'server.ts'), `
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
const API_KEY = "sk-1234567890abcdef1234567890abcdef";
|
||||
app.get('/api/data', (req, res) => {
|
||||
const id = req.query.id;
|
||||
res.json({ data: \`result for \${id}\` });
|
||||
});
|
||||
app.listen(3000);
|
||||
`);
|
||||
|
||||
// Planted vuln: .env tracked by git
|
||||
fs.writeFileSync(path.join(csoDir, '.env'), 'DATABASE_URL=postgres://admin:secretpass@prod.db.example.com:5432/myapp\n');
|
||||
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'initial']);
|
||||
// Synthetic credential-shaped canary: its bytes must never appear in agent output or saved reports.
|
||||
const CANARY = ['ghp_', 'f7K3mV9nT2qR8sL4xP6dW0bH5jY1cZ3aF9uE'].join('');
|
||||
interface Fixture { repo: string; state: string; original: Map<string, string>; }
|
||||
function git(repo: string, ...args: string[]): string {
|
||||
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 5000 });
|
||||
if (result.status !== 0) throw new Error(`Fixture git ${args[0]} failed: ${result.stderr}`);
|
||||
return result.stdout;
|
||||
}
|
||||
function fixture(files: Record<string, string>, diff?: Record<string, string>): Fixture {
|
||||
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cso-v3-'));
|
||||
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cso-state-'));
|
||||
git(repo, 'init', '-b', 'main');
|
||||
git(repo, 'config', 'user.email', 'test@example.invalid');
|
||||
git(repo, 'config', 'user.name', 'CSO Test');
|
||||
const write = (inputs: Record<string, string>) => {
|
||||
for (const [name, content] of Object.entries(inputs)) {
|
||||
fs.mkdirSync(path.dirname(path.join(repo, name)), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo, name), content);
|
||||
}
|
||||
git(repo, 'add', '.');
|
||||
git(repo, 'commit', '-m', 'CSO fixture');
|
||||
};
|
||||
write(files);
|
||||
if (diff) { git(repo, 'checkout', '-b', 'fixture-change'); write(diff); }
|
||||
return { repo, state, original: new Map(Object.entries({ ...files, ...diff })) };
|
||||
}
|
||||
function removeFixture(f: Fixture): void {
|
||||
fs.rmSync(f.repo, { recursive: true, force: true });
|
||||
fs.rmSync(f.state, { recursive: true, force: true });
|
||||
}
|
||||
async function withFixture<T>(files:Record<string,string>,diff:Record<string,string>|undefined,run:(fixture:Fixture)=>Promise<T>):Promise<T>{const f=fixture(files,diff);try{return await run(f);}finally{removeFixture(f);}}
|
||||
function reportsUnder(dir: string): string[] {
|
||||
// Bounded to the exact private namespace; no report in source or old v2 namespace is accepted.
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||||
const child = path.join(dir, entry.name);
|
||||
return entry.isDirectory() ? reportsUnder(child) : entry.name === 'report.json' ? [child] : [];
|
||||
});
|
||||
}
|
||||
function requireReport(f: Fixture): RunReportV3 {
|
||||
const reports = reportsUnder(path.join(f.state, 'security', 'cso'));
|
||||
expect(reports).toHaveLength(1); // A mandatory assertion: absent reports used to silently pass.
|
||||
const raw = fs.readFileSync(reports[0], 'utf8');
|
||||
expect(raw).not.toContain(CANARY);
|
||||
expect(raw).not.toContain(fs.realpathSync(f.repo));
|
||||
const report: RunReportV3 = JSON.parse(raw);
|
||||
expect(report.schemaVersion).toBe(3);
|
||||
expect(report.status).toBe('finished');
|
||||
expect(report.source.root).toBe('<REDACTED-internal.user_path>');
|
||||
expect(report.policy.mode).toBe('daily');
|
||||
expect(report.policy.offline).toBe(true);
|
||||
expect(report.coverage.length).toBeGreaterThan(0);
|
||||
report.coverage.forEach(validateCoverage);
|
||||
expect(report.completeness).toBe(completeness(report));
|
||||
for (const finding of report.findings) {
|
||||
const { id, fingerprint, reproduction, repair, closure, verificationId, reproductionAttemptId, ...submitted } = finding;
|
||||
validateFinding(submitted);
|
||||
expect(id).toBe(fingerprint);
|
||||
expect(id).toMatch(/^[a-f0-9]{32}$/);
|
||||
expect(finding.evidence).toBe('supported');
|
||||
expect(reproduction).toBe('not_attempted');
|
||||
expect(repair).toBe('not_attempted');
|
||||
expect(closure).toBe('open');
|
||||
expect(verificationId).toBeUndefined();
|
||||
expect(reproductionAttemptId).toBeUndefined();
|
||||
}
|
||||
const markdown = fs.readFileSync(path.join(path.dirname(reports[0]), 'report.md'), 'utf8');
|
||||
expect(markdown).toMatch(/^(complete|partial|not assessed) — /);
|
||||
expect(markdown).not.toContain(CANARY);
|
||||
if (process.platform !== 'win32') expect(fs.statSync(reports[0]).mode & 0o777).toBe(0o600);
|
||||
for (const [name, content] of f.original) expect(fs.readFileSync(path.join(f.repo, name), 'utf8')).toBe(content);
|
||||
expect(git(f.repo, 'status', '--porcelain=v1', '--untracked-files=all')).toBe('');
|
||||
return report;
|
||||
}
|
||||
function prompt(f: Fixture, flags: string): string {
|
||||
return `Read ${path.join(ROOT, 'cso/SKILL.md')} and perform /cso ${flags} --offline --base main on ${f.repo}.
|
||||
The trusted installed helper for this evaluation is ${path.join(ROOT, 'bin', process.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher')}.
|
||||
GSTACK_HOME is already set to the private evaluation state directory.
|
||||
Complete the helper-owned report even when coverage is partial. Do not use AskUserQuestion or request new scope.
|
||||
Use one sequential challenge (independent agent unavailable for this evaluation); do not spawn agents.
|
||||
This is a static audit: do not execute the application, test provider credentials, or install dependencies.
|
||||
Do not read or execute files outside this repository except the trusted skill/helper and its private run artifacts.`;
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(csoDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
const PACKAGE = JSON.stringify({ name: 'cso-v3-evaluation', version: '1.0.0', private: true });
|
||||
|
||||
test('/cso finds planted vulnerabilities', async () => {
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-full-audit',
|
||||
runId: `cso-full-audit-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: `Read the file ${path.join(ROOT, 'cso', 'SKILL.md')} for the CSO skill instructions.
|
||||
|
||||
Run /cso on this repo (full daily audit, no flags).
|
||||
describeIfSelected('CSO v3 — full audit', ['cso-full-audit'], () => {
|
||||
test('/cso persists supported tenant-boundary findings with redacted evidence', async () => {
|
||||
await withFixture({
|
||||
'package.json': PACKAGE,
|
||||
'.env': `PROVIDER_TOKEN=${CANARY}\n`,
|
||||
'server.js': `const http = require('node:http');
|
||||
const sessions = new Map([['alice-token', { tenant: 'alice' }], ['bob-token', { tenant: 'bob' }]]);
|
||||
const invoices = [{ id: 'invoice-a', tenant: 'alice', total: 42 }, { id: 'invoice-b', tenant: 'bob', total: 950 }];
|
||||
http.createServer((req, res) => {
|
||||
const user = sessions.get((req.headers.authorization || '').replace('Bearer ', ''));
|
||||
if (!user) { res.writeHead(401); return res.end('unauthorized'); }
|
||||
const id = req.url.split('/').pop();
|
||||
const invoice = invoices.find(item => item.id === id);
|
||||
if (!invoice) { res.writeHead(404); return res.end('not found'); }
|
||||
res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(invoice));
|
||||
}).listen(3000);
|
||||
`,
|
||||
},undefined,async f=>{
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-full-audit',
|
||||
runId: `cso-full-audit-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: prompt(f, ''), workingDirectory: f.repo,
|
||||
env: { GSTACK_HOME: f.state }, maxTurns: 40, allowedTools: ['Bash', 'Read', 'Write', 'Grep', 'Glob'], timeout: CAPTURE_LONG_MS,
|
||||
});
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
expect(result.output).not.toContain(CANARY);
|
||||
expect(JSON.stringify(result.transcript)).not.toContain(CANARY);
|
||||
// Dedicated carve behavioral guard: a real audit must actually load its domain section.
|
||||
expect(result.toolCalls.some(call => /Read|Bash/.test(call.tool)
|
||||
&& JSON.stringify(call.input).includes('sections/audit-phases.md'))).toBe(true);
|
||||
const report = requireReport(f);
|
||||
const tenant = report.findings.filter(item => item.location.path === 'server.js' && /tenant|owner|authoriz|access control/i.test(`${item.rootCause} ${item.impact}`));
|
||||
expect(tenant.length).toBeGreaterThan(0);
|
||||
expect(tenant.every(item => item.trace.length > 0 && item.challenge.controls.length > 0)).toBe(true);
|
||||
passed = result.browseErrors.length === 0;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-full-audit', 'e2e-cso', result, { passed });
|
||||
}
|
||||
});
|
||||
}, CAPTURE_LONG_MS + CAPTURE_CLEANUP_MS);
|
||||
});
|
||||
|
||||
IMPORTANT:
|
||||
- Do NOT use AskUserQuestion — skip any interactive prompts.
|
||||
- Focus on finding the planted vulnerabilities in this small repo.
|
||||
- Produce the SECURITY FINDINGS table.
|
||||
- Save the report to .gstack/security-reports/.`,
|
||||
workingDirectory: csoDir,
|
||||
maxTurns: 30,
|
||||
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent'],
|
||||
timeout: CAPTURE_MS,
|
||||
describeIfSelected('CSO v3 — diff mode', ['cso-diff-mode'], () => {
|
||||
test('/cso --diff records its base and investigates changed security paths', async () => {
|
||||
await withFixture({ 'package.json': PACKAGE, 'app.js': 'console.log("fixture baseline");\n' }, {
|
||||
'webhook.js': `const http = require('node:http');
|
||||
const payments = new Map();
|
||||
http.createServer((req, res) => {
|
||||
if (req.method !== 'POST' || req.url !== '/webhook/payment') { res.writeHead(404); return res.end(); }
|
||||
let body = ''; req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
const event = JSON.parse(body);
|
||||
payments.set(event.accountId, { plan: 'paid', amount: event.amount });
|
||||
res.end('payment applied');
|
||||
});
|
||||
}).listen(3000);
|
||||
`,
|
||||
},async f=>{
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-diff-mode',
|
||||
runId: `cso-diff-mode-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: prompt(f, '--diff'), workingDirectory: f.repo,
|
||||
env: { GSTACK_HOME: f.state }, maxTurns: 40, allowedTools: ['Bash', 'Read', 'Write', 'Grep', 'Glob'], timeout: CAPTURE_LONG_MS,
|
||||
});
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
const report = requireReport(f);
|
||||
expect(report.policy.diff).toBe(true);
|
||||
expect(report.policy.base).toBe('main');
|
||||
expect(report.source.baseCommit).toBe(git(f.repo, 'rev-parse', 'main').trim());
|
||||
expect(report.findings.some(item => item.location.path === 'webhook.js' && /signature|authenticat|forg/i.test(`${item.rootCause} ${item.impact}`))).toBe(true);
|
||||
expect(report.findings.every(item => item.location.path === 'webhook.js')).toBe(true);
|
||||
passed = result.browseErrors.length === 0;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-diff-mode', 'e2e-cso', result, { passed });
|
||||
}
|
||||
});
|
||||
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
|
||||
// Should detect hardcoded API key
|
||||
const output = result.output.toLowerCase();
|
||||
expect(
|
||||
output.includes('sk-') || output.includes('hardcoded') || output.includes('api key') || output.includes('api_key')
|
||||
).toBe(true);
|
||||
|
||||
// Should detect .env tracked by git
|
||||
expect(
|
||||
output.includes('.env') && (output.includes('tracked') || output.includes('gitignore'))
|
||||
).toBe(true);
|
||||
|
||||
// Should produce a findings table
|
||||
expect(
|
||||
output.includes('security findings') || output.includes('SECURITY FINDINGS')
|
||||
).toBe(true);
|
||||
}, CAPTURE_LONG_MS + CAPTURE_CLEANUP_MS);
|
||||
});
|
||||
|
||||
// Should save a report
|
||||
const reportDir = path.join(csoDir, '.gstack', 'security-reports');
|
||||
const reportExists = fs.existsSync(reportDir);
|
||||
if (reportExists) {
|
||||
const reports = fs.readdirSync(reportDir).filter(f => f.endsWith('.json'));
|
||||
expect(reports.length).toBeGreaterThanOrEqual(1);
|
||||
describeIfSelected('CSO v3 — infra scope', ['cso-infra-scope'], () => {
|
||||
test('/cso --infra finds an attacker-to-credential execution path', async () => {
|
||||
await withFixture({ 'package.json': PACKAGE,
|
||||
'.github/workflows/comment.yml': `name: comment automation
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
reply:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Handle untrusted comment
|
||||
env:
|
||||
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
||||
run: echo "\${{ github.event.comment.body }}"
|
||||
`,
|
||||
'Dockerfile': 'FROM node:22\nWORKDIR /app\nCOPY . .\nCMD ["node", "server.js"]\n',
|
||||
},undefined,async f=>{
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-infra-scope',
|
||||
runId: `cso-infra-scope-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: prompt(f, '--infra'), workingDirectory: f.repo,
|
||||
env: { GSTACK_HOME: f.state }, maxTurns: 40, allowedTools: ['Bash', 'Read', 'Write', 'Grep', 'Glob'], timeout: CAPTURE_LONG_MS,
|
||||
});
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
const report = requireReport(f);
|
||||
expect(report.policy.scope).toBe('infra');
|
||||
const workflow=report.findings.find(item=>item.location.path==='.github/workflows/comment.yml');
|
||||
expect(workflow).toBeDefined();
|
||||
const chain=[workflow!.title,workflow!.rootCause,workflow!.attackerControl,workflow!.impact,workflow!.scenario,...workflow!.trace].join(' ');
|
||||
expect(chain).toMatch(/github\.event\.comment\.body|issue comment body|comment body/i);
|
||||
expect(chain).toMatch(/run(?: step|:)|shell|bash/i);
|
||||
expect(chain).toMatch(/GITHUB_TOKEN|contents:\s*write|repository write/i);
|
||||
// A missing USER directive is only a hardening lead without demonstrated attacker impact.
|
||||
expect(report.findings.some(item => item.location.path === 'Dockerfile' && /critical|high/.test(item.severity))).toBe(false);
|
||||
passed = result.browseErrors.length === 0;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-infra-scope', 'e2e-cso', result, { passed });
|
||||
}
|
||||
passed = true;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-full-audit', 'e2e-cso', result, { passed: passed && result.browseErrors.length === 0 });
|
||||
}
|
||||
}, CAPTURE_MS + CAPTURE_CLEANUP_MS);
|
||||
});
|
||||
|
||||
describeIfSelected('CSO v2 — diff mode', ['cso-diff-mode'], () => {
|
||||
let csoDiffDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
csoDiffDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cso-diff-'));
|
||||
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
spawnSync(cmd, args, { cwd: csoDiffDir, stdio: 'pipe', timeout: 5000 });
|
||||
|
||||
run('git', ['init', '-b', 'main']);
|
||||
run('git', ['config', 'user.email', 'test@test.com']);
|
||||
run('git', ['config', 'user.name', 'Test']);
|
||||
|
||||
// Clean initial commit
|
||||
fs.writeFileSync(path.join(csoDiffDir, 'package.json'), JSON.stringify({
|
||||
name: 'cso-diff-test', version: '1.0.0',
|
||||
}, null, 2));
|
||||
fs.writeFileSync(path.join(csoDiffDir, 'app.ts'), 'console.log("hello");\n');
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'initial']);
|
||||
|
||||
// Feature branch with a vuln
|
||||
run('git', ['checkout', '-b', 'feat/add-webhook']);
|
||||
fs.writeFileSync(path.join(csoDiffDir, 'webhook.ts'), `
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
// No signature verification!
|
||||
app.post('/webhook/stripe', (req, res) => {
|
||||
const event = req.body;
|
||||
processPayment(event);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
`);
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'feat: add webhook']);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(csoDiffDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
test('/cso --diff scopes to branch changes', async () => {
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-diff-mode',
|
||||
runId: `cso-diff-mode-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: `Read the file ${path.join(ROOT, 'cso', 'SKILL.md')} for the CSO skill instructions.
|
||||
|
||||
Run /cso --diff on this repo. The base branch is "main".
|
||||
|
||||
IMPORTANT:
|
||||
- Do NOT use AskUserQuestion — skip any interactive prompts.
|
||||
- Focus on changes in the current branch vs main.
|
||||
- The webhook.ts file was added on this branch — it should be analyzed.`,
|
||||
workingDirectory: csoDiffDir,
|
||||
maxTurns: 40,
|
||||
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent'],
|
||||
// 360s/40 turns: the v1.67 wave grew the audit session legitimately —
|
||||
// transcript-verified, the agent finds the webhook vuln, spawns the
|
||||
// verification subagent, and writes the report, then gets killed at
|
||||
// ~215s in its CLOSING telemetry under the old 240s/25-turn budget.
|
||||
// The full-audit sibling already runs at 300s.
|
||||
timeout: 360_000,
|
||||
});
|
||||
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
|
||||
const output = result.output.toLowerCase();
|
||||
// Should mention webhook and missing signature verification
|
||||
expect(
|
||||
output.includes('webhook') && (output.includes('signature') || output.includes('verify'))
|
||||
).toBe(true);
|
||||
passed = true;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-diff-mode', 'e2e-cso', result, { passed: passed && result.browseErrors.length === 0 });
|
||||
}
|
||||
}, CAPTURE_LONG_MS);
|
||||
});
|
||||
|
||||
describeIfSelected('CSO v2 — infra scope', ['cso-infra-scope'], () => {
|
||||
let csoInfraDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
csoInfraDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cso-infra-'));
|
||||
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
spawnSync(cmd, args, { cwd: csoInfraDir, stdio: 'pipe', timeout: 5000 });
|
||||
|
||||
run('git', ['init', '-b', 'main']);
|
||||
run('git', ['config', 'user.email', 'test@test.com']);
|
||||
run('git', ['config', 'user.name', 'Test']);
|
||||
|
||||
// CI workflow with unpinned action
|
||||
fs.mkdirSync(path.join(csoInfraDir, '.github', 'workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(csoInfraDir, '.github', 'workflows', 'ci.yml'), `
|
||||
name: CI
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: some-third-party/action@main
|
||||
- run: echo "Building..."
|
||||
`);
|
||||
|
||||
// Dockerfile running as root
|
||||
fs.writeFileSync(path.join(csoInfraDir, 'Dockerfile'), `
|
||||
FROM node:20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
`);
|
||||
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'initial']);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(csoInfraDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
test('/cso --infra runs infrastructure phases only', async () => {
|
||||
const result = await runSkillTest({
|
||||
testName: 'cso-infra-scope',
|
||||
runId: `cso-infra-scope-${process.env.EVALS_RUN_ID ?? runId}-${process.pid}-${++captureSequence}`,
|
||||
publicStreamDiagnostics: true,
|
||||
prompt: `Read the file ${path.join(ROOT, 'cso', 'SKILL.md')} for the CSO skill instructions.
|
||||
|
||||
Run /cso --infra on this repo. This should run infrastructure-only phases (0-6, 12-14).
|
||||
|
||||
IMPORTANT:
|
||||
- Do NOT use AskUserQuestion — skip any interactive prompts.
|
||||
- This is a TINY repo with only 3 files: .github/workflows/ci.yml, Dockerfile, and package.json. Do NOT waste turns exploring — just read those files directly and audit them.
|
||||
- The Dockerfile has no USER directive (runs as root). The CI workflow uses an unpinned third-party GitHub Action (some-third-party/action@main).
|
||||
- Focus on infrastructure findings, NOT code-level OWASP scanning.
|
||||
- Skip the preamble (gstack-update-check, telemetry, etc.) — go straight to the audit.
|
||||
- Do NOT use the Agent tool for exploration or verification — read the files yourself. This repo is too small to need subagents.`,
|
||||
workingDirectory: csoInfraDir,
|
||||
maxTurns: 30,
|
||||
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob'],
|
||||
timeout: CAPTURE_LONG_MS,
|
||||
});
|
||||
|
||||
logCost('cso', result);
|
||||
let passed = false;
|
||||
try {
|
||||
expect(result.exitReason).toBe('success');
|
||||
|
||||
const output = result.output.toLowerCase();
|
||||
// Should mention unpinned action or Dockerfile issues
|
||||
expect(
|
||||
output.includes('unpinned') || output.includes('third-party') ||
|
||||
output.includes('user directive') || output.includes('root')
|
||||
).toBe(true);
|
||||
passed = true;
|
||||
} finally {
|
||||
recordE2E(evalCollector, 'cso-infra-scope', 'e2e-cso', result, { passed: passed && result.browseErrors.length === 0 });
|
||||
}
|
||||
}, CAPTURE_LONG_MS + CAPTURE_CLEANUP_MS);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user