UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

115 lines (114 loc) • 4.57 kB
/** * Cross-source secret-finding dedup (acceptance gate for #131 Mode 3). * * Hardcoded / committed secrets can be reported by up to three independent * pi-lens sources for the *same* line: * - gitleaks (session scan, #130) — rule id e.g. `aws-access-token` * - trivy secret (session scan, #131) — rule id e.g. `aws-access-key-id` * - ast-grep rules (per-edit dispatch) — rule id e.g. `*-hardcoded-secret-*` * * The existing diagnostic dedup keys on `filePath:line:rule`, so these three * have *different* rule ids and would NOT collapse — exactly the triple-report * noise that got the old regex secrets-scanner removed (b8f368d). This module * collapses by **location** (normalized file + line), keeping the rule/source * provenance, so a secret is surfaced once with "detected by gitleaks + trivy + * ast-grep" rather than three times. * * Pure + side-effect-free so the acceptance gate ("one surfaced finding, not * three") is a unit test, independent of the turn_end plumbing that consumes it. * * Refs: #131 (Mode 3), #130 */ import { normalizeFilePath } from "./path-utils.js"; /** * Rule-id shape that marks a dispatch warning as a hardcoded-secret finding. * Deliberately conservative — the bundled ast-grep secret rules all carry one * of these tokens (`*-hardcoded-secret-*`, `*-hardcoded-password-*`, * `*-hardcoded-session-key-*`, `*-hardcoded-connection-password-*`, …) so we * don't sweep unrelated rules (e.g. a parser rule that merely mentions "token") * into the secrets channel. */ const SECRET_RULE_PATTERN = /hardcoded|secret|password|credential/i; /** Does this dispatch warning look like a hardcoded-secret finding? */ export function isSecretWarning(warning) { const id = `${warning.rule ?? ""} ${warning.code ?? ""}`; return SECRET_RULE_PATTERN.test(id); } /** Location key used to collapse the same secret across sources. */ export function secretLocationKey(file, line) { return `${normalizeFilePath(file)}:${line}`; } export function fromGitleaks(findings) { return findings.map((f) => ({ file: f.file, line: f.startLine, sources: ["gitleaks"], rule: f.ruleId, description: f.description, })); } export function fromTrivySecrets(findings) { return findings.map((f) => ({ file: f.file, line: f.line, sources: ["trivy"], rule: f.ruleId, description: f.title, })); } export function fromAstGrepWarnings(warnings) { const out = []; for (const w of warnings) { if (!isSecretWarning(w) || typeof w.line !== "number") continue; out.push({ file: w.filePath, line: w.line, sources: ["ast-grep"], rule: w.rule ?? w.code ?? "hardcoded-secret", description: w.message, }); } return out; } // gitleaks > trivy > ast-grep: when the same location is flagged by several // scanners we keep the most-specific committed-secret rule id for display. const SOURCE_PRIORITY = { gitleaks: 0, trivy: 1, "ast-grep": 2, }; function sortSources(sources) { return [...new Set(sources)].sort((a, b) => SOURCE_PRIORITY[a] - SOURCE_PRIORITY[b]); } /** * Collapse secret findings that share a location (normalized file + line) into * one, merging their `sources` and keeping the highest-priority source's rule. * Output preserves first-appearance order so surfacing is stable across turns. * * This is THE cross-source collapse the #131 Mode 3 acceptance gate verifies: * the same secret fed in from gitleaks + trivy + ast-grep yields a single * finding, not three. */ export function dedupeSecretFindings(findings) { const byKey = new Map(); for (const f of findings) { const key = secretLocationKey(f.file, f.line); const incoming = sortSources(f.sources); const existed = byKey.get(key); if (!existed) { byKey.set(key, { ...f, sources: incoming }); continue; } // `existed.sources` is kept sorted, so [0] is its current rule's source. const existingTop = SOURCE_PRIORITY[existed.sources[0]]; existed.sources = sortSources([...existed.sources, ...incoming]); // Keep the rule from the highest-priority source now present. if (SOURCE_PRIORITY[incoming[0]] < existingTop) { existed.rule = f.rule; } // Fill a missing description from any source. existed.description ??= f.description; } return [...byKey.values()]; }