@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.
- package/README.md +28 -12
- package/bin/cdxgen.js +227 -119
- package/bin/evinse.js +5 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/remediations.json +24 -0
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +43 -4
- package/lib/cli/jsBom.js +49 -24
- package/lib/cli/jvmBom.js +46 -12
- package/lib/cli/managedBom.js +112 -41
- package/lib/cli/nativeBom.js +125 -14
- package/lib/core/env.js +5 -6
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +64 -31
- package/lib/ecosystems/ecosystems.js +19 -8
- package/lib/ecosystems/gradleutils.js +42 -7
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +10 -3
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-dotnet.js +51 -10
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-misc.js +747 -161
- package/lib/ecosystems/parsers-python.js +26 -14
- package/lib/ecosystems/parsers-rust.js +24 -18
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +32 -1
- package/lib/evinser/evinser.js +436 -128
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +550 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/evidenceUtils.js +14 -6
- package/lib/inventory/fetchBatch.js +116 -17
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +37 -19
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/stages/postgen/postgen.js +68 -15
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +20 -20
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +59 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +64 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- 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
|
|
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
|
|
218
|
+
return true;
|
|
167
219
|
}
|
|
168
|
-
|
|
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
|
}
|