@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,742 @@
1
+ import { ALL_RULES } from '../rules/registry.js';
2
+ import { LANGUAGES } from '../parse/languages.js';
3
+ /**
4
+ * THE SITE IS GENERATED, FOR THE SAME REASON THE README BLOCKS ARE.
5
+ * ================================================================
6
+ *
7
+ * A marketing page is the single most likely thing in any project to carry a
8
+ * number the code moved past two months ago. It is written once, in a hurry,
9
+ * by someone who knows the figures by heart - and then it sits there being
10
+ * quietly wrong while the engine improves underneath it.
11
+ *
12
+ * This tool cannot afford that. Its whole claim is that it does not say more
13
+ * than it knows, and a landing page boasting a precision figure the benchmark
14
+ * has since revised would be exactly the failure it sells against.
15
+ *
16
+ * So every figure on the page comes from `docs/benchmark-result.json` and
17
+ * `docs/label-split-result.json`, the rule and language counts come from the
18
+ * registry, and `npm test` fails if `docs/index.html` disagrees with what this
19
+ * function produces. The page cannot be wrong without the build going red.
20
+ *
21
+ * Nothing here is hand-typed except the prose - and the prose makes no
22
+ * quantitative claims at all, which is deliberate rather than lazy.
23
+ *
24
+ *
25
+ * ON THE LOOK.
26
+ * ============
27
+ *
28
+ * ONE ACCENT, AND IT IS THE MARK'S OWN.
29
+ *
30
+ * #792CF7 is not a purple chosen to go with the logo - it is the exact value
31
+ * sampled out of `docs/defuse-mark.png`, and the same value the Instagram
32
+ * carousel uses. An earlier version of this page ran on #9F6BFF, a lighter
33
+ * purple that matched nothing, which is how a brand ends up with three
34
+ * different primaries and no one able to say which is right.
35
+ *
36
+ * The ground (#262626), the raised surface (#2C2C2C), the white (#F5F5F5) and
37
+ * the muted text (#A8A8A8) are all measured out of the carousel for the same
38
+ * reason. The site and the social page are now one thing.
39
+ *
40
+ * PROVEN VERSUS GUESSED IS CARRIED BY VALUE AND SHAPE, NOT BY HUE.
41
+ *
42
+ * This replaces an ice/rose temperature split, and the replacement is better
43
+ * rather than merely different. Any hue pair - green/amber, blue/pink - fails
44
+ * for someone: red-green colour blindness is the most common kind there is,
45
+ * and every one of them dies in a grayscale print. So the distinction is now
46
+ * carried three ways at once, none of which is colour:
47
+ *
48
+ * flow-verified solid filled dot white text solid border
49
+ * signature-based hollow ring muted text dashed border
50
+ *
51
+ * Filled against hollow. Bright against dim. Unbroken against broken. That
52
+ * survives grayscale, every form of colour blindness, and a bad monitor - and
53
+ * it reads as what it means, because a proof is substantial and a guess is an
54
+ * outline. Purple stays a brand colour and never becomes a status colour,
55
+ * which is the mistake that made the previous palette need four accents.
56
+ *
57
+ * GLASS, HONESTLY.
58
+ *
59
+ * Surfaces are translucent over the grid, so the panels read as laid on the
60
+ * page rather than cut into it. Every one also declares a solid base colour
61
+ * underneath, because `backdrop-filter` is the first thing to be switched off
62
+ * on a locked-down machine and a panel that becomes unreadable without it is
63
+ * a decoration pretending to be a layout.
64
+ *
65
+ * MOTION EARNS ITS PLACE ONCE.
66
+ *
67
+ * The trace on the proof section draws itself, source to sink, one hop at a
68
+ * time. That animation IS the product. The glow sweep and the pulsing halos
69
+ * that used to be here were not - they were the house style of every generated
70
+ * landing page on the internet, and they have been removed. Everything that
71
+ * remains is disabled under `prefers-reduced-motion`.
72
+ */
73
+ export const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
74
+ /** The install line, in one place - the hero prints it and the button copies it. */
75
+ export const INSTALL_COMMAND = 'npx github:Sikkuumi/Defuse scan .';
76
+ /**
77
+ * WHAT EVERY PAGE OF THE SITE SHARES - the fonts, the colours, the base type,
78
+ * the glass surface, the navigation bar and the footer - so the landing page
79
+ * and the pricing page cannot drift apart in the brand they wear.
80
+ */
81
+ export const BASE_CSS = ` /*
82
+ * THE FONTS ARE SERVED FROM THIS ORIGIN. THE PAGE CALLS NOBODY.
83
+ *
84
+ * This used to be a stylesheet link to fonts.googleapis.com, which meant the
85
+ * homepage of a security tool made a request to a third party before it had
86
+ * drawn anything - handing that third party the visitor's IP address and
87
+ * user agent on the way. The people most likely to open devtools on this
88
+ * page are exactly the people who would notice, and they would be right to.
89
+ *
90
+ * Six files, about 100KB, latin subsets only. Both families are SIL Open
91
+ * Font License 1.1, which permits redistribution and requires the licence to
92
+ * travel with the files - hence docs/fonts/LICENSE-*.txt, which are served
93
+ * alongside rather than being a gesture.
94
+ *
95
+ * font-display:swap draws the text immediately in the fallback and repaints
96
+ * when the real face arrives. The alternative, block, hides the heading until
97
+ * the font loads, and a blank hero is a worse failure than a brief reflow.
98
+ */
99
+ @font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-latin-300-normal.woff2") format("woff2");font-weight:300;font-style:normal;font-display:swap}
100
+ @font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-latin-400-normal.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
101
+ @font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-latin-500-normal.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
102
+ @font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-latin-700-normal.woff2") format("woff2");font-weight:700;font-style:normal;font-display:swap}
103
+ @font-face{font-family:"IBM Plex Mono";src:url("fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
104
+ @font-face{font-family:"IBM Plex Mono";src:url("fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
105
+
106
+ :root{
107
+ /* Ground and surfaces, sampled from the Instagram carousel. */
108
+ --bg:#262626; --panel:#2C2C2C; --raised:#2F2F2F;
109
+ --white:#F5F5F5; --mute:#A8A8A8; --faint:#6E6E6E;
110
+ /* One accent, three tints. #792CF7 is the mark's own value; the lighter two
111
+ exist only so that text on this ground clears contrast - 792CF7 sits at
112
+ 3.4:1, which is fine for a 40px heading and not fine for a 14px line. */
113
+ --brand:#792CF7; --brand-lift:#9153F7; --brand-soft:#A87BFF;
114
+ --line:rgba(245,245,245,.10); --hair:rgba(245,245,245,.06);
115
+ --glass:rgba(245,245,245,.035);
116
+ }
117
+ *{box-sizing:border-box;margin:0;padding:0}
118
+ html{scroll-behavior:smooth}
119
+ body{
120
+ background:var(--bg); color:var(--white);
121
+ font-family:"Space Grotesk",system-ui,sans-serif; font-weight:400; line-height:1.6;
122
+ -webkit-font-smoothing:antialiased; overflow-x:hidden;
123
+ /* The 34px grid from the carousel, at the faintest weight that still reads. */
124
+ background-image:
125
+ linear-gradient(rgba(245,245,245,.020) 1px,transparent 1px),
126
+ linear-gradient(90deg,rgba(245,245,245,.020) 1px,transparent 1px);
127
+ background-size:34px 34px;
128
+ }
129
+ .wrap{max-width:1140px;margin:0 auto;padding:0 28px}
130
+ a{color:var(--brand-soft);text-decoration:none;transition:color 200ms ease}
131
+ a:hover{color:var(--white)}
132
+ .mono,code{font-family:"IBM Plex Mono",ui-monospace,monospace}
133
+ .kicker{font-family:"IBM Plex Mono",monospace;font-size:12px;letter-spacing:2.6px;text-transform:uppercase;color:var(--faint)}
134
+ h1,h2,h3{font-weight:700;letter-spacing:-.9px;line-height:1.06}
135
+ h2{font-size:clamp(32px,4.6vw,52px);margin:18px 0 0}
136
+ p{color:var(--mute);max-width:66ch}
137
+ section{padding:clamp(60px,8vw,104px) 0;border-top:1px solid var(--hair);position:relative}
138
+
139
+ /* ---------- glass ---------- */
140
+ /* Every glass surface names --panel first, so the layout survives with
141
+ backdrop-filter switched off or unsupported. */
142
+ .glass{
143
+ background:var(--panel); background:linear-gradient(180deg,var(--glass),rgba(245,245,245,.012)),var(--panel);
144
+ -webkit-backdrop-filter:blur(16px) saturate(118%); backdrop-filter:blur(16px) saturate(118%);
145
+ border:1px solid var(--line); border-radius:16px;
146
+ }
147
+
148
+ `;
149
+ export const NAV_CSS = ` nav{position:relative;display:flex;align-items:center;gap:18px;padding:26px 0}
150
+ nav img{width:29px;height:auto}
151
+ .word{font-weight:700;font-size:25px;letter-spacing:-.6px;color:var(--brand-lift)}
152
+ .navlinks{display:none;gap:30px;margin-left:26px}
153
+ .navlinks a{font-size:14px;color:var(--mute)}
154
+ .navlinks a:hover{color:var(--white)}
155
+ @media(min-width:880px){.navlinks{display:flex}}
156
+ .ghbtn{margin-left:auto;font-family:"IBM Plex Mono",monospace;font-size:13px;color:var(--white);border:1px solid var(--line);border-radius:999px;padding:10px 19px;transition:background 220ms ease,border-color 220ms ease}
157
+ .ghbtn:hover{background:var(--glass);border-color:var(--brand-lift);color:var(--white)}
158
+
159
+ `;
160
+ export const FOOTER_CSS = ` footer{border-top:1px solid var(--hair);padding:46px 0 70px;display:flex;flex-wrap:wrap;gap:18px;align-items:center;justify-content:space-between}
161
+ `;
162
+ /** The navigation bar, the same on every page; `page` decides where the section links point. */
163
+ export function siteNav(page) {
164
+ const base = page === 'home' ? '' : 'index.html';
165
+ const sections = [
166
+ ['#finding', 'A finding'],
167
+ ['#labels', 'The labels'],
168
+ ['#numbers', 'Measurements'],
169
+ ['#gaps', 'Gaps'],
170
+ ];
171
+ const links = sections.map(([href, label]) => ` <a href="${base}${href}">${label}</a>`);
172
+ links.push(` <a href="pricing.html"${page === 'pricing' ? ' aria-current="page" style="color:var(--white)"' : ''}>Pricing</a>`);
173
+ return ` <nav>
174
+ <a href="index.html" style="display:flex;align-items:center;gap:18px" aria-label="Defuse home">
175
+ <img src="defuse-mark.png" alt="">
176
+ <span class="word">Defuse</span>
177
+ </a>
178
+ <div class="navlinks">
179
+ ${links.join('\n')}
180
+ </div>
181
+ <a class="ghbtn" href="https://github.com/Sikkuumi/Defuse">github</a>
182
+ </nav>`;
183
+ }
184
+ /** The footer, the same on every page. */
185
+ export function siteFooter() {
186
+ return ` <footer>
187
+ <div style="display:flex;align-items:center;gap:13px">
188
+ <img src="defuse-mark.png" alt="" style="width:25px;height:auto">
189
+ <span class="word" style="font-size:20px">Defuse</span>
190
+ </div>
191
+ <div class="mono" style="font-size:13px;color:var(--faint)"><a href="https://github.com/Sikkuumi/Defuse">github.com/Sikkuumi/Defuse</a> · <a href="pricing.html">pricing</a> · AGPL-3.0 · commercial licence available</div>
192
+ </footer>`;
193
+ }
194
+ export function renderSite(benchmark, split) {
195
+ const verified = split.tiers.find((t) => t.label.startsWith('flow-verified'));
196
+ const signature = split.tiers.find((t) => t.label.startsWith('signature-only'));
197
+ if (!verified || !signature)
198
+ throw new Error('label split is missing a tier');
199
+ const ruleCount = ALL_RULES.length;
200
+ const languageNames = LANGUAGES.map((l) => l.displayName);
201
+ const exclDecoy = ((verified.tp / (verified.tp + verified.nonDecoyFalsePositives)) *
202
+ 100).toFixed(1);
203
+ const gap = (Number.parseFloat(verified.precision) - Number.parseFloat(signature.precision)).toFixed(1);
204
+ /* Rule/language pairs the engine declares it does not implement. Counted, not
205
+ asserted - if a rule gains a language this number drops on its own. */
206
+ let notImplemented = 0;
207
+ for (const rule of ALL_RULES) {
208
+ for (const language of LANGUAGES) {
209
+ if (rule.support[language.id]?.status === 'not-implemented')
210
+ notImplemented++;
211
+ }
212
+ }
213
+ /* Bar widths are the measured percentages, so the chart cannot disagree with
214
+ the number printed beside it. */
215
+ const bar = (p) => Number.parseFloat(p).toFixed(1);
216
+ return `<!doctype html>
217
+ <html lang="en">
218
+ <head>
219
+ <meta charset="utf-8">
220
+ <meta name="viewport" content="width=device-width, initial-scale=1">
221
+ <title>Defuse — static analysis that says what it proved</title>
222
+ <meta name="description" content="A multi-language SAST scanner that labels every finding proven or guessed, prints the path behind every claim, and publishes what it did not check.">
223
+ <meta property="og:title" content="Defuse">
224
+ <meta property="og:description" content="Every scanner says possible. This one says what it proved.">
225
+ <meta property="og:type" content="website">
226
+ <meta name="theme-color" content="#262626">
227
+ <link rel="icon" href="defuse-mark.png">
228
+ <style>
229
+ ${BASE_CSS} /* ---------- motion ---------- */
230
+ @keyframes rise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
231
+ /*
232
+ * THE TRACE RUNS ON ONE CLOCK.
233
+ *
234
+ * It used to run on four. Each node shared a single "node" keyframe set and
235
+ * was staggered with animation-delay: 0s, 1.4s, 2.8s. That is not a stagger,
236
+ * and measuring it showed two separate faults.
237
+ *
238
+ * An animation-delay delays the START of the loop. Until it elapses the
239
+ * element renders in its ordinary un-animated state - fully opaque - so on
240
+ * load nodes 2 and 3 sat at opacity 1 while node 1 was still dark, then
241
+ * snapped down to the 0% keyframe when their delay ran out. The dots went
242
+ * OUT in sequence instead of lighting UP in sequence, which is the exact
243
+ * opposite of the thing this animation exists to show.
244
+ *
245
+ * And because a delay shifts the whole loop, each element restarted at a
246
+ * different moment: sampled at 7.3s the spine had reset to nothing while
247
+ * nodes 2 and 3 were still lit from the previous pass. After one cycle the
248
+ * four elements were permanently incoherent.
249
+ *
250
+ * Now the stagger lives in the keyframes and every element runs the same 7s
251
+ * period with no delay, so they reset together and the order holds forever.
252
+ * The percentages are the choreography: node 1 lights at 10%, the spine
253
+ * grows from 8% to 46%, and node 3 lights at 46% - the line arrives exactly
254
+ * as the sink does, rather than finishing 0.4s early as it used to.
255
+ */
256
+ @keyframes grow{0%{transform:scaleY(0);opacity:0}8%{transform:scaleY(0);opacity:1}46%,90%{transform:scaleY(1);opacity:1}100%{transform:scaleY(1);opacity:0}}
257
+ @keyframes n1{0%,3%{opacity:.18;transform:scale(.74)}10%,90%{opacity:1;transform:scale(1)}100%{opacity:.18;transform:scale(.74)}}
258
+ @keyframes n2{0%,20%{opacity:.18;transform:scale(.74)}28%,90%{opacity:1;transform:scale(1)}100%{opacity:.18;transform:scale(.74)}}
259
+ @keyframes n3{0%,38%{opacity:.18;transform:scale(.74)}46%,90%{opacity:1;transform:scale(1)}100%{opacity:.18;transform:scale(.74)}}
260
+ @keyframes barIn{from{transform:scaleX(0)}to{transform:scaleX(1)}}
261
+ .rise{animation:rise 780ms cubic-bezier(.22,.9,.24,1) both}
262
+ .spine{transform-origin:top center;animation:grow 7s cubic-bezier(.5,0,.2,1) infinite}
263
+ .n1{animation:n1 7s cubic-bezier(.4,0,.2,1) infinite}
264
+ .n2{animation:n2 7s cubic-bezier(.4,0,.2,1) infinite}
265
+ .n3{animation:n3 7s cubic-bezier(.4,0,.2,1) infinite}
266
+ .barfill{transform-origin:left center;animation:barIn 1400ms cubic-bezier(.22,.9,.24,1) both}
267
+
268
+ /* ---------- the ambient field ---------- */
269
+ /*
270
+ * Three soft purple masses that drift behind everything, very slowly.
271
+ *
272
+ * z-index:-1 rather than a stacking fight: the body's background - the grid -
273
+ * is painted onto the canvas before any negative-z-index layer, so the grid
274
+ * stays visible THROUGH these, and all ordinary content stays above them
275
+ * without a single z-index added anywhere else.
276
+ *
277
+ * The periods are 44s, 59s and 71s. They are deliberately not multiples of
278
+ * one another, so the three drifts only return to the same arrangement after
279
+ * about forty minutes - long enough that the motion never reads as a loop,
280
+ * which is the whole difference between ambient and animated. "alternate"
281
+ * eases each one back the way it came instead of snapping to its start.
282
+ *
283
+ * Only transform and opacity are animated. Moving a background-position
284
+ * or animating a blur would repaint a full-screen gradient every frame and
285
+ * make the page stutter while scrolling; a transform is handed to the GPU as
286
+ * a layer and costs essentially nothing.
287
+ *
288
+ * The alpha is low on purpose. At these values the purple lifts the ground by
289
+ * a few percent and never approaches the text, so nothing on the page loses
290
+ * contrast as a mass passes under it.
291
+ */
292
+ .field{position:fixed;inset:0;z-index:-1;pointer-events:none;overflow:hidden}
293
+ .orb{position:absolute;will-change:transform}
294
+ .orb.a{width:72vmax;height:72vmax;left:-16vmax;top:-22vmax;
295
+ background:radial-gradient(circle,rgba(121,44,247,.17),rgba(121,44,247,.05) 40%,transparent 67%);
296
+ animation:driftA 44s ease-in-out infinite alternate}
297
+ .orb.b{width:58vmax;height:58vmax;right:-18vmax;bottom:-16vmax;
298
+ background:radial-gradient(circle,rgba(121,44,247,.13),rgba(145,83,247,.04) 42%,transparent 68%);
299
+ animation:driftB 59s ease-in-out infinite alternate}
300
+ .orb.c{width:46vmax;height:46vmax;left:52%;top:28%;
301
+ background:radial-gradient(circle,rgba(145,83,247,.09),transparent 64%);
302
+ animation:driftC 71s ease-in-out infinite alternate}
303
+ @keyframes driftA{from{transform:translate3d(0,0,0) scale(1)}to{transform:translate3d(7vw,5vh,0) scale(1.14)}}
304
+ @keyframes driftB{from{transform:translate3d(0,0,0) scale(1.08)}to{transform:translate3d(-6vw,-4vh,0) scale(1)}}
305
+ @keyframes driftC{from{transform:translate3d(0,0,0) scale(.94);opacity:.75}to{transform:translate3d(-5vw,6vh,0) scale(1.1);opacity:1}}
306
+
307
+ @media (prefers-reduced-motion: reduce){
308
+ html{scroll-behavior:auto}
309
+ .rise,.spine,.n,.barfill{animation:none!important;opacity:1!important;transform:none!important}
310
+ .card{transition:none!important}
311
+ /* The field stays - it is colour, not motion - but it stops moving. */
312
+ .orb{animation:none!important}
313
+ }
314
+
315
+ /* ---------- nav + hero ---------- */
316
+ /* The header no longer carries its own gradient - the ambient field above
317
+ covers the whole page, and two purple washes stacked in the same corner
318
+ read as a smudge rather than as depth. */
319
+ header{position:relative;padding:0 0 clamp(54px,7vw,92px)}
320
+ ${NAV_CSS} .hero{position:relative;padding-top:clamp(38px,6vw,84px)}
321
+ h1{font-size:clamp(40px,7.4vw,86px);letter-spacing:-2.2px;margin:22px 0 0;max-width:15ch}
322
+ .lede{margin-top:24px;font-size:clamp(16px,1.9vw,19px);font-weight:300;max-width:60ch}
323
+
324
+ /* ---------- the command, and its copy control ---------- */
325
+ .cmdrow{margin-top:34px;display:flex;flex-wrap:wrap;gap:14px;align-items:center;max-width:100%}
326
+ /* min-width:0 on both the box and the text is what actually lets the command
327
+ scroll inside its own panel instead of pushing the copy button off a phone
328
+ screen - a flex item defaults to min-width:auto and refuses to shrink below
329
+ its content, which on a 390px viewport clipped the button entirely. */
330
+ .cmd{display:flex;align-items:center;gap:14px;padding:15px 16px 15px 22px;border-radius:13px;font-family:"IBM Plex Mono",monospace;font-size:clamp(13px,1.9vw,15.5px);max-width:100%;min-width:0}
331
+ .cmd .txt{white-space:nowrap;overflow-x:auto;scrollbar-width:none;min-width:0;flex:1 1 auto}
332
+ .cmd .txt::-webkit-scrollbar{display:none}
333
+ .copy{flex-shrink:0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border:1px solid var(--line);border-radius:9px;background:transparent;color:var(--mute);cursor:pointer;transition:color 180ms ease,border-color 180ms ease,background 180ms ease}
334
+ .copy:hover{color:var(--white);border-color:var(--brand-lift);background:var(--glass)}
335
+ .copy:focus-visible{outline:2px solid var(--brand-soft);outline-offset:2px}
336
+ .copy.ok{color:var(--brand-soft);border-color:var(--brand-soft)}
337
+ .copy svg{width:16px;height:16px}
338
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
339
+
340
+ /* ---------- the two labels ---------- */
341
+ /* Proven is filled, bright and unbroken. Guessed is hollow, dim and dashed.
342
+ Not one of those three is a hue. */
343
+ .chips{margin-top:30px;display:flex;flex-wrap:wrap;gap:12px}
344
+ .chip{display:flex;align-items:center;gap:11px;padding:12px 18px;border-radius:999px;font-size:13px;color:var(--mute)}
345
+ .chip.proven{border:1px solid var(--line);background:var(--glass)}
346
+ .chip.guessed{border:1px dashed rgba(245,245,245,.16)}
347
+ .dot{width:9px;height:9px;border-radius:50%;background:var(--brand);flex-shrink:0}
348
+ .ring{width:9px;height:9px;border-radius:50%;border:1.5px solid var(--faint);flex-shrink:0}
349
+ .chip .name{font-family:"IBM Plex Mono",monospace;font-size:13.5px}
350
+ .chip.proven .name{color:var(--white)}
351
+ .chip.guessed .name{color:var(--mute)}
352
+
353
+ .two{display:grid;grid-template-columns:1fr;gap:20px;margin-top:44px}
354
+ @media(min-width:880px){.two{grid-template-columns:1fr 1fr;gap:24px}}
355
+ /* The cards are buttons, so everything the browser puts on a button has to be
356
+ turned off before the card looks like a card again. */
357
+ .card{display:block;width:100%;text-align:left;font:inherit;color:inherit;cursor:pointer;
358
+ -webkit-appearance:none;appearance:none;padding:34px 32px 30px;border-radius:16px;
359
+ transition:transform 420ms cubic-bezier(.22,.9,.24,1),opacity 420ms ease,
360
+ border-color 420ms ease,box-shadow 420ms ease}
361
+ .card.proven{background:var(--panel);background:linear-gradient(180deg,var(--glass),rgba(245,245,245,.012)),var(--panel);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid var(--line)}
362
+ .card.guessed{background:transparent;border:1px dashed rgba(245,245,245,.14)}
363
+ .card .row{display:flex;align-items:center;gap:12px}
364
+ .card .body{display:block;margin-top:16px;font-size:15px;color:var(--mute)}
365
+ .quote{display:block;font-size:clamp(21px,2.6vw,27px);font-weight:500;letter-spacing:-.6px;line-height:1.24;color:var(--white);margin:22px 0 0}
366
+ .card.guessed .quote{color:var(--mute)}
367
+ .meta{display:block;border-top:1px solid var(--hair);margin-top:24px;padding-top:18px;font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--faint);line-height:1.9}
368
+
369
+ /* Hover only hints; the commitment is the click. */
370
+ @media(hover:hover){.card:hover{border-color:rgba(145,83,247,.45)}}
371
+ .card:focus-visible{outline:2px solid var(--brand-soft);outline-offset:3px}
372
+ /* Held: lifted, edged in the brand, with the light coming off it. */
373
+ .card.pop{transform:scale(1.022);border-color:var(--brand-lift);border-style:solid;
374
+ box-shadow:0 0 0 1px rgba(121,44,247,.3),0 22px 60px -26px rgba(121,44,247,.62)}
375
+ /* Let go: pushed back, never hidden. .42 keeps the text legible for anyone who
376
+ reads it anyway, since this is a de-emphasis the reader chose and can undo. */
377
+ .card.dim{opacity:.42;transform:scale(.99)}
378
+ .hint{margin-top:16px;font-size:13.5px;color:var(--faint)}
379
+
380
+ /* ---------- proof ---------- */
381
+ .proof{display:grid;grid-template-columns:1fr;gap:24px;margin-top:44px}
382
+ @media(min-width:980px){.proof{grid-template-columns:1.05fr .95fr;gap:30px}}
383
+ .term{border-radius:16px;overflow:hidden}
384
+ .term .bar{display:flex;align-items:center;gap:9px;padding:13px 18px;border-bottom:1px solid var(--line);background:rgba(245,245,245,.02)}
385
+ .term .bar i{width:9px;height:9px;border-radius:50%;background:rgba(245,245,245,.16)}
386
+ .term .body{padding:28px 24px;font-family:"IBM Plex Mono",monospace;font-size:clamp(12px,1.6vw,13.5px);line-height:2;color:var(--mute);overflow-x:auto}
387
+ .sev{color:var(--white);font-weight:500;letter-spacing:.5px}
388
+ .trace{position:relative;padding:32px 30px;border-radius:16px}
389
+ /* The spine is anchored to the hop list rather than to the card, so it starts
390
+ at the first node and stops at the last however the text rewraps. Pinned to
391
+ the card it needed two magic offsets and was wrong at every width but one. */
392
+ .hops{position:relative}
393
+ /* left/top/bottom are measured, not guessed: the nodes are 15px wide so their
394
+ centre line sits at 7.5px, and a 2px spine centres on it at 6.5px. The
395
+ first hop's top margin collapses out of .hops, which is why the first node
396
+ centre lands at 10.5 and not at 44.5. */
397
+ .spine{position:absolute;left:6.5px;top:10.5px;bottom:40px;width:2px;background:linear-gradient(180deg,var(--brand),rgba(121,44,247,.16));border-radius:2px}
398
+ .hop{position:relative;display:flex;gap:22px;align-items:flex-start;margin-top:34px}
399
+ .n{width:15px;height:15px;border-radius:50%;flex-shrink:0;margin-top:3px;background:var(--bg);border:2px solid var(--faint)}
400
+ .n.on{background:var(--brand);border-color:var(--brand)}
401
+
402
+ /* ---------- numbers ---------- */
403
+ .row{display:grid;grid-template-columns:150px 1fr 62px 62px;gap:14px;align-items:center;padding:20px 4px;border-bottom:1px solid var(--hair)}
404
+ @media(min-width:880px){.row{grid-template-columns:190px 1fr 86px 86px;gap:22px;padding:24px 8px}}
405
+ .track{flex-grow:1;height:8px;background:rgba(245,245,245,.06);border-radius:999px;overflow:hidden}
406
+ .pc{font-weight:700;font-size:clamp(21px,2.6vw,28px);letter-spacing:-.8px;min-width:78px}
407
+ .num{font-family:"IBM Plex Mono",monospace;font-size:13px;color:var(--mute);text-align:right}
408
+ .lbl{font-family:"IBM Plex Mono",monospace;font-size:13.5px;display:flex;align-items:center;gap:10px}
409
+ .foot{display:flex;gap:18px;margin-top:38px;padding-top:28px;border-top:1px solid var(--hair)}
410
+ .foot .rule{width:2px;flex-shrink:0;background:var(--brand);border-radius:2px}
411
+
412
+ /* ---------- stats + languages ---------- */
413
+ .stats{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:40px}
414
+ @media(min-width:760px){.stats{grid-template-columns:repeat(4,1fr);gap:18px}}
415
+ .stat{padding:24px 22px;border-radius:14px}
416
+ .stat b{display:block;font-weight:700;font-size:clamp(26px,3.2vw,34px);letter-spacing:-1px}
417
+ .stat span{display:block;margin-top:5px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;letter-spacing:1.6px;text-transform:uppercase;color:var(--faint)}
418
+ .langs{margin-top:22px;display:flex;flex-wrap:wrap;gap:9px}
419
+ .langs span{font-family:"IBM Plex Mono",monospace;font-size:12.5px;color:var(--mute);border:1px solid var(--hair);border-radius:7px;padding:7px 12px}
420
+
421
+ ${FOOTER_CSS}</style>
422
+ </head>
423
+ <body>
424
+
425
+ <div class="field" aria-hidden="true">
426
+ <div class="orb a"></div>
427
+ <div class="orb b"></div>
428
+ <div class="orb c"></div>
429
+ </div>
430
+
431
+ <header>
432
+ <div class="wrap">
433
+ ${siteNav('home')}
434
+
435
+ <div class="hero rise">
436
+ <div class="kicker">Static application security testing</div>
437
+ <h1>Every scanner says possible. This one says what it proved.</h1>
438
+ <p class="lede">Every finding carries one of two labels, and they are never blended. One matched a
439
+ pattern. The other traced attacker data from source to sink and prints every hop it walked, so you
440
+ can audit the claim instead of believing it.</p>
441
+
442
+ <div class="cmdrow">
443
+ <div class="cmd glass">
444
+ <span class="txt"><span style="color:var(--faint)">$</span>&nbsp; ${esc(INSTALL_COMMAND)}</span>
445
+ <button class="copy" id="copycmd" type="button" hidden
446
+ data-copy="${esc(INSTALL_COMMAND)}" aria-label="Copy the install command">
447
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
448
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
449
+ <rect x="9" y="9" width="11" height="11" rx="2.5"></rect>
450
+ <path d="M5 15V6a2.5 2.5 0 0 1 2.5-2.5H15"></path>
451
+ </svg>
452
+ </button>
453
+ </div>
454
+ <span class="mono" style="font-size:12px;color:var(--faint)">no install · no account · AGPL-3.0</span>
455
+ </div>
456
+ <span class="sr" id="copystate" role="status" aria-live="polite"></span>
457
+
458
+ <div class="chips">
459
+ <div class="chip proven"><span class="dot"></span><span class="name">flow-verified</span>a path that was walked</div>
460
+ <div class="chip guessed"><span class="ring"></span><span class="name">signature-based</span>a shape that matched</div>
461
+ </div>
462
+ </div>
463
+ </div>
464
+ </header>
465
+
466
+ <section id="finding">
467
+ <div class="wrap">
468
+ <div class="kicker">Before anything is claimed</div>
469
+ <h2>This is one finding,<br>printed whole.</h2>
470
+ <p style="margin-top:20px">Nothing below is summarised. The label, the path, and the limits of what the
471
+ engine could establish all ship together, because a claim without its working is just a louder guess.</p>
472
+
473
+ <div class="proof">
474
+ <div class="term glass">
475
+ <div class="bar"><i></i><i></i><i></i><span class="mono" style="font-size:12px;color:var(--faint);margin-left:10px">defuse scan .</span></div>
476
+ <div class="body">
477
+ <div><span class="sev">CRITICAL</span>&nbsp;&nbsp;<span style="color:var(--faint)">cmdi.c:44</span></div>
478
+ <div style="color:var(--white)">Attacker-controlled data from argv[1]</div>
479
+ <div style="color:var(--white)">reaches a shell, which runs the string</div>
480
+ <div style="margin-top:16px">command-injection &nbsp;·&nbsp; CWE-78</div>
481
+ <div><span class="dot" style="display:inline-block;margin-right:7px"></span><span style="color:var(--white)">[flow-verified]</span> <span style="color:var(--faint)">— 3 hops, printed below</span></div>
482
+ <div style="margin-top:20px;color:var(--faint)">limitations</div>
483
+ <div style="color:var(--faint);font-size:12.5px;line-height:1.85">confirms the data flow, not exploitability<br>in production. Does not model memory<br>safety in C: it tracks values, not sizes.</div>
484
+ </div>
485
+ </div>
486
+
487
+ <div class="trace glass">
488
+ <div class="kicker" style="margin-bottom:30px">The path it walked</div>
489
+ <div class="hops">
490
+ <div class="spine"></div>
491
+ <div class="hop">
492
+ <span class="n on n1"></span>
493
+ <div><div class="mono" style="font-size:14.5px;color:var(--white)">argv[1]</div><div style="margin-top:5px;font-size:14px;color:var(--mute)">a command-line argument — the source</div></div>
494
+ </div>
495
+ <div class="hop">
496
+ <span class="n on n2"></span>
497
+ <div><div class="mono" style="font-size:14.5px;color:var(--white)">sprintf(cmd, …)</div><div style="margin-top:5px;font-size:14px;color:var(--mute)">written into <code style="font-size:13px">cmd</code>, unescaped</div></div>
498
+ </div>
499
+ <div class="hop">
500
+ <span class="n on n3"></span>
501
+ <div><div class="mono" style="font-size:14.5px;color:var(--white)">system(cmd)</div><div style="margin-top:5px;font-size:14px;color:var(--mute)">arrives at the shell — the sink</div></div>
502
+ </div>
503
+ </div>
504
+ <p style="margin-top:34px;border-top:1px solid var(--hair);padding-top:18px;font-size:14px">Three hops,
505
+ every one printed. Where a hop rests on something the engine could not follow, that hop says so
506
+ rather than being smoothed over.</p>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </section>
511
+
512
+ <section id="labels">
513
+ <div class="wrap">
514
+ <div class="kicker">Two labels, never blurred</div>
515
+ <h2>A guess and a proof are<br>not the same sentence.</h2>
516
+ <p style="margin-top:20px">Most tools report everything at one confidence and leave the triage to you.
517
+ The word they hide behind is <em style="font-style:italic;color:var(--white)">possible</em> — and it
518
+ covers both a traced data path and a lucky-looking regex.</p>
519
+
520
+ <!--
521
+ Real <button>s, not divs wearing role="button". That buys keyboard
522
+ operation, focus, and the pressed state from the platform instead of from
523
+ script. The price is that a button may only contain phrasing content, so
524
+ every <p> and <div> inside became a <span style="display:block"> - same
525
+ rendering, valid markup.
526
+ -->
527
+ <div class="two" id="labelcards">
528
+ <button type="button" class="card proven" aria-pressed="false">
529
+ <span class="row"><span class="dot" style="width:11px;height:11px"></span><span class="mono" style="font-size:16px;color:var(--white)">flow-verified</span></span>
530
+ <span class="quote">“Attacker data actually reaches here.”</span>
531
+ <span class="body">Traced hop by hop, across functions and across files, with
532
+ the whole path printed beside the finding — including the steps where its knowledge ran out.</span>
533
+ <span class="meta">evidence &nbsp;·&nbsp; a printed source&rarr;sink path<br>claim &nbsp;&nbsp;&nbsp;&nbsp;·&nbsp; check it yourself</span>
534
+ </button>
535
+ <button type="button" class="card guessed" aria-pressed="false">
536
+ <span class="row"><span class="ring" style="width:11px;height:11px"></span><span class="mono" style="font-size:16px;color:var(--mute)">signature-based</span></span>
537
+ <span class="quote">“This code has the shape of a bug.”</span>
538
+ <span class="body">A pattern matched in the syntax tree. Data flow was not
539
+ traced. It is a lead worth opening, not a fact — and the report says exactly that, every time.</span>
540
+ <span class="meta">evidence &nbsp;·&nbsp; one matched pattern<br>claim &nbsp;&nbsp;&nbsp;&nbsp;·&nbsp; worth your attention</span>
541
+ </button>
542
+ </div>
543
+ <p class="hint" hidden>Pick one to hold it. Pick it again to let both go.</p>
544
+
545
+ <div style="display:flex;gap:16px;align-items:flex-start;margin-top:40px">
546
+ <svg viewBox="0 0 24 24" style="width:20px;height:20px;flex-shrink:0;margin-top:3px" aria-hidden="true"><path d="M12 3l7 3.5v5c0 4.2-2.9 7.8-7 9-4.1-1.2-7-4.8-7-9v-5z" fill="none" stroke="#9153F7" stroke-width="1.5" stroke-linejoin="round"></path></svg>
547
+ <p style="font-size:16.5px;max-width:68ch;color:var(--white)">A rule cannot set its own confidence. Only a
548
+ printed path earns the filled label — and that is enforced by the type system, not by a style guide.</p>
549
+ </div>
550
+ </div>
551
+ </section>
552
+
553
+ <section id="numbers">
554
+ <div class="wrap">
555
+ <div class="kicker">Measured, and published whole</div>
556
+ <h2>What the filled label<br>is actually worth.</h2>
557
+ <p style="margin-top:20px">Scored against OWASP BenchmarkJava, ${benchmark.scored} labelled test cases, on
558
+ engine ${esc(benchmark.engineVersion)} (${esc(split.measuredAt)}). Each label is scored
559
+ <em style="font-style:italic;color:var(--white)">separately</em>, because one blended figure would be the
560
+ same averaging this tool refuses to do inside a single finding.</p>
561
+
562
+ <div style="margin-top:38px">
563
+ <div class="row" style="padding-bottom:10px">
564
+ <span class="mono" style="font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--faint)">tier</span>
565
+ <span class="mono" style="font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--faint)">precision</span>
566
+ <span class="num" style="font-size:11px;letter-spacing:2px;text-transform:uppercase">found</span>
567
+ <span class="num" style="font-size:11px;letter-spacing:2px;text-transform:uppercase">wrong</span>
568
+ </div>
569
+
570
+ <div class="row">
571
+ <span class="lbl" style="color:var(--white)"><span class="dot"></span>flow-verified</span>
572
+ <div style="display:flex;align-items:center;gap:16px">
573
+ <div class="track"><div class="barfill" style="width:${bar(verified.precision)}%;height:8px;background:var(--brand);border-radius:999px"></div></div>
574
+ <span class="pc">${esc(verified.precision)}</span>
575
+ </div>
576
+ <span class="num">${verified.tp}</span><span class="num">${verified.fp}</span>
577
+ </div>
578
+
579
+ <div class="row">
580
+ <span class="lbl" style="color:var(--mute)"><span class="ring"></span>signature-based</span>
581
+ <div style="display:flex;align-items:center;gap:16px">
582
+ <div class="track"><div class="barfill" style="width:${bar(signature.precision)}%;height:8px;background:rgba(245,245,245,.22);border-radius:999px;animation-delay:150ms"></div></div>
583
+ <span class="pc" style="color:var(--mute)">${esc(signature.precision)}</span>
584
+ </div>
585
+ <span class="num">${signature.tp}</span><span class="num">${signature.fp}</span>
586
+ </div>
587
+
588
+ <div class="row">
589
+ <span class="lbl" style="color:var(--faint)">both combined</span>
590
+ <div style="display:flex;align-items:center;gap:16px">
591
+ <div class="track"><div class="barfill" style="width:${bar(benchmark.precision)}%;height:8px;background:rgba(121,44,247,.45);border-radius:999px;animation-delay:300ms"></div></div>
592
+ <span class="pc" style="color:var(--mute)">${esc(benchmark.precision)}</span>
593
+ </div>
594
+ <span class="num">${benchmark.tp}</span><span class="num">${benchmark.fp}</span>
595
+ </div>
596
+ </div>
597
+
598
+ <p style="margin-top:32px;font-size:19px;color:var(--white);max-width:68ch">The filled label is worth
599
+ <span style="font-weight:700;font-size:27px;letter-spacing:-1px;color:var(--brand-soft)">${gap}</span>
600
+ points of precision over the hollow one. That gap is the entire claim this tool makes, and it is measured
601
+ here rather than asserted.</p>
602
+
603
+ <div class="foot">
604
+ <div class="rule"></div>
605
+ <div>
606
+ <div class="mono" style="font-size:11px;letter-spacing:2.2px;text-transform:uppercase;color:var(--brand-soft)">The footnote, printed rather than buried</div>
607
+ ${
608
+ /*
609
+ * Three shapes, chosen by the measurement. This paragraph once read
610
+ * "0 of the 0 false positives (0%) are decoys. That leaves 0 ordinary
611
+ * mistakes" - a template filled with zeros is not a sentence.
612
+ */
613
+ verified.fp === 0
614
+ ? `<p style="margin-top:12px;font-size:15px;max-width:72ch">No false positive wears the filled
615
+ label on this benchmark: all <span style="color:var(--white)">${verified.tp}</span>
616
+ <code style="font-size:14px;color:var(--white)">flow-verified</code> claims are real
617
+ vulnerabilities. That is a fact about these ${benchmark.scored.toLocaleString('en-US')} test cases, not a
618
+ promise about your code — a proof means the path exists in the source, and the ways it can still be
619
+ wrong are in the limitations below, which a synthetic benchmark does not exercise.</p>`
620
+ : verified.decoyFalsePositives === 0
621
+ ? `<p style="margin-top:12px;font-size:15px;max-width:72ch">None of the ${verified.fp} false
622
+ positives still wearing the filled label is a constant-branch decoy. They are
623
+ <span style="color:var(--white)">${verified.nonDecoyFalsePositives}</span> ordinary mistakes, and
624
+ <code style="font-size:14px;color:var(--white)">flow-verified</code> precision is ${exclDecoy}% with
625
+ nothing set aside.</p>`
626
+ : `<p style="margin-top:12px;font-size:15px;max-width:72ch">${verified.decoyFalsePositives} of the
627
+ ${verified.fp} false positives still wearing the filled label (${esc(verified.decoyShare)}) are the
628
+ benchmark’s own constant-branch decoys — a data path that genuinely exists, inside a branch that can
629
+ never run. That leaves <span style="color:var(--white)">${verified.nonDecoyFalsePositives}</span>
630
+ ordinary mistakes, which would put <code style="font-size:14px;color:var(--white)">flow-verified</code>
631
+ precision at ${exclDecoy}%. Both numbers are here because neither alone is the truth: the first is
632
+ contaminated by synthetic traps, the second needs cases excluded, and you deserve to see the size of
633
+ that choice rather than inherit it.</p>`}
634
+ </div>
635
+ </div>
636
+ </div>
637
+ </section>
638
+
639
+ <section id="gaps">
640
+ <div class="wrap">
641
+ <div class="kicker">The part nobody ships</div>
642
+ <h2>It prints what it<br>didn’t check.</h2>
643
+ <p style="margin-top:20px">Every run ends with the rule and language combinations this engine does not
644
+ implement — currently ${notImplemented} of them — each with the real reason, not a placeholder. In C it
645
+ states outright that it does not model memory safety: no buffer overflows, no use-after-free. It tracks
646
+ values, not sizes.</p>
647
+ <p style="margin-top:14px">A gap you can read is a gap you can plan around. A gap you can’t see is the one
648
+ that gets you.</p>
649
+
650
+ <div class="stats">
651
+ <div class="stat glass"><b>${LANGUAGES.length}</b><span>languages</span></div>
652
+ <div class="stat glass"><b>${ruleCount}</b><span>rules</span></div>
653
+ <div class="stat glass"><b>${benchmark.filesParsed}</b><span>files scored</span></div>
654
+ <div class="stat glass"><b>AGPL</b><span>3.0 · or commercial</span></div>
655
+ </div>
656
+ <div class="langs">${languageNames.map((n) => `<span>${esc(n)}</span>`).join('')}</div>
657
+ </div>
658
+ </section>
659
+
660
+ <section id="cost">
661
+ <div class="wrap">
662
+ <div class="kicker">What it costs</div>
663
+ <h2>Free, and specific<br>about what that means.</h2>
664
+ <p style="margin-top:20px">The scanner is AGPL-3.0. Pointing it at your own code — closed-source,
665
+ proprietary, commercial, any of it — puts no obligation on you whatsoever, and running it unmodified
666
+ inside a company is free and unencumbered.</p>
667
+ <p style="margin-top:14px">The one obligation attaches if you modify it and then serve your modified
668
+ version to other people over a network, in which case those users are owed your source. If those terms
669
+ do not work for your company, and for some they genuinely do not, the copyright sits in one place and a
670
+ commercial licence can be discussed.</p>
671
+ <p style="margin-top:14px">The scanner is the whole of the free plan, not a trial of it. Tools for working
672
+ through findings over time, and across a team, are paid - <a href="pricing.html">see plans and pricing</a>.</p>
673
+ </div>
674
+ </section>
675
+
676
+ <div class="wrap">
677
+ ${siteFooter()}
678
+ </div>
679
+
680
+ <script>
681
+ /* The copy button starts hidden and is revealed only once we know the clipboard
682
+ is reachable, so a browser without it shows no dead control - the command is
683
+ plain selectable text either way, and the page needs no script to be usable. */
684
+ (function () {
685
+ var btn = document.getElementById('copycmd');
686
+ var live = document.getElementById('copystate');
687
+ if (!btn || !navigator.clipboard) return;
688
+ var idle = btn.innerHTML;
689
+ var done = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"></path></svg>';
690
+ var timer;
691
+ btn.hidden = false;
692
+ btn.addEventListener('click', function () {
693
+ navigator.clipboard.writeText(btn.getAttribute('data-copy')).then(function () {
694
+ clearTimeout(timer);
695
+ btn.innerHTML = done;
696
+ btn.classList.add('ok');
697
+ if (live) live.textContent = 'Install command copied';
698
+ timer = setTimeout(function () {
699
+ btn.innerHTML = idle;
700
+ btn.classList.remove('ok');
701
+ if (live) live.textContent = '';
702
+ }, 1800);
703
+ });
704
+ });
705
+ })();
706
+
707
+ /* Hold one label, push the other back.
708
+ The hint stays hidden until this runs, so a reader with no JavaScript is
709
+ never told to click something that will not respond. The cards are real
710
+ buttons, so Enter and Space already work and aria-pressed carries the state
711
+ to a screen reader - all this adds is the two classes. */
712
+ (function () {
713
+ var wrap = document.getElementById('labelcards');
714
+ if (!wrap) return;
715
+ var cards = Array.prototype.slice.call(wrap.querySelectorAll('.card'));
716
+ if (cards.length < 2) return;
717
+ var hint = document.querySelector('.hint');
718
+ if (hint) hint.hidden = false;
719
+
720
+ cards.forEach(function (card) {
721
+ card.addEventListener('click', function () {
722
+ var alreadyHeld = card.classList.contains('pop');
723
+ cards.forEach(function (c) {
724
+ c.classList.remove('pop', 'dim');
725
+ c.setAttribute('aria-pressed', 'false');
726
+ });
727
+ if (alreadyHeld) return; // a second click on the held card releases both
728
+ card.classList.add('pop');
729
+ card.setAttribute('aria-pressed', 'true');
730
+ cards.forEach(function (c) {
731
+ if (c !== card) c.classList.add('dim');
732
+ });
733
+ });
734
+ });
735
+ })();
736
+ </script>
737
+
738
+ </body>
739
+ </html>
740
+ `;
741
+ }
742
+ //# sourceMappingURL=site.js.map