@cdxgen/cdxgen 13.2.0 → 13.3.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -1,17 +1,29 @@
1
- import { lstatSync, readFileSync } from "node:fs";
1
+ import {
2
+ lstatSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ realpathSync,
6
+ statSync,
7
+ } from "node:fs";
2
8
  import { tmpdir, totalmem } from "node:os";
3
9
  import { delimiter as _delimiter, dirname, join, resolve } from "node:path";
4
10
  import process from "node:process";
5
11
 
6
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
12
+ import {
13
+ DEBUG_MODE,
14
+ isSecureMode,
15
+ readEnvironmentVariable,
16
+ } from "../core/activity.js";
7
17
  import {
8
18
  safeExistsSync,
9
19
  safeMkdtempSync,
10
20
  safeRmSync,
11
21
  safeSpawnSync,
22
+ TIMEOUT_MS,
12
23
  } from "../core/fs.js";
13
24
  import { TRACE_MODE } from "../core/logger.js";
14
25
  import { dirNameStr, isWin } from "../core/paths.js";
26
+ import { readJsonFile } from "../parsers/largeJson.js";
15
27
 
16
28
  const ASTGEN_DEFAULT_IGNORE_DIRS = [
17
29
  "venv",
@@ -57,6 +69,256 @@ export const ATOM_NATIVE_PACKAGES = new Set([
57
69
 
58
70
  const ATOM_PHP_LANGUAGES = new Set(["php"]);
59
71
 
72
+ /** atom `-l` values that parse C/C++ headers without function bodies. */
73
+ const ATOM_HEADER_LANGUAGES = new Set(["h", "hpp", "i"]);
74
+
75
+ /** atom `-l` values handled by the C/C++ frontend. */
76
+ const ATOM_C_LANGUAGES = new Set([
77
+ "c",
78
+ "cpp",
79
+ "c++",
80
+ "newc",
81
+ ...ATOM_HEADER_LANGUAGES,
82
+ ]);
83
+
84
+ const COMPILE_COMMANDS_FILE = "compile_commands.json";
85
+
86
+ /** Build directories, under the scan root, searched for a compilation database. */
87
+ const COMPILE_COMMANDS_BUILD_DIRS = ["build", "out", "builddir"];
88
+
89
+ /**
90
+ * Characters a Windows shell command line would interpret: `&`, `|`, `<` and
91
+ * `>` chain or redirect commands, `%` expands variables even inside quotes,
92
+ * `^` escapes, `!` expands under delayed expansion, and a quote breaks the
93
+ * quoting Node applies to arguments when `shell` is on. A database is often
94
+ * discovered inside the scanned tree, so its path is not trusted input.
95
+ */
96
+ const UNSAFE_SHELL_CHARS_RE = /[\r\n\t&|<>%^!"]/;
97
+
98
+ function compileCommandsIn(dir) {
99
+ for (const candidate of [
100
+ join(dir, COMPILE_COMMANDS_FILE),
101
+ join(dir, "build", COMPILE_COMMANDS_FILE),
102
+ ]) {
103
+ try {
104
+ if (statSync(candidate).isFile()) {
105
+ return candidate;
106
+ }
107
+ } catch (_err) {
108
+ // not there
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ /**
115
+ * Locate the JSON compilation database (`compile_commands.json`) of a C/C++
116
+ * project, which lets atom parse each file with the include paths, macros and
117
+ * language its build uses.
118
+ *
119
+ * An explicit `options.compileCommands` (a file, or a directory holding one
120
+ * directly or under `build/`) wins. Otherwise the scan root, `build/`, `out/`,
121
+ * `builddir/`, `cmake-build-*` and the directory of `options.cmakeCache` are
122
+ * searched. A database can come with the code it describes, and atom asks the
123
+ * GCC or Clang driver it names (from the PATH, or an absolute path outside
124
+ * the project) for its predefined macros, so in secure mode only an explicit
125
+ * database is used.
126
+ *
127
+ * @param {string} src Project scan root
128
+ * @param {Object} options CLI options
129
+ * @returns {string|undefined} Absolute path of the database
130
+ */
131
+ export function findCompileCommands(src, options = {}) {
132
+ if (options.compileCommands) {
133
+ const requested = resolve(`${options.compileCommands}`);
134
+ let found;
135
+ try {
136
+ found = statSync(requested).isDirectory()
137
+ ? compileCommandsIn(requested)
138
+ : requested;
139
+ } catch (_err) {
140
+ found = undefined;
141
+ }
142
+ if (!found) {
143
+ console.warn(
144
+ `No compilation database at ${requested}. Parsing the C/C++ sources without one.`,
145
+ );
146
+ }
147
+ return found;
148
+ }
149
+ if (isSecureMode || !src) {
150
+ return undefined;
151
+ }
152
+ const root = resolve(src);
153
+ let rootIsDir = false;
154
+ try {
155
+ rootIsDir = statSync(root).isDirectory();
156
+ } catch (_err) {
157
+ return undefined;
158
+ }
159
+ if (!rootIsDir) {
160
+ return undefined;
161
+ }
162
+ const dirs = [root, ...COMPILE_COMMANDS_BUILD_DIRS.map((d) => join(root, d))];
163
+ try {
164
+ dirs.push(
165
+ ...readdirSync(root, { withFileTypes: true })
166
+ .filter((e) => e.isDirectory() && e.name.startsWith("cmake-build-"))
167
+ .map((e) => join(root, e.name))
168
+ .sort(),
169
+ );
170
+ } catch (_err) {
171
+ // an unreadable root has no build directories to offer
172
+ }
173
+ if (options.cmakeCache) {
174
+ dirs.push(dirname(resolve(`${options.cmakeCache}`)));
175
+ }
176
+ for (const dir of dirs) {
177
+ const candidate = join(dir, COMPILE_COMMANDS_FILE);
178
+ try {
179
+ if (statSync(candidate).isFile()) {
180
+ return candidate;
181
+ }
182
+ } catch (_err) {
183
+ // not there
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ const atomFrontendArgKeysCache = new Map();
190
+
191
+ /**
192
+ * Split an atom command that may carry a leading argument (`node
193
+ * script.js`, a custom `ATOM_CMD`) into its binary and that argument. The
194
+ * first space only splits when it can: a binary under a path with spaces
195
+ * (`C:\Program Files\...\node.exe script.js`) would otherwise be cut
196
+ * mid-path and the shell told to run `C:\Program`. A command name without a
197
+ * path separator (`node script.js`) resolves through the PATH, so it splits
198
+ * at its first space as before; otherwise the first prefix that exists as a
199
+ * file is the binary, and when none does the first space keeps the old
200
+ * behaviour.
201
+ *
202
+ * @param {string} command The atom command, possibly with an argument
203
+ * @returns {[string, string|undefined]} The binary and its leading argument, when present
204
+ */
205
+ export function splitAtomCommand(command) {
206
+ const firstSpace = `${command}`.indexOf(" ");
207
+ if (firstSpace === -1) {
208
+ return [`${command}`, undefined];
209
+ }
210
+ const head = command.slice(0, firstSpace);
211
+ if (!/[/\\]/.test(head)) {
212
+ return [head, command.slice(firstSpace + 1)];
213
+ }
214
+ let splitAt = firstSpace;
215
+ let index = firstSpace;
216
+ while ((index = command.indexOf(" ", index + 1)) !== -1) {
217
+ try {
218
+ if (statSync(command.slice(0, index)).isFile()) {
219
+ splitAt = index;
220
+ break;
221
+ }
222
+ } catch (_err) {
223
+ // not a file; a later space may still split at the real binary
224
+ }
225
+ }
226
+ return [command.slice(0, splitAt), command.slice(splitAt + 1)];
227
+ }
228
+
229
+ /**
230
+ * The `--frontend-args` keys the installed atom accepts for a language, as
231
+ * `atom --frontend-args-keys -l <language>` lists them. Asked once per atom
232
+ * command and language. An atom that cannot list them (a release without
233
+ * `--frontend-args`, or one that fails to start) accepts none.
234
+ *
235
+ * @param {string} language atom `-l` value
236
+ * @returns {Set<string>} Supported keys
237
+ */
238
+ export function atomFrontendArgKeys(language) {
239
+ const command = getAtomCommand();
240
+ const cacheKey = `${command}\u0000${language}`;
241
+ if (atomFrontendArgKeysCache.has(cacheKey)) {
242
+ return atomFrontendArgKeysCache.get(cacheKey);
243
+ }
244
+ const [bin, leadingArg] = splitAtomCommand(command);
245
+ const args = ["--frontend-args-keys", "-l", `${language}`];
246
+ if (leadingArg !== undefined) {
247
+ args.unshift(leadingArg);
248
+ }
249
+ const env = { ...process.env };
250
+ if (readEnvironmentVariable("ATOM_JAVA_HOME")) {
251
+ env.JAVA_HOME = readEnvironmentVariable("ATOM_JAVA_HOME");
252
+ }
253
+ const keys = new Set();
254
+ const result = safeSpawnSync(bin, args, {
255
+ shell: isWin,
256
+ env,
257
+ timeout: 120000,
258
+ });
259
+ if (result?.status === 0 && result.stdout) {
260
+ let inTable = false;
261
+ for (const line of `${result.stdout}`.split(/\r?\n/)) {
262
+ if (line.startsWith("----")) {
263
+ inTable = true;
264
+ } else if (inTable && /^[a-z][a-z0-9-]*\s/.test(line)) {
265
+ keys.add(line.split(/\s+/)[0]);
266
+ }
267
+ }
268
+ }
269
+ if (DEBUG_MODE && !keys.size) {
270
+ console.log(
271
+ `The installed atom did not list its --frontend-args keys for ${language}.`,
272
+ );
273
+ }
274
+ atomFrontendArgKeysCache.set(cacheKey, keys);
275
+ return keys;
276
+ }
277
+
278
+ /**
279
+ * The `--frontend-args` atom takes for a C/C++ language: the project's
280
+ * compilation database, when there is one and the installed atom reads it
281
+ * (atom 4 and later). An older atom is given the arguments it has always
282
+ * been given, and parses the sources without the database.
283
+ *
284
+ * @param {string} src Project scan root
285
+ * @param {string} language atom `-l` value
286
+ * @param {Object} options CLI options
287
+ * @returns {string[]} Arguments to add to the atom command
288
+ */
289
+ export function atomCompileCommandsArgs(src, language, options = {}) {
290
+ if (!ATOM_C_LANGUAGES.has(`${language}`.toLowerCase())) {
291
+ return [];
292
+ }
293
+ const database = findCompileCommands(src, options);
294
+ if (!database) {
295
+ return [];
296
+ }
297
+ // --frontend-args separates its key=value pairs with commas
298
+ if (database.includes(",")) {
299
+ console.warn(
300
+ `The compilation database path ${database} contains a comma, which atom cannot take. Parsing the C/C++ sources without it.`,
301
+ );
302
+ return [];
303
+ }
304
+ // The argument reaches a Windows shell command line through `shell: isWin`
305
+ // in executeAtom, and the path is routinely discovered inside the scanned
306
+ // tree, so a database whose path a shell would interpret is never passed on.
307
+ if (UNSAFE_SHELL_CHARS_RE.test(database)) {
308
+ console.warn(
309
+ `The compilation database path ${database} contains characters a shell would interpret. Parsing the C/C++ sources without it.`,
310
+ );
311
+ return [];
312
+ }
313
+ if (!atomFrontendArgKeys(language).has("compile-commands")) {
314
+ return [];
315
+ }
316
+ if (DEBUG_MODE) {
317
+ console.log(`Passing the compilation database ${database} to atom.`);
318
+ }
319
+ return ["--frontend-args", `compile-commands=${database}`];
320
+ }
321
+
60
322
  // Absolute ceiling on atom's heap. Past this point a larger heap buys a
61
323
  // collector that runs less often rather than an analysis that succeeds where it
62
324
  // otherwise would not, while the reservation itself is what pushes a build
@@ -610,12 +872,38 @@ export function resolveAtomProvider(opts = {}) {
610
872
  }
611
873
 
612
874
  /**
613
- * Returns `"native"` or `"jar"` for the currently resolved atom provider.
614
- * Used to gate Java/JDK advice so users on the five native platforms are not
615
- * told to install a JDK for a failure that has nothing to do with Java.
875
+ * Returns `"native"` or `"jar"` for the atom provider that will actually run.
876
+ *
877
+ * The platform decides which provider atom prefers, but the dispatcher falls
878
+ * back to the jar package when the platform's native package is not installed
879
+ * (optional dependencies skipped, a failed download). The two take the heap
880
+ * ceiling differently, and a native-image runtime option on a jar run reaches
881
+ * atom as an unknown argument that fails every slice. So the kind follows what
882
+ * is installed where cdxgen can see it: the native binary means native, the jar
883
+ * package without it means jar. With neither in view (a custom `ATOM_CMD`, an
884
+ * install elsewhere) the platform's kind stands.
885
+ *
886
+ * Also used to gate Java/JDK advice, so users on the five native platforms are
887
+ * not told to install a JDK for a failure that has nothing to do with Java.
616
888
  */
617
889
  export function atomProviderKind() {
618
- return resolveAtomProvider().kind;
890
+ const { kind } = resolveAtomProvider();
891
+ if (kind !== "native" || readEnvironmentVariable("ATOM_CMD")) {
892
+ return kind;
893
+ }
894
+ const atomHome = readEnvironmentVariable("ATOM_HOME");
895
+ if (atomHome) {
896
+ // A jar install's ATOM_HOME is its plugins/ directory, which carries lib/;
897
+ // a native sub-package directory does not.
898
+ return safeExistsSync(join(atomHome, "lib")) ? "jar" : kind;
899
+ }
900
+ if (resolveDirectAtomBinaryPath()) {
901
+ return "native";
902
+ }
903
+ const jarPlugins = atomSubPackageCandidates("atom-jar").some((dir) =>
904
+ safeExistsSync(join(dir, "plugins")),
905
+ );
906
+ return jarPlugins ? "jar" : kind;
619
907
  }
620
908
 
621
909
  /**
@@ -683,6 +971,25 @@ export function resolveDirectAtomBinaryPath() {
683
971
  }
684
972
  const folder = preferredPkg.split("/")[1];
685
973
  const exeName = isWin ? "atom.exe" : "atom";
974
+ for (const dir of atomSubPackageCandidates(folder)) {
975
+ const candidate = join(dir, "bin", exeName);
976
+ if (safeExistsSync(candidate)) {
977
+ return candidate;
978
+ }
979
+ }
980
+ return undefined;
981
+ }
982
+
983
+ /**
984
+ * Directories where an `@appthreat/atom-*` sub-package may be installed next to
985
+ * the dispatcher cdxgen uses, for npm, pnpm and global layouts: a sibling of
986
+ * `@appthreat/atom`, nested under it, in a pnpm virtual store, or a sibling of
987
+ * the dispatcher's real (symlink-resolved) directory.
988
+ *
989
+ * @param {string} folder Sub-package folder name, e.g. `atom-jar`
990
+ * @returns {string[]} Candidate package directories
991
+ */
992
+ function atomSubPackageCandidates(folder) {
686
993
  const version = readAtomVersion();
687
994
  const roots = [join(dirNameStr, "node_modules")];
688
995
  if (readEnvironmentVariable("GLOBAL_NODE_MODULES_PATH")) {
@@ -690,18 +997,9 @@ export function resolveDirectAtomBinaryPath() {
690
997
  }
691
998
  const candidates = [];
692
999
  for (const root of roots) {
693
- candidates.push(join(root, "@appthreat", folder, "bin", exeName));
1000
+ candidates.push(join(root, "@appthreat", folder));
694
1001
  candidates.push(
695
- join(
696
- root,
697
- "@appthreat",
698
- "atom",
699
- "node_modules",
700
- "@appthreat",
701
- folder,
702
- "bin",
703
- exeName,
704
- ),
1002
+ join(root, "@appthreat", "atom", "node_modules", "@appthreat", folder),
705
1003
  );
706
1004
  if (version) {
707
1005
  candidates.push(
@@ -712,18 +1010,19 @@ export function resolveDirectAtomBinaryPath() {
712
1010
  "node_modules",
713
1011
  "@appthreat",
714
1012
  folder,
715
- "bin",
716
- exeName,
717
1013
  ),
718
1014
  );
719
1015
  }
720
- }
721
- for (const candidate of candidates) {
722
- if (safeExistsSync(candidate)) {
723
- return candidate;
1016
+ // pnpm links @appthreat/atom to its store entry, where the sub-packages
1017
+ // are its siblings.
1018
+ try {
1019
+ const atomDir = realpathSync(join(root, "@appthreat", "atom"));
1020
+ candidates.push(join(dirname(atomDir), folder));
1021
+ } catch {
1022
+ // not installed under this root
724
1023
  }
725
1024
  }
726
- return undefined;
1025
+ return [...new Set(candidates)];
727
1026
  }
728
1027
 
729
1028
  function readAtomVersion() {
@@ -874,6 +1173,93 @@ function applyAtomHeapLimit(args, env) {
874
1173
  }
875
1174
  }
876
1175
 
1176
+ // How long cdxgen waits past atom's own time limit for the dispatcher to stop
1177
+ // the runtime and exit. It covers the dispatcher's SIGTERM-to-SIGKILL grace (10
1178
+ // seconds) and the runtime's exit. A short limit (a test, a smoke run) gets a
1179
+ // quarter of itself instead.
1180
+ const ATOM_STOP_GRACE_MS = 30_000;
1181
+
1182
+ /**
1183
+ * Compute how long atom may run, and how long cdxgen waits for it.
1184
+ *
1185
+ * cdxgen's spawn timeout kills only the process cdxgen started, and for atom
1186
+ * that is the npm dispatcher. The runtime under it (the native binary or the
1187
+ * JVM) used to survive that kill, re-parented and still holding its heap. atom
1188
+ * therefore gets its own limit, `ATOM_TIMEOUT`, which the dispatcher enforces on
1189
+ * the runtime, and cdxgen's spawn timeout sits a grace period beyond it as the
1190
+ * last resort.
1191
+ *
1192
+ * An explicit `ATOM_TIMEOUT` (milliseconds) sets atom's limit independently of
1193
+ * `CDXGEN_TIMEOUT_MS`, which keeps bounding every other tool. Otherwise atom's
1194
+ * limit is `CDXGEN_TIMEOUT_MS` less the grace period.
1195
+ *
1196
+ * @returns {{atomTimeoutMs: number, spawnTimeoutMs: number}} atom's own limit and cdxgen's spawn timeout for it
1197
+ */
1198
+ export function atomTimeouts() {
1199
+ const graceFor = (ms) => Math.min(ATOM_STOP_GRACE_MS, Math.floor(ms / 4));
1200
+ const configured = Number.parseInt(
1201
+ readEnvironmentVariable("ATOM_TIMEOUT"),
1202
+ 10,
1203
+ );
1204
+ if (configured > 0) {
1205
+ return {
1206
+ atomTimeoutMs: configured,
1207
+ spawnTimeoutMs: configured + graceFor(configured),
1208
+ };
1209
+ }
1210
+ return {
1211
+ atomTimeoutMs: TIMEOUT_MS - graceFor(TIMEOUT_MS),
1212
+ spawnTimeoutMs: TIMEOUT_MS,
1213
+ };
1214
+ }
1215
+
1216
+ function formatDuration(ms) {
1217
+ if (ms >= 60_000) {
1218
+ const minutes = Math.round(ms / 6_000) / 10;
1219
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
1220
+ }
1221
+ return `${Math.round(ms / 100) / 10} seconds`;
1222
+ }
1223
+
1224
+ // The status the atom dispatcher exits with when ATOM_TIMEOUT stops atom.
1225
+ const ATOM_TIMEOUT_EXIT_CODE = 124;
1226
+
1227
+ /**
1228
+ * Whether an atom run ended because it ran out of time.
1229
+ *
1230
+ * The dispatcher reports its own limit with status 124. cdxgen's spawn timeout
1231
+ * shows up as `ETIMEDOUT`. A dispatcher older than the status code stops atom
1232
+ * with SIGTERM and exits 1, so status 1 at or past the limit counts as well,
1233
+ * unless atom's own output shows it failed for another reason first (an
1234
+ * exhausted heap, a crash), which deserves its own diagnosis.
1235
+ *
1236
+ * @param {Object} result spawnSync result
1237
+ * @param {number} elapsedMs How long the run took
1238
+ * @param {number} atomTimeoutMs atom's time limit
1239
+ * @returns {boolean} true when the run hit a time limit
1240
+ */
1241
+ export function atomRunTimedOut(result, elapsedMs, atomTimeoutMs) {
1242
+ if (result?.status === ATOM_TIMEOUT_EXIT_CODE) {
1243
+ return true;
1244
+ }
1245
+ if (result?.error?.code === "ETIMEDOUT") {
1246
+ return true;
1247
+ }
1248
+ if (result?.status !== 1 || elapsedMs < atomTimeoutMs) {
1249
+ return false;
1250
+ }
1251
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`;
1252
+ return !ATOM_FAILURE_MARKERS.some((marker) => output.includes(marker));
1253
+ }
1254
+
1255
+ // Output that means atom failed by itself, whatever the clock says.
1256
+ const ATOM_FAILURE_MARKERS = [
1257
+ "OutOfMemoryError",
1258
+ "A fatal error has been detected by the Java Runtime Environment",
1259
+ "The crash happened outside the Java Virtual Machine in native code",
1260
+ "Failure: ",
1261
+ ];
1262
+
877
1263
  /**
878
1264
  * Execute the atom tool against a source directory or file with the given arguments.
879
1265
  *
@@ -914,14 +1300,12 @@ export function executeAtom(src, args, extra_env = {}) {
914
1300
  ...extra_env,
915
1301
  };
916
1302
  // Bound the heap while argv still starts at atom's own arguments: the
917
- // space-split below prepends the launcher script, which has to stay first.
1303
+ // split below prepends the launcher script, which has to stay first.
918
1304
  applyAtomHeapLimit(args, env);
919
- if (ATOM_BIN.includes(" ")) {
920
- const tmpA = ATOM_BIN.split(" ");
921
- if (tmpA && tmpA.length > 1) {
922
- ATOM_BIN = tmpA[0];
923
- args.unshift(tmpA[1]);
924
- }
1305
+ const [resolvedBin, leadingArg] = splitAtomCommand(ATOM_BIN);
1306
+ if (leadingArg !== undefined) {
1307
+ ATOM_BIN = resolvedBin;
1308
+ args.unshift(leadingArg);
925
1309
  }
926
1310
  if (DEBUG_MODE) {
927
1311
  console.log("Executing", ATOM_BIN);
@@ -952,12 +1336,33 @@ export function executeAtom(src, args, extra_env = {}) {
952
1336
  ".bin",
953
1337
  )}`;
954
1338
  }
1339
+ const { atomTimeoutMs, spawnTimeoutMs } = atomTimeouts();
1340
+ env.ATOM_TIMEOUT = String(atomTimeoutMs);
1341
+ // Name cdxgen as the supervisor: the dispatcher, and atom under it, stop when
1342
+ // cdxgen is gone (killed by a caller's own timeout, say) instead of analysing
1343
+ // on for nobody.
1344
+ env.ATOM_PARENT_PID = String(process.pid);
1345
+ const startedAt = Date.now();
955
1346
  const result = safeSpawnSync(ATOM_BIN, args, {
956
1347
  cwd,
957
1348
  shell: isWin,
958
1349
  killSignal: "SIGKILL",
959
1350
  env,
1351
+ timeout: spawnTimeoutMs,
960
1352
  });
1353
+ if (atomRunTimedOut(result, Date.now() - startedAt, atomTimeoutMs)) {
1354
+ console.warn(
1355
+ `WARN: atom did not finish within ${formatDuration(atomTimeoutMs)} and was stopped, so its results are missing or incomplete. Allow more time with ATOM_TIMEOUT (milliseconds, atom only) or CDXGEN_TIMEOUT_MS (every external command).`,
1356
+ );
1357
+ // A stopped atom leaves its graph half written, and the next slice of the
1358
+ // same run would try to reuse it and fail to load it.
1359
+ const atomFileIndex = args.indexOf("-o");
1360
+ const atomFile = atomFileIndex >= 0 ? args[atomFileIndex + 1] : undefined;
1361
+ if (atomFile && safeExistsSync(atomFile)) {
1362
+ safeRmSync(atomFile, { recursive: true, force: true });
1363
+ }
1364
+ return false;
1365
+ }
961
1366
  const isJarKind = atomProviderKind() === "jar";
962
1367
  if (result.stderr) {
963
1368
  if (
@@ -1036,6 +1441,130 @@ export function executeAtom(src, args, extra_env = {}) {
1036
1441
  return isSupported && !result.error && result.status === 0;
1037
1442
  }
1038
1443
 
1444
+ const CHUNK_MTIME_SLACK_MS = 2000;
1445
+
1446
+ /**
1447
+ * The numbered chunks atom wrote beside a reachables slices file.
1448
+ *
1449
+ * atom writes reachables at most 1000 flows per file: `<base>.json`, then
1450
+ * `<base>_1.json`, `<base>_2.json` and so on. Chunks are taken until the first
1451
+ * gap. A chunk older than the base file was left by an earlier run with more
1452
+ * flows (atom before 4.0.0 did not remove those), so it ends the sequence
1453
+ * instead of being merged.
1454
+ *
1455
+ * @param {string} slicesFile Path of the base reachables slices file
1456
+ * @returns {string[]} Chunk file paths, in order
1457
+ */
1458
+ export function reachablesChunkFiles(slicesFile) {
1459
+ const baseMtimeMs = mtimeMsOf(slicesFile);
1460
+ if (!slicesFile?.endsWith(".json") || baseMtimeMs === undefined) {
1461
+ return [];
1462
+ }
1463
+ const stem = slicesFile.slice(0, -".json".length);
1464
+ // atom writes the base file first, so its chunks are never older. The slack
1465
+ // covers filesystems that store modification times in whole seconds or two;
1466
+ // it assumes no earlier run wrote chunks to the same path within that window.
1467
+ // atom 4 and cdxgen both remove old chunks before atom writes new ones, so
1468
+ // the mtime test only matters for slices files produced elsewhere.
1469
+ const oldestChunkMtimeMs = baseMtimeMs - CHUNK_MTIME_SLACK_MS;
1470
+ const chunks = [];
1471
+ for (let index = 1; ; index++) {
1472
+ const chunk = `${stem}_${index}.json`;
1473
+ const chunkMtimeMs = mtimeMsOf(chunk);
1474
+ if (chunkMtimeMs === undefined || chunkMtimeMs < oldestChunkMtimeMs) {
1475
+ break;
1476
+ }
1477
+ chunks.push(chunk);
1478
+ }
1479
+ return chunks;
1480
+ }
1481
+
1482
+ // A file that disappears between listing and stat ends the sequence, not the run.
1483
+ function mtimeMsOf(file) {
1484
+ if (!file || !safeExistsSync(file)) {
1485
+ return undefined;
1486
+ }
1487
+ try {
1488
+ return statSync(file).mtimeMs;
1489
+ } catch {
1490
+ return undefined;
1491
+ }
1492
+ }
1493
+
1494
+ /**
1495
+ * Remove the numbered chunks beside a reachables slices file, before atom
1496
+ * writes a new set there, so no chunk of an earlier run survives next to it.
1497
+ *
1498
+ * @param {string} slicesFile Path of the base reachables slices file
1499
+ */
1500
+ export function removeReachablesChunkFiles(slicesFile) {
1501
+ if (!slicesFile?.endsWith(".json")) {
1502
+ return;
1503
+ }
1504
+ const stem = slicesFile.slice(0, -".json".length);
1505
+ for (let index = 1; safeExistsSync(`${stem}_${index}.json`); index++) {
1506
+ safeRmSync(`${stem}_${index}.json`, { force: true });
1507
+ }
1508
+ }
1509
+
1510
+ /**
1511
+ * Read an atom slices file, or warn and return `undefined` when it cannot be
1512
+ * parsed. A run stopped while writing (a timeout, a kill) can leave a
1513
+ * truncated file, and the evidence from the other slices is still worth
1514
+ * keeping.
1515
+ *
1516
+ * @param {string} slicesFile Path of the slices file
1517
+ * @param {string} sliceType Slice type, for the warning
1518
+ * @returns {*} The parsed slices, or `undefined`
1519
+ */
1520
+ export function readSlicesFile(slicesFile, sliceType) {
1521
+ if (!slicesFile || !safeExistsSync(slicesFile)) {
1522
+ return undefined;
1523
+ }
1524
+ try {
1525
+ return readJsonFile(slicesFile);
1526
+ } catch (e) {
1527
+ console.warn(
1528
+ `WARN: Ignoring the ${sliceType} slices file ${slicesFile}, which is incomplete or not valid JSON (${e.message}).`,
1529
+ );
1530
+ return undefined;
1531
+ }
1532
+ }
1533
+
1534
+ /**
1535
+ * Read a reachables slices file together with its chunks.
1536
+ *
1537
+ * Reading only the base file silently dropped every flow past the first
1538
+ * thousand. A chunk that cannot be parsed ends the sequence, keeping the flows
1539
+ * read before it.
1540
+ *
1541
+ * @param {string} slicesFile Path of the base reachables slices file
1542
+ * @returns {Object[]|Object|undefined} Every flow, in the base file's shape (an array, or an object with `reachables`)
1543
+ */
1544
+ export function readReachablesSlices(slicesFile) {
1545
+ const data = readSlicesFile(slicesFile, "reachables");
1546
+ if (data === undefined) {
1547
+ return undefined;
1548
+ }
1549
+ const chunks = reachablesChunkFiles(slicesFile);
1550
+ if (!chunks.length) {
1551
+ return data;
1552
+ }
1553
+ const flowsOf = (slices) =>
1554
+ Array.isArray(slices) ? slices : slices?.reachables || [];
1555
+ const flows = [...flowsOf(data)];
1556
+ for (const chunk of chunks) {
1557
+ const chunkData = readSlicesFile(chunk, "reachables");
1558
+ if (chunkData === undefined) {
1559
+ break;
1560
+ }
1561
+ for (const flow of flowsOf(chunkData)) {
1562
+ flows.push(flow);
1563
+ }
1564
+ }
1565
+ return Array.isArray(data) || !data ? flows : { ...data, reachables: flows };
1566
+ }
1567
+
1039
1568
  /**
1040
1569
  * Find the imported modules in the application with atom parsedeps command
1041
1570
  *
@@ -1068,6 +1597,14 @@ export function findAppModules(
1068
1597
  "--slice-outfile",
1069
1598
  resolve(slicesFile),
1070
1599
  ];
1600
+ // The header languages parse without function bodies. Their AST cache, kept
1601
+ // in <src>/.chen, would be replayed by a later full C/C++ run over the same
1602
+ // directory (evinse, or `-t c --deep`) on atom releases that do not key the
1603
+ // cache on that difference, leaving that run with no method bodies.
1604
+ if (ATOM_HEADER_LANGUAGES.has(`${language}`.toLowerCase())) {
1605
+ args.push("--no-ast-cache");
1606
+ }
1607
+ args.push(...atomCompileCommandsArgs(src, language, options));
1071
1608
  args.push(resolve(src));
1072
1609
  executeAtom(src, args, buildAtomCommandEnv(options, language));
1073
1610
  if (safeExistsSync(slicesFile)) {