@clear-capabilities/agentic-security-scanner 0.78.0 → 0.80.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/bin/.agentic-security/findings.json +16 -16
- package/bin/.agentic-security/last-scan.json +16 -16
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +51 -0
- package/bin/.agentic-security/streak.json +5 -5
- package/bin/agentic-security.js +22 -7
- package/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/476.index.js +5 -5
- package/dist/637.index.js +1 -1
- package/dist/700.index.js +138 -0
- package/dist/718.index.js +53 -0
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +95 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/.agentic-security/findings.json +29799 -7803
- package/src/.agentic-security/last-scan.json +29799 -7803
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +5119 -2611
- package/src/.agentic-security/streak.json +6 -6
- package/src/dataflow/.agentic-security/findings.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +68 -520
- package/src/dataflow/.agentic-security/streak.json +6 -7
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/engine.js +52 -8
- 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 +890 -132
- package/src/integrations/index.js +2 -1
- package/src/ir/.agentic-security/findings.json +240 -6
- package/src/ir/.agentic-security/last-scan.json +240 -6
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +16 -594
- package/src/ir/.agentic-security/streak.json +8 -9
- package/src/ir/callgraph.js +27 -7
- 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/llm-validator/index.js +7 -5
- 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 +143 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/audit.js +5 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +6689 -177
- package/src/posture/.agentic-security/streak.json +8 -7
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/calibration-drift.js +2 -1
- package/src/posture/calibration.js +3 -2
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/fix-history.js +8 -2
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/profile.js +4 -5
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/rule-overrides.js +2 -3
- package/src/posture/rule-pack-signing.js +2 -3
- package/src/posture/rule-synthesis.js +5 -6
- 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/security-trend.js +4 -7
- package/src/posture/state-dir.js +124 -0
- package/src/posture/streak.js +3 -0
- package/src/posture/suppressions.js +5 -8
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +29 -6
- package/src/posture/validator-metrics.js +3 -6
- package/src/sast/.agentic-security/findings.json +996 -32
- package/src/sast/.agentic-security/last-scan.json +996 -32
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +565 -32
- package/src/sast/.agentic-security/streak.json +10 -8
- 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/db-taint.js +24 -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/rust.js +26 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +83 -6
- package/src/sca/.agentic-security/streak.json +9 -9
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +146 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/sigstore-verify.js +215 -0
- package/src/sca/vendor-detect.js +53 -0
- package/src/report/.agentic-security/findings.json +0 -80
- package/src/report/.agentic-security/last-scan.json +0 -80
- package/src/report/.agentic-security/last-scan.json.sig +0 -1
- package/src/report/.agentic-security/scan-history.json +0 -35
- package/src/report/.agentic-security/streak.json +0 -22
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"firstScanDate": "2026-05-
|
|
3
|
-
"lastScanDate": "2026-05-
|
|
4
|
-
"totalScans":
|
|
2
|
+
"firstScanDate": "2026-05-28T17:47:06.515Z",
|
|
3
|
+
"lastScanDate": "2026-05-29T16:57:31.774Z",
|
|
4
|
+
"totalScans": 34,
|
|
5
5
|
"daysCleanCritical": 0,
|
|
6
6
|
"lastCleanDate": null,
|
|
7
|
-
"lastCriticalDate": "2026-05-
|
|
7
|
+
"lastCriticalDate": "2026-05-29",
|
|
8
8
|
"hasEverHadCritical": true,
|
|
9
9
|
"bestDaysCleanCritical": 0,
|
|
10
|
-
"totalFindingsAtFirstScan":
|
|
11
|
-
"totalFindingsAtLastScan":
|
|
10
|
+
"totalFindingsAtFirstScan": 256,
|
|
11
|
+
"totalFindingsAtLastScan": 320,
|
|
12
12
|
"totalFixesInferred": 0,
|
|
13
13
|
"lastGrade": "C",
|
|
14
14
|
"bestGrade": "C",
|
|
15
15
|
"launchCheckPassedAt": null,
|
|
16
16
|
"achievements": [
|
|
17
|
-
"first-scan"
|
|
17
|
+
"first-scan",
|
|
18
|
+
"scan-veteran-25"
|
|
18
19
|
],
|
|
19
20
|
"previousGrade": "C"
|
|
20
21
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// API schema-aware scanning — Recommendation #3 of the world-class+2 plan.
|
|
2
|
+
//
|
|
3
|
+
// Cross-references the project's declared API contract (OpenAPI 3.x,
|
|
4
|
+
// GraphQL SDL, protobuf) against the route handlers actually detected in
|
|
5
|
+
// the source. Catches:
|
|
6
|
+
//
|
|
7
|
+
// - undocumented-endpoint Route in code but not in the contract
|
|
8
|
+
// - undeclared-route Contract path with no implementation
|
|
9
|
+
// - missing-auth-on-route Contract says auth required, code doesn't enforce
|
|
10
|
+
// - parameter-type-mismatch Contract says int, code reads as raw string
|
|
11
|
+
// - missing-validation Contract says enum/pattern, code doesn't check
|
|
12
|
+
//
|
|
13
|
+
// Loaders:
|
|
14
|
+
// - openapi.{yaml,yml,json} / swagger.{yaml,yml,json} → OpenAPI 3.x
|
|
15
|
+
// - schema.graphql / *.graphql → GraphQL SDL
|
|
16
|
+
// - *.proto → protobuf
|
|
17
|
+
//
|
|
18
|
+
// Outputs new findings with family 'api-contract' and per-rule subfamily.
|
|
19
|
+
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import * as yaml from 'js-yaml';
|
|
23
|
+
|
|
24
|
+
const CONTRACT_FILE_PATTERNS = [
|
|
25
|
+
{ glob: /(?:openapi|swagger)\.(?:ya?ml|json)$/i, kind: 'openapi' },
|
|
26
|
+
{ glob: /schema\.graphql$/i, kind: 'graphql' },
|
|
27
|
+
{ glob: /\.proto$/i, kind: 'protobuf' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk the scan root for contract files. Returns up to 5 parsed contracts.
|
|
32
|
+
*/
|
|
33
|
+
export function loadContracts(scanRoot) {
|
|
34
|
+
const out = [];
|
|
35
|
+
function walk(dir, depth) {
|
|
36
|
+
if (depth > 4) return;
|
|
37
|
+
let entries;
|
|
38
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
41
|
+
const fp = path.join(dir, e.name);
|
|
42
|
+
if (e.isDirectory()) { walk(fp, depth + 1); continue; }
|
|
43
|
+
for (const pat of CONTRACT_FILE_PATTERNS) {
|
|
44
|
+
if (pat.glob.test(e.name)) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
47
|
+
out.push({ path: fp, kind: pat.kind, doc: pat.kind === 'openapi' ? yaml.load(raw) : raw });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
out.push({ path: fp, kind: pat.kind, error: String(err && err.message) });
|
|
50
|
+
}
|
|
51
|
+
if (out.length >= 5) return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
walk(scanRoot, 0);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse an OpenAPI document into a normalized route list:
|
|
62
|
+
* [{ method, path, parameters: [{name,in,required,type,pattern,enum}],
|
|
63
|
+
* requiresAuth, requestBodyRequired, operationId, tags }]
|
|
64
|
+
*/
|
|
65
|
+
export function parseOpenAPI(doc) {
|
|
66
|
+
if (!doc || !doc.paths) return [];
|
|
67
|
+
const routes = [];
|
|
68
|
+
const globalAuth = Array.isArray(doc.security) && doc.security.length > 0;
|
|
69
|
+
for (const [routePath, pathItem] of Object.entries(doc.paths)) {
|
|
70
|
+
for (const method of ['get','post','put','patch','delete','head','options']) {
|
|
71
|
+
const op = pathItem[method];
|
|
72
|
+
if (!op) continue;
|
|
73
|
+
const localAuth = Array.isArray(op.security) ? op.security.length > 0 : globalAuth;
|
|
74
|
+
const params = (op.parameters || []).concat(pathItem.parameters || []).map(p => ({
|
|
75
|
+
name: p.name, in: p.in, required: !!p.required,
|
|
76
|
+
type: (p.schema && p.schema.type) || p.type || null,
|
|
77
|
+
pattern: (p.schema && p.schema.pattern) || p.pattern || null,
|
|
78
|
+
enum: (p.schema && p.schema.enum) || p.enum || null,
|
|
79
|
+
}));
|
|
80
|
+
routes.push({
|
|
81
|
+
method: method.toUpperCase(),
|
|
82
|
+
path: routePath,
|
|
83
|
+
parameters: params,
|
|
84
|
+
requiresAuth: localAuth,
|
|
85
|
+
requestBodyRequired: !!(op.requestBody && op.requestBody.required),
|
|
86
|
+
operationId: op.operationId || null,
|
|
87
|
+
tags: op.tags || [],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return routes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse a GraphQL SDL fragment into a route-like list of fields.
|
|
96
|
+
* Each field is treated as a "route" for cross-reference purposes.
|
|
97
|
+
*/
|
|
98
|
+
export function parseGraphQL(sdl) {
|
|
99
|
+
if (typeof sdl !== 'string') return [];
|
|
100
|
+
const routes = [];
|
|
101
|
+
const re = /\b(?:type\s+(?:Query|Mutation|Subscription))\b\s*\{([\s\S]*?)\}/g;
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = re.exec(sdl))) {
|
|
104
|
+
for (const fieldLine of m[1].split('\n')) {
|
|
105
|
+
const fm = fieldLine.match(/^\s*(\w+)\s*(?:\(([^)]*)\))?\s*:\s*(\w+)/);
|
|
106
|
+
if (!fm) continue;
|
|
107
|
+
const params = (fm[2] || '').split(',').map(s => s.trim()).filter(Boolean).map(s => {
|
|
108
|
+
const pm = s.match(/^(\w+)\s*:\s*([\w!\[\]]+)/);
|
|
109
|
+
return pm ? { name: pm[1], type: pm[2], required: /!/.test(pm[2]) } : null;
|
|
110
|
+
}).filter(Boolean);
|
|
111
|
+
routes.push({ method: 'GRAPHQL', path: '/' + fm[1], parameters: params, requiresAuth: null });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return routes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cross-reference declared contract routes against detected code routes.
|
|
119
|
+
* `codeRoutes` is the engine's discovered route list (engine.scan.routes).
|
|
120
|
+
*
|
|
121
|
+
* Emits findings for:
|
|
122
|
+
* - undeclared-route Contract declares it, code doesn't implement
|
|
123
|
+
* - undocumented-endpoint Code exposes it, contract doesn't mention it
|
|
124
|
+
* - missing-auth-on-route Contract says required, code is unauth
|
|
125
|
+
*/
|
|
126
|
+
export function diffRoutes(contractRoutes, codeRoutes) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
if (!Array.isArray(contractRoutes) || !Array.isArray(codeRoutes)) return findings;
|
|
129
|
+
function matchKey(r) {
|
|
130
|
+
return `${(r.method || 'ANY').toUpperCase()} ${r.path || ''}`.trim();
|
|
131
|
+
}
|
|
132
|
+
const codeByKey = new Map(codeRoutes.map(r => [matchKey(r), r]));
|
|
133
|
+
const contractByKey = new Map(contractRoutes.map(r => [matchKey(r), r]));
|
|
134
|
+
// 1. Undeclared routes
|
|
135
|
+
for (const [key, c] of contractByKey) {
|
|
136
|
+
if (!codeByKey.has(key)) {
|
|
137
|
+
findings.push({
|
|
138
|
+
family: 'api-contract', subfamily: 'undeclared-route',
|
|
139
|
+
severity: 'medium', cwe: 'CWE-1059',
|
|
140
|
+
vuln: `Contract declares ${key}, no matching route handler found in code`,
|
|
141
|
+
file: c.contractFile || 'openapi.yaml', line: 0,
|
|
142
|
+
remediation: 'Implement the route declared in the contract OR remove the contract entry. Declared-but-missing routes signal documentation drift.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// 2. Undocumented endpoints
|
|
147
|
+
for (const [key, c] of codeByKey) {
|
|
148
|
+
if (!contractByKey.has(key)) {
|
|
149
|
+
findings.push({
|
|
150
|
+
family: 'api-contract', subfamily: 'undocumented-endpoint',
|
|
151
|
+
severity: 'high', cwe: 'CWE-1059',
|
|
152
|
+
vuln: `Route ${key} is implemented but not declared in the API contract`,
|
|
153
|
+
file: c.file, line: c.line,
|
|
154
|
+
remediation: 'Add the route to the OpenAPI/GraphQL contract. Undocumented endpoints are the standard vector for accidentally-public internal APIs (the metadata-service pattern).',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 3. Auth mismatch — contract says auth required but code marks unauth
|
|
159
|
+
for (const [key, c] of contractByKey) {
|
|
160
|
+
const code = codeByKey.get(key);
|
|
161
|
+
if (!code) continue;
|
|
162
|
+
if (c.requiresAuth === true && code.requiresAuth === false) {
|
|
163
|
+
findings.push({
|
|
164
|
+
family: 'api-contract', subfamily: 'missing-auth-on-route',
|
|
165
|
+
severity: 'critical', cwe: 'CWE-306',
|
|
166
|
+
vuln: `Route ${key} declared as requiring auth in contract, but handler is unauthenticated`,
|
|
167
|
+
file: code.file, line: code.line,
|
|
168
|
+
remediation: 'Either gate the handler with the documented auth mechanism, or update the contract if the route legitimately should be public.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run the full pipeline: load contracts, parse, diff vs codeRoutes,
|
|
177
|
+
* return findings.
|
|
178
|
+
*/
|
|
179
|
+
export function runApiContractScan(scanRoot, codeRoutes) {
|
|
180
|
+
const contracts = loadContracts(scanRoot);
|
|
181
|
+
const findings = [];
|
|
182
|
+
for (const c of contracts) {
|
|
183
|
+
if (c.error) continue;
|
|
184
|
+
let contractRoutes = [];
|
|
185
|
+
if (c.kind === 'openapi') contractRoutes = parseOpenAPI(c.doc);
|
|
186
|
+
else if (c.kind === 'graphql') contractRoutes = parseGraphQL(c.doc);
|
|
187
|
+
contractRoutes.forEach(r => { r.contractFile = c.path; });
|
|
188
|
+
findings.push(...diffRoutes(contractRoutes, codeRoutes || []));
|
|
189
|
+
}
|
|
190
|
+
return findings;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const _internals = { CONTRACT_FILE_PATTERNS, parseOpenAPI, parseGraphQL };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Attack-taxonomy annotator — Item #8 of the world-class+3 plan.
|
|
2
|
+
//
|
|
3
|
+
// Stamps every finding with the relevant standard-taxonomy IDs so that
|
|
4
|
+
// downstream SIEM/SOAR systems can correlate scanner findings with their
|
|
5
|
+
// existing detection rules, threat intel, and defensive coverage.
|
|
6
|
+
//
|
|
7
|
+
// Annotations added:
|
|
8
|
+
//
|
|
9
|
+
// attck — MITRE ATT&CK Enterprise / Mobile technique IDs (e.g. "T1190")
|
|
10
|
+
// attckName — human-readable name (e.g. "Exploit Public-Facing Application")
|
|
11
|
+
// atlas — MITRE ATLAS (Adversarial Threat Landscape for AI Systems)
|
|
12
|
+
// technique IDs for ML/AI findings (e.g. "AML.T0043")
|
|
13
|
+
// d3fend — MITRE D3FEND countermeasure IDs (e.g. "D3-RFS")
|
|
14
|
+
// attckTactic — MITRE ATT&CK Enterprise tactic (initial-access |
|
|
15
|
+
// execution | persistence | privilege-escalation | etc.)
|
|
16
|
+
// capec — Common Attack Pattern Enumeration & Classification ID
|
|
17
|
+
//
|
|
18
|
+
// No exploit generation, no PoC synthesis — those are already covered by
|
|
19
|
+
// poc-generator / exploit-bundle / three-agent-pipeline / security-poc-
|
|
20
|
+
// generator / security-chain-synthesizer.
|
|
21
|
+
//
|
|
22
|
+
// Mappings are curated. Coverage is reported via summary; unmapped families
|
|
23
|
+
// remain unannotated rather than guessed.
|
|
24
|
+
//
|
|
25
|
+
// Opt-out: AGENTIC_SECURITY_NO_ATTACK_TAX=1
|
|
26
|
+
|
|
27
|
+
// ── Mapping table (family → taxonomy IDs) ──────────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// Sources:
|
|
30
|
+
// ATT&CK attack.mitre.org/techniques/enterprise/ (v15.1)
|
|
31
|
+
// ATLAS atlas.mitre.org (Jan 2025 release)
|
|
32
|
+
// D3FEND d3fend.mitre.org (v0.16)
|
|
33
|
+
// CAPEC capec.mitre.org
|
|
34
|
+
|
|
35
|
+
const FAMILY_MAP = {
|
|
36
|
+
// ── Injection / RCE ──────────────────────────────────────────────────────
|
|
37
|
+
'sqli': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-66'] },
|
|
38
|
+
'sql-injection': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-66'] },
|
|
39
|
+
'xss': { attck: ['T1059.007'], attckName: 'Command and Scripting Interpreter: JavaScript', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-63'] },
|
|
40
|
+
'mutation-xss': { attck: ['T1059.007'], attckName: 'Command and Scripting Interpreter: JavaScript', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-63'] },
|
|
41
|
+
'command-injection': { attck: ['T1059.004'], attckName: 'Command and Scripting Interpreter: Unix Shell', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-88'] },
|
|
42
|
+
'code-injection': { attck: ['T1059'], attckName: 'Command and Scripting Interpreter', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-242'] },
|
|
43
|
+
'deserialization': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-586'] },
|
|
44
|
+
'ldap-injection': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-136'] },
|
|
45
|
+
'xpath-injection': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-83'] },
|
|
46
|
+
'nosql-injection': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-676'] },
|
|
47
|
+
'jndi': { attck: ['T1190', 'T1059'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-242'] },
|
|
48
|
+
'ssti': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-94'] },
|
|
49
|
+
|
|
50
|
+
// ── Auth / authZ ─────────────────────────────────────────────────────────
|
|
51
|
+
'auth-missing': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'initial-access', capec: ['CAPEC-115'] },
|
|
52
|
+
'authz': { attck: ['T1078.004'], attckName: 'Valid Accounts: Cloud Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
53
|
+
'idor': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
54
|
+
'mass-assignment': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-IVV'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
55
|
+
'jwt-exp': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-ANET'], attckTactic: 'defense-evasion', capec: ['CAPEC-593'] },
|
|
56
|
+
'csrf': { attck: ['T1059.007'], attckName: 'Command and Scripting Interpreter: JavaScript', d3fend: ['D3-ITF'], attckTactic: 'execution', capec: ['CAPEC-62'] },
|
|
57
|
+
'tx-origin-auth': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-94'] },
|
|
58
|
+
'signature-replay': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-ANET'], attckTactic: 'defense-evasion', capec: ['CAPEC-60'] },
|
|
59
|
+
'erc4337-validation': { attck: ['T1550'], attckName: 'Use Alternate Authentication Material', d3fend: ['D3-ANET'], attckTactic: 'defense-evasion', capec: ['CAPEC-115'] },
|
|
60
|
+
|
|
61
|
+
// ── Crypto ───────────────────────────────────────────────────────────────
|
|
62
|
+
'crypto-weak-cipher': { attck: ['T1573.001'], attckName: 'Encrypted Channel: Symmetric Cryptography', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-475'] },
|
|
63
|
+
'crypto-weak-hash': { attck: ['T1110.002'], attckName: 'Brute Force: Password Cracking', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-461'] },
|
|
64
|
+
'crypto-ecb': { attck: ['T1573.001'], attckName: 'Encrypted Channel: Symmetric Cryptography', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-475'] },
|
|
65
|
+
'crypto-static-iv': { attck: ['T1573.001'], attckName: 'Encrypted Channel: Symmetric Cryptography', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-475'] },
|
|
66
|
+
'crypto-tls-version': { attck: ['T1557.002'], attckName: 'Adversary-in-the-Middle: ARP Cache Poisoning', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-94'] },
|
|
67
|
+
'crypto-tls-no-verify': { attck: ['T1557'], attckName: 'Adversary-in-the-Middle', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-94'] },
|
|
68
|
+
'crypto-jwt-none': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-ANET'], attckTactic: 'defense-evasion', capec: ['CAPEC-593'] },
|
|
69
|
+
'crypto-jwt-key-confusion': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-ANET'], attckTactic: 'defense-evasion', capec: ['CAPEC-593'] },
|
|
70
|
+
'crypto-kdf-weak': { attck: ['T1110.002'], attckName: 'Brute Force: Password Cracking', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-49'] },
|
|
71
|
+
'crypto-weak-rng': { attck: ['T1518.001'], attckName: 'Software Discovery', d3fend: ['D3-CH'], attckTactic: 'discovery', capec: ['CAPEC-485'] },
|
|
72
|
+
'pqc-migration': { attck: ['T1573'], attckName: 'Encrypted Channel', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-475'] },
|
|
73
|
+
|
|
74
|
+
// ── Supply chain / SCA ───────────────────────────────────────────────────
|
|
75
|
+
'vulnerable-dependency': { attck: ['T1195.001'], attckName: 'Supply Chain Compromise: Compromise Software Dependencies and Development Tools', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
76
|
+
'dependency-confusion': { attck: ['T1195.002'], attckName: 'Supply Chain Compromise: Compromise Software Supply Chain', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-538'] },
|
|
77
|
+
'dependency-drift': { attck: ['T1195'], attckName: 'Supply Chain Compromise', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
78
|
+
'license-graph': { attck: ['T1195'], attckName: 'Supply Chain Compromise', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
79
|
+
|
|
80
|
+
// ── Secrets / credentials ────────────────────────────────────────────────
|
|
81
|
+
'secret': { attck: ['T1552.001'], attckName: 'Unsecured Credentials: Credentials In Files', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-117'] },
|
|
82
|
+
'hardcoded-secret': { attck: ['T1552.001'], attckName: 'Unsecured Credentials: Credentials In Files', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-117'] },
|
|
83
|
+
'aws-no-mfa': { attck: ['T1078.004'], attckName: 'Valid Accounts: Cloud Accounts', d3fend: ['D3-MFA'], attckTactic: 'initial-access', capec: ['CAPEC-115'] },
|
|
84
|
+
'rpc-key-inline': { attck: ['T1552.001'], attckName: 'Unsecured Credentials: Credentials In Files', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-117'] },
|
|
85
|
+
|
|
86
|
+
// ── Cloud / IAM ──────────────────────────────────────────────────────────
|
|
87
|
+
'aws-public-s3': { attck: ['T1530'], attckName: 'Data from Cloud Storage', d3fend: ['D3-ACL'], attckTactic: 'collection', capec: ['CAPEC-186'] },
|
|
88
|
+
'aws-public-trust': { attck: ['T1078.004'], attckName: 'Valid Accounts: Cloud Accounts', d3fend: ['D3-ANCI'], attckTactic: 'initial-access', capec: ['CAPEC-122'] },
|
|
89
|
+
'aws-passrole-wildcard': { attck: ['T1098.001'], attckName: 'Account Manipulation: Additional Cloud Credentials', d3fend: ['D3-ANCI'], attckTactic: 'persistence', capec: ['CAPEC-122'] },
|
|
90
|
+
'aws-overbroad-managed': { attck: ['T1098.001'], attckName: 'Account Manipulation: Additional Cloud Credentials', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
91
|
+
'gcp-public-binding': { attck: ['T1530'], attckName: 'Data from Cloud Storage', d3fend: ['D3-ACL'], attckTactic: 'collection', capec: ['CAPEC-186'] },
|
|
92
|
+
'gcp-owner-overuse': { attck: ['T1098.001'], attckName: 'Account Manipulation: Additional Cloud Credentials', d3fend: ['D3-ANCI'], attckTactic: 'persistence', capec: ['CAPEC-122'] },
|
|
93
|
+
'azure-auth-wildcard': { attck: ['T1098.003'], attckName: 'Account Manipulation: Additional Cloud Roles', d3fend: ['D3-ANCI'], attckTactic: 'persistence', capec: ['CAPEC-122'] },
|
|
94
|
+
'azure-owner-sub': { attck: ['T1098.003'], attckName: 'Account Manipulation: Additional Cloud Roles', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
95
|
+
'iam-overpermissive': { attck: ['T1098.001'], attckName: 'Account Manipulation: Additional Cloud Credentials', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
96
|
+
|
|
97
|
+
// ── Kubernetes ───────────────────────────────────────────────────────────
|
|
98
|
+
'k8s-rbac-cluster-admin': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
99
|
+
'k8s-rbac-anonymous': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'initial-access', capec: ['CAPEC-115'] },
|
|
100
|
+
'k8s-rbac-wildcard': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
101
|
+
'k8s-rbac-overbroad-binding': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'privilege-escalation', capec: ['CAPEC-122'] },
|
|
102
|
+
'k8s-pod-security-privileged':{ attck: ['T1611'], attckName: 'Escape to Host', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-441'] },
|
|
103
|
+
'k8s-pod-security-hostnetwork': { attck: ['T1610'], attckName: 'Deploy Container', d3fend: ['D3-PSEP'], attckTactic: 'lateral-movement', capec: ['CAPEC-441'] },
|
|
104
|
+
'k8s-pod-security-hostpid': { attck: ['T1611'], attckName: 'Escape to Host', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-441'] },
|
|
105
|
+
'k8s-pod-security-hostipc': { attck: ['T1611'], attckName: 'Escape to Host', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-441'] },
|
|
106
|
+
'k8s-pod-security-hostpath': { attck: ['T1611'], attckName: 'Escape to Host', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-441'] },
|
|
107
|
+
'k8s-pod-security-allow-privesc': { attck: ['T1068'], attckName: 'Exploitation for Privilege Escalation', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-69'] },
|
|
108
|
+
'k8s-pod-security-run-as-root': { attck: ['T1068'], attckName: 'Exploitation for Privilege Escalation', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-69'] },
|
|
109
|
+
'k8s-pod-security-capabilities-broad': { attck: ['T1611'], attckName: 'Escape to Host', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-441'] },
|
|
110
|
+
'k8s-webhook-bypass': { attck: ['T1562.001'], attckName: 'Impair Defenses: Disable or Modify Tools', d3fend: ['D3-PA'], attckTactic: 'defense-evasion', capec: ['CAPEC-180'] },
|
|
111
|
+
|
|
112
|
+
// ── Web3 ─────────────────────────────────────────────────────────────────
|
|
113
|
+
'reentrancy': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IVV'], attckTactic: 'exploitation', capec: ['CAPEC-26'] },
|
|
114
|
+
'defi-no-slippage': { attck: ['T1565.001'], attckName: 'Data Manipulation: Stored Data Manipulation', d3fend: ['D3-IVV'], attckTactic: 'impact', capec: ['CAPEC-176'] },
|
|
115
|
+
'defi-spot-price-oracle': { attck: ['T1565.001'], attckName: 'Data Manipulation: Stored Data Manipulation', d3fend: ['D3-IVV'], attckTactic: 'impact', capec: ['CAPEC-176'] },
|
|
116
|
+
'upgradeable-init': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IVV'], attckTactic: 'exploitation', capec: ['CAPEC-26'] },
|
|
117
|
+
'ecdsa-malleability': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-475'] },
|
|
118
|
+
'unlimited-approval': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ACL'], attckTactic: 'collection', capec: ['CAPEC-122'] },
|
|
119
|
+
'eth-sign-used': { attck: ['T1550.001'], attckName: 'Use Alternate Authentication Material: Application Access Token', d3fend: ['D3-CH'], attckTactic: 'defense-evasion', capec: ['CAPEC-94'] },
|
|
120
|
+
'private-key-in-frontend': { attck: ['T1552.001'], attckName: 'Unsecured Credentials: Credentials In Files', d3fend: ['D3-CH'], attckTactic: 'credential-access', capec: ['CAPEC-117'] },
|
|
121
|
+
|
|
122
|
+
// ── SSRF / XXE / IDOR / open redirect ────────────────────────────────────
|
|
123
|
+
'ssrf': { attck: ['T1090.001'], attckName: 'Proxy: Internal Proxy', d3fend: ['D3-NTA'], attckTactic: 'discovery', capec: ['CAPEC-664'] },
|
|
124
|
+
'ssrf-cloud-metadata': { attck: ['T1552.005'], attckName: 'Unsecured Credentials: Cloud Instance Metadata API', d3fend: ['D3-NTA'], attckTactic: 'credential-access', capec: ['CAPEC-664'] },
|
|
125
|
+
'xxe': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IAA'], attckTactic: 'exploitation', capec: ['CAPEC-221'] },
|
|
126
|
+
'open-redirect': { attck: ['T1204.001'], attckName: 'User Execution: Malicious Link', d3fend: ['D3-IDA'], attckTactic: 'initial-access', capec: ['CAPEC-178'] },
|
|
127
|
+
'path-traversal': { attck: ['T1083'], attckName: 'File and Directory Discovery', d3fend: ['D3-IDA'], attckTactic: 'discovery', capec: ['CAPEC-126'] },
|
|
128
|
+
'zip-slip': { attck: ['T1083'], attckName: 'File and Directory Discovery', d3fend: ['D3-IDA'], attckTactic: 'discovery', capec: ['CAPEC-130'] },
|
|
129
|
+
'race-condition': { attck: ['T1068'], attckName: 'Exploitation for Privilege Escalation', d3fend: ['D3-PSA'], attckTactic: 'privilege-escalation', capec: ['CAPEC-25'] },
|
|
130
|
+
'host-header': { attck: ['T1190'], attckName: 'Exploit Public-Facing Application', d3fend: ['D3-IDA'], attckTactic: 'exploitation', capec: ['CAPEC-105'] },
|
|
131
|
+
|
|
132
|
+
// ── LLM / AI / MCP ───────────────────────────────────────────────────────
|
|
133
|
+
// ATLAS-mapped where appropriate; ATT&CK provides the closest enterprise mapping.
|
|
134
|
+
'llm-app-security': { attck: ['T1059'], attckName: 'Command and Scripting Interpreter', atlas: ['AML.T0051.000'], atlasName: 'LLM Prompt Injection: Direct', d3fend: ['D3-IDA'], attckTactic: 'execution', capec: ['CAPEC-242'] },
|
|
135
|
+
'training-data-pii': { attck: ['T1530'], attckName: 'Data from Cloud Storage', atlas: ['AML.T0034'], atlasName: 'Cost Harvesting', d3fend: ['D3-CH'], attckTactic: 'collection', capec: ['CAPEC-118'] },
|
|
136
|
+
'prompt-injection': { attck: ['T1059'], attckName: 'Command and Scripting Interpreter', atlas: ['AML.T0051.000'], atlasName: 'LLM Prompt Injection: Direct', d3fend: ['D3-IDA'], attckTactic: 'execution', capec: ['CAPEC-242'] },
|
|
137
|
+
'agent-tool-exec': { attck: ['T1059'], attckName: 'Command and Scripting Interpreter', atlas: ['AML.T0050'], atlasName: 'Command and Scripting Interpreter', d3fend: ['D3-IDA'], attckTactic: 'execution', capec: ['CAPEC-242'] },
|
|
138
|
+
'hf-datasets-rce': { attck: ['T1195.001'], attckName: 'Supply Chain Compromise: Compromise Software Dependencies and Development Tools', atlas: ['AML.T0010'], atlasName: 'ML Supply Chain Compromise: ML Software', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
139
|
+
'mlflow-untrusted-uri': { attck: ['T1195'], attckName: 'Supply Chain Compromise', atlas: ['AML.T0010.003'], atlasName: 'ML Supply Chain Compromise: Model', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
140
|
+
'onnx-providers': { attck: ['T1583.006'], attckName: 'Acquire Infrastructure: Web Services', atlas: ['AML.T0011'], atlasName: 'Acquire Public ML Artifacts', d3fend: ['D3-SBV'], attckTactic: 'resource-development', capec: ['CAPEC-437'] },
|
|
141
|
+
'streaming-dataset-url': { attck: ['T1195'], attckName: 'Supply Chain Compromise', atlas: ['AML.T0019'], atlasName: 'Publish Poisoned Datasets', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
142
|
+
'prompt-integrity': { attck: ['T1059'], attckName: 'Command and Scripting Interpreter', atlas: ['AML.T0051.000'], atlasName: 'LLM Prompt Injection: Direct', d3fend: ['D3-IDA'], attckTactic: 'execution', capec: ['CAPEC-242'] },
|
|
143
|
+
'gradio-auth': { attck: ['T1078'], attckName: 'Valid Accounts', d3fend: ['D3-ANCI'], attckTactic: 'initial-access', capec: ['CAPEC-115'] },
|
|
144
|
+
'hf-endpoint-override': { attck: ['T1583.006'], attckName: 'Acquire Infrastructure: Web Services', atlas: ['AML.T0010'], atlasName: 'ML Supply Chain Compromise: ML Software', d3fend: ['D3-SBV'], attckTactic: 'resource-development', capec: ['CAPEC-437'] },
|
|
145
|
+
'model-format': { attck: ['T1195.001'], attckName: 'Supply Chain Compromise: Compromise Software Dependencies and Development Tools', atlas: ['AML.T0010.003'], atlasName: 'ML Supply Chain Compromise: Model', d3fend: ['D3-SBV'], attckTactic: 'initial-access', capec: ['CAPEC-437'] },
|
|
146
|
+
|
|
147
|
+
// ── Mobile ───────────────────────────────────────────────────────────────
|
|
148
|
+
'mobile-debuggable': { attck: ['T1404'], attckName: 'Exploitation for Privilege Escalation (Mobile)', d3fend: ['D3-PSEP'], attckTactic: 'privilege-escalation', capec: ['CAPEC-69'] },
|
|
149
|
+
'mobile-exported-component': { attck: ['T1421'], attckName: 'System Network Configuration Discovery (Mobile)', d3fend: ['D3-NTA'], attckTactic: 'discovery', capec: ['CAPEC-125'] },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Family aliases — different detectors use slightly different family names
|
|
153
|
+
// for the same underlying concept. Map them to a canonical family before lookup.
|
|
154
|
+
const FAMILY_ALIAS = {
|
|
155
|
+
'sql': 'sqli',
|
|
156
|
+
'sql-inj': 'sqli',
|
|
157
|
+
'xss-reflected': 'xss',
|
|
158
|
+
'xss-stored': 'xss',
|
|
159
|
+
'cmd-injection': 'command-injection',
|
|
160
|
+
'cmd-i': 'command-injection',
|
|
161
|
+
'rce': 'code-injection',
|
|
162
|
+
'pickle-rce': 'deserialization',
|
|
163
|
+
'java-deserialization': 'deserialization',
|
|
164
|
+
'pq-migration': 'pqc-migration',
|
|
165
|
+
'sca-vuln': 'vulnerable-dependency',
|
|
166
|
+
'sca': 'vulnerable-dependency',
|
|
167
|
+
'license': 'license-graph',
|
|
168
|
+
'cloud-iam-overpermissive': 'iam-overpermissive',
|
|
169
|
+
'http-no-tls': 'crypto-tls-version',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function _canonical(family) {
|
|
173
|
+
if (!family) return null;
|
|
174
|
+
const f = String(family).toLowerCase();
|
|
175
|
+
return FAMILY_ALIAS[f] || f;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Annotator ──────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
export function annotateAttackTaxonomy(findings) {
|
|
181
|
+
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX === '1') return { annotated: 0, total: 0 };
|
|
182
|
+
if (!Array.isArray(findings) || findings.length === 0) return { annotated: 0, total: 0 };
|
|
183
|
+
let annotated = 0;
|
|
184
|
+
for (const f of findings) {
|
|
185
|
+
if (!f || typeof f !== 'object') continue;
|
|
186
|
+
const canon = _canonical(f.family);
|
|
187
|
+
const map = canon ? FAMILY_MAP[canon] : null;
|
|
188
|
+
if (!map) continue;
|
|
189
|
+
f.attck = map.attck;
|
|
190
|
+
f.attckName = map.attckName;
|
|
191
|
+
if (map.atlas) {
|
|
192
|
+
f.atlas = map.atlas;
|
|
193
|
+
f.atlasName = map.atlasName;
|
|
194
|
+
}
|
|
195
|
+
f.d3fend = map.d3fend;
|
|
196
|
+
f.attckTactic = map.attckTactic;
|
|
197
|
+
f.capec = map.capec;
|
|
198
|
+
annotated++;
|
|
199
|
+
}
|
|
200
|
+
return { annotated, total: findings.length };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Summary helper (consumed by report layer / MCP explain_finding) ───────
|
|
204
|
+
|
|
205
|
+
export function summarizeTaxonomy(findings) {
|
|
206
|
+
const attckCount = new Map();
|
|
207
|
+
const atlasCount = new Map();
|
|
208
|
+
const attckTacticCount = new Map();
|
|
209
|
+
const unmapped = new Set();
|
|
210
|
+
for (const f of findings || []) {
|
|
211
|
+
if (Array.isArray(f.attck)) for (const t of f.attck) attckCount.set(t, (attckCount.get(t) || 0) + 1);
|
|
212
|
+
if (Array.isArray(f.atlas)) for (const t of f.atlas) atlasCount.set(t, (atlasCount.get(t) || 0) + 1);
|
|
213
|
+
if (f.attckTactic) attckTacticCount.set(f.attckTactic, (attckTacticCount.get(f.attckTactic) || 0) + 1);
|
|
214
|
+
if (f.family && !f.attck) unmapped.add(f.family);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
attckTechniques: Object.fromEntries([...attckCount].sort((a, b) => b[1] - a[1])),
|
|
218
|
+
atlasTechniques: Object.fromEntries([...atlasCount].sort((a, b) => b[1] - a[1])),
|
|
219
|
+
attckTacticDistribution: Object.fromEntries([...attckTacticCount]),
|
|
220
|
+
unmappedFamilies: [...unmapped],
|
|
221
|
+
coverageRatio: findings && findings.length
|
|
222
|
+
? (findings.filter(f => f.attck).length / findings.length).toFixed(3)
|
|
223
|
+
: '0.000',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const _internals = { FAMILY_MAP, FAMILY_ALIAS, _canonical };
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
import * as fs from 'node:fs';
|
|
30
30
|
import * as path from 'node:path';
|
|
31
|
+
import { statePath } from './state-dir.js';
|
|
31
32
|
|
|
32
33
|
const DEFAULT_THRESHOLD = 0.15;
|
|
33
34
|
const MIN_SAMPLE_SIZE = 10;
|
|
34
35
|
const WINDOW_DAYS = 30;
|
|
35
36
|
|
|
36
37
|
function loadTriageFeedback(scanRoot) {
|
|
37
|
-
const fp =
|
|
38
|
+
const fp = statePath(scanRoot, 'triage-feedback.json');
|
|
38
39
|
try {
|
|
39
40
|
if (!fs.existsSync(fp)) return [];
|
|
40
41
|
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import * as fs from 'node:fs';
|
|
29
29
|
import * as path from 'node:path';
|
|
30
|
+
import { statePath } from './state-dir.js';
|
|
30
31
|
|
|
31
32
|
const MIN_SAMPLES_FOR_CALIBRATION = 30;
|
|
32
33
|
|
|
@@ -102,7 +103,7 @@ function _readJsonMaybe(fp) {
|
|
|
102
103
|
// seed file. The bundled seed ships with this release; the customer file
|
|
103
104
|
// overrides per-family when N is higher there.
|
|
104
105
|
export function loadCalibrationHistory(scanRoot) {
|
|
105
|
-
const customer = _readJsonMaybe(
|
|
106
|
+
const customer = _readJsonMaybe(statePath(scanRoot, 'validator-metrics.json')) || {};
|
|
106
107
|
const seedPath = new URL('./calibration-seed.json', import.meta.url);
|
|
107
108
|
let seed = null;
|
|
108
109
|
try { seed = JSON.parse(fs.readFileSync(seedPath, 'utf8')); } catch { seed = null; }
|
|
@@ -122,7 +123,7 @@ export function loadCalibrationHistory(scanRoot) {
|
|
|
122
123
|
if (customer) merge(customer);
|
|
123
124
|
// Merge triage-derived TP/FP counts (auto-feedback loop)
|
|
124
125
|
try {
|
|
125
|
-
const triage = _readJsonMaybe(
|
|
126
|
+
const triage = _readJsonMaybe(statePath(scanRoot, 'triage.json'));
|
|
126
127
|
if (triage && triage.findings) {
|
|
127
128
|
const triageFams = {};
|
|
128
129
|
for (const f of Object.values(triage.findings)) {
|