@clear-capabilities/agentic-security-scanner 0.79.0 → 0.84.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/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/839.index.js +170 -0
- package/dist/985.index.js +140 -1
- package/dist/agentic-security.mjs +10 -10
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -5
- package/src/.agentic-security/findings.json +117732 -0
- package/src/.agentic-security/last-scan.json +117732 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +12946 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +250 -0
- package/src/dataflow/.agentic-security/streak.json +21 -0
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/formal-verify.js +204 -0
- package/src/dataflow/ifds-precise.js +222 -0
- package/src/dataflow/k2-summary-cache.js +153 -0
- package/src/dataflow/lib-taint-summaries.js +198 -0
- package/src/dataflow/privacy-taint.js +205 -0
- package/src/dataflow/smt-feasibility.js +189 -0
- package/src/engine.js +825 -127
- package/src/ir/.agentic-security/findings.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +193 -0
- package/src/ir/.agentic-security/streak.json +20 -0
- package/src/ir/cpp-preprocessor.js +142 -0
- package/src/ir/csharp-ir.js +604 -0
- package/src/ir/universal-ir.js +403 -0
- package/src/mcp/.agentic-security/findings.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +331 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/tools.js +140 -1
- package/src/posture/.agentic-security/findings.json +77181 -0
- package/src/posture/.agentic-security/last-scan.json +77181 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +8904 -0
- package/src/posture/.agentic-security/streak.json +21 -0
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/auditor-walkthrough.js +252 -0
- package/src/posture/claude-authorship.js +197 -0
- package/src/posture/compliance-frameworks/.agentic-security/findings.json +80 -0
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +80 -0
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +90 -0
- package/src/posture/compliance-frameworks/.agentic-security/streak.json +22 -0
- package/src/posture/compliance-frameworks/ccpa.json +32 -0
- package/src/posture/compliance-frameworks/eu-ai-act.json +51 -0
- package/src/posture/compliance-frameworks/gdpr.json +45 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/src/posture/compliance-frameworks/nist-csf-2.json +73 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/cross-repo-memory.js +180 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/dep-add-guard.js +197 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/findings-memory.js +152 -0
- package/src/posture/fix-style-mirror.js +118 -0
- package/src/posture/git-history.js +141 -0
- package/src/posture/intent-context.js +175 -0
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/model-rescan.js +76 -0
- package/src/posture/pattern-propagation.js +39 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/pr-augment.js +234 -0
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/risk-dollars.js +158 -0
- package/src/posture/runtime-correlation.js +174 -0
- package/src/posture/sbom-diff.js +171 -0
- package/src/posture/sca-policy.js +235 -0
- package/src/posture/sca-upgrade.js +259 -0
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/threat-model-grounding.js +169 -0
- package/src/posture/time-to-fix.js +129 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage-memory.js +151 -0
- package/src/posture/triage.js +40 -1
- package/src/posture/watch-mode.js +171 -0
- package/src/posture/workflow-installer.js +231 -0
- package/src/sast/.agentic-security/findings.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +941 -0
- package/src/sast/.agentic-security/streak.json +22 -0
- package/src/sast/_secret-entropy.js +145 -0
- package/src/sast/cloud-iam.js +312 -0
- package/src/sast/cpp.js +138 -4
- package/src/sast/crypto-protocol.js +388 -0
- package/src/sast/csharp-tokenizer.js +392 -0
- package/src/sast/csharp.js +924 -138
- package/src/sast/dapp-frontend.js +200 -0
- package/src/sast/k8s-admission.js +271 -0
- package/src/sast/llm-app.js +272 -0
- package/src/sast/ml-supply-chain.js +259 -0
- package/src/sast/mobile.js +224 -0
- package/src/sast/post-quantum-crypto.js +348 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +113 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +37 -15
- package/src/sca/sigstore-verify.js +215 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Auto-generated threat model — Recommendation #2 of the world-class+2 plan.
|
|
2
|
+
//
|
|
3
|
+
// Builds a STRIDE threat model from the scan's findings + IR + privacy
|
|
4
|
+
// taint. Outputs Mermaid diagrams + per-asset attack trees grounded in
|
|
5
|
+
// actual scanner evidence. The model is "live" — regenerated every scan
|
|
6
|
+
// so it can't bit-rot.
|
|
7
|
+
//
|
|
8
|
+
// Pipeline:
|
|
9
|
+
// 1. Identify external entities — every route, message-queue consumer,
|
|
10
|
+
// file ingest, S3 listener, etc.
|
|
11
|
+
// 2. Identify trust boundaries — entity → handler boundary, internal-
|
|
12
|
+
// service → external-service boundary
|
|
13
|
+
// 3. Identify assets — every PII/PHI/PCI field, every credential, every
|
|
14
|
+
// authoritative DB/cache, every secret-bearing service
|
|
15
|
+
// 4. Apply STRIDE per (entity, asset) pair using template-driven rules
|
|
16
|
+
// 5. Generate attack trees rooted at each high-value asset with leaves
|
|
17
|
+
// grounded in actual scanner findings
|
|
18
|
+
//
|
|
19
|
+
// Output:
|
|
20
|
+
// { entities, boundaries, assets, threats, attackTrees, mermaid }
|
|
21
|
+
//
|
|
22
|
+
// Persisted to .agentic-security/threat-model.json (machine-readable) and
|
|
23
|
+
// .agentic-security/threat-model.md (human-readable).
|
|
24
|
+
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
|
|
28
|
+
// STRIDE category descriptors
|
|
29
|
+
const STRIDE = {
|
|
30
|
+
S: { label: 'Spoofing', control: 'Authentication / Identity' },
|
|
31
|
+
T: { label: 'Tampering', control: 'Integrity / Authorization' },
|
|
32
|
+
R: { label: 'Repudiation', control: 'Audit logs / Non-repudiation' },
|
|
33
|
+
I: { label: 'Information Disclosure', control: 'Confidentiality / Encryption' },
|
|
34
|
+
D: { label: 'Denial of Service',control: 'Availability / Rate limiting' },
|
|
35
|
+
E: { label: 'Elevation of Privilege', control: 'Authorization / Least privilege' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Map CWE → STRIDE categories. Multi-mapping is allowed.
|
|
39
|
+
const CWE_TO_STRIDE = {
|
|
40
|
+
'CWE-22': ['I'], // path-traversal
|
|
41
|
+
'CWE-78': ['E', 'T'], // command-injection
|
|
42
|
+
'CWE-79': ['T'], // xss
|
|
43
|
+
'CWE-89': ['T', 'I'], // sql-injection
|
|
44
|
+
'CWE-90': ['T'], // ldap-injection
|
|
45
|
+
'CWE-94': ['E'], // code-injection
|
|
46
|
+
'CWE-113': ['T'], // header injection
|
|
47
|
+
'CWE-134': ['I'], // format-string
|
|
48
|
+
'CWE-200': ['I'], // information-exposure
|
|
49
|
+
'CWE-287': ['S'], // improper authentication
|
|
50
|
+
'CWE-307': ['S'], // brute-force
|
|
51
|
+
'CWE-327': ['I'], // weak-crypto
|
|
52
|
+
'CWE-330': ['S'], // weak-rng
|
|
53
|
+
'CWE-352': ['T'], // csrf
|
|
54
|
+
'CWE-359': ['I'], // private-info exposure
|
|
55
|
+
'CWE-415': ['D', 'E'], // double-free
|
|
56
|
+
'CWE-416': ['D', 'E'], // use-after-free
|
|
57
|
+
'CWE-434': ['E'], // file upload
|
|
58
|
+
'CWE-502': ['E'], // insecure-deserialization
|
|
59
|
+
'CWE-601': ['T'], // open-redirect
|
|
60
|
+
'CWE-611': ['I'], // xxe
|
|
61
|
+
'CWE-639': ['E'], // IDOR
|
|
62
|
+
'CWE-643': ['T'], // xpath injection
|
|
63
|
+
'CWE-798': ['I', 'S'], // hardcoded-secret
|
|
64
|
+
'CWE-918': ['I', 'E'], // ssrf
|
|
65
|
+
'CWE-1004':['I'], // missing cookie hardening
|
|
66
|
+
'CWE-1321':['T', 'E'], // prototype-pollution
|
|
67
|
+
'CWE-1333':['D'], // ReDoS
|
|
68
|
+
'CWE-1427':['T'], // prompt injection
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function _stridesForFinding(f) {
|
|
72
|
+
const c = f.cwe || '';
|
|
73
|
+
if (CWE_TO_STRIDE[c]) return CWE_TO_STRIDE[c];
|
|
74
|
+
// Family-based fallback
|
|
75
|
+
if (f.family === 'sql-injection') return ['T', 'I'];
|
|
76
|
+
if (f.family === 'command-injection') return ['E', 'T'];
|
|
77
|
+
if (f.family === 'xss') return ['T'];
|
|
78
|
+
if (f.family === 'hardcoded-secret') return ['I'];
|
|
79
|
+
return ['T'];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the threat-model graph. `scan` is the engine's scan result
|
|
84
|
+
* structure (findings + routes + supplyChain).
|
|
85
|
+
*/
|
|
86
|
+
export function buildThreatModel(scan, opts = {}) {
|
|
87
|
+
const entities = []; // external entities: routes, queue consumers, file ingest
|
|
88
|
+
const boundaries = []; // trust boundary edges
|
|
89
|
+
const assets = []; // valuables: PII, credentials, DBs
|
|
90
|
+
const threats = []; // (entity, asset, stride) tuples
|
|
91
|
+
|
|
92
|
+
// Step 1: external entities — routes
|
|
93
|
+
for (const r of (scan.routes || [])) {
|
|
94
|
+
entities.push({
|
|
95
|
+
kind: 'http-route',
|
|
96
|
+
id: `route:${r.method || 'ANY'}:${r.path || r.file + ':' + r.line}`,
|
|
97
|
+
method: r.method || 'ANY',
|
|
98
|
+
path: r.path || null,
|
|
99
|
+
file: r.file, line: r.line,
|
|
100
|
+
requiresAuth: !!r.requiresAuth,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// External entities — message queues / consumers / file ingest (best-effort
|
|
104
|
+
// heuristic from findings).
|
|
105
|
+
for (const f of (scan.findings || [])) {
|
|
106
|
+
if (/kafka|sqs|sns|rabbit|pubsub|kinesis/i.test(f.snippet || '')) {
|
|
107
|
+
entities.push({ kind: 'queue-consumer', id: `queue:${f.file}:${f.line}`, file: f.file, line: f.line });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 2: assets — PII / credentials / DBs
|
|
112
|
+
for (const f of (scan.findings || [])) {
|
|
113
|
+
if (f.family === 'hardcoded-secret') {
|
|
114
|
+
assets.push({ kind: 'credential', id: `cred:${f.file}:${f.line}`, file: f.file, line: f.line, name: (f.vuln||'').slice(0, 80) });
|
|
115
|
+
}
|
|
116
|
+
if (f.family === 'pii-exposure') {
|
|
117
|
+
assets.push({ kind: 'pii', id: `pii:${f.file}:${f.line}`, file: f.file, line: f.line, classes: f.piiClass || [] });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// DB-shaped assets: routes that touch SQL.
|
|
121
|
+
const dbAsset = { kind: 'datastore', id: 'datastore:default', name: 'Application Database' };
|
|
122
|
+
let hasDbFinding = false;
|
|
123
|
+
for (const f of (scan.findings || [])) {
|
|
124
|
+
if (f.family === 'sql-injection' || /SqlCommand|prepareStatement|EntityManager/i.test(f.snippet || '')) {
|
|
125
|
+
hasDbFinding = true; break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (hasDbFinding) assets.push(dbAsset);
|
|
129
|
+
|
|
130
|
+
// Step 3: trust boundaries
|
|
131
|
+
for (const e of entities) {
|
|
132
|
+
boundaries.push({ from: 'external', to: e.id, kind: 'trust-boundary', requiresAuth: e.requiresAuth });
|
|
133
|
+
}
|
|
134
|
+
for (const a of assets) {
|
|
135
|
+
boundaries.push({ from: 'application', to: a.id, kind: 'asset-boundary' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Step 4: STRIDE per finding (each finding implies one or more threats)
|
|
139
|
+
for (const f of (scan.findings || [])) {
|
|
140
|
+
const sts = _stridesForFinding(f);
|
|
141
|
+
for (const st of sts) {
|
|
142
|
+
threats.push({
|
|
143
|
+
stride: st,
|
|
144
|
+
strideLabel: STRIDE[st]?.label || st,
|
|
145
|
+
cwe: f.cwe || null,
|
|
146
|
+
family: f.family,
|
|
147
|
+
severity: f.severity,
|
|
148
|
+
file: f.file, line: f.line,
|
|
149
|
+
vuln: f.vuln,
|
|
150
|
+
finding_id: f.id,
|
|
151
|
+
affectsAsset: assets.find(a =>
|
|
152
|
+
(a.kind === 'credential' && f.family === 'hardcoded-secret') ||
|
|
153
|
+
(a.kind === 'pii' && f.family === 'pii-exposure') ||
|
|
154
|
+
(a.kind === 'datastore' && (f.family === 'sql-injection' || f.family === 'insecure-deserialization'))
|
|
155
|
+
)?.id || null,
|
|
156
|
+
atEntity: entities.find(e => e.file === f.file && Math.abs((e.line||0) - (f.line||0)) <= 50)?.id || null,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Step 5: attack trees — per high-value asset
|
|
162
|
+
const attackTrees = assets.map(a => {
|
|
163
|
+
const leaves = threats
|
|
164
|
+
.filter(t => t.affectsAsset === a.id)
|
|
165
|
+
.map(t => ({
|
|
166
|
+
label: `${t.strideLabel} via ${t.family} (${t.cwe || '—'})`,
|
|
167
|
+
severity: t.severity,
|
|
168
|
+
file: t.file, line: t.line,
|
|
169
|
+
finding_id: t.finding_id,
|
|
170
|
+
}));
|
|
171
|
+
return {
|
|
172
|
+
root: `Compromise ${a.kind}: ${a.name || a.id}`,
|
|
173
|
+
asset_id: a.id,
|
|
174
|
+
leaves,
|
|
175
|
+
severity: leaves.some(l => l.severity === 'critical') ? 'critical'
|
|
176
|
+
: leaves.some(l => l.severity === 'high') ? 'high' : 'medium',
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return { entities, boundaries, assets, threats, attackTrees };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Render the model as a Mermaid flowchart for visual review.
|
|
185
|
+
*/
|
|
186
|
+
export function renderMermaid(model) {
|
|
187
|
+
const lines = ['flowchart TB'];
|
|
188
|
+
lines.push(' subgraph External');
|
|
189
|
+
for (const e of model.entities.slice(0, 30)) {
|
|
190
|
+
lines.push(` ${_mid(e.id)}["${e.kind}: ${e.method || ''} ${e.path || (e.file||'') + ':' + (e.line||'')}"]`);
|
|
191
|
+
}
|
|
192
|
+
lines.push(' end');
|
|
193
|
+
lines.push(' subgraph Application');
|
|
194
|
+
for (const a of model.assets.slice(0, 30)) {
|
|
195
|
+
lines.push(` ${_mid(a.id)}{{"${a.kind}: ${a.name || a.id}"}}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push(' end');
|
|
198
|
+
for (const b of model.boundaries.slice(0, 100)) {
|
|
199
|
+
if (b.kind === 'trust-boundary') {
|
|
200
|
+
lines.push(` External --> ${_mid(b.to)}`);
|
|
201
|
+
} else if (b.kind === 'asset-boundary') {
|
|
202
|
+
lines.push(` ${_mid(b.from)} -.-> ${_mid(b.to)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _mid(id) { return String(id).replace(/[^A-Za-z0-9]/g, '_').slice(0, 60); }
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Persist threat model to disk: JSON for tooling, Markdown for review.
|
|
212
|
+
*/
|
|
213
|
+
export function persistThreatModel(scanRoot, model) {
|
|
214
|
+
const dir = path.join(scanRoot, '.agentic-security');
|
|
215
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
216
|
+
try { fs.writeFileSync(path.join(dir, 'threat-model.json'), JSON.stringify(model, null, 2)); } catch {}
|
|
217
|
+
try { fs.writeFileSync(path.join(dir, 'threat-model.md'), renderMarkdown(model)); } catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function renderMarkdown(model) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
lines.push('# Threat Model (auto-generated)');
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(`Generated by agentic-security on ${new Date().toISOString().slice(0,10)}.`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.');
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push('## Entities + boundaries');
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push('```mermaid');
|
|
231
|
+
lines.push(renderMermaid(model));
|
|
232
|
+
lines.push('```');
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push('## Assets');
|
|
235
|
+
lines.push('');
|
|
236
|
+
for (const a of model.assets.slice(0, 100)) {
|
|
237
|
+
lines.push(`- **${a.kind}**: ${a.name || a.id} — at \`${a.file || '(global)'}${a.line ? ':'+a.line : ''}\``);
|
|
238
|
+
}
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push('## STRIDE threats');
|
|
241
|
+
lines.push('');
|
|
242
|
+
const byStride = {};
|
|
243
|
+
for (const t of model.threats) (byStride[t.stride] ||= []).push(t);
|
|
244
|
+
for (const [st, threats] of Object.entries(byStride)) {
|
|
245
|
+
lines.push(`### ${STRIDE[st]?.label || st} (${threats.length})`);
|
|
246
|
+
lines.push('');
|
|
247
|
+
for (const t of threats.slice(0, 25)) {
|
|
248
|
+
lines.push(`- [${t.severity}] **${t.family}** (${t.cwe || '—'}) at \`${t.file}:${t.line}\` — ${t.vuln}`);
|
|
249
|
+
}
|
|
250
|
+
if (threats.length > 25) lines.push(`- … and ${threats.length - 25} more`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
}
|
|
253
|
+
lines.push('## Attack trees');
|
|
254
|
+
lines.push('');
|
|
255
|
+
for (const tree of model.attackTrees) {
|
|
256
|
+
lines.push(`### ${tree.root}`);
|
|
257
|
+
lines.push(`Severity rollup: **${tree.severity}**`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
for (const leaf of tree.leaves.slice(0, 20)) {
|
|
260
|
+
lines.push(`- [${leaf.severity}] ${leaf.label} — \`${leaf.file}:${leaf.line}\``);
|
|
261
|
+
}
|
|
262
|
+
if (tree.leaves.length > 20) lines.push(`- … and ${tree.leaves.length - 20} more leaves`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
}
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const _internals = { STRIDE, CWE_TO_STRIDE };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Threat-model-grounded prioritization.
|
|
2
|
+
//
|
|
3
|
+
// Reads project documentation (CLAUDE.md + docs/THREAT-MODEL.md + AGENTS.md)
|
|
4
|
+
// to extract the project's stated threat model and applies it to finding
|
|
5
|
+
// prioritization:
|
|
6
|
+
//
|
|
7
|
+
// - **Crown jewels** — file globs listed under "## Crown jewels" or
|
|
8
|
+
// "## Sensitive surfaces" boost severity by one tier when a finding
|
|
9
|
+
// lands there.
|
|
10
|
+
// - **Out-of-scope** — file globs under "## Out of scope" or "## Not in
|
|
11
|
+
// threat model" demote findings to low.
|
|
12
|
+
// - **Compliance regime** — declared under "## Compliance" (e.g.
|
|
13
|
+
// SOC2 / HIPAA / GDPR) adds compliance-tag fields to findings in
|
|
14
|
+
// matching families (PII → HIPAA/GDPR; auth → SOC2 CC6.1; etc.).
|
|
15
|
+
// - **Stated attacker** — "## Attacker model" / "## Threat actor"
|
|
16
|
+
// section sets f.attackerProfile = 'script-kiddie' | 'apt' | 'insider'
|
|
17
|
+
// for use in downstream prioritization.
|
|
18
|
+
//
|
|
19
|
+
// Opt-out: AGENTIC_SECURITY_NO_THREAT_MODEL_GROUNDING=1
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
|
|
24
|
+
const DOC_PATHS = [
|
|
25
|
+
'CLAUDE.md',
|
|
26
|
+
'docs/THREAT-MODEL.md',
|
|
27
|
+
'docs/threat-model.md',
|
|
28
|
+
'docs/THREATMODEL.md',
|
|
29
|
+
'THREAT-MODEL.md',
|
|
30
|
+
'.agentic-security/AGENTS.md',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function _readDoc(scanRoot, rel) {
|
|
34
|
+
try { return fs.readFileSync(path.join(scanRoot, rel), 'utf8'); } catch { return ''; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _allDocs(scanRoot) {
|
|
38
|
+
return DOC_PATHS.map(p => _readDoc(scanRoot, p)).join('\n\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _extractPathsFromSection(body, sectionRegex) {
|
|
42
|
+
const sec = body.match(sectionRegex);
|
|
43
|
+
if (!sec) return [];
|
|
44
|
+
const paths = [];
|
|
45
|
+
// Match `path/like/this/**`, "path/like", or list-item paths.
|
|
46
|
+
const re = /[`"]([\w./*?\-]+)[`"]|^\s*-\s+([\w./*?\-]+)/gm;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = re.exec(sec[0]))) {
|
|
49
|
+
const p = m[1] || m[2];
|
|
50
|
+
if (p && /[\/.]/.test(p)) paths.push(p);
|
|
51
|
+
}
|
|
52
|
+
return Array.from(new Set(paths));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _extractCompliance(body) {
|
|
56
|
+
const sec = body.match(/^#{1,3}\s+Compliance[\s\S]*?(?=\n#{1,3}\s|$(?![\s\S]))/im);
|
|
57
|
+
if (!sec) return [];
|
|
58
|
+
const found = new Set();
|
|
59
|
+
const re = /\b(SOC2|HIPAA|PCI[- ]DSS|GDPR|CCPA|FedRAMP|ISO[- ]?27001|NIST(?:[- ]?(?:CSF|800-53|AI 600-1))?|EU AI Act|OWASP (?:ASVS|LLM Top 10))\b/gi;
|
|
60
|
+
let m;
|
|
61
|
+
while ((m = re.exec(sec[0]))) found.add(m[1].toUpperCase().replace(/\s+/g, '-'));
|
|
62
|
+
return Array.from(found);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _extractAttacker(body) {
|
|
66
|
+
const sec = body.match(/^#{1,3}\s+(?:Attacker model|Threat actor|Adversary)[\s\S]*?(?=\n#{1,3}\s|$(?![\s\S]))/im);
|
|
67
|
+
if (!sec) return null;
|
|
68
|
+
const txt = sec[0].toLowerCase();
|
|
69
|
+
if (/\bapt\b|nation[- ]?state|sophisticated/.test(txt)) return 'apt';
|
|
70
|
+
if (/\binsider\b|employee|disgruntled/.test(txt)) return 'insider';
|
|
71
|
+
if (/script[- ]?kiddie|automated|opportunistic/.test(txt)) return 'script-kiddie';
|
|
72
|
+
return 'general';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _globMatch(pattern, p) {
|
|
76
|
+
const norm = String(p).replace(/\\/g, '/');
|
|
77
|
+
const re = new RegExp(
|
|
78
|
+
'^' + String(pattern).replace(/\\/g, '/')
|
|
79
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
80
|
+
.replace(/\*\*/g, '###DSTAR###')
|
|
81
|
+
.replace(/\*/g, '[^/]*')
|
|
82
|
+
.replace(/###DSTAR###/g, '.*')
|
|
83
|
+
+ '$',
|
|
84
|
+
);
|
|
85
|
+
return re.test(norm);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SEVERITY_RANK = ['info', 'low', 'medium', 'high', 'critical'];
|
|
89
|
+
|
|
90
|
+
function _bumpSeverity(sev) {
|
|
91
|
+
const i = SEVERITY_RANK.indexOf(sev);
|
|
92
|
+
if (i < 0 || i >= SEVERITY_RANK.length - 1) return sev;
|
|
93
|
+
return SEVERITY_RANK[i + 1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const FAMILY_TO_REGIME = {
|
|
97
|
+
'pii-exposure': ['HIPAA', 'GDPR'],
|
|
98
|
+
'training-data-pii': ['GDPR'],
|
|
99
|
+
'auth-missing': ['SOC2'],
|
|
100
|
+
'authz': ['SOC2'],
|
|
101
|
+
'idor': ['SOC2'],
|
|
102
|
+
'crypto-weak-cipher': ['PCI-DSS', 'FedRAMP'],
|
|
103
|
+
'crypto-tls-no-verify': ['PCI-DSS'],
|
|
104
|
+
'hardcoded-secret': ['SOC2', 'PCI-DSS'],
|
|
105
|
+
'k8s-rbac-cluster-admin': ['SOC2'],
|
|
106
|
+
'aws-public-s3': ['SOC2', 'GDPR'],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read project threat model from documentation. Cached per-scan-root via
|
|
111
|
+
* a module-level WeakMap-like... actually just pure read each time, since
|
|
112
|
+
* scan-time overhead is tiny.
|
|
113
|
+
*/
|
|
114
|
+
export function loadThreatModel(scanRoot) {
|
|
115
|
+
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL_GROUNDING === '1') {
|
|
116
|
+
return { crownJewels: [], outOfScope: [], compliance: [], attacker: null };
|
|
117
|
+
}
|
|
118
|
+
const body = _allDocs(scanRoot);
|
|
119
|
+
return {
|
|
120
|
+
crownJewels: _extractPathsFromSection(body, /^#{1,3}\s+(?:Crown jewels|Sensitive surfaces?)[\s\S]*?(?=\n#{1,3}\s|$(?![\s\S]))/im),
|
|
121
|
+
outOfScope: _extractPathsFromSection(body, /^#{1,3}\s+(?:Out of scope|Not in threat model)[\s\S]*?(?=\n#{1,3}\s|$(?![\s\S]))/im),
|
|
122
|
+
compliance: _extractCompliance(body),
|
|
123
|
+
attacker: _extractAttacker(body),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Annotator: applies the project's threat model to each finding.
|
|
129
|
+
*/
|
|
130
|
+
export function applyThreatModel(scanRoot, findings) {
|
|
131
|
+
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL_GROUNDING === '1') return { applied: 0 };
|
|
132
|
+
if (!Array.isArray(findings) || findings.length === 0) return { applied: 0 };
|
|
133
|
+
const tm = loadThreatModel(scanRoot);
|
|
134
|
+
if (!tm.crownJewels.length && !tm.outOfScope.length && !tm.compliance.length && !tm.attacker) {
|
|
135
|
+
return { applied: 0, reason: 'no-threat-model-found' };
|
|
136
|
+
}
|
|
137
|
+
let applied = 0;
|
|
138
|
+
for (const f of findings) {
|
|
139
|
+
const rel = f.file ? (path.isAbsolute(f.file) ? path.relative(scanRoot, f.file) : f.file) : '';
|
|
140
|
+
|
|
141
|
+
// Out-of-scope demotion wins over crown-jewel promotion.
|
|
142
|
+
if (rel && tm.outOfScope.some(g => _globMatch(g, rel))) {
|
|
143
|
+
f.threatModel = { ...(f.threatModel || {}), outOfScope: true };
|
|
144
|
+
f.severity = 'low';
|
|
145
|
+
applied++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (rel && tm.crownJewels.some(g => _globMatch(g, rel))) {
|
|
149
|
+
f.threatModel = { ...(f.threatModel || {}), crownJewel: true };
|
|
150
|
+
f.severity = _bumpSeverity(f.severity || 'medium');
|
|
151
|
+
applied++;
|
|
152
|
+
}
|
|
153
|
+
// Compliance regime tagging based on family.
|
|
154
|
+
const regimes = FAMILY_TO_REGIME[f.family];
|
|
155
|
+
if (regimes && tm.compliance.length) {
|
|
156
|
+
const matched = regimes.filter(r => tm.compliance.includes(r));
|
|
157
|
+
if (matched.length) {
|
|
158
|
+
f.threatModel = { ...(f.threatModel || {}), compliance: matched };
|
|
159
|
+
applied++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (tm.attacker) {
|
|
163
|
+
f.threatModel = { ...(f.threatModel || {}), attacker: tm.attacker };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { applied, total: findings.length, threatModel: tm };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const _internals = { _extractPathsFromSection, _extractCompliance, _extractAttacker, _bumpSeverity, _globMatch };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Time-to-fix estimator.
|
|
2
|
+
//
|
|
3
|
+
// Estimates engineering hours to remediate each finding from:
|
|
4
|
+
//
|
|
5
|
+
// - family base difficulty (regex auth-missing ≠ deserialization)
|
|
6
|
+
// - patch shape (single-line vs cross-file refactor) from fix.code if present
|
|
7
|
+
// - prior fix-history for the same family in this project (learned base)
|
|
8
|
+
// - reachability tier (tests + verify cost adjusts)
|
|
9
|
+
//
|
|
10
|
+
// Output: f.estimatedFixHours, rolled-up totals + a per-family rollup so
|
|
11
|
+
// the PM/PR view can show "this PR ships ~6 hours of security debt."
|
|
12
|
+
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const STATE = '.agentic-security';
|
|
17
|
+
const HISTORY_FILE = 'fix-history/log.json';
|
|
18
|
+
|
|
19
|
+
// Family base estimates (hours). Tuned from typical patch shapes.
|
|
20
|
+
const FAMILY_BASE_HOURS = {
|
|
21
|
+
'sqli': 0.5, 'sql-injection': 0.5, // parameterize one query
|
|
22
|
+
'xss': 0.5, 'mutation-xss': 1.0,
|
|
23
|
+
'command-injection': 0.5,
|
|
24
|
+
'code-injection': 2.0, // usually needs refactor
|
|
25
|
+
'deserialization': 4.0, // protocol/serializer swap
|
|
26
|
+
'auth-missing': 0.5, // add middleware
|
|
27
|
+
'authz': 2.0, 'idor': 2.0, // ownership checks across handlers
|
|
28
|
+
'csrf': 1.0,
|
|
29
|
+
'ssrf': 1.5, 'ssrf-cloud-metadata': 1.0,
|
|
30
|
+
'xxe': 0.5,
|
|
31
|
+
'open-redirect': 0.5,
|
|
32
|
+
'path-traversal': 1.0,
|
|
33
|
+
'crypto-weak-cipher': 2.0, // algorithm swap + key plumbing
|
|
34
|
+
'crypto-weak-hash': 1.0,
|
|
35
|
+
'crypto-tls-no-verify': 0.5,
|
|
36
|
+
'crypto-tls-version': 1.0,
|
|
37
|
+
'crypto-jwt-none': 0.5,
|
|
38
|
+
'crypto-jwt-key-confusion': 0.5,
|
|
39
|
+
'hardcoded-secret': 1.0, // env-var plumbing + rotation
|
|
40
|
+
'vulnerable-dependency': 0.5, // npm install bump
|
|
41
|
+
'dependency-confusion': 1.0,
|
|
42
|
+
'iam-overpermissive': 1.5,
|
|
43
|
+
'k8s-rbac-cluster-admin': 1.0,
|
|
44
|
+
'k8s-pod-security-privileged': 1.0,
|
|
45
|
+
'prompt-injection': 3.0, // architectural — prompt isolation
|
|
46
|
+
'agent-tool-exec': 4.0, // narrow the tool surface
|
|
47
|
+
'reentrancy': 4.0, // Solidity refactor + tests
|
|
48
|
+
'pqc-migration': 8.0, // multi-quarter project
|
|
49
|
+
'license-graph': 2.0, // dep swap or policy negotiation
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function _loadFixHistory(scanRoot) {
|
|
53
|
+
const fp = path.join(scanRoot, STATE, HISTORY_FILE);
|
|
54
|
+
if (!fs.existsSync(fp)) return [];
|
|
55
|
+
try {
|
|
56
|
+
const arr = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
57
|
+
return Array.isArray(arr) ? arr : [];
|
|
58
|
+
} catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _historicalAvg(history, family) {
|
|
62
|
+
const matches = history.filter(h => h.family === family && typeof h.elapsedHours === 'number');
|
|
63
|
+
if (!matches.length) return null;
|
|
64
|
+
const sum = matches.reduce((a, b) => a + b.elapsedHours, 0);
|
|
65
|
+
return sum / matches.length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _patchShapeAdjust(finding) {
|
|
69
|
+
// If we have the synthesized fix code, estimate complexity from size.
|
|
70
|
+
const code = finding.fix?.code || finding.fix?.replacement || '';
|
|
71
|
+
if (!code) return 1.0;
|
|
72
|
+
const lines = code.split('\n').length;
|
|
73
|
+
if (lines <= 3) return 1.0;
|
|
74
|
+
if (lines <= 10) return 1.4;
|
|
75
|
+
if (lines <= 30) return 2.0;
|
|
76
|
+
return 3.0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _reachAdjust(finding) {
|
|
80
|
+
// Higher reachability → more careful testing → slightly higher cost.
|
|
81
|
+
const tier = finding.reachabilityTier;
|
|
82
|
+
if (tier === 'reachable-public' || tier === 'public-unauthed') return 1.3;
|
|
83
|
+
if (tier === 'route-reachable') return 1.15;
|
|
84
|
+
if (tier === 'unreachable') return 0.7;
|
|
85
|
+
return 1.0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Annotate findings with estimatedFixHours. Returns
|
|
90
|
+
* { perFinding: count, totalHours, perFamily: { fam: hours, ... } }
|
|
91
|
+
*/
|
|
92
|
+
export function annotateTimeToFix(scanRoot, findings) {
|
|
93
|
+
if (!Array.isArray(findings) || findings.length === 0) {
|
|
94
|
+
return { perFinding: 0, totalHours: 0, perFamily: {} };
|
|
95
|
+
}
|
|
96
|
+
const history = _loadFixHistory(scanRoot);
|
|
97
|
+
let total = 0;
|
|
98
|
+
const perFamily = {};
|
|
99
|
+
for (const f of findings) {
|
|
100
|
+
const base = _historicalAvg(history, f.family) ?? FAMILY_BASE_HOURS[f.family] ?? 1.5;
|
|
101
|
+
const patchAdj = _patchShapeAdjust(f);
|
|
102
|
+
const reachAdj = _reachAdjust(f);
|
|
103
|
+
const hours = Number((base * patchAdj * reachAdj).toFixed(2));
|
|
104
|
+
f.estimatedFixHours = hours;
|
|
105
|
+
f.estimatedFixHoursSource = _historicalAvg(history, f.family) != null ? 'history' : 'family-base';
|
|
106
|
+
total += hours;
|
|
107
|
+
perFamily[f.family || 'unknown'] = (perFamily[f.family || 'unknown'] || 0) + hours;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
perFinding: findings.length,
|
|
111
|
+
totalHours: Number(total.toFixed(1)),
|
|
112
|
+
perFamily: Object.fromEntries(
|
|
113
|
+
Object.entries(perFamily)
|
|
114
|
+
.sort((a, b) => b[1] - a[1])
|
|
115
|
+
.map(([k, v]) => [k, Number(v.toFixed(1))]),
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Render a one-paragraph PM summary.
|
|
122
|
+
*/
|
|
123
|
+
export function renderTimeSummary(roll) {
|
|
124
|
+
if (!roll || roll.perFinding === 0) return 'No findings — 0 hours of security debt.';
|
|
125
|
+
const top = Object.entries(roll.perFamily).slice(0, 3).map(([k, v]) => `${k} (${v}h)`).join(', ');
|
|
126
|
+
return `${roll.perFinding} finding(s) — ~${roll.totalHours} engineering hours of security debt. Top families: ${top}.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const _internals = { FAMILY_BASE_HOURS, _patchShapeAdjust, _reachAdjust, _historicalAvg };
|