@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
package/lib/core/fs.js CHANGED
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import {
5
5
  accessSync,
6
+ closeSync,
6
7
  copyFileSync,
7
8
  createReadStream,
8
9
  existsSync,
@@ -10,10 +11,14 @@ import {
10
11
  lstatSync,
11
12
  mkdirSync,
12
13
  mkdtempSync,
14
+ openSync,
13
15
  readFileSync,
16
+ renameSync,
14
17
  rmSync,
18
+ statSync,
15
19
  unlinkSync,
16
20
  writeFileSync,
21
+ writeSync,
17
22
  } from "node:fs";
18
23
  import { tmpdir } from "node:os";
19
24
  import path, { basename, dirname, join, resolve } from "node:path";
@@ -144,6 +149,53 @@ export function safeIsExecutableSync(filePath) {
144
149
  * @returns {void}
145
150
  */
146
151
  export function safeWriteSync(filePath, data, options) {
152
+ if (isWriteBlocked(filePath)) {
153
+ return undefined;
154
+ }
155
+ writeFileSync(filePath, data, options);
156
+ recordFilesystemActivity("write", filePath, "completed");
157
+ }
158
+
159
+ /**
160
+ * Permission- and dry-run-aware write of a sequence of chunks, for content too
161
+ * large to join into one string first. The chunks go to a temporary file next
162
+ * to the target, which is renamed into place once complete, so a chunk may be
163
+ * read from the file being replaced and a failed write leaves the target as it
164
+ * was.
165
+ *
166
+ * @param {string} filePath File path to write.
167
+ * @param {Iterable<string|Buffer>} chunks Chunks written in order.
168
+ * @returns {void}
169
+ */
170
+ export function safeWriteChunksSync(filePath, chunks) {
171
+ if (isWriteBlocked(filePath)) {
172
+ return undefined;
173
+ }
174
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
175
+ const fd = openSync(tempPath, "w");
176
+ try {
177
+ for (const chunk of chunks) {
178
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
179
+ let written = 0;
180
+ while (written < bytes.length) {
181
+ written += writeSync(fd, bytes, written);
182
+ }
183
+ }
184
+ closeSync(fd);
185
+ renameSync(tempPath, filePath);
186
+ } catch (err) {
187
+ try {
188
+ closeSync(fd);
189
+ } catch {
190
+ // Already closed before the rename failed.
191
+ }
192
+ rmSync(tempPath, { force: true });
193
+ throw err;
194
+ }
195
+ recordFilesystemActivity("write", filePath, "completed");
196
+ }
197
+
198
+ function isWriteBlocked(filePath) {
147
199
  if (isDryRun) {
148
200
  recordFilesystemActivity(
149
201
  "write",
@@ -151,7 +203,7 @@ export function safeWriteSync(filePath, data, options) {
151
203
  "blocked",
152
204
  "Dry run mode blocks filesystem writes.",
153
205
  );
154
- return undefined;
206
+ return true;
155
207
  }
156
208
  if (!hasWritePermission(filePath)) {
157
209
  if (DEBUG_MODE) {
@@ -163,10 +215,9 @@ export function safeWriteSync(filePath, data, options) {
163
215
  "blocked",
164
216
  "cdxgen lacks write permission for this path.",
165
217
  );
166
- return undefined;
218
+ return true;
167
219
  }
168
- writeFileSync(filePath, data, options);
169
- recordFilesystemActivity("write", filePath, "completed");
220
+ return false;
170
221
  }
171
222
 
172
223
  /**
@@ -483,6 +534,116 @@ const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
483
534
  const POSIX_SHELL_METACHARACTERS = /[;&|<>$`\\\n\r]/;
484
535
  const WINDOWS_SHELL_METACHARACTERS = /[&|<>^%\n\r]/;
485
536
 
537
+ /**
538
+ * Quote one argument of a shell command line that is built as a single
539
+ * string. The shell flag exists so cmd.exe can resolve bare command names,
540
+ * `.cmd` shims and scripts, and every argument is data (paths, flags) that
541
+ * must arrive as one token whatever it contains: joined unquoted, an
542
+ * argument with spaces splits into several and hands the tool the wrong
543
+ * tokens.
544
+ *
545
+ * Windows follows the quoting rules cmd.exe and the MSVC runtimes share:
546
+ * wrap in double quotes, doubling the backslashes that precede a quote and
547
+ * escaping the quote itself. Inside double quotes cmd.exe already treats
548
+ * `&`, `|`, `<`, `>`, `^` and parentheses as literals, and the
549
+ * shell-metacharacter block above refuses `%` and the rest before this
550
+ * runs. A POSIX shell gets single quotes, which no expansion can leave.
551
+ *
552
+ * The same quoting is applied to the command itself when it names an
553
+ * existing file under a path with spaces; a command the shell must parse
554
+ * (a bare name, `sh -c`) never names a file as a whole and stays verbatim.
555
+ *
556
+ * @param {string} arg Argument to quote
557
+ * @param {boolean} [forWindows] Quote for cmd.exe rather than a POSIX shell; defaults to the current platform
558
+ * @returns {string} The argument, quoted when the shell needs it
559
+ */
560
+ export function shellQuoteArgument(arg, forWindows = isWin) {
561
+ const value = `${arg}`;
562
+ if (forWindows) {
563
+ if (value === "") {
564
+ return '""';
565
+ }
566
+ if (!/[\s"]/.test(value)) {
567
+ return value;
568
+ }
569
+ // Every backslash run that precedes a quote doubles, each quote escapes,
570
+ // and the trailing run doubles for the closing quote this returns. A
571
+ // character scan keeps it linear; the equivalent `(\\*)"` pattern is
572
+ // polynomial on runs of backslashes that no quote ends.
573
+ let quoted = "";
574
+ let backslashes = 0;
575
+ for (const ch of value) {
576
+ if (ch === "\\") {
577
+ backslashes++;
578
+ quoted += ch;
579
+ } else if (ch === '"') {
580
+ quoted += "\\".repeat(backslashes) + '\\"';
581
+ backslashes = 0;
582
+ } else {
583
+ quoted += ch;
584
+ backslashes = 0;
585
+ }
586
+ }
587
+ quoted += "\\".repeat(backslashes);
588
+ return `"${quoted}"`;
589
+ }
590
+ if (value === "") {
591
+ return "''";
592
+ }
593
+ if (!/[^-\w@%+=:,./]/.test(value)) {
594
+ return value;
595
+ }
596
+ return `'${value.replaceAll("'", `'\\''`)}'`;
597
+ }
598
+
599
+ /**
600
+ * Whether the path names an existing regular file, without throwing on the
601
+ * many values that are not paths at all (a bare command name, a command with
602
+ * arguments, anything the shell is meant to parse).
603
+ *
604
+ * @param {string} file Candidate path
605
+ * @returns {boolean} True when a regular file exists at the path
606
+ */
607
+ function isFileNoThrow(file) {
608
+ try {
609
+ return statSync(`${file}`).isFile();
610
+ } catch (_err) {
611
+ return false;
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Build the single command string a shell spawn runs. Every argument is
617
+ * quoted for the platform shell, so an argument containing spaces stays one
618
+ * token and cannot alter the command, and a command that is itself a file
619
+ * under a path with spaces (`C:\Program Files\...\node.exe`) is quoted too;
620
+ * a command the shell must parse (`sh -c`, a bare name) never names an
621
+ * existing file as a whole and is left verbatim.
622
+ *
623
+ * The one exception is a caller that composes shell fragments itself (sbt
624
+ * command lines embed their own quoting for the shell to strip): it passes
625
+ * `rawShellArgs` and keeps the verbatim join, under the shell-metacharacter
626
+ * block in safeSpawnSync like every other argument.
627
+ *
628
+ * @param {string} command The executable
629
+ * @param {string[]|undefined} args Arguments, when the caller passed any
630
+ * @param {boolean} [rawShellArgs] Join the arguments verbatim instead of quoting them
631
+ * @returns {string} The command line the shell will run
632
+ */
633
+ export function joinShellCommand(command, args, rawShellArgs = false) {
634
+ let commandToken = `${command}`;
635
+ if (/\s/.test(commandToken) && isFileNoThrow(commandToken)) {
636
+ commandToken = shellQuoteArgument(commandToken);
637
+ }
638
+ if (!args?.length) {
639
+ return commandToken;
640
+ }
641
+ return [
642
+ commandToken,
643
+ ...(rawShellArgs ? args : args.map((arg) => shellQuoteArgument(arg))),
644
+ ].join(" ");
645
+ }
646
+
486
647
  function hasShellMetacharacters(value) {
487
648
  if (value === undefined || value === null) {
488
649
  return false;
@@ -696,9 +857,12 @@ export function safeSpawnSync(command, args, options) {
696
857
  }
697
858
  }
698
859
  }
860
+ // Read before the option is stripped below: a caller composing shell
861
+ // fragments itself opts out of argument quoting with this flag.
862
+ const rawShellArgs = options?.cdxgenRawShellArgs === true;
699
863
  if (!options) {
700
864
  options = {};
701
- } else if (options.cdxgenActivity) {
865
+ } else if (options.cdxgenActivity || options.cdxgenRawShellArgs) {
702
866
  options = {
703
867
  ...options,
704
868
  };
@@ -706,6 +870,9 @@ export function safeSpawnSync(command, args, options) {
706
870
  if (options.cdxgenActivity) {
707
871
  delete options.cdxgenActivity;
708
872
  }
873
+ if (options.cdxgenRawShellArgs) {
874
+ delete options.cdxgenRawShellArgs;
875
+ }
709
876
  if (options.shell === true) {
710
877
  const unsafeShellToken = getUnsafeShellToken(command, args);
711
878
  if (unsafeShellToken !== undefined) {
@@ -817,8 +984,9 @@ export function safeSpawnSync(command, args, options) {
817
984
  commandsExecuted.add(command);
818
985
  // Fix for DEP0190 warning
819
986
  if (options?.shell === true) {
987
+ const joined = joinShellCommand(command, args, rawShellArgs);
988
+ command = joined;
820
989
  if (args?.length) {
821
- command = `${command} ${args.join(" ")}`;
822
990
  args = undefined;
823
991
  }
824
992
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The spellings under which a config file may list custom metadata
3
+ * properties: those of the `--metadata-property` flag, and the plural that
4
+ * the CDXGEN_METADATA_PROPERTIES environment variable maps to.
5
+ */
6
+ export const METADATA_PROPERTY_CONFIG_KEYS = [
7
+ "metadata-property",
8
+ "metadataProperty",
9
+ "metadata-properties",
10
+ "metadataProperties",
11
+ ];
12
+
13
+ // Namespaces whose properties cdxgen itself asserts: `cdx:` is its registered
14
+ // taxonomy, and `internal:` carries its own bookkeeping
15
+ const RESERVED_METADATA_PROPERTY_PREFIXES = ["cdx:", "internal:"];
16
+
17
+ /**
18
+ * Report whether a property name belongs to a namespace cdxgen reserves for
19
+ * the facts it establishes itself, such as `cdx:bom:componentSrcFiles`.
20
+ *
21
+ * @param {string} name Property name
22
+ * @returns {boolean} true for a reserved name
23
+ */
24
+ export function isReservedMetadataPropertyName(name) {
25
+ return RESERVED_METADATA_PROPERTY_PREFIXES.some((prefix) =>
26
+ `${name}`.startsWith(prefix),
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Normalize user-supplied metadata properties into `[{name, value}]`
32
+ * (discussion 4391).
33
+ *
34
+ * Accepted shapes:
35
+ * - a `name=value` string, or a JSON document holding any of the shapes below
36
+ * — an environment variable
37
+ * - an array of `name=value` strings or `{name, value}` objects — the
38
+ * `--metadata-property` flag and a JSON config file
39
+ * - a plain object mapping a name to a value, or to a list of values for a
40
+ * property that repeats — a JSON document or a YAML config file
41
+ *
42
+ * Values must be strings, numbers, or booleans. An entry without a name, a
43
+ * `name=value` string without a separator, and an empty or structured value
44
+ * are dropped rather than guessed at: a property with a mangled name or an
45
+ * `[object Object]` value is worse than none.
46
+ *
47
+ * @param {unknown} input Properties in any accepted shape
48
+ * @returns {{name: string, value: string}[]} Normalized properties
49
+ */
50
+ export function parseMetadataProperties(input) {
51
+ const properties = [];
52
+ if (typeof input === "string") {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(input);
56
+ } catch (_err) {
57
+ parsed = undefined;
58
+ }
59
+ input =
60
+ parsed && typeof parsed === "object"
61
+ ? parsed
62
+ : [typeof parsed === "string" ? parsed : input];
63
+ }
64
+ const append = (name, value) => {
65
+ if (typeof name !== "string" && typeof name !== "number") {
66
+ return;
67
+ }
68
+ const propName = `${name}`.trim();
69
+ if (
70
+ !propName.length ||
71
+ !["string", "number", "boolean"].includes(typeof value) ||
72
+ !`${value}`.length
73
+ ) {
74
+ return;
75
+ }
76
+ properties.push({ name: propName, value: `${value}` });
77
+ };
78
+ if (Array.isArray(input)) {
79
+ for (const entry of input) {
80
+ if (typeof entry === "string") {
81
+ const separator = entry.indexOf("=");
82
+ if (separator > 0) {
83
+ append(entry.slice(0, separator), entry.slice(separator + 1));
84
+ }
85
+ } else if (entry && typeof entry === "object") {
86
+ append(entry.name, entry.value);
87
+ }
88
+ }
89
+ } else if (input && typeof input === "object") {
90
+ for (const [name, value] of Object.entries(input)) {
91
+ for (const avalue of Array.isArray(value) ? value : [value]) {
92
+ append(name, avalue);
93
+ }
94
+ }
95
+ }
96
+ return properties;
97
+ }
@@ -1,5 +1,10 @@
1
1
  import path from "node:path";
2
2
 
3
+ import {
4
+ isReservedMetadataPropertyName,
5
+ METADATA_PROPERTY_CONFIG_KEYS,
6
+ parseMetadataProperties,
7
+ } from "./metadataProperties.js";
3
8
  import { toCamel } from "./paths.js";
4
9
 
5
10
  /**
@@ -87,19 +92,41 @@ function isContainedPath(configDir, value) {
87
92
  * whenever that tree is. Path options are therefore confined to the config's
88
93
  * own directory - a scanned project may choose where in itself the BOM lands,
89
94
  * but not write outside it - and options that redirect data off the host or
90
- * widen what cdxgen executes are announced on the console.
95
+ * widen what cdxgen executes are announced on the console. A project may
96
+ * describe itself with custom metadata properties, but not in the namespaces
97
+ * cdxgen reserves for the facts it establishes, so a scanned tree cannot pass
98
+ * its own claims off as cdxgen's.
91
99
  *
92
100
  * @param {Object} config parsed config file contents
93
101
  * @param {string} configDir directory the config file was read from
94
- * @returns {{config: Object, rejected: string[], announced: string[]}} sanitized config and what it did
102
+ * @returns {{config: Object, rejected: string[], announced: string[], reservedProperties: string[]}} sanitized config and what it did
95
103
  */
96
104
  export function sanitizeProjectConfig(config, configDir) {
97
105
  if (!config || typeof config !== "object") {
98
- return { announced: [], config: {}, rejected: [] };
106
+ return { announced: [], config: {}, rejected: [], reservedProperties: [] };
99
107
  }
100
108
  const sanitized = { ...config };
101
109
  const rejected = [];
102
110
  const announced = [];
111
+ const reservedProperties = [];
112
+ for (const key of METADATA_PROPERTY_CONFIG_KEYS) {
113
+ if (sanitized[key] === undefined) {
114
+ continue;
115
+ }
116
+ const properties = [];
117
+ for (const aprop of parseMetadataProperties(sanitized[key])) {
118
+ if (isReservedMetadataPropertyName(aprop.name)) {
119
+ reservedProperties.push(aprop.name);
120
+ } else {
121
+ properties.push(aprop);
122
+ }
123
+ }
124
+ if (properties.length) {
125
+ sanitized[key] = properties;
126
+ } else {
127
+ delete sanitized[key];
128
+ }
129
+ }
103
130
  for (const option of PATH_OPTIONS) {
104
131
  for (const key of optionSpellings(option)) {
105
132
  if (sanitized[key] === undefined) {
@@ -118,5 +145,5 @@ export function sanitizeProjectConfig(config, configDir) {
118
145
  }
119
146
  }
120
147
  }
121
- return { announced, config: sanitized, rejected };
148
+ return { announced, config: sanitized, rejected, reservedProperties };
122
149
  }