feat(commands): E14 part 3 — /security mcp-baseline-reset slash command
Wave C step C3: closes E14 with the user-facing reset command. After a legitimate MCP server upgrade the sticky baseline (added in C1) becomes a stale "what the tool used to say" anchor and every subsequent post-mcp-verify advisory will re-flag the change. /security mcp-baseline-reset lets the user acknowledge the upgrade so the next call seeds a fresh baseline. New files: - scanners/mcp-baseline-reset.mjs — small CLI wrapper around clearBaseline / listBaselines. Modes: --list (read-only), --target <name>, no-args (all). Outputs JSON summary on stdout. Exit 0 always (idempotent). - commands/mcp-baseline-reset.md — dispatcher following mcp-inspect.md shape. Frontmatter: name=security:mcp-baseline-reset, sonnet model, Read/Bash/AskUserQuestion tools. 4-step body (list -> confirm scope -> execute -> confirm result). - tests/scanners/mcp-baseline-reset.test.mjs — 10 CLI tests across --list, --target, clear-all, idempotency, history preservation, and bare-positional sugar. Updated: - commands/security.md — new row in commands table after mcp-inspect. - CLAUDE.md — new commands-table row + new v7.3.0 narrative section describing the baseline schema, cumulative-drift detection, reset semantics, and the LLM_SECURITY_MCP_CACHE_FILE override. - Plugin README.md — new MCP-baseline-reset row in commands table, scanner count 12 standalone -> 13 standalone, new "MCP Description Drift (E14, v7.3.0)" subsection explaining the sticky baseline, cumulative threshold, reset semantics, and env-var override. - Root marketplace README.md — scanner count 22 -> 23 (10 orchestrated + 13 standalone), command count 19 -> 20, test count 1511 -> 1768. Wave C complete: 1738 -> 1768 tests (+30 across C1/C2/C3). Per plan, Wave C does NOT bump the plugin version — that lands at the wave-bundle release. The advisory text in post-mcp-verify already references the new command path so the user has a ready remediation step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
427b68eca9
commit
001df2ebe8
7 changed files with 454 additions and 5 deletions
101
plugins/llm-security/scanners/mcp-baseline-reset.mjs
Normal file
101
plugins/llm-security/scanners/mcp-baseline-reset.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#!/usr/bin/env node
|
||||
// mcp-baseline-reset.mjs — Reset MCP description-cache baselines.
|
||||
//
|
||||
// Purpose:
|
||||
// The description cache (scanners/lib/mcp-description-cache.mjs) anchors a
|
||||
// sticky baseline per MCP tool so that cumulative drift can be detected
|
||||
// across many small updates. After a legitimate MCP server upgrade the
|
||||
// baseline becomes a stale "what the tool used to say" reference and must
|
||||
// be reset so the next call seeds a fresh baseline.
|
||||
//
|
||||
// Modes:
|
||||
// --list Read-only — list current baselines as JSON.
|
||||
// --target <toolName> Clear baseline for one tool.
|
||||
// (no args) Clear baselines for all tools.
|
||||
//
|
||||
// Output: JSON summary on stdout. Exit 0 always (idempotent).
|
||||
//
|
||||
// Used by /security mcp-baseline-reset slash command. Not part of
|
||||
// scan-orchestrator.
|
||||
|
||||
import {
|
||||
clearBaseline,
|
||||
listBaselines,
|
||||
loadCache,
|
||||
} from './lib/mcp-description-cache.mjs';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { list: false, target: null };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--list') {
|
||||
args.list = true;
|
||||
} else if (a === '--target' || a === '-t') {
|
||||
args.target = argv[++i] || null;
|
||||
} else if (a === '--help' || a === '-h') {
|
||||
args.help = true;
|
||||
} else if (!a.startsWith('--')) {
|
||||
// bare positional treated as target for convenience
|
||||
args.target = a;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function help() {
|
||||
process.stdout.write(
|
||||
'mcp-baseline-reset.mjs — Reset MCP description-cache baselines.\n\n' +
|
||||
'Usage:\n' +
|
||||
' node scanners/mcp-baseline-reset.mjs --list\n' +
|
||||
' node scanners/mcp-baseline-reset.mjs --target <tool>\n' +
|
||||
' node scanners/mcp-baseline-reset.mjs # clear all\n\n' +
|
||||
'Output: JSON. Exit code 0 always.\n',
|
||||
);
|
||||
}
|
||||
|
||||
function emit(obj) {
|
||||
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
if (args.help) {
|
||||
help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args.list) {
|
||||
const baselines = listBaselines();
|
||||
emit({
|
||||
mode: 'list',
|
||||
count: baselines.length,
|
||||
baselines: baselines.map((b) => ({
|
||||
tool: b.tool,
|
||||
baseline_excerpt: (b.baseline || '').slice(0, 120),
|
||||
seen_at: b.seenAt,
|
||||
last_seen: b.lastSeen,
|
||||
history_events: b.history,
|
||||
})),
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reset path
|
||||
const result = clearBaseline(args.target || undefined);
|
||||
// After clearing, count remaining baselines
|
||||
const cache = loadCache();
|
||||
let remaining = 0;
|
||||
for (const entry of Object.values(cache)) {
|
||||
if (entry && entry.baseline) remaining++;
|
||||
}
|
||||
emit({
|
||||
mode: 'reset',
|
||||
target: args.target || null,
|
||||
cleared: result.cleared,
|
||||
tools: result.tools,
|
||||
remaining,
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
process.exit(main());
|
||||
Loading…
Add table
Add a link
Reference in a new issue