@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
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* THE COMMAND-LINE INTERFACE
|
|
4
|
+
*
|
|
5
|
+
* Hand-rolled argument parsing, no library. It is about 60 lines, you can read
|
|
6
|
+
* all of them, and it means the tool has exactly two runtime dependencies -
|
|
7
|
+
* both of them the parser.
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* defuse scan <path> scan a file or directory
|
|
11
|
+
* defuse ast <file> print the syntax tree (learning / debugging)
|
|
12
|
+
* defuse rules list the rules and what they do NOT cover
|
|
13
|
+
* defuse doctor check every grammar loads and every query compiles
|
|
14
|
+
*
|
|
15
|
+
* EXIT CODES matter for CI:
|
|
16
|
+
* 0 nothing at or above the --fail-on threshold
|
|
17
|
+
* 1 findings at or above the threshold
|
|
18
|
+
* 2 the tool itself failed (bad arguments, unreadable path, internal error)
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { stat } from 'node:fs/promises';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { SEVERITY_ORDER } from './core/finding.js';
|
|
24
|
+
import { buildCoverageReport } from './core/coverage.js';
|
|
25
|
+
import { LICENCE_LINE, LICENCE_SPDX } from './core/licence.js';
|
|
26
|
+
import { scan } from './engine/scan.js';
|
|
27
|
+
import { SHAPE_QUERY_SOURCES } from './engine/shapes.js';
|
|
28
|
+
import { dumpAst } from './parse/astDump.js';
|
|
29
|
+
import { detectLanguage, LANGUAGES } from './parse/languages.js';
|
|
30
|
+
import { getLanguage, initEngine } from './parse/parser.js';
|
|
31
|
+
import { parseFile, useNodeGrammars } from './parse/node-grammars.js';
|
|
32
|
+
import { checkDependencies, findLockfiles, queryOsv } from './platform/deps.js';
|
|
33
|
+
import { RUNTIME_VERSION_RULE } from './platform/runtime-version.js';
|
|
34
|
+
import { ALL_RULES, validateRegistry } from './rules/registry.js';
|
|
35
|
+
import { color, g, setAsciiMode, wrapText } from './report/colors.js';
|
|
36
|
+
import { BANNER_WIDTH, bannerLines } from './report/mark.js';
|
|
37
|
+
import { renderHuman } from './report/human.js';
|
|
38
|
+
import { renderJson } from './report/json.js';
|
|
39
|
+
import { renderSarif } from './report/sarif.js';
|
|
40
|
+
import { renderHtmlReport } from './report/html-report.js';
|
|
41
|
+
/**
|
|
42
|
+
* Flags that TAKE A VALUE, so `--explain xss` works as well as `--explain=xss`.
|
|
43
|
+
*
|
|
44
|
+
* Without this list the parser cannot tell `--explain xss` (one flag with a
|
|
45
|
+
* value) from `--json somefile.js` (a boolean flag followed by a path) - both
|
|
46
|
+
* are "a flag then a word". Every real CLI parser solves it the same way: by
|
|
47
|
+
* being told in advance which flags expect something after them.
|
|
48
|
+
*/
|
|
49
|
+
const VALUE_FLAGS = new Set([
|
|
50
|
+
'explain',
|
|
51
|
+
'only',
|
|
52
|
+
'exclude',
|
|
53
|
+
'min-severity',
|
|
54
|
+
'fail-on',
|
|
55
|
+
'depth',
|
|
56
|
+
'max-lines',
|
|
57
|
+
'report',
|
|
58
|
+
'report-name',
|
|
59
|
+
'report-by',
|
|
60
|
+
'report-note',
|
|
61
|
+
]);
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const positional = [];
|
|
64
|
+
const flags = new Map();
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const token = argv[i];
|
|
67
|
+
if (token === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
if (token.startsWith('--')) {
|
|
70
|
+
const body = token.slice(2);
|
|
71
|
+
const eq = body.indexOf('=');
|
|
72
|
+
if (eq !== -1) {
|
|
73
|
+
flags.set(body.slice(0, eq), body.slice(eq + 1));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// `--explain xss`: consume the next token as this flag's value, but only
|
|
77
|
+
// if it exists and is not itself a flag.
|
|
78
|
+
const next = argv[i + 1];
|
|
79
|
+
if (VALUE_FLAGS.has(body) && next !== undefined && !next.startsWith('-')) {
|
|
80
|
+
flags.set(body, next);
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
flags.set(body, true);
|
|
85
|
+
}
|
|
86
|
+
else if (token.startsWith('-') && token.length > 1) {
|
|
87
|
+
flags.set(token.slice(1), true);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
positional.push(token);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { command: positional[0] ?? '', positional: positional.slice(1), flags };
|
|
94
|
+
}
|
|
95
|
+
function flagString(args, name) {
|
|
96
|
+
const value = args.flags.get(name);
|
|
97
|
+
return typeof value === 'string' ? value : undefined;
|
|
98
|
+
}
|
|
99
|
+
const SEVERITIES = new Set(Object.keys(SEVERITY_ORDER));
|
|
100
|
+
function parseSeverity(value, fallback) {
|
|
101
|
+
if (!value)
|
|
102
|
+
return fallback;
|
|
103
|
+
if (!SEVERITIES.has(value)) {
|
|
104
|
+
throw new Error(`unknown severity '${value}'. Use one of: ${[...SEVERITIES].join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
const CONFIG_NAME = '.defuse.json';
|
|
109
|
+
function loadConfig(startDir) {
|
|
110
|
+
for (const dir of [startDir, process.cwd()]) {
|
|
111
|
+
const candidate = path.join(dir, CONFIG_NAME);
|
|
112
|
+
if (!existsSync(candidate))
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
116
|
+
return { config: parsed, path: candidate };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// A broken config is reported, never ignored. Silently falling back to
|
|
120
|
+
// defaults would mean a repo believing it has a policy that never ran.
|
|
121
|
+
console.error(color.red(`error: ${candidate} is not valid JSON — ${error.message}`));
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { config: {}, path: null };
|
|
126
|
+
}
|
|
127
|
+
/* ------------------------------------------------------------------ help -- */
|
|
128
|
+
function printHelp() {
|
|
129
|
+
const b = color.bold;
|
|
130
|
+
console.log(`
|
|
131
|
+
${b('Defuse')} — honesty-first static application security testing
|
|
132
|
+
${color.dim(`Signature matching plus data-flow verification, in all ${LANGUAGES.length} languages.`)}
|
|
133
|
+
${color.dim('Flows are followed across functions and, where imports resolve, across files.')}
|
|
134
|
+
|
|
135
|
+
${b('USAGE')}
|
|
136
|
+
defuse scan <path> [options]
|
|
137
|
+
defuse ast <file> [options]
|
|
138
|
+
defuse rules [--explain <rule-id>]
|
|
139
|
+
defuse doctor
|
|
140
|
+
defuse deps [path] [--json] known advisories for pinned library versions.
|
|
141
|
+
The one command that uses the network: it sends
|
|
142
|
+
package names and versions (no code) to osv.dev.
|
|
143
|
+
|
|
144
|
+
${b('SCAN OPTIONS')}
|
|
145
|
+
--full the long terminal report: reasoning and limits on every
|
|
146
|
+
finding, the gauge arithmetic, OWASP table and every
|
|
147
|
+
unchecked rule by name. Default is a short report.
|
|
148
|
+
--json machine-readable output (superset of the terminal report)
|
|
149
|
+
--sarif SARIF 2.1.0, for GitHub/GitLab code scanning. A
|
|
150
|
+
flow-verified finding carries its traced path as a
|
|
151
|
+
SARIF codeFlow; a signature-based one carries none,
|
|
152
|
+
and every message opens with which it is.
|
|
153
|
+
--report=<file.html> also write a report for the project owner: one
|
|
154
|
+
self-contained HTML file (nothing loaded from the
|
|
155
|
+
web) that prints to PDF - proven problems with their
|
|
156
|
+
paths, what to review, what was cleared, and what
|
|
157
|
+
was not checked
|
|
158
|
+
--report-name=<name> the project name shown on that report
|
|
159
|
+
--report-by=<name> "Prepared by" on that report
|
|
160
|
+
--report-note=<text> one line under the report's header, e.g. that it is a sample or a draft
|
|
161
|
+
--compact with --json or --sarif, emit a single line
|
|
162
|
+
--min-severity=<level> hide findings below this level in the terminal report
|
|
163
|
+
(they remain in --json). critical|high|medium|low|info
|
|
164
|
+
--fail-on=<level> exit code 1 at or above this level. default: high
|
|
165
|
+
use --fail-on=none to always exit 0
|
|
166
|
+
--only=<ids> comma-separated rule ids, e.g. --only=sql-injection,xss
|
|
167
|
+
--exclude=<globs> comma-separated paths to skip. A bare word matches any
|
|
168
|
+
directory of that name; * and ** work as usual.
|
|
169
|
+
e.g. --exclude=tests,vendor,**/*.min.js
|
|
170
|
+
--no-config ignore .defuse.json
|
|
171
|
+
--deps also check pinned library versions against osv.dev.
|
|
172
|
+
Off by default: it sends package names and versions
|
|
173
|
+
(no code) off the machine. Same as defuse deps.
|
|
174
|
+
--show-suppressed list findings silenced by defuse:ignore comments
|
|
175
|
+
--coverage-matrix print the full rule x language support table
|
|
176
|
+
--no-cross-file do not resolve imports; analyse each file alone.
|
|
177
|
+
Faster, at the cost of every finding that crosses a
|
|
178
|
+
module. It does NOT save memory - that was measured
|
|
179
|
+
and was not true. Set DEFUSE_TREE_BUDGET_MB to trade
|
|
180
|
+
memory for speed instead.
|
|
181
|
+
--ascii plain ASCII output - no box-drawing characters.
|
|
182
|
+
Chosen automatically when redirecting on Windows.
|
|
183
|
+
--quiet suppress the banner and the per-file progress line
|
|
184
|
+
|
|
185
|
+
${b('AST OPTIONS')}
|
|
186
|
+
--depth=<n> how deep to print (default 12)
|
|
187
|
+
--anonymous include punctuation/keyword nodes
|
|
188
|
+
--max-lines=<n> truncate output (default 400)
|
|
189
|
+
|
|
190
|
+
${b('ENVIRONMENT')}
|
|
191
|
+
DEFUSE_TREE_BUDGET_MB how much memory syntax trees may hold before the
|
|
192
|
+
least recently used one is freed and the file parsed
|
|
193
|
+
again on demand. Default 1024. Lower it to finish on
|
|
194
|
+
a small machine; raise it to go faster. The findings
|
|
195
|
+
are the same either way - a test fails if they are
|
|
196
|
+
not - and the report says when files were re-parsed.
|
|
197
|
+
|
|
198
|
+
${b('EXIT CODES')}
|
|
199
|
+
0 nothing at or above the --fail-on threshold
|
|
200
|
+
1 findings at or above it
|
|
201
|
+
2 the tool failed
|
|
202
|
+
|
|
203
|
+
${b('THE POINT OF THIS TOOL')}
|
|
204
|
+
Every finding is labelled ${color.yellow('signature-based')} or ${color.green('flow-verified')}.
|
|
205
|
+
${color.yellow('signature-based')} = we matched a shape. Unverified, may be a false positive.
|
|
206
|
+
${color.green('flow-verified')} = we followed attacker data from its source into the sink
|
|
207
|
+
and print every hop, so you can check the claim yourself.
|
|
208
|
+
The report also lists what was NOT checked. That is the product.
|
|
209
|
+
|
|
210
|
+
${b('LICENCE')}
|
|
211
|
+
${LICENCE_LINE}
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
/* ------------------------------------------------------------------ scan -- */
|
|
215
|
+
async function commandScan(args) {
|
|
216
|
+
const target = args.positional[0];
|
|
217
|
+
if (!target) {
|
|
218
|
+
console.error(color.red('error: scan needs a path. usage: defuse scan <path>'));
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
/* Extra paths used to be dropped in silence. `scan src ui docs` printed a
|
|
222
|
+
* clean, confident report for `src` and said nothing about the two
|
|
223
|
+
* directories it never opened - a scanner reporting on less than it was
|
|
224
|
+
* asked about, without saying so, which is the one failure mode this whole
|
|
225
|
+
* project is built to avoid. Refuse instead of quietly narrowing. */
|
|
226
|
+
if (args.positional.length > 1) {
|
|
227
|
+
console.error(color.red(`error: scan takes ONE path, but ${args.positional.length} were given ` +
|
|
228
|
+
`(${args.positional.join(', ')}).`));
|
|
229
|
+
console.error(' Scan the common parent and narrow with --exclude=<globs>, ' +
|
|
230
|
+
'or run the scans separately.');
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
let resolved;
|
|
234
|
+
try {
|
|
235
|
+
resolved = path.resolve(target);
|
|
236
|
+
await stat(resolved);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
console.error(color.red(`error: cannot read '${target}'`));
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
const asJson = args.flags.has('json');
|
|
243
|
+
const asSarif = args.flags.has('sarif');
|
|
244
|
+
const quiet = args.flags.has('quiet') || asJson || asSarif;
|
|
245
|
+
// Decoration for a person at a terminal only: never in piped output, CI logs,
|
|
246
|
+
// or a window too narrow to hold it.
|
|
247
|
+
if (!quiet && process.stdout.isTTY && (process.stdout.columns ?? 0) >= BANNER_WIDTH) {
|
|
248
|
+
const purple = (s) => (color.enabled ? `\x1b[38;2;121;44;247m${s}\x1b[39m` : s);
|
|
249
|
+
console.log(`\n${bannerLines().map(purple).join('\n')}`);
|
|
250
|
+
}
|
|
251
|
+
const configForOnly = args.flags.has('no-config')
|
|
252
|
+
? {}
|
|
253
|
+
: loadConfig(statSync(resolved).isDirectory() ? resolved : path.dirname(resolved)).config;
|
|
254
|
+
const only = flagString(args, 'only')?.split(',').map((s) => s.trim()).filter(Boolean) ??
|
|
255
|
+
configForOnly.only ??
|
|
256
|
+
[];
|
|
257
|
+
for (const id of only) {
|
|
258
|
+
if (!ALL_RULES.some((r) => r.id === id) && id !== RUNTIME_VERSION_RULE.id) {
|
|
259
|
+
console.error(color.red(`error: unknown rule '${id}'. Known: ${[...ALL_RULES.map((r) => r.id), RUNTIME_VERSION_RULE.id].join(', ')}`));
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// The command line wins over the file; the file wins over the defaults.
|
|
264
|
+
const { config, path: configPath } = args.flags.has('no-config')
|
|
265
|
+
? { config: {}, path: null }
|
|
266
|
+
: loadConfig(statSync(resolved).isDirectory() ? resolved : path.dirname(resolved));
|
|
267
|
+
if (configPath && !quiet) {
|
|
268
|
+
console.error(color.dim(`using ${path.relative(process.cwd(), configPath) || CONFIG_NAME}`));
|
|
269
|
+
}
|
|
270
|
+
const minSeverity = parseSeverity(flagString(args, 'min-severity') ?? config.minSeverity, 'info');
|
|
271
|
+
const failOnRaw = flagString(args, 'fail-on') ?? config.failOn ?? 'high';
|
|
272
|
+
const failOn = failOnRaw === 'none' ? null : parseSeverity(failOnRaw, 'high');
|
|
273
|
+
const exclude = flagString(args, 'exclude')?.split(',').map((s) => s.trim()).filter(Boolean) ??
|
|
274
|
+
config.exclude ??
|
|
275
|
+
[];
|
|
276
|
+
let result = await scan(resolved, {
|
|
277
|
+
only,
|
|
278
|
+
exclude,
|
|
279
|
+
noCrossFile: args.flags.has('no-cross-file'),
|
|
280
|
+
...(quiet
|
|
281
|
+
? {}
|
|
282
|
+
: {
|
|
283
|
+
// analyze() reports 2N steps for N files: every file is read once,
|
|
284
|
+
// then checked once. "scanning 3/4" for a two-file folder looked like
|
|
285
|
+
// a counting bug, so the two passes are named and counted per file.
|
|
286
|
+
onProgress: (done, total, file) => {
|
|
287
|
+
const files = total / 2;
|
|
288
|
+
const step = done > files ? `checking ${done - files}` : `reading ${done}`;
|
|
289
|
+
process.stderr.write(`\r${color.dim(`${step}/${files} ${path.basename(file).slice(0, 40)}`.padEnd(70))}`);
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
if (!quiet)
|
|
294
|
+
process.stderr.write(`\r${' '.repeat(70)}\r`);
|
|
295
|
+
/*
|
|
296
|
+
* --deps: the one network call, added here rather than inside scan() so the
|
|
297
|
+
* browser page (which runs scan's engine) can never make it. The findings
|
|
298
|
+
* join the same list, so every report and SARIF carries them unchanged.
|
|
299
|
+
*/
|
|
300
|
+
if (args.flags.has('deps')) {
|
|
301
|
+
if (!quiet)
|
|
302
|
+
process.stderr.write(color.dim('sending package names and versions (no code) to api.osv.dev...\n'));
|
|
303
|
+
try {
|
|
304
|
+
const deps = await checkDependencies(statSync(resolved).isDirectory() ? resolved : path.dirname(resolved), (f) => path.relative(process.cwd(), f) || f, exclude);
|
|
305
|
+
result = { ...result, findings: [...result.findings, ...deps.findings], dependencies: deps.report };
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error(color.red(`error: --deps lookup failed, so the scan was not reported - ${error.message}`));
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/*
|
|
313
|
+
* The client report is WRITTEN BY THE TOOL, never piped. On Windows,
|
|
314
|
+
* PowerShell's `>` writes UTF-16, and a report redirected that way opens as
|
|
315
|
+
* noise in a browser - so the file is written here, as UTF-8, and the
|
|
316
|
+
* terminal says where it went.
|
|
317
|
+
*/
|
|
318
|
+
const reportPath = flagString(args, 'report');
|
|
319
|
+
if (args.flags.has('report') && !reportPath) {
|
|
320
|
+
console.error(color.red('error: --report needs a file name, e.g. --report=report.html'));
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
if (reportPath) {
|
|
324
|
+
const html = renderHtmlReport(result, {
|
|
325
|
+
generatedAt: new Date(),
|
|
326
|
+
root: path.relative(process.cwd(), resolved),
|
|
327
|
+
...(flagString(args, 'report-name') ? { projectName: flagString(args, 'report-name') } : {}),
|
|
328
|
+
...(flagString(args, 'report-by') ? { preparedBy: flagString(args, 'report-by') } : {}),
|
|
329
|
+
...(flagString(args, 'report-note') ? { note: flagString(args, 'report-note') } : {}),
|
|
330
|
+
});
|
|
331
|
+
writeFileSync(path.resolve(reportPath), html, 'utf8');
|
|
332
|
+
console.error(color.dim(`client report written to ${path.resolve(reportPath)}`));
|
|
333
|
+
}
|
|
334
|
+
if (asSarif) {
|
|
335
|
+
console.log(renderSarif(result, !args.flags.has('compact')));
|
|
336
|
+
}
|
|
337
|
+
else if (asJson) {
|
|
338
|
+
console.log(renderJson(result, !args.flags.has('compact')));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log(renderHuman(result, {
|
|
342
|
+
showSuppressed: args.flags.has('show-suppressed'),
|
|
343
|
+
showCoverageMatrix: args.flags.has('coverage-matrix'),
|
|
344
|
+
full: args.flags.has('full'),
|
|
345
|
+
minSeverity,
|
|
346
|
+
target: path.relative(process.cwd(), resolved) || '.',
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
if (!failOn)
|
|
350
|
+
return 0;
|
|
351
|
+
const triggering = result.findings.filter((f) => SEVERITY_ORDER[f.severity] >= SEVERITY_ORDER[failOn]);
|
|
352
|
+
return triggering.length > 0 ? 1 : 0;
|
|
353
|
+
}
|
|
354
|
+
/* ------------------------------------------------------------------- ast -- */
|
|
355
|
+
async function commandAst(args) {
|
|
356
|
+
const target = args.positional[0];
|
|
357
|
+
if (!target) {
|
|
358
|
+
console.error(color.red('error: ast needs a file. usage: defuse ast <file>'));
|
|
359
|
+
return 2;
|
|
360
|
+
}
|
|
361
|
+
useNodeGrammars();
|
|
362
|
+
const outcome = await parseFile(path.resolve(target));
|
|
363
|
+
if (outcome.status === 'skipped') {
|
|
364
|
+
console.error(color.red(`error: ${outcome.reason}`));
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
const file = outcome.file;
|
|
368
|
+
console.log(color.dim(`${file.path}\n${file.language.displayName} (grammar: ${file.grammar}, detected by ${file.detectedBy})\n`));
|
|
369
|
+
console.log(dumpAst(file.root, {
|
|
370
|
+
includeAnonymous: args.flags.has('anonymous'),
|
|
371
|
+
maxDepth: Number(flagString(args, 'depth') ?? 12),
|
|
372
|
+
maxLines: Number(flagString(args, 'max-lines') ?? 400),
|
|
373
|
+
}));
|
|
374
|
+
if (file.parseIssues.length > 0) {
|
|
375
|
+
console.log(color.yellow(`\n${file.parseIssues.length} parse issue(s):`));
|
|
376
|
+
for (const issue of file.parseIssues) {
|
|
377
|
+
console.log(color.dim(` ${issue.kind} at ${issue.line}:${issue.column} ${issue.text}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
/* ----------------------------------------------------------------- rules -- */
|
|
383
|
+
function commandRules(args) {
|
|
384
|
+
const explain = flagString(args, 'explain');
|
|
385
|
+
if (explain) {
|
|
386
|
+
const rule = ALL_RULES.find((r) => r.id === explain);
|
|
387
|
+
if (!rule) {
|
|
388
|
+
console.error(color.red(`error: no rule '${explain}'`));
|
|
389
|
+
return 2;
|
|
390
|
+
}
|
|
391
|
+
console.log(`\n${color.bold(rule.name)} ${color.dim(`(${rule.id})`)}`);
|
|
392
|
+
console.log(color.dim(`${rule.cwe} · ${rule.owasp} · severity ${rule.severity}\n`));
|
|
393
|
+
console.log(`${color.cyan('In one line')}\n ${wrapText(rule.summary, 86, ' ')}\n`);
|
|
394
|
+
console.log(`${color.cyan('The vulnerability')}\n ${wrapText(rule.explanation, 86, ' ')}\n`);
|
|
395
|
+
console.log(`${color.green('How to fix it')}\n ${wrapText(rule.fix, 86, ' ')}\n`);
|
|
396
|
+
console.log(`${color.yellow('What this rule cannot tell you')}\n ${wrapText(rule.limitations, 86, ' ')}\n`);
|
|
397
|
+
console.log(color.cyan('Per-language support'));
|
|
398
|
+
for (const language of LANGUAGES) {
|
|
399
|
+
const support = rule.support[language.id];
|
|
400
|
+
const status = support?.status ?? 'not-implemented';
|
|
401
|
+
const mark = status === 'implemented' ? color.green(g('dotFull')) : status === 'partial' ? color.yellow(g('dotHalf')) : color.gray(g('dotEmpty'));
|
|
402
|
+
console.log(` ${mark} ${language.displayName.padEnd(12)} ${color.dim(status)}`);
|
|
403
|
+
console.log(` ${color.dim(wrapText(support?.note ?? 'no pattern for this language', 82, ' '))}`);
|
|
404
|
+
}
|
|
405
|
+
console.log('');
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
const coverage = buildCoverageReport();
|
|
409
|
+
console.log(`\n${color.bold(`${ALL_RULES.length} rules`)} ${color.dim('· run with --explain <id> for the full write-up')}\n`);
|
|
410
|
+
for (const rule of ALL_RULES) {
|
|
411
|
+
const cells = coverage.matrix.filter((c) => c.ruleId === rule.id);
|
|
412
|
+
const marks = cells
|
|
413
|
+
.map((c) => c.status === 'implemented' ? color.green(g('dotFull')) : c.status === 'partial' ? color.yellow(g('dotHalf')) : color.gray('○'))
|
|
414
|
+
.join('');
|
|
415
|
+
console.log(` ${marks} ${color.bold(rule.id.padEnd(20))} ${color.dim(`${rule.cwe} · ${rule.severity}`)}`);
|
|
416
|
+
console.log(` ${color.dim(rule.name)}`);
|
|
417
|
+
}
|
|
418
|
+
console.log(color.dim(`\n legend: ${color.green(g('dotFull'))} implemented ${color.yellow(g('dotHalf'))} partial ${color.gray(g('dotEmpty'))} not implemented` +
|
|
419
|
+
`\n columns: ${LANGUAGES.map((l) => l.id).join(' ')}\n`));
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
/* ------------------------------------------------------------------ deps -- */
|
|
423
|
+
async function commandDeps(args) {
|
|
424
|
+
const target = args.positional[0] ?? '.';
|
|
425
|
+
try {
|
|
426
|
+
await stat(target);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
console.error(color.red(`error: cannot read '${target}'`));
|
|
430
|
+
return 2;
|
|
431
|
+
}
|
|
432
|
+
const asJson = args.flags.has('json');
|
|
433
|
+
const { read, unread } = findLockfiles(path.resolve(target));
|
|
434
|
+
const rel = (f) => path.relative(process.cwd(), f) || f;
|
|
435
|
+
const pinned = read.flatMap((r) => r.packages);
|
|
436
|
+
// Said BEFORE anything is sent, on stderr so --json stays clean.
|
|
437
|
+
console.error(color.dim(pinned.length
|
|
438
|
+
? `Sending ${pinned.length} package names and versions (no code) to api.osv.dev...`
|
|
439
|
+
: 'No pinned packages found - nothing sent anywhere.'));
|
|
440
|
+
let result;
|
|
441
|
+
try {
|
|
442
|
+
result = pinned.length ? await queryOsv(pinned) : { hits: [], checked: 0, incomplete: [] };
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.error(color.red(`error: the advisory lookup failed, so nothing was checked - ${error.message}`));
|
|
446
|
+
return 2;
|
|
447
|
+
}
|
|
448
|
+
if (asJson) {
|
|
449
|
+
console.log(JSON.stringify({
|
|
450
|
+
source: 'api.osv.dev',
|
|
451
|
+
checkedAt: new Date().toISOString(),
|
|
452
|
+
confidence: 'signature-based',
|
|
453
|
+
read: read.map((r) => ({ file: rel(r.file), packages: r.packages.length, skipped: r.skipped })),
|
|
454
|
+
notRead: unread.map(rel),
|
|
455
|
+
checked: result.checked,
|
|
456
|
+
incomplete: result.incomplete,
|
|
457
|
+
hits: result.hits.map((h) => ({ ...h, pkg: { ...h.pkg, file: rel(h.pkg.file) } })),
|
|
458
|
+
}, null, args.flags.has('compact') ? 0 : 2));
|
|
459
|
+
return result.hits.length ? 1 : 0;
|
|
460
|
+
}
|
|
461
|
+
const sev = (s) => (s === 'critical' || s === 'high' ? color.red : s === 'medium' ? color.yellow : color.dim)(s.toUpperCase().padEnd(8));
|
|
462
|
+
console.log(`\n${color.bold('Dependency check')} ${color.dim('- known advisories for pinned library versions, from osv.dev')}\n`);
|
|
463
|
+
for (const r of read)
|
|
464
|
+
console.log(` read ${rel(r.file)} ${color.dim(`(${r.packages.length} packages)`)}`);
|
|
465
|
+
for (const f of unread)
|
|
466
|
+
console.log(` ${color.yellow('not read')} ${rel(f)} ${color.dim('- this lockfile format is not supported yet')}`);
|
|
467
|
+
if (!read.length && !unread.length)
|
|
468
|
+
console.log(color.dim(' no lockfiles found (package-lock.json, composer.lock, requirements.txt, go.mod)'));
|
|
469
|
+
for (const h of result.hits) {
|
|
470
|
+
console.log(`\n ${color.bold(`${h.pkg.name} ${h.pkg.version}`)} ${color.dim(`${h.pkg.ecosystem} · ${rel(h.pkg.file)}`)}`);
|
|
471
|
+
for (const a of h.advisories) {
|
|
472
|
+
const ids = [a.id, ...a.aliases.filter((x) => x.startsWith('CVE-'))].join(' ');
|
|
473
|
+
const fix = a.fixed.length ? color.green(`fixed in ${a.fixed.join(', ')}`) : color.dim('no fixed version published');
|
|
474
|
+
console.log(` ${sev(a.severity)} ${ids}\n ${a.summary}\n ${fix}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const skipped = read.flatMap((r) => r.skipped.map((s) => `${rel(r.file)}: ${s}`));
|
|
478
|
+
if (skipped.length || result.incomplete.length || unread.length) {
|
|
479
|
+
console.log(`\n${color.bold(' What this check did NOT read')}`);
|
|
480
|
+
for (const s of [...skipped, ...result.incomplete])
|
|
481
|
+
console.log(` ${g('cross')} ${s}`);
|
|
482
|
+
for (const f of unread)
|
|
483
|
+
console.log(` ${g('cross')} ${rel(f)}: format not supported yet`);
|
|
484
|
+
}
|
|
485
|
+
console.log(`\n ${result.checked} distinct package versions checked, ${color.bold(String(result.hits.length))} with known advisories.\n` +
|
|
486
|
+
color.dim(' signature-based: an advisory matching a pinned version means the vulnerable code is\n' +
|
|
487
|
+
' installed - not that this project ever calls it.\n'));
|
|
488
|
+
return result.hits.length ? 1 : 0;
|
|
489
|
+
}
|
|
490
|
+
/* ---------------------------------------------------------------- doctor -- */
|
|
491
|
+
/**
|
|
492
|
+
* A self-check. The nastiest failure a scanner can have is a query with a
|
|
493
|
+
* typo'd node name: it compiles conceptually, matches nothing, and reports a
|
|
494
|
+
* clean scan forever. `doctor` compiles every query against every grammar so
|
|
495
|
+
* that failure is loud instead of silent.
|
|
496
|
+
*/
|
|
497
|
+
async function commandDoctor() {
|
|
498
|
+
useNodeGrammars();
|
|
499
|
+
console.log(`\n${color.bold('Defuse self-check')}\n`);
|
|
500
|
+
let failures = 0;
|
|
501
|
+
try {
|
|
502
|
+
validateRegistry();
|
|
503
|
+
console.log(` ${color.green(g('tick'))} rule registry satisfies the honesty contract`);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
failures++;
|
|
507
|
+
console.log(` ${color.red(g('cross'))} ${error.message}`);
|
|
508
|
+
}
|
|
509
|
+
await initEngine();
|
|
510
|
+
const { Query } = await import('web-tree-sitter');
|
|
511
|
+
for (const language of LANGUAGES) {
|
|
512
|
+
const grammars = [language.grammar, ...(language.dialects ?? []).map((d) => d.grammar)];
|
|
513
|
+
for (const grammar of grammars) {
|
|
514
|
+
try {
|
|
515
|
+
const loaded = await getLanguage(grammar);
|
|
516
|
+
for (const kind of ['calls', 'assignments']) {
|
|
517
|
+
new Query(loaded, SHAPE_QUERY_SOURCES[kind][language.id]);
|
|
518
|
+
}
|
|
519
|
+
console.log(` ${color.green(g('tick'))} ${language.displayName.padEnd(12)} grammar '${grammar}' loads; shape queries compile`);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
failures++;
|
|
523
|
+
console.log(` ${color.red(g('cross'))} ${language.displayName.padEnd(12)} grammar '${grammar}': ${error.message}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Language detection sanity: every declared extension must map back correctly.
|
|
528
|
+
for (const language of LANGUAGES) {
|
|
529
|
+
const all = [...language.extensions, ...(language.dialects ?? []).flatMap((d) => d.extensions)];
|
|
530
|
+
for (const ext of all) {
|
|
531
|
+
const match = detectLanguage(`example${ext}`);
|
|
532
|
+
if (match?.spec.id !== language.id) {
|
|
533
|
+
failures++;
|
|
534
|
+
console.log(` ${color.red(g('cross'))} extension ${ext} does not resolve to ${language.id}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (failures === 0)
|
|
539
|
+
console.log(` ${color.green(g('tick'))} all declared extensions resolve to a language`);
|
|
540
|
+
console.log(failures === 0
|
|
541
|
+
? `\n ${color.green('All checks passed.')}\n`
|
|
542
|
+
: `\n ${color.red(`${failures} check(s) failed.`)}\n`);
|
|
543
|
+
return failures === 0 ? 0 : 1;
|
|
544
|
+
}
|
|
545
|
+
/* ------------------------------------------------------------------ main -- */
|
|
546
|
+
async function main() {
|
|
547
|
+
const args = parseArgs(process.argv.slice(2));
|
|
548
|
+
if (args.flags.has('ascii'))
|
|
549
|
+
setAsciiMode(true);
|
|
550
|
+
if (args.flags.has('version') || args.flags.has('v')) {
|
|
551
|
+
console.log(buildCoverageReport().engine.version);
|
|
552
|
+
console.log(LICENCE_SPDX);
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
if (args.command === '' || args.flags.has('help') || args.flags.has('h')) {
|
|
556
|
+
printHelp();
|
|
557
|
+
return args.command === '' ? 2 : 0;
|
|
558
|
+
}
|
|
559
|
+
switch (args.command) {
|
|
560
|
+
case 'scan':
|
|
561
|
+
return commandScan(args);
|
|
562
|
+
case 'ast':
|
|
563
|
+
return commandAst(args);
|
|
564
|
+
case 'rules':
|
|
565
|
+
return commandRules(args);
|
|
566
|
+
case 'doctor':
|
|
567
|
+
return commandDoctor();
|
|
568
|
+
case 'deps':
|
|
569
|
+
return commandDeps(args);
|
|
570
|
+
default:
|
|
571
|
+
console.error(color.red(`error: unknown command '${args.command}'`));
|
|
572
|
+
printHelp();
|
|
573
|
+
return 2;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
main()
|
|
577
|
+
.then((code) => {
|
|
578
|
+
process.exitCode = code;
|
|
579
|
+
})
|
|
580
|
+
.catch((error) => {
|
|
581
|
+
const message = error.message ?? String(error);
|
|
582
|
+
/*
|
|
583
|
+
* `Aborted()` is the WebAssembly heap giving up, and on its own it tells
|
|
584
|
+
* the reader nothing at all.
|
|
585
|
+
*
|
|
586
|
+
* Every syntax tree tree-sitter builds lives in a WASM heap that JavaScript
|
|
587
|
+
* garbage collection cannot reach, and nothing frees them during a scan
|
|
588
|
+
* because cross-file tracing has to be able to resolve into any file at any
|
|
589
|
+
* time. Memory therefore grows with total SOURCE BYTES - measured at 60-70x
|
|
590
|
+
* - and past roughly 120MB on an 8GB machine the heap aborts or the
|
|
591
|
+
* operating system kills the process.
|
|
592
|
+
*
|
|
593
|
+
* The failure is total: no findings, no partial report, nothing. Somebody
|
|
594
|
+
* hitting this deserves to know what happened and what to do instead of a
|
|
595
|
+
* bare `Aborted()`, which is what they got the first time it happened to a
|
|
596
|
+
* real user scanning VS Code.
|
|
597
|
+
*/
|
|
598
|
+
if (/Aborted|Maximum call stack|heap out of memory|allocation failed/i.test(message)) {
|
|
599
|
+
console.error(color.red('\nout of memory - the scan did not finish and produced no report.'));
|
|
600
|
+
console.error(color.dim('\n Memory grows with the total SIZE of the source, not the file count: about\n' +
|
|
601
|
+
' 60-70x the bytes on disk, because every parsed syntax tree is held for the\n' +
|
|
602
|
+
' whole scan so cross-file tracing can resolve into it. On an 8GB machine that\n' +
|
|
603
|
+
' is a ceiling of roughly 120MB of source.\n' +
|
|
604
|
+
'\n Scan one package at a time instead:\n' +
|
|
605
|
+
' scan src/vs/base scan src/vs/platform scan src/vs/editor\n' +
|
|
606
|
+
'\n --exclude=tests,node_modules also helps, and node --max-old-space-size=8192\n' +
|
|
607
|
+
' buys some headroom. This is a known limitation, recorded in the report\'s\n' +
|
|
608
|
+
' "not implemented" list, and it needs a redesign rather than a flag.'));
|
|
609
|
+
if (process.env['NS1_DEBUG'])
|
|
610
|
+
console.error(error.stack);
|
|
611
|
+
process.exitCode = 2;
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
console.error(color.red(`\ninternal error: ${message}`));
|
|
615
|
+
if (process.env['NS1_DEBUG'])
|
|
616
|
+
console.error(error.stack);
|
|
617
|
+
process.exitCode = 2;
|
|
618
|
+
});
|
|
619
|
+
//# sourceMappingURL=cli.js.map
|