@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
@@ -0,0 +1,178 @@
1
+ /**
2
+ * THE HONESTY CONTRACT
3
+ * ====================
4
+ *
5
+ * This file is the most important file in the project. Everything else is
6
+ * plumbing. Read this one carefully.
7
+ *
8
+ * Defuse promises one thing that Snyk / Semgrep / SonarQube do not
9
+ * promise clearly: every single finding tells you HOW SURE we are, in a way you
10
+ * can act on. There are exactly two levels, and they mean precise things:
11
+ *
12
+ * "signature-based" We matched a SHAPE in your code. We saw a pattern that
13
+ * is *often* a vulnerability. We did NOT trace where the
14
+ * data actually came from. This might be a real bug, or it
15
+ * might be completely safe. YOU still have to look.
16
+ *
17
+ * "flow-verified" We traced actual data, step by step, from a place an
18
+ * attacker controls (a "source", e.g. an HTTP request)
19
+ * all the way into a dangerous operation (a "sink", e.g.
20
+ * a database query), and we confirmed nothing cleaned it
21
+ * up along the way. We can show you every hop.
22
+ *
23
+ * A RULE CAN ONLY EVER PRODUCE "signature-based". Only the taint engine can
24
+ * produce the other label.
25
+ *
26
+ * We do not enforce that with willpower or a code review checklist. We enforce
27
+ * it with the type system:
28
+ *
29
+ * - A FlowVerifiedFinding REQUIRES a non-empty `flowPath` of concrete steps.
30
+ * - Only the taint engine can build one. A rule cannot, because a rule only
31
+ * ever sees a single shape - it has no idea where the value came from.
32
+ * - So a rule physically cannot emit a "flow-verified" finding: there is no
33
+ * path it could supply, and TypeScript refuses to compile the attempt.
34
+ *
35
+ * That is the difference between a promise and a guarantee.
36
+ *
37
+ * ARCHITECTURAL DECISION (matters later): we are modelling Finding as a
38
+ * "discriminated union" - two shapes sharing one `confidence` tag - rather than
39
+ * one flat object with an optional flag. The taint engine supplies the second
40
+ * shape, and every report renderer is forced by the compiler to handle it.
41
+ * Nothing silently upgrades its own confidence.
42
+ */
43
+ import { LANGUAGES } from '../parse/languages.js';
44
+ export const SEVERITY_ORDER = {
45
+ critical: 4,
46
+ high: 3,
47
+ medium: 2,
48
+ low: 1,
49
+ info: 0,
50
+ };
51
+ /**
52
+ * The constructor for everything the RULE engine produces.
53
+ *
54
+ * Note what it does not accept: there is no `confidence` parameter. A rule
55
+ * author cannot pass "flow-verified" even by mistake, because the value is
56
+ * hard-coded here and nowhere else.
57
+ */
58
+ export function signatureFinding(input) {
59
+ return {
60
+ ruleId: input.ruleId,
61
+ ruleName: input.ruleName,
62
+ cwe: input.cwe,
63
+ owasp: input.owasp,
64
+ severity: input.severity,
65
+ message: input.message,
66
+ location: input.location,
67
+ reasoning: input.reasoning,
68
+ limitations: input.limitations,
69
+ detectedAt: input.detectedAt ?? new Date().toISOString(),
70
+ confidence: 'signature-based',
71
+ verified: false,
72
+ flowPath: null,
73
+ };
74
+ }
75
+ /**
76
+ * THE SECOND CONSTRUCTOR.
77
+ *
78
+ * For the whole first stretch of this project, this function threw. It was a
79
+ * deliberate, documented stub: the type for a flow-verified finding existed, so
80
+ * anyone could read what the claim would REQUIRE, but nothing could produce one.
81
+ *
82
+ * It is real now, and the requirement did not soften. Note the runtime checks
83
+ * below: a flow-verified finding must carry a path that BEGINS at a source and
84
+ * ENDS at a sink. Not "a path". Not "some steps". If the taint engine ever
85
+ * hands over a path that doesn't start where attacker data enters and finish
86
+ * where it does damage, this throws rather than emitting the claim.
87
+ *
88
+ * The type system stops a rule from lying. This function stops the ENGINE from
89
+ * lying - including a future version of it written by someone in a hurry.
90
+ */
91
+ export function flowVerifiedFinding(input) {
92
+ const path = input.flowPath;
93
+ if (path.length < 2) {
94
+ throw new Error(`flowVerifiedFinding: a verified path needs at least a source and a sink, got ${path.length} step(s).`);
95
+ }
96
+ if (path[0]?.kind !== 'source') {
97
+ throw new Error(`flowVerifiedFinding: path must begin at a source, begins at '${path[0]?.kind}'.`);
98
+ }
99
+ if (path[path.length - 1]?.kind !== 'sink') {
100
+ throw new Error(`flowVerifiedFinding: path must end at a sink, ends at '${path[path.length - 1]?.kind}'.`);
101
+ }
102
+ // A path containing a sanitiser is, by definition, not a confirmed exploit.
103
+ // If the engine ever tries to report one, that is a bug in the engine and we
104
+ // would rather crash the scan than publish the claim.
105
+ const sanitised = path.find((step) => step.kind === 'sanitizer');
106
+ if (sanitised) {
107
+ throw new Error(`flowVerifiedFinding: path contains a sanitiser (${sanitised.description}) - that is a CLEAN flow, not a verified vulnerability.`);
108
+ }
109
+ return {
110
+ ruleId: input.ruleId,
111
+ ruleName: input.ruleName,
112
+ cwe: input.cwe,
113
+ owasp: input.owasp,
114
+ severity: input.severity,
115
+ message: input.message,
116
+ location: input.location,
117
+ reasoning: input.reasoning,
118
+ limitations: input.limitations,
119
+ detectedAt: input.detectedAt ?? new Date().toISOString(),
120
+ confidence: 'flow-verified',
121
+ verified: true,
122
+ flowPath: path,
123
+ unmodelledHops: input.unmodelledHops ?? [],
124
+ };
125
+ }
126
+ /** What this build of the engine can and cannot do. Printed in every report. */
127
+ export const ENGINE_CAPABILITIES = {
128
+ /**
129
+ * What the banner prints, on the terminal and in the browser.
130
+ *
131
+ * A phase number used to live here. It described OUR build order, not
132
+ * anything a user of the tool can act on - and it went stale the moment work
133
+ * happened out of order: the banner read "PHASE 3C" while the version string
134
+ * read "phase2", which is precisely the kind of small lie this project exists
135
+ * to not tell. A capability label cannot drift, because it can only change
136
+ * when the engine actually changes.
137
+ */
138
+ analysisLabel: 'SIGNATURE + DATA FLOW',
139
+ version: '0.5.0',
140
+ implemented: [
141
+ `Tree-sitter AST parsing for ${LANGUAGES.map((l) => l.displayName).join(', ')}`,
142
+ 'Pattern (signature) matching over real syntax trees, not text/regex',
143
+ `Taint analysis in all ${LANGUAGES.length} languages: source to sink, following values into other functions AND - in every language except PHP - into other files through resolved imports`,
144
+ 'Prepared-statement awareness: arguments bound to a prepared query are parameters, not instructions',
145
+ 'Kind-scoped sanitiser recognition - an HTML escaper does not clear a SQL sink',
146
+ 'Retraction: a signature guess is withdrawn when the tracer proves a sanitiser runs',
147
+ 'Verified-clean reporting: lines the tracer reached and proved safe are listed, so "no finding here" can be told apart from "never looked here"',
148
+ 'One engine, two shells: the terminal and the browser run the same compiled analysis module, not two implementations of it',
149
+ 'Receiver state: a tainted argument dirties the object it was handed to, so `sb.append(dirty)` and `list.add(dirty)` are followed - and a sink that takes NO arguments because the payload was loaded earlier (`pb.command(list); pb.start()`) is recognised',
150
+ 'Return-value sinks in Python: a Flask/Django view that returns an HTML string is treated as writing the response body, so the commonest reflected XSS in Python is seen at all - and the escaped version is credited as clean rather than merely unflagged',
151
+ 'Escape proof before complaint (JavaScript/TypeScript): before reporting a value written into HTML, local variables, local render helpers and their arguments are resolved to see whether every spliced-in value is a literal, a number, or the output of a known escaper. A finding that survives names the exact sub-expression it could not prove safe.',
152
+ 'SQL is recognised by statement SHAPE (SELECT paired with FROM, INSERT with INTO) inside a bounded window, not by keywords scattered anywhere in the text - so a paragraph of prose containing "call" and "where" is not a query',
153
+ 'Per-rule and per-language coverage reporting, including gaps',
154
+ ],
155
+ notImplemented: [
156
+ 'Third-party code: only files inside the scan are resolved. A call into node_modules, a JAR or a vendored package ends the trace.',
157
+ 'Ambiguous imports: when several files in scope define the same function name, we decline to guess and the trace stops there. The count is reported.',
158
+ 'Re-exports, dynamic imports, computed requires, and build-tool path aliases (tsconfig paths, webpack) are not resolved.',
159
+ 'Whether a function is actually EXPORTED is not checked - a module-private helper with a matching name could be entered.',
160
+ 'Call chains deeper than four functions are not followed, and a call that resolves by name to a function already being followed on the same path - recursion, or a same-named method on another object - is not re-entered. At either stop the value is carried on as an ASSUMPTION that the unread call preserves it, and the finding names the call and why it was not read. So a sanitiser hidden past a stop is missed and the line is reported; before this was fixed the stop declared the value clean, which cost BenchmarkJava 38 real vulnerabilities.',
161
+ 'Calls are resolved by NAME, with two checks where the language makes them sound: in Java, Go, C and C++ a call must pass a number of arguments the target accepts, and in Java a call on a receiver of a declared type only reaches methods declared in that type. An interface-typed receiver therefore reaches no body at all, and the value is carried past it as a named assumption.',
162
+ 'Object and field state: `obj.field = dirty` and `sb.append(dirty)` are invisible - we track variables, not the interiors of objects.',
163
+ 'Sources are recognised by expression shape (req.query, request.form), not by proving the object really is a request.',
164
+ 'Branch conditions are evaluated only when they are fixed at compile time: literals, arithmetic, comparisons and logic over them, a local DECLARED once with such a value (in Python, PHP and a JavaScript `var`: written once at the top of the function, since those have no block-scoped declarations), a character literal, and Java\'s `charAt` on a string literal. A name that is a parameter, a field, incremented, passed by address or reference, part of a destructuring or multi-value write, or written more than once is not fixed; nor is anything in C++ unless it is declared const or constexpr, since a reference can write to it invisibly; nor anything in a function that uses eval, with, arguments, $$name, extract() or exec(), that C code can goto into, or that did not parse cleanly. Only exact arithmetic is folded: no octal literals, no division outside integer languages, and no value outside the range its declared type holds. Such a condition decides `if`, ternaries and switch statements (Java, JavaScript, TypeScript, C, C++, PHP and Go - not Python\'s `match`); strings are never compared, so a switch on a string stays undecided. Every branch or case skipped this way is listed in diagnostics.unreachableCodeSkipped with the decision behind it. Every other condition is not read, so we make the weaker claim that a CONDITIONAL write cannot prove a value clean: taint set anywhere survives a clean assignment inside an if/else, switch or loop, while an unconditional one still clears it. That errs toward reporting a value that might be clean rather than staying silent about one that might be dirty - and it is why `if (cond) bar = dirty; else bar = "safe";` is still reported.',
165
+ 'Loops are not iterated to a fixpoint, so a value that becomes tainted late in a loop body is missed.',
166
+ 'Containers are tainted whole, not per element: after `list.add(clean)` and `list.add(dirty)`, or after `Object[] a = {"safe", dirty}`, every read from that container is treated as dirty. This is the price of following values into objects at all, and it errs toward reporting rather than missing. TWO EXCEPTIONS, Java only, where every write the collection can receive is visible: a map created locally as `new HashMap<>()` or `new LinkedHashMap<>()` and touched by nothing but straight-line `put("literal", value);` statements and reads answers `get("literal")` with the last put under that key; and a list created locally as `new ArrayList<>()` or `new LinkedList<>()` and touched by nothing but straight-line `add(value);` and `remove(n);` statements and reads is replayed, so `get(n)` returns what sits at that position. Anything else - a conditional write, a variable key or index, the collection handed to anything, any other method - keeps the whole-collection answer. A stored value that can change in place (a StringBuilder, an array, a list) is not read this way, since an alias could change it unseen. Every read answered this way is listed in diagnostics.collectionReadsResolved.',
167
+ 'Framework awareness is PER-FRAMEWORK and partial. Recognised: Express/Koa request objects, Flask/Django/DRF request objects, URL path parameters named by a Flask or FastAPI route decorator, NestJS @Param/@Query/@Body/@Headers parameters, Spring and JAX-RS parameter annotations, servlet getters, net/http and Gin/Echo context values, PHP superglobals and $request accessors. NOT recognised, and each one is a silent zero: any house-built REST layer (elasticsearch\'s own RestRequest is 783 uses in one repository), Micronaut, Quarkus outside JAX-RS, Vert.x, Play, NestJS decorators other than @Param, @Query, @Body and @Headers, FastAPI query parameters a route path does not name and Pydantic body models, and Express ROUTING itself - a value that only becomes attacker-controlled through a framework binding we do not list is never seen as a source. The report now prints how many sources were recognised, so a zero is visible rather than reading as a clean bill.',
168
+ 'The escape proof is JavaScript and TypeScript only, and same-file only. An escaper imported from another module, applied by a framework, or reached through more than six nested calls reads as unproven - so Python, Java, PHP and Go HTML sinks still report on shape alone.',
169
+ 'MEMORY SCALES WITH THE SOURCE, at roughly 7x its bytes plus a fixed ~300MB for Node and the seven WebAssembly grammars. Syntax trees live in a WASM heap that JavaScript garbage collection cannot touch, so they are freed explicitly: the scan holds as many as DEFUSE_TREE_BUDGET_MB allows and parses a file again if something needs it after its tree was dropped. Measured on one 54MB corpus, same binary, only the budget changed: 1474MB peak holding everything, 716MB with a 256MB tree budget, findings byte-identical, 7% slower. That puts the practical ceiling near 1.1GB of source on an 8GB machine. A scan that does not fit gets SLOWER rather than being killed, and the report says so - diagnostics.treeMemory.reparses above zero means files were parsed more than once. THE PREVIOUS VERSION OF THIS NOTE WAS WRONG, and is recorded here rather than quietly deleted: it said trees were never freed "because cross-file tracing needs to resolve into any file at any time", and cited a 120MB ceiling. Cross-file had nothing to do with it - measured, --no-cross-file used 535.8MB against 528.8MB with it on, because nothing called delete() in either mode. The ceiling was real; the explanation for it was invented.',
170
+ 'Expressions nested deeper than 400 levels are not walked. Hand-written code never reaches this; generated, minified and stress-test files do. The count is reported as a blind spot rather than silently skipped - an earlier version had no limit at all and one such file killed an entire 25,809-file scan with a stack overflow, losing every finding in it.',
171
+ 'Vendor documentation example credentials (a key matching a published format but ending in EXAMPLE, or filled with 0000/1234567890) are treated as placeholders and not reported. A real credential that happened to look like one would be missed.',
172
+ ],
173
+ meaningOfConfidence: {
174
+ 'signature-based': 'Pattern matched in the syntax tree. Data flow NOT traced. May be a false positive.',
175
+ 'flow-verified': 'Attacker-controlled data was traced from a source into a sink - across functions and, where imports resolve, across files - with no sanitiser on the path. Every hop is listed in flowPath, with its own file and line.',
176
+ },
177
+ };
178
+ //# sourceMappingURL=finding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finding.js","sourceRoot":"","sources":["../../../src/core/finding.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * THE LICENCE, IN ONE PLACE
3
+ * =========================
4
+ *
5
+ * WHY A MODULE AND NOT A STRING IN THREE FILES.
6
+ *
7
+ * This project has been bitten four times by the same shape of bug: a fact
8
+ * written down in prose next to the same fact held in data, and the two
9
+ * drifting apart. The README claimed 112 checks while 207 ran. It claimed
10
+ * 13.8% recall for roughly twenty versions after that stopped being true. The
11
+ * engine's own limitation text explained the memory ceiling with a cause that
12
+ * a two-line experiment disproved.
13
+ *
14
+ * A licence is a worse place for that to happen than a count is. "This is
15
+ * AGPL-3.0" printed in a terminal, "UNLICENSED" in package.json and an MIT file
16
+ * on disk is not a cosmetic inconsistency - it is three different legal claims
17
+ * about the same program, and the person who gets hurt is whoever believed one
18
+ * of them. So the licence is declared once here, and a test fails if the
19
+ * LICENSE file or package.json disagrees.
20
+ *
21
+ * WHY AGPL-3.0 SPECIFICALLY.
22
+ *
23
+ * The tool can be run as a network service - `npm run ui` serves the browser
24
+ * shell, and the same compiled engine runs inside it. Under a permissive
25
+ * licence somebody could host exactly that, modify it, and never publish a
26
+ * line. Section 13 of the AGPL is the clause that closes it: anyone who runs a
27
+ * MODIFIED version and lets other people interact with it over a network must
28
+ * offer those users the corresponding source.
29
+ *
30
+ * Note what that does NOT say. Using this tool on your own private code puts no
31
+ * obligation on you whatsoever, and neither does running it unmodified inside
32
+ * your company. The obligation attaches to distributing or network-serving a
33
+ * CHANGED version. Scanning a closed-source repository with it is exactly the
34
+ * intended use and carries no licence consequence for the code being scanned.
35
+ */
36
+ /** SPDX identifier. Must match package.json's `license` field exactly. */
37
+ export declare const LICENCE_SPDX = "AGPL-3.0-only";
38
+ /** The first line of the LICENSE file, checked against it by the test suite. */
39
+ export declare const LICENCE_FILE_HEADING = "GNU AFFERO GENERAL PUBLIC LICENSE";
40
+ /** One line, for the bottom of `--help`. */
41
+ export declare const LICENCE_LINE: string;
42
+ /**
43
+ * The section 13 notice, for the browser UI.
44
+ *
45
+ * The UI is the reason the licence is AGPL rather than GPL, so this is the one
46
+ * place the notice is not merely good manners. It is shown rather than buried
47
+ * in a file nobody opens, because section 13 says "prominently offer" and a
48
+ * link in a LICENSE file the user never sees is not an offer.
49
+ */
50
+ export declare const NETWORK_SOURCE_NOTICE: string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * THE LICENCE, IN ONE PLACE
3
+ * =========================
4
+ *
5
+ * WHY A MODULE AND NOT A STRING IN THREE FILES.
6
+ *
7
+ * This project has been bitten four times by the same shape of bug: a fact
8
+ * written down in prose next to the same fact held in data, and the two
9
+ * drifting apart. The README claimed 112 checks while 207 ran. It claimed
10
+ * 13.8% recall for roughly twenty versions after that stopped being true. The
11
+ * engine's own limitation text explained the memory ceiling with a cause that
12
+ * a two-line experiment disproved.
13
+ *
14
+ * A licence is a worse place for that to happen than a count is. "This is
15
+ * AGPL-3.0" printed in a terminal, "UNLICENSED" in package.json and an MIT file
16
+ * on disk is not a cosmetic inconsistency - it is three different legal claims
17
+ * about the same program, and the person who gets hurt is whoever believed one
18
+ * of them. So the licence is declared once here, and a test fails if the
19
+ * LICENSE file or package.json disagrees.
20
+ *
21
+ * WHY AGPL-3.0 SPECIFICALLY.
22
+ *
23
+ * The tool can be run as a network service - `npm run ui` serves the browser
24
+ * shell, and the same compiled engine runs inside it. Under a permissive
25
+ * licence somebody could host exactly that, modify it, and never publish a
26
+ * line. Section 13 of the AGPL is the clause that closes it: anyone who runs a
27
+ * MODIFIED version and lets other people interact with it over a network must
28
+ * offer those users the corresponding source.
29
+ *
30
+ * Note what that does NOT say. Using this tool on your own private code puts no
31
+ * obligation on you whatsoever, and neither does running it unmodified inside
32
+ * your company. The obligation attaches to distributing or network-serving a
33
+ * CHANGED version. Scanning a closed-source repository with it is exactly the
34
+ * intended use and carries no licence consequence for the code being scanned.
35
+ */
36
+ /** SPDX identifier. Must match package.json's `license` field exactly. */
37
+ export const LICENCE_SPDX = 'AGPL-3.0-only';
38
+ /** The first line of the LICENSE file, checked against it by the test suite. */
39
+ export const LICENCE_FILE_HEADING = 'GNU AFFERO GENERAL PUBLIC LICENSE';
40
+ /** One line, for the bottom of `--help`. */
41
+ export const LICENCE_LINE = `${LICENCE_SPDX} - free to use, including on closed-source code. ` +
42
+ 'If you MODIFY it and let others use it over a network, section 13 says you ' +
43
+ 'must offer them the source. See LICENSE.';
44
+ /**
45
+ * The section 13 notice, for the browser UI.
46
+ *
47
+ * The UI is the reason the licence is AGPL rather than GPL, so this is the one
48
+ * place the notice is not merely good manners. It is shown rather than buried
49
+ * in a file nobody opens, because section 13 says "prominently offer" and a
50
+ * link in a LICENSE file the user never sees is not an offer.
51
+ */
52
+ export const NETWORK_SOURCE_NOTICE = `Defuse is ${LICENCE_SPDX}. This page runs the same analysis engine as the ` +
53
+ 'command line, entirely in your browser - nothing you load here is uploaded ' +
54
+ 'anywhere. If you are running a MODIFIED copy of this tool as a service for ' +
55
+ 'other people, the licence requires you to offer them its source.';
56
+ //# sourceMappingURL=licence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"licence.js","sourceRoot":"","sources":["../../../src/core/licence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,YAAY,GAAG,eAAe,CAAC;AAE5C,gFAAgF;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,mCAAmC,CAAC;AAExE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,YAAY,GACvB,GAAG,YAAY,mDAAmD;IAClE,6EAA6E;IAC7E,0CAA0C,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAChC,aAAa,YAAY,mDAAmD;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,kEAAkE,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * PATH HELPERS WITH NO PLATFORM ATTACHED
3
+ *
4
+ * `node:path` was the last thing standing between the analysis code and a
5
+ * browser. The import graph needs to join and normalise paths, and Node's
6
+ * version of that pulls in a runtime the browser does not have.
7
+ *
8
+ * These are the four operations the resolver actually uses, written in plain
9
+ * JavaScript over forward slashes. Windows paths are normalised on the way in,
10
+ * so `C:\\src\\db.ts` and `C:/src/db.ts` compare equal - which matters, because
11
+ * a path that fails to match is an import edge silently not resolved.
12
+ */
13
+ /** Backslashes to forward slashes. Everything else here assumes this ran. */
14
+ export declare function toPosix(filePath: string): string;
15
+ /** Collapse `.` and `..` segments. Keeps a leading `/` or a `C:` drive. */
16
+ export declare function normalizePath(filePath: string): string;
17
+ export declare function dirnameOf(filePath: string): string;
18
+ export declare function basenameOf(filePath: string): string;
19
+ export declare function joinPath(...parts: readonly string[]): string;
20
+ /** Resolve a relative specifier against a directory. */
21
+ export declare function resolveFrom(directory: string, relative: string): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * PATH HELPERS WITH NO PLATFORM ATTACHED
3
+ *
4
+ * `node:path` was the last thing standing between the analysis code and a
5
+ * browser. The import graph needs to join and normalise paths, and Node's
6
+ * version of that pulls in a runtime the browser does not have.
7
+ *
8
+ * These are the four operations the resolver actually uses, written in plain
9
+ * JavaScript over forward slashes. Windows paths are normalised on the way in,
10
+ * so `C:\\src\\db.ts` and `C:/src/db.ts` compare equal - which matters, because
11
+ * a path that fails to match is an import edge silently not resolved.
12
+ */
13
+ /** Backslashes to forward slashes. Everything else here assumes this ran. */
14
+ export function toPosix(filePath) {
15
+ return filePath.replace(/\\/g, '/');
16
+ }
17
+ /** Collapse `.` and `..` segments. Keeps a leading `/` or a `C:` drive. */
18
+ export function normalizePath(filePath) {
19
+ const posix = toPosix(filePath);
20
+ const absolute = posix.startsWith('/');
21
+ const parts = [];
22
+ for (const segment of posix.split('/')) {
23
+ if (!segment || segment === '.')
24
+ continue;
25
+ if (segment === '..') {
26
+ const last = parts[parts.length - 1];
27
+ if (parts.length > 0 && last !== '..')
28
+ parts.pop();
29
+ else if (!absolute)
30
+ parts.push('..');
31
+ continue;
32
+ }
33
+ parts.push(segment);
34
+ }
35
+ return (absolute ? '/' : '') + parts.join('/');
36
+ }
37
+ export function dirnameOf(filePath) {
38
+ const posix = toPosix(filePath);
39
+ const cut = posix.lastIndexOf('/');
40
+ if (cut < 0)
41
+ return '';
42
+ return cut === 0 ? '/' : posix.slice(0, cut);
43
+ }
44
+ export function basenameOf(filePath) {
45
+ const posix = toPosix(filePath);
46
+ return posix.slice(posix.lastIndexOf('/') + 1);
47
+ }
48
+ export function joinPath(...parts) {
49
+ return normalizePath(parts.filter(Boolean).join('/'));
50
+ }
51
+ /** Resolve a relative specifier against a directory. */
52
+ export function resolveFrom(directory, relative) {
53
+ const posix = toPosix(relative);
54
+ if (posix.startsWith('/'))
55
+ return normalizePath(posix);
56
+ return normalizePath(`${toPosix(directory)}/${posix}`);
57
+ }
58
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CAAC,QAAgB;IACtC,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,GAAG;YAAE,SAAS;QAC1C,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI;gBAAE,KAAK,CAAC,GAAG,EAAE,CAAC;iBAC9C,IAAI,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrC,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IACvB,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAG,KAAwB;IAClD,OAAO,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,QAAgB;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,aAAa,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * THE GAUGES
3
+ * ==========
4
+ *
5
+ * Three needles. They are never added together, and this file will not let
6
+ * them be - there is no `overallScore` export, and there never should be.
7
+ *
8
+ * WHY THAT MATTERS. The comment at the top of finding.ts says it best:
9
+ *
10
+ * "Severity = how bad is this if it turns out to be real? Deliberately
11
+ * separate from confidence, which is how sure are we it's real?
12
+ * Conflating those two numbers is the single most common lie in SAST
13
+ * tooling: a tool shows one 'risk' bar and you cannot tell whether it
14
+ * means 'probably harmless but catastrophic if not' or 'definitely real
15
+ * but minor'."
16
+ *
17
+ * A single 0-500 risk dial IS that bar. So the dial got split along the exact
18
+ * line the project already draws:
19
+ *
20
+ * VERIFIED EXPOSURE severity, over findings we PROVED reach a sink
21
+ * UNVERIFIED SURFACE severity, over findings we only pattern-matched
22
+ * ANALYSIS COVERAGE how much of the code we could actually reason about
23
+ *
24
+ * Two of those are the same weights over two different confidence levels, kept
25
+ * side by side so you can see the shape of what is proven against the shape of
26
+ * what is merely suspected. Reading them together is the point; averaging them
27
+ * would destroy it.
28
+ *
29
+ * Every reading carries its own arithmetic in `working`, so the number can be
30
+ * CHECKED rather than believed. That is the whole standard here: a score you
31
+ * cannot recompute by hand is a vibe with a decimal point on it.
32
+ */
33
+ import type { Severity } from './finding.js';
34
+ import type { AnalysisResult } from './analyze.js';
35
+ /**
36
+ * How much one finding contributes, by severity.
37
+ *
38
+ * These are a judgement, not a measurement, and calling them anything else
39
+ * would be the first lie. What makes them defensible is that they are FIXED,
40
+ * PUBLISHED and applied identically to both severity gauges - so while the
41
+ * absolute number is arbitrary, every comparison made with it is real: two
42
+ * criticals always outweigh three mediums, in both directions, in both shells.
43
+ */
44
+ export declare const SEVERITY_WEIGHT: Readonly<Record<Severity, number>>;
45
+ /** The top of the severity dials. A sum past this is reported, not hidden. */
46
+ export declare const GAUGE_MAX = 500;
47
+ export interface GaugeReading {
48
+ readonly id: 'verified-exposure' | 'unverified-surface' | 'analysis-coverage';
49
+ readonly label: string;
50
+ /** Where the needle sits. Already clamped to [0, max]. */
51
+ readonly value: number;
52
+ readonly max: number;
53
+ readonly unit: '' | '%';
54
+ /** One line under the needle: what it means, and what it does NOT mean. */
55
+ readonly meaning: string;
56
+ /** The arithmetic. Anyone can recompute the value from these lines. */
57
+ readonly working: readonly string[];
58
+ /**
59
+ * The raw total before clamping. Equal to `value` unless the scan blew past
60
+ * the top of the dial, in which case the dial is pinned and this is the
61
+ * truth. A pinned needle that quietly stops counting is a lie by omission.
62
+ */
63
+ readonly rawTotal: number;
64
+ readonly capped: boolean;
65
+ /**
66
+ * True when there was nothing to measure, as distinct from measuring zero.
67
+ *
68
+ * These are not the same fact and must never be drawn the same way. A scan of
69
+ * an empty file and a scan of ten lines of careful, correctly parameterised
70
+ * code both produced "0 findings, 94% coverage, no blind spots" - identical
71
+ * dashboards for "we checked and found nothing" and "there was nothing to
72
+ * check". The second is a result; the first is the absence of one.
73
+ */
74
+ readonly noReading: boolean;
75
+ /**
76
+ * What the total is MADE OF, largest severity first.
77
+ *
78
+ * A single needle throws this away, and on real code it throws away almost
79
+ * everything: across the corpus the severity dials sit between 0% and 24% of
80
+ * their scale, so the arc is a stub and the stub is all you get. "75" cannot
81
+ * tell you whether that is three criticals or twenty-five lows, and those are
82
+ * different afternoons.
83
+ *
84
+ * Empty for the coverage gauge, which is a ratio and has no composition.
85
+ */
86
+ readonly segments: readonly {
87
+ readonly severity: Severity;
88
+ readonly count: number;
89
+ readonly weight: number;
90
+ /** Share of this gauge's own total, 0-1. Zero when the total is zero. */
91
+ readonly share: number;
92
+ }[];
93
+ }
94
+ export interface ScoreReport {
95
+ readonly gauges: readonly GaugeReading[];
96
+ /**
97
+ * Nothing in this scan could be analysed - no statement, call or assignment
98
+ * ever reached a rule. Every reading below is vacuous when this is true, and
99
+ * any surface showing them has to say so rather than printing zeroes.
100
+ */
101
+ readonly examinedNothing: boolean;
102
+ /** Code constructs the rules actually looked at. The denominator of trust. */
103
+ readonly shapesExamined: number;
104
+ /**
105
+ * Places this scan is known to have stopped early. Not a gauge, because a
106
+ * count of blind spots is not a measurement of risk - but printed beside the
107
+ * gauges, because it is what tells you how much of the dial to trust.
108
+ */
109
+ readonly blindSpots: readonly {
110
+ readonly label: string;
111
+ readonly count: number;
112
+ }[];
113
+ /**
114
+ * How many attacker-controlled SOURCES the tracer recognised in this scan.
115
+ *
116
+ * Reported ALWAYS, including - especially - when it is zero, which is why it
117
+ * cannot live in blindSpots above: that list drops anything with a count of
118
+ * zero, and zero is the interesting reading here.
119
+ *
120
+ * Elasticsearch is why. 3,999 Java files, a REST API over the network, and
121
+ * zero flow-verified findings. It contains 0 `@RequestParam` and 0
122
+ * `getParameter()` - it has its own REST layer, which the dictionaries do not
123
+ * model, so there was never a source to start a trace from. "0 flows proven"
124
+ * read like good news and was actually "we could not begin".
125
+ *
126
+ * Every flow number in a report is downstream of this one.
127
+ */
128
+ readonly sourcesFound: number;
129
+ }
130
+ /**
131
+ * Build all three readings.
132
+ *
133
+ * `filesFound` comes from the walker (Node) or the drop handler (browser), so
134
+ * it is passed in rather than read off the result - the analyzer only ever
135
+ * sees the files that made it that far.
136
+ */
137
+ export declare function scoreAnalysis(result: AnalysisResult, filesFound?: number): ScoreReport;