@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal colour, in about forty lines and zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* ANSI escape codes are just special character sequences: printing "\x1b[31m"
|
|
5
|
+
* tells the terminal "red from here", and "\x1b[0m" means "back to normal".
|
|
6
|
+
* Every colour library is a wrapper around that idea.
|
|
7
|
+
*
|
|
8
|
+
* We turn colour OFF when output is being piped into a file or another program
|
|
9
|
+
* (`process.stdout.isTTY` is false), and we honour the NO_COLOR convention,
|
|
10
|
+
* because escape codes in a log file are unreadable garbage.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const enabled =
|
|
14
|
+
process.env['NO_COLOR'] === undefined &&
|
|
15
|
+
process.env['TERM'] !== 'dumb' &&
|
|
16
|
+
process.stdout.isTTY === true;
|
|
17
|
+
|
|
18
|
+
function wrap(open: number, close: number) {
|
|
19
|
+
return (text: string): string => (enabled ? `\x1b[${open}m${text}\x1b[${close}m` : text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const color = {
|
|
23
|
+
enabled,
|
|
24
|
+
bold: wrap(1, 22),
|
|
25
|
+
dim: wrap(2, 22),
|
|
26
|
+
italic: wrap(3, 23),
|
|
27
|
+
underline: wrap(4, 24),
|
|
28
|
+
red: wrap(31, 39),
|
|
29
|
+
green: wrap(32, 39),
|
|
30
|
+
yellow: wrap(33, 39),
|
|
31
|
+
blue: wrap(34, 39),
|
|
32
|
+
magenta: wrap(35, 39),
|
|
33
|
+
cyan: wrap(36, 39),
|
|
34
|
+
white: wrap(37, 39),
|
|
35
|
+
gray: wrap(90, 39),
|
|
36
|
+
bgRed: wrap(41, 49),
|
|
37
|
+
bgYellow: wrap(43, 49),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Word-wrap a paragraph to `width`, indenting every line after the first. */
|
|
41
|
+
export function wrapText(text: string, width: number, indent: string): string {
|
|
42
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
let current = '';
|
|
45
|
+
|
|
46
|
+
for (const word of words) {
|
|
47
|
+
if (current.length === 0) {
|
|
48
|
+
current = word;
|
|
49
|
+
} else if (current.length + 1 + word.length <= width) {
|
|
50
|
+
current += ` ${word}`;
|
|
51
|
+
} else {
|
|
52
|
+
lines.push(current);
|
|
53
|
+
current = word;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (current) lines.push(current);
|
|
57
|
+
return lines.join(`\n${indent}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* BOX-DRAWING GLYPHS, AND WHY THEY NEED A FALLBACK
|
|
63
|
+
*
|
|
64
|
+
* The report uses characters like ─ ✓ ● because they read well in a terminal.
|
|
65
|
+
* They are also a portability hazard: they are multi-byte UTF-8, and the moment
|
|
66
|
+
* output leaves a UTF-8 terminal the bytes can be re-interpreted by something
|
|
67
|
+
* that assumes a legacy code page. Redirecting a scan to a file on Windows and
|
|
68
|
+
* opening it in Notepad turns every `──` into `ÔöÇÔöÇ` - the report becomes
|
|
69
|
+
* unreadable while the analysis behind it was perfectly correct.
|
|
70
|
+
*
|
|
71
|
+
* So: one table, two spellings, and a switch. ASCII is chosen automatically
|
|
72
|
+
* when output is being redirected on Windows (the case that actually breaks),
|
|
73
|
+
* and can be forced anywhere with --ascii or NS1_ASCII=1.
|
|
74
|
+
*/
|
|
75
|
+
let asciiMode =
|
|
76
|
+
process.env['NS1_ASCII'] === '1' ||
|
|
77
|
+
(process.platform === 'win32' && process.stdout.isTTY !== true);
|
|
78
|
+
|
|
79
|
+
export function setAsciiMode(enabled: boolean): void {
|
|
80
|
+
asciiMode = enabled;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const GLYPHS = {
|
|
84
|
+
rule: ['\u2500', '-'],
|
|
85
|
+
vertical: ['\u2502', '|'],
|
|
86
|
+
cornerTop: ['\u250c', '+'],
|
|
87
|
+
cornerBottom: ['\u2514', '+'],
|
|
88
|
+
tick: ['\u2713', 'ok'],
|
|
89
|
+
cross: ['\u2717', 'X'],
|
|
90
|
+
dotFull: ['\u25cf', '*'],
|
|
91
|
+
dotHalf: ['\u25d0', '~'],
|
|
92
|
+
dotEmpty: ['\u25cb', 'o'],
|
|
93
|
+
middot: ['\u00b7', '-'],
|
|
94
|
+
arrow: ['\u2192', '->'],
|
|
95
|
+
ellipsis: ['\u2026', '...'],
|
|
96
|
+
// Windows PowerShell renders U+26A0 inconsistently; the ASCII fallback keeps
|
|
97
|
+
// the warning legible everywhere, which for a warning is the whole point.
|
|
98
|
+
warn: ['\u26a0', '!'],
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
export type GlyphName = keyof typeof GLYPHS;
|
|
102
|
+
|
|
103
|
+
/** The right spelling of a glyph for wherever this output is going. */
|
|
104
|
+
export function g(name: GlyphName): string {
|
|
105
|
+
const pair = GLYPHS[name];
|
|
106
|
+
return asciiMode ? pair[1] : pair[0];
|
|
107
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE README, DERIVED FROM THE REGISTRY.
|
|
3
|
+
* =====================================
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS.
|
|
6
|
+
*
|
|
7
|
+
* Pointing a fresh reader at this repository produced a list of things the
|
|
8
|
+
* README asserts that the code contradicts:
|
|
9
|
+
*
|
|
10
|
+
* README says 5 rules registry ships 8
|
|
11
|
+
* README says 5 languages languages.ts 6 (PHP absent entirely)
|
|
12
|
+
* README says "Not SARIF" src/report/sarif.ts is a SARIF 2.1.0 exporter
|
|
13
|
+
* README says 13.8% recall the scorer measures 76.4%
|
|
14
|
+
*
|
|
15
|
+
* Every one of those was true when it was written. The project simply moved and
|
|
16
|
+
* the prose did not, because the prose was a hand-maintained restatement of
|
|
17
|
+
* facts the registry already knew.
|
|
18
|
+
*
|
|
19
|
+
* THIS IS THE FOURTH TIME THIS EXACT CLASS HAS BITTEN, and the previous three
|
|
20
|
+
* are recorded elsewhere in this codebase: sources built from the frameworks we
|
|
21
|
+
* had been shown (elasticsearch), escapers likewise (Jenkins), test-path
|
|
22
|
+
* conventions likewise (cal.com). The pattern is always a list maintained by
|
|
23
|
+
* hand that looks complete until reality moves.
|
|
24
|
+
*
|
|
25
|
+
* What makes this instance the sharpest is where the stale list was. There IS a
|
|
26
|
+
* documentation-drift check in the suite, and it passed the whole time - because
|
|
27
|
+
* it greps the README for exactly one regex, the `npm test - N checks` line.
|
|
28
|
+
* A narrow instrument was trusted as a broad one. The checker itself was the
|
|
29
|
+
* hand-maintained list.
|
|
30
|
+
*
|
|
31
|
+
* So: the counts and the matrix are now GENERATED from ALL_RULES and LANGUAGES,
|
|
32
|
+
* the benchmark table is generated from the scorer's own output file, and
|
|
33
|
+
* `npm test` fails when the README disagrees with either. Prose that restates a
|
|
34
|
+
* fact the program knows is a bug waiting for a quiet afternoon.
|
|
35
|
+
*/
|
|
36
|
+
import { ALL_RULES } from '../rules/registry.js';
|
|
37
|
+
import { LANGUAGES, type LanguageId } from '../parse/languages.js';
|
|
38
|
+
import type { SupportStatus } from '../rules/contract.js';
|
|
39
|
+
|
|
40
|
+
/** Opening and closing markers for a generated block in a markdown file. */
|
|
41
|
+
export const BLOCK_START = (name: string): string => `<!-- derived:${name} -->`;
|
|
42
|
+
export const BLOCK_END = '<!-- /derived -->';
|
|
43
|
+
|
|
44
|
+
const MARK: Record<SupportStatus, string> = {
|
|
45
|
+
implemented: '●',
|
|
46
|
+
partial: '◐',
|
|
47
|
+
'not-implemented': '○',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SHORT: Partial<Record<LanguageId, string>> = {
|
|
51
|
+
javascript: 'JS',
|
|
52
|
+
typescript: 'TS',
|
|
53
|
+
python: 'PY',
|
|
54
|
+
java: 'JAVA',
|
|
55
|
+
php: 'PHP',
|
|
56
|
+
go: 'GO',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const columnLabel = (id: LanguageId): string => SHORT[id] ?? id.toUpperCase();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The rule × language coverage matrix.
|
|
63
|
+
*
|
|
64
|
+
* `Rule.support` is already a per-language honesty declaration with a REQUIRED
|
|
65
|
+
* note on every entry, printed at the end of every scan. The README's table was
|
|
66
|
+
* a second copy of it, kept by hand, and it is the copy that went stale.
|
|
67
|
+
*/
|
|
68
|
+
export function renderRuleMatrix(): string {
|
|
69
|
+
const languages = LANGUAGES.map((l) => l.id);
|
|
70
|
+
const header = `| Rule | CWE | OWASP | ${languages.map(columnLabel).join(' | ')} |`;
|
|
71
|
+
const divider = `|---|---|---|${languages.map(() => ':--:').join('|')}|`;
|
|
72
|
+
const rows = ALL_RULES.map((rule) => {
|
|
73
|
+
const cells = languages.map((id) => MARK[rule.support[id]?.status ?? 'not-implemented']);
|
|
74
|
+
return `| \`${rule.id}\` | ${rule.cwe} | ${rule.owasp} | ${cells.join(' | ')} |`;
|
|
75
|
+
});
|
|
76
|
+
return [
|
|
77
|
+
header,
|
|
78
|
+
divider,
|
|
79
|
+
...rows,
|
|
80
|
+
'',
|
|
81
|
+
'● implemented ◐ partial ○ not implemented',
|
|
82
|
+
'',
|
|
83
|
+
`${ALL_RULES.length} rules across ${languages.length} languages. This table is generated from`,
|
|
84
|
+
'`ALL_RULES` and `LANGUAGES` by `npm run sync:readme`, and `npm test` fails if it',
|
|
85
|
+
'drifts - the counts in it are not maintained by hand.',
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One sentence naming the current totals, for use in running prose. */
|
|
90
|
+
export function renderCounts(): string {
|
|
91
|
+
const names = LANGUAGES.map((l) => l.displayName);
|
|
92
|
+
return (
|
|
93
|
+
`**${ALL_RULES.length} rules** across **${names.length} languages** ` +
|
|
94
|
+
`(${names.join(', ')}).`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface BenchmarkResult {
|
|
99
|
+
readonly scored: number;
|
|
100
|
+
readonly tp: number;
|
|
101
|
+
readonly fp: number;
|
|
102
|
+
readonly fn: number;
|
|
103
|
+
readonly tn: number;
|
|
104
|
+
readonly precision: string;
|
|
105
|
+
readonly recall: string;
|
|
106
|
+
readonly missedByCategory: Record<string, number>;
|
|
107
|
+
readonly filesParsed: number;
|
|
108
|
+
readonly parseErrors: number;
|
|
109
|
+
readonly durationMs: number;
|
|
110
|
+
/** False positives carrying BenchmarkJava's constant-branch decoy. */
|
|
111
|
+
readonly decoyFalsePositives: number;
|
|
112
|
+
readonly decoyShare: string;
|
|
113
|
+
readonly engineVersion: string;
|
|
114
|
+
readonly measuredAt: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The OWASP BenchmarkJava table.
|
|
119
|
+
*
|
|
120
|
+
* This one cannot be derived from the registry, because it is a MEASUREMENT
|
|
121
|
+
* rather than a declaration - so it is derived from the measurement instead.
|
|
122
|
+
* `npm run benchmark` writes docs/benchmark-result.json, this renders it, and
|
|
123
|
+
* the test compares the README against it. The loop closes: you cannot change
|
|
124
|
+
* the engine's score without the README either following or failing the build.
|
|
125
|
+
*
|
|
126
|
+
* The stale figures this replaced were 64.5% precision / 13.8% recall. They
|
|
127
|
+
* were honestly measured and then left behind by the Spring and JAX-RS source
|
|
128
|
+
* bindings and by declaredTypes(), which took recall to 76.4% and cost
|
|
129
|
+
* precision on the way - a real trade the old table could not show.
|
|
130
|
+
*/
|
|
131
|
+
export function renderBenchmark(result: BenchmarkResult): string {
|
|
132
|
+
/*
|
|
133
|
+
* THE HEADLINE IS DERIVED TOO, and that is the point of this function.
|
|
134
|
+
*
|
|
135
|
+
* The first version of this block generated the TABLE and left the sentence
|
|
136
|
+
* underneath it hand-written. That sentence read "Recall is low and that is
|
|
137
|
+
* the honest headline" - an interpretation of the OLD numbers, which went
|
|
138
|
+
* stale in the very commit that fixed the table it sat beneath. Deriving the
|
|
139
|
+
* facts and hand-keeping their meaning just moves the drift one line down.
|
|
140
|
+
*
|
|
141
|
+
* So the weak side is computed. More false positives than false negatives
|
|
142
|
+
* means precision is where the tool is worst; the reverse means recall is.
|
|
143
|
+
* Whichever it is, the README says it because the arithmetic said it.
|
|
144
|
+
*/
|
|
145
|
+
const precisionIsWeaker = result.fp > result.fn;
|
|
146
|
+
const ratio = (Math.max(result.fp, result.fn) / Math.max(1, Math.min(result.fp, result.fn)))
|
|
147
|
+
.toFixed(1);
|
|
148
|
+
/*
|
|
149
|
+
* A ZERO IS NOT A RATIO. When the hunt for BenchmarkJava's 38 misses took
|
|
150
|
+
* false negatives to 0, this sentence printed "271 false positives against 0
|
|
151
|
+
* false negatives - 271.0x as many": arithmetic on a divide-by-one guard,
|
|
152
|
+
* dressed up as a measurement. A zero is said in words, and so is its limit:
|
|
153
|
+
* no misses HERE is a fact about these 1,210 test cases, not about your code.
|
|
154
|
+
*/
|
|
155
|
+
const scope =
|
|
156
|
+
'That is a statement about these scored test cases, not about code in general - ' +
|
|
157
|
+
'the limitations list is where the misses this benchmark cannot show are written down.';
|
|
158
|
+
const headline =
|
|
159
|
+
result.fn === 0 && result.fp > 0
|
|
160
|
+
? `**Precision is the weak side.** ${result.fp} false positives and no false ` +
|
|
161
|
+
`negatives, so on this benchmark the cost of this engine is triage time, not ` +
|
|
162
|
+
`missed bugs. ${scope}`
|
|
163
|
+
: result.fp === 0 && result.fn > 0
|
|
164
|
+
? `**Recall is the weak side.** ${result.fn} false negatives and no false ` +
|
|
165
|
+
`positives, so on this benchmark the cost of this engine is missed bugs, not ` +
|
|
166
|
+
`triage time. ${scope}`
|
|
167
|
+
: precisionIsWeaker
|
|
168
|
+
? `**Precision is the weaker side.** ${result.fp} false positives against ` +
|
|
169
|
+
`${result.fn} false negatives - ${ratio}x as many - so the cost of this engine ` +
|
|
170
|
+
`is triage time, not missed bugs.`
|
|
171
|
+
: `**Recall is the weaker side.** ${result.fn} false negatives against ` +
|
|
172
|
+
`${result.fp} false positives - ${ratio}x as many - so the cost of this engine ` +
|
|
173
|
+
`is missed bugs, not triage time.`;
|
|
174
|
+
const missed = Object.entries(result.missedByCategory);
|
|
175
|
+
|
|
176
|
+
return [
|
|
177
|
+
'| | cases | TP | FP | FN | TN | precision | recall |',
|
|
178
|
+
'|---|--:|--:|--:|--:|--:|--:|--:|',
|
|
179
|
+
`| **overall** | **${result.scored}** | **${result.tp}** | **${result.fp}** | ` +
|
|
180
|
+
`**${result.fn}** | **${result.tn}** | **${result.precision}** | **${result.recall}** |`,
|
|
181
|
+
'',
|
|
182
|
+
`${result.filesParsed} files, ${result.parseErrors} parse errors, ` +
|
|
183
|
+
`${(result.durationMs / 1000).toFixed(1)} seconds, on engine ${result.engineVersion} ` +
|
|
184
|
+
`(${result.measuredAt}).`,
|
|
185
|
+
'',
|
|
186
|
+
headline,
|
|
187
|
+
'',
|
|
188
|
+
(missed.length > 0
|
|
189
|
+
? `Missed by category: ${missed.map(([k, v]) => `${k} ${v}`).join(', ')}. `
|
|
190
|
+
: 'Nothing was missed in the scored categories. ') +
|
|
191
|
+
`Of the ${result.fp} false positives, **${result.decoyFalsePositives} ` +
|
|
192
|
+
`(${result.decoyShare})** carry the shape of BenchmarkJava's constant-branch decoy - ` +
|
|
193
|
+
`a condition like \`if ((7 * 42) - num > 200)\`. The data-flow pass, the SQL rule ` +
|
|
194
|
+
`and the command-injection rule all decide such a condition, so the shape alone ` +
|
|
195
|
+
`explains none of them: every false positive here is ours.`,
|
|
196
|
+
'',
|
|
197
|
+
'Every number above is generated by `npm run benchmark` into',
|
|
198
|
+
'`docs/benchmark-result.json` - including the sentence naming the weaker side,',
|
|
199
|
+
'because an interpretation kept by hand goes stale exactly like a figure does.',
|
|
200
|
+
].join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Replace the contents of one `<!-- derived:name -->` block. */
|
|
204
|
+
export interface LabelSplitTier {
|
|
205
|
+
readonly label: string;
|
|
206
|
+
readonly tp: number;
|
|
207
|
+
readonly fp: number;
|
|
208
|
+
readonly fn: number;
|
|
209
|
+
readonly precision: string;
|
|
210
|
+
readonly recall: string;
|
|
211
|
+
readonly decoyFalsePositives: number;
|
|
212
|
+
readonly decoyShare: string;
|
|
213
|
+
readonly nonDecoyFalsePositives: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface LabelSplitResult {
|
|
217
|
+
readonly scored: number;
|
|
218
|
+
readonly findingsByConfidence: Record<string, number>;
|
|
219
|
+
readonly tiers: readonly LabelSplitTier[];
|
|
220
|
+
readonly engineVersion: string;
|
|
221
|
+
readonly measuredAt: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* THE BLOCK THAT SCORES THE LABELS SEPARATELY.
|
|
226
|
+
*
|
|
227
|
+
* `renderBenchmark` reports one precision figure covering both labels mixed
|
|
228
|
+
* together. That figure is an average across two populations this whole tool
|
|
229
|
+
* exists to keep apart - the same blending it refuses to do inside a single
|
|
230
|
+
* finding, done one level up in the report about itself.
|
|
231
|
+
*
|
|
232
|
+
* So this renders the split, and like the benchmark block it derives its own
|
|
233
|
+
* interpretation rather than leaving a hand-written sentence to go stale
|
|
234
|
+
* underneath it. Two sentences are computed:
|
|
235
|
+
*
|
|
236
|
+
* - whether the green label is actually worth more than the amber one, and
|
|
237
|
+
* by how much. If a change ever made them equal, this block would say so
|
|
238
|
+
* in the README rather than waiting for someone to notice.
|
|
239
|
+
*
|
|
240
|
+
* - how much of the green tier's error is BenchmarkJava's synthetic decoy
|
|
241
|
+
* rather than a real mistake. Quoting the decoy-excluded figure alone
|
|
242
|
+
* would be picking the flattering number; quoting only the blended one
|
|
243
|
+
* hides that the benchmark's traps dominate what is left.
|
|
244
|
+
*/
|
|
245
|
+
export function renderLabelSplit(result: LabelSplitResult): string {
|
|
246
|
+
const pct = (s: string): number => Number.parseFloat(s);
|
|
247
|
+
const verified = result.tiers.find((t) => t.label.startsWith('flow-verified'));
|
|
248
|
+
const signature = result.tiers.find((t) => t.label.startsWith('signature-only'));
|
|
249
|
+
|
|
250
|
+
const rows = result.tiers.map(
|
|
251
|
+
(t) =>
|
|
252
|
+
`| \`${t.label}\` | ${t.tp} | ${t.fp} | ${t.fn} | **${t.precision}** | ${t.recall} | ` +
|
|
253
|
+
`${t.decoyFalsePositives} (${t.decoyShare}) |`,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const lines: string[] = [
|
|
257
|
+
'| tier | TP | FP | FN | precision | recall | FPs that are decoys |',
|
|
258
|
+
'|---|--:|--:|--:|--:|--:|--:|',
|
|
259
|
+
...rows,
|
|
260
|
+
'',
|
|
261
|
+
`${Object.entries(result.findingsByConfidence)
|
|
262
|
+
.map(([k, v]) => `${v} \`${k}\``)
|
|
263
|
+
.join(', ')} findings, on engine ${result.engineVersion} (${result.measuredAt}).`,
|
|
264
|
+
'',
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
if (verified && signature) {
|
|
268
|
+
const gap = pct(verified.precision) - pct(signature.precision);
|
|
269
|
+
lines.push(
|
|
270
|
+
gap > 0
|
|
271
|
+
? `**The green label is worth ${gap.toFixed(1)} points.** \`flow-verified\` runs ` +
|
|
272
|
+
`${verified.precision} against \`signature-based\` at ${signature.precision}. The ` +
|
|
273
|
+
`gap is the whole claim this tool makes; it is measured here rather than asserted.`
|
|
274
|
+
: `**The labels are not separating.** \`flow-verified\` runs ${verified.precision} ` +
|
|
275
|
+
`against \`signature-based\` at ${signature.precision}, so the green label is ` +
|
|
276
|
+
`currently buying nothing. That is a defect, not a footnote.`,
|
|
277
|
+
);
|
|
278
|
+
lines.push('');
|
|
279
|
+
|
|
280
|
+
const real = verified.tp + verified.nonDecoyFalsePositives;
|
|
281
|
+
const excl = real === 0 ? 0 : (verified.tp / real) * 100;
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* TWO SENTENCES, AND WHICH ONE IS TRUE IS NOT OURS TO CHOOSE.
|
|
285
|
+
*
|
|
286
|
+
* For a long time the green label carried a large pile of the benchmark's
|
|
287
|
+
* constant-branch decoys, so the honest thing was to print the headline AND
|
|
288
|
+
* the decoy-excluded figure, and to say plainly that neither alone was the
|
|
289
|
+
* truth.
|
|
290
|
+
*
|
|
291
|
+
* Then constant-condition evaluation landed and the decoys went to zero.
|
|
292
|
+
* Keeping the old paragraph would have had the page arguing for a
|
|
293
|
+
* distinction between two numbers that are now identical - drift caused by
|
|
294
|
+
* an improvement, which is the sneakiest kind, because nothing failed and
|
|
295
|
+
* the prose simply stopped being about anything.
|
|
296
|
+
*
|
|
297
|
+
* So the branch is derived from the measurement, like every other sentence
|
|
298
|
+
* in this file. If decoys ever come back, so does the caveat.
|
|
299
|
+
*/
|
|
300
|
+
if (verified.fp === 0) {
|
|
301
|
+
/*
|
|
302
|
+
* Zero is said in words, and so is its limit - the same rule the benchmark
|
|
303
|
+
* block follows for zero false negatives. "0 ordinary mistakes out of 456"
|
|
304
|
+
* would be arithmetic dressed as a boast.
|
|
305
|
+
*/
|
|
306
|
+
lines.push(
|
|
307
|
+
`**No false positive carries the green label here.** All ${verified.tp} ` +
|
|
308
|
+
`\`flow-verified\` claims on this benchmark are real vulnerabilities. That is a ` +
|
|
309
|
+
`statement about these scored test cases, not a promise about your code: a proof ` +
|
|
310
|
+
`means the path exists in the source, and the ways that can still be wrong are ` +
|
|
311
|
+
`the ones in the limitations list, which a synthetic benchmark does not exercise.`,
|
|
312
|
+
);
|
|
313
|
+
} else if (verified.decoyFalsePositives === 0) {
|
|
314
|
+
lines.push(
|
|
315
|
+
`**There is no footnote any more.** Not one of the ${verified.fp} false positives ` +
|
|
316
|
+
`still carrying the green label is a constant-branch decoy. Until constant ` +
|
|
317
|
+
`conditions were evaluated, that pile was most of them, and this paragraph had to ` +
|
|
318
|
+
`print two precision figures and explain why neither alone was the truth. ` +
|
|
319
|
+
`\`flow-verified\` now reads **${excl.toFixed(1)}%** with nothing set aside and ` +
|
|
320
|
+
`nothing excluded - ${verified.nonDecoyFalsePositives} ordinary mistakes out of ` +
|
|
321
|
+
`${verified.tp + verified.fp} claims.`,
|
|
322
|
+
);
|
|
323
|
+
} else {
|
|
324
|
+
lines.push(
|
|
325
|
+
`Of the ${verified.fp} false positives still carrying the green label, ` +
|
|
326
|
+
`**${verified.decoyFalsePositives} (${verified.decoyShare})** are BenchmarkJava's ` +
|
|
327
|
+
`constant-branch decoy - a path that genuinely exists inside a branch that cannot ` +
|
|
328
|
+
`run - leaving **${verified.nonDecoyFalsePositives}** that are ordinary mistakes. ` +
|
|
329
|
+
`Set the decoys aside and \`flow-verified\` precision is **${excl.toFixed(1)}%**. ` +
|
|
330
|
+
`Both figures are printed because neither alone is the truth: the first is ` +
|
|
331
|
+
`contaminated by synthetic traps, the second requires excluding cases, and a ` +
|
|
332
|
+
`reader deserves to see the size of that choice rather than inherit it.`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return lines.join('\n');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function replaceBlock(markdown: string, name: string, body: string): string {
|
|
341
|
+
const start = BLOCK_START(name);
|
|
342
|
+
const from = markdown.indexOf(start);
|
|
343
|
+
if (from === -1) throw new Error(`README has no block named "${name}"`);
|
|
344
|
+
const to = markdown.indexOf(BLOCK_END, from);
|
|
345
|
+
if (to === -1) throw new Error(`block "${name}" is not closed with ${BLOCK_END}`);
|
|
346
|
+
return (
|
|
347
|
+
markdown.slice(0, from + start.length) + '\n' + body + '\n' + markdown.slice(to)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Read the current contents of one block, for comparison. */
|
|
352
|
+
export function readBlock(markdown: string, name: string): string | null {
|
|
353
|
+
const start = BLOCK_START(name);
|
|
354
|
+
const from = markdown.indexOf(start);
|
|
355
|
+
if (from === -1) return null;
|
|
356
|
+
const to = markdown.indexOf(BLOCK_END, from);
|
|
357
|
+
if (to === -1) return null;
|
|
358
|
+
return markdown.slice(from + start.length, to).trim();
|
|
359
|
+
}
|