@sikkuumiii/defuse 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/README.md +561 -2
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +619 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/analyze.d.ts +178 -0
- package/dist/src/core/analyze.js +834 -0
- package/dist/src/core/analyze.js.map +1 -0
- package/dist/src/core/coverage.d.ts +87 -0
- package/dist/src/core/coverage.js +181 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/finding.d.ts +196 -0
- package/dist/src/core/finding.js +178 -0
- package/dist/src/core/finding.js.map +1 -0
- package/dist/src/core/licence.d.ts +50 -0
- package/dist/src/core/licence.js +56 -0
- package/dist/src/core/licence.js.map +1 -0
- package/dist/src/core/paths.d.ts +21 -0
- package/dist/src/core/paths.js +58 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/score.d.ts +137 -0
- package/dist/src/core/score.js +232 -0
- package/dist/src/core/score.js.map +1 -0
- package/dist/src/dast/targets.d.ts +30 -0
- package/dist/src/dast/targets.js +79 -0
- package/dist/src/dast/targets.js.map +1 -0
- package/dist/src/dast/zap.d.ts +42 -0
- package/dist/src/dast/zap.js +184 -0
- package/dist/src/dast/zap.js.map +1 -0
- package/dist/src/engine/scan.d.ts +80 -0
- package/dist/src/engine/scan.js +129 -0
- package/dist/src/engine/scan.js.map +1 -0
- package/dist/src/engine/shapes.d.ts +47 -0
- package/dist/src/engine/shapes.js +328 -0
- package/dist/src/engine/shapes.js.map +1 -0
- package/dist/src/engine/walk.d.ts +60 -0
- package/dist/src/engine/walk.js +156 -0
- package/dist/src/engine/walk.js.map +1 -0
- package/dist/src/parse/astDump.d.ts +31 -0
- package/dist/src/parse/astDump.js +66 -0
- package/dist/src/parse/astDump.js.map +1 -0
- package/dist/src/parse/languages.d.ts +70 -0
- package/dist/src/parse/languages.js +174 -0
- package/dist/src/parse/languages.js.map +1 -0
- package/dist/src/parse/location.d.ts +17 -0
- package/dist/src/parse/location.js +38 -0
- package/dist/src/parse/location.js.map +1 -0
- package/dist/src/parse/node-grammars.d.ts +12 -0
- package/dist/src/parse/node-grammars.js +35 -0
- package/dist/src/parse/node-grammars.js.map +1 -0
- package/dist/src/parse/parser.d.ts +126 -0
- package/dist/src/parse/parser.js +202 -0
- package/dist/src/parse/parser.js.map +1 -0
- package/dist/src/parse/tree-store.d.ts +139 -0
- package/dist/src/parse/tree-store.js +236 -0
- package/dist/src/parse/tree-store.js.map +1 -0
- package/dist/src/platform/dependency-rule.d.ts +27 -0
- package/dist/src/platform/dependency-rule.js +21 -0
- package/dist/src/platform/dependency-rule.js.map +1 -0
- package/dist/src/platform/deps.d.ts +86 -0
- package/dist/src/platform/deps.js +304 -0
- package/dist/src/platform/deps.js.map +1 -0
- package/dist/src/platform/php-facts.d.ts +49 -0
- package/dist/src/platform/php-facts.js +67 -0
- package/dist/src/platform/php-facts.js.map +1 -0
- package/dist/src/platform/runtime-version.d.ts +78 -0
- package/dist/src/platform/runtime-version.js +573 -0
- package/dist/src/platform/runtime-version.js.map +1 -0
- package/dist/src/report/colors.d.ts +50 -0
- package/dist/src/report/colors.js +97 -0
- package/dist/src/report/colors.js.map +1 -0
- package/dist/src/report/coverage-table.d.ts +90 -0
- package/dist/src/report/coverage-table.js +291 -0
- package/dist/src/report/coverage-table.js.map +1 -0
- package/dist/src/report/html-report.d.ts +66 -0
- package/dist/src/report/html-report.js +489 -0
- package/dist/src/report/html-report.js.map +1 -0
- package/dist/src/report/human.d.ts +44 -0
- package/dist/src/report/human.js +575 -0
- package/dist/src/report/human.js.map +1 -0
- package/dist/src/report/json.d.ts +24 -0
- package/dist/src/report/json.js +178 -0
- package/dist/src/report/json.js.map +1 -0
- package/dist/src/report/mark.d.ts +17 -0
- package/dist/src/report/mark.js +39 -0
- package/dist/src/report/mark.js.map +1 -0
- package/dist/src/report/pricing.d.ts +33 -0
- package/dist/src/report/pricing.js +343 -0
- package/dist/src/report/pricing.js.map +1 -0
- package/dist/src/report/sarif.d.ts +39 -0
- package/dist/src/report/sarif.js +219 -0
- package/dist/src/report/sarif.js.map +1 -0
- package/dist/src/report/site.d.ts +87 -0
- package/dist/src/report/site.js +742 -0
- package/dist/src/report/site.js.map +1 -0
- package/dist/src/rules/code-injection.d.ts +47 -0
- package/dist/src/rules/code-injection.js +326 -0
- package/dist/src/rules/code-injection.js.map +1 -0
- package/dist/src/rules/command-injection.d.ts +30 -0
- package/dist/src/rules/command-injection.js +286 -0
- package/dist/src/rules/command-injection.js.map +1 -0
- package/dist/src/rules/contract.d.ts +198 -0
- package/dist/src/rules/contract.js +39 -0
- package/dist/src/rules/contract.js.map +1 -0
- package/dist/src/rules/format-string.d.ts +46 -0
- package/dist/src/rules/format-string.js +146 -0
- package/dist/src/rules/format-string.js.map +1 -0
- package/dist/src/rules/hardcoded-secret.d.ts +38 -0
- package/dist/src/rules/hardcoded-secret.js +440 -0
- package/dist/src/rules/hardcoded-secret.js.map +1 -0
- package/dist/src/rules/lib/guards.d.ts +168 -0
- package/dist/src/rules/lib/guards.js +1179 -0
- package/dist/src/rules/lib/guards.js.map +1 -0
- package/dist/src/rules/lib/html-safety.d.ts +75 -0
- package/dist/src/rules/lib/html-safety.js +755 -0
- package/dist/src/rules/lib/html-safety.js.map +1 -0
- package/dist/src/rules/lib/local-build.d.ts +59 -0
- package/dist/src/rules/lib/local-build.js +320 -0
- package/dist/src/rules/lib/local-build.js.map +1 -0
- package/dist/src/rules/lib/strings.d.ts +208 -0
- package/dist/src/rules/lib/strings.js +1970 -0
- package/dist/src/rules/lib/strings.js.map +1 -0
- package/dist/src/rules/path-traversal.d.ts +33 -0
- package/dist/src/rules/path-traversal.js +96 -0
- package/dist/src/rules/path-traversal.js.map +1 -0
- package/dist/src/rules/registry.d.ts +19 -0
- package/dist/src/rules/registry.js +77 -0
- package/dist/src/rules/registry.js.map +1 -0
- package/dist/src/rules/sql-injection.d.ts +36 -0
- package/dist/src/rules/sql-injection.js +299 -0
- package/dist/src/rules/sql-injection.js.map +1 -0
- package/dist/src/rules/ssrf.d.ts +39 -0
- package/dist/src/rules/ssrf.js +111 -0
- package/dist/src/rules/ssrf.js.map +1 -0
- package/dist/src/rules/unbounded-copy.d.ts +37 -0
- package/dist/src/rules/unbounded-copy.js +147 -0
- package/dist/src/rules/unbounded-copy.js.map +1 -0
- package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
- package/dist/src/rules/unsafe-deserialization.js +276 -0
- package/dist/src/rules/unsafe-deserialization.js.map +1 -0
- package/dist/src/rules/weak-hash.d.ts +37 -0
- package/dist/src/rules/weak-hash.js +197 -0
- package/dist/src/rules/weak-hash.js.map +1 -0
- package/dist/src/rules/xss.d.ts +38 -0
- package/dist/src/rules/xss.js +527 -0
- package/dist/src/rules/xss.js.map +1 -0
- package/dist/src/taint/content-type.d.ts +19 -0
- package/dist/src/taint/content-type.js +20 -0
- package/dist/src/taint/content-type.js.map +1 -0
- package/dist/src/taint/dictionaries.d.ts +23 -0
- package/dist/src/taint/dictionaries.js +1778 -0
- package/dist/src/taint/dictionaries.js.map +1 -0
- package/dist/src/taint/fold.d.ts +59 -0
- package/dist/src/taint/fold.js +1177 -0
- package/dist/src/taint/fold.js.map +1 -0
- package/dist/src/taint/java-scope.d.ts +61 -0
- package/dist/src/taint/java-scope.js +212 -0
- package/dist/src/taint/java-scope.js.map +1 -0
- package/dist/src/taint/local-map.d.ts +79 -0
- package/dist/src/taint/local-map.js +603 -0
- package/dist/src/taint/local-map.js.map +1 -0
- package/dist/src/taint/project.d.ts +172 -0
- package/dist/src/taint/project.js +617 -0
- package/dist/src/taint/project.js.map +1 -0
- package/dist/src/taint/tracer.d.ts +265 -0
- package/dist/src/taint/tracer.js +3817 -0
- package/dist/src/taint/tracer.js.map +1 -0
- package/dist/src/taint/types.d.ts +484 -0
- package/dist/src/taint/types.js +47 -0
- package/dist/src/taint/types.js.map +1 -0
- package/dist/src/taint/visibility.d.ts +25 -0
- package/dist/src/taint/visibility.js +96 -0
- package/dist/src/taint/visibility.js.map +1 -0
- package/docs/examples/defuse.json +20 -0
- package/docs/examples/dvwa-urls.json +14 -0
- package/docs/examples/github-code-scanning.yml +54 -0
- package/docs/examples/tiny.js +4 -0
- package/package.json +73 -4
- package/src/cli.ts +747 -0
- package/src/core/analyze.ts +1072 -0
- package/src/core/coverage.ts +269 -0
- package/src/core/finding.ts +311 -0
- package/src/core/licence.ts +61 -0
- package/src/core/paths.ts +59 -0
- package/src/core/score.ts +353 -0
- package/src/dast/targets.ts +121 -0
- package/src/dast/zap.ts +246 -0
- package/src/engine/scan.ts +200 -0
- package/src/engine/shapes.ts +348 -0
- package/src/engine/walk.ts +196 -0
- package/src/parse/astDump.ts +85 -0
- package/src/parse/languages.ts +232 -0
- package/src/parse/location.ts +44 -0
- package/src/parse/node-grammars.ts +41 -0
- package/src/parse/parser.ts +262 -0
- package/src/parse/tree-store.ts +301 -0
- package/src/platform/dependency-rule.ts +33 -0
- package/src/platform/deps.ts +374 -0
- package/src/platform/php-facts.ts +85 -0
- package/src/platform/runtime-version.ts +710 -0
- package/src/report/colors.ts +107 -0
- package/src/report/coverage-table.ts +359 -0
- package/src/report/html-report.ts +601 -0
- package/src/report/human.ts +811 -0
- package/src/report/json.ts +189 -0
- package/src/report/mark.ts +47 -0
- package/src/report/pricing.ts +374 -0
- package/src/report/sarif.ts +235 -0
- package/src/report/site.ts +760 -0
- package/src/rules/code-injection.ts +346 -0
- package/src/rules/command-injection.ts +296 -0
- package/src/rules/contract.ts +220 -0
- package/src/rules/format-string.ts +158 -0
- package/src/rules/hardcoded-secret.ts +485 -0
- package/src/rules/lib/guards.ts +1235 -0
- package/src/rules/lib/html-safety.ts +793 -0
- package/src/rules/lib/local-build.ts +329 -0
- package/src/rules/lib/strings.ts +2034 -0
- package/src/rules/path-traversal.ts +101 -0
- package/src/rules/registry.ts +90 -0
- package/src/rules/sql-injection.ts +311 -0
- package/src/rules/ssrf.ts +118 -0
- package/src/rules/unbounded-copy.ts +162 -0
- package/src/rules/unsafe-deserialization.ts +291 -0
- package/src/rules/weak-hash.ts +226 -0
- package/src/rules/xss.ts +572 -0
- package/src/taint/content-type.ts +20 -0
- package/src/taint/dictionaries.ts +1855 -0
- package/src/taint/fold.ts +1206 -0
- package/src/taint/java-scope.ts +200 -0
- package/src/taint/local-map.ts +594 -0
- package/src/taint/project.ts +774 -0
- package/src/taint/tracer.ts +4298 -0
- package/src/taint/types.ts +511 -0
- package/src/taint/visibility.ts +100 -0
- package/tsconfig.json +61 -0
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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> ${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> <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 · 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 · a printed source→sink path<br>claim · 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 · one matched pattern<br>claim · 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
|