New DIS scanner detects tools that appear in BOTH permissions.deny
and permissions.allow within the same settings.json file. The deny
list wins, so allow entries are dead config but still load on every
turn and confuse intent.
Tool identity = bare name (everything before "("). `Bash(npm:*)` and
`Bash` are treated as the same tool, so a deny on `Bash` flags any
`Bash(...)` allow entry.
Severity: low. Wired into scan-orchestrator + scoring (area: Settings).
Fixture denied-tools-in-schema has Bash in both arrays; healthy-project
serves as the negative case.
[skip-docs] reason: v5 plan fences off README/CLAUDE.md badge updates
to Session 5; Forgejo pre-commit-docs-gate hook requires this tag.
Tests: 611 → 617 (+6).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
68 lines
2.8 KiB
JavaScript
68 lines
2.8 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { resetCounter } from '../../scanners/lib/output.mjs';
|
|
import { scan } from '../../scanners/disabled-in-schema-scanner.mjs';
|
|
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const FIXTURES = resolve(__dirname, '../fixtures');
|
|
|
|
async function runScanner(fixtureName) {
|
|
resetCounter();
|
|
const path = resolve(FIXTURES, fixtureName);
|
|
const discovery = await discoverConfigFiles(path);
|
|
return scan(path, discovery);
|
|
}
|
|
|
|
describe('DIS scanner — basic structure', () => {
|
|
it('reports scanner prefix DIS', async () => {
|
|
const result = await runScanner('denied-tools-in-schema');
|
|
assert.equal(result.scanner, 'DIS');
|
|
});
|
|
|
|
it('finding IDs match CA-DIS-NNN pattern', async () => {
|
|
const result = await runScanner('denied-tools-in-schema');
|
|
for (const f of result.findings) {
|
|
assert.match(f.id, /^CA-DIS-\d{3}$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('DIS scanner — Bash in both arrays → finding', () => {
|
|
it('flags Bash overlap with low severity', async () => {
|
|
const result = await runScanner('denied-tools-in-schema');
|
|
const f = result.findings.find(x => /both permissions\.deny and permissions\.allow/i.test(x.title || ''));
|
|
assert.ok(f, `expected DIS finding; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
|
assert.equal(f.severity, 'low', `expected low, got ${f.severity}`);
|
|
assert.match(String(f.evidence || ''), /Bash/);
|
|
});
|
|
|
|
it('evidence references the allow + deny entries', async () => {
|
|
const result = await runScanner('denied-tools-in-schema');
|
|
const f = result.findings.find(x => /both permissions/i.test(x.title || ''));
|
|
assert.ok(f);
|
|
assert.match(String(f.evidence || ''), /allow=/);
|
|
assert.match(String(f.evidence || ''), /deny=/);
|
|
});
|
|
});
|
|
|
|
describe('DIS scanner — clean settings → no finding', () => {
|
|
it('healthy-project has no DIS findings', async () => {
|
|
const result = await runScanner('healthy-project');
|
|
const f = result.findings.find(x => /both permissions/i.test(x.title || ''));
|
|
assert.equal(f, undefined,
|
|
`expected no DIS finding for healthy-project; got: ${f?.title}`);
|
|
});
|
|
});
|
|
|
|
describe('DIS scanner — orchestrator wiring', () => {
|
|
it('DIS appears in scan-orchestrator scanner list', async () => {
|
|
const orch = await import('../../scanners/scan-orchestrator.mjs');
|
|
const path = resolve(FIXTURES, 'denied-tools-in-schema');
|
|
const env = await orch.runAllScanners(path, { filterFixtures: false });
|
|
const dis = env.scanners.find(r => r.scanner === 'DIS');
|
|
assert.ok(dis, `expected DIS in orchestrator results; got: ${env.scanners.map(r => r.scanner).join(', ')}`);
|
|
});
|
|
});
|