@clear-capabilities/agentic-security-scanner 0.122.0 → 0.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- 3249693b62570256c597f206447598e9ed6b618032f0d0a5a25dbbf62d53afc8 agentic-security.mjs
1
+ a89f9898e9c186c99eef3e08ab7a5bf886af031eb068aff011ff617d3a12a08e agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.122.0",
3
+ "version": "0.124.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -64,7 +64,7 @@
64
64
  "test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js",
65
65
  "test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js",
66
66
  "test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js",
67
- "test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js",
67
+ "test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js",
68
68
  "test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
69
69
  "test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js && node ../scripts/lint-command-descriptions.mjs",
70
70
  "test:eval": "node --test test/independent-eval.test.js",
@@ -7,6 +7,76 @@ import { SCANNER_VERSION } from '../posture/version.js';
7
7
  const SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
8
8
  const SEV_TO_SARIF = { critical: 'error', high: 'error', medium: 'warning', low: 'note', info: 'none' };
9
9
 
10
+ // A short "likely lower risk than its label" note for a high/critical finding the
11
+ // reachability / exploitability / confidence pipeline has already marked down.
12
+ // Detector severities are hardcoded per rule, so a `critical` can outrun its real
13
+ // risk; this surfaces that in the rendered output so the label isn't taken at face
14
+ // value. Returns null when the headline severity is fair as-is. (We annotate, not
15
+ // mutate — `severity` stays canonical for SARIF/exit codes/baselines.)
16
+ function riskNote(f) {
17
+ if (!/^(critical|high)$/.test(String(f.severity || ''))) return null;
18
+ const mv = String(f.mitigationVerdict || '').toLowerCase();
19
+ if (f.unreachable === true || f.unreachableInProd === true || mv.includes('unreachable')) {
20
+ return 'likely lower risk — not reachable in prod';
21
+ }
22
+ if (mv.includes('saniti')) return 'likely lower risk — input appears sanitized';
23
+ const et = String(f.exploitabilityTier || '').toLowerCase();
24
+ if (et === 'minimal' || et === 'low') return `likely lower risk — ${et} exploitability`;
25
+ const ct = String(f.confidenceTier || '').toLowerCase();
26
+ if (ct === 'low' || (typeof f.confidence === 'number' && f.confidence > 0 && f.confidence < 0.5)) {
27
+ return 'lower confidence — verify before prioritising';
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function _firstSentences(text, n) {
33
+ if (!text) return '';
34
+ const t = String(text).replace(/\s+/g, ' ').trim();
35
+ return t.split(/(?<=[.!?])\s+/).slice(0, n).join(' ');
36
+ }
37
+
38
+ // "How it fires" summary from the whyFired provenance already on the finding:
39
+ // the source→sink flow (or the sink line), plus rejected sanitizers / observed
40
+ // guards / reachability demotion. Returns null when no provenance exists.
41
+ function _flowSummary(f) {
42
+ const w = f.whyFired;
43
+ if (!w || !w.evidence) return null;
44
+ const ev = w.evidence;
45
+ const steps = Array.isArray(ev.pathSteps) ? ev.pathSteps.filter(s => s && s.label) : [];
46
+ let flow = null;
47
+ if (steps.length) flow = steps.map(s => s.label).join(' → ');
48
+ else if (ev.sourceSnippet && ev.sinkSnippet && ev.sourceSnippet !== ev.sinkSnippet) flow = `${ev.sourceSnippet.trim()} → ${ev.sinkSnippet.trim()}`;
49
+ else if (ev.sinkSnippet) flow = String(ev.sinkSnippet).trim();
50
+ const tags = [];
51
+ if (Array.isArray(ev.sanitizers) && ev.sanitizers.length) tags.push(`${ev.sanitizers.length} sanitizer(s) rejected`);
52
+ if (Array.isArray(ev.guards) && ev.guards.length) tags.push(`${ev.guards.length} guard(s) seen`);
53
+ if (w.considered && w.considered.reachabilityFilter === 'demoted') tags.push('reachability-demoted');
54
+ return { detector: w.detector || null, parser: w.parser || null, flow, tags };
55
+ }
56
+
57
+ // The inline "explain" depth — assembled entirely from fields already on the
58
+ // finding (narration + whyFired + fix), so the default finding view carries the
59
+ // same depth as `/triage --explain` without a second command. verbose=false
60
+ // trims the narration to 2 sentences and omits the evidence tags + fix code.
61
+ function explainParts(f, { verbose = false } = {}) {
62
+ const why = f.narration
63
+ ? (verbose ? String(f.narration).replace(/\s+/g, ' ').trim() : _firstSentences(f.narration, 2))
64
+ : '';
65
+ const fs = _flowSummary(f);
66
+ let how = '';
67
+ if (fs && (fs.flow || fs.detector)) {
68
+ const head = fs.detector ? fs.detector + (fs.parser ? ` (${fs.parser})` : '') : '';
69
+ how = [head, fs.flow].filter(Boolean).join(' · ');
70
+ if (verbose && fs.tags.length) how += ` · ${fs.tags.join(' · ')}`;
71
+ }
72
+ let fix = '';
73
+ if (f.fix && typeof f.fix === 'object' && typeof f.fix.description === 'string') fix = f.fix.description;
74
+ else if (typeof f.fix === 'string') fix = f.fix;
75
+ else if (typeof f.remediation === 'string') fix = f.remediation;
76
+ const fixCode = verbose && f.fix && typeof f.fix === 'object' && typeof f.fix.code === 'string' ? f.fix.code : '';
77
+ return { why, how, fix: fix.replace(/\s+/g, ' ').trim(), fixCode };
78
+ }
79
+
10
80
  function fingerprint(f){
11
81
  const s = `${f.file}:${f.line||f.source?.line||0}:${f.vuln||f.type||''}`;
12
82
  return crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
@@ -681,7 +751,12 @@ export function toVex(scan, meta = {}) {
681
751
  }
682
752
 
683
753
  export function toHTML(scan, meta = {}) {
684
- const findings = normalizeFindings(scan);
754
+ // Attach the "likely lower risk" note + inline explain depth (computed
755
+ // server-side) so the browser render shows them without a second command.
756
+ const findings = normalizeFindings(scan).map(f => {
757
+ const ex = explainParts(f, { verbose: true });
758
+ return { ...f, _riskNote: riskNote(f), _explainWhy: ex.why, _explainHow: ex.how };
759
+ });
685
760
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
686
761
  for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
687
762
  const stride = {};
@@ -745,9 +820,13 @@ export function toHTML(scan, meta = {}) {
745
820
  .f-vuln{font-weight:600;flex:1}
746
821
  .f-cwe{color:#64748b;font-size:11px;font-family:ui-monospace,monospace}
747
822
  .f-epss{font-size:11px;font-weight:600;color:#f59e0b;background:#f59e0b18;padding:1px 6px;border-radius:3px;white-space:nowrap}
823
+ .f-note{font-size:11px;font-weight:600;color:#a3a3a3;background:#a3a3a318;padding:1px 6px;border-radius:3px;white-space:nowrap}
748
824
  .f-body{display:none;margin-top:12px;padding-top:12px;border-top:1px solid #1e293b;font-size:12px}
749
825
  .f.expanded .f-body{display:block}
750
826
  .f-body pre{background:#020617;padding:10px;border-radius:4px;overflow-x:auto;font-size:11px;line-height:1.5}
827
+ .f-why{margin-top:8px;color:#cbd5e1}
828
+ .f-how{margin-top:6px;color:#94a3b8;font-size:13px}
829
+ .f-how code{font-family:ui-monospace,monospace;color:#e2e8f4}
751
830
  .f-fix{background:#0d1f3d;border-left:3px solid #38bdf8;padding:8px 12px;margin-top:8px;border-radius:0 4px 4px 0}
752
831
  .hidden{display:none!important}
753
832
  </style></head>
@@ -796,8 +875,11 @@ function makeCard(f) {
796
875
  '<span class="f-vuln">' + esc(f.vuln) + '</span>' +
797
876
  '<span class="f-cwe">' + esc(f.cwe||'') + '</span>' +
798
877
  epssHtml +
878
+ (f._riskNote ? '<span class="f-note" title="The reachability / confidence pipeline marked this down from its rule-default severity">↓ ' + esc(f._riskNote) + '</span>' : '') +
799
879
  '</div>' +
800
880
  '<div class="f-body">' +
881
+ (f._explainWhy ? '<div class="f-why"><b>Why it matters:</b> ' + esc(f._explainWhy) + '</div>' : '') +
882
+ (f._explainHow ? '<div class="f-how"><b>How it fires:</b> <code>' + esc(f._explainHow) + '</code></div>' : '') +
801
883
  (f.snippet ? '<pre>' + esc(f.snippet) + '</pre>' : '') +
802
884
  (f.masked ? '<pre style="color:#f97316">' + esc(f.masked) + ' (masked)</pre>' : '') +
803
885
  (f.fix && f.fix.description ? '<div class="f-fix"><b>Fix:</b> ' + esc(f.fix.description) + (f.fix.code ? '<pre>' + esc(f.fix.code) + '</pre>' : '') + '</div>' : '') +
@@ -897,11 +979,15 @@ export function toCLI(scan, { verbose=false, color=true }={}){
897
979
  ? c(` V:${f.validator_verdict}`, DIM)
898
980
  : '';
899
981
  lines.push(`${sevTag} ${c(f.cwe||' ', DIM)} ${f.file}:${f.line} ${BOLD}${f.vuln}${RESET}${epssTag}${kevTag}${verdictTag}`);
982
+ const rn = riskNote(f);
983
+ if (rn) lines.push(` ${c('↓ ' + rn, '\x1b[2;33m')}`);
900
984
  if (f.masked) lines.push(` ${c('value:', DIM)} ${f.masked}`);
901
- if (verbose && f.fix?.description) {
902
- lines.push(` ${c('fix:', DIM)} ${f.fix.description}`);
903
- if (f.fix.code) for (const ln of f.fix.code.split('\n').slice(0, 6)) lines.push(` ${c(ln, DIM)}`);
904
- }
985
+ // Inline explain depth — why it matters / how it fires / the fix (#explain).
986
+ const ex = explainParts(f, { verbose });
987
+ if (ex.why) lines.push(` ${c('why:', DIM)} ${ex.why}`);
988
+ if (ex.how) lines.push(` ${c('how:', DIM)} ${ex.how}`);
989
+ if (ex.fix) lines.push(` ${c('fix:', DIM)} ${ex.fix}`);
990
+ if (ex.fixCode) for (const ln of ex.fixCode.split('\n').slice(0, 6)) lines.push(` ${c(ln, DIM)}`);
905
991
  }
906
992
  lines.push('');
907
993
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
@@ -1051,6 +1137,14 @@ export function toShipVerdict(scan, options = {}) {
1051
1137
  } else if (advisoryCount > 0) {
1052
1138
  lines.push(c(` ${advisoryCount} advisory item${advisoryCount === 1 ? '' : 's'} — run /security-scan-all --firehose to see them.`, DIM));
1053
1139
  }
1140
+ // Discoverability: the depth (per-finding explanation) and the shareable report
1141
+ // exist but aren't obvious from the one-screen verdict — point to them.
1142
+ if (findings.length > 0) {
1143
+ lines.push('');
1144
+ lines.push(c(' Want more detail?', BOLD));
1145
+ lines.push(c(' /triage --explain <id> why it fired, the data-flow trace, and the fix', DIM));
1146
+ lines.push(c(' scan . --format html -o report.html shareable browser report (charts + filters)', DIM));
1147
+ }
1054
1148
  // Coverage-honesty line (#5/#6): show the scan's blind spots — which
1055
1149
  // languages got flow analysis vs pattern-only, what was skipped, and how
1056
1150
  // many dangerous-looking calls had no finding. One concise line, not bloat.
@@ -1117,6 +1211,11 @@ export function toProTable(scan, options = {}) {
1117
1211
  else if (columns === 'capec') lines.push(`${sev} ${where} ${capec} ${vuln} ${conf}`);
1118
1212
  else if (columns === 'owasp') lines.push(`${sev} ${where} ${cwe} ${owasp} ${vuln} ${conf}`);
1119
1213
  else lines.push(`${sev} ${where} ${cwe} ${cvss} ${owasp} ${vuln} ${conf}`);
1214
+ const rn = riskNote(f);
1215
+ if (rn) lines.push(c(' ↓ ' + rn, '\x1b[2;33m'));
1216
+ // One compact "why it matters" line keeps the table scannable but not bare.
1217
+ const why1 = f.narration ? _firstSentences(f.narration, 1) : '';
1218
+ if (why1) lines.push(c(' ↳ ' + why1, DIM));
1120
1219
  }
1121
1220
 
1122
1221
  // Footer counts.