@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,204 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HPC Spack parser for `spack.lock`.
|
|
7
|
+
*
|
|
8
|
+
* Spack writes a JSON lock file when an environment is concretised:
|
|
9
|
+
*
|
|
10
|
+
* - `roots` — the specs the user asked for, each as `{hash, spec}`.
|
|
11
|
+
* - `concrete_specs` — every node of the resolved DAG, keyed by its DAG
|
|
12
|
+
* hash. A node carries its `name`, `version`, `namespace`, the compiler
|
|
13
|
+
* and architecture it was built for, and a `dependencies` list whose
|
|
14
|
+
* entries reference other nodes by hash.
|
|
15
|
+
*
|
|
16
|
+
* The DAG hash is the identity Spack itself uses: two builds of the same
|
|
17
|
+
* version with different variants or compilers are different hashes, so the
|
|
18
|
+
* graph is keyed by hash throughout and the hash is carried into each
|
|
19
|
+
* component's bom-ref.
|
|
20
|
+
*
|
|
21
|
+
* No `spack` purl type is registered, so nodes are identified as generic
|
|
22
|
+
* packages carrying a `cdx:purl:proposedType=spack` property, following the
|
|
23
|
+
* convention used for nix and zig. The DAG hash is kept as a property because
|
|
24
|
+
* it identifies the concrete build, not the artifact bytes.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a `spack.lock` file.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} spackLockFile Path to `spack.lock`
|
|
31
|
+
* @returns {{ pkgList: object[], dependencies: object[], rootInputs: string[] }}
|
|
32
|
+
*/
|
|
33
|
+
export function parseSpackLock(spackLockFile) {
|
|
34
|
+
let lock;
|
|
35
|
+
try {
|
|
36
|
+
lock = JSON.parse(readFileSync(spackLockFile, "utf-8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn(`Failed to parse ${spackLockFile}: ${error.message}`);
|
|
39
|
+
return emptyResult();
|
|
40
|
+
}
|
|
41
|
+
const concreteSpecs =
|
|
42
|
+
lock?.concrete_specs && typeof lock.concrete_specs === "object"
|
|
43
|
+
? lock.concrete_specs
|
|
44
|
+
: undefined;
|
|
45
|
+
if (!concreteSpecs) {
|
|
46
|
+
return emptyResult();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pkgList = [];
|
|
50
|
+
const refByHash = new Map();
|
|
51
|
+
for (const [hash, node] of Object.entries(concreteSpecs)) {
|
|
52
|
+
if (typeof node?.name !== "string" || !node.name) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const pkg = spackPackage(node.name, stringOrUndefined(node.version), {
|
|
56
|
+
hash,
|
|
57
|
+
namespace: stringOrUndefined(node.namespace),
|
|
58
|
+
arch: architectureLabel(node.arch),
|
|
59
|
+
compiler: compilerLabel(node.compiler),
|
|
60
|
+
srcFile: spackLockFile,
|
|
61
|
+
});
|
|
62
|
+
pkgList.push(pkg);
|
|
63
|
+
refByHash.set(hash, pkg["bom-ref"]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dependencies = [];
|
|
67
|
+
for (const [hash, node] of Object.entries(concreteSpecs)) {
|
|
68
|
+
const ref = refByHash.get(hash);
|
|
69
|
+
if (!ref) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Link and run edges are the ones that survive into the installed DAG;
|
|
73
|
+
// build-only edges are dropped to keep the graph at runtime relevance.
|
|
74
|
+
const dependsOn = new Set();
|
|
75
|
+
for (const dep of Array.isArray(node.dependencies)
|
|
76
|
+
? node.dependencies
|
|
77
|
+
: []) {
|
|
78
|
+
const types = Array.isArray(dep?.type) ? dep.type : [];
|
|
79
|
+
if (!types.includes("link") && !types.includes("run")) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const targetRef = refByHash.get(dep?.hash);
|
|
83
|
+
if (targetRef && targetRef !== ref) {
|
|
84
|
+
dependsOn.add(targetRef);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The environment names its own roots; nothing has to be inferred.
|
|
91
|
+
const rootInputs = (Array.isArray(lock.roots) ? lock.roots : [])
|
|
92
|
+
.map((root) => refByHash.get(root?.hash))
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
return { pkgList, dependencies, rootInputs: [...new Set(rootInputs)] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a component-like package record for a Spack node.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} name Node name
|
|
101
|
+
* @param {string|undefined} version Node version
|
|
102
|
+
* @param {object} opts Extra context (`hash`, `namespace`, `arch`, `compiler`, `srcFile`)
|
|
103
|
+
* @returns {object} Package record
|
|
104
|
+
*/
|
|
105
|
+
function spackPackage(name, version, opts) {
|
|
106
|
+
const purl = tryBuildPurl({
|
|
107
|
+
type: "generic",
|
|
108
|
+
namespace: "spack",
|
|
109
|
+
name,
|
|
110
|
+
version: version || undefined,
|
|
111
|
+
});
|
|
112
|
+
const properties = [
|
|
113
|
+
{ name: "internal:SrcFile", value: opts.srcFile },
|
|
114
|
+
{ name: "cdx:purl:proposedType", value: "spack" },
|
|
115
|
+
];
|
|
116
|
+
if (opts.hash) {
|
|
117
|
+
properties.push({ name: "cdx:spack:hash", value: opts.hash });
|
|
118
|
+
}
|
|
119
|
+
if (opts.namespace) {
|
|
120
|
+
properties.push({ name: "cdx:spack:namespace", value: opts.namespace });
|
|
121
|
+
}
|
|
122
|
+
if (opts.arch) {
|
|
123
|
+
properties.push({ name: "cdx:spack:arch", value: opts.arch });
|
|
124
|
+
}
|
|
125
|
+
if (opts.compiler) {
|
|
126
|
+
properties.push({ name: "cdx:spack:compiler", value: opts.compiler });
|
|
127
|
+
}
|
|
128
|
+
const pkg = {
|
|
129
|
+
name,
|
|
130
|
+
...(version ? { version } : {}),
|
|
131
|
+
type: "library",
|
|
132
|
+
scope: "required",
|
|
133
|
+
properties,
|
|
134
|
+
};
|
|
135
|
+
if (purl) {
|
|
136
|
+
pkg.purl = purl;
|
|
137
|
+
// An environment can hold two builds of one version that differ only in
|
|
138
|
+
// their variants or compiler, and the DAG hash is what tells them apart,
|
|
139
|
+
// so it is carried in the bom-ref as a subpath to keep the refs distinct.
|
|
140
|
+
pkg["bom-ref"] = opts.hash
|
|
141
|
+
? `${decodeURIComponent(purl)}#${opts.hash}`
|
|
142
|
+
: decodeURIComponent(purl);
|
|
143
|
+
} else {
|
|
144
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}:${opts.hash || ""}`;
|
|
145
|
+
}
|
|
146
|
+
return pkg;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render a node's architecture as `platform-os-target`.
|
|
151
|
+
*
|
|
152
|
+
* @param {object|undefined} arch Node `arch` object
|
|
153
|
+
* @returns {string|undefined} Architecture label
|
|
154
|
+
*/
|
|
155
|
+
function architectureLabel(arch) {
|
|
156
|
+
if (!arch || typeof arch !== "object") {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
// Newer lock versions nest the microarchitecture under `target.name`.
|
|
160
|
+
const target =
|
|
161
|
+
typeof arch.target === "string"
|
|
162
|
+
? arch.target
|
|
163
|
+
: stringOrUndefined(arch.target?.name);
|
|
164
|
+
const parts = [
|
|
165
|
+
stringOrUndefined(arch.platform),
|
|
166
|
+
stringOrUndefined(arch.platform_os),
|
|
167
|
+
target,
|
|
168
|
+
].filter(Boolean);
|
|
169
|
+
return parts.length ? parts.join("-") : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Render a node's compiler as `name@version`.
|
|
174
|
+
*
|
|
175
|
+
* @param {object|undefined} compiler Node `compiler` object
|
|
176
|
+
* @returns {string|undefined} Compiler label
|
|
177
|
+
*/
|
|
178
|
+
function compilerLabel(compiler) {
|
|
179
|
+
const name = stringOrUndefined(compiler?.name);
|
|
180
|
+
if (!name) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const version = stringOrUndefined(compiler?.version);
|
|
184
|
+
return version ? `${name}@${version}` : name;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Narrow a value to a non-empty string.
|
|
189
|
+
*
|
|
190
|
+
* @param {*} value Value to check
|
|
191
|
+
* @returns {string|undefined} The string, or undefined
|
|
192
|
+
*/
|
|
193
|
+
function stringOrUndefined(value) {
|
|
194
|
+
return typeof value === "string" && value ? value : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Shared empty result for unreadable or malformed locks.
|
|
199
|
+
*
|
|
200
|
+
* @returns {{pkgList: object[], dependencies: object[], rootInputs: string[]}}
|
|
201
|
+
*/
|
|
202
|
+
function emptyResult() {
|
|
203
|
+
return { pkgList: [], dependencies: [], rootInputs: [] };
|
|
204
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { parse as loadYaml } from "yaml";
|
|
4
|
+
|
|
5
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Haskell Stack parser for `stack.yaml` and its lock file.
|
|
9
|
+
*
|
|
10
|
+
* Stack resolves packages from a snapshot plus the `extra-deps` a project
|
|
11
|
+
* declares, and writes the completed locations to `stack.yaml.lock` (named
|
|
12
|
+
* after the configuration file, so a custom `foo.yaml` locks to
|
|
13
|
+
* `foo.yaml.lock`). The lock is YAML with two lists:
|
|
14
|
+
*
|
|
15
|
+
* - `packages` — one entry per extra-dep, each holding the `original`
|
|
16
|
+
* incomplete specification and the `completed` location. A Hackage
|
|
17
|
+
* location is the string `<name>-<version>@sha256:<hex>,<size>`; a
|
|
18
|
+
* repository location carries `url`, `commit` and `subdir`.
|
|
19
|
+
* - `snapshots` — the resolved snapshot chain, each with the `original`
|
|
20
|
+
* name (`lts-22.28`) and the `completed` url, size and sha256.
|
|
21
|
+
*
|
|
22
|
+
* The lock pins only the extra-deps: packages taken from the snapshot are not
|
|
23
|
+
* listed, because the snapshot url and digest already pin them. The snapshot
|
|
24
|
+
* is therefore recorded on the parent component rather than expanded.
|
|
25
|
+
*
|
|
26
|
+
* Hackage packages are identified with the registered `hackage` purl type
|
|
27
|
+
* (`pkg:hackage/<name>@<version>`).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a Stack project from its lock file, with optional `stack.yaml`.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} stackLockFile Path to `stack.yaml.lock` or `stack.lock`
|
|
34
|
+
* @param {string} [stackYamlFile] Path to `stack.yaml`, if present
|
|
35
|
+
* @returns {{ pkgList: object[], parentComponent: object, rootInputs: string[] }}
|
|
36
|
+
*/
|
|
37
|
+
export function parseStackProject(stackLockFile, stackYamlFile) {
|
|
38
|
+
const lock = readYamlFile(stackLockFile);
|
|
39
|
+
const stackYaml = stackYamlFile ? readYamlFile(stackYamlFile) : undefined;
|
|
40
|
+
const snapshot = snapshotName(lock, stackYaml);
|
|
41
|
+
|
|
42
|
+
const pkgList = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const entry of Array.isArray(lock?.packages) ? lock.packages : []) {
|
|
45
|
+
const pkg = stackPackageFromEntry(entry, stackLockFile);
|
|
46
|
+
if (!pkg || seen.has(pkg["bom-ref"])) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
seen.add(pkg["bom-ref"]);
|
|
50
|
+
pkgList.push(pkg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parentComponent = {};
|
|
54
|
+
if (stackYamlFile || pkgList.length) {
|
|
55
|
+
const name =
|
|
56
|
+
typeof stackYaml?.name === "string" && stackYaml.name
|
|
57
|
+
? stackYaml.name
|
|
58
|
+
: projectDirName(stackYamlFile || stackLockFile);
|
|
59
|
+
parentComponent.type = "application";
|
|
60
|
+
parentComponent.name = name;
|
|
61
|
+
parentComponent.description = `Haskell Stack project: ${name}`;
|
|
62
|
+
parentComponent.properties = [
|
|
63
|
+
{ name: "internal:SrcFile", value: stackYamlFile || stackLockFile },
|
|
64
|
+
];
|
|
65
|
+
if (snapshot) {
|
|
66
|
+
parentComponent.properties.push({
|
|
67
|
+
name: "cdx:stack:snapshot",
|
|
68
|
+
value: snapshot,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
pkgList,
|
|
74
|
+
parentComponent,
|
|
75
|
+
rootInputs: pkgList.map((pkg) => pkg["bom-ref"]),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a package record from one `packages` entry of the lock.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} entry Lock entry with `original` and `completed`
|
|
83
|
+
* @param {string} srcFile Lock file path for provenance
|
|
84
|
+
* @returns {object|undefined} Package record, or undefined when unrecognised
|
|
85
|
+
*/
|
|
86
|
+
function stackPackageFromEntry(entry, srcFile) {
|
|
87
|
+
const completed = entry?.completed ?? {};
|
|
88
|
+
const original = entry?.original ?? {};
|
|
89
|
+
const hackage =
|
|
90
|
+
typeof completed.hackage === "string"
|
|
91
|
+
? completed.hackage
|
|
92
|
+
: typeof original.hackage === "string"
|
|
93
|
+
? original.hackage
|
|
94
|
+
: undefined;
|
|
95
|
+
if (hackage) {
|
|
96
|
+
return hackagePackage(hackage, srcFile);
|
|
97
|
+
}
|
|
98
|
+
// Repository extra-deps pin a commit rather than a Hackage release; the
|
|
99
|
+
// package name is the repository's last path segment unless a subdir names
|
|
100
|
+
// the package more precisely.
|
|
101
|
+
const url = typeof completed.url === "string" ? completed.url : undefined;
|
|
102
|
+
const commit =
|
|
103
|
+
typeof completed.commit === "string" ? completed.commit : undefined;
|
|
104
|
+
if (!url) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const subdir =
|
|
108
|
+
typeof completed.subdir === "string" && completed.subdir
|
|
109
|
+
? completed.subdir.split("/").filter(Boolean).pop()
|
|
110
|
+
: undefined;
|
|
111
|
+
const repoName = url
|
|
112
|
+
.split(/[?#]/u)[0]
|
|
113
|
+
.replace(/\.git$/u, "")
|
|
114
|
+
.split("/")
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.pop();
|
|
117
|
+
const name = subdir || repoName;
|
|
118
|
+
if (!name) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return finishPackage(name, commit, srcFile, [
|
|
122
|
+
{ name: "cdx:stack:repository", value: url },
|
|
123
|
+
...(commit ? [{ name: "cdx:stack:commit", value: commit }] : []),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build a package record from a Hackage location string.
|
|
129
|
+
*
|
|
130
|
+
* The string is `<name>-<version>@sha256:<hex>,<size>`; the trailing
|
|
131
|
+
* `-<version>` separates the name, and the digest covers the package tarball.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} location Hackage location as the lock writes it
|
|
134
|
+
* @param {string} srcFile Lock file path for provenance
|
|
135
|
+
* @returns {object|undefined} Package record
|
|
136
|
+
*/
|
|
137
|
+
function hackagePackage(location, srcFile) {
|
|
138
|
+
const [coordinate, rest] = splitOnce(location, "@");
|
|
139
|
+
const match = coordinate.match(/^(.+)-(\d[^-]*)$/u);
|
|
140
|
+
if (!match) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const [, name, version] = match;
|
|
144
|
+
const sha256 = rest?.startsWith("sha256:")
|
|
145
|
+
? rest.slice("sha256:".length).split(",")[0]
|
|
146
|
+
: undefined;
|
|
147
|
+
const pkg = finishPackage(name, version, srcFile, []);
|
|
148
|
+
if (sha256 && /^[0-9a-f]{64}$/iu.test(sha256)) {
|
|
149
|
+
pkg.hashes = [{ alg: "SHA-256", content: sha256.toLowerCase() }];
|
|
150
|
+
}
|
|
151
|
+
return pkg;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Assemble the common fields of a Stack package record.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} name Package name
|
|
158
|
+
* @param {string|undefined} version Version or pinned commit
|
|
159
|
+
* @param {string} srcFile Lock file path for provenance
|
|
160
|
+
* @param {object[]} extraProperties Additional properties
|
|
161
|
+
* @returns {object} Package record
|
|
162
|
+
*/
|
|
163
|
+
function finishPackage(name, version, srcFile, extraProperties) {
|
|
164
|
+
const purl = tryBuildPurl({
|
|
165
|
+
type: "hackage",
|
|
166
|
+
name,
|
|
167
|
+
version: version || undefined,
|
|
168
|
+
});
|
|
169
|
+
const pkg = {
|
|
170
|
+
name,
|
|
171
|
+
...(version ? { version } : {}),
|
|
172
|
+
type: "library",
|
|
173
|
+
scope: "required",
|
|
174
|
+
properties: [
|
|
175
|
+
{ name: "internal:SrcFile", value: srcFile },
|
|
176
|
+
// The lock records extra-deps only; snapshot packages never appear.
|
|
177
|
+
{ name: "cdx:stack:dependency", value: "extra-dep" },
|
|
178
|
+
...extraProperties,
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
if (purl) {
|
|
182
|
+
pkg.purl = purl;
|
|
183
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
184
|
+
} else {
|
|
185
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
186
|
+
}
|
|
187
|
+
return pkg;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Determine the snapshot the project resolves against.
|
|
192
|
+
*
|
|
193
|
+
* `stack.yaml` names it under `snapshot` (or the older `resolver`); the lock
|
|
194
|
+
* repeats it as the `original` of the first snapshot entry.
|
|
195
|
+
*
|
|
196
|
+
* @param {object|undefined} lock Parsed lock file
|
|
197
|
+
* @param {object|undefined} stackYaml Parsed `stack.yaml`
|
|
198
|
+
* @returns {string|undefined} Snapshot name or url
|
|
199
|
+
*/
|
|
200
|
+
function snapshotName(lock, stackYaml) {
|
|
201
|
+
for (const candidate of [stackYaml?.snapshot, stackYaml?.resolver]) {
|
|
202
|
+
if (typeof candidate === "string" && candidate) {
|
|
203
|
+
return candidate;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const first = Array.isArray(lock?.snapshots) ? lock.snapshots[0] : undefined;
|
|
207
|
+
const original = first?.original;
|
|
208
|
+
if (typeof original === "string" && original) {
|
|
209
|
+
return original;
|
|
210
|
+
}
|
|
211
|
+
if (typeof original?.url === "string" && original.url) {
|
|
212
|
+
return original.url;
|
|
213
|
+
}
|
|
214
|
+
if (typeof first?.completed?.url === "string") {
|
|
215
|
+
return first.completed.url;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Split a string at the first occurrence of a separator.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} value Value to split
|
|
224
|
+
* @param {string} separator Separator character
|
|
225
|
+
* @returns {[string, string|undefined]} Head and remainder
|
|
226
|
+
*/
|
|
227
|
+
function splitOnce(value, separator) {
|
|
228
|
+
const index = value.indexOf(separator);
|
|
229
|
+
if (index === -1) {
|
|
230
|
+
return [value, undefined];
|
|
231
|
+
}
|
|
232
|
+
return [value.slice(0, index), value.slice(index + 1)];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Derive a project name from the directory holding the Stack files.
|
|
237
|
+
*
|
|
238
|
+
* Stack manifests never carry a project name, so the directory is the name
|
|
239
|
+
* developers already use for the project.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} filePath File path
|
|
242
|
+
* @returns {string} Directory name
|
|
243
|
+
*/
|
|
244
|
+
function projectDirName(filePath) {
|
|
245
|
+
const segments = filePath.split(/[\\/]/u).filter(Boolean);
|
|
246
|
+
// Drop the file itself, keeping the directory.
|
|
247
|
+
segments.pop();
|
|
248
|
+
return segments[segments.length - 1] || "stack-project";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Read and parse a YAML file, warning instead of throwing.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} filePath File to read
|
|
255
|
+
* @returns {object|undefined} Parsed value
|
|
256
|
+
*/
|
|
257
|
+
function readYamlFile(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
return loadYaml(readFileSync(filePath, "utf-8"));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.warn(`Failed to parse ${filePath}: ${error.message}`);
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Terraform and OpenTofu parser for `.terraform.lock.hcl`.
|
|
7
|
+
*
|
|
8
|
+
* The lock file pins every provider the configuration resolves to, with its
|
|
9
|
+
* exact version, the version constraints that produced it, and content
|
|
10
|
+
* hashes. It is written in a small, regular subset of HCL: provider blocks,
|
|
11
|
+
* `version` and `constraints` attributes, and a `hashes` list of prefixed
|
|
12
|
+
* digests. A line scanner is sufficient and avoids a general HCL parser.
|
|
13
|
+
*
|
|
14
|
+
* Provider addresses look like `registry.terraform.io/hashicorp/aws`; the
|
|
15
|
+
* registry host and namespace identify the provider upstream, so they are
|
|
16
|
+
* kept in the component group and a generic purl
|
|
17
|
+
* (`pkg:generic/<host>/<namespace>/<type>@<version>`), with the intended type
|
|
18
|
+
* recorded as a `cdx:purl:proposedType` property because no `terraform`
|
|
19
|
+
* purl type is registered.
|
|
20
|
+
*
|
|
21
|
+
* The two hash schemes in the `hashes` list mean different things. A `zh:`
|
|
22
|
+
* digest is a SHA-256 of the official `.zip` package as the registry indexes
|
|
23
|
+
* it, so it is the one a consumer can check an artifact against and the one
|
|
24
|
+
* emitted in the CycloneDX `hashes` array. An `h1:` digest is base64 over a
|
|
25
|
+
* hash of the package's *contents* rather than the archive, which lets
|
|
26
|
+
* Terraform verify an unpacked directory but makes it useless as an artifact
|
|
27
|
+
* checksum; it is kept as a property instead of being mislabelled SHA-256.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a `.terraform.lock.hcl` file.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} lockFile Path to the lock file
|
|
34
|
+
* @returns {{ pkgList: object[] }} Provider components
|
|
35
|
+
*/
|
|
36
|
+
export function parseTerraformLockFile(lockFile) {
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = readFileSync(lockFile, "utf-8");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn(`Failed to read ${lockFile}: ${error.message}`);
|
|
42
|
+
return { pkgList: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pkgList = [];
|
|
46
|
+
let current;
|
|
47
|
+
let inHashes = false;
|
|
48
|
+
for (const rawLine of text.split("\n")) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
if (!line || line.startsWith("#")) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (inHashes) {
|
|
54
|
+
if (line.startsWith("]")) {
|
|
55
|
+
inHashes = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const hash = line.match(/^"([^"]+)"/u);
|
|
59
|
+
if (hash && current) {
|
|
60
|
+
current.candidates.push(hash[1]);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const block = line.match(/^provider\s+"([^"]+)"\s*\{/u);
|
|
65
|
+
if (block) {
|
|
66
|
+
current = { address: block[1], candidates: [] };
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line.startsWith("}")) {
|
|
70
|
+
if (current) {
|
|
71
|
+
const pkg = buildProviderComponent(current, lockFile);
|
|
72
|
+
if (pkg) {
|
|
73
|
+
pkgList.push(pkg);
|
|
74
|
+
}
|
|
75
|
+
current = undefined;
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const version = matchAttribute(line, "version");
|
|
80
|
+
if (version && current) {
|
|
81
|
+
current.version = version;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const constraints = matchAttribute(line, "constraints");
|
|
85
|
+
if (constraints && current) {
|
|
86
|
+
current.constraints = constraints;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (/^hashes\s*=\s*\[/u.test(line) && current) {
|
|
90
|
+
inHashes = true;
|
|
91
|
+
// Some writers put the first digest on the same line as the bracket.
|
|
92
|
+
const inline = line.replace(/^hashes\s*=\s*\[\s*/u, "");
|
|
93
|
+
const inlineHash = inline.match(/^"([^"]+)"/u);
|
|
94
|
+
if (inlineHash) {
|
|
95
|
+
current.candidates.push(inlineHash[1]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { pkgList };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Match a string attribute assignment such as `version = "5.80.0"`.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} line Trimmed line
|
|
106
|
+
* @param {string} name Attribute name
|
|
107
|
+
* @returns {string|undefined} Attribute value when present
|
|
108
|
+
*/
|
|
109
|
+
function matchAttribute(line, name) {
|
|
110
|
+
const match = line.match(new RegExp(`^${name}\\s*=\\s*"([^"]*)"`, "u"));
|
|
111
|
+
return match ? match[1] : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build a provider component from one parsed block.
|
|
116
|
+
*
|
|
117
|
+
* @param {{address: string, version?: string, constraints?: string, candidates: string[]}} block
|
|
118
|
+
* Parsed provider block
|
|
119
|
+
* @param {string} srcFile Lock file path
|
|
120
|
+
* @returns {object|undefined} Component record
|
|
121
|
+
*/
|
|
122
|
+
function buildProviderComponent(block, srcFile) {
|
|
123
|
+
const segments = block.address.split("/").filter(Boolean);
|
|
124
|
+
if (segments.length < 2) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const name = segments[segments.length - 1];
|
|
128
|
+
const namespace = segments.slice(0, -1).join("/");
|
|
129
|
+
const purl = tryBuildPurl({
|
|
130
|
+
type: "generic",
|
|
131
|
+
namespace,
|
|
132
|
+
name,
|
|
133
|
+
version: block.version,
|
|
134
|
+
});
|
|
135
|
+
const properties = [
|
|
136
|
+
{ name: "internal:SrcFile", value: srcFile },
|
|
137
|
+
{ name: "cdx:purl:proposedType", value: "terraform-provider" },
|
|
138
|
+
{ name: "cdx:tf:address", value: block.address },
|
|
139
|
+
];
|
|
140
|
+
if (block.constraints) {
|
|
141
|
+
properties.push({ name: "cdx:tf:constraints", value: block.constraints });
|
|
142
|
+
}
|
|
143
|
+
const contentHash = block.candidates.find((candidate) =>
|
|
144
|
+
candidate.startsWith("h1:"),
|
|
145
|
+
);
|
|
146
|
+
if (contentHash) {
|
|
147
|
+
properties.push({ name: "cdx:tf:h1", value: contentHash });
|
|
148
|
+
}
|
|
149
|
+
const pkg = {
|
|
150
|
+
group: namespace,
|
|
151
|
+
name,
|
|
152
|
+
...(block.version ? { version: block.version } : {}),
|
|
153
|
+
type: "library",
|
|
154
|
+
scope: "required",
|
|
155
|
+
properties,
|
|
156
|
+
};
|
|
157
|
+
const sha256 = zipDigestsHex(block.candidates);
|
|
158
|
+
if (sha256.length) {
|
|
159
|
+
pkg.hashes = sha256.map((content) => ({ alg: "SHA-256", content }));
|
|
160
|
+
}
|
|
161
|
+
if (purl) {
|
|
162
|
+
pkg.purl = purl;
|
|
163
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
164
|
+
} else {
|
|
165
|
+
pkg["bom-ref"] = `library:${block.address}:${block.version || ""}`;
|
|
166
|
+
}
|
|
167
|
+
return pkg;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Collect the `zh:` package digests of a provider block.
|
|
172
|
+
*
|
|
173
|
+
* A provider is published as one zip per platform and the lock records a
|
|
174
|
+
* digest for each, so a block yields several digests for the same version.
|
|
175
|
+
* All of them are returned: each is a genuine SHA-256 of a distributed
|
|
176
|
+
* artifact for this component, and the lock does not say which platform a
|
|
177
|
+
* given digest belongs to.
|
|
178
|
+
*
|
|
179
|
+
* @param {string[]} candidates Prefixed digests from the hashes list
|
|
180
|
+
* @returns {string[]} Lowercase hex digests, in file order and deduplicated
|
|
181
|
+
*/
|
|
182
|
+
function zipDigestsHex(candidates) {
|
|
183
|
+
const digests = new Set();
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
if (!candidate.startsWith("zh:")) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const digest = candidate.slice(3).toLowerCase();
|
|
189
|
+
if (/^[0-9a-f]{64}$/u.test(digest)) {
|
|
190
|
+
digests.add(digest);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return [...digests];
|
|
194
|
+
}
|