@clear-capabilities/agentic-security-scanner 0.123.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 +19 -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 +1 -1
- package/src/report/index.js +68 -7
|
@@ -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",
|
package/src/report/index.js
CHANGED
|
@@ -29,6 +29,54 @@ function riskNote(f) {
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
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
|
+
|
|
32
80
|
function fingerprint(f){
|
|
33
81
|
const s = `${f.file}:${f.line||f.source?.line||0}:${f.vuln||f.type||''}`;
|
|
34
82
|
return crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
@@ -703,9 +751,12 @@ export function toVex(scan, meta = {}) {
|
|
|
703
751
|
}
|
|
704
752
|
|
|
705
753
|
export function toHTML(scan, meta = {}) {
|
|
706
|
-
// Attach the "likely lower risk" note
|
|
707
|
-
// render
|
|
708
|
-
const findings = normalizeFindings(scan).map(f =>
|
|
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
|
+
});
|
|
709
760
|
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
710
761
|
for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
711
762
|
const stride = {};
|
|
@@ -773,6 +824,9 @@ export function toHTML(scan, meta = {}) {
|
|
|
773
824
|
.f-body{display:none;margin-top:12px;padding-top:12px;border-top:1px solid #1e293b;font-size:12px}
|
|
774
825
|
.f.expanded .f-body{display:block}
|
|
775
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}
|
|
776
830
|
.f-fix{background:#0d1f3d;border-left:3px solid #38bdf8;padding:8px 12px;margin-top:8px;border-radius:0 4px 4px 0}
|
|
777
831
|
.hidden{display:none!important}
|
|
778
832
|
</style></head>
|
|
@@ -824,6 +878,8 @@ function makeCard(f) {
|
|
|
824
878
|
(f._riskNote ? '<span class="f-note" title="The reachability / confidence pipeline marked this down from its rule-default severity">↓ ' + esc(f._riskNote) + '</span>' : '') +
|
|
825
879
|
'</div>' +
|
|
826
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>' : '') +
|
|
827
883
|
(f.snippet ? '<pre>' + esc(f.snippet) + '</pre>' : '') +
|
|
828
884
|
(f.masked ? '<pre style="color:#f97316">' + esc(f.masked) + ' (masked)</pre>' : '') +
|
|
829
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>' : '') +
|
|
@@ -926,10 +982,12 @@ export function toCLI(scan, { verbose=false, color=true }={}){
|
|
|
926
982
|
const rn = riskNote(f);
|
|
927
983
|
if (rn) lines.push(` ${c('↓ ' + rn, '\x1b[2;33m')}`);
|
|
928
984
|
if (f.masked) lines.push(` ${c('value:', DIM)} ${f.masked}`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
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)}`);
|
|
933
991
|
}
|
|
934
992
|
lines.push('');
|
|
935
993
|
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
@@ -1155,6 +1213,9 @@ export function toProTable(scan, options = {}) {
|
|
|
1155
1213
|
else lines.push(`${sev} ${where} ${cwe} ${cvss} ${owasp} ${vuln} ${conf}`);
|
|
1156
1214
|
const rn = riskNote(f);
|
|
1157
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));
|
|
1158
1219
|
}
|
|
1159
1220
|
|
|
1160
1221
|
// Footer counts.
|