@clear-capabilities/agentic-security-scanner 0.124.1 → 0.128.1
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 +206 -0
- package/bin/agentic-security.js +75 -2
- package/dist/11.index.js +353 -0
- package/dist/113.index.js +525 -0
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +193 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2406 -0
- package/dist/449.index.js +135 -0
- package/dist/637.index.js +1 -1
- package/dist/752.index.js +7 -4
- package/dist/801.index.js +87 -0
- package/dist/826.index.js +4 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +1 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -6
- package/src/engine.js +31 -1
- package/src/integrations/tickets.js +9 -3
- package/src/ir/CLAUDE.md +22 -17
- package/src/llm-validator/index.js +47 -12
- package/src/mcp/tools.js +108 -3
- package/src/posture/CLAUDE.md +10 -1
- package/src/posture/cache-economics.js +7 -4
- package/src/posture/deterministic-fix.js +65 -0
- package/src/posture/entrypoint-inventory.js +248 -0
- package/src/posture/falsification.js +121 -0
- package/src/posture/fix-honesty-gate.js +175 -0
- package/src/posture/fix-verify.js +18 -3
- package/src/posture/model-routing.js +126 -0
- package/src/posture/mttr.js +25 -0
- package/src/posture/provider-catalog.js +108 -0
- package/src/posture/root-cause-sweep.js +262 -0
- package/src/posture/secret-live-check.js +71 -0
- package/src/pr-comment.js +3 -1
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/api-authz.js +36 -0
- package/src/sast/file-upload.js +118 -0
- package/src/sast/llm-cost-advisor.js +88 -0
- package/src/util/untrusted.js +148 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const id = 449;
|
|
2
|
+
export const ids = [449];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 5830:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ buildBaselineMap: () => (/* binding */ buildBaselineMap),
|
|
10
|
+
/* harmony export */ renderSlaSummary: () => (/* binding */ renderSlaSummary),
|
|
11
|
+
/* harmony export */ stampFindingTimestamps: () => (/* binding */ stampFindingTimestamps)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/* unused harmony exports findingsExceedingSLA, computeMTTR */
|
|
14
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
15
|
+
// 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
|
|
16
|
+
//
|
|
17
|
+
// Stamps every finding with `firstSeenAt` (preserved from the baseline if the
|
|
18
|
+
// finding existed previously) and `lastSeenAt` (the current scan time). Surfaces
|
|
19
|
+
// findings exceeding an SLA threshold per severity.
|
|
20
|
+
//
|
|
21
|
+
// Pure function — does not write to disk. The caller (CLI / fix workflow) decides
|
|
22
|
+
// when to persist firstSeenAt back into the baseline.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
|
|
27
|
+
function _fingerprint(f) {
|
|
28
|
+
const file = (f.file || '').split(' -> ').pop();
|
|
29
|
+
const line = f.line || f.source?.line || f.sink?.line || 0;
|
|
30
|
+
const vuln = (f.vuln || f.type || '').replace(/\W+/g, '_').toLowerCase();
|
|
31
|
+
const cwe = (f.cwe || '').toUpperCase();
|
|
32
|
+
return node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(`${file}:${line}:${vuln}:${cwe}`).digest('hex').slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Stamp findings in-place with firstSeenAt / lastSeenAt / ageDays.
|
|
36
|
+
// `findings` — current scan findings (will be mutated).
|
|
37
|
+
// `baselineMap` — optional Map of fingerprint → { firstSeenAt }. Pass an empty Map for first run.
|
|
38
|
+
// `now` — Date.now() at scan time (allow injection for tests).
|
|
39
|
+
function stampFindingTimestamps(findings, baselineMap = new Map(), now = Date.now()) {
|
|
40
|
+
const nowIso = new Date(now).toISOString();
|
|
41
|
+
for (const f of findings) {
|
|
42
|
+
const fp = _fingerprint(f);
|
|
43
|
+
f._fp = fp;
|
|
44
|
+
const prev = baselineMap.get(fp);
|
|
45
|
+
f.firstSeenAt = prev?.firstSeenAt || nowIso;
|
|
46
|
+
f.lastSeenAt = nowIso;
|
|
47
|
+
const firstMs = Date.parse(f.firstSeenAt);
|
|
48
|
+
f.ageDays = Math.max(0, Math.floor((now - firstMs) / 86400000));
|
|
49
|
+
}
|
|
50
|
+
return findings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build a baseline map from an existing baseline JSON (or scan JSON shape).
|
|
54
|
+
// Recognised top-level: { findings, secrets, supplyChain }. Each entry retains
|
|
55
|
+
// firstSeenAt if it had one previously.
|
|
56
|
+
function buildBaselineMap(baselineJson) {
|
|
57
|
+
const map = new Map();
|
|
58
|
+
const all = [
|
|
59
|
+
...(baselineJson?.findings || []),
|
|
60
|
+
...(baselineJson?.secrets || []),
|
|
61
|
+
...(baselineJson?.supplyChain || []).filter(s => s.type === 'vulnerable_dep'),
|
|
62
|
+
];
|
|
63
|
+
for (const f of all) {
|
|
64
|
+
const fp = _fingerprint(f);
|
|
65
|
+
if (f.firstSeenAt) map.set(fp, { firstSeenAt: f.firstSeenAt });
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Identify findings exceeding an SLA threshold.
|
|
71
|
+
// slaDays: { critical: 7, high: 30, medium: 60, low: 90, info: 180 } (default).
|
|
72
|
+
function findingsExceedingSLA(findings, slaDays = null) {
|
|
73
|
+
const SLA = slaDays || { critical: 7, high: 30, medium: 60, low: 90, info: 180 };
|
|
74
|
+
return findings.filter(f => {
|
|
75
|
+
const limit = SLA[f.severity] ?? 90;
|
|
76
|
+
return (f.ageDays || 0) > limit;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Median age (days) of the currently-open findings — a single-scan proxy for
|
|
81
|
+
// "how long has this debt been sitting". True MTTR (computeMTTR) needs the set
|
|
82
|
+
// of findings that were FIXED; this reports the open backlog's median age so a
|
|
83
|
+
// scan can show whether debt is getting older. Returns null on empty input.
|
|
84
|
+
// Local — surfaced only through renderSlaSummary (its sole consumer).
|
|
85
|
+
function medianOpenAgeDays(findings) {
|
|
86
|
+
const ages = (findings || []).map(f => f.ageDays || 0).sort((a, b) => a - b);
|
|
87
|
+
if (!ages.length) return null;
|
|
88
|
+
return ages[Math.floor(ages.length / 2)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// One-line SLA-breach summary for surfacing after a scan (#10). Returns null
|
|
92
|
+
// when nothing is past its per-severity SLA. Pairs with medianOpenAgeDays for a
|
|
93
|
+
// "is my security debt aging" readout that the vibecoder can act on.
|
|
94
|
+
function renderSlaSummary(findings, slaDays = null) {
|
|
95
|
+
const breached = findingsExceedingSLA(findings || [], slaDays);
|
|
96
|
+
if (!breached.length) return null;
|
|
97
|
+
const bySev = {};
|
|
98
|
+
for (const f of breached) bySev[f.severity] = (bySev[f.severity] || 0) + 1;
|
|
99
|
+
const parts = ['critical', 'high', 'medium', 'low', 'info'].filter(s => bySev[s]).map(s => `${bySev[s]} ${s}`);
|
|
100
|
+
const median = medianOpenAgeDays(findings);
|
|
101
|
+
const ageNote = median != null ? ` (median open age ${median}d)` : '';
|
|
102
|
+
return `${breached.length} finding(s) past remediation SLA: ${parts.join(', ')}${ageNote}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Compute MTTR statistics from a series of saved scans (each with firstSeen/lastSeen).
|
|
106
|
+
// Useful for trend reporting.
|
|
107
|
+
function computeMTTR(removedFindings) {
|
|
108
|
+
// removedFindings: findings that existed in baseline but no longer in current
|
|
109
|
+
// (i.e., were fixed). Each carries firstSeenAt and lastSeenAt from the baseline.
|
|
110
|
+
if (!removedFindings.length) return { count: 0, meanDays: null, medianDays: null, perSeverity: {} };
|
|
111
|
+
const ages = removedFindings.map(f => {
|
|
112
|
+
const first = Date.parse(f.firstSeenAt || 0);
|
|
113
|
+
const last = Date.parse(f.lastSeenAt || 0);
|
|
114
|
+
return Math.max(0, (last - first) / 86400000);
|
|
115
|
+
}).sort((a, b) => a - b);
|
|
116
|
+
const meanDays = ages.reduce((s, x) => s + x, 0) / ages.length;
|
|
117
|
+
const medianDays = ages[Math.floor(ages.length / 2)];
|
|
118
|
+
const perSeverity = {};
|
|
119
|
+
for (const f of removedFindings) {
|
|
120
|
+
const sev = f.severity || 'medium';
|
|
121
|
+
(perSeverity[sev] = perSeverity[sev] || []).push(
|
|
122
|
+
Math.max(0, (Date.parse(f.lastSeenAt || 0) - Date.parse(f.firstSeenAt || 0)) / 86400000)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
for (const k of Object.keys(perSeverity)) {
|
|
126
|
+
const a = perSeverity[k];
|
|
127
|
+
perSeverity[k] = { count: a.length, meanDays: a.reduce((s,x)=>s+x,0)/a.length };
|
|
128
|
+
}
|
|
129
|
+
return { count: removedFindings.length, meanDays, medianDays, perSeverity };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/***/ })
|
|
134
|
+
|
|
135
|
+
};
|
package/dist/637.index.js
CHANGED
|
@@ -11,7 +11,7 @@ export const modules = {
|
|
|
11
11
|
/* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
|
|
12
12
|
/* harmony export */ });
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
|
|
14
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8215);
|
|
15
15
|
// Shadowscan / security-DELTA on PR (v0.72).
|
|
16
16
|
//
|
|
17
17
|
// Most SAST PR-comment integrations show absolute counts — "12 findings
|
package/dist/752.index.js
CHANGED
|
@@ -42,9 +42,11 @@ function money(n) {
|
|
|
42
42
|
|
|
43
43
|
// Per-1M-token rates (input / output). Mirror hooks/model-cost-advisor.js MODELS.
|
|
44
44
|
const MODEL_RATES = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
fable: { label: 'Fable 5', in: 10, out: 50 },
|
|
46
|
+
opus: { label: 'Opus 4.8', in: 5, out: 25 },
|
|
47
|
+
sonnet5: { label: 'Sonnet 5', in: 3, out: 15 },
|
|
48
|
+
sonnet: { label: 'Sonnet 4.6', in: 3, out: 15 },
|
|
49
|
+
haiku: { label: 'Haiku 4.5', in: 1, out: 5 },
|
|
48
50
|
};
|
|
49
51
|
const CACHE_READ_MULT = 0.1; // cache read ≈ 0.1× input
|
|
50
52
|
const CACHE_WRITE_MULT = 1.25; // 5-minute cache write ≈ 1.25× input
|
|
@@ -56,8 +58,9 @@ const TTL_MS = 5 * 60 * 1000;
|
|
|
56
58
|
function rateFor(model) {
|
|
57
59
|
if (typeof model !== 'string') return null;
|
|
58
60
|
const s = model.toLowerCase();
|
|
61
|
+
if (s.includes('fable') || s.includes('mythos')) return MODEL_RATES.fable;
|
|
59
62
|
if (s.includes('haiku')) return MODEL_RATES.haiku;
|
|
60
|
-
if (s.includes('sonnet')) return MODEL_RATES.sonnet;
|
|
63
|
+
if (s.includes('sonnet')) return (s.includes('sonnet-5') || s.includes('sonnet 5')) ? MODEL_RATES.sonnet5 : MODEL_RATES.sonnet;
|
|
61
64
|
if (s.includes('opus')) return MODEL_RATES.opus;
|
|
62
65
|
return null;
|
|
63
66
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const id = 801;
|
|
2
|
+
export const ids = [801];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 9801:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ checkSecretLive: () => (/* binding */ checkSecretLive)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export _internal */
|
|
12
|
+
// Live-secret validation (#22) — label a detected secret live | dead | unknown.
|
|
13
|
+
//
|
|
14
|
+
// "This Stripe/GitHub key is LIVE and was committed 40 commits ago" is a P0 the
|
|
15
|
+
// vibecoder must rotate now; "you have a high-entropy string" is noise. This
|
|
16
|
+
// closes that gap for the providers with a cheap, read-only "whoami" check.
|
|
17
|
+
//
|
|
18
|
+
// STRICTLY opt-in (a --validate-secrets flag / AGENTIC_SECURITY_VALIDATE_SECRETS)
|
|
19
|
+
// and OFFLINE-DEGRADING: any network error, timeout, or unrecognized provider
|
|
20
|
+
// yields 'unknown' — never a false 'dead'. No runtime cloud calls by default,
|
|
21
|
+
// per the scanner's no-network-by-default convention. The request builder is
|
|
22
|
+
// pure (no I/O) so it's testable without hitting a provider.
|
|
23
|
+
|
|
24
|
+
// Map a detected secret to a read-only validation request, or null when we have
|
|
25
|
+
// no safe check for that provider. Only providers whose token is a self-
|
|
26
|
+
// contained bearer/token credential (no signing, no extra params) are covered.
|
|
27
|
+
function buildLiveCheckRequest(secret) {
|
|
28
|
+
const val = (secret && (secret.match || secret.value || secret.secret || secret.token)) || '';
|
|
29
|
+
if (typeof val !== 'string' || val.length < 8) return null;
|
|
30
|
+
|
|
31
|
+
// GitHub PAT / OAuth token → GET /user (200 = live, 401 = dead).
|
|
32
|
+
if (/^gh[posru]_[A-Za-z0-9]{20,}$/.test(val) || /^github_pat_[A-Za-z0-9_]{20,}$/.test(val)) {
|
|
33
|
+
return { provider: 'github', method: 'GET', url: 'https://api.github.com/user',
|
|
34
|
+
headers: { Authorization: `token ${val}`, 'User-Agent': 'agentic-security', Accept: 'application/vnd.github+json' } };
|
|
35
|
+
}
|
|
36
|
+
// Stripe secret key → GET /v1/account (200 = live, 401 = dead).
|
|
37
|
+
if (/^sk_live_[A-Za-z0-9]{16,}$/.test(val) || /^rk_live_[A-Za-z0-9]{16,}$/.test(val)) {
|
|
38
|
+
return { provider: 'stripe', method: 'GET', url: 'https://api.stripe.com/v1/account',
|
|
39
|
+
headers: { Authorization: `Bearer ${val}` } };
|
|
40
|
+
}
|
|
41
|
+
// OpenAI key → GET /v1/models.
|
|
42
|
+
if (/^sk-[A-Za-z0-9]{20,}$/.test(val) && !/^sk_live_/.test(val)) {
|
|
43
|
+
return { provider: 'openai', method: 'GET', url: 'https://api.openai.com/v1/models',
|
|
44
|
+
headers: { Authorization: `Bearer ${val}` } };
|
|
45
|
+
}
|
|
46
|
+
// SendGrid key → GET /v3/scopes.
|
|
47
|
+
if (/^SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}$/.test(val)) {
|
|
48
|
+
return { provider: 'sendgrid', method: 'GET', url: 'https://api.sendgrid.com/v3/scopes',
|
|
49
|
+
headers: { Authorization: `Bearer ${val}` } };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Classify an HTTP status into a liveness verdict. 200-2xx = live; 401/403 =
|
|
55
|
+
// dead (rejected credential); anything else = unknown (rate-limit, 5xx, etc. —
|
|
56
|
+
// we don't know, so don't claim dead).
|
|
57
|
+
function classifyStatus(status) {
|
|
58
|
+
if (status >= 200 && status < 300) return 'live';
|
|
59
|
+
if (status === 401 || status === 403) return 'dead';
|
|
60
|
+
return 'unknown';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Perform the validation. Returns { verdict: 'live'|'dead'|'unknown', provider }.
|
|
64
|
+
// Offline-degrading: on any error/timeout, verdict is 'unknown'.
|
|
65
|
+
async function checkSecretLive(secret, { timeoutMs = 4000 } = {}) {
|
|
66
|
+
const req = buildLiveCheckRequest(secret);
|
|
67
|
+
if (!req) return { verdict: 'unknown', provider: null };
|
|
68
|
+
const ctrl = new AbortController();
|
|
69
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
const r = await fetch(req.url, { method: req.method, headers: req.headers, signal: ctrl.signal });
|
|
72
|
+
return { verdict: classifyStatus(r.status), provider: req.provider };
|
|
73
|
+
} catch {
|
|
74
|
+
return { verdict: 'unknown', provider: req.provider };
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(t);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Pure surfaces exposed for tests (no network) — kept off the public API so the
|
|
81
|
+
// dead-module guard doesn't flag them; `checkSecretLive` is the wired entry.
|
|
82
|
+
const _internal = { buildLiveCheckRequest, classifyStatus };
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
/***/ })
|
|
86
|
+
|
|
87
|
+
};
|
package/dist/826.index.js
CHANGED
|
@@ -9,6 +9,7 @@ export const modules = {
|
|
|
9
9
|
/* harmony export */ renderPrComment: () => (/* binding */ renderPrComment)
|
|
10
10
|
/* harmony export */ });
|
|
11
11
|
/* unused harmony export _internal */
|
|
12
|
+
/* harmony import */ var _util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7097);
|
|
12
13
|
// Advisor-tone PR comment renderer (v0.72).
|
|
13
14
|
//
|
|
14
15
|
// Replaces the typical "12 findings detected, see SARIF" wall of text
|
|
@@ -36,6 +37,8 @@ export const modules = {
|
|
|
36
37
|
// route through an LLM for richer prose when AGENTIC_SECURITY_LLM_ENDPOINT
|
|
37
38
|
// is configured.
|
|
38
39
|
|
|
40
|
+
|
|
41
|
+
|
|
39
42
|
const SEVERITY_GLYPH = {
|
|
40
43
|
critical: '🟥',
|
|
41
44
|
high: '🟧',
|
|
@@ -147,7 +150,7 @@ function renderPrComment(delta, { repoName, prNumber, prTitle } = {}) {
|
|
|
147
150
|
const sev = SEVERITY_GLYPH[f.severity] || '⬜';
|
|
148
151
|
const route = _route(f);
|
|
149
152
|
const where = route ? `\`${route}\` (\`${f.file}:${f.line}\`)` : `\`${f.file}:${f.line}\``;
|
|
150
|
-
lines.push(`${sev} **${meta?.name || f.vuln}** — ${where}`);
|
|
153
|
+
lines.push(`${sev} **${meta?.name || (0,_util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__/* .escapeMarkdown */ .FV)(f.vuln)}** — ${where}`);
|
|
151
154
|
if (meta) lines.push(` > ${meta.why}`);
|
|
152
155
|
if (f.remediation) {
|
|
153
156
|
const onelineFix = String(f.remediation).split('\n')[0].slice(0, 240);
|
package/dist/838.index.js
CHANGED
|
@@ -14,7 +14,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
14
14
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
15
15
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
16
16
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
17
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
17
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3793);
|
|
18
18
|
// Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
|
|
19
19
|
//
|
|
20
20
|
// Given a candidate patch (the new file content + the finding stableId being
|