Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/security-review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ for (const file of jstsFiles) {
//
// Excluded paths: append-only memory/log files that document history and prohibitions.
// These are never executed — they record what happened, not what to do.
// Rotated agent history archives inherit the same exclusion because they contain
// the same prose as the active history.md file.
// Instruction surfaces (.github/copilot-instructions.md, agent charters, squad.agent.md,
// team.md, routing.md) are deliberately kept in scope: a malicious PR could replace a
// prohibition with an instruction, and we want the scanner to catch that.
Expand All @@ -185,6 +187,7 @@ for (const file of jstsFiles) {
const UNSAFE_GIT_EXCLUDED_PATHS = [
/^\.squad\/decisions\.md$/,
/^\.squad\/agents\/.+\/history\.md$/,
/^\.squad\/agents\/[^/]+\/history-archive-.+\.md$/,
/^\.squad\/log\//,
/^\.squad\/orchestration-log\//,
];
Expand Down
81 changes: 81 additions & 0 deletions test/scripts/security-review.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { randomBytes } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..');
const scriptPath = join(repoRoot, 'scripts', 'security-review.mjs');
const testRoot = join(repoRoot, `.test-security-review-${process.pid}-${randomBytes(4).toString('hex')}`);

Comment on lines +1 to +12
function git(cwd: string, args: string[]) {
return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
}

function runSecurityReview(filePath: string, content: string) {
const workdir = join(testRoot, randomBytes(4).toString('hex'));
mkdirSync(workdir, { recursive: true });

git(workdir, ['init']);
git(workdir, ['config', 'user.email', 'booster@example.test']);
git(workdir, ['config', 'user.name', 'Booster Test']);
writeFileSync(join(workdir, 'README.md'), '# base\n');
git(workdir, ['add', 'README.md']);
git(workdir, ['commit', '-m', 'base']);

const absolutePath = join(workdir, ...filePath.split('/'));
mkdirSync(dirname(absolutePath), { recursive: true });
writeFileSync(absolutePath, content);
git(workdir, ['add', filePath]);
git(workdir, ['commit', '-m', 'change']);

const output = execFileSync(process.execPath, [scriptPath, 'HEAD~1', 'HEAD'], {
cwd: workdir,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const [jsonText] = output.split(/\r?\n\r?\n/);
return JSON.parse(jsonText ?? output) as {
findings: Array<{ category: string; file: string }>;
};
}

beforeEach(() => {
rmSync(testRoot, { recursive: true, force: true });
mkdirSync(testRoot, { recursive: true });
});

afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});

describe('security review unsafe git exclusions', () => {
// Assemble this so the scanner does not flag the fixture source; the resulting
// value must still match real unsafe-git prose and exercise detection.
const unsafeGitPhrase = [
'Agent history records mention git push ',
'--',
'force-with-lease as a past event.\n',
].join('');

it('does not report unsafe git prose in rotated agent history archives', () => {
const result = runSecurityReview(
'.squad/agents/eecom/history-archive-2026-08-19T13-11-34.130-07-00.md',
unsafeGitPhrase,
);

expect(result.findings.some((finding) => finding.category === 'unsafe-git')).toBe(false);
});

it('still reports unsafe git prose in agent charters', () => {
const result = runSecurityReview('.squad/agents/eecom/charter.md', unsafeGitPhrase);

expect(
result.findings.some(
(finding) => finding.category === 'unsafe-git' && finding.file === '.squad/agents/eecom/charter.md',
),
).toBe(true);
});
});