@cdxgen/cdxgen 13.0.1 → 13.2.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 +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- 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 +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.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 +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { inflateRawSync } from "node:zlib";
|
|
4
|
+
|
|
5
|
+
import { DEBUG_MODE } from "../core/activity.js";
|
|
6
|
+
import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
|
|
7
|
+
import { sanitizeMcpRefToken } from "./mcpDiscovery.js";
|
|
8
|
+
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
|
|
9
|
+
|
|
10
|
+
const MCPB_BUNDLE_PATTERNS = ["**/*.mcpb", "**/*.dxt"];
|
|
11
|
+
|
|
12
|
+
// Bound only what is decompressed. The archive itself is no longer fully
|
|
13
|
+
// buffered: the End-Of-Central-Directory record, the central directory, and a
|
|
14
|
+
// single local entry are read positionally, so a large vendored bundle is
|
|
15
|
+
// inspected rather than silently skipped.
|
|
16
|
+
const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
// Cap the central directory read so a hostile archive cannot force a huge
|
|
18
|
+
// allocation. 16 MiB comfortably covers tens of thousands of entries.
|
|
19
|
+
const MAX_CENTRAL_DIR_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
// The EOCD record is 22 bytes plus an optional comment of up to 65535 bytes.
|
|
21
|
+
const MAX_EOCD_SCAN_BYTES = 22 + 65535;
|
|
22
|
+
const MAX_DESCRIPTION_BYTES = 1000;
|
|
23
|
+
const EOCD_SIGNATURE = 0x06054b50;
|
|
24
|
+
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
25
|
+
const LOCAL_SIGNATURE = 0x04034b50;
|
|
26
|
+
|
|
27
|
+
const SECRET_FIELD_NAME_PATTERN =
|
|
28
|
+
/(token|secret|password|api[_-]?key|client[_-]?secret|authorization|credential)/iu;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a byte range from an open file descriptor into a fresh buffer.
|
|
32
|
+
*
|
|
33
|
+
* @param {number} fd Open file descriptor
|
|
34
|
+
* @param {number} position Absolute byte offset
|
|
35
|
+
* @param {number} length Number of bytes to read
|
|
36
|
+
* @returns {Buffer|undefined} Filled buffer, or undefined on short read
|
|
37
|
+
*/
|
|
38
|
+
function readRange(fd, position, length) {
|
|
39
|
+
if (length <= 0) {
|
|
40
|
+
return Buffer.alloc(0);
|
|
41
|
+
}
|
|
42
|
+
const buffer = Buffer.alloc(length);
|
|
43
|
+
const bytesRead = readSync(fd, buffer, 0, length, position);
|
|
44
|
+
if (bytesRead !== length) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return buffer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read and decompress the `manifest.json` entry from a zip archive using
|
|
52
|
+
* positional reads, without any third-party dependency and without buffering
|
|
53
|
+
* the whole archive. Only the End-Of-Central-Directory record, the central
|
|
54
|
+
* directory, and the single manifest entry are read, so an untrusted bundle is
|
|
55
|
+
* never fully expanded.
|
|
56
|
+
*
|
|
57
|
+
* Zip64 archives (more than 65535 entries, or offsets beyond 4 GiB) are not
|
|
58
|
+
* decoded; such a bundle is skipped with a debug log rather than mis-read.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath Path to the .mcpb/.dxt bundle
|
|
61
|
+
* @returns {string|undefined} Decoded manifest.json text, or undefined
|
|
62
|
+
*/
|
|
63
|
+
function readManifestFromBundle(filePath) {
|
|
64
|
+
let fd;
|
|
65
|
+
try {
|
|
66
|
+
const size = statSync(filePath).size;
|
|
67
|
+
if (size < 22) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
fd = openSync(filePath, "r");
|
|
71
|
+
const eocd = findEndOfCentralDirectory(fd, size);
|
|
72
|
+
if (!eocd) {
|
|
73
|
+
debugSkip(filePath, "no end-of-central-directory record");
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (eocd.entryCount === 0xffff || eocd.centralDirOffset === 0xffffffff) {
|
|
77
|
+
debugSkip(filePath, "zip64 archive is not supported");
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (eocd.centralDirSize > MAX_CENTRAL_DIR_BYTES) {
|
|
81
|
+
debugSkip(filePath, "central directory exceeds the read cap");
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const central = readRange(fd, eocd.centralDirOffset, eocd.centralDirSize);
|
|
85
|
+
if (!central) {
|
|
86
|
+
debugSkip(filePath, "could not read the central directory");
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const entry = findManifestEntry(central, eocd.entryCount);
|
|
90
|
+
if (!entry) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const data = readLocalEntry(fd, size, entry);
|
|
94
|
+
if (data === undefined) {
|
|
95
|
+
debugSkip(filePath, "could not inflate the manifest entry");
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
debugSkip(filePath, err?.message || "read error");
|
|
100
|
+
return undefined;
|
|
101
|
+
} finally {
|
|
102
|
+
if (fd !== undefined) {
|
|
103
|
+
try {
|
|
104
|
+
closeSync(fd);
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore close failures
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function debugSkip(filePath, reason) {
|
|
113
|
+
if (DEBUG_MODE) {
|
|
114
|
+
console.log(`Skipping MCPB bundle ${filePath}: ${reason}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function findEndOfCentralDirectory(fd, size) {
|
|
119
|
+
const scanLength = Math.min(size, MAX_EOCD_SCAN_BYTES);
|
|
120
|
+
const start = size - scanLength;
|
|
121
|
+
const tail = readRange(fd, start, scanLength);
|
|
122
|
+
if (!tail) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
for (let offset = tail.length - 22; offset >= 0; offset--) {
|
|
126
|
+
if (tail.readUInt32LE(offset) === EOCD_SIGNATURE) {
|
|
127
|
+
return {
|
|
128
|
+
entryCount: tail.readUInt16LE(offset + 10),
|
|
129
|
+
centralDirSize: tail.readUInt32LE(offset + 12),
|
|
130
|
+
centralDirOffset: tail.readUInt32LE(offset + 16),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findManifestEntry(central, entryCount) {
|
|
138
|
+
let pointer = 0;
|
|
139
|
+
for (let index = 0; index < entryCount; index++) {
|
|
140
|
+
if (pointer + 46 > central.length) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (central.readUInt32LE(pointer) !== CENTRAL_SIGNATURE) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const compressionMethod = central.readUInt16LE(pointer + 10);
|
|
147
|
+
const compressedSize = central.readUInt32LE(pointer + 20);
|
|
148
|
+
const uncompressedSize = central.readUInt32LE(pointer + 24);
|
|
149
|
+
const fileNameLength = central.readUInt16LE(pointer + 28);
|
|
150
|
+
const extraLength = central.readUInt16LE(pointer + 30);
|
|
151
|
+
const commentLength = central.readUInt16LE(pointer + 32);
|
|
152
|
+
const localHeaderOffset = central.readUInt32LE(pointer + 42);
|
|
153
|
+
const fileName = central
|
|
154
|
+
.subarray(pointer + 46, pointer + 46 + fileNameLength)
|
|
155
|
+
.toString("utf-8");
|
|
156
|
+
const normalizedName = fileName.replaceAll("\\", "/");
|
|
157
|
+
const isManifest =
|
|
158
|
+
normalizedName === "manifest.json" ||
|
|
159
|
+
normalizedName.endsWith("/manifest.json");
|
|
160
|
+
if (isManifest && uncompressedSize <= MAX_MANIFEST_BYTES) {
|
|
161
|
+
return { compressionMethod, compressedSize, localHeaderOffset };
|
|
162
|
+
}
|
|
163
|
+
pointer += 46 + fileNameLength + extraLength + commentLength;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readLocalEntry(fd, size, entry) {
|
|
169
|
+
const { localHeaderOffset, compressionMethod, compressedSize } = entry;
|
|
170
|
+
// Bound the compressed read for every method, not just stored entries. The
|
|
171
|
+
// size comes from the archive's own central directory, so without this a
|
|
172
|
+
// hostile bundle could declare a multi-gigabyte entry and force that much to
|
|
173
|
+
// be allocated before `maxOutputLength` ever gets a chance to apply. A
|
|
174
|
+
// deflate stream that inflates to at most MAX_MANIFEST_BYTES cannot
|
|
175
|
+
// meaningfully exceed that compressed.
|
|
176
|
+
if (compressedSize > MAX_MANIFEST_BYTES) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const header = readRange(fd, localHeaderOffset, 30);
|
|
180
|
+
if (!header || header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const fileNameLength = header.readUInt16LE(26);
|
|
184
|
+
const extraLength = header.readUInt16LE(28);
|
|
185
|
+
const dataStart = localHeaderOffset + 30 + fileNameLength + extraLength;
|
|
186
|
+
if (dataStart + compressedSize > size) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const compressed = readRange(fd, dataStart, compressedSize);
|
|
190
|
+
if (!compressed) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
if (compressionMethod === 0) {
|
|
195
|
+
return compressed.toString("utf-8");
|
|
196
|
+
}
|
|
197
|
+
if (compressionMethod === 8) {
|
|
198
|
+
// Bound the inflated output: the central-directory size field is
|
|
199
|
+
// attacker-controlled, so the real guard is the output length limit.
|
|
200
|
+
return inflateRawSync(compressed, {
|
|
201
|
+
maxOutputLength: MAX_MANIFEST_BYTES,
|
|
202
|
+
}).toString("utf-8");
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function addUniqueProperty(properties, name, value) {
|
|
211
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
212
|
+
if (
|
|
213
|
+
sanitizedValue === undefined ||
|
|
214
|
+
sanitizedValue === null ||
|
|
215
|
+
sanitizedValue === ""
|
|
216
|
+
) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
properties.some(
|
|
221
|
+
(property) =>
|
|
222
|
+
property.name === name && property.value === String(sanitizedValue),
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
properties.push({ name, value: String(sanitizedValue) });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function countCollection(value) {
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
return value.length;
|
|
233
|
+
}
|
|
234
|
+
if (value && typeof value === "object") {
|
|
235
|
+
return Object.keys(value).length;
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Count how many declared user-configuration fields look secret-bearing.
|
|
242
|
+
*
|
|
243
|
+
* The MCPB client injects `user_config` values as environment variables when it
|
|
244
|
+
* launches the bundled server, so secret-typed fields are a credential surface
|
|
245
|
+
* worth surfacing. Only field names and types are inspected; no user-provided
|
|
246
|
+
* values are read or emitted.
|
|
247
|
+
*
|
|
248
|
+
* @param {Object|Array} userConfig Declared user_config block
|
|
249
|
+
* @returns {{ fieldCount: number, secretFieldCount: number }} field counts
|
|
250
|
+
*/
|
|
251
|
+
function summarizeUserConfig(userConfig) {
|
|
252
|
+
const entries = Array.isArray(userConfig)
|
|
253
|
+
? userConfig.map((entry) => [entry?.key || entry?.name || "", entry])
|
|
254
|
+
: Object.entries(userConfig || {});
|
|
255
|
+
let secretFieldCount = 0;
|
|
256
|
+
for (const [key, definition] of entries) {
|
|
257
|
+
const type = String(definition?.type || "").toLowerCase();
|
|
258
|
+
const sensitive =
|
|
259
|
+
definition?.sensitive === true ||
|
|
260
|
+
type === "secret" ||
|
|
261
|
+
type === "password" ||
|
|
262
|
+
SECRET_FIELD_NAME_PATTERN.test(String(key));
|
|
263
|
+
if (sensitive) {
|
|
264
|
+
secretFieldCount++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { fieldCount: entries.length, secretFieldCount };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Count how many precompiled platform binaries a bundle declares across the
|
|
272
|
+
* `server.mcp_config.platform_overrides` / `compatibility.platforms` shapes
|
|
273
|
+
* used by MCPB manifests.
|
|
274
|
+
*
|
|
275
|
+
* @param {Object} manifest Parsed manifest.json
|
|
276
|
+
* @returns {number} platform binary count
|
|
277
|
+
*/
|
|
278
|
+
function platformBinaryCount(manifest) {
|
|
279
|
+
const overrides =
|
|
280
|
+
manifest?.server?.mcp_config?.platform_overrides ||
|
|
281
|
+
manifest?.server?.platform_overrides;
|
|
282
|
+
if (overrides) {
|
|
283
|
+
return countCollection(overrides);
|
|
284
|
+
}
|
|
285
|
+
const platforms = manifest?.compatibility?.platforms || manifest?.platforms;
|
|
286
|
+
return countCollection(platforms);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Build a component from a parsed MCPB manifest.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} filePath Bundle path
|
|
293
|
+
* @param {Object} manifest Parsed manifest.json
|
|
294
|
+
* @returns {Object} CycloneDX component
|
|
295
|
+
*/
|
|
296
|
+
function componentFromManifest(filePath, manifest) {
|
|
297
|
+
const name =
|
|
298
|
+
(typeof manifest?.name === "string" && manifest.name) || basename(filePath);
|
|
299
|
+
const serverType = String(manifest?.server?.type || "").toLowerCase();
|
|
300
|
+
const userConfig = summarizeUserConfig(manifest?.user_config);
|
|
301
|
+
const properties = [
|
|
302
|
+
{ name: "internal:SrcFile", value: filePath },
|
|
303
|
+
{ name: "cdx:file:kind", value: "mcp-bundle" },
|
|
304
|
+
{ name: "cdx:mcp:inventorySource", value: "mcp-bundle" },
|
|
305
|
+
// A bundle ships a whole local server plus its dependencies, so treat it as
|
|
306
|
+
// an install-time supply-chain surface that warrants review.
|
|
307
|
+
{ name: "cdx:mcp:reviewNeeded", value: "true" },
|
|
308
|
+
];
|
|
309
|
+
addUniqueProperty(
|
|
310
|
+
properties,
|
|
311
|
+
"cdx:mcpb:manifestVersion",
|
|
312
|
+
manifest?.manifest_version ? String(manifest.manifest_version) : undefined,
|
|
313
|
+
);
|
|
314
|
+
addUniqueProperty(properties, "cdx:mcpb:serverType", serverType || undefined);
|
|
315
|
+
addUniqueProperty(
|
|
316
|
+
properties,
|
|
317
|
+
"cdx:mcpb:toolCount",
|
|
318
|
+
String(countCollection(manifest?.tools)),
|
|
319
|
+
);
|
|
320
|
+
addUniqueProperty(
|
|
321
|
+
properties,
|
|
322
|
+
"cdx:mcpb:promptCount",
|
|
323
|
+
String(countCollection(manifest?.prompts)),
|
|
324
|
+
);
|
|
325
|
+
addUniqueProperty(
|
|
326
|
+
properties,
|
|
327
|
+
"cdx:mcpb:userConfigFieldCount",
|
|
328
|
+
String(userConfig.fieldCount),
|
|
329
|
+
);
|
|
330
|
+
if (userConfig.secretFieldCount) {
|
|
331
|
+
addUniqueProperty(
|
|
332
|
+
properties,
|
|
333
|
+
"cdx:mcpb:secretFieldCount",
|
|
334
|
+
String(userConfig.secretFieldCount),
|
|
335
|
+
);
|
|
336
|
+
addUniqueProperty(properties, "cdx:mcpb:injectsSecretEnv", "true");
|
|
337
|
+
}
|
|
338
|
+
const binaryCount = platformBinaryCount(manifest);
|
|
339
|
+
if (binaryCount) {
|
|
340
|
+
addUniqueProperty(
|
|
341
|
+
properties,
|
|
342
|
+
"cdx:mcpb:platformBinaryCount",
|
|
343
|
+
String(binaryCount),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const component = {
|
|
347
|
+
"bom-ref": `urn:mcp:bundle:${sanitizeMcpRefToken(name)}:${sanitizeMcpRefToken(basename(filePath))}`,
|
|
348
|
+
type: "application",
|
|
349
|
+
name,
|
|
350
|
+
properties,
|
|
351
|
+
};
|
|
352
|
+
if (manifest?.version) {
|
|
353
|
+
component.version = String(manifest.version);
|
|
354
|
+
}
|
|
355
|
+
// The description is attacker-controlled free text from an untrusted zip.
|
|
356
|
+
// Sanitize, truncate, and scan it for hidden Unicode like the sibling
|
|
357
|
+
// manifest/agent parsers do, rather than copying it verbatim.
|
|
358
|
+
if (typeof manifest?.description === "string") {
|
|
359
|
+
const sanitizedDescription = sanitizeBomPropertyValue(
|
|
360
|
+
"description",
|
|
361
|
+
manifest.description.slice(0, MAX_DESCRIPTION_BYTES),
|
|
362
|
+
);
|
|
363
|
+
if (sanitizedDescription) {
|
|
364
|
+
component.description = String(sanitizedDescription);
|
|
365
|
+
}
|
|
366
|
+
const hiddenUnicodeScan = scanTextForHiddenUnicode(manifest.description, {
|
|
367
|
+
syntax: "text",
|
|
368
|
+
});
|
|
369
|
+
if (hiddenUnicodeScan.hasHiddenUnicode) {
|
|
370
|
+
addUniqueProperty(
|
|
371
|
+
component.properties,
|
|
372
|
+
"cdx:file:hasHiddenUnicode",
|
|
373
|
+
"true",
|
|
374
|
+
);
|
|
375
|
+
addUniqueProperty(
|
|
376
|
+
component.properties,
|
|
377
|
+
"cdx:file:hiddenUnicodeCodePoints",
|
|
378
|
+
hiddenUnicodeScan.codePoints.join(","),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return component;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Parser for MCPB / DXT desktop-extension bundles.
|
|
387
|
+
*
|
|
388
|
+
* A bundle is a zip archive carrying a local MCP server, its dependencies, and
|
|
389
|
+
* a `manifest.json`. This parser reads only the manifest to describe the
|
|
390
|
+
* bundled server, the tools/prompts it exposes, the number of precompiled
|
|
391
|
+
* platform binaries, and whether it injects secret-typed configuration values
|
|
392
|
+
* as environment variables.
|
|
393
|
+
*
|
|
394
|
+
* @type {{id: string, patterns: string[], parse(files: string[], options?: Object): {components: Object[]}}}
|
|
395
|
+
*/
|
|
396
|
+
export const mcpbBundleParser = {
|
|
397
|
+
id: "mcp-bundle",
|
|
398
|
+
patterns: MCPB_BUNDLE_PATTERNS,
|
|
399
|
+
parse(files, _options = {}) {
|
|
400
|
+
const components = [];
|
|
401
|
+
for (const filePath of [...new Set(files || [])]) {
|
|
402
|
+
const manifestText = readManifestFromBundle(filePath);
|
|
403
|
+
if (!manifestText) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
let manifest;
|
|
407
|
+
try {
|
|
408
|
+
manifest = JSON.parse(manifestText);
|
|
409
|
+
} catch {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (!manifest || typeof manifest !== "object") {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
components.push(componentFromManifest(filePath, manifest));
|
|
416
|
+
}
|
|
417
|
+
return { components };
|
|
418
|
+
},
|
|
419
|
+
};
|
package/lib/inventory/osinfo.js
CHANGED
package/lib/inventory/plugins.js
CHANGED
|
@@ -19,6 +19,7 @@ const PLUGIN_ENV_COMMAND_NAMES = {
|
|
|
19
19
|
trivy: "TRIVY_CMD",
|
|
20
20
|
trustinspector: "TRUSTINSPECTOR_CMD",
|
|
21
21
|
cdxui: "CDXUI_CMD",
|
|
22
|
+
kosi: "KOSI_CMD",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
function isMusl() {
|
|
@@ -41,6 +42,7 @@ function hasUsablePluginsDir(pluginsDir) {
|
|
|
41
42
|
"trivy",
|
|
42
43
|
"trustinspector",
|
|
43
44
|
"cdxui",
|
|
45
|
+
"kosi",
|
|
44
46
|
].some((pluginName) => safeExistsSync(join(pluginsDir, pluginName))))
|
|
45
47
|
);
|
|
46
48
|
}
|
|
@@ -431,6 +433,7 @@ function resolveBundledPluginBinary(toolName, pluginRuntime) {
|
|
|
431
433
|
case "rusi":
|
|
432
434
|
case "cdxrs":
|
|
433
435
|
case "cdxui":
|
|
436
|
+
case "kosi":
|
|
434
437
|
return join(
|
|
435
438
|
pluginRuntime.pluginsDir,
|
|
436
439
|
toolName,
|
|
@@ -3,18 +3,19 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import {
|
|
4
4
|
bomStats,
|
|
5
5
|
convertBom,
|
|
6
|
-
createBom,
|
|
7
6
|
decodeBomBinary,
|
|
8
|
-
decodeBomJson,
|
|
9
7
|
encodeBomBinary,
|
|
10
8
|
encodeBomJson,
|
|
9
|
+
isSupportedSpecVersion,
|
|
11
10
|
parseBomBinary,
|
|
12
|
-
parseBomJson,
|
|
13
11
|
supportedSpecVersions,
|
|
12
|
+
toBomMessage,
|
|
14
13
|
} from "@cdxgen/cdx-proto";
|
|
14
|
+
import { isProtoBomFile } from "@cdxgen/cdx-proto/node";
|
|
15
15
|
|
|
16
16
|
import { safeExistsSync, safeWriteSync } from "../core/fs.js";
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
export { isProtoBomFile };
|
|
18
19
|
|
|
19
20
|
const JSON_READ_OPTIONS = {
|
|
20
21
|
ignoreUnknownFields: true,
|
|
@@ -28,35 +29,8 @@ const BINARY_WRITE_OPTIONS = {
|
|
|
28
29
|
writeUnknownFields: true,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
|
|
32
|
-
|
|
33
32
|
const DEFAULT_SPEC_VERSION =
|
|
34
33
|
supportedSpecVersions[supportedSpecVersions.length - 1];
|
|
35
|
-
const PROTO_SUPPORTED_SPEC_VERSIONS = new Set(
|
|
36
|
-
supportedSpecVersions.map((specVersion) =>
|
|
37
|
-
toCycloneDxSpecVersionString(specVersion),
|
|
38
|
-
),
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const isProtoMessageBom = (bom) =>
|
|
42
|
-
Boolean(
|
|
43
|
-
bom &&
|
|
44
|
-
typeof bom === "object" &&
|
|
45
|
-
!Array.isArray(bom) &&
|
|
46
|
-
typeof bom.$typeName === "string" &&
|
|
47
|
-
bom.specVersion,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const hasExplicitSpecVersion = (bomJson) =>
|
|
51
|
-
Boolean(
|
|
52
|
-
bomJson &&
|
|
53
|
-
typeof bomJson === "object" &&
|
|
54
|
-
!Array.isArray(bomJson) &&
|
|
55
|
-
(bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const resolveExplicitSpecVersion = (bomJson) =>
|
|
59
|
-
bomJson?.specVersion ?? bomJson?.spec_version;
|
|
60
34
|
|
|
61
35
|
const hasProvidedSpecVersion = (specVersion) =>
|
|
62
36
|
specVersion !== undefined &&
|
|
@@ -66,19 +40,15 @@ const hasProvidedSpecVersion = (specVersion) =>
|
|
|
66
40
|
/**
|
|
67
41
|
* Determine whether a spec version is supported for protobuf serialization.
|
|
68
42
|
*
|
|
43
|
+
* Delegates to cdx-proto's `isSupportedSpecVersion`, which accepts the
|
|
44
|
+
* spellings seen in the wild (`v1.6`, `1.6`, `1.6.0`) in addition to the
|
|
45
|
+
* canonical strings.
|
|
46
|
+
*
|
|
69
47
|
* @param {string|undefined} specVersion CycloneDX spec version string
|
|
70
48
|
* @returns {boolean} `true` when supported by `@cdxgen/cdx-proto`, or when no spec version is provided
|
|
71
49
|
*/
|
|
72
|
-
export const isProtoSupportedSpecVersion = (specVersion) =>
|
|
73
|
-
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
|
|
77
|
-
return (
|
|
78
|
-
normalizedSpecVersion !== undefined &&
|
|
79
|
-
PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion)
|
|
80
|
-
);
|
|
81
|
-
};
|
|
50
|
+
export const isProtoSupportedSpecVersion = (specVersion) =>
|
|
51
|
+
!hasProvidedSpecVersion(specVersion) || isSupportedSpecVersion(specVersion);
|
|
82
52
|
|
|
83
53
|
/**
|
|
84
54
|
* Assert that a spec version is supported by `@cdxgen/cdx-proto`.
|
|
@@ -94,56 +64,42 @@ export const assertProtoSupportedSpecVersion = (
|
|
|
94
64
|
specVersion,
|
|
95
65
|
operation = "protobuf operations",
|
|
96
66
|
) => {
|
|
97
|
-
if (!hasProvidedSpecVersion(specVersion)) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
|
|
101
67
|
if (isProtoSupportedSpecVersion(specVersion)) {
|
|
102
68
|
return;
|
|
103
69
|
}
|
|
104
|
-
const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim();
|
|
105
70
|
throw new Error(
|
|
106
|
-
`CycloneDX ${
|
|
71
|
+
`CycloneDX ${`${specVersion}`.trim()} is not currently supported for ${operation}. @cdxgen/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
|
|
107
72
|
);
|
|
108
73
|
};
|
|
109
74
|
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
typeof bomJson === "string" || bomJson instanceof String
|
|
116
|
-
? JSON.parse(`${bomJson}`)
|
|
117
|
-
: bomJson;
|
|
118
|
-
if (
|
|
119
|
-
parsedBomJson &&
|
|
120
|
-
typeof parsedBomJson === "object" &&
|
|
121
|
-
!Array.isArray(parsedBomJson)
|
|
122
|
-
) {
|
|
123
|
-
if (hasExplicitSpecVersion(parsedBomJson)) {
|
|
124
|
-
assertProtoSupportedSpecVersion(
|
|
125
|
-
resolveExplicitSpecVersion(parsedBomJson),
|
|
126
|
-
"protobuf serialization",
|
|
127
|
-
);
|
|
128
|
-
return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
|
|
75
|
+
const resolveExplicitInputSpecVersion = (bomJson) => {
|
|
76
|
+
if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
|
|
77
|
+
const explicitSpecVersion = bomJson.specVersion ?? bomJson.spec_version;
|
|
78
|
+
if (hasProvidedSpecVersion(explicitSpecVersion)) {
|
|
79
|
+
return explicitSpecVersion;
|
|
129
80
|
}
|
|
130
|
-
assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
|
|
131
|
-
return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
|
|
132
81
|
}
|
|
133
|
-
return
|
|
82
|
+
return undefined;
|
|
134
83
|
};
|
|
135
84
|
|
|
136
85
|
/**
|
|
137
|
-
*
|
|
86
|
+
* Coerce a BOM input into a decoded message via cdx-proto's `toBomMessage`
|
|
87
|
+
* (messages pass through, JSON strings are parsed, canonical JSON objects are
|
|
88
|
+
* decoded, anything else yields an empty BOM). The version is asserted first
|
|
89
|
+
* so an unsupported BOM keeps failing with cdxgen's operation-specific error
|
|
90
|
+
* rather than cdx-proto's generic one.
|
|
138
91
|
*
|
|
139
|
-
* @param {string}
|
|
140
|
-
* @
|
|
92
|
+
* @param {string | Object} bomJson BOM Json, BOM Json string, or proto message
|
|
93
|
+
* @param {string | number} specVersion CycloneDX spec version fallback for BOMs without specVersion
|
|
94
|
+
* @param {string} operation Operation label used in the error message
|
|
95
|
+
* @returns {Object} Decoded BOM message
|
|
141
96
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
97
|
+
const toSerializableBomMessage = (bomJson, specVersion, operation) => {
|
|
98
|
+
assertProtoSupportedSpecVersion(
|
|
99
|
+
resolveExplicitInputSpecVersion(bomJson) ?? specVersion,
|
|
100
|
+
operation,
|
|
146
101
|
);
|
|
102
|
+
return toBomMessage(bomJson, specVersion, JSON_READ_OPTIONS);
|
|
147
103
|
};
|
|
148
104
|
|
|
149
105
|
/**
|
|
@@ -159,7 +115,11 @@ export const writeBinary = (
|
|
|
159
115
|
specVersion = DEFAULT_SPEC_VERSION,
|
|
160
116
|
) => {
|
|
161
117
|
if (bomJson && binFile) {
|
|
162
|
-
const bomMessage =
|
|
118
|
+
const bomMessage = toSerializableBomMessage(
|
|
119
|
+
bomJson,
|
|
120
|
+
specVersion,
|
|
121
|
+
"protobuf serialization",
|
|
122
|
+
);
|
|
163
123
|
safeWriteSync(binFile, encodeBomBinary(bomMessage, BINARY_WRITE_OPTIONS));
|
|
164
124
|
}
|
|
165
125
|
};
|
|
@@ -196,19 +156,27 @@ export const readBinary = (binFile, asJson, specVersion) => {
|
|
|
196
156
|
* @returns {Object} Component and dependency counts with JSON/binary byte sizes and compression ratio
|
|
197
157
|
*/
|
|
198
158
|
export const getBomStats = (bomJson, specVersion = DEFAULT_SPEC_VERSION) =>
|
|
199
|
-
bomStats(
|
|
159
|
+
bomStats(
|
|
160
|
+
toSerializableBomMessage(bomJson, specVersion, "protobuf statistics"),
|
|
161
|
+
);
|
|
200
162
|
|
|
201
163
|
/**
|
|
202
164
|
* Method to cross-convert a BOM between CycloneDX specification versions using
|
|
203
165
|
* the protobuf schemas.
|
|
204
166
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
167
|
+
* cdx-proto's conversion is cardinality-safe: fields that change shape between
|
|
168
|
+
* versions (`metadata.licenses` and `evidence.identity`, singular in 1.5 and an
|
|
169
|
+
* array since 1.6) are reshaped automatically — wrapped on upgrade, collapsed
|
|
170
|
+
* to their first entry on downgrade — including `evidence.identity` on nested
|
|
171
|
+
* components, `metadata.component`, `metadata.tools.components[]`, and
|
|
172
|
+
* `formulation[].components[]`. Downgrades are otherwise lossy: fields the
|
|
173
|
+
* target version does not define are dropped and reported in `warnings` as
|
|
174
|
+
* field paths, alongside collapsed list siblings.
|
|
207
175
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* `applySpecVersionCompatibility` from the postgen stage
|
|
176
|
+
* Only the protobuf-supported versions (1.5–1.7) can be targeted here. For the
|
|
177
|
+
* full JSON-level normalization across every version cdxgen emits (component
|
|
178
|
+
* types, license attributes, 1.4/2.0 reshaping), prefer
|
|
179
|
+
* `applySpecVersionCompatibility` from the postgen stage.
|
|
212
180
|
*
|
|
213
181
|
* @param {string | Object} bomJson BOM Json, BOM Json string, or proto message
|
|
214
182
|
* @param {string | number} targetSpecVersion Target CycloneDX spec version
|
|
@@ -221,11 +189,9 @@ export const convertBomSpecVersion = (
|
|
|
221
189
|
specVersion = DEFAULT_SPEC_VERSION,
|
|
222
190
|
) => {
|
|
223
191
|
assertProtoSupportedSpecVersion(targetSpecVersion, "protobuf conversion");
|
|
224
|
-
const normalizedTargetSpecVersion =
|
|
225
|
-
toCycloneDxSpecVersionString(targetSpecVersion) || `${targetSpecVersion}`;
|
|
226
192
|
const { bom, warnings } = convertBom(
|
|
227
|
-
|
|
228
|
-
|
|
193
|
+
toSerializableBomMessage(bomJson, specVersion, "protobuf conversion"),
|
|
194
|
+
targetSpecVersion,
|
|
229
195
|
);
|
|
230
196
|
return { bomJson: encodeBomJson(bom), warnings };
|
|
231
197
|
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Kept local (not imported from `@cdxgen/cdx-proto/node`) because this module
|
|
2
|
+
// gates whether the optional protobuf dependency is loaded at all. The
|
|
3
|
+
// authoritative list is `protoBomFileExtensions` in `@cdxgen/cdx-proto/node`;
|
|
4
|
+
// keep the two in sync.
|
|
1
5
|
const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
|
|
2
6
|
|
|
3
7
|
/**
|