@sikkuumiii/defuse 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
@@ -0,0 +1,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