@clear-capabilities/agentic-security-scanner 0.86.0 → 0.118.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +404 -0
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +84 -84
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +12 -6
- package/src/.agentic-security/dpia.md +26 -0
- package/src/.agentic-security/pqc-migration-plan.json +80 -0
- package/src/.agentic-security/pqc-migration-plan.md +32 -0
- package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/.agentic-security/threat-model.json +3702 -0
- package/src/.agentic-security/threat-model.md +118 -0
- package/src/dataflow/.agentic-security/dpia.md +26 -0
- package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/dataflow/.agentic-security/threat-model.json +161 -0
- package/src/dataflow/.agentic-security/threat-model.md +43 -0
- package/src/dataflow/CLAUDE.md +17 -5
- package/src/dataflow/catalog.js +9 -0
- package/src/dataflow/engine.js +30 -4
- package/src/dataflow/index.js +8 -0
- package/src/dataflow/proof-gate.js +95 -0
- package/src/dataflow/summaries.js +44 -5
- package/src/engine.js +199 -5
- package/src/ir/.agentic-security/dpia.md +26 -0
- package/src/ir/.agentic-security/exploit-bundles.json +77 -0
- package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
- package/src/ir/.agentic-security/threat-model.json +143 -0
- package/src/ir/.agentic-security/threat-model.md +39 -0
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/callgraph.js +12 -1
- package/src/ir/tree-sitter-loader.js +108 -0
- package/src/posture/.agentic-security/dpia.md +1 -1
- package/src/posture/.agentic-security/pqc-migration-plan.json +1 -1
- package/src/posture/.agentic-security/pqc-migration-plan.md +1 -1
- package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
- package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/posture/.agentic-security/threat-model.json +3 -3
- package/src/posture/.agentic-security/threat-model.md +1 -1
- package/src/posture/corpus-status.js +74 -0
- package/src/posture/coverage-report.js +113 -0
- package/src/posture/holdout-eval.js +57 -0
- package/src/posture/router.js +33 -3
- package/src/report/.agentic-security/dpia.md +26 -0
- package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
- package/src/report/.agentic-security/threat-model.md +1 -1
- package/src/report/index.js +17 -0
- package/src/sast/.agentic-security/dpia.md +26 -0
- package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
- package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
- package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
- package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
- package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
- package/src/sast/.agentic-security/threat-model.json +816 -0
- package/src/sast/.agentic-security/threat-model.md +91 -0
- package/src/sast/CLAUDE.md +9 -3
- package/src/sast/code-injection-multilang.js +117 -0
- package/src/sast/crypto-protocol.js +43 -1
- package/src/sast/csharp-structural.js +65 -0
- package/src/sast/csharp.js +3 -0
- package/src/sast/csrf.js +24 -3
- package/src/sast/csv-injection.js +84 -0
- package/src/sast/frontend-hygiene.js +106 -0
- package/src/sast/go-structural.js +60 -0
- package/src/sast/java-structural.js +81 -0
- package/src/sast/js-framework-structural.js +115 -0
- package/src/sast/kotlin.js +72 -0
- package/src/sast/ldap-injection.js +73 -17
- package/src/sast/open-redirect.js +47 -5
- package/src/sast/php.js +35 -0
- package/src/sast/python-structural.js +83 -0
- package/src/sast/redos-nfa.js +75 -0
- package/src/sast/response-splitting.js +48 -19
- package/src/sast/ruby.js +38 -0
- package/src/sast/secret-concat.js +60 -0
- package/src/sast/ssrf-cloud-metadata.js +13 -0
- package/src/sast/stored-taint.js +88 -0
- package/src/sast/tree-sitter-sinks.js +62 -0
- package/src/sast/weak-password-hash.js +28 -0
- package/src/sast/weak-randomness.js +46 -5
- package/src/sast/wrong-context-sanitizer.js +94 -0
- package/src/sast/xpath-injection.js +30 -6
- package/src/sast/xss-reflected-multilang.js +116 -0
- package/src/sast/xxe.js +67 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entities": [],
|
|
3
|
+
"boundaries": [],
|
|
4
|
+
"assets": [],
|
|
5
|
+
"threats": [
|
|
6
|
+
{
|
|
7
|
+
"stride": "T",
|
|
8
|
+
"strideLabel": "Tampering",
|
|
9
|
+
"cwe": "CWE-915",
|
|
10
|
+
"family": "mass-assignment",
|
|
11
|
+
"severity": "high",
|
|
12
|
+
"file": "parser-cs.js",
|
|
13
|
+
"vuln": "Mass Assignment (req.body Direct to Model)",
|
|
14
|
+
"finding_id": "struct:parser-cs.js:208:Mass_Assignment_(req.body_Direct_to_Model)",
|
|
15
|
+
"affectsAsset": null,
|
|
16
|
+
"atEntity": null
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"stride": "T",
|
|
20
|
+
"strideLabel": "Tampering",
|
|
21
|
+
"cwe": "CWE-915",
|
|
22
|
+
"family": "mass-assignment",
|
|
23
|
+
"severity": "high",
|
|
24
|
+
"file": "parser-go.js",
|
|
25
|
+
"vuln": "Mass Assignment (req.body Direct to Model)",
|
|
26
|
+
"finding_id": "struct:parser-go.js:253:Mass_Assignment_(req.body_Direct_to_Model)",
|
|
27
|
+
"affectsAsset": null,
|
|
28
|
+
"atEntity": null
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"stride": "T",
|
|
32
|
+
"strideLabel": "Tampering",
|
|
33
|
+
"cwe": "CWE-915",
|
|
34
|
+
"family": "mass-assignment",
|
|
35
|
+
"severity": "high",
|
|
36
|
+
"file": "parser-kt.js",
|
|
37
|
+
"vuln": "Mass Assignment (req.body Direct to Model)",
|
|
38
|
+
"finding_id": "struct:parser-kt.js:207:Mass_Assignment_(req.body_Direct_to_Model)",
|
|
39
|
+
"affectsAsset": null,
|
|
40
|
+
"atEntity": null
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"stride": "T",
|
|
44
|
+
"strideLabel": "Tampering",
|
|
45
|
+
"cwe": "CWE-915",
|
|
46
|
+
"family": "mass-assignment",
|
|
47
|
+
"severity": "high",
|
|
48
|
+
"file": "parser-php.js",
|
|
49
|
+
"vuln": "Mass Assignment (req.body Direct to Model)",
|
|
50
|
+
"finding_id": "struct:parser-php.js:209:Mass_Assignment_(req.body_Direct_to_Model)",
|
|
51
|
+
"affectsAsset": null,
|
|
52
|
+
"atEntity": null
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"stride": "T",
|
|
56
|
+
"strideLabel": "Tampering",
|
|
57
|
+
"cwe": "CWE-915",
|
|
58
|
+
"family": "mass-assignment",
|
|
59
|
+
"severity": "high",
|
|
60
|
+
"file": "parser-rb.js",
|
|
61
|
+
"vuln": "Mass Assignment (req.body Direct to Model)",
|
|
62
|
+
"finding_id": "struct:parser-rb.js:201:Mass_Assignment_(req.body_Direct_to_Model)",
|
|
63
|
+
"affectsAsset": null,
|
|
64
|
+
"atEntity": null
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"stride": "T",
|
|
68
|
+
"strideLabel": "Tampering",
|
|
69
|
+
"cwe": "CWE-400",
|
|
70
|
+
"family": "dos-sync-io",
|
|
71
|
+
"severity": "medium",
|
|
72
|
+
"file": "cpp-preprocessor.js",
|
|
73
|
+
"vuln": "Synchronous Blocking I/O (DoS Risk in Server Context)",
|
|
74
|
+
"finding_id": "struct:cpp-preprocessor.js:94:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
|
|
75
|
+
"affectsAsset": null,
|
|
76
|
+
"atEntity": null
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"stride": "T",
|
|
80
|
+
"strideLabel": "Tampering",
|
|
81
|
+
"cwe": "CWE-400",
|
|
82
|
+
"family": "dos-sync-io",
|
|
83
|
+
"severity": "medium",
|
|
84
|
+
"file": "type-stubs.js",
|
|
85
|
+
"vuln": "Synchronous Blocking I/O (DoS Risk in Server Context)",
|
|
86
|
+
"finding_id": "struct:type-stubs.js:48:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
|
|
87
|
+
"affectsAsset": null,
|
|
88
|
+
"atEntity": null
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"stride": "T",
|
|
92
|
+
"strideLabel": "Tampering",
|
|
93
|
+
"cwe": "CWE-400",
|
|
94
|
+
"family": "dos-sync-io",
|
|
95
|
+
"severity": "medium",
|
|
96
|
+
"file": "parser-py-cst.js",
|
|
97
|
+
"vuln": "Synchronous Blocking I/O (DoS Risk in Server Context)",
|
|
98
|
+
"finding_id": "struct:parser-py-cst.js:91:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
|
|
99
|
+
"affectsAsset": null,
|
|
100
|
+
"atEntity": null
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"stride": "I",
|
|
104
|
+
"strideLabel": "Information Disclosure",
|
|
105
|
+
"cwe": "CWE-327",
|
|
106
|
+
"family": "crypto-weak-hash",
|
|
107
|
+
"severity": "medium",
|
|
108
|
+
"file": "parser-py.helper.py",
|
|
109
|
+
"line": 68,
|
|
110
|
+
"vuln": "Weak hash algorithm (MD5 / SHA-1 / MD2 / MD4) used",
|
|
111
|
+
"finding_id": "crypto-weak-hash:parser-py.helper.py:68",
|
|
112
|
+
"affectsAsset": null,
|
|
113
|
+
"atEntity": null
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"stride": "T",
|
|
117
|
+
"strideLabel": "Tampering",
|
|
118
|
+
"cwe": "CWE-367",
|
|
119
|
+
"family": "toctou-file-existence-permission-check-b",
|
|
120
|
+
"severity": "medium",
|
|
121
|
+
"file": "type-stubs.js",
|
|
122
|
+
"line": 48,
|
|
123
|
+
"vuln": "TOCTOU: file existence/permission check before open",
|
|
124
|
+
"finding_id": "toctou-fs:type-stubs.js:48",
|
|
125
|
+
"affectsAsset": null,
|
|
126
|
+
"atEntity": null
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"stride": "T",
|
|
130
|
+
"strideLabel": "Tampering",
|
|
131
|
+
"cwe": "CWE-176",
|
|
132
|
+
"family": "path-normalization",
|
|
133
|
+
"severity": "medium",
|
|
134
|
+
"file": "universal-ir.js",
|
|
135
|
+
"line": 93,
|
|
136
|
+
"vuln": "Path Normalization Gap — extension extraction without null-byte/traversal sanitization",
|
|
137
|
+
"finding_id": "path-norm:universal-ir.js:93",
|
|
138
|
+
"affectsAsset": null,
|
|
139
|
+
"atEntity": null
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"attackTrees": []
|
|
143
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Threat Model (auto-generated)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security on 2026-05-31.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Entities + boundaries
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
subgraph External
|
|
12
|
+
end
|
|
13
|
+
subgraph Application
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Assets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## STRIDE threats
|
|
21
|
+
|
|
22
|
+
### Tampering (10)
|
|
23
|
+
|
|
24
|
+
- [high] **mass-assignment** (CWE-915) at `parser-cs.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
25
|
+
- [high] **mass-assignment** (CWE-915) at `parser-go.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
26
|
+
- [high] **mass-assignment** (CWE-915) at `parser-kt.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
27
|
+
- [high] **mass-assignment** (CWE-915) at `parser-php.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
28
|
+
- [high] **mass-assignment** (CWE-915) at `parser-rb.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
29
|
+
- [medium] **dos-sync-io** (CWE-400) at `cpp-preprocessor.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
30
|
+
- [medium] **dos-sync-io** (CWE-400) at `type-stubs.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
31
|
+
- [medium] **dos-sync-io** (CWE-400) at `parser-py-cst.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
32
|
+
- [medium] **toctou-file-existence-permission-check-b** (CWE-367) at `type-stubs.js:48` — TOCTOU: file existence/permission check before open
|
|
33
|
+
- [medium] **path-normalization** (CWE-176) at `universal-ir.js:93` — Path Normalization Gap — extension extraction without null-byte/traversal sanitization
|
|
34
|
+
|
|
35
|
+
### Information Disclosure (1)
|
|
36
|
+
|
|
37
|
+
- [medium] **crypto-weak-hash** (CWE-327) at `parser-py.helper.py:68` — Weak hash algorithm (MD5 / SHA-1 / MD2 / MD4) used
|
|
38
|
+
|
|
39
|
+
## Attack trees
|
package/src/ir/CLAUDE.md
CHANGED
|
@@ -11,6 +11,7 @@ consumed by `scanner/src/dataflow/` for taint analysis.
|
|
|
11
11
|
| Python | `parser-py-cst.js` | Python 3.8+ stdlib `ast` via subprocess (default when available) |
|
|
12
12
|
| Python | `parser-py.js` | Hand-rolled regex parser (fallback when python3 missing) |
|
|
13
13
|
| Java | `parser-java.js` | `java-parser` npm package (async) |
|
|
14
|
+
| Long-tail (rust/solidity/cpp/c/go/swift/dart) | `tree-sitter-loader.js` | **Optional** `web-tree-sitter` + `tree-sitter-wasms` (ABI-pinned 0.20.8 ↔ 0.1.13), lazy + degrades when absent. Powers `sast/tree-sitter-sinks.js` (opt-in via `AGENTIC_SECURITY_TREE_SITTER=1`). Marked `--external` in the build so the committed bundle never embeds WASM. |
|
|
14
15
|
|
|
15
16
|
## Python parser — dual-path with auto fallback
|
|
16
17
|
|
package/src/ir/callgraph.js
CHANGED
|
@@ -67,8 +67,19 @@ export function buildCallGraph(perFileIR, fileContents) {
|
|
|
67
67
|
// the call graph for the callee's qid at the assign-from-call site.
|
|
68
68
|
// Same precedence as the edge resolution above (same-file ident wins,
|
|
69
69
|
// ClassName.method falls back).
|
|
70
|
-
function resolve(name) {
|
|
70
|
+
function resolve(name, callerFile) {
|
|
71
71
|
if (!name || typeof name !== 'string') return null;
|
|
72
|
+
// Roadmap #3: same-file preference. A bare name (`handler`, `save`,
|
|
73
|
+
// `query`) defined in several files would otherwise resolve to whichever
|
|
74
|
+
// file Map is iterated first, mis-targeting interprocedural taint to a
|
|
75
|
+
// same-named function in an unrelated file. When the caller's own file
|
|
76
|
+
// defines the name, that is overwhelmingly the intended callee — prefer
|
|
77
|
+
// it. Backward-compatible: with no callerFile (or no local match) the
|
|
78
|
+
// original resolution order is unchanged, so no edge is ever dropped.
|
|
79
|
+
if (callerFile) {
|
|
80
|
+
const local = byNameInFile.get(callerFile);
|
|
81
|
+
if (local && local.has(name)) return local.get(name);
|
|
82
|
+
}
|
|
72
83
|
for (const m of byNameInFile.values()) {
|
|
73
84
|
if (m.has(name)) return m.get(name);
|
|
74
85
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Tree-sitter parser loader (roadmap #8) — OPTIONAL, runtime-lazy, degrades.
|
|
2
|
+
//
|
|
3
|
+
// web-tree-sitter + tree-sitter-wasms are OPTIONAL dependencies (pinned to an
|
|
4
|
+
// ABI-matched pair: web-tree-sitter 0.20.8 ↔ tree-sitter-wasms 0.1.13). They
|
|
5
|
+
// are NOT bundled into dist (the build marks them `--external`), so the
|
|
6
|
+
// committed bundle stays self-contained and small; this loader requires them
|
|
7
|
+
// lazily at runtime and returns null when they're absent. That keeps the
|
|
8
|
+
// scanner fully bootable offline / without the optional deps — long-tail
|
|
9
|
+
// languages simply fall back to the existing pattern detectors.
|
|
10
|
+
//
|
|
11
|
+
// Long-tail languages this unlocks (no first-class IR parser today):
|
|
12
|
+
// rust, solidity, cpp, c, go, swift, dart. (js/ts/py/java/etc. already have
|
|
13
|
+
// dedicated IR parsers and don't need this.)
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
// language → prebuilt grammar wasm shipped by tree-sitter-wasms.
|
|
20
|
+
const GRAMMAR_WASM = {
|
|
21
|
+
rust: 'tree-sitter-rust.wasm',
|
|
22
|
+
solidity: 'tree-sitter-solidity.wasm',
|
|
23
|
+
cpp: 'tree-sitter-cpp.wasm',
|
|
24
|
+
c: 'tree-sitter-c.wasm',
|
|
25
|
+
go: 'tree-sitter-go.wasm',
|
|
26
|
+
swift: 'tree-sitter-swift.wasm',
|
|
27
|
+
dart: 'tree-sitter-dart.wasm',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const EXT_TO_LANG = {
|
|
31
|
+
rs: 'rust', sol: 'solidity', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
32
|
+
c: 'c', h: 'c', go: 'go', swift: 'swift', dart: 'dart',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function treeSitterLangOf(file) {
|
|
36
|
+
if (typeof file !== 'string') return null;
|
|
37
|
+
const ext = file.split('.').pop().toLowerCase();
|
|
38
|
+
return EXT_TO_LANG[ext] || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let _Parser; // undefined = not tried, null = unavailable, fn = the class
|
|
42
|
+
let _initPromise = null;
|
|
43
|
+
const _langCache = new Map(); // language → Language | null
|
|
44
|
+
const _parserCache = new Map(); // language → Parser | null
|
|
45
|
+
|
|
46
|
+
async function _ensureRuntime() {
|
|
47
|
+
if (_Parser === null) return null;
|
|
48
|
+
if (_Parser) return _Parser;
|
|
49
|
+
try {
|
|
50
|
+
// 0.20 API: the module's default export IS the Parser class.
|
|
51
|
+
const mod = require('web-tree-sitter');
|
|
52
|
+
_Parser = mod && (mod.Parser || mod.default || mod);
|
|
53
|
+
if (!_initPromise) _initPromise = _Parser.init();
|
|
54
|
+
await _initPromise;
|
|
55
|
+
return _Parser;
|
|
56
|
+
} catch {
|
|
57
|
+
_Parser = null; // optional dep absent or failed to init → degrade.
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _grammarPath(language) {
|
|
63
|
+
const file = GRAMMAR_WASM[language];
|
|
64
|
+
if (!file) return null;
|
|
65
|
+
try { return require.resolve('tree-sitter-wasms/out/' + file); } catch { return null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Returns a ready-to-use Parser for `language`, or null if tree-sitter or the
|
|
69
|
+
// grammar isn't available. Cached per language. `parser.parse(src)` is then
|
|
70
|
+
// synchronous, so callers can parse inside a sync loop after awaiting this.
|
|
71
|
+
export async function getParserFor(language) {
|
|
72
|
+
if (_parserCache.has(language)) return _parserCache.get(language);
|
|
73
|
+
const Parser = await _ensureRuntime();
|
|
74
|
+
if (!Parser) { _parserCache.set(language, null); return null; }
|
|
75
|
+
try {
|
|
76
|
+
let Lang = _langCache.get(language);
|
|
77
|
+
if (Lang === undefined) {
|
|
78
|
+
const wasm = _grammarPath(language);
|
|
79
|
+
Lang = wasm ? await Parser.Language.load(wasm) : null;
|
|
80
|
+
_langCache.set(language, Lang);
|
|
81
|
+
}
|
|
82
|
+
if (!Lang) { _parserCache.set(language, null); return null; }
|
|
83
|
+
const p = new Parser();
|
|
84
|
+
p.setLanguage(Lang);
|
|
85
|
+
_parserCache.set(language, p);
|
|
86
|
+
return p;
|
|
87
|
+
} catch {
|
|
88
|
+
_parserCache.set(language, null);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function isTreeSitterAvailable() {
|
|
94
|
+
return (await _ensureRuntime()) != null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Walk every named node, invoking cb(node). Iterative to avoid deep recursion.
|
|
98
|
+
export function walkNamed(root, cb) {
|
|
99
|
+
if (!root) return;
|
|
100
|
+
const stack = [root];
|
|
101
|
+
while (stack.length) {
|
|
102
|
+
const n = stack.pop();
|
|
103
|
+
cb(n);
|
|
104
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) stack.push(n.namedChild(i));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const _internals = { GRAMMAR_WASM, EXT_TO_LANG, _grammarPath };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Data Protection Impact Assessment (DPIA)
|
|
2
2
|
|
|
3
|
-
Generated by agentic-security scanner on 2026-05-
|
|
3
|
+
Generated by agentic-security scanner on 2026-05-31.
|
|
4
4
|
|
|
5
5
|
This is an automated DPIA scaffold derived from static analysis.
|
|
6
6
|
It must be reviewed and completed by a privacy officer before use.
|
|
@@ -396,7 +396,7 @@
|
|
|
396
396
|
"severity": "low",
|
|
397
397
|
"file": "holdout-eval.js",
|
|
398
398
|
"vuln": "Synchronous Blocking I/O (DoS Risk in Server Context)",
|
|
399
|
-
"finding_id": "struct:holdout-eval.js:
|
|
399
|
+
"finding_id": "struct:holdout-eval.js:73:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
|
|
400
400
|
"affectsAsset": null,
|
|
401
401
|
"atEntity": null
|
|
402
402
|
},
|
|
@@ -1559,9 +1559,9 @@
|
|
|
1559
1559
|
"family": "toctou-file-existence-permission-check-b",
|
|
1560
1560
|
"severity": "low",
|
|
1561
1561
|
"file": "holdout-eval.js",
|
|
1562
|
-
"line":
|
|
1562
|
+
"line": 73,
|
|
1563
1563
|
"vuln": "TOCTOU: file existence/permission check before open",
|
|
1564
|
-
"finding_id": "toctou-fs:holdout-eval.js:
|
|
1564
|
+
"finding_id": "toctou-fs:holdout-eval.js:73",
|
|
1565
1565
|
"affectsAsset": null,
|
|
1566
1566
|
"atEntity": null
|
|
1567
1567
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Threat Model (auto-generated)
|
|
2
2
|
|
|
3
|
-
Generated by agentic-security on 2026-05-
|
|
3
|
+
Generated by agentic-security on 2026-05-31.
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Corpus coverage / gap reporter (roadmap #10).
|
|
2
|
+
//
|
|
3
|
+
// "Scale the ground-truth corpus" is a data-acquisition effort, not a code one
|
|
4
|
+
// — but you can't prioritize what you can't see. This turns the CVE-replay
|
|
5
|
+
// corpus into an actionable coverage map: progress toward the target size, and
|
|
6
|
+
// exactly which CWE×language cells have zero ground-truth entries. It measures
|
|
7
|
+
// the existing corpus only; it never invents entries.
|
|
8
|
+
|
|
9
|
+
// The corpus's declared target families/languages (mirrors bench manifest).
|
|
10
|
+
export const TARGET_CWES = [
|
|
11
|
+
'CWE-22', 'CWE-78', 'CWE-79', 'CWE-89', 'CWE-90', 'CWE-94', 'CWE-113',
|
|
12
|
+
'CWE-327', 'CWE-329', 'CWE-338', 'CWE-352', 'CWE-502', 'CWE-601', 'CWE-611',
|
|
13
|
+
'CWE-643', 'CWE-798', 'CWE-916', 'CWE-918', 'CWE-1321', 'CWE-1333',
|
|
14
|
+
];
|
|
15
|
+
export const TARGET_LANGUAGES = [
|
|
16
|
+
'javascript', 'python', 'java', 'go', 'ruby', 'php', 'csharp', 'kotlin',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Pure analyzer. `entries` = [{ cwe, language, family, cve, tier? }, …].
|
|
20
|
+
export function analyzeCorpus(entries, opts = {}) {
|
|
21
|
+
const target = opts.target || 500;
|
|
22
|
+
const cwes = opts.cweFamilies || TARGET_CWES;
|
|
23
|
+
const langs = opts.languages || TARGET_LANGUAGES;
|
|
24
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
25
|
+
|
|
26
|
+
const byLanguage = {}, byCwe = {}, matrix = {};
|
|
27
|
+
for (const e of list) {
|
|
28
|
+
const lang = (e && e.language) || 'unknown';
|
|
29
|
+
const cwe = (e && e.cwe) || 'unknown';
|
|
30
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
31
|
+
byCwe[cwe] = (byCwe[cwe] || 0) + 1;
|
|
32
|
+
(matrix[cwe] || (matrix[cwe] = {}))[lang] = (matrix[cwe][lang] || 0) + 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Empty cells in the target CWE×language matrix = prioritized gaps.
|
|
36
|
+
const gaps = [];
|
|
37
|
+
for (const cwe of cwes) {
|
|
38
|
+
for (const lang of langs) {
|
|
39
|
+
if (!(matrix[cwe] && matrix[cwe][lang])) gaps.push({ cwe, language: lang });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const cellsTotal = cwes.length * langs.length;
|
|
43
|
+
return {
|
|
44
|
+
total: list.length,
|
|
45
|
+
target,
|
|
46
|
+
progressPct: Math.round((list.length / target) * 100),
|
|
47
|
+
remainingToTarget: Math.max(0, target - list.length),
|
|
48
|
+
byLanguage,
|
|
49
|
+
byCwe,
|
|
50
|
+
matrix,
|
|
51
|
+
cellsTotal,
|
|
52
|
+
cellsCovered: cellsTotal - gaps.length,
|
|
53
|
+
gapCount: gaps.length,
|
|
54
|
+
gaps,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function summarizeCorpusStatus(r) {
|
|
59
|
+
if (!r) return 'corpus: (no data)';
|
|
60
|
+
const lines = [
|
|
61
|
+
`corpus: ${r.total}/${r.target} entries (${r.progressPct}%, ${r.remainingToTarget} to target)`,
|
|
62
|
+
`matrix: ${r.cellsCovered}/${r.cellsTotal} CWE×language cells covered (${r.gapCount} gaps)`,
|
|
63
|
+
];
|
|
64
|
+
// Top under-covered languages (fewest entries among target languages).
|
|
65
|
+
const langCounts = Object.entries(r.byLanguage).sort((a, b) => a[1] - b[1]);
|
|
66
|
+
if (langCounts.length) {
|
|
67
|
+
lines.push('thinnest languages: ' + langCounts.slice(0, 3).map(([l, n]) => `${l}=${n}`).join(', '));
|
|
68
|
+
}
|
|
69
|
+
// A few example gaps to act on first.
|
|
70
|
+
if (r.gaps.length) {
|
|
71
|
+
lines.push('example gaps: ' + r.gaps.slice(0, 6).map(g => `${g.cwe}/${g.language}`).join(', '));
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Analysis-coverage honesty report (roadmap #5 + #6).
|
|
2
|
+
//
|
|
3
|
+
// A "perfect" SAST scanner must publish its blind spots, not hide them. This
|
|
4
|
+
// module turns three things the engine already knows internally into an
|
|
5
|
+
// explicit, user-facing coverage summary:
|
|
6
|
+
//
|
|
7
|
+
// 1. Per-language ANALYSIS TIER — which languages in this scan got the full
|
|
8
|
+
// IR + taint engine vs. only pattern (regex) detectors. The taint engine
|
|
9
|
+
// is wired for js/ts/py/java/cs/kt/go/php/rb; everything else
|
|
10
|
+
// (c/c++/rust/swift/solidity/dart/…) is pattern-only today.
|
|
11
|
+
// 2. Files SKIPPED and why (too large, too dense, timed out).
|
|
12
|
+
// 3. UNMODELED-SINK candidates (#5) — calls that match a dangerous shape but
|
|
13
|
+
// have NO finding at their line, i.e. likely recall blind spots to verify.
|
|
14
|
+
//
|
|
15
|
+
// All of this is informational; it never changes a finding's severity.
|
|
16
|
+
|
|
17
|
+
// Languages that flow through the Layer-1 IR + Layer-2 taint engine.
|
|
18
|
+
export const IR_TAINT_LANGS = new Set(['js', 'ts', 'py', 'java', 'cs', 'kt', 'go', 'php', 'rb']);
|
|
19
|
+
|
|
20
|
+
const EXT_TO_LANG = {
|
|
21
|
+
js: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'ts',
|
|
22
|
+
py: 'py', pyw: 'py', java: 'java', go: 'go', rb: 'rb', php: 'php',
|
|
23
|
+
cs: 'cs', kt: 'kt', kts: 'kt', swift: 'swift', rs: 'rs',
|
|
24
|
+
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
25
|
+
sol: 'sol', dart: 'dart', scala: 'scala', m: 'objc',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function langOfFile(file) {
|
|
29
|
+
if (typeof file !== 'string') return null;
|
|
30
|
+
const ext = file.split('.').pop().toLowerCase();
|
|
31
|
+
return EXT_TO_LANG[ext] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Given the list of scanned files, report which languages got IR-taint vs.
|
|
35
|
+
// pattern-only analysis, with a per-language file count.
|
|
36
|
+
export function computeAnalysisTiers(fileList) {
|
|
37
|
+
const counts = {};
|
|
38
|
+
for (const f of fileList || []) {
|
|
39
|
+
const l = langOfFile(f);
|
|
40
|
+
if (!l) continue;
|
|
41
|
+
counts[l] = (counts[l] || 0) + 1;
|
|
42
|
+
}
|
|
43
|
+
const irTaint = {}, patternOnly = {};
|
|
44
|
+
for (const [lang, n] of Object.entries(counts)) {
|
|
45
|
+
(IR_TAINT_LANGS.has(lang) ? irTaint : patternOnly)[lang] = n;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
irTaint, // { js: 12, py: 4 }
|
|
49
|
+
patternOnly, // { rs: 3, sol: 1 } → flow analysis NOT applied
|
|
50
|
+
languages: Object.keys(counts).sort(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Dangerous-call shapes worth surfacing when no finding lands on their line.
|
|
55
|
+
// Deliberately conservative + cross-language; this is a "did we miss a sink?"
|
|
56
|
+
// prompt for the user, emitted at info level only.
|
|
57
|
+
const DANGER_TOKENS = [
|
|
58
|
+
[/\beval\s*\(/, 'eval'],
|
|
59
|
+
[/\bnew\s+Function\s*\(/, 'Function-constructor'],
|
|
60
|
+
[/\bvm\.\s*runIn\w+\s*\(/, 'vm.runIn*'],
|
|
61
|
+
[/\bchild_process\b|\bexecSync?\s*\(|\bspawnSync?\s*\(/, 'child_process'],
|
|
62
|
+
[/\bdeserialize\s*\(|readObject\s*\(|ObjectInputStream\b/, 'deserialize'],
|
|
63
|
+
[/\byaml\.\s*load\s*\(|\bsafe_load\b/, 'yaml.load'],
|
|
64
|
+
[/\bpickle\.\s*loads?\s*\(/, 'pickle.load'],
|
|
65
|
+
[/\bMarshal\.\s*load\b|\bunserialize\s*\(/, 'unserialize'],
|
|
66
|
+
[/\bRuntime\b[\s\S]{0,40}\.exec\s*\(|\bProcessBuilder\b/, 'Runtime.exec/ProcessBuilder'],
|
|
67
|
+
[/\bos\.\s*system\s*\(|\bsubprocess\.\s*(?:call|run|Popen)\s*\(/, 'os.system/subprocess'],
|
|
68
|
+
[/\.innerHTML\s*=|\bdangerouslySetInnerHTML\b/, 'innerHTML'],
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Scan file contents for danger tokens that have NO finding on their line.
|
|
72
|
+
// findings: array with {file,line}. fc: { path: content }. Returns a capped
|
|
73
|
+
// example list + total count. Lines are 1-based to match finding lines.
|
|
74
|
+
export function countUnmodeledSinkCandidates(fc, findings, opts = {}) {
|
|
75
|
+
const cap = opts.cap || 12;
|
|
76
|
+
const covered = new Set();
|
|
77
|
+
for (const f of findings || []) {
|
|
78
|
+
if (f && f.file && typeof f.line === 'number') covered.add(`${f.file}:${f.line}`);
|
|
79
|
+
}
|
|
80
|
+
const examples = [];
|
|
81
|
+
let count = 0;
|
|
82
|
+
for (const [file, content] of Object.entries(fc || {})) {
|
|
83
|
+
if (typeof content !== 'string') continue;
|
|
84
|
+
const lines = content.split('\n');
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
for (const [re, token] of DANGER_TOKENS) {
|
|
87
|
+
if (!re.test(lines[i])) continue;
|
|
88
|
+
if (covered.has(`${file}:${i + 1}`)) continue; // already flagged → modelled
|
|
89
|
+
count++;
|
|
90
|
+
if (examples.length < cap) examples.push({ file, line: i + 1, token });
|
|
91
|
+
break; // one token per line is enough
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { count, examples };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// One-line CLI summary of an analysis-coverage object.
|
|
99
|
+
export function summarizeCoverage(meta) {
|
|
100
|
+
if (!meta) return 'coverage: (none)';
|
|
101
|
+
const t = meta.analysisTier || {};
|
|
102
|
+
const irLangs = Object.keys(t.irTaint || {});
|
|
103
|
+
const patLangs = Object.keys(t.patternOnly || {});
|
|
104
|
+
const parts = [
|
|
105
|
+
`scanned=${meta.filesScanned ?? '?'}`,
|
|
106
|
+
`skipped=${(meta.filesSkipped || 0) + (meta.filesDenseSkipped || 0)}`,
|
|
107
|
+
`timedOut=${meta.filesTimedOut || 0}`,
|
|
108
|
+
`ir-taint=[${irLangs.join(',')}]`,
|
|
109
|
+
];
|
|
110
|
+
if (patLangs.length) parts.push(`pattern-only=[${patLangs.join(',')}]`);
|
|
111
|
+
if (meta.unmodeledSinkCandidates?.count) parts.push(`unmodeled-sink-candidates=${meta.unmodeledSinkCandidates.count}`);
|
|
112
|
+
return parts.join(' · ');
|
|
113
|
+
}
|