Files
ruvnet__ruflo/v3/@claude-flow/cli/benchmarks/results/scripts/settings-risk-benchmark.mjs
rUv 0e50fe7f57 dream(security): #3043 advisory scanner for untrusted settings.json hooks/allow-rules (evaluated, ACCEPT) (#3044)
* dream(security): #3043 advisory scanner for untrusted settings.json hooks/allow-rules carried forward by ruflo init/--upgrade

mergeSettingsForUpgrade() and writeSettings() both read a target
project's pre-existing .claude/settings.json and spread its hooks /
permissions.allow entries into the merged output verbatim — the same
trust shape as CVE-2025-59536 (settings.json hook payload achieving
command execution with no review step), reached via ruflo init's own
merge logic rather than Claude Code's loader. Neither of last night's
flagged CVEs (2025-59536, 2025-6514) applies to Ruflo's own hook
dispatch or OAuth client directly, but this distinct gap in the same
family was genuinely unaddressed.

Adds a static, advisory-only (non-blocking) scanner that inspects
hook commands and Bash allow-rules for known-dangerous patterns
before the merge and surfaces findings as CLI warnings. Merge/write
behavior itself is completely unchanged. Hardened after an
independent adversarial-critic pass found 6 concrete bypasses
(intermediate-pipe-stage downloads, two-step download+exec,
eval-wrapped base64, interpreter-mediated dangerous commands,
absolute-path-prefixed commands, whitespace-evasive allow rules) —
all fixed and pinned as a held-out regression set. Also fixes an
ANSI/control-character injection risk in the warning output itself,
self-identified during review.

87/87 relevant tests green (1 pre-existing environmental failure
elsewhere in the package, unrelated — unbuilt sibling package).

Dream Cycle 2026-08-16, security surface. Full research, evaluation
receipts, and adversarial critique: issue #3043 and
docs/dream-cycle/dream-gist-2026-08-16.md.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01NVdeUqt73Bv7vnPxYpZy7i

* docs(dream-cycle): update ledger — backfill 2026-08-14/2026-08-15, add 2026-08-16

2026-08-14 (swarm, REJECT) and 2026-08-15 (performance, REJECT) both had
fully-run pipelines whose live ledger rows never reached main (every
dream-cycle PR to date is unmerged, so branch-local ledger commits never
merge forward) — verified via git ls-remote / search_pull_requests before
backfilling, not assumed.

2026-08-16 (security, ACCEPT): advisory scanner for untrusted settings.json
hooks/allow-rules carried forward by ruflo init/--upgrade. Issue #3043,
PR #3044.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01NVdeUqt73Bv7vnPxYpZy7i

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-21 10:39:22 -04:00

79 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Deterministic baseline-vs-candidate evaluation for the settings-risk-scanner
* candidate (Ruflo Dream Cycle 2026-08-16, security).
*
* Baseline = no scanner (every existing hook/allow-rule reported clean, the
* status quo before tonight's candidate). Candidate = scanSettingsForRisk().
* Corpus: ../../settings-risk-corpus.json (hand-written, provenance recorded
* in the corpus file itself — see STEP 7/STEP 10 fairness-check discipline).
*
* Run: node v3/@claude-flow/cli/benchmarks/results/scripts/settings-risk-benchmark.mjs
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '../../../../../..');
const corpusPath = path.join(repoRoot, 'v3/@claude-flow/cli/benchmarks/settings-risk-corpus.json');
const scannerPath = path.join(repoRoot, 'v3/@claude-flow/cli/src/init/settings-risk-scanner.ts');
const { scanCommandStringForRisk, scanAllowRuleForRisk } = await import(scannerPath);
const corpus = JSON.parse(readFileSync(corpusPath, 'utf-8'));
function evaluate(name, classify) {
let tp = 0, fp = 0, tn = 0, fn = 0;
const misclassified = [];
for (const sample of corpus.hookCommands) {
const flagged = classify.command(sample.value);
if (flagged && sample.malicious) tp++;
else if (flagged && !sample.malicious) { fp++; misclassified.push({ type: 'command', ...sample }); }
else if (!flagged && sample.malicious) { fn++; misclassified.push({ type: 'command', ...sample }); }
else tn++;
}
for (const sample of corpus.allowRules) {
const flagged = classify.allow(sample.value);
if (flagged && sample.malicious) tp++;
else if (flagged && !sample.malicious) { fp++; misclassified.push({ type: 'allow', ...sample }); }
else if (!flagged && sample.malicious) { fn++; misclassified.push({ type: 'allow', ...sample }); }
else tn++;
}
const precision = tp + fp === 0 ? null : tp / (tp + fp);
const recall = tp + fn === 0 ? null : tp / (tp + fn);
const f1 = precision === null || recall === null || precision + recall === 0
? null
: (2 * precision * recall) / (precision + recall);
return { name, tp, fp, tn, fn, precision, recall, f1, falsePositiveRate: fp / (fp + tn), misclassified };
}
const baseline = evaluate('baseline (no scanner)', {
command: () => false,
allow: () => false,
});
const candidate = evaluate('candidate (scanSettingsForRisk)', {
command: (v) => scanCommandStringForRisk(v).length > 0,
allow: (v) => scanAllowRuleForRisk(v).length > 0,
});
const receipt = {
timestamp: '2026-08-16',
corpusProvenance: corpus._provenance,
corpusSize: corpus.hookCommands.length + corpus.allowRules.length,
maliciousCount: [...corpus.hookCommands, ...corpus.allowRules].filter((s) => s.malicious).length,
benignCount: [...corpus.hookCommands, ...corpus.allowRules].filter((s) => !s.malicious).length,
baseline,
candidate,
};
const outDir = path.join(repoRoot, 'v3/@claude-flow/cli/benchmarks/results');
writeFileSync(path.join(outDir, 'receipt-baseline.json'), JSON.stringify(baseline, null, 2));
writeFileSync(path.join(outDir, 'receipt-candidate.json'), JSON.stringify(candidate, null, 2));
writeFileSync(path.join(outDir, 'comparison-settings-risk-final.json'), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt, null, 2));