@cdxgen/cdxgen 13.1.0 → 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 +8 -7
- package/bin/cdxgen.js +16 -3
- package/bin/evinse.js +2 -0
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +23 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +12 -0
- package/lib/cli/index.js +192 -1
- package/lib/cli/jsBom.js +59 -2
- package/lib/cli/jvmBom.js +22 -0
- package/lib/cli/managedBom.js +152 -0
- package/lib/cli/nativeBom.js +798 -1
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +39 -1
- 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 +241 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +143 -2
- 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-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +172 -0
- 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 +1 -1
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +932 -17
- 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/rusi.js +126 -6
- package/lib/evinser/evinser.js +111 -1
- package/lib/helpers/agenticToolutils.js +925 -0
- 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 +8 -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/dosai.js +77 -0
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +74 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- 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/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/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +169 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/package.json +25 -25
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -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/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- 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 +20 -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-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- 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-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/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +29 -0
- 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/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 +4 -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/dosai.d.ts +23 -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 +34 -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 +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- 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/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/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/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nim parser for `.nimble` manifests and `nimble.lock`.
|
|
8
|
+
*
|
|
9
|
+
* A `.nimble` file is a Nim script whose practical surface is a set of
|
|
10
|
+
* `key = "value"` assignments plus `requires "dep >= 1.0"` lines; it is read
|
|
11
|
+
* with a line scanner over those shapes rather than a Nim interpreter.
|
|
12
|
+
* Nimble persists the resolved dependency set to `nimble.lock` (JSON), which
|
|
13
|
+
* is the source of truth for versions.
|
|
14
|
+
*
|
|
15
|
+
* No `nim` purl type is registered, so packages are identified as generic
|
|
16
|
+
* carrying a `cdx:purl:proposedType=nim` property, following the convention
|
|
17
|
+
* used for nix and zig.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a Nim project from a `.nimble` manifest and optional `nimble.lock`.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} nimbleFile Path to the `.nimble` file
|
|
24
|
+
* @param {string} [nimbleLockFile] Path to `nimble.lock`, if present
|
|
25
|
+
* @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs?: string[] }}
|
|
26
|
+
*/
|
|
27
|
+
export function parseNimProject(nimbleFile, nimbleLockFile) {
|
|
28
|
+
const fields = readNimbleFile(nimbleFile);
|
|
29
|
+
const parentComponent = {};
|
|
30
|
+
// Nimble files frequently omit `name`; the manifest file name is the
|
|
31
|
+
// package name in that case, which is also how nimble itself refers to it.
|
|
32
|
+
const projectName =
|
|
33
|
+
fields.name || basename(nimbleFile).replace(/\.nimble$/iu, "");
|
|
34
|
+
if (projectName) {
|
|
35
|
+
parentComponent.type = "application";
|
|
36
|
+
parentComponent.name = projectName;
|
|
37
|
+
if (fields.version) {
|
|
38
|
+
parentComponent.version = fields.version;
|
|
39
|
+
}
|
|
40
|
+
parentComponent.description =
|
|
41
|
+
fields.description || `Nim package: ${fields.name}`;
|
|
42
|
+
parentComponent.properties = [
|
|
43
|
+
{ name: "internal:SrcFile", value: nimbleFile },
|
|
44
|
+
];
|
|
45
|
+
if (fields.license) {
|
|
46
|
+
parentComponent.license = fields.license;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let lock;
|
|
51
|
+
if (nimbleLockFile) {
|
|
52
|
+
try {
|
|
53
|
+
lock = JSON.parse(readFileSync(nimbleLockFile, "utf-8"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`Failed to parse ${nimbleLockFile}: ${error.message}`);
|
|
56
|
+
lock = undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// nimble.lock has carried `packages` both as an object keyed by name and as
|
|
61
|
+
// an array of records; both shapes are accepted so locks from either
|
|
62
|
+
// nimble generation parse.
|
|
63
|
+
const lockedEntries = [];
|
|
64
|
+
if (Array.isArray(lock?.packages)) {
|
|
65
|
+
lockedEntries.push(...lock.packages);
|
|
66
|
+
} else if (lock?.packages && typeof lock.packages === "object") {
|
|
67
|
+
for (const [name, entry] of Object.entries(lock.packages)) {
|
|
68
|
+
lockedEntries.push({ name, ...entry });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!lockedEntries.length) {
|
|
73
|
+
// Lockless fallback: emit declared requirements without resolved versions.
|
|
74
|
+
const pkgList = fields.requires.map((req) =>
|
|
75
|
+
nimPackage(req.name, undefined, { direct: true, srcFile: nimbleFile }),
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
pkgList,
|
|
79
|
+
dependencies: [],
|
|
80
|
+
parentComponent,
|
|
81
|
+
rootInputs: pkgList.map((p) => p["bom-ref"]),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const directNames = new Set(fields.requires.map((req) => req.name));
|
|
86
|
+
const pkgList = [];
|
|
87
|
+
const refByName = new Map();
|
|
88
|
+
const edges = [];
|
|
89
|
+
for (const entry of lockedEntries) {
|
|
90
|
+
if (typeof entry?.name !== "string" || !entry.name) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const version =
|
|
94
|
+
typeof entry.version === "string" && entry.version
|
|
95
|
+
? entry.version
|
|
96
|
+
: undefined;
|
|
97
|
+
const pkg = nimPackage(entry.name, version, {
|
|
98
|
+
direct: directNames.has(entry.name),
|
|
99
|
+
vcsRevision:
|
|
100
|
+
typeof entry.vcsRevision === "string" && entry.vcsRevision
|
|
101
|
+
? entry.vcsRevision
|
|
102
|
+
: undefined,
|
|
103
|
+
srcFile: nimbleLockFile,
|
|
104
|
+
});
|
|
105
|
+
pkgList.push(pkg);
|
|
106
|
+
refByName.set(entry.name, pkg["bom-ref"]);
|
|
107
|
+
}
|
|
108
|
+
for (const entry of lockedEntries) {
|
|
109
|
+
const ref = refByName.get(entry.name);
|
|
110
|
+
if (!ref) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const dependsOn = (
|
|
114
|
+
Array.isArray(entry.dependencies) ? entry.dependencies : []
|
|
115
|
+
)
|
|
116
|
+
.map((depName) =>
|
|
117
|
+
typeof depName === "string" ? refByName.get(depName) : undefined,
|
|
118
|
+
)
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
edges.push({ ref, dependsOn: [...new Set(dependsOn)].sort() });
|
|
121
|
+
}
|
|
122
|
+
const rootInputs = [...directNames]
|
|
123
|
+
.map((name) => refByName.get(name))
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
return { pkgList, dependencies: edges, parentComponent, rootInputs };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build a component-like package record for a Nim package.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} name Package name
|
|
132
|
+
* @param {string|undefined} version Resolved version, if known
|
|
133
|
+
* @param {object} opts Extra context (`direct`, `vcsRevision`, `srcFile`)
|
|
134
|
+
* @returns {object} Package record
|
|
135
|
+
*/
|
|
136
|
+
function nimPackage(name, version, opts) {
|
|
137
|
+
const purl = tryBuildPurl({
|
|
138
|
+
type: "generic",
|
|
139
|
+
name,
|
|
140
|
+
version: version || undefined,
|
|
141
|
+
});
|
|
142
|
+
const properties = [
|
|
143
|
+
{ name: "internal:SrcFile", value: opts.srcFile },
|
|
144
|
+
{ name: "cdx:purl:proposedType", value: "nim" },
|
|
145
|
+
{
|
|
146
|
+
name: "cdx:nim:dependency",
|
|
147
|
+
value: opts.direct ? "direct" : "transitive",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
if (opts.vcsRevision) {
|
|
151
|
+
properties.push({ name: "cdx:nim:vcsRevision", value: opts.vcsRevision });
|
|
152
|
+
}
|
|
153
|
+
const pkg = {
|
|
154
|
+
name,
|
|
155
|
+
...(version ? { version } : {}),
|
|
156
|
+
type: "library",
|
|
157
|
+
scope: "required",
|
|
158
|
+
properties,
|
|
159
|
+
};
|
|
160
|
+
if (purl) {
|
|
161
|
+
pkg.purl = purl;
|
|
162
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
163
|
+
} else {
|
|
164
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
165
|
+
}
|
|
166
|
+
return pkg;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read the assignments and requirements of a `.nimble` file.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} filePath File to read
|
|
173
|
+
* @returns {{name?: string, version?: string, description?: string, license?: string, requires: Array<{name: string, constraint?: string}>}}
|
|
174
|
+
*/
|
|
175
|
+
export function readNimbleFile(filePath) {
|
|
176
|
+
const result = { requires: [] };
|
|
177
|
+
let text;
|
|
178
|
+
try {
|
|
179
|
+
text = readFileSync(filePath, "utf-8");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`Failed to read ${filePath}: ${error.message}`);
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
for (const rawLine of text.split("\n")) {
|
|
185
|
+
const line = rawLine.trim();
|
|
186
|
+
if (!line || line.startsWith("#")) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const assignment = line.match(
|
|
190
|
+
/^(name|version|description|license)\s*=\s*"([^"]*)"/u,
|
|
191
|
+
);
|
|
192
|
+
if (assignment) {
|
|
193
|
+
const value = assignment[2];
|
|
194
|
+
if (value) {
|
|
195
|
+
result[assignment[1]] = value;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const requires = line.match(/^requires\s+"([^"]+)"/u);
|
|
200
|
+
if (requires) {
|
|
201
|
+
const tokens = requires[1].split(/\s+/u);
|
|
202
|
+
const first = tokens[0];
|
|
203
|
+
const constraint = tokens.slice(1).join(" ");
|
|
204
|
+
if (first.startsWith("http") || first.includes("://")) {
|
|
205
|
+
// URL requirements are identified by the repository name, which is
|
|
206
|
+
// how nimble itself keys them in the lock file. A trailing .nim
|
|
207
|
+
// extension is dropped because the lock keys by the bare name.
|
|
208
|
+
const withoutExtension = first.split(/[?#]/u)[0].replace(/\.git$/u, "");
|
|
209
|
+
const segments = withoutExtension.split("/").filter(Boolean);
|
|
210
|
+
const repoName = segments[segments.length - 1] || first;
|
|
211
|
+
result.requires.push({
|
|
212
|
+
name: repoName.replace(/\.nim$/iu, ""),
|
|
213
|
+
constraint,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
result.requires.push({ name: first, constraint });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OCaml opam parser for `.opam.locked` files.
|
|
7
|
+
*
|
|
8
|
+
* `opam lock` writes a `<name>.opam.locked` file that pins the full resolved
|
|
9
|
+
* dependency set of a project: a `depends:` field listing every package with
|
|
10
|
+
* an equality constraint on its version, and a `pin-depends:` field for
|
|
11
|
+
* packages pinned to a URL. The file is opam's own syntax, a small
|
|
12
|
+
* line-oriented field language, which is read here with a line scanner rather
|
|
13
|
+
* than a monolithic expression.
|
|
14
|
+
*
|
|
15
|
+
* A locked file holds the closure only: it does not record which packages are
|
|
16
|
+
* direct dependencies of the project, so every entry is reported with the
|
|
17
|
+
* same standing and the project root links to all of them.
|
|
18
|
+
*
|
|
19
|
+
* Packages are identified with the registered `opam` purl type
|
|
20
|
+
* (`pkg:opam/<name>@<version>`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse an `.opam.locked` (or plain `.opam`) file.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} opamLockedFile Path to the opam file
|
|
27
|
+
* @returns {{ pkgList: object[], pins: Array<{name: string, url: string}> }}
|
|
28
|
+
*/
|
|
29
|
+
export function parseOpamLockedFile(opamLockedFile) {
|
|
30
|
+
let text;
|
|
31
|
+
try {
|
|
32
|
+
text = readFileSync(opamLockedFile, "utf-8");
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn(`Failed to read ${opamLockedFile}: ${error.message}`);
|
|
35
|
+
return { pkgList: [], pins: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const depends = [];
|
|
39
|
+
const pins = [];
|
|
40
|
+
const lines = text.split("\n");
|
|
41
|
+
let section;
|
|
42
|
+
for (const rawLine of lines) {
|
|
43
|
+
const line = rawLine.trim();
|
|
44
|
+
if (!line || line.startsWith("#")) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (line.startsWith("depends:")) {
|
|
48
|
+
section = "depends";
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith("pin-depends:")) {
|
|
52
|
+
section = "pin-depends";
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// A bare `field:` at the start of a line ends the previous list section.
|
|
56
|
+
if (/^[A-Za-z0-9_-]+:/u.test(line)) {
|
|
57
|
+
section = undefined;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (section === "depends") {
|
|
61
|
+
collectDependsEntry(line, depends, opamLockedFile);
|
|
62
|
+
} else if (section === "pin-depends") {
|
|
63
|
+
collectPinEntry(line, pins);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// A pin overrides where a package is fetched from; reflect that on the
|
|
67
|
+
// resolved component so consumers can see the divergence from the registry.
|
|
68
|
+
for (const pin of pins) {
|
|
69
|
+
const target = depends.find((dep) => dep.name === pin.name);
|
|
70
|
+
if (target) {
|
|
71
|
+
target.properties.push({ name: "cdx:opam:pinned", value: "true" });
|
|
72
|
+
target.properties.push({
|
|
73
|
+
name: "cdx:opam:pinSource",
|
|
74
|
+
value: pin.url,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { pkgList: depends, pins };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Turn one `depends` list line into a pinned package record.
|
|
83
|
+
*
|
|
84
|
+
* Entries look like `"dune" {= "3.16.0"}` or `"ocaml" {= "5.1.1" & build}`.
|
|
85
|
+
* Only equality pins carry a version; constraint-only entries are recorded
|
|
86
|
+
* without one so the package is still inventoried.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} line Trimmed list line
|
|
89
|
+
* @param {object[]} depends Accumulator for package records
|
|
90
|
+
* @param {string} srcFile Source path for provenance
|
|
91
|
+
*/
|
|
92
|
+
function collectDependsEntry(line, depends, srcFile) {
|
|
93
|
+
const nameMatch = line.match(/^"([^"]+)"/u);
|
|
94
|
+
if (!nameMatch) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const name = nameMatch[1];
|
|
98
|
+
let version;
|
|
99
|
+
const constraints = line.match(/\{[^}]*\}/gu) || [];
|
|
100
|
+
for (const constraint of constraints) {
|
|
101
|
+
// A pin is a standalone `=`, not the tail of `>=`, `<=`, or `!=`.
|
|
102
|
+
const pinned = constraint.match(/(?<![<>!=])=(?!=)\s*"([^"]+)"/u);
|
|
103
|
+
if (pinned) {
|
|
104
|
+
version = pinned[1];
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (depends.some((dep) => dep.name === name && dep.version === version)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const purl = tryBuildPurl({
|
|
112
|
+
type: "opam",
|
|
113
|
+
name,
|
|
114
|
+
version: version || undefined,
|
|
115
|
+
});
|
|
116
|
+
const properties = [{ name: "internal:SrcFile", value: srcFile }];
|
|
117
|
+
const pkg = {
|
|
118
|
+
name,
|
|
119
|
+
...(version ? { version } : {}),
|
|
120
|
+
type: "library",
|
|
121
|
+
scope: "required",
|
|
122
|
+
properties,
|
|
123
|
+
};
|
|
124
|
+
if (purl) {
|
|
125
|
+
pkg.purl = purl;
|
|
126
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
127
|
+
} else {
|
|
128
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
129
|
+
}
|
|
130
|
+
depends.push(pkg);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Turn one `pin-depends` list line into a pin record.
|
|
135
|
+
*
|
|
136
|
+
* Entries look like `[ "mylib.dev" "git+https://host/path#rev" ]`. The first
|
|
137
|
+
* string is a package *version* identifier — `<name>.<version>` — whereas the
|
|
138
|
+
* matching `depends` entry carries the bare name, so the version suffix is
|
|
139
|
+
* split off before the two can be matched. Package names cannot contain a
|
|
140
|
+
* dot, which makes the first dot the separator.
|
|
141
|
+
*
|
|
142
|
+
* The URL is reduced to its origin and path so credentials embedded in a
|
|
143
|
+
* query string never reach the BOM.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} line Trimmed list line
|
|
146
|
+
* @param {Array<{name: string, url: string}>} pins Accumulator for pins
|
|
147
|
+
*/
|
|
148
|
+
function collectPinEntry(line, pins) {
|
|
149
|
+
const strings = line.match(/"[^"]*"/gu) || [];
|
|
150
|
+
if (strings.length < 2) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const identifier = strings[0].slice(1, -1);
|
|
154
|
+
const rawUrl = strings[1].slice(1, -1);
|
|
155
|
+
pins.push({
|
|
156
|
+
name: identifier.split(".")[0],
|
|
157
|
+
url: sanitizedPinUrl(rawUrl),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reduce a pin URL to a safe origin plus path.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} rawUrl URL as written in the lock file
|
|
165
|
+
* @returns {string} Sanitised URL, or the raw value when it is not a URL
|
|
166
|
+
*/
|
|
167
|
+
function sanitizedPinUrl(rawUrl) {
|
|
168
|
+
const schemeSplit = rawUrl.split("://");
|
|
169
|
+
if (schemeSplit.length < 2) {
|
|
170
|
+
return rawUrl;
|
|
171
|
+
}
|
|
172
|
+
const scheme = schemeSplit[0];
|
|
173
|
+
const rest = schemeSplit[1];
|
|
174
|
+
const pathStart = rest.indexOf("/");
|
|
175
|
+
const authority = pathStart === -1 ? rest : rest.slice(0, pathStart);
|
|
176
|
+
const path = pathStart === -1 ? "" : rest.slice(pathStart).split(/[?#]/u)[0];
|
|
177
|
+
// Userinfo is dropped by taking only the authority's host portion.
|
|
178
|
+
const host = authority.split("@").pop();
|
|
179
|
+
return `${scheme}://${host}${path}`;
|
|
180
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Perl parser for `cpanfile` and `cpanfile.snapshot`.
|
|
7
|
+
*
|
|
8
|
+
* Carton snapshots the resolved install set as a text document: one entry per
|
|
9
|
+
* distribution, each carrying the `pathname` it was fetched from (which
|
|
10
|
+
* encodes the PAUSE author id), the packages it provides, and the
|
|
11
|
+
* requirements it was resolved against. The declared `cpanfile` lists the
|
|
12
|
+
* project's own requirements.
|
|
13
|
+
*
|
|
14
|
+
* Distributions are identified with the registered `cpan` purl type, whose
|
|
15
|
+
* rules require the author id as the namespace
|
|
16
|
+
* (`pkg:cpan/<AUTHOR>/<Distribution>@<version>`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a `cpanfile.snapshot` into distributions with a dependency graph.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} snapshotFile Path to `cpanfile.snapshot`
|
|
23
|
+
* @param {string} [cpanfileFile] Path to `cpanfile`, if present
|
|
24
|
+
* @returns {{ pkgList: object[], dependencies: object[] }}
|
|
25
|
+
*/
|
|
26
|
+
export function parseCpanSnapshot(snapshotFile, cpanfileFile) {
|
|
27
|
+
let text;
|
|
28
|
+
try {
|
|
29
|
+
text = readFileSync(snapshotFile, "utf-8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(`Failed to read ${snapshotFile}: ${error.message}`);
|
|
32
|
+
return { pkgList: [], dependencies: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const distributions = [];
|
|
36
|
+
let current;
|
|
37
|
+
let section;
|
|
38
|
+
for (const rawLine of text.split("\n")) {
|
|
39
|
+
if (!rawLine.trim() || rawLine.trim().startsWith("#")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
43
|
+
const line = rawLine.trim();
|
|
44
|
+
if (indent === 0) {
|
|
45
|
+
// Top-level headers separate the snapshot sections.
|
|
46
|
+
current = undefined;
|
|
47
|
+
section = line === "DISTRIBUTIONS" ? "distributions" : undefined;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (line.startsWith("DISTRIBUTIONS")) {
|
|
51
|
+
section = "distributions";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (section !== "distributions") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// A distribution line is indented by two spaces and carries
|
|
58
|
+
// `<Name>-<version>`; its details follow indented deeper.
|
|
59
|
+
if (indent === 2) {
|
|
60
|
+
const dist = parseDistributionLine(line);
|
|
61
|
+
if (dist) {
|
|
62
|
+
distributions.push(dist);
|
|
63
|
+
current = dist;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!current) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (line.startsWith("pathname:")) {
|
|
71
|
+
current.author = authorFromPathname(
|
|
72
|
+
line.slice("pathname:".length).trim(),
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (line.startsWith("provides:")) {
|
|
77
|
+
current.inProvides = true;
|
|
78
|
+
current.inRequirements = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (line.startsWith("requirements:")) {
|
|
82
|
+
current.inRequirements = true;
|
|
83
|
+
current.inProvides = false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Deeper indented `key value` lines under provides/requirements.
|
|
87
|
+
const pair = line.match(/^([A-Za-z0-9_:.-]+)\s+(\S+)$/u);
|
|
88
|
+
if (pair) {
|
|
89
|
+
if (current.inProvides) {
|
|
90
|
+
current.provides.set(pair[1], pair[2]);
|
|
91
|
+
} else if (current.inRequirements) {
|
|
92
|
+
current.requirements.push(pair[1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const directNames = cpanfileFile
|
|
98
|
+
? directRequirementsFromCpanfile(cpanfileFile)
|
|
99
|
+
: new Set();
|
|
100
|
+
|
|
101
|
+
// Map every provided package name to its distribution so requirements can
|
|
102
|
+
// be resolved to edges between distributions.
|
|
103
|
+
const distByPackage = new Map();
|
|
104
|
+
for (const dist of distributions) {
|
|
105
|
+
for (const provided of dist.provides.keys()) {
|
|
106
|
+
if (!distByPackage.has(provided)) {
|
|
107
|
+
distByPackage.set(provided, dist);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pkgList = [];
|
|
113
|
+
const refByDist = new Map();
|
|
114
|
+
for (const dist of distributions) {
|
|
115
|
+
const name = dist.name;
|
|
116
|
+
const version = dist.version;
|
|
117
|
+
const direct =
|
|
118
|
+
directNames.size === 0
|
|
119
|
+
? true
|
|
120
|
+
: [...dist.provides.keys()].some((pkg) => directNames.has(pkg));
|
|
121
|
+
const pkg = cpanPackage(name, version, dist.author, {
|
|
122
|
+
direct,
|
|
123
|
+
srcFile: snapshotFile,
|
|
124
|
+
});
|
|
125
|
+
pkgList.push(pkg);
|
|
126
|
+
refByDist.set(dist, pkg["bom-ref"]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const dependencies = [];
|
|
130
|
+
for (const dist of distributions) {
|
|
131
|
+
const ref = refByDist.get(dist);
|
|
132
|
+
const dependsOn = new Set();
|
|
133
|
+
for (const requirement of dist.requirements) {
|
|
134
|
+
const target = distByPackage.get(requirement);
|
|
135
|
+
if (target && target !== dist) {
|
|
136
|
+
const targetRef = refByDist.get(target);
|
|
137
|
+
if (targetRef) {
|
|
138
|
+
dependsOn.add(targetRef);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
|
|
143
|
+
}
|
|
144
|
+
return { pkgList, dependencies };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse a `Name-Version` distribution line.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} line Trimmed distribution line
|
|
151
|
+
* @param {string} srcFile Source path for provenance
|
|
152
|
+
* @returns {object|undefined} Distribution accumulator
|
|
153
|
+
*/
|
|
154
|
+
function parseDistributionLine(line) {
|
|
155
|
+
const match = line.match(/^([A-Za-z0-9_.-]+?)-(\d[^ ]*)$/u);
|
|
156
|
+
if (!match) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
name: match[1],
|
|
161
|
+
version: match[2],
|
|
162
|
+
author: undefined,
|
|
163
|
+
provides: new Map(),
|
|
164
|
+
requirements: [],
|
|
165
|
+
inProvides: false,
|
|
166
|
+
inRequirements: false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract the PAUSE author id from a CPAN pathname.
|
|
172
|
+
*
|
|
173
|
+
* Pathnames look like `M/MI/MIYAGAWA/Plack-1.0051.tar.gz`; the author id is
|
|
174
|
+
* the third segment.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} pathname CPAN pathname
|
|
177
|
+
* @returns {string|undefined} Author id
|
|
178
|
+
*/
|
|
179
|
+
function authorFromPathname(pathname) {
|
|
180
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
181
|
+
return segments.length >= 3 ? segments[2] : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build a component-like package record for a CPAN distribution.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} name Distribution name
|
|
188
|
+
* @param {string|undefined} version Distribution version
|
|
189
|
+
* @param {string|undefined} author PAUSE author id
|
|
190
|
+
* @param {object} opts Extra context (`direct`, `srcFile`)
|
|
191
|
+
* @returns {object} Package record
|
|
192
|
+
*/
|
|
193
|
+
function cpanPackage(name, version, author, opts) {
|
|
194
|
+
// The cpan purl type requires the author namespace; without it the purl is
|
|
195
|
+
// dropped rather than emitted without its identity anchor.
|
|
196
|
+
const purl = author
|
|
197
|
+
? tryBuildPurl({
|
|
198
|
+
type: "cpan",
|
|
199
|
+
namespace: author,
|
|
200
|
+
name,
|
|
201
|
+
version: version || undefined,
|
|
202
|
+
})
|
|
203
|
+
: null;
|
|
204
|
+
const properties = [
|
|
205
|
+
{ name: "internal:SrcFile", value: opts.srcFile },
|
|
206
|
+
{
|
|
207
|
+
name: "cdx:cpan:dependency",
|
|
208
|
+
value: opts.direct ? "direct" : "transitive",
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
const pkg = {
|
|
212
|
+
name,
|
|
213
|
+
...(version ? { version } : {}),
|
|
214
|
+
type: "library",
|
|
215
|
+
scope: "required",
|
|
216
|
+
properties,
|
|
217
|
+
};
|
|
218
|
+
if (purl) {
|
|
219
|
+
pkg.purl = purl;
|
|
220
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
221
|
+
} else {
|
|
222
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
223
|
+
}
|
|
224
|
+
return pkg;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Collect the requirement names declared in a `cpanfile`.
|
|
229
|
+
*
|
|
230
|
+
* Only the `requires` keyword outside phase blocks is read; phases such as
|
|
231
|
+
* `on 'test'` are ignored so test-only modules do not become direct
|
|
232
|
+
* dependencies.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} cpanfileFile Path to `cpanfile`
|
|
235
|
+
* @returns {Set<string>} Declared requirement names
|
|
236
|
+
*/
|
|
237
|
+
export function directRequirementsFromCpanfile(cpanfileFile) {
|
|
238
|
+
const names = new Set();
|
|
239
|
+
let text;
|
|
240
|
+
try {
|
|
241
|
+
text = readFileSync(cpanfileFile, "utf-8");
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.warn(`Failed to read ${cpanfileFile}: ${error.message}`);
|
|
244
|
+
return names;
|
|
245
|
+
}
|
|
246
|
+
let inPhaseBlock = false;
|
|
247
|
+
let blockDepth = 0;
|
|
248
|
+
for (const rawLine of text.split("\n")) {
|
|
249
|
+
const line = rawLine.trim();
|
|
250
|
+
if (!line || line.startsWith("#")) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (inPhaseBlock) {
|
|
254
|
+
for (const char of line) {
|
|
255
|
+
if (char === "{") {
|
|
256
|
+
blockDepth += 1;
|
|
257
|
+
} else if (char === "}") {
|
|
258
|
+
blockDepth -= 1;
|
|
259
|
+
if (blockDepth === 0) {
|
|
260
|
+
inPhaseBlock = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (/^on\s+['"]/.test(line) && line.includes("sub")) {
|
|
267
|
+
inPhaseBlock = true;
|
|
268
|
+
blockDepth = line.includes("{") ? 1 : 0;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const requires = line.match(/^requires\s+['"]([^'"]+)['"]/u);
|
|
272
|
+
if (requires) {
|
|
273
|
+
names.add(requires[1]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return names;
|
|
277
|
+
}
|
|
@@ -4,7 +4,7 @@ import process from "node:process";
|
|
|
4
4
|
import { URL } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { build } from "@cdxgen/cdx-purl";
|
|
7
|
-
import toml from "
|
|
7
|
+
import * as toml from "smol-toml";
|
|
8
8
|
import { parse as _load } from "yaml";
|
|
9
9
|
|
|
10
10
|
import { DEBUG_MODE } from "../core/activity.js";
|