@cdxgen/cdxgen 13.2.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -12
- package/bin/cdxgen.js +227 -119
- package/bin/evinse.js +5 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/remediations.json +24 -0
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +43 -4
- package/lib/cli/jsBom.js +49 -24
- package/lib/cli/jvmBom.js +46 -12
- package/lib/cli/managedBom.js +112 -41
- package/lib/cli/nativeBom.js +125 -14
- package/lib/core/env.js +5 -6
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +64 -31
- package/lib/ecosystems/ecosystems.js +19 -8
- package/lib/ecosystems/gradleutils.js +42 -7
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +10 -3
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-dotnet.js +51 -10
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-misc.js +747 -161
- package/lib/ecosystems/parsers-python.js +26 -14
- package/lib/ecosystems/parsers-rust.js +24 -18
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +32 -1
- package/lib/evinser/evinser.js +436 -128
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +550 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/evidenceUtils.js +14 -6
- package/lib/inventory/fetchBatch.js +116 -17
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +37 -19
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/stages/postgen/postgen.js +68 -15
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +20 -20
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +59 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +64 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
package/lib/ecosystems/caxa.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { shouldFetchPackageMetadata } from "../core/env.js";
|
|
5
|
+
import {
|
|
6
|
+
getAllFiles,
|
|
7
|
+
getTmpDir,
|
|
8
|
+
safeExistsSync,
|
|
9
|
+
safeMkdtempSync,
|
|
10
|
+
safeRmSync,
|
|
11
|
+
} from "../core/fs.js";
|
|
4
12
|
import { sanitizeIngestedPurl } from "../inventory/purl.js";
|
|
5
|
-
import { getNpmMetadata } from "./ecosystems.js";
|
|
13
|
+
import { extractJarArchive, getNpmMetadata } from "./ecosystems.js";
|
|
14
|
+
import { parsePnpmLock } from "./parsers-js.js";
|
|
15
|
+
import { parseComposerJson, parseComposerLock } from "./parsers-misc.js";
|
|
16
|
+
import { parseGemfileLockData } from "./rubyutils.js";
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* Parse caxa self-extracting executable metadata.
|
|
@@ -83,3 +94,424 @@ export async function parseCaxaMetadata(mfile) {
|
|
|
83
94
|
}
|
|
84
95
|
return mdata;
|
|
85
96
|
}
|
|
97
|
+
|
|
98
|
+
// Manifests and SBOMs read from an app are data the binary ships, so each is
|
|
99
|
+
// size-capped before it is parsed.
|
|
100
|
+
const MAX_APP_DOCUMENT_BYTES = 64 * 1024 * 1024;
|
|
101
|
+
|
|
102
|
+
function readAppJson(file) {
|
|
103
|
+
try {
|
|
104
|
+
if (statSync(file).size > MAX_APP_DOCUMENT_BYTES) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
108
|
+
} catch (_e) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A path a manifest in the app names must stay inside the directory it is
|
|
114
|
+
// relative to.
|
|
115
|
+
function resolveInside(baseDir, relativePath) {
|
|
116
|
+
if (typeof relativePath !== "string" || !relativePath.length) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
if (isAbsolute(relativePath)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const resolved = join(baseDir, relativePath);
|
|
123
|
+
const back = relative(baseDir, resolved);
|
|
124
|
+
if (!back || back.startsWith("..") || isAbsolute(back)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toPosix(filePath) {
|
|
131
|
+
return filePath.split(sep).join("/");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parsers record the files they read by absolute path, which for an extracted
|
|
135
|
+
// app is a temporary directory. The path inside the app is the one that means
|
|
136
|
+
// something to a reader of the SBOM.
|
|
137
|
+
function relativizeToApp(component, appDir) {
|
|
138
|
+
const inApp = (value) =>
|
|
139
|
+
typeof value === "string" && value.startsWith(`${appDir}${sep}`)
|
|
140
|
+
? toPosix(relative(appDir, value))
|
|
141
|
+
: value;
|
|
142
|
+
for (const property of component.properties || []) {
|
|
143
|
+
if (property.name === "internal:SrcFile") {
|
|
144
|
+
property.value = inApp(property.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const identities = component.evidence?.identity;
|
|
148
|
+
for (const identity of Array.isArray(identities)
|
|
149
|
+
? identities
|
|
150
|
+
: [identities]) {
|
|
151
|
+
for (const method of identity?.methods || []) {
|
|
152
|
+
method.value = inApp(method.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const occurrence of component.evidence?.occurrences || []) {
|
|
156
|
+
occurrence.location = inApp(occurrence.location);
|
|
157
|
+
}
|
|
158
|
+
return component;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Components taken from the app's own SBOMs keep their bom-refs even when
|
|
162
|
+
// their purl has to be repaired, since the dependency graph refers to them by
|
|
163
|
+
// that ref. A component that has only a purl is referred to by it.
|
|
164
|
+
function ingestComponent(component) {
|
|
165
|
+
const ref = component["bom-ref"];
|
|
166
|
+
sanitizeIngestedPurl(component);
|
|
167
|
+
if (ref) {
|
|
168
|
+
component["bom-ref"] = ref;
|
|
169
|
+
} else if (!component["bom-ref"] && component.purl) {
|
|
170
|
+
component["bom-ref"] = decodeURIComponent(component.purl);
|
|
171
|
+
}
|
|
172
|
+
return component;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Index the npm packages of an extracted caxa app by directory. Each entry is
|
|
177
|
+
* the directory of a package.json relative to the app, mapped to the bom-ref
|
|
178
|
+
* of the metadata component with the same name and version.
|
|
179
|
+
*/
|
|
180
|
+
function indexAppPackages(appDir, components) {
|
|
181
|
+
const refs = new Set(components.map((c) => c["bom-ref"]));
|
|
182
|
+
const packageDirs = [];
|
|
183
|
+
const packageJsons = getAllFiles(appDir, "**/package.json", {
|
|
184
|
+
includeDot: true,
|
|
185
|
+
includeNodeModulesDir: true,
|
|
186
|
+
});
|
|
187
|
+
for (const file of packageJsons) {
|
|
188
|
+
const pkg = readAppJson(file);
|
|
189
|
+
if (!pkg?.name || !pkg?.version) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const ref = `pkg:npm/${pkg.name}@${pkg.version}`;
|
|
193
|
+
if (refs.has(ref)) {
|
|
194
|
+
packageDirs.push({ dir: toPosix(relative(appDir, dirname(file))), ref });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Innermost first, so a nested node_modules package owns its own files.
|
|
198
|
+
packageDirs.sort((a, b) => b.dir.length - a.dir.length);
|
|
199
|
+
return packageDirs;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ownerOf(packageDirs, relativeFile) {
|
|
203
|
+
return packageDirs.find(
|
|
204
|
+
({ dir }) => dir === "" || relativeFile.startsWith(`${dir}/`),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function addDependencies(dependencyMap, ref, dependsOn) {
|
|
209
|
+
if (!ref || !dependsOn?.length) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const existing = dependencyMap.get(ref) || new Set();
|
|
213
|
+
for (const target of dependsOn) {
|
|
214
|
+
if (target && target !== ref) {
|
|
215
|
+
existing.add(target);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
dependencyMap.set(ref, existing);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Refs that nothing in the list depends on: the entry points of a sub-graph.
|
|
222
|
+
function unrequiredRefs(components, dependencies) {
|
|
223
|
+
const required = new Set(dependencies.flatMap((d) => d.dependsOn || []));
|
|
224
|
+
return components
|
|
225
|
+
.map((c) => c["bom-ref"])
|
|
226
|
+
.filter((ref) => ref && !required.has(ref));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read the integrity of each installed npm package from the lockfile pnpm or
|
|
231
|
+
* npm leaves in the app's node_modules (or the app's own lockfile).
|
|
232
|
+
*
|
|
233
|
+
* @returns {Promise<Map<string, string>>} npm bom-ref to integrity string
|
|
234
|
+
*/
|
|
235
|
+
async function readAppIntegrity(appDir) {
|
|
236
|
+
const integrity = new Map();
|
|
237
|
+
for (const lockFile of [
|
|
238
|
+
"node_modules/.package-lock.json",
|
|
239
|
+
"package-lock.json",
|
|
240
|
+
]) {
|
|
241
|
+
const lock = readAppJson(join(appDir, lockFile));
|
|
242
|
+
for (const [installPath, entry] of Object.entries(lock?.packages || {})) {
|
|
243
|
+
const name =
|
|
244
|
+
entry?.name || installPath.split("node_modules/").pop() || undefined;
|
|
245
|
+
if (installPath && name && entry?.version && entry?.integrity) {
|
|
246
|
+
integrity.set(`pkg:npm/${name}@${entry.version}`, entry.integrity);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const lockFile of ["node_modules/.pnpm/lock.yaml", "pnpm-lock.yaml"]) {
|
|
251
|
+
const file = join(appDir, lockFile);
|
|
252
|
+
if (!safeExistsSync(file)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const { pkgList } = await parsePnpmLock(file);
|
|
257
|
+
for (const pkg of pkgList || []) {
|
|
258
|
+
if (pkg?._integrity && pkg.name && pkg.version) {
|
|
259
|
+
const name = pkg.group ? `${pkg.group}/${pkg.name}` : pkg.name;
|
|
260
|
+
integrity.set(`pkg:npm/${name}@${pkg.version}`, pkg._integrity);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (_e) {
|
|
264
|
+
// An unreadable lockfile leaves the components without hashes.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return integrity;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Add the native tools a package ships, as described by a cdxgen plugins
|
|
272
|
+
* manifest (`plugins-manifest.json`, from @cdxgen/cdxgen-plugins-bin*). Only
|
|
273
|
+
* entries whose binary is present are used, so a pruned bundle lists only what
|
|
274
|
+
* it kept. Their own dependencies come from the aggregate
|
|
275
|
+
* `sbom-postbuild.cdx.json` next to the manifest, or else from each entry's
|
|
276
|
+
* `sbomFile`, limited to what the present tools reach.
|
|
277
|
+
*/
|
|
278
|
+
function addPluginTools(appDir, packageDirs, state) {
|
|
279
|
+
const manifests = getAllFiles(appDir, "**/plugins-manifest.json", {
|
|
280
|
+
includeDot: true,
|
|
281
|
+
includeNodeModulesDir: true,
|
|
282
|
+
});
|
|
283
|
+
for (const manifestFile of manifests) {
|
|
284
|
+
const owner = ownerOf(packageDirs, toPosix(relative(appDir, manifestFile)));
|
|
285
|
+
const manifest = readAppJson(manifestFile);
|
|
286
|
+
if (!owner || !Array.isArray(manifest?.plugins)) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const packageDir = join(appDir, owner.dir);
|
|
290
|
+
const ownerComponent = state.byRef.get(owner.ref);
|
|
291
|
+
const lazyMembers = new Set(
|
|
292
|
+
(ownerComponent?.properties || [])
|
|
293
|
+
.filter((p) => p.name === "cdx:caxa:lazyMember")
|
|
294
|
+
.map((p) => p.value),
|
|
295
|
+
);
|
|
296
|
+
const toolRefs = [];
|
|
297
|
+
const sbomDocuments = [];
|
|
298
|
+
for (const plugin of manifest.plugins) {
|
|
299
|
+
const component = plugin?.component;
|
|
300
|
+
const binary = resolveInside(packageDir, plugin?.binaryPath);
|
|
301
|
+
if (
|
|
302
|
+
!component?.name ||
|
|
303
|
+
typeof component["bom-ref"] !== "string" ||
|
|
304
|
+
!binary ||
|
|
305
|
+
!safeExistsSync(binary)
|
|
306
|
+
) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const tool = ingestComponent(structuredClone(component));
|
|
310
|
+
tool.properties = tool.properties || [];
|
|
311
|
+
tool.properties.push({
|
|
312
|
+
name: "internal:SrcFile",
|
|
313
|
+
value: toPosix(relative(appDir, manifestFile)),
|
|
314
|
+
});
|
|
315
|
+
if (lazyMembers.has(plugin.binaryPath)) {
|
|
316
|
+
tool.properties.push({
|
|
317
|
+
name: "cdx:caxa:lazyMember",
|
|
318
|
+
value: plugin.binaryPath,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (state.addComponent(tool)) {
|
|
322
|
+
toolRefs.push(tool["bom-ref"]);
|
|
323
|
+
}
|
|
324
|
+
const sbomFile = resolveInside(packageDir, plugin.sbomFile);
|
|
325
|
+
if (sbomFile && safeExistsSync(sbomFile)) {
|
|
326
|
+
sbomDocuments.push({ file: sbomFile, rootRef: tool["bom-ref"] });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!toolRefs.length) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
addDependencies(state.dependencyMap, owner.ref, toolRefs);
|
|
333
|
+
const aggregate = join(dirname(manifestFile), "sbom-postbuild.cdx.json");
|
|
334
|
+
const documents = safeExistsSync(aggregate)
|
|
335
|
+
? [{ file: aggregate }]
|
|
336
|
+
: sbomDocuments;
|
|
337
|
+
addReachableSbomContents(documents, toolRefs, state);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Merge the components and dependencies of the tools' own SBOMs, keeping only
|
|
342
|
+
// what the given tools reach. A per-tool SBOM's root is renamed to the tool's
|
|
343
|
+
// ref, since the manifest and the tool's build may name it differently.
|
|
344
|
+
function addReachableSbomContents(documents, toolRefs, state) {
|
|
345
|
+
const candidates = new Map();
|
|
346
|
+
const edges = new Map();
|
|
347
|
+
for (const { file, rootRef } of documents) {
|
|
348
|
+
const bom = readAppJson(file);
|
|
349
|
+
if (!bom) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const sbomRoot = bom.metadata?.component?.["bom-ref"];
|
|
353
|
+
const rename = (ref) => (rootRef && ref === sbomRoot ? rootRef : ref);
|
|
354
|
+
for (const component of bom.components || []) {
|
|
355
|
+
if (component?.["bom-ref"] && !candidates.has(component["bom-ref"])) {
|
|
356
|
+
candidates.set(component["bom-ref"], component);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const dependency of bom.dependencies || []) {
|
|
360
|
+
addDependencies(
|
|
361
|
+
edges,
|
|
362
|
+
rename(dependency.ref),
|
|
363
|
+
(dependency.dependsOn || []).map(rename),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const reached = new Set(toolRefs);
|
|
368
|
+
const queue = [...toolRefs];
|
|
369
|
+
while (queue.length) {
|
|
370
|
+
for (const target of edges.get(queue.shift()) || []) {
|
|
371
|
+
if (!reached.has(target) && candidates.has(target)) {
|
|
372
|
+
reached.add(target);
|
|
373
|
+
queue.push(target);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const ref of reached) {
|
|
378
|
+
const component = candidates.get(ref);
|
|
379
|
+
if (component && !toolRefs.includes(ref)) {
|
|
380
|
+
state.addComponent(ingestComponent(structuredClone(component)));
|
|
381
|
+
}
|
|
382
|
+
addDependencies(
|
|
383
|
+
state.dependencyMap,
|
|
384
|
+
ref,
|
|
385
|
+
[...(edges.get(ref) || [])].filter((target) => reached.has(target)),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Add the PHP (composer.lock), Ruby (Gemfile.lock) and Java (*.jar) packages
|
|
392
|
+
* that npm packages of the app vendor, each linked from the npm package that
|
|
393
|
+
* contains it.
|
|
394
|
+
*/
|
|
395
|
+
async function addEmbeddedPackages(appDir, packageDirs, state) {
|
|
396
|
+
const options = { includeDot: true, includeNodeModulesDir: true };
|
|
397
|
+
const embedded = [];
|
|
398
|
+
for (const composerLock of getAllFiles(appDir, "**/composer.lock", options)) {
|
|
399
|
+
const composerJson = join(dirname(composerLock), "composer.json");
|
|
400
|
+
const { rootRequires } = safeExistsSync(composerJson)
|
|
401
|
+
? parseComposerJson(composerJson)
|
|
402
|
+
: { rootRequires: {} };
|
|
403
|
+
embedded.push({
|
|
404
|
+
file: composerLock,
|
|
405
|
+
result: parseComposerLock(composerLock, rootRequires),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (const gemLock of getAllFiles(appDir, "**/Gemfile.lock", options)) {
|
|
409
|
+
embedded.push({
|
|
410
|
+
file: gemLock,
|
|
411
|
+
result: await parseGemfileLockData(
|
|
412
|
+
readFileSync(gemLock, "utf-8"),
|
|
413
|
+
gemLock,
|
|
414
|
+
),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const jars = getAllFiles(appDir, "**/*.jar", options);
|
|
418
|
+
if (jars.length) {
|
|
419
|
+
const tempDir = safeMkdtempSync(join(getTmpDir(), "caxa-jars-"));
|
|
420
|
+
try {
|
|
421
|
+
for (const jar of jars) {
|
|
422
|
+
embedded.push({
|
|
423
|
+
file: jar,
|
|
424
|
+
result: { pkgList: (await extractJarArchive(jar, tempDir)) || [] },
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
} finally {
|
|
428
|
+
safeRmSync(tempDir, { recursive: true, force: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const { file, result } of embedded) {
|
|
432
|
+
const owner = ownerOf(packageDirs, toPosix(relative(appDir, file)));
|
|
433
|
+
const pkgList = result?.pkgList || [];
|
|
434
|
+
if (!owner || !pkgList.length) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
for (const pkg of pkgList) {
|
|
438
|
+
pkg.properties = pkg.properties || [];
|
|
439
|
+
if (!pkg.properties.some((p) => p.name === "internal:SrcFile")) {
|
|
440
|
+
pkg.properties.push({ name: "internal:SrcFile", value: file });
|
|
441
|
+
}
|
|
442
|
+
state.addComponent(relativizeToApp(ingestComponent(pkg), appDir));
|
|
443
|
+
}
|
|
444
|
+
const dependencies = result.dependenciesList || [];
|
|
445
|
+
for (const dependency of dependencies) {
|
|
446
|
+
addDependencies(
|
|
447
|
+
state.dependencyMap,
|
|
448
|
+
dependency.ref,
|
|
449
|
+
dependency.dependsOn,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
// The lockfile's direct dependencies, or failing that every package in
|
|
453
|
+
// it that nothing else requires, hang off the npm package.
|
|
454
|
+
const direct = (result.rootList || [])
|
|
455
|
+
.map((pkg) => pkg["bom-ref"])
|
|
456
|
+
.filter(Boolean);
|
|
457
|
+
addDependencies(
|
|
458
|
+
state.dependencyMap,
|
|
459
|
+
owner.ref,
|
|
460
|
+
direct.length ? direct : unrequiredRefs(pkgList, dependencies),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Complete caxa metadata from the application the binary extracts (or was
|
|
467
|
+
* built from): npm integrity from the lockfile pnpm or npm leaves in
|
|
468
|
+
* node_modules, the native tools a cdxgen plugins manifest describes, with
|
|
469
|
+
* their own dependencies, and the PHP, Ruby and Java packages vendored inside
|
|
470
|
+
* npm packages. Everything found is linked from the npm package whose
|
|
471
|
+
* directory contains it; npm packages that are not in the metadata are
|
|
472
|
+
* ignored.
|
|
473
|
+
*
|
|
474
|
+
* @param {Object} mdata Parsed caxa metadata (see parseCaxaMetadata)
|
|
475
|
+
* @param {string} appDir Directory of the extracted app
|
|
476
|
+
* @returns {Promise<Object>} The metadata, with components and dependencies added
|
|
477
|
+
*/
|
|
478
|
+
export async function enrichCaxaMetadataFromApp(mdata, appDir) {
|
|
479
|
+
if (!mdata?.components?.length || !appDir || !safeExistsSync(appDir)) {
|
|
480
|
+
return mdata;
|
|
481
|
+
}
|
|
482
|
+
const byRef = new Map(mdata.components.map((c) => [c["bom-ref"], c]));
|
|
483
|
+
const dependencyMap = new Map();
|
|
484
|
+
for (const dependency of mdata.dependencies || []) {
|
|
485
|
+
addDependencies(dependencyMap, dependency.ref, dependency.dependsOn);
|
|
486
|
+
}
|
|
487
|
+
const state = {
|
|
488
|
+
byRef,
|
|
489
|
+
dependencyMap,
|
|
490
|
+
addComponent(component) {
|
|
491
|
+
const ref = component?.["bom-ref"];
|
|
492
|
+
if (!ref || byRef.has(ref)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
// It ships in the binary, unless its own SBOM said otherwise.
|
|
496
|
+
component.scope = component.scope || "required";
|
|
497
|
+
byRef.set(ref, component);
|
|
498
|
+
mdata.components.push(component);
|
|
499
|
+
return true;
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
const integrity = await readAppIntegrity(appDir);
|
|
503
|
+
for (const component of mdata.components) {
|
|
504
|
+
const value = integrity.get(component["bom-ref"]);
|
|
505
|
+
if (value && !component.hashes?.length && !component._integrity) {
|
|
506
|
+
component._integrity = value;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const packageDirs = indexAppPackages(appDir, mdata.components);
|
|
510
|
+
addPluginTools(appDir, packageDirs, state);
|
|
511
|
+
await addEmbeddedPackages(appDir, packageDirs, state);
|
|
512
|
+
mdata.dependencies = [...dependencyMap].map(([ref, dependsOn]) => ({
|
|
513
|
+
ref,
|
|
514
|
+
dependsOn: [...dependsOn].sort(),
|
|
515
|
+
}));
|
|
516
|
+
return mdata;
|
|
517
|
+
}
|
|
@@ -584,4 +584,70 @@ export function collapseCmakeVersions(pkgList) {
|
|
|
584
584
|
return collapsed;
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
/** `cdx:cmake:depKind` values for dependencies downloaded at configure time. */
|
|
588
|
+
const FETCHED_KINDS = new Set(["fetch", "cpm", "external-project"]);
|
|
589
|
+
|
|
590
|
+
function cmakeProperty(pkg, name) {
|
|
591
|
+
return pkg?.properties?.find((prop) => prop.name === name)?.value;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Keep one component per dependency downloaded at configure time.
|
|
596
|
+
*
|
|
597
|
+
* The same `FetchContent_Declare` may be read from several files, and a
|
|
598
|
+
* `find_package(<name>)` names the package a `FetchContent_Declare(<name>)` or
|
|
599
|
+
* `CPMAddPackage` fetches. Within the components scraped from CMake files, the
|
|
600
|
+
* first fetched component of each name stands for all of them, and a plain
|
|
601
|
+
* `find_package` entry of that name is dropped.
|
|
602
|
+
*
|
|
603
|
+
* @param {Object[]} pkgList Components scraped from CMake-like files
|
|
604
|
+
* @returns {Object[]} The components, in first-seen order
|
|
605
|
+
*/
|
|
606
|
+
export function preferFetchedCmakeDependencies(pkgList) {
|
|
607
|
+
if (!Array.isArray(pkgList)) {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
const fetched = new Map();
|
|
611
|
+
for (const pkg of pkgList) {
|
|
612
|
+
const key = pkg?.name?.toLowerCase();
|
|
613
|
+
if (
|
|
614
|
+
key &&
|
|
615
|
+
FETCHED_KINDS.has(cmakeProperty(pkg, "cdx:cmake:depKind")) &&
|
|
616
|
+
!fetched.has(key)
|
|
617
|
+
) {
|
|
618
|
+
fetched.set(key, pkg);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return pkgList.filter((pkg) => {
|
|
622
|
+
const key = pkg?.name?.toLowerCase();
|
|
623
|
+
if (!key || !fetched.has(key)) {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
const kind = cmakeProperty(pkg, "cdx:cmake:depKind");
|
|
627
|
+
return FETCHED_KINDS.has(kind) ? fetched.get(key) === pkg : !!kind;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Drop the components scraped from CMake files for a dependency the build tree
|
|
633
|
+
* has resolved: the gitclone script names the repository and tag that were
|
|
634
|
+
* really checked out, so its component replaces the declared one.
|
|
635
|
+
*
|
|
636
|
+
* @param {Object[]} pkgList Components collected so far
|
|
637
|
+
* @param {string} name Name of the resolved dependency
|
|
638
|
+
* @returns {Object[]} The components without the scraped entries for `name`
|
|
639
|
+
*/
|
|
640
|
+
export function withoutScrapedFetchDependency(pkgList, name) {
|
|
641
|
+
const key = name?.toLowerCase();
|
|
642
|
+
return pkgList.filter(
|
|
643
|
+
(pkg) =>
|
|
644
|
+
!(
|
|
645
|
+
key &&
|
|
646
|
+
pkg?.name?.toLowerCase() === key &&
|
|
647
|
+
cmakeProperty(pkg, "cdx:cmake:resolvedVia") === "cmake-lists" &&
|
|
648
|
+
FETCHED_KINDS.has(cmakeProperty(pkg, "cdx:cmake:depKind"))
|
|
649
|
+
),
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
587
653
|
export { GIT_COMMAND, toRepoRelative };
|
|
@@ -30,6 +30,30 @@ import {
|
|
|
30
30
|
parseXmakeRequiresLock,
|
|
31
31
|
} from "./parsers-misc.js";
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Add the symbols a file uses to a component's `internal:ImportedSymbols`,
|
|
35
|
+
* keeping every other property and the symbols already recorded.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} apkg Component to enrich
|
|
38
|
+
* @param {string[]} symbols Symbol names
|
|
39
|
+
*/
|
|
40
|
+
function mergeImportedSymbols(apkg, symbols) {
|
|
41
|
+
apkg.properties = apkg.properties || [];
|
|
42
|
+
const prop = apkg.properties.find(
|
|
43
|
+
(p) => p.name === "internal:ImportedSymbols",
|
|
44
|
+
);
|
|
45
|
+
if (prop) {
|
|
46
|
+
prop.value = Array.from(new Set([...prop.value.split("|"), ...symbols]))
|
|
47
|
+
.sort()
|
|
48
|
+
.join("|");
|
|
49
|
+
} else {
|
|
50
|
+
apkg.properties.push({
|
|
51
|
+
name: "internal:ImportedSymbols",
|
|
52
|
+
value: symbols.join("|"),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/**
|
|
34
58
|
* Method to find c/c++ modules by collecting usages with atom
|
|
35
59
|
*
|
|
@@ -48,8 +72,11 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
|
|
|
48
72
|
let parentComponent;
|
|
49
73
|
const dependsOn = new Set();
|
|
50
74
|
|
|
75
|
+
// Components from CMake and Conan carry no group: the key spells a missing
|
|
76
|
+
// group the same way on insert and on lookup.
|
|
77
|
+
const epkgKey = (group, name) => `${group || ""}/${name}`;
|
|
51
78
|
(epkgList || []).forEach((p) => {
|
|
52
|
-
epkgMap[
|
|
79
|
+
epkgMap[epkgKey(p.group, p.name)] = p;
|
|
53
80
|
});
|
|
54
81
|
// Let's look for any vcpkg.json file to tell us about the directory we're scanning
|
|
55
82
|
// users can use this file to give us a clue even if they do not use vcpkg library manager
|
|
@@ -106,9 +133,9 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
|
|
|
106
133
|
// Is this a dependency we haven't seen before including the all lower and upper case version?
|
|
107
134
|
if (
|
|
108
135
|
avcpkgName &&
|
|
109
|
-
!epkgMap[
|
|
110
|
-
!epkgMap[
|
|
111
|
-
!epkgMap[
|
|
136
|
+
!epkgMap[epkgKey("", avcpkgName)] &&
|
|
137
|
+
!epkgMap[epkgKey("", avcpkgName.toLowerCase())] &&
|
|
138
|
+
!epkgMap[epkgKey("", avcpkgName.toUpperCase())]
|
|
112
139
|
) {
|
|
113
140
|
const pkgPurl = build({
|
|
114
141
|
type: pkgType,
|
|
@@ -205,6 +232,8 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
|
|
|
205
232
|
);
|
|
206
233
|
}
|
|
207
234
|
const usageData = parseCUsageSlice(sliceData);
|
|
235
|
+
const osComponents = new Map();
|
|
236
|
+
const addedRefs = new Set();
|
|
208
237
|
for (let afile of Object.keys(usageData)) {
|
|
209
238
|
// Normalize windows separator
|
|
210
239
|
afile = afile.replace("..\\", "").replace(/\\/g, "/");
|
|
@@ -235,13 +264,31 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
|
|
|
235
264
|
if (CPP_STD_MODULES.includes(name)) {
|
|
236
265
|
continue;
|
|
237
266
|
}
|
|
238
|
-
|
|
239
|
-
|
|
267
|
+
// The OS package list is shared by every header: a component taken from it
|
|
268
|
+
// is a copy, so the internal:PkgProvides the next header is matched against
|
|
269
|
+
// stays intact. A component the caller already listed is enriched in place
|
|
270
|
+
// and not listed again.
|
|
271
|
+
const osPkg = getOSPackageForFile(afile, osPkgsList);
|
|
272
|
+
const listedPkg = osPkg ? undefined : epkgMap[epkgKey(group, name)];
|
|
273
|
+
let apkg;
|
|
274
|
+
if (osPkg) {
|
|
275
|
+
const osRef = osPkg["bom-ref"] || osPkg.purl || osPkg.name;
|
|
276
|
+
apkg = osComponents.get(osRef);
|
|
277
|
+
if (!apkg) {
|
|
278
|
+
apkg = {
|
|
279
|
+
...osPkg,
|
|
280
|
+
properties: (osPkg.properties || []).map((prop) => ({ ...prop })),
|
|
281
|
+
};
|
|
282
|
+
osComponents.set(osRef, apkg);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
apkg = listedPkg || {
|
|
240
286
|
name,
|
|
241
287
|
group,
|
|
242
288
|
version: "",
|
|
243
289
|
type: pkgType,
|
|
244
290
|
};
|
|
291
|
+
}
|
|
245
292
|
// If this is a relative file, there is a good chance we can reuse the project group
|
|
246
293
|
if (!afile.startsWith(_sep) && !group.length) {
|
|
247
294
|
group = options.projectGroup || "";
|
|
@@ -291,36 +338,22 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
|
|
|
291
338
|
)
|
|
292
339
|
.map((v) => v.split(":")[0])
|
|
293
340
|
.sort();
|
|
294
|
-
if (
|
|
295
|
-
apkg
|
|
296
|
-
{ name: "internal:ImportedSymbols", value: usymbols.join("|") },
|
|
297
|
-
];
|
|
298
|
-
} else {
|
|
299
|
-
apkg["properties"] = [];
|
|
300
|
-
}
|
|
301
|
-
const newProps = [];
|
|
302
|
-
let symbolsPropertyFound = false;
|
|
303
|
-
for (const prop of apkg["properties"]) {
|
|
304
|
-
if (prop.name === "internal:ImportedSymbols") {
|
|
305
|
-
symbolsPropertyFound = true;
|
|
306
|
-
let existingSymbols = prop.value.split("|");
|
|
307
|
-
existingSymbols = existingSymbols.concat(usymbols);
|
|
308
|
-
prop.value = Array.from(new Set(existingSymbols)).sort().join("|");
|
|
309
|
-
}
|
|
310
|
-
newProps.push(prop);
|
|
311
|
-
}
|
|
312
|
-
if (!symbolsPropertyFound && usymbols.length) {
|
|
313
|
-
apkg["properties"].push({
|
|
314
|
-
name: "internal:ImportedSymbols",
|
|
315
|
-
value: usymbols.join("|"),
|
|
316
|
-
});
|
|
341
|
+
if (usymbols.length) {
|
|
342
|
+
mergeImportedSymbols(apkg, usymbols);
|
|
317
343
|
}
|
|
318
|
-
apkg["properties"] = newProps;
|
|
319
344
|
}
|
|
320
345
|
// At this point, we have a package but we don't know what it's called
|
|
321
346
|
// So let's try to locate this generic package using some heuristics
|
|
322
347
|
apkg = locateGenericPackage(apkg);
|
|
323
|
-
if (
|
|
348
|
+
if (listedPkg) {
|
|
349
|
+
dependsOn.add(apkg["bom-ref"]);
|
|
350
|
+
} else if (osPkg) {
|
|
351
|
+
if (!addedRefs.has(apkg["bom-ref"])) {
|
|
352
|
+
pkgList.push(apkg);
|
|
353
|
+
dependsOn.add(apkg["bom-ref"]);
|
|
354
|
+
addedRefs.add(apkg["bom-ref"]);
|
|
355
|
+
}
|
|
356
|
+
} else if (!pkgAddedMap[name]) {
|
|
324
357
|
pkgList.push(apkg);
|
|
325
358
|
dependsOn.add(apkg["bom-ref"]);
|
|
326
359
|
pkgAddedMap[name] = true;
|