@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.
- package/LICENSE +235 -0
- package/README.md +561 -2
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +619 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/analyze.d.ts +178 -0
- package/dist/src/core/analyze.js +834 -0
- package/dist/src/core/analyze.js.map +1 -0
- package/dist/src/core/coverage.d.ts +87 -0
- package/dist/src/core/coverage.js +181 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/finding.d.ts +196 -0
- package/dist/src/core/finding.js +178 -0
- package/dist/src/core/finding.js.map +1 -0
- package/dist/src/core/licence.d.ts +50 -0
- package/dist/src/core/licence.js +56 -0
- package/dist/src/core/licence.js.map +1 -0
- package/dist/src/core/paths.d.ts +21 -0
- package/dist/src/core/paths.js +58 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/score.d.ts +137 -0
- package/dist/src/core/score.js +232 -0
- package/dist/src/core/score.js.map +1 -0
- package/dist/src/dast/targets.d.ts +30 -0
- package/dist/src/dast/targets.js +79 -0
- package/dist/src/dast/targets.js.map +1 -0
- package/dist/src/dast/zap.d.ts +42 -0
- package/dist/src/dast/zap.js +184 -0
- package/dist/src/dast/zap.js.map +1 -0
- package/dist/src/engine/scan.d.ts +80 -0
- package/dist/src/engine/scan.js +129 -0
- package/dist/src/engine/scan.js.map +1 -0
- package/dist/src/engine/shapes.d.ts +47 -0
- package/dist/src/engine/shapes.js +328 -0
- package/dist/src/engine/shapes.js.map +1 -0
- package/dist/src/engine/walk.d.ts +60 -0
- package/dist/src/engine/walk.js +156 -0
- package/dist/src/engine/walk.js.map +1 -0
- package/dist/src/parse/astDump.d.ts +31 -0
- package/dist/src/parse/astDump.js +66 -0
- package/dist/src/parse/astDump.js.map +1 -0
- package/dist/src/parse/languages.d.ts +70 -0
- package/dist/src/parse/languages.js +174 -0
- package/dist/src/parse/languages.js.map +1 -0
- package/dist/src/parse/location.d.ts +17 -0
- package/dist/src/parse/location.js +38 -0
- package/dist/src/parse/location.js.map +1 -0
- package/dist/src/parse/node-grammars.d.ts +12 -0
- package/dist/src/parse/node-grammars.js +35 -0
- package/dist/src/parse/node-grammars.js.map +1 -0
- package/dist/src/parse/parser.d.ts +126 -0
- package/dist/src/parse/parser.js +202 -0
- package/dist/src/parse/parser.js.map +1 -0
- package/dist/src/parse/tree-store.d.ts +139 -0
- package/dist/src/parse/tree-store.js +236 -0
- package/dist/src/parse/tree-store.js.map +1 -0
- package/dist/src/platform/dependency-rule.d.ts +27 -0
- package/dist/src/platform/dependency-rule.js +21 -0
- package/dist/src/platform/dependency-rule.js.map +1 -0
- package/dist/src/platform/deps.d.ts +86 -0
- package/dist/src/platform/deps.js +304 -0
- package/dist/src/platform/deps.js.map +1 -0
- package/dist/src/platform/php-facts.d.ts +49 -0
- package/dist/src/platform/php-facts.js +67 -0
- package/dist/src/platform/php-facts.js.map +1 -0
- package/dist/src/platform/runtime-version.d.ts +78 -0
- package/dist/src/platform/runtime-version.js +573 -0
- package/dist/src/platform/runtime-version.js.map +1 -0
- package/dist/src/report/colors.d.ts +50 -0
- package/dist/src/report/colors.js +97 -0
- package/dist/src/report/colors.js.map +1 -0
- package/dist/src/report/coverage-table.d.ts +90 -0
- package/dist/src/report/coverage-table.js +291 -0
- package/dist/src/report/coverage-table.js.map +1 -0
- package/dist/src/report/html-report.d.ts +66 -0
- package/dist/src/report/html-report.js +489 -0
- package/dist/src/report/html-report.js.map +1 -0
- package/dist/src/report/human.d.ts +44 -0
- package/dist/src/report/human.js +575 -0
- package/dist/src/report/human.js.map +1 -0
- package/dist/src/report/json.d.ts +24 -0
- package/dist/src/report/json.js +178 -0
- package/dist/src/report/json.js.map +1 -0
- package/dist/src/report/mark.d.ts +17 -0
- package/dist/src/report/mark.js +39 -0
- package/dist/src/report/mark.js.map +1 -0
- package/dist/src/report/pricing.d.ts +33 -0
- package/dist/src/report/pricing.js +343 -0
- package/dist/src/report/pricing.js.map +1 -0
- package/dist/src/report/sarif.d.ts +39 -0
- package/dist/src/report/sarif.js +219 -0
- package/dist/src/report/sarif.js.map +1 -0
- package/dist/src/report/site.d.ts +87 -0
- package/dist/src/report/site.js +742 -0
- package/dist/src/report/site.js.map +1 -0
- package/dist/src/rules/code-injection.d.ts +47 -0
- package/dist/src/rules/code-injection.js +326 -0
- package/dist/src/rules/code-injection.js.map +1 -0
- package/dist/src/rules/command-injection.d.ts +30 -0
- package/dist/src/rules/command-injection.js +286 -0
- package/dist/src/rules/command-injection.js.map +1 -0
- package/dist/src/rules/contract.d.ts +198 -0
- package/dist/src/rules/contract.js +39 -0
- package/dist/src/rules/contract.js.map +1 -0
- package/dist/src/rules/format-string.d.ts +46 -0
- package/dist/src/rules/format-string.js +146 -0
- package/dist/src/rules/format-string.js.map +1 -0
- package/dist/src/rules/hardcoded-secret.d.ts +38 -0
- package/dist/src/rules/hardcoded-secret.js +440 -0
- package/dist/src/rules/hardcoded-secret.js.map +1 -0
- package/dist/src/rules/lib/guards.d.ts +168 -0
- package/dist/src/rules/lib/guards.js +1179 -0
- package/dist/src/rules/lib/guards.js.map +1 -0
- package/dist/src/rules/lib/html-safety.d.ts +75 -0
- package/dist/src/rules/lib/html-safety.js +755 -0
- package/dist/src/rules/lib/html-safety.js.map +1 -0
- package/dist/src/rules/lib/local-build.d.ts +59 -0
- package/dist/src/rules/lib/local-build.js +320 -0
- package/dist/src/rules/lib/local-build.js.map +1 -0
- package/dist/src/rules/lib/strings.d.ts +208 -0
- package/dist/src/rules/lib/strings.js +1970 -0
- package/dist/src/rules/lib/strings.js.map +1 -0
- package/dist/src/rules/path-traversal.d.ts +33 -0
- package/dist/src/rules/path-traversal.js +96 -0
- package/dist/src/rules/path-traversal.js.map +1 -0
- package/dist/src/rules/registry.d.ts +19 -0
- package/dist/src/rules/registry.js +77 -0
- package/dist/src/rules/registry.js.map +1 -0
- package/dist/src/rules/sql-injection.d.ts +36 -0
- package/dist/src/rules/sql-injection.js +299 -0
- package/dist/src/rules/sql-injection.js.map +1 -0
- package/dist/src/rules/ssrf.d.ts +39 -0
- package/dist/src/rules/ssrf.js +111 -0
- package/dist/src/rules/ssrf.js.map +1 -0
- package/dist/src/rules/unbounded-copy.d.ts +37 -0
- package/dist/src/rules/unbounded-copy.js +147 -0
- package/dist/src/rules/unbounded-copy.js.map +1 -0
- package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
- package/dist/src/rules/unsafe-deserialization.js +276 -0
- package/dist/src/rules/unsafe-deserialization.js.map +1 -0
- package/dist/src/rules/weak-hash.d.ts +37 -0
- package/dist/src/rules/weak-hash.js +197 -0
- package/dist/src/rules/weak-hash.js.map +1 -0
- package/dist/src/rules/xss.d.ts +38 -0
- package/dist/src/rules/xss.js +527 -0
- package/dist/src/rules/xss.js.map +1 -0
- package/dist/src/taint/content-type.d.ts +19 -0
- package/dist/src/taint/content-type.js +20 -0
- package/dist/src/taint/content-type.js.map +1 -0
- package/dist/src/taint/dictionaries.d.ts +23 -0
- package/dist/src/taint/dictionaries.js +1778 -0
- package/dist/src/taint/dictionaries.js.map +1 -0
- package/dist/src/taint/fold.d.ts +59 -0
- package/dist/src/taint/fold.js +1177 -0
- package/dist/src/taint/fold.js.map +1 -0
- package/dist/src/taint/java-scope.d.ts +61 -0
- package/dist/src/taint/java-scope.js +212 -0
- package/dist/src/taint/java-scope.js.map +1 -0
- package/dist/src/taint/local-map.d.ts +79 -0
- package/dist/src/taint/local-map.js +603 -0
- package/dist/src/taint/local-map.js.map +1 -0
- package/dist/src/taint/project.d.ts +172 -0
- package/dist/src/taint/project.js +617 -0
- package/dist/src/taint/project.js.map +1 -0
- package/dist/src/taint/tracer.d.ts +265 -0
- package/dist/src/taint/tracer.js +3817 -0
- package/dist/src/taint/tracer.js.map +1 -0
- package/dist/src/taint/types.d.ts +484 -0
- package/dist/src/taint/types.js +47 -0
- package/dist/src/taint/types.js.map +1 -0
- package/dist/src/taint/visibility.d.ts +25 -0
- package/dist/src/taint/visibility.js +96 -0
- package/dist/src/taint/visibility.js.map +1 -0
- package/docs/examples/defuse.json +20 -0
- package/docs/examples/dvwa-urls.json +14 -0
- package/docs/examples/github-code-scanning.yml +54 -0
- package/docs/examples/tiny.js +4 -0
- package/package.json +73 -4
- package/src/cli.ts +747 -0
- package/src/core/analyze.ts +1072 -0
- package/src/core/coverage.ts +269 -0
- package/src/core/finding.ts +311 -0
- package/src/core/licence.ts +61 -0
- package/src/core/paths.ts +59 -0
- package/src/core/score.ts +353 -0
- package/src/dast/targets.ts +121 -0
- package/src/dast/zap.ts +246 -0
- package/src/engine/scan.ts +200 -0
- package/src/engine/shapes.ts +348 -0
- package/src/engine/walk.ts +196 -0
- package/src/parse/astDump.ts +85 -0
- package/src/parse/languages.ts +232 -0
- package/src/parse/location.ts +44 -0
- package/src/parse/node-grammars.ts +41 -0
- package/src/parse/parser.ts +262 -0
- package/src/parse/tree-store.ts +301 -0
- package/src/platform/dependency-rule.ts +33 -0
- package/src/platform/deps.ts +374 -0
- package/src/platform/php-facts.ts +85 -0
- package/src/platform/runtime-version.ts +710 -0
- package/src/report/colors.ts +107 -0
- package/src/report/coverage-table.ts +359 -0
- package/src/report/html-report.ts +601 -0
- package/src/report/human.ts +811 -0
- package/src/report/json.ts +189 -0
- package/src/report/mark.ts +47 -0
- package/src/report/pricing.ts +374 -0
- package/src/report/sarif.ts +235 -0
- package/src/report/site.ts +760 -0
- package/src/rules/code-injection.ts +346 -0
- package/src/rules/command-injection.ts +296 -0
- package/src/rules/contract.ts +220 -0
- package/src/rules/format-string.ts +158 -0
- package/src/rules/hardcoded-secret.ts +485 -0
- package/src/rules/lib/guards.ts +1235 -0
- package/src/rules/lib/html-safety.ts +793 -0
- package/src/rules/lib/local-build.ts +329 -0
- package/src/rules/lib/strings.ts +2034 -0
- package/src/rules/path-traversal.ts +101 -0
- package/src/rules/registry.ts +90 -0
- package/src/rules/sql-injection.ts +311 -0
- package/src/rules/ssrf.ts +118 -0
- package/src/rules/unbounded-copy.ts +162 -0
- package/src/rules/unsafe-deserialization.ts +291 -0
- package/src/rules/weak-hash.ts +226 -0
- package/src/rules/xss.ts +572 -0
- package/src/taint/content-type.ts +20 -0
- package/src/taint/dictionaries.ts +1855 -0
- package/src/taint/fold.ts +1206 -0
- package/src/taint/java-scope.ts +200 -0
- package/src/taint/local-map.ts +594 -0
- package/src/taint/project.ts +774 -0
- package/src/taint/tracer.ts +4298 -0
- package/src/taint/types.ts +511 -0
- package/src/taint/visibility.ts +100 -0
- package/tsconfig.json +61 -0
package/README.md
CHANGED
|
@@ -1,3 +1,562 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Defuse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Static analysis that tells you which findings it proved and which it only
|
|
4
|
+
guessed.**
|
|
5
|
+
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](#quick-start)
|
|
8
|
+
[](#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 {};
|