@clear-capabilities/agentic-security-scanner 0.121.0 → 0.123.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
- 058bfb1a08d130e2e5f07a456b303b52a9874c5d2b679f89408d7e4446e1f214 agentic-security.mjs
1
+ c8cf8da77f8cf7ba57141e8280ea3642843125b66c871e2051e5a99c1894cbbd agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.121.0",
3
+ "version": "0.123.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",
package/src/mcp/tools.js CHANGED
@@ -18,7 +18,7 @@ import * as path from 'node:path';
18
18
  import * as crypto from 'node:crypto';
19
19
  import { applyFix as applyFixHistory, fixAcceptanceRate } from '../posture/fix-history.js';
20
20
  import { verifyLastScan } from '../posture/integrity.js';
21
- import { analyzeTranscript, formatCacheReport } from '../posture/cache-economics.js';
21
+ import { analyzeTranscript, formatCacheReport, renderCacheStatusLine } from '../posture/cache-economics.js';
22
22
  import { redactString, redactFinding } from './redact.js';
23
23
 
24
24
  // Lazy-loaded: these transitively pull in npm packages (fast-glob,
@@ -1043,6 +1043,7 @@ export const query_cache_telemetry = {
1043
1043
  metrics: result.metrics,
1044
1044
  leaks: result.leaks,
1045
1045
  report: formatCacheReport(result),
1046
+ statusline: renderCacheStatusLine(result.metrics),
1046
1047
  };
1047
1048
  },
1048
1049
  };
@@ -203,6 +203,14 @@ const CAUSE_LABEL = {
203
203
  'prefix-change': 'prefix changed (system prompt / tools / context edit)',
204
204
  };
205
205
 
206
+ // F6 — one-line HUD for a Claude Code statusLine command (mirrors
207
+ // watch-mode.js renderStatusLine). Takes the metrics from computeCacheEconomics.
208
+ export function renderCacheStatusLine(metrics) {
209
+ if (!metrics || !metrics.turns) return 'agentic-security: no session cost yet';
210
+ const hit = Math.round(metrics.cacheHitRatio * 100);
211
+ return `agentic-security: ${money(metrics.actualUsd)} · ${hit}% cached · ${money(metrics.costPerTurnUsd)}/turn`;
212
+ }
213
+
206
214
  export function formatCacheReport(result) {
207
215
  if (!result.ok) {
208
216
  return result.reason === 'no-transcript'
@@ -7,6 +7,28 @@ 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
+
10
32
  function fingerprint(f){
11
33
  const s = `${f.file}:${f.line||f.source?.line||0}:${f.vuln||f.type||''}`;
12
34
  return crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
@@ -681,7 +703,9 @@ export function toVex(scan, meta = {}) {
681
703
  }
682
704
 
683
705
  export function toHTML(scan, meta = {}) {
684
- const findings = normalizeFindings(scan);
706
+ // Attach the "likely lower risk" note (computed server-side) so the browser
707
+ // render can badge over-stated high/critical findings.
708
+ const findings = normalizeFindings(scan).map(f => ({ ...f, _riskNote: riskNote(f) }));
685
709
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
686
710
  for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
687
711
  const stride = {};
@@ -745,6 +769,7 @@ export function toHTML(scan, meta = {}) {
745
769
  .f-vuln{font-weight:600;flex:1}
746
770
  .f-cwe{color:#64748b;font-size:11px;font-family:ui-monospace,monospace}
747
771
  .f-epss{font-size:11px;font-weight:600;color:#f59e0b;background:#f59e0b18;padding:1px 6px;border-radius:3px;white-space:nowrap}
772
+ .f-note{font-size:11px;font-weight:600;color:#a3a3a3;background:#a3a3a318;padding:1px 6px;border-radius:3px;white-space:nowrap}
748
773
  .f-body{display:none;margin-top:12px;padding-top:12px;border-top:1px solid #1e293b;font-size:12px}
749
774
  .f.expanded .f-body{display:block}
750
775
  .f-body pre{background:#020617;padding:10px;border-radius:4px;overflow-x:auto;font-size:11px;line-height:1.5}
@@ -796,6 +821,7 @@ function makeCard(f) {
796
821
  '<span class="f-vuln">' + esc(f.vuln) + '</span>' +
797
822
  '<span class="f-cwe">' + esc(f.cwe||'') + '</span>' +
798
823
  epssHtml +
824
+ (f._riskNote ? '<span class="f-note" title="The reachability / confidence pipeline marked this down from its rule-default severity">↓ ' + esc(f._riskNote) + '</span>' : '') +
799
825
  '</div>' +
800
826
  '<div class="f-body">' +
801
827
  (f.snippet ? '<pre>' + esc(f.snippet) + '</pre>' : '') +
@@ -897,6 +923,8 @@ export function toCLI(scan, { verbose=false, color=true }={}){
897
923
  ? c(` V:${f.validator_verdict}`, DIM)
898
924
  : '';
899
925
  lines.push(`${sevTag} ${c(f.cwe||' ', DIM)} ${f.file}:${f.line} ${BOLD}${f.vuln}${RESET}${epssTag}${kevTag}${verdictTag}`);
926
+ const rn = riskNote(f);
927
+ if (rn) lines.push(` ${c('↓ ' + rn, '\x1b[2;33m')}`);
900
928
  if (f.masked) lines.push(` ${c('value:', DIM)} ${f.masked}`);
901
929
  if (verbose && f.fix?.description) {
902
930
  lines.push(` ${c('fix:', DIM)} ${f.fix.description}`);
@@ -1051,6 +1079,14 @@ export function toShipVerdict(scan, options = {}) {
1051
1079
  } else if (advisoryCount > 0) {
1052
1080
  lines.push(c(` ${advisoryCount} advisory item${advisoryCount === 1 ? '' : 's'} — run /security-scan-all --firehose to see them.`, DIM));
1053
1081
  }
1082
+ // Discoverability: the depth (per-finding explanation) and the shareable report
1083
+ // exist but aren't obvious from the one-screen verdict — point to them.
1084
+ if (findings.length > 0) {
1085
+ lines.push('');
1086
+ lines.push(c(' Want more detail?', BOLD));
1087
+ lines.push(c(' /triage --explain <id> why it fired, the data-flow trace, and the fix', DIM));
1088
+ lines.push(c(' scan . --format html -o report.html shareable browser report (charts + filters)', DIM));
1089
+ }
1054
1090
  // Coverage-honesty line (#5/#6): show the scan's blind spots — which
1055
1091
  // languages got flow analysis vs pattern-only, what was skipped, and how
1056
1092
  // many dangerous-looking calls had no finding. One concise line, not bloat.
@@ -1117,6 +1153,8 @@ export function toProTable(scan, options = {}) {
1117
1153
  else if (columns === 'capec') lines.push(`${sev} ${where} ${capec} ${vuln} ${conf}`);
1118
1154
  else if (columns === 'owasp') lines.push(`${sev} ${where} ${cwe} ${owasp} ${vuln} ${conf}`);
1119
1155
  else lines.push(`${sev} ${where} ${cwe} ${cvss} ${owasp} ${vuln} ${conf}`);
1156
+ const rn = riskNote(f);
1157
+ if (rn) lines.push(c(' ↓ ' + rn, '\x1b[2;33m'));
1120
1158
  }
1121
1159
 
1122
1160
  // Footer counts.