@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.
- package/CHANGELOG.md +38 -0
- package/bin/agentic-security.js +7 -0
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/report/index.js +104 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
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",
|
package/src/report/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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.
|