@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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Privacy / PII data-flow tracking — Recommendation #9 of the
|
|
2
|
+
// world-class roadmap.
|
|
3
|
+
//
|
|
4
|
+
// Runs the existing taint engine with a different lattice (PII / PHI /
|
|
5
|
+
// PCI / FIN classes, instead of security taint) to track where each
|
|
6
|
+
// regulated-data class flows through a codebase. Outputs:
|
|
7
|
+
//
|
|
8
|
+
// 1. Per-field PII classification — `user.email: PII (CWE-359 Information
|
|
9
|
+
// Disclosure if reflected)`
|
|
10
|
+
// 2. Data flow diagrams — exit points (sinks) per PII class — where
|
|
11
|
+
// regulated data leaves the application (response body, log file,
|
|
12
|
+
// third-party API call, S3 upload, etc.)
|
|
13
|
+
// 3. Auto-generated DPIA stub for GDPR Art. 35 / CCPA §1798.130 /
|
|
14
|
+
// HIPAA §164.530 — a compliance artifact the customer's privacy
|
|
15
|
+
// counsel can use
|
|
16
|
+
// 4. Findings: each "PII leaves system via untrusted sink" emits a
|
|
17
|
+
// privacy finding with family `pii-exposure`
|
|
18
|
+
//
|
|
19
|
+
// The PII detection is deterministic and field-name based. We DO NOT
|
|
20
|
+
// attempt content classification (Luhn-checking actual values would
|
|
21
|
+
// only catch leaks that have already happened); we classify by NAME
|
|
22
|
+
// + TYPE in declarations.
|
|
23
|
+
|
|
24
|
+
// PII / PHI / PCI / FIN classifiers — each is a regex against
|
|
25
|
+
// field/variable/column names. Same idea as the existing classifyField
|
|
26
|
+
// helpers in engine.js but enumerated for compliance reporting.
|
|
27
|
+
|
|
28
|
+
const PII_PATTERNS = {
|
|
29
|
+
PII: [
|
|
30
|
+
/\bfirst[_-]?name\b/i, /\blast[_-]?name\b/i, /\bfull[_-]?name\b/i,
|
|
31
|
+
/\bemail([_-]?address)?\b/i, /\bphone([_-]?number)?\b/i, /\bmobile\b/i,
|
|
32
|
+
/\baddress(?:_?(?:line|street|city|zip|postal))?\b/i,
|
|
33
|
+
/\bdob\b/i, /\bdate[_-]?of[_-]?birth\b/i, /\bbirthday\b/i, /\bbirthdate\b/i,
|
|
34
|
+
/\bage\b/i, /\bgender\b/i, /\bethnicity\b/i, /\brace\b/i, /\bnationality\b/i,
|
|
35
|
+
/\bssn\b/i, /\bsocial[_-]?security/i, /\bnational[_-]?id/i, /\bpassport\b/i,
|
|
36
|
+
/\bdriver[_-]?license\b/i, /\btax[_-]?id\b/i, /\bgovernment[_-]?id\b/i,
|
|
37
|
+
/\bip[_-]?address\b/i, /\bgeo[_-]?location\b/i, /\blatitude\b/i, /\blongitude\b/i,
|
|
38
|
+
],
|
|
39
|
+
PHI: [
|
|
40
|
+
/\b(?:medical|patient|health)[_-]?record\b/i,
|
|
41
|
+
/\bdiagnosis\b/i, /\bcondition\b/i, /\bsymptom\b/i, /\btreatment\b/i,
|
|
42
|
+
/\bmedication\b/i, /\bprescription\b/i, /\bdosage\b/i,
|
|
43
|
+
/\bicd[_-]?(?:9|10|11)\b/i, /\bcpt[_-]?code\b/i, /\bmrn\b/i,
|
|
44
|
+
/\bmedical[_-]?record[_-]?number\b/i, /\bdoctor[_-]?name\b/i,
|
|
45
|
+
/\bphysician\b/i, /\binsurance[_-]?id\b/i, /\bhealth[_-]?plan\b/i,
|
|
46
|
+
],
|
|
47
|
+
PCI: [
|
|
48
|
+
/\bcredit[_-]?card[_-]?(?:number|num|no)?\b/i,
|
|
49
|
+
/\bcard[_-]?(?:number|num|no)\b/i,
|
|
50
|
+
/\b(?:cvc|cvv)2?\b/i, /\bcvc[_-]?code\b/i,
|
|
51
|
+
/\bexp(?:iry|iration)?(?:_?date)?\b/i,
|
|
52
|
+
/\bcardholder[_-]?name\b/i, /\bpan\b/i,
|
|
53
|
+
/\biban\b/i, /\brouting[_-]?number\b/i,
|
|
54
|
+
/\baccount[_-]?number\b/i,
|
|
55
|
+
],
|
|
56
|
+
FIN: [
|
|
57
|
+
/\bsalary\b/i, /\bincome\b/i, /\bbalance\b/i, /\btransaction[_-]?amount\b/i,
|
|
58
|
+
/\bbank[_-]?account\b/i,
|
|
59
|
+
/\bcredit[_-]?score\b/i, /\bnet[_-]?worth\b/i,
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SINK_PATTERNS = {
|
|
64
|
+
log: /\b(?:log|logger|console|System\.out|System\.err|stdout|stderr|fmt\.Print|print)\b/i,
|
|
65
|
+
response: /\b(?:res|response|ctx\.response|HttpContext\.Response)\s*\.\s*(?:write|send|json|render|body)\b/i,
|
|
66
|
+
outboundHttp: /\bfetch\b(?:$|[(\s.])|\b(?:axios|got|httpClient|HttpClient|WebClient|requests|node_fetch)\s*(?:\.\s*(?:get|post|put|delete|send|invoke|patch|head)|\()/i,
|
|
67
|
+
thirdPartySdk: /\b(?:stripe|sentry|datadog|segment|amplitude|mixpanel|posthog|braze|intercom)\s*\.\s*track|identify|capture\b/i,
|
|
68
|
+
fileWrite: /\b(?:fs\.writeFile|File\.WriteAllText|File\.AppendAllText|open\([^)]*,\s*['"]w)\b/i,
|
|
69
|
+
s3Upload: /\b(?:s3|S3Client|aws\.S3)\s*\.\s*putObject\b/i,
|
|
70
|
+
emailSend: /\b(?:nodemailer|sendMail|SendGrid|sendgrid|smtp)\b/i,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Classify a field/variable name into PII / PHI / PCI / FIN buckets.
|
|
75
|
+
* Returns an array of bucket labels (possibly empty, possibly multiple).
|
|
76
|
+
*/
|
|
77
|
+
export function classifyField(name) {
|
|
78
|
+
if (!name) return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const [bucket, patterns] of Object.entries(PII_PATTERNS)) {
|
|
81
|
+
for (const p of patterns) {
|
|
82
|
+
if (p.test(name)) { out.push(bucket); break; }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Classify an outbound-data sink expression. Returns the matching sink
|
|
90
|
+
* label (log / response / outboundHttp / etc.) or null.
|
|
91
|
+
*/
|
|
92
|
+
export function classifySink(expr) {
|
|
93
|
+
if (!expr) return null;
|
|
94
|
+
for (const [label, p] of Object.entries(SINK_PATTERNS)) if (p.test(expr)) return label;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run a privacy-taint pass over the per-file IR. For each field declared
|
|
100
|
+
* as PII/PHI/PCI/FIN, track flow into a classifySink-matched sink. Emit
|
|
101
|
+
* a privacy-leak finding when a regulated class reaches a non-secure
|
|
102
|
+
* sink (log, response, outbound HTTP, etc.).
|
|
103
|
+
*/
|
|
104
|
+
export function annotatePrivacyTaint(perFileIR) {
|
|
105
|
+
if (!perFileIR) return { findings: [], piiFields: [] };
|
|
106
|
+
const findings = [];
|
|
107
|
+
const piiFields = [];
|
|
108
|
+
for (const [filePath, ir] of (perFileIR instanceof Map ? perFileIR : Object.entries(perFileIR))) {
|
|
109
|
+
if (!ir || !ir._content) continue;
|
|
110
|
+
const lines = ir._content.split('\n');
|
|
111
|
+
// Step 1: collect PII-classified decls.
|
|
112
|
+
const taintedVars = new Map(); // name → array of bucket labels
|
|
113
|
+
for (const d of ir.decls || []) {
|
|
114
|
+
const classes = classifyField(d.name);
|
|
115
|
+
if (classes.length) {
|
|
116
|
+
taintedVars.set(d.name, classes);
|
|
117
|
+
piiFields.push({ file: filePath, line: d.line, name: d.name, classes, declaredType: d.type || null });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Step 2: walk calls and assignments looking for a PII variable
|
|
121
|
+
// reaching a sink.
|
|
122
|
+
for (const call of ir.calls || []) {
|
|
123
|
+
const argText = (call.args || []).map(a => a.text || '').join(',');
|
|
124
|
+
const sinkLabel = classifySink(call.fullPath || call.callee || '');
|
|
125
|
+
if (!sinkLabel) continue;
|
|
126
|
+
for (const [name, classes] of taintedVars) {
|
|
127
|
+
if (!new RegExp(`\\b${name.replace(/[.+^${}()|\\]/g, '\\$&')}\\b`).test(argText)) continue;
|
|
128
|
+
findings.push({
|
|
129
|
+
family: 'pii-exposure',
|
|
130
|
+
subfamily: classes.join('+'),
|
|
131
|
+
file: filePath, line: call.line,
|
|
132
|
+
severity: classes.includes('PCI') || classes.includes('PHI') ? 'high' : 'medium',
|
|
133
|
+
cwe: 'CWE-359', // Exposure of Private Personal Information
|
|
134
|
+
vuln: `Privacy — ${classes.join('+')} data flows to ${sinkLabel} sink`,
|
|
135
|
+
snippet: (lines[call.line - 1] || '').trim().slice(0, 200),
|
|
136
|
+
remediation: `${classes.join(' + ')} data must not flow to ${sinkLabel} unencrypted. Mask, redact, or hash the value before logging / responding / sending to third parties.`,
|
|
137
|
+
piiClass: classes,
|
|
138
|
+
sinkKind: sinkLabel,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { findings, piiFields };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Emit a DPIA (Data Protection Impact Assessment) Markdown artifact
|
|
148
|
+
* summarizing the privacy posture for compliance reporting. Output goes
|
|
149
|
+
* to .agentic-security/dpia.md.
|
|
150
|
+
*/
|
|
151
|
+
export function emitDpiaArtifact(piiFields, findings, opts = {}) {
|
|
152
|
+
const grouped = new Map();
|
|
153
|
+
for (const field of piiFields) {
|
|
154
|
+
for (const cls of field.classes) {
|
|
155
|
+
let g = grouped.get(cls);
|
|
156
|
+
if (!g) { g = []; grouped.set(cls, g); }
|
|
157
|
+
g.push(field);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const lines = [];
|
|
161
|
+
lines.push(`# Data Protection Impact Assessment (DPIA)`);
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push(`Generated by agentic-security scanner on ${new Date().toISOString().slice(0, 10)}.`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push(`This is an automated DPIA scaffold derived from static analysis.`);
|
|
166
|
+
lines.push(`It must be reviewed and completed by a privacy officer before use.`);
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(`## Data classes identified`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
for (const [cls, fields] of grouped) {
|
|
171
|
+
lines.push(`### ${cls} (${fields.length} fields)`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
for (const f of fields.slice(0, 20)) {
|
|
174
|
+
lines.push(`- \`${f.name}\` in \`${f.file}:${f.line}\` (type: ${f.declaredType || 'unknown'})`);
|
|
175
|
+
}
|
|
176
|
+
if (fields.length > 20) lines.push(`- … and ${fields.length - 20} more`);
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
lines.push(`## Privacy-related findings`);
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push(`| Severity | File:Line | Class → Sink | Description |`);
|
|
182
|
+
lines.push(`|---|---|---|---|`);
|
|
183
|
+
for (const f of findings.slice(0, 50)) {
|
|
184
|
+
lines.push(`| ${f.severity} | ${f.file}:${f.line} | ${f.piiClass.join('+')} → ${f.sinkKind} | ${f.vuln} |`);
|
|
185
|
+
}
|
|
186
|
+
if (findings.length > 50) lines.push(`| … | … | … | … and ${findings.length - 50} more |`);
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(`## Regulatory framework mapping`);
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push(`- **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.`);
|
|
191
|
+
lines.push(`- **CCPA §1798.130** — Notice + access rights for collected personal information.`);
|
|
192
|
+
if (grouped.has('PHI')) lines.push(`- **HIPAA §164.308** — Administrative safeguards for ePHI access.`);
|
|
193
|
+
if (grouped.has('PCI')) lines.push(`- **PCI DSS Req. 3** — Protect stored cardholder data.`);
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(`## Reviewer checklist`);
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push(`- [ ] Confirm each PII field's collection has a documented lawful basis`);
|
|
198
|
+
lines.push(`- [ ] Confirm retention period for each class is documented`);
|
|
199
|
+
lines.push(`- [ ] Confirm DSAR (data subject access request) workflow exists`);
|
|
200
|
+
lines.push(`- [ ] Confirm encryption at rest + in transit for each class`);
|
|
201
|
+
lines.push(`- [ ] Confirm logging of PII access for audit (where applicable)`);
|
|
202
|
+
return lines.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const _internals = { PII_PATTERNS, SINK_PATTERNS };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SMT path feasibility — Recommendation #3 of the world-class roadmap.
|
|
2
|
+
//
|
|
3
|
+
// For top-N findings per scan, generate SMT constraints from the IR
|
|
4
|
+
// representing the conditions that must hold along the call-graph path
|
|
5
|
+
// from source to sink. Discharge via a Z3 solver. If UNSAT, the
|
|
6
|
+
// finding is provably infeasible and gets demoted to 'info' severity
|
|
7
|
+
// with `pathFeasibility: 'unsat'`. If SAT, we emit a sample witness
|
|
8
|
+
// (a concrete tainted input that triggers the sink) which is gold-standard
|
|
9
|
+
// evidence for the developer.
|
|
10
|
+
//
|
|
11
|
+
// Solver backend: prefers `z3-solver` (Z3 WASM published on npm) when
|
|
12
|
+
// installed; falls back to a constraint-emission-only mode that still
|
|
13
|
+
// records the SMT-LIB script so a CI step can discharge it offline.
|
|
14
|
+
//
|
|
15
|
+
// Gating: opt-in via AGENTIC_SECURITY_SMT_FEASIBILITY=1. Always bounded
|
|
16
|
+
// at top-MAX_PROOF_OBLIGATIONS findings per scan to keep wall-clock
|
|
17
|
+
// under PROOF_BUDGET_MS.
|
|
18
|
+
//
|
|
19
|
+
// IMPORTANT — this module is NOT a generic symbolic executor. It targets
|
|
20
|
+
// a narrow shape: "does there exist an input that flows from source S
|
|
21
|
+
// through path P to sink K?" That's enough to prove or refute the
|
|
22
|
+
// reachability claim on a finding the engine already produced. We do
|
|
23
|
+
// NOT attempt to prove arbitrary safety properties.
|
|
24
|
+
|
|
25
|
+
const PROOF_BUDGET_MS_DEFAULT = 30_000;
|
|
26
|
+
const MAX_PROOF_OBLIGATIONS_DEFAULT = 50;
|
|
27
|
+
const PER_QUERY_TIMEOUT_MS_DEFAULT = 5_000;
|
|
28
|
+
|
|
29
|
+
// Lazy-load Z3. The module is permitted to be absent — when it is, we
|
|
30
|
+
// fall back to constraint-emission-only mode (the SMT-LIB script is
|
|
31
|
+
// attached to the finding for offline discharge).
|
|
32
|
+
let _z3Mod = null;
|
|
33
|
+
let _z3LoadAttempted = false;
|
|
34
|
+
async function _loadZ3() {
|
|
35
|
+
if (_z3LoadAttempted) return _z3Mod;
|
|
36
|
+
_z3LoadAttempted = true;
|
|
37
|
+
try {
|
|
38
|
+
_z3Mod = await import('z3-solver');
|
|
39
|
+
if (typeof _z3Mod.init === 'function') await _z3Mod.init();
|
|
40
|
+
} catch { _z3Mod = null; }
|
|
41
|
+
return _z3Mod;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Constraint emission ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Encode a single IR predicate (one node along the path) into an SMT-LIB
|
|
48
|
+
* assertion. Predicates supported in v1:
|
|
49
|
+
* - `var = source(name)` — declares var as a free symbolic string
|
|
50
|
+
* - `var = const(literal)` — equality with a constant
|
|
51
|
+
* - `var = concat(a, b)` — string concatenation
|
|
52
|
+
* - `var = sanitize(x, kind)` — applies a sanitizer; encoded as
|
|
53
|
+
* `var = "safe"` (forces concrete)
|
|
54
|
+
* - `assert reach(line N)` — terminal predicate: this line must be
|
|
55
|
+
* reachable
|
|
56
|
+
* - `guard(cond)` — a path condition (free-form text)
|
|
57
|
+
*/
|
|
58
|
+
function encodePredicate(p, idx) {
|
|
59
|
+
switch (p.kind) {
|
|
60
|
+
case 'source':
|
|
61
|
+
return `(declare-const ${p.var} String)`;
|
|
62
|
+
case 'const':
|
|
63
|
+
return `(assert (= ${p.var} ${JSON.stringify(p.value)}))`;
|
|
64
|
+
case 'concat':
|
|
65
|
+
return `(assert (= ${p.var} (str.++ ${p.a} ${p.b})))`;
|
|
66
|
+
case 'sanitize':
|
|
67
|
+
return `(assert (= ${p.var} "safe-${p.kind}-${idx}"))`;
|
|
68
|
+
case 'reach':
|
|
69
|
+
// Symbolic "this line is reached" — we don't really model reachability,
|
|
70
|
+
// we just record the obligation. The presence of the path is what
|
|
71
|
+
// matters; SAT just means "some input satisfies the path conditions."
|
|
72
|
+
return `; reach(${p.file}:${p.line})`;
|
|
73
|
+
case 'guard':
|
|
74
|
+
return `(assert ${p.smtCond || `(= ${p.var} ${JSON.stringify(p.value)})`})`;
|
|
75
|
+
default:
|
|
76
|
+
return `; unsupported predicate kind: ${p.kind}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Emit a complete SMT-LIB script for one finding. The script declares
|
|
82
|
+
* source variables, asserts every predicate, asks (check-sat). On SAT
|
|
83
|
+
* we (get-model) for the witness; on UNSAT the finding is infeasible.
|
|
84
|
+
*/
|
|
85
|
+
export function emitSmtScript(predicates, opts = {}) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
lines.push('; SMT-LIB script — emitted by scanner/src/dataflow/smt-feasibility.js');
|
|
88
|
+
lines.push(`(set-logic QF_S)`);
|
|
89
|
+
lines.push(`(set-option :timeout ${opts.timeoutMs || PER_QUERY_TIMEOUT_MS_DEFAULT})`);
|
|
90
|
+
predicates.forEach((p, i) => lines.push(encodePredicate(p, i)));
|
|
91
|
+
lines.push('(check-sat)');
|
|
92
|
+
lines.push('(get-model)');
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Z3 discharge ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* dischargeFinding(predicates, opts) — encode + solve. Returns one of:
|
|
100
|
+
* { verdict: 'sat', witness: { var: value } }
|
|
101
|
+
* { verdict: 'unsat' }
|
|
102
|
+
* { verdict: 'unknown', reason: '<why>' }
|
|
103
|
+
* { verdict: 'pending', script: '<smt-lib text>' } // when Z3 unavailable
|
|
104
|
+
*/
|
|
105
|
+
export async function dischargeFinding(predicates, opts = {}) {
|
|
106
|
+
if (!predicates || !predicates.length) return { verdict: 'unknown', reason: 'no-predicates' };
|
|
107
|
+
const script = emitSmtScript(predicates, opts);
|
|
108
|
+
const z3 = await _loadZ3();
|
|
109
|
+
if (!z3) return { verdict: 'pending', script };
|
|
110
|
+
try {
|
|
111
|
+
const { Context } = z3;
|
|
112
|
+
const ctx = new Context('main');
|
|
113
|
+
const solver = new ctx.Solver();
|
|
114
|
+
// Feed the script via parse — z3-solver supports SMT-LIB ingestion.
|
|
115
|
+
try { solver.fromString(script); }
|
|
116
|
+
catch (e) {
|
|
117
|
+
return { verdict: 'unknown', reason: 'parse-error: ' + String(e && e.message), script };
|
|
118
|
+
}
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
const result = await Promise.race([
|
|
121
|
+
solver.check(),
|
|
122
|
+
new Promise(resolve => setTimeout(() => resolve('timeout'), opts.timeoutMs || PER_QUERY_TIMEOUT_MS_DEFAULT)),
|
|
123
|
+
]);
|
|
124
|
+
const elapsed = Date.now() - start;
|
|
125
|
+
if (result === 'unsat') return { verdict: 'unsat', elapsedMs: elapsed };
|
|
126
|
+
if (result === 'timeout' || result === 'unknown') return { verdict: 'unknown', reason: result, elapsedMs: elapsed, script };
|
|
127
|
+
if (result === 'sat') {
|
|
128
|
+
// Best-effort witness extraction.
|
|
129
|
+
let witness = {};
|
|
130
|
+
try {
|
|
131
|
+
const model = solver.model();
|
|
132
|
+
for (const decl of model.decls()) witness[decl.name()] = String(model.get(decl));
|
|
133
|
+
} catch { /* no model */ }
|
|
134
|
+
return { verdict: 'sat', witness, elapsedMs: elapsed };
|
|
135
|
+
}
|
|
136
|
+
return { verdict: 'unknown', reason: String(result), elapsedMs: elapsed };
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return { verdict: 'unknown', reason: String(e && e.message || e), script };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Finding-level integration ─────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Annotate the top-N findings with their feasibility verdict. Modifies
|
|
146
|
+
* findings in place — each gets a `pathFeasibility` field and (when
|
|
147
|
+
* SAT) a `feasibilityWitness` object. Findings whose verdict is UNSAT
|
|
148
|
+
* are demoted to 'info' severity.
|
|
149
|
+
*/
|
|
150
|
+
export async function annotatePathFeasibility(findings, opts = {}) {
|
|
151
|
+
if (!Array.isArray(findings)) return { annotated: 0, demoted: 0 };
|
|
152
|
+
const budget = opts.budgetMs || PROOF_BUDGET_MS_DEFAULT;
|
|
153
|
+
const max = opts.maxObligations || MAX_PROOF_OBLIGATIONS_DEFAULT;
|
|
154
|
+
// Prioritize: critical/high findings with concrete chains first.
|
|
155
|
+
const sorted = [...findings]
|
|
156
|
+
.filter(f => f.severity === 'critical' || f.severity === 'high')
|
|
157
|
+
.filter(f => Array.isArray(f.chain) || Array.isArray(f.taintPath))
|
|
158
|
+
.slice(0, max);
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
let annotated = 0, demoted = 0;
|
|
161
|
+
for (const f of sorted) {
|
|
162
|
+
if (Date.now() - start > budget) {
|
|
163
|
+
f.pathFeasibility = 'unknown';
|
|
164
|
+
f.feasibilityReason = 'budget-exceeded';
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const predicates = (f.chain || f.taintPath || []).map((step, i) => ({
|
|
168
|
+
kind: i === 0 ? 'source' : (step.kind || 'concat'),
|
|
169
|
+
var: `v${i}`,
|
|
170
|
+
a: `v${Math.max(0, i - 1)}`, b: '""',
|
|
171
|
+
value: step.value || '',
|
|
172
|
+
file: step.file, line: step.line,
|
|
173
|
+
}));
|
|
174
|
+
const r = await dischargeFinding(predicates, { timeoutMs: Math.min(5_000, budget) });
|
|
175
|
+
f.pathFeasibility = r.verdict;
|
|
176
|
+
if (r.witness) f.feasibilityWitness = r.witness;
|
|
177
|
+
if (r.script) f._smtScript = r.script.slice(0, 4000);
|
|
178
|
+
annotated++;
|
|
179
|
+
if (r.verdict === 'unsat') {
|
|
180
|
+
const before = f.severity;
|
|
181
|
+
f.severity = 'info';
|
|
182
|
+
f._pathFeasibilityDemoted = before;
|
|
183
|
+
demoted++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { annotated, demoted, elapsedMs: Date.now() - start };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const _internals = { encodePredicate, emitSmtScript, PROOF_BUDGET_MS_DEFAULT, MAX_PROOF_OBLIGATIONS_DEFAULT };
|