feat(dis): flag ineffective allow wildcards; treat Tool(*) as deny-all
Extends the DIS scanner and its shared permission-rules lib with two
documented Claude Code permission footguns. Verified verbatim against
code.claude.com/docs/en/permissions (fetched 2026-06-19).
- lib/permission-rules.mjs: new isIneffectiveAllowGlob(entry) — unanchored
tool-name globs in permissions.allow (`*`, `B*`, `mcp__*`) that CC silently
skips ("does not auto-approve anything"); valid only as a glob-free
`mcp__<server>__*`. Shared with CNF.
- lib/permission-rules.mjs: dominates() now treats the `Tool(*)` deny-all glob
as equivalent to a bare deny (covers a bare allow) — CC: "Bash(*) is
equivalent to Bash ... both forms remove the tool from Claude's context".
- DIS: new finding "Ineffective allow wildcard — Claude Code ignores this rule"
(low, permissions-hygiene, CA-DIS-NNN); the existing dead-allow finding now
also catches a bare allow killed by a Tool(*) deny.
- 9 new tests (5 lib, 4 DIS) + 2 fixtures (force-added past .gitignore .claude/).
Suite 903 -> 912. Snapshot unchanged, contamination grep clean. README/CLAUDE/
scanner-internals document the broadened DIS mandate; test badge synced.
self-audit: PASS, configGrade A 96, pluginGrade A 100, readme gate passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
dfe9049b55
commit
03949c6c98
9 changed files with 198 additions and 28 deletions
|
|
@ -22,7 +22,7 @@ import { readTextFile } from './lib/file-discovery.mjs';
|
|||
import { finding, scannerResult } from './lib/output.mjs';
|
||||
import { SEVERITY } from './lib/severity.mjs';
|
||||
import { parseJson } from './lib/yaml-parser.mjs';
|
||||
import { dominates, parseRule } from './lib/permission-rules.mjs';
|
||||
import { dominates, parseRule, isIneffectiveAllowGlob } from './lib/permission-rules.mjs';
|
||||
|
||||
const SCANNER = 'DIS';
|
||||
|
||||
|
|
@ -52,6 +52,19 @@ function findDenyAllowOverlaps(settings) {
|
|||
return overlaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find `permissions.allow` entries that are unanchored tool-name globs Claude
|
||||
* Code silently skips (e.g. `mcp__*`, `B*`, `*`). They auto-approve nothing but
|
||||
* the author usually believes they grant access. Returns array of entry strings.
|
||||
*/
|
||||
function findIneffectiveAllowGlobs(settings) {
|
||||
if (!settings || typeof settings !== 'object') return [];
|
||||
const perms = settings.permissions;
|
||||
if (!perms || typeof perms !== 'object') return [];
|
||||
const allowList = Array.isArray(perms.allow) ? perms.allow : [];
|
||||
return allowList.filter(e => isIneffectiveAllowGlob(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main scanner entry point.
|
||||
*
|
||||
|
|
@ -70,28 +83,50 @@ export async function scan(targetPath, discovery) {
|
|||
if (!content) continue;
|
||||
const parsed = parseJson(content);
|
||||
if (!parsed) continue;
|
||||
const overlaps = findDenyAllowOverlaps(parsed);
|
||||
if (overlaps.length === 0) continue;
|
||||
|
||||
const evidence = overlaps.slice(0, 5)
|
||||
.map(o => `${o.tool}: allow="${o.allowEntry}" + deny="${o.denyEntry}"`)
|
||||
.join('; ');
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Tool listed in both permissions.deny and permissions.allow',
|
||||
description:
|
||||
`${f.relPath || f.absPath} contains ${overlaps.length} tool` +
|
||||
`${overlaps.length === 1 ? '' : 's'} present in both deny and allow lists. ` +
|
||||
'The deny list wins — the allow entries are dead config but still load on ' +
|
||||
'every turn and may confuse future readers about intent.',
|
||||
file: f.absPath,
|
||||
evidence,
|
||||
recommendation:
|
||||
'Remove the redundant allow entries. If you actually want this tool enabled, ' +
|
||||
'remove it from the deny list instead. Settings should express intent clearly.',
|
||||
category: 'permissions-hygiene',
|
||||
}));
|
||||
const overlaps = findDenyAllowOverlaps(parsed);
|
||||
if (overlaps.length > 0) {
|
||||
const evidence = overlaps.slice(0, 5)
|
||||
.map(o => `${o.tool}: allow="${o.allowEntry}" + deny="${o.denyEntry}"`)
|
||||
.join('; ');
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Tool listed in both permissions.deny and permissions.allow',
|
||||
description:
|
||||
`${f.relPath || f.absPath} contains ${overlaps.length} tool` +
|
||||
`${overlaps.length === 1 ? '' : 's'} present in both deny and allow lists. ` +
|
||||
'The deny list wins — the allow entries are dead config but still load on ' +
|
||||
'every turn and may confuse future readers about intent.',
|
||||
file: f.absPath,
|
||||
evidence,
|
||||
recommendation:
|
||||
'Remove the redundant allow entries. If you actually want this tool enabled, ' +
|
||||
'remove it from the deny list instead. Settings should express intent clearly.',
|
||||
category: 'permissions-hygiene',
|
||||
}));
|
||||
}
|
||||
|
||||
const ineffective = findIneffectiveAllowGlobs(parsed);
|
||||
if (ineffective.length > 0) {
|
||||
const evidence = `allow: ${ineffective.slice(0, 5).map(e => `"${e}"`).join(', ')}`;
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Ineffective allow wildcard — Claude Code ignores this rule',
|
||||
description:
|
||||
`${f.relPath || f.absPath} has ${ineffective.length} permissions.allow ` +
|
||||
`entr${ineffective.length === 1 ? 'y' : 'ies'} that Claude Code skips: an ` +
|
||||
'unanchored tool-name wildcard auto-approves nothing. CC accepts allow ' +
|
||||
'wildcards only after a literal `mcp__<server>__` prefix.',
|
||||
file: f.absPath,
|
||||
evidence,
|
||||
recommendation:
|
||||
'Replace `*`/`mcp__*` with explicit tool names, or anchor MCP wildcards to ' +
|
||||
'a server (`mcp__<server>__*`). As written these entries grant nothing.',
|
||||
category: 'permissions-hygiene',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue