@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,189 @@
1
+ /**
2
+ * MACHINE-READABLE REPORT
3
+ *
4
+ * Two rules for this format:
5
+ *
6
+ * 1. It is a SUPERSET of the human report. Anything the terminal shows must be
7
+ * in here, including the gaps and the suppressions. A JSON consumer must not
8
+ * be able to build a rosier picture than a human reading the same scan.
9
+ *
10
+ * 2. `confidence` is a top-level field on every finding, and the `honesty`
11
+ * block at the document root explains what the values mean. A dashboard that
12
+ * ingests this cannot accidentally present unverified matches as confirmed
13
+ * vulnerabilities without deliberately discarding the field.
14
+ *
15
+ * RELATIONSHIP TO SARIF: this shape deliberately resembles SARIF (the OASIS
16
+ * standard GitHub code scanning consumes) without pretending to be it, because
17
+ * emitting invalid SARIF and calling it SARIF would be its own small lie. Real
18
+ * SARIF now exists behind `--sarif` (report/sarif.ts), typed against the
19
+ * official 2.1.0 definitions so the compiler rejects a malformed document.
20
+ * This format stays because it carries things SARIF has no room for - the
21
+ * coverage matrix, verified-clean lines, trace limits and the score gauges.
22
+ */
23
+
24
+ import type { ScanResult } from '../engine/scan.js';
25
+ import { LICENCE_SPDX } from '../core/licence.js';
26
+
27
+ export function renderJson(result: ScanResult, pretty = true): string {
28
+ const bySeverity: Record<string, number> = {};
29
+ for (const finding of result.findings) {
30
+ bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
31
+ }
32
+
33
+ const document = {
34
+ tool: {
35
+ name: 'Defuse',
36
+ version: result.coverage.engine.version,
37
+ analysis: result.coverage.engine.analysisLabel,
38
+ // Taken from the capability block rather than retyped here. This list was
39
+ // hand-written once and went stale immediately - it still claimed the
40
+ // tracer worked "within one function (JS/TS/Python)" long after it
41
+ // followed values across files in every language. A machine-readable
42
+ // report that overstates OR understates the engine is the same bug.
43
+ analysisTypes: result.coverage.engine.implemented,
44
+ // Machine-readable, because a consumer feeding this into a compliance
45
+ // pipeline should not have to read a README to learn the terms.
46
+ license: LICENCE_SPDX,
47
+ },
48
+ target: result.target,
49
+ scannedAt: new Date().toISOString(),
50
+
51
+ // Placed BEFORE findings on purpose: a consumer reading top-to-bottom meets
52
+ // the caveats before the results.
53
+ honesty: {
54
+ signatureBasedFindings: result.findings.filter((f) => f.confidence === 'signature-based')
55
+ .length,
56
+ flowVerifiedFindings: result.findings.filter((f) => f.confidence === 'flow-verified').length,
57
+ signatureGuessesSupersededByProof: result.stats.signaturesUpgraded,
58
+ signatureGuessesWithdrawnAsSanitised: result.stats.signaturesRetracted,
59
+ /*
60
+ * Withdrawn because every value spliced in is provably a fixed literal on
61
+ * every path that runs - a branch that cannot run, a condition decided at
62
+ * compile time, a same-file helper that only ever returns a literal. Each
63
+ * one is listed under diagnostics.verifiedClean with kind "constant" and
64
+ * the reason, so the reasoning can be checked line by line.
65
+ */
66
+ signatureGuessesWithdrawnAsConstant: result.stats.signaturesProvedConstant,
67
+ /*
68
+ * Branches and cases the tracer did not follow because a condition fixed
69
+ * at compile time means they cannot run. A flow that lived only inside one
70
+ * of them is not reported - so each is listed under
71
+ * diagnostics.unreachableCodeSkipped, with the decision, rather than
72
+ * leaving a missing finding with no explanation.
73
+ */
74
+ regionsProvedUnreachable: result.stats.regionsProvedUnreachable,
75
+ /*
76
+ * Reads from a local map or list answered by the key or the position,
77
+ * because every write the collection could receive is visible (see
78
+ * taint/local-map.ts) - each listed under diagnostics.collectionReadsResolved
79
+ * with the write that answers it.
80
+ */
81
+ collectionReadsResolved: result.stats.collectionReadsResolved,
82
+ /*
83
+ * Calls where a trace ENDED because the function returns new data - a
84
+ * database row, a file's contents - not its argument. Not a claim of
85
+ * safety: nothing is cleared by a stop, and a sink further on is still
86
+ * reported by its signature rule. Each is listed under
87
+ * diagnostics.tracesStoppedAtNewData with what the call returns instead.
88
+ */
89
+ tracesStoppedAtNewData: result.stats.tracesStoppedAtNewData,
90
+ meaningOfConfidence: result.coverage.engine.meaningOfConfidence,
91
+ notImplemented: result.coverage.engine.notImplemented,
92
+ dataFlowCoverageByLanguage: result.coverage.taint,
93
+ crossFileResolution: result.crossFile,
94
+ warning:
95
+ 'Findings labelled signature-based are UNVERIFIED pattern matches - the data flow was ' +
96
+ 'not traced and they may be false positives. Findings labelled flow-verified carry a ' +
97
+ 'complete source-to-sink path in `flowPath`; check it rather than trusting the label. ' +
98
+ 'Neither label proves exploitability in a running system.',
99
+ },
100
+
101
+ summary: {
102
+ total: result.findings.length,
103
+ bySeverity,
104
+ byConfidence: {
105
+ 'signature-based': result.findings.filter((f) => f.confidence === 'signature-based').length,
106
+ 'flow-verified': result.findings.filter((f) => f.confidence === 'flow-verified').length,
107
+ },
108
+ suppressed: result.suppressions.length,
109
+ },
110
+
111
+ findings: result.findings.map((finding) => ({
112
+ ruleId: finding.ruleId,
113
+ ruleName: finding.ruleName,
114
+ severity: finding.severity,
115
+ confidence: finding.confidence,
116
+ verified: finding.verified,
117
+ flowPath: finding.flowPath,
118
+ cwe: finding.cwe,
119
+ owasp: finding.owasp,
120
+ message: finding.message,
121
+ reasoning: finding.reasoning,
122
+ limitations: finding.limitations,
123
+ location: finding.location,
124
+ detectedAt: finding.detectedAt,
125
+ })),
126
+
127
+ coverage: {
128
+ counts: result.coverage.counts,
129
+ gaps: result.coverage.gaps,
130
+ matrix: result.coverage.matrix,
131
+ fileTypesPresentButNotScanned: result.coverage.unscannedExtensions,
132
+ },
133
+
134
+ diagnostics: {
135
+ stats: result.stats,
136
+ filesByLanguage: result.byLanguage,
137
+ parseProblems: result.parseProblems,
138
+ suppressions: result.suppressions,
139
+ /*
140
+ * Every line proved clean, each with `reason` - what cleaned it and on
141
+ * which line - and, when there were any, `assumes`: functions Defuse
142
+ * does not model that the value went through AFTER it was cleaned, each
143
+ * taken to hand back what it was given. A tool reading this file can
144
+ * hold the receipts that rest on trust to a different standard.
145
+ */
146
+ verifiedClean: result.verifiedClean,
147
+ unreachableCodeSkipped: result.unreachableCode,
148
+ collectionReadsResolved: result.collectionReadsResolved,
149
+ tracesStoppedAtNewData: result.tracesStoppedAtNewData,
150
+ /*
151
+ * Which PHP the project declares (Dockerfile, compose, composer.json,
152
+ * .php-version) and what php.net's facts say about each - with the date
153
+ * those facts were checked, and notes for what could not be checked.
154
+ * Null when --only left the check out.
155
+ */
156
+ runtimeVersions: result.runtimeVersions
157
+ ? {
158
+ factsChecked: result.runtimeVersions.factsChecked,
159
+ declared: result.runtimeVersions.declared,
160
+ notes: result.runtimeVersions.notes,
161
+ }
162
+ : null,
163
+ /** What `--deps` read and asked osv.dev; null when it did not run. */
164
+ dependencies: result.dependencies ?? null,
165
+ oversizedFilesSkipped: result.oversizedFiles,
166
+ excludedByUser: result.excluded,
167
+ /**
168
+ * What the scan spent on syntax trees.
169
+ *
170
+ * `reparses` above zero means the project did not fit in the tree budget
171
+ * and files were parsed more than once. The findings are unaffected - a
172
+ * re-parse of the same bytes with the same grammar is the same tree - but
173
+ * the scan took longer than it needed to, and saying so is the difference
174
+ * between a user tuning the budget and a user assuming the tool is slow.
175
+ */
176
+ treeMemory: {
177
+ ...result.treeMemory,
178
+ note:
179
+ result.treeMemory.reparses === 0
180
+ ? 'Every file was parsed exactly once; the project fitted in the tree budget.'
181
+ : `The project did not fit in the tree budget, so ${result.treeMemory.reparses} ` +
182
+ 'file(s) were parsed more than once. Answers are unchanged; the scan was slower. ' +
183
+ 'Raise DEFUSE_TREE_BUDGET_MB to trade memory back for speed.',
184
+ },
185
+ },
186
+ };
187
+
188
+ return JSON.stringify(document, null, pretty ? 2 : 0);
189
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * THE MARK, AS A VECTOR.
3
+ *
4
+ * docs/defuse-mark.png is the source of truth for the mark, and it is not in
5
+ * the npm package - so a report written by an installed copy of the CLI could
6
+ * not include it. This is the same shape traced to a path (potrace, from the PNG
7
+ * at 4x), small enough to inline, so a report file carries its own mark and
8
+ * loads nothing from anywhere.
9
+ */
10
+
11
+ /** #792CF7 - the mark's own colour, sampled from the PNG. */
12
+ export const MARK_COLOUR = '#792CF7';
13
+
14
+ const MARK_PATH =
15
+ 'M162 9580 c-7 -11 -18 -20 -26 -20 -17 0 -96 -78 -96 -95 0 -6 -9 -20 -20 -30 -20 -18 -20 -31 -20 -2350 0 -2009 2 -2334 14 -2338 8 -3 22 -22 31 -41 22 -47 83 -94 142 -112 40 -12 240 -14 1227 -14 1123 1 1181 0 1187 -17 12 -29 10 -1733 -1 -1754 -10 -18 -51 -18 -1203 -19 l-1192 0 -55 -28 c-37 -18 -66 -42 -87 -72 -18 -25 -39 -53 -48 -62 -13 -16 -15 -154 -15 -1231 0 -1201 0 -1214 20 -1232 11 -10 20 -22 20 -27 0 -20 48 -72 88 -95 23 -14 42 -29 42 -34 0 -5 501 -9 1225 -9 l1224 0 25 23 c14 12 41 35 61 51 20 16 47 51 61 77 l24 48 0 1018 c0 560 4 1024 8 1031 7 10 249 12 1208 13 l1199 0 5 -1028 c5 -1108 3 -1061 58 -1128 14 -16 41 -41 61 -55 20 -14 40 -31 44 -37 13 -20 702 -18 749 2 19 7 84 19 144 25 172 16 341 48 589 111 186 48 206 54 515 164 25 8 54 20 65 25 11 5 72 31 135 59 134 59 386 186 478 241 17 11 47 29 65 40 17 11 48 29 67 40 59 35 154 98 271 182 163 115 355 277 559 468 283 266 641 727 833 1073 9 18 35 64 57 102 59 104 220 441 220 460 0 4 8 26 19 49 21 48 28 65 91 251 60 180 90 277 90 293 0 8 8 44 19 80 43 153 83 375 102 567 6 61 17 125 25 144 20 49 20 757 0 816 -8 22 -19 92 -25 155 -26 275 -107 634 -210 935 -67 197 -79 229 -95 260 -9 17 -16 34 -16 39 0 5 -9 30 -21 55 -12 25 -29 64 -39 86 -41 90 -123 254 -160 320 -22 39 -48 84 -57 102 -66 118 -166 273 -293 451 -220 311 -578 681 -920 952 -162 129 -178 140 -340 247 -63 42 -127 84 -142 94 -15 11 -30 19 -34 19 -4 0 -15 6 -23 14 -52 45 -492 262 -676 333 -22 8 -55 21 -72 29 -29 12 -228 79 -353 118 -25 8 -94 26 -155 40 -60 15 -137 33 -170 42 -91 22 -279 52 -406 65 -63 6 -129 17 -148 25 -28 12 -472 14 -2985 14 -2940 0 -2951 0 -2964 -20z m5648 -2648 c153 -13 203 -20 340 -51 111 -25 188 -48 227 -66 17 -8 35 -15 40 -15 47 0 389 -183 494 -265 15 -11 39 -29 55 -40 114 -80 352 -322 434 -441 36 -52 108 -162 127 -194 42 -70 153 -303 153 -323 1 -6 9 -30 20 -52 11 -22 19 -46 20 -54 0 -8 9 -44 20 -80 98 -330 97 -754 -2 -1111 -113 -409 -365 -796 -693 -1068 -44 -37 -82 -69 -85 -72 -3 -3 -16 -13 -30 -22 -14 -10 -44 -31 -67 -47 -163 -116 -433 -240 -628 -290 -235 -60 -306 -68 -645 -73 l-185 -3 -5 1095 -5 1095 -27 52 c-33 61 -103 122 -155 134 -21 5 -572 9 -1225 9 l-1188 0 -5 23 c-3 12 -4 435 -3 940 3 803 5 919 18 927 23 14 2817 7 3000 -8z';
16
+
17
+ /** An inline <svg> of the mark, `width` pixels wide; height follows the 262:240 shape. */
18
+ export function markSvg(width: number, title = ''): string {
19
+ const height = Math.round(((width * 240) / 262) * 100) / 100;
20
+ const label = title ? ` role="img" aria-label="${title}"` : ' aria-hidden="true"';
21
+ return (
22
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1048.12 960.13" width="${width}" height="${height}"${label}>` +
23
+ `<g transform="translate(0,960) scale(0.1,-0.1)" fill="${MARK_COLOUR}"><path d="${MARK_PATH}"/></g></svg>`
24
+ );
25
+ }
26
+
27
+ /*
28
+ * THE MARK, AS BLOCKS - the terminal banner.
29
+ *
30
+ * "DEFUSE" in chunky block letters with a box-drawing shadow.
31
+ */
32
+ const BANNER = [
33
+ '██████╗ ███████╗███████╗██╗ ██╗███████╗███████╗',
34
+ '██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝',
35
+ '██║ ██║█████╗ █████╗ ██║ ██║███████╗█████╗',
36
+ '██║ ██║██╔══╝ ██╔══╝ ██║ ██║╚════██║██╔══╝',
37
+ '██████╔╝███████╗██║ ╚██████╔╝███████║███████╗',
38
+ '╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚══════╝╚══════╝',
39
+ ];
40
+
41
+ /** Columns the banner needs. */
42
+ export const BANNER_WIDTH = Math.max(...BANNER.map((l) => l.length));
43
+
44
+ /** The banner as plain lines. */
45
+ export function bannerLines(): string[] {
46
+ return [...BANNER];
47
+ }
@@ -0,0 +1,374 @@
1
+ import { ALL_RULES } from '../rules/registry.js';
2
+ import { LANGUAGES } from '../parse/languages.js';
3
+ import type { BenchmarkResult } from './coverage-table.js';
4
+ import { BASE_CSS, FOOTER_CSS, NAV_CSS, esc, siteFooter, siteNav } from './site.js';
5
+
6
+ /**
7
+ * THE PRICING PAGE
8
+ * ================
9
+ *
10
+ * Generated like the landing page, and held to the same rule: it may not say
11
+ * more than is true today.
12
+ *
13
+ * - Every feature listed under a plan EXISTS. Nothing planned is listed as
14
+ * if it were built; a plan that is not built yet is shown as "coming soon"
15
+ * with no price and no feature list.
16
+ * - Numbers come from the registry and the measurement files, not from the
17
+ * prose, so `npm test` fails if the page and the engine disagree.
18
+ * - No "most popular" badge. There is nothing to be popular among yet, and a
19
+ * badge that says otherwise is the kind of claim this project exists to
20
+ * refuse. The cards do carry a badge, because the design has a place for
21
+ * one - and it holds a FACT about the plan ("Open source", "5+ developers"),
22
+ * never a ranking.
23
+ * - How buying works is said plainly: there is no checkout. A plan is set up
24
+ * by hand after someone asks.
25
+ *
26
+ * The paid tools are Defuse Team, a separate private program that reads the
27
+ * scanner's JSON. They organise findings; they never re-decide one. That is
28
+ * stated on the page, because it is the promise a buyer most needs.
29
+ */
30
+
31
+ /** Where a plan enquiry goes: a public GitHub issue form, .github/ISSUE_TEMPLATE/plan-enquiry.yml. */
32
+ const ENQUIRY_URL = 'https://github.com/Sikkuumi/Defuse/issues/new?template=plan-enquiry.yml';
33
+ const enquiry = (plan: string): string => `${ENQUIRY_URL}&title=${encodeURIComponent(`Plan enquiry: ${plan}`)}`;
34
+
35
+ export interface PlanFeature {
36
+ readonly name: string;
37
+ readonly detail: string;
38
+ }
39
+
40
+ export interface Plan {
41
+ readonly name: string;
42
+ readonly status: 'available' | 'coming-soon';
43
+ /** Shown only for an available plan. */
44
+ readonly price?: { readonly amount: string; readonly unit: string; readonly note?: string };
45
+ readonly forWhom: string;
46
+ /**
47
+ * A fact about the plan, shown in the corner of its card. A fact, never a
48
+ * ranking: "open source", "from 5 developers". See the note above on badges.
49
+ */
50
+ readonly badge?: string;
51
+ /** "Everything in Pro, and:" - the plan below this one. */
52
+ readonly builds?: string;
53
+ readonly features: readonly PlanFeature[];
54
+ readonly cta: { readonly label: string; readonly href: string; readonly primary?: boolean };
55
+ }
56
+
57
+ /** Team is priced per developer with a floor of this many. */
58
+ export const TEAM_MIN_SEATS = 5;
59
+ const TEAM_PRICE = 49;
60
+
61
+ /** Rule/language pairs the engine declares it does not implement - counted, never typed in. */
62
+ function countNotImplemented(): number {
63
+ let n = 0;
64
+ for (const rule of ALL_RULES) {
65
+ for (const language of LANGUAGES) {
66
+ if (rule.support[language.id]?.status === 'not-implemented') n++;
67
+ }
68
+ }
69
+ return n;
70
+ }
71
+
72
+ export function plans(): Plan[] {
73
+ const notImplemented = countNotImplemented();
74
+ return [
75
+ {
76
+ name: 'Community',
77
+ status: 'available',
78
+ price: { amount: 'Free', unit: 'for everyone, for good', note: 'Open source, AGPL-3.0' },
79
+ forWhom: 'Anyone scanning their own code, on their own machine or in their own CI.',
80
+ badge: 'Open source',
81
+ builds: 'Included:',
82
+ features: [
83
+ {
84
+ name: 'The scanner',
85
+ detail: `${LANGUAGES.length} languages and ${ALL_RULES.length} rules. Data flow is traced in every language, into other functions, and into other files in every language but PHP.`,
86
+ },
87
+ {
88
+ name: 'Two labels on every finding',
89
+ detail: 'Proven, with the path from source to sink printed; or to review, a matched pattern that was not traced. Never blended into one score.',
90
+ },
91
+ {
92
+ name: 'What it did not check',
93
+ detail: `Every run lists the rule and language pairs it does not cover - ${notImplemented} today - each with its reason.`,
94
+ },
95
+ {
96
+ name: 'Terminal, browser and CI',
97
+ detail: 'A terminal command, a browser page that scans on your own machine and uploads nothing, JSON output, and SARIF for GitHub code scanning.',
98
+ },
99
+ {
100
+ name: 'Client report',
101
+ detail: 'One HTML file for a client or a manager, with the proven problems, what to review and what was not checked. Prints to PDF.',
102
+ },
103
+ ],
104
+ cta: { label: 'Run it now', href: 'https://github.com/Sikkuumi/Defuse#readme' },
105
+ },
106
+ {
107
+ name: 'Pro',
108
+ status: 'available',
109
+ price: { amount: '$19', unit: 'per developer / month' },
110
+ forWhom: 'One developer working through findings over time, on their own machine.',
111
+ badge: 'One developer',
112
+ builds: 'Everything in Community, and:',
113
+ features: [
114
+ {
115
+ name: 'New problems only',
116
+ detail: 'Compare two scans and see only what a change added. A line that moved is matched, not called new; what disappeared is "no longer reported", never "fixed".',
117
+ },
118
+ {
119
+ name: 'Trend dashboard',
120
+ detail: 'Proven and to-review counts across your scans, in one file that opens offline. Where the scanner itself changed, the point is marked with the reason.',
121
+ },
122
+ ],
123
+ cta: { label: 'Ask for Pro', href: enquiry('Pro') },
124
+ },
125
+ {
126
+ name: 'Team',
127
+ status: 'available',
128
+ price: {
129
+ amount: `$${TEAM_PRICE}`,
130
+ unit: 'per developer / month',
131
+ note: `From ${TEAM_MIN_SEATS} developers ($${TEAM_PRICE * TEAM_MIN_SEATS} a month).`,
132
+ },
133
+ forWhom: 'A team that wants every pull request checked, and one shared record of what was decided.',
134
+ badge: `${TEAM_MIN_SEATS}+ developers`,
135
+ builds: 'Everything in Pro, and:',
136
+ features: [
137
+ {
138
+ name: 'Pull-request check',
139
+ detail: 'Each pull request is scanned against its base. One comment, updated on every push, lists only what it adds, and the check fails only on new problems at the level you set.',
140
+ },
141
+ {
142
+ name: 'Shared triage',
143
+ detail: 'False alarms and accepted risks are marked in the repository with a reason and a name. A mark never hides a finding, ends when the code changes, and a pull request cannot approve its own.',
144
+ },
145
+ {
146
+ name: 'Automatic history',
147
+ detail: 'One scan a day kept on its own branch, and the trend dashboard rebuilt on every push to main. On private repositories, unless you choose otherwise.',
148
+ },
149
+ ],
150
+ cta: { label: 'Ask for Team', href: enquiry('Team'), primary: true },
151
+ },
152
+ /*
153
+ * Business and Enterprise were listed here as "coming soon", each with a
154
+ * "Tell us you need it" button. Removed: a button for a plan that does not
155
+ * exist only brings requests nobody can fill. A plan is added back when it
156
+ * is built - and if one is listed before then, it is drawn as "coming
157
+ * soon", with no price and no features (planCard).
158
+ */
159
+ ];
160
+ }
161
+
162
+ /** Lucide's "check" icon (ISC licence), inline so the page still calls nobody. */
163
+ const CHECK_ICON =
164
+ '<svg class="tick" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.25" ' +
165
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
166
+
167
+ function planCard(p: Plan): string {
168
+ if (p.status === 'coming-soon') {
169
+ return ` <div class="plan soon">
170
+ <h3 class="pname">${esc(p.name)}</h3>
171
+ <div class="tag">Coming soon</div>
172
+ <p class="who">${esc(p.forWhom)}</p>
173
+ <p class="who">Not available yet. If you need it, say so - it will be shaped around real requests rather than guessed at.</p>
174
+ <a class="pbtn" href="${esc(p.cta.href)}">${esc(p.cta.label)}</a>
175
+ </div>`;
176
+ }
177
+ const price = p.price!;
178
+ // One card: a header (name, who it is for, a fact in the corner), the price
179
+ // on one line, the features as a checked list, and one full-width button.
180
+ // The plan whose button is the primary one gets a brand-coloured ring.
181
+ return ` <div class="plan glass${p.cta.primary ? ' featured' : ''}">
182
+ <div class="phead">
183
+ <h3 class="pname">${esc(p.name)}</h3>
184
+ ${p.badge ? `<span class="badge">${esc(p.badge)}</span>` : ''}
185
+ <p class="who">${esc(p.forWhom)}</p>
186
+ </div>
187
+ <div class="pricebox">
188
+ <div class="priceline"><span class="amount">${esc(price.amount)}</span><span class="unit">${esc(price.unit)}</span></div>
189
+ ${price.note ? `<div class="pnote">${esc(price.note)}</div>` : ''}
190
+ </div>
191
+ ${p.builds ? `<div class="builds">${esc(p.builds)}</div>` : ''}
192
+ <ul class="feats">
193
+ ${p.features.map((f) => ` <li>${CHECK_ICON}<div><b>${esc(f.name)}</b><span>${esc(f.detail)}</span></div></li>`).join('\n')}
194
+ </ul>
195
+ <a class="pbtn${p.cta.primary ? ' primary' : ''}" href="${esc(p.cta.href)}">${esc(p.cta.label)}</a>
196
+ </div>`;
197
+ }
198
+
199
+ export function renderPricing(benchmark: BenchmarkResult): string {
200
+ // The precision figures stay on the main page, beside the footnote that says what they do and do not
201
+ // promise. Quoted alone on a page that sells, a 100% would read as a promise about your code.
202
+ const all = plans();
203
+ const available = all.filter((p) => p.status === 'available');
204
+ const soon = all.filter((p) => p.status === 'coming-soon');
205
+ const notImplemented = countNotImplemented();
206
+
207
+ return `<!doctype html>
208
+ <html lang="en">
209
+ <head>
210
+ <meta charset="utf-8">
211
+ <meta name="viewport" content="width=device-width, initial-scale=1">
212
+ <title>Defuse — plans and pricing</title>
213
+ <meta name="description" content="Defuse is free and open source. Paid plans add tools for working through findings over time and across a team - and never change what the scanner finds.">
214
+ <meta property="og:title" content="Defuse — plans and pricing">
215
+ <meta property="og:description" content="The scanner is free. Paid plans organise findings; they never re-decide one.">
216
+ <meta property="og:type" content="website">
217
+ <meta name="theme-color" content="#262626">
218
+ <link rel="icon" href="defuse-mark.png">
219
+ <style>
220
+ ${BASE_CSS}
221
+ header{position:relative;padding:0 0 clamp(40px,6vw,72px)}
222
+ ${NAV_CSS}
223
+ h1{font-size:clamp(38px,6.4vw,72px);letter-spacing:-2px;margin:22px 0 0;max-width:17ch}
224
+ .lede{margin-top:22px;font-size:clamp(16px,1.9vw,19px);font-weight:300;max-width:62ch}
225
+
226
+ /* ---------- the plans ----------
227
+ Card layout after shadcn's pricing card: a header with a fact badge in the
228
+ corner, the price on one line, a checked feature list, one full-width
229
+ button, and a ring on the plan whose button is the primary one. Drawn
230
+ with this site's own colours, fonts and glass surface - no framework. */
231
+ .plans{display:grid;grid-template-columns:1fr;gap:20px;margin-top:8px;max-width:420px;margin-left:auto;margin-right:auto}
232
+ @media(min-width:960px){.plans{grid-template-columns:repeat(3,minmax(0,1fr));gap:22px;max-width:none}}
233
+ .plan{position:relative;display:flex;flex-direction:column;border-radius:12px;box-shadow:0 1px 2px rgba(0,0,0,.25)}
234
+ .plan.featured{border-color:var(--brand);box-shadow:0 0 0 1px var(--brand),0 18px 50px -24px rgba(121,44,247,.55)}
235
+ .phead{position:relative;padding:24px 24px 0}
236
+ .pname{font-size:24px;font-weight:600;letter-spacing:-.4px;line-height:1.1;color:var(--white);padding-right:120px}
237
+ .badge{position:absolute;top:24px;right:24px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;font-weight:500;letter-spacing:.3px;
238
+ color:var(--brand-soft);background:rgba(121,44,247,.14);border:1px solid rgba(121,44,247,.35);border-radius:999px;padding:3px 10px;white-space:nowrap}
239
+ .plan.featured .badge{color:var(--white);background:var(--brand);border-color:var(--brand)}
240
+ .who{margin-top:8px;font-size:14px;line-height:1.55;color:var(--mute)}
241
+ .pricebox{padding:22px 24px 0}
242
+ .priceline{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px}
243
+ .amount{font-weight:600;font-size:clamp(32px,3.2vw,38px);letter-spacing:-1.2px;line-height:1;color:var(--white);font-variant-numeric:tabular-nums}
244
+ .unit{font-size:14px;color:var(--mute)}
245
+ .pnote{margin-top:8px;font-size:13px;color:var(--faint)}
246
+ /* Side by side, the headers and price blocks are given the same height, so
247
+ the feature lists start on one line across the three cards. */
248
+ @media(min-width:960px){.plan .phead{min-height:130px}.plan .pricebox{min-height:92px}}
249
+ .builds{margin:20px 24px 0;padding-top:16px;border-top:1px solid var(--hair);font-family:"IBM Plex Mono",monospace;font-size:12px;letter-spacing:1px;color:var(--faint)}
250
+ .feats{list-style:none;margin:12px 24px 24px;flex-grow:1;display:flex;flex-direction:column;gap:14px}
251
+ .feats li{display:flex;gap:10px;align-items:flex-start}
252
+ .feats .tick{flex-shrink:0;margin-top:3px;color:var(--brand-soft)}
253
+ .feats b{display:block;font-weight:500;font-size:14.5px;color:var(--white)}
254
+ .feats span{display:block;margin-top:3px;font-size:13.5px;line-height:1.55;color:var(--mute)}
255
+ .pbtn{display:flex;align-items:center;justify-content:center;height:42px;margin:0 24px 24px;font-size:14px;font-weight:500;color:var(--white);
256
+ background:var(--glass);border:1px solid var(--line);border-radius:8px;padding:0 16px;transition:background 200ms ease,border-color 200ms ease}
257
+ .pbtn:hover{background:rgba(245,245,245,.07);border-color:var(--brand-lift);color:var(--white)}
258
+ .pbtn.primary{background:var(--brand);border-color:var(--brand)}
259
+ .pbtn.primary:hover{background:var(--brand-lift);border-color:var(--brand-lift)}
260
+ .pbtn:focus-visible{outline:2px solid var(--brand-soft);outline-offset:3px}
261
+
262
+ /* Not built yet: drawn as an outline with no price, so it cannot be mistaken
263
+ for something on sale. None are listed today - see plans(). */
264
+ .soonrow{display:grid;grid-template-columns:1fr;gap:18px;margin-top:18px}
265
+ @media(min-width:760px){.soonrow{grid-template-columns:1fr 1fr;gap:20px}}
266
+ .plan.soon{border:1px solid var(--hair);border-radius:12px;background:transparent;padding:24px;box-shadow:none}
267
+ .plan.soon .pname{color:var(--mute);font-size:20px;padding-right:0}
268
+ .plan.soon .who{margin-top:10px}
269
+ .plan.soon .pbtn{margin:20px 0 0;align-self:flex-start;color:var(--mute)}
270
+ .tag{margin-top:12px;align-self:flex-start;font-family:"IBM Plex Mono",monospace;font-size:11.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--mute);border:1px solid var(--line);border-radius:6px;padding:4px 9px}
271
+
272
+ /* ---------- promises, effort, buying ---------- */
273
+ .three{display:grid;grid-template-columns:1fr;gap:18px;margin-top:40px}
274
+ @media(min-width:900px){.three{grid-template-columns:repeat(3,1fr);gap:20px}}
275
+ .three .glass{padding:26px 24px}
276
+ .three h3{font-size:19px;font-weight:500;letter-spacing:-.3px;color:var(--white)}
277
+ .three p{margin-top:10px;font-size:14.5px}
278
+ .stats{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:40px}
279
+ @media(min-width:760px){.stats{grid-template-columns:repeat(4,1fr);gap:18px}}
280
+ .stat{padding:24px 22px;border-radius:14px}
281
+ .stat b{display:block;font-weight:700;font-size:clamp(26px,3.2vw,34px);letter-spacing:-1px}
282
+ .stat span{display:block;margin-top:5px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--faint)}
283
+ .steps{margin:26px 0 0 20px;color:var(--mute);max-width:70ch}
284
+ .steps li{margin:10px 0}
285
+ .steps strong{color:var(--white);font-weight:500}
286
+ ${FOOTER_CSS}
287
+ </style>
288
+ </head>
289
+ <body>
290
+
291
+ <header>
292
+ <div class="wrap">
293
+ ${siteNav('pricing')}
294
+ <div class="kicker" style="margin-top:clamp(30px,5vw,64px)">Plans and pricing</div>
295
+ <h1>The scanner is free. Teams pay for the tools around it.</h1>
296
+ <p class="lede">Community is the whole scanner, open source, with nothing held back. The paid plans add
297
+ tools for working through findings over time and across a team. They read what the scanner found;
298
+ they never change what it found or how it is labelled.</p>
299
+ </div>
300
+ </header>
301
+
302
+ <section id="plans" style="padding-top:clamp(36px,5vw,56px)">
303
+ <div class="wrap">
304
+ <div class="plans">
305
+ ${available.map(planCard).join('\n')}
306
+ </div>
307
+ ${soon.length > 0 ? ` <div class="soonrow">\n${soon.map(planCard).join('\n')}\n </div>\n` : ''} <p class="mono" style="margin-top:22px;font-size:12.5px;color:var(--faint);max-width:none">Prices in US dollars.
308
+ There is no checkout yet - see how buying works below.</p>
309
+ </div>
310
+ </section>
311
+
312
+ <section id="promise">
313
+ <div class="wrap">
314
+ <div class="kicker">The same in every plan</div>
315
+ <h2>Paying never makes<br>a guess look like a proof.</h2>
316
+ <div class="three">
317
+ <div class="glass">
318
+ <h3>The scanner decides</h3>
319
+ <p>The paid tools read the scanner's own output. They organise findings - compare them, mark them,
320
+ chart them - and never re-decide or relabel one.</p>
321
+ </div>
322
+ <div class="glass">
323
+ <h3>Nothing is hidden</h3>
324
+ <p>A finding your team marks is still listed, with the reason beside it. A comparison never calls a
325
+ finding fixed; it says "no longer reported", because that is all it can know.</p>
326
+ </div>
327
+ <div class="glass">
328
+ <h3>Your code stays with you</h3>
329
+ <p>Everything runs on your machine or in your own CI. There is no account to make and nothing is
330
+ uploaded anywhere.</p>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </section>
335
+
336
+ <section id="work">
337
+ <div class="wrap">
338
+ <div class="kicker">The work behind it</div>
339
+ <h2>Measured, and<br>published whole.</h2>
340
+ <p style="margin-top:20px">The engine is scored against OWASP BenchmarkJava, with the two labels scored
341
+ separately. Safety rules are switched off one at a time to prove a test notices. And the scanner
342
+ is run on large real projects - Django, WordPress and VS Code among them - to find what synthetic tests
343
+ miss. <a href="index.html#numbers">The measurements, and what they do not promise, are on the main page.</a></p>
344
+ <div class="stats">
345
+ <div class="stat glass"><b>${benchmark.scored.toLocaleString('en-US')}</b><span>benchmark cases scored</span></div>
346
+ <div class="stat glass"><b>${ALL_RULES.length}</b><span>rules</span></div>
347
+ <div class="stat glass"><b>${LANGUAGES.length}</b><span>languages traced</span></div>
348
+ <div class="stat glass"><b>${notImplemented}</b><span>gaps listed by name</span></div>
349
+ </div>
350
+ </div>
351
+ </section>
352
+
353
+ <section id="buying">
354
+ <div class="wrap">
355
+ <div class="kicker">How buying works, today</div>
356
+ <h2>Set up by hand,<br>for now.</h2>
357
+ <ol class="steps">
358
+ <li><strong>Ask</strong> using the button on the plan you want. It opens a short request form on GitHub.</li>
359
+ <li><strong>Hear back</strong> privately, with the licence terms and an invoice.</li>
360
+ <li><strong>Get the package</strong> - a file your machine or your CI installs next to the scanner.</li>
361
+ </ol>
362
+ <p style="margin-top:20px">The request form is public. Write only the plan, the number of developers and
363
+ how to reach you privately - never code, keys or company details.</p>
364
+ </div>
365
+ </section>
366
+
367
+ <div class="wrap">
368
+ ${siteFooter()}
369
+ </div>
370
+
371
+ </body>
372
+ </html>
373
+ `;
374
+ }