@sikkuumiii/defuse 0.0.0-stage → 0.5.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.
Files changed (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
package/README.md CHANGED
@@ -1,3 +1,562 @@
1
- # Temporary Holding Version
1
+ # Defuse
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Static analysis that tells you which findings it proved and which it only
4
+ guessed.**
5
+
6
+ [![licence: AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-792cf7?style=flat-square)](LICENSE)
7
+ [![no build step](https://img.shields.io/badge/install-npx,%20no%20toolchain-16c97c?style=flat-square)](#quick-start)
8
+ [![runtime dependencies: 2](https://img.shields.io/badge/runtime%20deps-2-6f6f6f?style=flat-square)](#quick-start)
9
+
10
+ ```bash
11
+ npx @sikkuumiii/defuse scan .
12
+ ```
13
+
14
+ <!--
15
+ DELIBERATELY FEW BADGES. A badge is a hand-kept copy of a fact the program
16
+ already knows, which is the exact failure `sync-readme.mjs` exists to stop.
17
+ Only figures that do not drift get one; the counts and the scores below are
18
+ generated from the registry and from the benchmark result, so they cannot go
19
+ stale without failing the build.
20
+ -->
21
+
22
+ Every finding is labelled **`signature-based`** or **`flow-verified`**, and the
23
+ two mean precise, different things:
24
+
25
+ | label | claim | evidence |
26
+ |---|---|---|
27
+ | `signature-based` | "this code has the *shape* of a bug" | a pattern matched in the syntax tree |
28
+ | `flow-verified` | "attacker data actually *reaches* here" | the full source→sink path, printed hop by hop |
29
+
30
+ A `flow-verified` finding prints its own receipt: every line the value passed
31
+ through, so you can check the claim instead of trusting it — including the hops
32
+ where our knowledge runs out, which are labelled as such rather than quietly
33
+ smoothed over.
34
+
35
+ The tool also prints, on every run, a list of what it did **not** check. That is
36
+ the product. The rules are table stakes; the labelling is the difference.
37
+
38
+ ### What the distinction is actually worth
39
+
40
+ Measured, not asserted — and measured separately for each label, because one
41
+ blended precision figure would be the same averaging this tool refuses to do
42
+ inside a single finding.
43
+
44
+ <!-- derived:label-split -->
45
+ | tier | TP | FP | FN | precision | recall | FPs that are decoys |
46
+ |---|--:|--:|--:|--:|--:|--:|
47
+ | `flow-verified only` | 602 | 0 | 42 | **100.0%** | 93.5% | 0 (0%) |
48
+ | `signature-only` | 42 | 35 | 602 | **54.5%** | 6.5% | 0 (0%) |
49
+ | `combined (published)` | 644 | 35 | 0 | **94.8%** | 100.0% | 0 (0%) |
50
+
51
+ 658 `flow-verified`, 1198 `signature-based` findings, on engine 0.5.0 (2026-10-28).
52
+
53
+ **The green label is worth 45.5 points.** `flow-verified` runs 100.0% against `signature-based` at 54.5%. The gap is the whole claim this tool makes; it is measured here rather than asserted.
54
+
55
+ **No false positive carries the green label here.** All 602 `flow-verified` claims on this benchmark are real vulnerabilities. That is a statement about these scored test cases, not a promise about your code: a proof means the path exists in the source, and the ways that can still be wrong are the ones in the limitations list, which a synthetic benchmark does not exercise.
56
+ <!-- /derived -->
57
+
58
+ <!-- derived:counts -->
59
+ **11 rules** across **8 languages** (JavaScript, TypeScript, Python, Java, PHP, Go, C, C++).
60
+ <!-- /derived -->
61
+
62
+ ---
63
+
64
+ ## Quick start
65
+
66
+ **Run it without installing anything:**
67
+
68
+ ```bash
69
+ npx @sikkuumiii/defuse scan .
70
+ ```
71
+
72
+ > Published on npm as `@sikkuumiii/defuse`, because the plain name `defuse` is
73
+ > already taken by an unrelated package. Stated plainly rather than printing an
74
+ > `npx defuse` line that would not resolve: a README whose very first command
75
+ > fails is a worse first impression than one extra clause. Once installed, the
76
+ > command is still `defuse`.
77
+
78
+ That is the whole setup. The WASM grammars ship inside the package and are
79
+ resolved from `node_modules`, so there is no toolchain to install, no compiler,
80
+ and nothing to configure — two runtime dependencies and a Node 18+ runtime.
81
+
82
+ Two install checks prove the published package reports exactly what a local build reports - see [docs/TESTING.md](docs/TESTING.md#install-checks).
83
+
84
+ **Working on the engine itself:**
85
+
86
+ ```bash
87
+ npm install
88
+ npm run build
89
+
90
+ node dist/src/cli.js doctor # self-check: grammars + queries
91
+ node dist/src/cli.js scan tests/fixtures # scan the sample vulnerable code
92
+ npm test # verify every rule fires correctly
93
+
94
+ npm run ui # the browser UI on :4173
95
+ npm run install-check # is the published package the same program?
96
+ npm run git-install-check # does a clone of this repo build and behave?
97
+ npm run memory # what does a scan cost per byte of source?
98
+ npm run identity # did a refactor move any answer?
99
+ npm run review-reasons -- scan.json . # why is each "to review" finding still open? (a sorting aid)
100
+ npm run scoreboard # every practice app with an answer key: mistakes grouped by cause
101
+ ```
102
+
103
+ `npm run identity` hashes the findings of twelve fixed targets, so a refactor that is meant to change nothing can prove it - see [docs/TESTING.md](docs/TESTING.md#identity-check).
104
+
105
+ > **Windows / PowerShell note.** In this README and in `--help`, `<path>` and
106
+ > `<file>` are *placeholders* — replace them with a real path. Do not type the
107
+ > angle brackets: PowerShell treats `<` as a reserved redirection operator and
108
+ > will refuse with *"The '<' operator is reserved for future use."* Also paste
109
+ > one command per line, without the trailing `# comment`.
110
+ >
111
+ > ```powershell
112
+ > node dist/src/cli.js scan .\tests\fixtures
113
+ > node dist/src/cli.js ast .\tests\fixtures\vulnerable\sqli.js
114
+ > ```
115
+
116
+ Installed globally (`npm i -g @sikkuumiii/defuse`), the command is `defuse`:
117
+
118
+ ```bash
119
+ defuse scan ./src
120
+ defuse scan ./src --json > findings.json
121
+ defuse scan ./src --sarif > results.sarif # for GitHub code scanning
122
+ defuse ast ./src/app.js # see the syntax tree
123
+ defuse rules --explain sql-injection # read the write-up for a rule
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Browser UI
129
+
130
+ `npm run ui` serves a browser shell on :4173. Drop a folder on it and the same
131
+ compiled engine the CLI uses runs in a Web Worker, so nothing leaves your
132
+ machine. It shows severity, confidence and coverage as three gauges that are
133
+ never merged into one score, and a three-pane deck (files, code, findings)
134
+ with every hop of a flow linked. The full tour: [docs/UI.md](docs/UI.md).
135
+
136
+ ---
137
+
138
+ ## What it does, in order
139
+
140
+ ```
141
+ your files
142
+ │
143
+ ▼
144
+ ┌───────────────┐ file extension or #! line decides the language
145
+ │ parse/ │ tree-sitter turns text into a syntax tree (AST)
146
+ └───────────────┘
147
+ │
148
+ ├──────────────────────────────┐
149
+ ▼ ▼
150
+ ┌───────────────┐ ┌────────────────┐
151
+ │ engine/ │ shapes │ taint/ │ follows values
152
+ │ shapes.ts │ CallSite │ tracer.ts │ source → sink,
153
+ │ rules/*.ts │ Assignment│ project.ts │ across functions
154
+ └───────────────┘ └────────────────┘ AND across files
155
+ │ │
156
+ ▼ ▼
157
+ signature-based flow-verified
158
+ (a pattern) (a proof, with the path)
159
+ │ │
160
+ └──────────────┬───────────────┘
161
+ ▼
162
+ the verified one SUPERSEDES the guess
163
+ at the same line; everything unconfirmed
164
+ stays signature-based and stays visible
165
+ │
166
+ ▼
167
+ terminal report or --json
168
+ ```
169
+
170
+ Why a syntax tree instead of regular expressions, and why one shape layer lets each rule work in every language: [docs/ENGINE.md](docs/ENGINE.md).
171
+
172
+ ---
173
+
174
+ ## The honesty contract
175
+
176
+ `src/core/finding.ts` is the file to read first. Three mechanisms enforce the
177
+ project's central claim:
178
+
179
+ **1. The type system makes overclaiming impossible.**
180
+ `Finding` is a union of two shapes, and `FlowVerifiedFinding` *requires* a
181
+ `flowPath`. Before the tracer existed no code could build one, so the label was
182
+ unreachable by construction rather than by discipline.
183
+
184
+ The tracer activated that second constructor without softening the requirement.
185
+ `flowVerifiedFinding()` throws unless the path has at least two steps, **begins
186
+ at a source**, **ends at a sink**, and contains **no sanitiser**. The type system
187
+ stops a rule from lying; that function stops the *engine* from lying, including
188
+ a future version of it written in a hurry.
189
+
190
+ **2. Rules cannot set their own confidence.**
191
+ A rule returns a `RuleHit` — a node, a message, a reason. The engine converts it
192
+ to a `Finding`. The confidence field is not in the rule's vocabulary.
193
+
194
+ **3. Every finding must state its limitations.**
195
+ `reasoning` and `limitations` are required fields, not optional ones.
196
+ `validateRegistry()` runs before every scan and refuses to start if a rule ships
197
+ with an empty limitations string or declares partial language support without
198
+ saying what is missing. The scan aborts rather than emit a finding that cannot
199
+ explain itself.
200
+
201
+ Every report ends with **"What this scan did NOT check"** — engine limits,
202
+ partial rules, rule/language combinations that do not exist, and file types
203
+ present in the tree that were never opened.
204
+
205
+ ---
206
+
207
+ ## The data-flow engine
208
+
209
+ Four words carry the design: **source**, **propagation**, **sanitiser**, **sink**. Sanitisers are kind-scoped (an HTML escaper does nothing for SQL), values are followed into other functions and other files through a resolved import graph, prepared statements are recognised as the fix, and a call the engine does not model makes a trace a possibility, never a proof. The full walk-through, with examples, is in [docs/ENGINE.md](docs/ENGINE.md).
210
+
211
+ ## Signature rules
212
+
213
+ <!-- derived:rule-matrix -->
214
+ | Rule | CWE | OWASP | JS | TS | PY | JAVA | PHP | GO | C | CPP |
215
+ |---|---|---|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
216
+ | `sql-injection` | CWE-89 | A05:2025 Injection | ● | ● | ● | ● | ● | ● | ● | ● |
217
+ | `command-injection` | CWE-78 | A05:2025 Injection | ● | ● | ● | ◐ | ● | ◐ | ● | ● |
218
+ | `format-string` | CWE-134 | A05:2025 Injection | ○ | ○ | ○ | ○ | ○ | ○ | ● | ● |
219
+ | `unbounded-copy` | CWE-120 | A05:2025 Injection | ○ | ○ | ○ | ○ | ○ | ○ | ◐ | ◐ |
220
+ | `code-injection` | CWE-94 | A05:2025 Injection | ● | ● | ● | ◐ | ● | ○ | ○ | ○ |
221
+ | `xss` | CWE-79 | A05:2025 Injection | ● | ● | ◐ | ◐ | ◐ | ◐ | ○ | ○ |
222
+ | `ssrf` | CWE-918 | A01:2025 Broken Access Control | ● | ● | ● | ◐ | ● | ◐ | ○ | ○ |
223
+ | `path-traversal` | CWE-22 | A01:2025 Broken Access Control | ● | ● | ● | ● | ● | ● | ● | ◐ |
224
+ | `unsafe-deserialization` | CWE-502 | A08:2025 Software or Data Integrity Failures | ◐ | ◐ | ● | ◐ | ● | ○ | ○ | ○ |
225
+ | `hardcoded-secret` | CWE-798 | A07:2025 Authentication Failures | ● | ● | ● | ● | ● | ● | ● | ● |
226
+ | `weak-hash` | CWE-327 | A04:2025 Cryptographic Failures | ● | ● | ● | ● | ● | ◐ | ● | ● |
227
+
228
+ ● implemented ◐ partial ○ not implemented
229
+
230
+ 11 rules across 8 languages. This table is generated from
231
+ `ALL_RULES` and `LANGUAGES` by `npm run sync:readme`, and `npm test` fails if it
232
+ drifts - the counts in it are not maintained by hand.
233
+ <!-- /derived -->
234
+
235
+ `defuse rules --explain <id>` prints the full write-up for any rule,
236
+ including exactly what each ◐ is missing. Those notes are also printed at the
237
+ end of every scan — they are not buried in documentation.
238
+
239
+ Defuse also checks the PHP version a project declares (Dockerfile, compose, composer.json, `.php-version`) against dated php.net facts - see [docs/ENGINE.md](docs/ENGINE.md#the-php-version-check-runtime-version).
240
+
241
+ ---
242
+
243
+ ## Output
244
+
245
+ **Terminal** (default): file, line:column, severity in colour, the confidence
246
+ tag, the matched source, the AST node path, a `why` paragraph and a `gap`
247
+ paragraph, followed by the confidence summary and the coverage ledger.
248
+
249
+ **`--json`**: a superset of the terminal report. The `honesty` block sits
250
+ *above* `findings` in the document so a consumer reading top to bottom meets the
251
+ caveats first. A dashboard cannot present these as confirmed vulnerabilities
252
+ without deliberately discarding the `confidence` field.
253
+
254
+ Useful flags:
255
+
256
+ ```
257
+ --min-severity=high hide low-severity noise in the terminal (kept in --json)
258
+ --fail-on=critical CI exit code control; --fail-on=none always exits 0
259
+ --only=sql-injection run a single rule
260
+ --exclude=tests,vendor skip paths. A bare word matches any directory of that
261
+ name; * and ** work as usual (--exclude=**/*.min.js)
262
+ --no-cross-file analyse each file alone: faster, far less memory, and
263
+ no finding that crosses a module boundary
264
+ --full the long terminal report: reasoning, limits, OWASP table
265
+ --coverage-matrix print the full rule × language table
266
+ --show-suppressed list findings silenced by defuse:ignore comments
267
+ --sarif SARIF 2.1.0, for GitHub code scanning and any SARIF viewer
268
+ --report=report.html also write a client-readable HTML report (prints to PDF)
269
+ --compact one line per finding
270
+ --no-config ignore .defuse.json even if one is present
271
+ ```
272
+
273
+ **`--report=report.html`**: a report for the person who owns the code rather
274
+ than the person who ran the scan - one HTML file that loads nothing from the web,
275
+ runs no script, and prints to PDF from any browser. In order: what was **proven**,
276
+ each with the path Defuse followed; what is **to review**, grouped by check, with
277
+ what that check cannot tell; what was **checked and cleared**, with the reason;
278
+ and what was **not checked** at all, including that dependency folders are not
279
+ scanned and library versions are not checked. `--report-name` and `--report-by`
280
+ fill in the project name and "Prepared by", and `--report-note` adds one line under
281
+ the header (for example that the report is a draft). The browser UI has the same report
282
+ behind **Download report**, made by the same renderer from the same analysis.
283
+
284
+ ```bash
285
+ npx @sikkuumiii/defuse scan ./app --report=app-report.html --report-by="Your Name"
286
+ ```
287
+
288
+ Two things are deliberate. Every piece of text taken from the scanned code is
289
+ escaped, and a test feeds the renderer a snippet built to break out of the page:
290
+ a security report that ran the code it quoted would be an attack on its reader.
291
+ And the value of a reported hardcoded secret is cut to its first few characters
292
+ and its length, because this file is made to be sent to someone - the full value
293
+ stays in the code and in the scan's own output.
294
+
295
+ **`--sarif`**: real SARIF 2.1.0, typed against `@types/sarif`. The confidence
296
+ label is carried three ways so it cannot be lost in transit — a `[flow-verified]`
297
+ or `[signature-based, UNVERIFIED]` message prefix, a `properties.confidence`
298
+ field, and a `codeFlows` entry that exists **only** for verified findings. A
299
+ test fails if a `codeFlow` ever appears on an unverified one. See
300
+ `docs/examples/github-code-scanning.yml` for the workflow.
301
+
302
+ **`.defuse.json`**: project config for `exclude`, `only`, `failOn` and
303
+ `minSeverity`. Command-line flags win over the file, broken JSON exits 2 rather
304
+ than being ignored, and **a rule cannot be disabled by config** — by design, so
305
+ a repository cannot quietly switch off the check it fails. See
306
+ `docs/examples/defuse.json`.
307
+
308
+ `--exclude` never hides quietly. The report prints how many files and whole
309
+ directories it removed, because a blind spot you asked for is still a blind
310
+ spot:
311
+
312
+ ```
313
+ --exclude (tests, vendor) skipped 118 file(s) and 2 whole director(ies):
314
+ tests, vendor - not scanned, not counted
315
+ ```
316
+
317
+ Suppression is a comment: `// defuse:ignore sql-injection - table name is a
318
+ constant`. Suppressed findings are **counted and listed**, never erased.
319
+
320
+ ---
321
+
322
+ ## Testing
323
+
324
+ Three suites, because they fail at different things.
325
+
326
+ **`npm test` — 1212 checks, annotation-driven.** Fixtures carry their own
327
+ expectations as comments, so there is no second list to keep in sync. Catches
328
+ regressions and honesty-contract violations. Its ceiling is that it only ever
329
+ checks what somebody already thought of.
330
+
331
+ `npm run metamorphic` generates programs whose answer is known by construction, and hand-written adversarial fixtures cover the design flaws neither suite finds. All three, and how to read the output: [docs/TESTING.md](docs/TESTING.md#test-suites).
332
+
333
+ ---
334
+
335
+ ## Measured on real code
336
+
337
+ Scans of five real repositories and three deliberately vulnerable apps, and the full history of what moved the score, are in [docs/MEASUREMENTS.md](docs/MEASUREMENTS.md).
338
+
339
+ ### Measured against OWASP Benchmark (ground truth)
340
+
341
+ BenchmarkJava is 2,740 Java test cases, each labelled by OWASP as a real
342
+ vulnerability or a deliberate decoy. It is the closest thing this field has to
343
+ an exam with an answer key, so here is the exam result, unedited:
344
+
345
+ <!-- derived:benchmark -->
346
+ | | cases | TP | FP | FN | TN | precision | recall |
347
+ |---|--:|--:|--:|--:|--:|--:|--:|
348
+ | **overall** | **1210** | **644** | **35** | **0** | **531** | **94.8%** | **100.0%** |
349
+
350
+ 2766 files, 0 parse errors, 18.1 seconds, on engine 0.5.0 (2026-10-28).
351
+
352
+ **Precision is the weak side.** 35 false positives and no false negatives, so on this benchmark the cost of this engine is triage time, not missed bugs. That is a statement about these scored test cases, not about code in general - the limitations list is where the misses this benchmark cannot show are written down.
353
+
354
+ Nothing was missed in the scored categories. Of the 35 false positives, **0 (0%)** carry the shape of BenchmarkJava's constant-branch decoy - a condition like `if ((7 * 42) - num > 200)`. The data-flow pass, the SQL rule and the command-injection rule all decide such a condition, so the shape alone explains none of them: every false positive here is ours.
355
+
356
+ Every number above is generated by `npm run benchmark` into
357
+ `docs/benchmark-result.json` - including the sentence naming the weaker side,
358
+ because an interpretation kept by hand goes stale exactly like a figure does.
359
+ <!-- /derived -->
360
+
361
+ **That table is a training score.** Every BenchmarkJava miss and false positive
362
+ above was fixed by looking at BenchmarkJava, so 100% recall says the engine
363
+ learned that exam, not that it generalises. The honest number is one it was
364
+ not tuned on. **OWASP BenchmarkPython** (383 scored cases in the six
365
+ categories with a rule) was that set until 2026-11-08:
366
+
367
+ | BenchmarkPython, first run (held out) | precision | recall | proofs that were wrong |
368
+ |---|--:|--:|--:|
369
+ | before any fix | 69.9% | 76.6% | **10** |
370
+ | after fixing what it showed | 75.0% | 85.4% | **0** |
371
+ | second pass | 78.0% | 85.4% | **0** |
372
+
373
+ The 10 wrong proofs had three causes: an `eval` fenced to a single quoted
374
+ literal, a Python list read after `pop`, and `exec()` in a function blocking the
375
+ constant folder. XSS went from 0 of 31 to 14 when a str returned from a Flask
376
+ route became a sink. Having been fixed against, BenchmarkPython is now training
377
+ data too - `npm run scoreboard` runs it, and the next honest number needs the
378
+ next set nobody has looked at. Still missed there: XSS via a value stored in a
379
+ local dict and formatted out (`'{0[bar]}'.format(d)`), and request wrappers.
380
+
381
+ The second pass found what the answer key cannot: the Flask sink had made
382
+ about 60 **wrong proofs of XSS in files scored for other categories**, every one
383
+ through the benchmark's own `escape_for_html()`, which the engine does not
384
+ model and so assumed passes the value through. A hop whose own name says it
385
+ escapes, encodes or sanitises now makes a trace a possibility, not a proof
386
+ (an HTML escaper counts only against XSS; `b64decode(b64encode(x))` cleans
387
+ nothing). The 11 XSS proofs left there were read by hand and are real. Code
388
+ injection also gained the constant proof SQL had: 7 decoy `eval`s withdrawn,
389
+ each with a receipt. BenchmarkJava, DVWA and Juice Shop were re-run after both
390
+ changes: 0 wrong proofs, and BenchmarkJava unchanged at 94.8% / 100%.
391
+
392
+ **What the misses were.** All 38 were one shape - a helper calling a
393
+ same-named method on another object, read as recursion and cut short. That is
394
+ fixed, and recall is 100% on the scored cases; misses this benchmark cannot
395
+ show are in the limitations list above.
396
+
397
+ **What the decoys are.** The share named in the block above is BenchmarkJava's
398
+ deliberate trap, which is always the same shape:
399
+
400
+ ```java
401
+ int num = 86;
402
+ if ((7 * 42) - num > 200) bar = "This_should_always_happen";
403
+ else bar = param;
404
+ ```
405
+
406
+ `294 - 86 = 208`, so the constant branch always wins and `bar` is never tainted.
407
+ Seeing that needs constant folding plus branch feasibility. The data-flow pass
408
+ has both, and so do the SQL and command-injection rules. Until this release the
409
+ block above counted four false positives with the shape the other way round -
410
+ `(500 / 42) + num > 200`, where the branch that runs is the one that assigns the
411
+ "input". That input was a held object's fixed value, and those four went with
412
+ the 21 described below.
413
+
414
+ That share used to be stated here as "most". It was **37%** when finally
415
+ measured, and it had been written when the false-positive count was 49 rather
416
+ than 345 — an empirical claim that outlived the thing that changed its subject.
417
+ It is generated now, from the scorer reading every false-positive file.
418
+
419
+ These numbers are *measured*, which is the point: a scanner that will not publish
420
+ its own exam result is asking you to take its word for something it has not
421
+ checked either.
422
+
423
+ ---
424
+
425
+ ## What the engine does NOT do
426
+
427
+ Stated here, in the code, and in every scan report:
428
+
429
+ - **Only files inside the scan.** A call into `node_modules`, a JAR or a
430
+ vendored package ends the trace. Call chains deeper than four are not
431
+ followed: the value is carried past the cut and the finding says so.
432
+ - **Calls are resolved by name.** Java, Go, C and C++ also check the number of
433
+ arguments, and Java checks the receiver's declared type. Elsewhere, two
434
+ same-named functions can be confused.
435
+ - **Ambiguous names are not guessed.** When several files in scope define the
436
+ same function name, the trace stops and the decline is counted.
437
+ - **Re-exports, dynamic imports and build-tool path aliases** (tsconfig `paths`,
438
+ webpack) are not resolved, and whether a function is really *exported* is not
439
+ checked.
440
+ - **Object interiors are invisible.** `obj.field = dirty` and
441
+ `sb.append(dirty)` are not tracked — we follow variables, not object state.
442
+ - **Sources are recognised by shape.** A `req.query`-looking expression is
443
+ assumed to be a real HTTP request; we do not prove it with type inference.
444
+ - **Branch conditions are not evaluated.** A value cleaned only inside
445
+ `if (isAdmin)` is treated as cleaned afterwards — and, in the other
446
+ direction, `if (alwaysTrue) x = "safe"; else x = dirty;` is treated as dirty.
447
+ - **Loops are not iterated to a fixpoint.**
448
+ - **Unmodelled functions are assumed to preserve taint**, and named on the
449
+ finding when they do.
450
+ - **No framework awareness.** Express routing and Django views are just code.
451
+ - **No template files.** `.html`, `.jinja`, `.jsp` are never parsed, so
452
+ `{{ x|safe }}` inside a template is invisible.
453
+ - **No taint through a database or a queue.** Stored (second-order) XSS, where
454
+ a value is written on one request and printed on a later one, is not followed.
455
+
456
+ What a scan costs in memory, per byte of source: [docs/MEASUREMENTS.md](docs/MEASUREMENTS.md#how-much-memory-a-scan-costs).
457
+
458
+ ---
459
+
460
+ ## Roadmap
461
+
462
+ This used to be a table of phase numbers. They described the order *we* built
463
+ things, which stopped matching anything once the work happened out of order —
464
+ the banner read `PHASE 3C` while the version read `phase2`. A tool that sells
465
+ honest labelling cannot mislabel itself, so the roadmap now lists capabilities,
466
+ and `npm test` fails if a phase number reappears anywhere a user can read it.
467
+
468
+ **Built and verified**
469
+
470
+ | Capability | Evidence |
471
+ |---|---|
472
+ | Tree-sitter parsing, shape layer — see the coverage matrix above for rules × languages | 99 fixtures, zero parse errors |
473
+ | SARIF 2.1.0 export (`--sarif`) carrying the confidence label three ways | a test fails if a `codeFlow` appears on an unverified finding |
474
+ | `.defuse.json` config, `--exclude` / `--only` / `--fail-on` / `--min-severity` | a rule cannot be disabled by config, by design |
475
+ | Honesty labelling enforced by the type system | `flowVerifiedFinding()` throws on an incomplete path |
476
+ | Taint tracing within a function, kind-scoped sanitisers | verified flows asserted per-line by `EXPECT-FLOW` annotations |
477
+ | Call graph within a file, unmodelled hops named on the finding | measured against OWASP Benchmark |
478
+ | Cross-file tracing through a resolved import graph | 4 findings with paths spanning 2+ files |
479
+ | Framework bindings as sources: Spring, JAX-RS, NestJS parameter decorators, Flask/FastAPI route path parameters | `SpringController.java`, `JaxrsResource.java`, `nest-decorated-params.ts`, `route-path-params.py` — each with a fence that must stay unproven |
480
+ | Prepared-statement awareness, `--exclude` | govwa: 4 findings → 1, three were correct code |
481
+ | Browser UI running the same compiled engine in a Web Worker | CLI and browser produce identical findings, checked by a test |
482
+ | Analysis deck: file tree, code with four gutter states, linked flow hops | `verifiedClean` and `fileRoles` back every mark |
483
+ | Three gauges that are never merged, each showing its arithmetic | a test fails if a combined risk score is ever exported |
484
+ | Blind spots counted per scan, not disclaimed in general | `traceLimits` from the tracer's own truncation points |
485
+ | PHP version check: Dockerfile, compose, composer.json, `.php-version` against dated php.net facts | `php-runtime.dockerfile`, and 27 checks that pin each decision with the scan date fixed |
486
+ | `defuse deps`: pinned library versions in package-lock.json, composer.lock, requirements.txt and go.mod against osv.dev. Opt-in, the one command that uses the network, and it sends names and versions only | 10 offline checks, including one that fails if a malformed OSV answer reads as "no advisories" |
487
+
488
+ **Not built**
489
+
490
+ | Next | Why it matters |
491
+ |---|---|
492
+ | Remediation: context-aware fixes and before/after diffs | a finding you can act on beats a finding you can read |
493
+ | The same version check for Node and Python; yarn, pnpm, Poetry, Maven and Cargo lockfiles in `defuse deps` | the language runtime is checked for PHP only, and those lockfiles are listed as not read |
494
+ | Language expansion — 31 more grammars already ship in `tree-sitter-wasms` | the parser layer already supports them; the dictionaries do not |
495
+ | Framework bindings still unread: Django URLconf kwargs, Spring `@ModelAttribute`, Pydantic/DTO object fields | each binds through a second file or an object’s fields, which the tracer does not follow |
496
+
497
+ ---
498
+
499
+ ## Project layout
500
+
501
+ ```
502
+ src/
503
+ core/finding.ts the honesty contract — read this first
504
+ core/coverage.ts the "what we did not check" ledger
505
+ parse/languages.ts language registry (add a language here)
506
+ parse/parser.ts tree-sitter setup + what an AST is
507
+ parse/astDump.ts the human-readable tree printer
508
+ engine/shapes.ts tree-sitter queries → CallSite / Assignment
509
+ taint/types.ts source / propagation / sanitiser / sink, explained
510
+ taint/dictionaries.ts pure data: what is a source in Express, Flask, ...
511
+ taint/tracer.ts follows values through one function, builds the path
512
+ engine/scan.ts the pipeline, top to bottom
513
+ engine/walk.ts file discovery
514
+ rules/contract.ts the interface every rule implements
515
+ rules/lib/strings.ts string-building analysis shared by all injection rules
516
+ rules/*.ts one file per vulnerability, each opening with an explanation
517
+ report/human.ts terminal output
518
+ report/json.ts machine-readable output
519
+ cli.ts argument parsing and commands
520
+ tests/
521
+ fixtures/vulnerable/ deliberately broken code, one file per rule per language
522
+ fixtures/safe/ the correct version of each — must produce zero findings
523
+ run-tests.ts the annotation-driven runner
524
+ docs/GLOSSARY.md every security term used in this repo, in plain language
525
+ ```
526
+
527
+ Two runtime dependencies: `web-tree-sitter` and `tree-sitter-wasms`. No CLI
528
+ framework, no colour library — both are short enough to read in full
529
+ (`src/cli.ts`, `src/report/colors.ts`).
530
+
531
+ ---
532
+
533
+ ## Licence
534
+
535
+ **AGPL-3.0-only.** The full text is in [LICENSE](LICENSE).
536
+
537
+ In plain language, because a licence nobody understands protects nobody:
538
+
539
+ - **Scanning your own code puts no obligation on you at all.** Running this tool
540
+ over a closed-source, proprietary or commercial repository is the intended use
541
+ and changes nothing about the code being scanned. The licence covers *this
542
+ program*, not what you point it at.
543
+ - **Running it unmodified inside your company is free and unencumbered.**
544
+ - The obligation attaches in one case: if you **modify** it and let other people
545
+ use your modified version **over a network**, section 13 says you must offer
546
+ those users its source.
547
+
548
+ That last clause is the reason it is AGPL rather than GPL. `npm run ui` serves a
549
+ browser shell that runs the same compiled engine; under a permissive licence
550
+ somebody could host exactly that, change it, and never publish a line.
551
+
552
+ If those terms do not work for you — and for some companies they genuinely do
553
+ not — the copyright is held in one place and a commercial licence can be
554
+ discussed.
555
+
556
+ The licence is declared once, in `src/core/licence.ts`, and `npm test` fails if
557
+ this file, `package.json` or `LICENSE` disagrees with it. Four copies of a legal
558
+ fact is four chances to tell somebody something untrue, and this repository has
559
+ a documented history of exactly that: the README claimed 112 checks while 207
560
+ ran, and quoted a 13.8% recall figure for about twenty versions after it stopped
561
+ being true. A wrong check count embarrasses the author; a wrong licence misleads
562
+ whoever relied on it.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * THE COMMAND-LINE INTERFACE
4
+ *
5
+ * Hand-rolled argument parsing, no library. It is about 60 lines, you can read
6
+ * all of them, and it means the tool has exactly two runtime dependencies -
7
+ * both of them the parser.
8
+ *
9
+ * Commands:
10
+ * defuse scan <path> scan a file or directory
11
+ * defuse ast <file> print the syntax tree (learning / debugging)
12
+ * defuse rules list the rules and what they do NOT cover
13
+ * defuse doctor check every grammar loads and every query compiles
14
+ *
15
+ * EXIT CODES matter for CI:
16
+ * 0 nothing at or above the --fail-on threshold
17
+ * 1 findings at or above the threshold
18
+ * 2 the tool itself failed (bad arguments, unreadable path, internal error)
19
+ */
20
+ export {};