@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
|
@@ -70,7 +70,6 @@ import {
|
|
|
70
70
|
} from "../inventory/fetchBatch.js";
|
|
71
71
|
import {
|
|
72
72
|
applyPurl,
|
|
73
|
-
encodeForPurl,
|
|
74
73
|
genericPurl,
|
|
75
74
|
nugetPurl,
|
|
76
75
|
pypiPurl,
|
|
@@ -2500,8 +2499,8 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2500
2499
|
}
|
|
2501
2500
|
}
|
|
2502
2501
|
const apkg = {
|
|
2503
|
-
group: group
|
|
2504
|
-
name: name
|
|
2502
|
+
group: group || "",
|
|
2503
|
+
name: name || "",
|
|
2505
2504
|
version,
|
|
2506
2505
|
purl,
|
|
2507
2506
|
evidence: {
|
|
@@ -2631,16 +2630,28 @@ const METADATA_VERSION_PROP = "cdx:nuget:metadata_version";
|
|
|
2631
2630
|
|
|
2632
2631
|
async function getNugetUrl() {
|
|
2633
2632
|
const req = "https://api.nuget.org/v3/index.json";
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2633
|
+
const fallbackUrl = "https://api.nuget.org/v3/registration3/";
|
|
2634
|
+
let res;
|
|
2635
|
+
try {
|
|
2636
|
+
res = await cdxgenAgent.get(req, {
|
|
2637
|
+
responseType: "json",
|
|
2638
|
+
});
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
// Package lookups already degrade to unenriched components on error. An
|
|
2641
|
+
// unreachable service index used to reject here instead and abort the
|
|
2642
|
+
// whole BOM, so fall back to the well-known registration base.
|
|
2643
|
+
thoughtLog(
|
|
2644
|
+
`Unable to read the NuGet service index (${err.code || err.message}). Using ${fallbackUrl}.`,
|
|
2645
|
+
);
|
|
2646
|
+
return fallbackUrl;
|
|
2647
|
+
}
|
|
2648
|
+
const urls = res?.body?.resources || [];
|
|
2638
2649
|
for (const resource of urls) {
|
|
2639
2650
|
if (resource["@type"] === "RegistrationsBaseUrl/3.6.0") {
|
|
2640
2651
|
return resource["@id"];
|
|
2641
2652
|
}
|
|
2642
2653
|
}
|
|
2643
|
-
return
|
|
2654
|
+
return fallbackUrl;
|
|
2644
2655
|
}
|
|
2645
2656
|
|
|
2646
2657
|
/**
|
|
@@ -41,19 +41,33 @@ function isJvmToolPinned() {
|
|
|
41
41
|
/**
|
|
42
42
|
* Method to return the gradle command to use.
|
|
43
43
|
*
|
|
44
|
+
* Project-local wrapper scripts (`gradlew`) are attacker-controlled files in
|
|
45
|
+
* the scanned project, so they are only used when dependency tooling is
|
|
46
|
+
* allowed (`options.installDeps !== false`); otherwise the system Gradle is
|
|
47
|
+
* used.
|
|
48
|
+
*
|
|
44
49
|
* @param {string} srcPath Path to look for gradlew wrapper
|
|
45
50
|
* @param {string|null} rootPath Root directory to look for gradlew wrapper
|
|
51
|
+
* @param {Object} [options] CLI options (`installDeps` gates wrapper use)
|
|
46
52
|
*/
|
|
47
|
-
export function getGradleCommand(srcPath, rootPath) {
|
|
53
|
+
export function getGradleCommand(srcPath, rootPath, options = {}) {
|
|
48
54
|
let gradleCmd = "gradle";
|
|
49
55
|
const pinnedToolSelected = isJvmToolPinned();
|
|
56
|
+
// Introspection mode exists to observe the project's own toolchain, so it
|
|
57
|
+
// keeps using wrappers even when dependency installation is disabled.
|
|
58
|
+
const allowProjectWrappers =
|
|
59
|
+
options.installDeps !== false || options.introspect === true;
|
|
50
60
|
|
|
51
61
|
let findGradleFile = "gradlew";
|
|
52
62
|
if (platform() === "win32") {
|
|
53
63
|
findGradleFile = "gradlew.bat";
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
allowProjectWrappers &&
|
|
68
|
+
!pinnedToolSelected &&
|
|
69
|
+
safeExistsSync(join(srcPath, findGradleFile))
|
|
70
|
+
) {
|
|
57
71
|
// Use local gradle wrapper if available
|
|
58
72
|
// Enable execute permission
|
|
59
73
|
try {
|
|
@@ -72,6 +86,7 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
72
86
|
reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
|
|
73
87
|
});
|
|
74
88
|
} else if (
|
|
89
|
+
allowProjectWrappers &&
|
|
75
90
|
!pinnedToolSelected &&
|
|
76
91
|
rootPath &&
|
|
77
92
|
safeExistsSync(join(rootPath, findGradleFile))
|
|
@@ -380,8 +395,9 @@ export function executeParallelGradleProperties(
|
|
|
380
395
|
dir,
|
|
381
396
|
allProjectsStr,
|
|
382
397
|
extraArgs = [],
|
|
398
|
+
options = {},
|
|
383
399
|
) {
|
|
384
|
-
const gradleCmd = getGradleCommand(dir, null);
|
|
400
|
+
const gradleCmd = getGradleCommand(dir, null, options);
|
|
385
401
|
const gradleArgs = buildGradleCommandArguments(
|
|
386
402
|
extraArgs.concat(
|
|
387
403
|
readEnvironmentVariable("GRADLE_ARGS")
|
|
@@ -1205,11 +1221,16 @@ export async function collectGradleDependencies(
|
|
|
1205
1221
|
/**
|
|
1206
1222
|
* Method to return the mill command to use.
|
|
1207
1223
|
*
|
|
1224
|
+
* The project-local `mill` script is an attacker-controlled file in the
|
|
1225
|
+
* scanned project, so it is only used when dependency tooling is allowed
|
|
1226
|
+
* (`options.installDeps !== false`).
|
|
1227
|
+
*
|
|
1208
1228
|
* @param {string} srcPath Path to look for mill wrapper
|
|
1229
|
+
* @param {Object} [options] CLI options (`installDeps` gates wrapper use)
|
|
1209
1230
|
*/
|
|
1210
|
-
export function getMillCommand(srcPath) {
|
|
1231
|
+
export function getMillCommand(srcPath, options = {}) {
|
|
1211
1232
|
let millCmd = `mill${platform() === "win32" ? ".bat" : ""}`;
|
|
1212
|
-
if (safeExistsSync(join(srcPath, millCmd))) {
|
|
1233
|
+
if (options.installDeps !== false && safeExistsSync(join(srcPath, millCmd))) {
|
|
1213
1234
|
// Use local mill wrapper if available
|
|
1214
1235
|
// Enable execute permission
|
|
1215
1236
|
try {
|
|
@@ -1226,15 +1247,24 @@ export function getMillCommand(srcPath) {
|
|
|
1226
1247
|
/**
|
|
1227
1248
|
* Method to return the maven command to use.
|
|
1228
1249
|
*
|
|
1250
|
+
* Project-local wrapper scripts (`mvnw`) are attacker-controlled files in the
|
|
1251
|
+
* scanned project, so they are only used when dependency tooling is allowed
|
|
1252
|
+
* (`options.installDeps !== false`); otherwise the installed Maven is used.
|
|
1253
|
+
*
|
|
1229
1254
|
* @param {string} srcPath Path to look for maven wrapper
|
|
1230
1255
|
* @param {string} rootPath Root directory to look for maven wrapper
|
|
1256
|
+
* @param {Object} [options] CLI options (`installDeps` gates wrapper use)
|
|
1231
1257
|
*/
|
|
1232
|
-
export function getMavenCommand(srcPath, rootPath) {
|
|
1258
|
+
export function getMavenCommand(srcPath, rootPath, options = {}) {
|
|
1233
1259
|
let mavenCmd = "mvn";
|
|
1234
1260
|
// Check if the wrapper script is both available and functional
|
|
1235
1261
|
let isWrapperReady = false;
|
|
1236
1262
|
let isWrapperFound = false;
|
|
1237
1263
|
const pinnedToolSelected = isJvmToolPinned();
|
|
1264
|
+
// Introspection mode exists to observe the project's own toolchain, so it
|
|
1265
|
+
// keeps using wrappers even when dependency installation is disabled.
|
|
1266
|
+
const allowProjectWrappers =
|
|
1267
|
+
options.installDeps !== false || options.introspect === true;
|
|
1238
1268
|
let findMavenFile = "mvnw";
|
|
1239
1269
|
let mavenWrapperCmd = null;
|
|
1240
1270
|
if (platform() === "win32") {
|
|
@@ -1247,7 +1277,11 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
1247
1277
|
}
|
|
1248
1278
|
}
|
|
1249
1279
|
|
|
1250
|
-
if (
|
|
1280
|
+
if (
|
|
1281
|
+
allowProjectWrappers &&
|
|
1282
|
+
!pinnedToolSelected &&
|
|
1283
|
+
safeExistsSync(join(srcPath, findMavenFile))
|
|
1284
|
+
) {
|
|
1251
1285
|
// Use local maven wrapper if available
|
|
1252
1286
|
// Enable execute permission
|
|
1253
1287
|
try {
|
|
@@ -1267,6 +1301,7 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
1267
1301
|
reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
1268
1302
|
});
|
|
1269
1303
|
} else if (
|
|
1304
|
+
allowProjectWrappers &&
|
|
1270
1305
|
!pinnedToolSelected &&
|
|
1271
1306
|
rootPath &&
|
|
1272
1307
|
safeExistsSync(join(rootPath, findMavenFile))
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
11
|
-
import { DEBUG_MODE } from "../core/activity.js";
|
|
12
11
|
import { safeExistsSync } from "../core/fs.js";
|
|
13
12
|
import { createOccurrenceEvidence } from "../inventory/evidenceUtils.js";
|
|
13
|
+
import { isPkgTypeOnlyImport, setNpmTypeOnlyProperty } from "./npmutils.js";
|
|
14
14
|
import { parsePkgJson } from "./parsers-js.js";
|
|
15
15
|
|
|
16
16
|
const NPM_BIN_IMPORT_PREFIX = "cdx:npm:bin/";
|
|
@@ -109,8 +109,11 @@ function applyNpmBinCommandEvidence(pkg, allImports) {
|
|
|
109
109
|
*
|
|
110
110
|
* For each component, resolves its module aliases (including the deno jsr
|
|
111
111
|
* specifier), records imported/exported modules as properties, attaches
|
|
112
|
-
* occurrence evidence, and
|
|
113
|
-
* package is used
|
|
112
|
+
* occurrence evidence, and promotes the component scope to "required" when the
|
|
113
|
+
* package is used. A package with no observed usage keeps the scope its
|
|
114
|
+
* manifest parser assigned: static import analysis cannot see packages loaded
|
|
115
|
+
* from configuration, bundler entrypoints, stylesheets, or plugins, so a
|
|
116
|
+
* missing import is not evidence that a package is optional.
|
|
114
117
|
*
|
|
115
118
|
* @param {Array<object>} pkgList Package components to enrich.
|
|
116
119
|
* @param {object} allImports Map of import specifier to usage evidence objects.
|
|
@@ -128,10 +131,6 @@ export async function addEvidenceForImports(
|
|
|
128
131
|
const impPkgs = Object.keys(allImports);
|
|
129
132
|
const exportedPkgs = Object.keys(allExports);
|
|
130
133
|
for (const pkg of pkgList) {
|
|
131
|
-
if (impPkgs?.length) {
|
|
132
|
-
// Assume that all packages are optional until we see an evidence
|
|
133
|
-
pkg.scope = "optional";
|
|
134
|
-
}
|
|
135
134
|
const { group, name } = pkg;
|
|
136
135
|
// Evidence belonging to a type must be associated with the package
|
|
137
136
|
if (group === "@types") {
|
|
@@ -155,7 +154,6 @@ export async function addEvidenceForImports(
|
|
|
155
154
|
aliases.push(jsrSpecifier);
|
|
156
155
|
}
|
|
157
156
|
}
|
|
158
|
-
let isImported = false;
|
|
159
157
|
for (const alias of aliases) {
|
|
160
158
|
const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
|
|
161
159
|
const all_includes = impPkgs.filter(
|
|
@@ -213,11 +211,22 @@ export async function addEvidenceForImports(
|
|
|
213
211
|
}
|
|
214
212
|
// Identify all the imported modules of a component
|
|
215
213
|
if (impPkgs.includes(alias) || all_includes.length) {
|
|
216
|
-
isImported = true;
|
|
217
214
|
let importedModules = new Set();
|
|
218
215
|
let wasmExportedModules = new Set();
|
|
219
216
|
const seenOccurrenceLocations = new Set();
|
|
220
|
-
|
|
217
|
+
// A package imported only for its types is erased at compile time.
|
|
218
|
+
// Scoping it excluded here, rather than when the BOM is assembled,
|
|
219
|
+
// keeps propagateRequiredScopeFromDependencies from promoting what it
|
|
220
|
+
// depends on. A package whose bin command is used is still required.
|
|
221
|
+
if (
|
|
222
|
+
isPkgTypeOnlyImport(allImports, group, name) &&
|
|
223
|
+
!getNpmBinCommandEvidenceKeys(allImports, pkg).length
|
|
224
|
+
) {
|
|
225
|
+
pkg.scope = "excluded";
|
|
226
|
+
setNpmTypeOnlyProperty(pkg);
|
|
227
|
+
} else {
|
|
228
|
+
pkg.scope = "required";
|
|
229
|
+
}
|
|
221
230
|
for (const subevidence of all_includes) {
|
|
222
231
|
const evidences = allImports[subevidence];
|
|
223
232
|
for (const evidence of evidences) {
|
|
@@ -273,16 +282,6 @@ export async function addEvidenceForImports(
|
|
|
273
282
|
}
|
|
274
283
|
break;
|
|
275
284
|
}
|
|
276
|
-
if (
|
|
277
|
-
impPkgs?.length > 0 &&
|
|
278
|
-
!isImported &&
|
|
279
|
-
DEBUG_MODE &&
|
|
280
|
-
pkg?.scope !== "optional"
|
|
281
|
-
) {
|
|
282
|
-
console.debug(
|
|
283
|
-
`\x1b[1;35mNotice: Package ${pkg.name} has no usage in code. Check if it is needed.\x1b[0m`,
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
285
|
// Capture metadata such as description from local node_modules in deep mode
|
|
287
286
|
if (deep && !pkg.description && pkg.properties) {
|
|
288
287
|
let localNodeModulesPath;
|
package/lib/ecosystems/kosi.js
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
safeSpawnSync,
|
|
12
12
|
} from "../core/fs.js";
|
|
13
13
|
import { resolvePluginBinary } from "../inventory/plugins.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
parsePurlLoose,
|
|
16
|
+
tryBuildPurl,
|
|
17
|
+
tryParsePurl,
|
|
18
|
+
} from "../inventory/purl.js";
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* kosi — JS bridge to the Kotlin-native evidence binary (thirdparty/kosi in
|
|
@@ -46,7 +50,10 @@ export function kosiWorkspaceComponent(workspaceModule) {
|
|
|
46
50
|
if (!workspaceModule?.purl) {
|
|
47
51
|
return undefined;
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
// The version is read through the purl parser: cutting the text after `@`
|
|
54
|
+
// kept its escaping (`1.0.0%2Bbuild`) and any `?type=jar` qualifiers.
|
|
55
|
+
const version =
|
|
56
|
+
parsePurlLoose(workspaceModule.purl)?.version || "unspecified";
|
|
50
57
|
const cleanName = (workspaceModule.name || "").replace(/^['"]|['"]+$/g, "");
|
|
51
58
|
const purl =
|
|
52
59
|
tryParsePurl(workspaceModule.purl) ||
|
|
@@ -59,7 +66,7 @@ export function kosiWorkspaceComponent(workspaceModule) {
|
|
|
59
66
|
name: cleanName || workspaceModule.name,
|
|
60
67
|
version,
|
|
61
68
|
purl,
|
|
62
|
-
"bom-ref": purl,
|
|
69
|
+
"bom-ref": decodeURIComponent(purl),
|
|
63
70
|
};
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -2,8 +2,10 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { build, Purl } from "@cdxgen/cdx-purl";
|
|
5
|
+
import { clean } from "semver";
|
|
5
6
|
|
|
6
7
|
import { safeExistsSync } from "../core/fs.js";
|
|
8
|
+
import { parsePurlLoose } from "../inventory/purl.js";
|
|
7
9
|
import { parseNpmrc, parseNpmrcFromEnv } from "../parsers/npmrc.js";
|
|
8
10
|
|
|
9
11
|
const npmPackageHydrationFields = [
|
|
@@ -84,6 +86,89 @@ export function setNpmPeerProperty(pkg) {
|
|
|
84
86
|
addComponentProperty(pkg, "cdx:npm:package:peer", "true");
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Marks an npm component as imported only for its TypeScript types.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} pkg Component object to annotate
|
|
93
|
+
* @returns {void}
|
|
94
|
+
*/
|
|
95
|
+
export function setNpmTypeOnlyProperty(pkg) {
|
|
96
|
+
addComponentProperty(pkg, "cdx:npm:package:type-only", "true");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clean an npm version the way `npm publish` does before it records a release.
|
|
101
|
+
*
|
|
102
|
+
* Publishing a directory runs the manifest through `npm pkg fix`
|
|
103
|
+
* (`fixVersionField` in `@npmcli/package-json`, which calls `semver.clean` in
|
|
104
|
+
* loose mode) and then through libnpmpublish, which calls `semver.clean` again
|
|
105
|
+
* in strict mode and records the result as the packument version, the
|
|
106
|
+
* dist-tag, the `_id` and the tarball file name. Semver build metadata
|
|
107
|
+
* (`1.0.0+build.5`), a leading `v` or `=`, surrounding whitespace and leading
|
|
108
|
+
* zeros are dropped; a prerelease is kept.
|
|
109
|
+
*
|
|
110
|
+
* The `package.json` inside the published tarball keeps the original string,
|
|
111
|
+
* and so do the lockfiles npm, pnpm, yarn and bun write for a project, a
|
|
112
|
+
* workspace member or a git, file or link dependency. The cleaned form is the
|
|
113
|
+
* version the npm registry knows the package by.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} version Version as a manifest or lockfile records it
|
|
116
|
+
* @returns {string|null} The cleaned version, or null when npm would reject `version` as invalid semver
|
|
117
|
+
*/
|
|
118
|
+
export function cleanNpmVersion(version) {
|
|
119
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const looseClean = clean(version, { loose: true });
|
|
123
|
+
if (!looseClean) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return clean(looseClean);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Record the version npm would publish when it differs from the recorded one.
|
|
131
|
+
*
|
|
132
|
+
* Sets `cdx:npm:cleanedVersion` on an npm component whose version is valid
|
|
133
|
+
* semver but not in the form {@link cleanNpmVersion} produces, for example
|
|
134
|
+
* `1.0.0+build.5` (published as `1.0.0`). The component's `version` and `purl`
|
|
135
|
+
* keep the recorded string: that is what the manifest and lockfile say. A
|
|
136
|
+
* component whose version is already clean, or not semver at all (a git
|
|
137
|
+
* commit, a `file:` path), is left without the property, and a stale value from
|
|
138
|
+
* an earlier pass is replaced.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} component Component or package object with `version` and `purl`
|
|
141
|
+
* @returns {object} The same component, for chaining
|
|
142
|
+
*/
|
|
143
|
+
export function setNpmCleanedVersionProperty(component) {
|
|
144
|
+
if (!component || typeof component !== "object") {
|
|
145
|
+
return component;
|
|
146
|
+
}
|
|
147
|
+
const purl = component.purl;
|
|
148
|
+
if (typeof purl !== "string" || !purl.startsWith("pkg:npm/")) {
|
|
149
|
+
return component;
|
|
150
|
+
}
|
|
151
|
+
const cleaned = cleanNpmVersion(component.version);
|
|
152
|
+
const wanted = cleaned && cleaned !== component.version ? cleaned : undefined;
|
|
153
|
+
const existing = component.properties?.filter(
|
|
154
|
+
(property) => property?.name === "cdx:npm:cleanedVersion",
|
|
155
|
+
);
|
|
156
|
+
if (
|
|
157
|
+
(!existing?.length && !wanted) ||
|
|
158
|
+
(existing?.length === 1 && existing[0].value === wanted)
|
|
159
|
+
) {
|
|
160
|
+
return component;
|
|
161
|
+
}
|
|
162
|
+
const properties = (component.properties || []).filter(
|
|
163
|
+
(property) => property?.name !== "cdx:npm:cleanedVersion",
|
|
164
|
+
);
|
|
165
|
+
if (wanted) {
|
|
166
|
+
properties.push({ name: "cdx:npm:cleanedVersion", value: wanted });
|
|
167
|
+
}
|
|
168
|
+
component.properties = properties;
|
|
169
|
+
return component;
|
|
170
|
+
}
|
|
171
|
+
|
|
87
172
|
/**
|
|
88
173
|
* Helper function to create a properly encoded workspace PURL
|
|
89
174
|
*
|
|
@@ -130,14 +215,18 @@ export function findMatchingNpmWorkspace(workspacePackages, packageName) {
|
|
|
130
215
|
return undefined;
|
|
131
216
|
}
|
|
132
217
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
218
|
+
// Compare the parsed coordinates: a prefix match on the purl text would let
|
|
219
|
+
// `ws` match `wsmember`, and the reference may be escaped or decoded.
|
|
220
|
+
return workspacePackages.find((workspacePackage) => {
|
|
221
|
+
const parts = parsePurlLoose(workspacePackage);
|
|
222
|
+
if (parts?.type !== "npm") {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const fullName = parts.namespace
|
|
226
|
+
? `${parts.namespace}/${parts.name}`
|
|
227
|
+
: parts.name;
|
|
228
|
+
return fullName === packageName;
|
|
229
|
+
});
|
|
141
230
|
}
|
|
142
231
|
|
|
143
232
|
/**
|
|
@@ -201,6 +290,12 @@ export function collectNpmManifestSources(node) {
|
|
|
201
290
|
return manifestSources;
|
|
202
291
|
}
|
|
203
292
|
for (const edge of node.edgesIn) {
|
|
293
|
+
// Arborist adds a `workspace` edge from the root to every member, with an
|
|
294
|
+
// absolute `file:` spec it builds from the checkout path. No manifest
|
|
295
|
+
// declares it, and copying it would put the local path into the BOM.
|
|
296
|
+
if (edge?.workspace || edge?.type === "workspace") {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
204
299
|
const manifestSource = classifyNpmManifestSource(edge?.spec);
|
|
205
300
|
if (!manifestSource) {
|
|
206
301
|
continue;
|
|
@@ -520,7 +615,9 @@ export function parsePnpmGitLockKey(lockKey) {
|
|
|
520
615
|
* @returns {Object<string, object>} Map of lookup key to git package reference.
|
|
521
616
|
*/
|
|
522
617
|
export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
|
|
523
|
-
|
|
618
|
+
// A null prototype keeps inherited Object properties (such as `constructor`)
|
|
619
|
+
// from masquerading as lookup hits for keys that were never registered.
|
|
620
|
+
const gitPkgRefs = Object.create(null);
|
|
524
621
|
const registerEntry = (lockKey, packageNode) => {
|
|
525
622
|
const resolution = packageNode?.resolution;
|
|
526
623
|
const parsed = parsePnpmGitLockKey(lockKey);
|
|
@@ -531,6 +628,10 @@ export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
|
|
|
531
628
|
return;
|
|
532
629
|
}
|
|
533
630
|
const { group, name, gitSpec, fullName, packageName } = parsed;
|
|
631
|
+
if (name === "__proto__" || packageName === "__proto__") {
|
|
632
|
+
// A crafted lock key cannot become a lookup entry under this name.
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
534
635
|
const version = packageNode?.version || resolution?.commit || "";
|
|
535
636
|
const repo = resolution?.repo || "";
|
|
536
637
|
const commit = resolution?.commit || "";
|
|
@@ -466,6 +466,21 @@ export function getPropertyGroupTextNodes(propsFiles) {
|
|
|
466
466
|
return matches;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Read the trimmed text of an MSBuild property from a parsed PropertyGroup.
|
|
471
|
+
*
|
|
472
|
+
* @param {Object} propertyGroup PropertyGroup parsed by xml2js in compact mode
|
|
473
|
+
* @param {string} key Property name
|
|
474
|
+
* @returns {string|undefined} The text, or undefined when absent or empty
|
|
475
|
+
*/
|
|
476
|
+
function propertyGroupText(propertyGroup, key) {
|
|
477
|
+
const text = propertyGroup?.[key]?.[0]?._?.[0];
|
|
478
|
+
if (typeof text !== "string" || !text.trim().length) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
return text.trim();
|
|
482
|
+
}
|
|
483
|
+
|
|
469
484
|
/**
|
|
470
485
|
* Method to parse .csproj like xml files
|
|
471
486
|
*
|
|
@@ -514,6 +529,35 @@ export function parseCsProjData(
|
|
|
514
529
|
return pkgList;
|
|
515
530
|
}
|
|
516
531
|
const project = projects[0];
|
|
532
|
+
// SDK-style projects declare their version through the MSBuild properties
|
|
533
|
+
// that `dotnet pack` reads: PackageVersion, which defaults to Version, which
|
|
534
|
+
// in turn defaults to VersionPrefix plus an optional `-VersionSuffix`. A
|
|
535
|
+
// property may be set in any PropertyGroup, so they are collected first.
|
|
536
|
+
const sdkVersionProps = {};
|
|
537
|
+
for (const apg of project?.PropertyGroup || []) {
|
|
538
|
+
for (const key of [
|
|
539
|
+
"PackageVersion",
|
|
540
|
+
"Version",
|
|
541
|
+
"VersionPrefix",
|
|
542
|
+
"VersionSuffix",
|
|
543
|
+
]) {
|
|
544
|
+
const value = propertyGroupText(apg, key);
|
|
545
|
+
if (value !== undefined) {
|
|
546
|
+
sdkVersionProps[key] = value;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let sdkVersion =
|
|
551
|
+
sdkVersionProps.PackageVersion ||
|
|
552
|
+
sdkVersionProps.Version ||
|
|
553
|
+
(sdkVersionProps.VersionPrefix
|
|
554
|
+
? `${sdkVersionProps.VersionPrefix}${sdkVersionProps.VersionSuffix ? `-${sdkVersionProps.VersionSuffix}` : ""}`
|
|
555
|
+
: undefined);
|
|
556
|
+
// An MSBuild property reference such as $(BuildNumber) is only known at
|
|
557
|
+
// build time, so a version that still contains one is not reported.
|
|
558
|
+
if (sdkVersion?.includes("$(")) {
|
|
559
|
+
sdkVersion = undefined;
|
|
560
|
+
}
|
|
517
561
|
// First make up a parentcomponent name based on the .csproj file name
|
|
518
562
|
if (projFile) {
|
|
519
563
|
parentComponent.name = basename(projFile).replaceAll(
|
|
@@ -571,10 +615,6 @@ export function parseCsProjData(
|
|
|
571
615
|
if (outputType === "Library") {
|
|
572
616
|
parentComponent.type = "library";
|
|
573
617
|
}
|
|
574
|
-
applyPurl(
|
|
575
|
-
parentComponent,
|
|
576
|
-
nugetPurl(parentComponent.name, parentComponent.version || "latest"),
|
|
577
|
-
);
|
|
578
618
|
// The MSBuild OutputType (Exe, WinExe, …) used to ride along as an
|
|
579
619
|
// `output_type` purl qualifier, which nuget does not define — every such
|
|
580
620
|
// purl was invalid. It is project metadata, so it belongs in properties.
|
|
@@ -924,21 +964,22 @@ export function parseCsProjData(
|
|
|
924
964
|
}
|
|
925
965
|
}
|
|
926
966
|
}
|
|
927
|
-
|
|
928
|
-
|
|
967
|
+
if (sdkVersion) {
|
|
968
|
+
parentComponent.version = sdkVersion;
|
|
969
|
+
}
|
|
970
|
+
// The purl is built once every PropertyGroup has been read, so a version
|
|
971
|
+
// declared after OutputType is not lost.
|
|
972
|
+
if (parentComponent.name) {
|
|
929
973
|
applyPurl(
|
|
930
974
|
parentComponent,
|
|
931
975
|
nugetPurl(parentComponent.name, parentComponent.version || "latest"),
|
|
932
976
|
);
|
|
933
977
|
}
|
|
934
|
-
if (parentComponent?.purl) {
|
|
935
|
-
parentComponent["bom-ref"] = parentComponent.purl;
|
|
936
|
-
}
|
|
937
978
|
let dependencies = [];
|
|
938
979
|
if (parentComponent?.["bom-ref"]) {
|
|
939
980
|
dependencies = [
|
|
940
981
|
{
|
|
941
|
-
ref: parentComponent
|
|
982
|
+
ref: parentComponent["bom-ref"],
|
|
942
983
|
dependsOn: [...new Set(pkgList.map((p) => p["bom-ref"]))].sort(),
|
|
943
984
|
},
|
|
944
985
|
];
|
|
@@ -7,7 +7,12 @@ import { Purl } from "@cdxgen/cdx-purl";
|
|
|
7
7
|
import { DEBUG_MODE } from "../core/activity.js";
|
|
8
8
|
import { shouldFetchLicense, shouldFetchVCS } from "../core/env.js";
|
|
9
9
|
import { safeExistsSync } from "../core/fs.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
applyPurl,
|
|
12
|
+
canonicalPurlFromLooseString,
|
|
13
|
+
tryBuildPurl,
|
|
14
|
+
tryParsePurl,
|
|
15
|
+
} from "../inventory/purl.js";
|
|
11
16
|
import {
|
|
12
17
|
_clearMetadataCache,
|
|
13
18
|
getGoPkgLicense,
|
|
@@ -443,6 +448,23 @@ function _addGoComponentEvidence(component, goModFile, confidence = 0.8) {
|
|
|
443
448
|
return component;
|
|
444
449
|
}
|
|
445
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Build the purl of one `module@version` token from `go mod graph` output.
|
|
453
|
+
*
|
|
454
|
+
* The version is written the way Go prints it, so `+incompatible` and the build
|
|
455
|
+
* metadata of a pseudo-version are unescaped. Reading the token strictly would
|
|
456
|
+
* reject it and leave a reference that keeps the module path's upper-case
|
|
457
|
+
* letters, which then never matches the lower-cased component and drops the
|
|
458
|
+
* dependency edge.
|
|
459
|
+
*
|
|
460
|
+
* @param {string} token Module path, optionally followed by `@version`
|
|
461
|
+
* @returns {Purl|null} Parsed purl, or null when the token forms no valid golang purl
|
|
462
|
+
*/
|
|
463
|
+
function goModGraphTokenPurl(token) {
|
|
464
|
+
const canonical = canonicalPurlFromLooseString(`pkg:golang/${token}`);
|
|
465
|
+
return canonical ? Purl.parse(canonical) : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
446
468
|
/**
|
|
447
469
|
* Parse go mod graph
|
|
448
470
|
*
|
|
@@ -512,22 +534,20 @@ export async function parseGoModGraph(
|
|
|
512
534
|
// cdx-purl requires a namespace. Use the raw ref when parsing fails.
|
|
513
535
|
let sourceRefString;
|
|
514
536
|
let sourceName = tmpA[0].split("@")[0];
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
sourcePurl = Purl.parse(`pkg:golang/${tmpA[0]}`);
|
|
537
|
+
const sourcePurl = goModGraphTokenPurl(tmpA[0]);
|
|
538
|
+
if (sourcePurl) {
|
|
518
539
|
sourceRefString = decodeURIComponent(sourcePurl.toString());
|
|
519
540
|
sourceName = `${sourcePurl.namespace ? `${sourcePurl.namespace}/` : ""}${sourcePurl.name}`;
|
|
520
|
-
}
|
|
541
|
+
} else {
|
|
521
542
|
sourceRefString = `pkg:golang/${tmpA[0]}`;
|
|
522
543
|
}
|
|
523
544
|
let dependsRefString;
|
|
524
545
|
let dependsName = tmpA[1].split("@")[0];
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
dependsPurl = Purl.parse(`pkg:golang/${tmpA[1]}`);
|
|
546
|
+
const dependsPurl = goModGraphTokenPurl(tmpA[1]);
|
|
547
|
+
if (dependsPurl) {
|
|
528
548
|
dependsRefString = decodeURIComponent(dependsPurl.toString());
|
|
529
549
|
dependsName = `${dependsPurl.namespace ? `${dependsPurl.namespace}/` : ""}${dependsPurl.name}`;
|
|
530
|
-
}
|
|
550
|
+
} else {
|
|
531
551
|
dependsRefString = `pkg:golang/${tmpA[1]}`;
|
|
532
552
|
}
|
|
533
553
|
// Since go mod graph over-reports direct dependencies we use the existing list
|
|
@@ -683,6 +703,24 @@ export function parseGoModWhy(rawOutput) {
|
|
|
683
703
|
return undefined;
|
|
684
704
|
}
|
|
685
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Reports whether `go mod why -m` output says the main module does not need
|
|
708
|
+
* the module. go prints `(main module does not need module X)`, or
|
|
709
|
+
* `(main module does not need to vendor module X)` with `-vendor`, and exits
|
|
710
|
+
* with status 0 either way.
|
|
711
|
+
*
|
|
712
|
+
* @param {string} rawOutput Output from go mod why
|
|
713
|
+
* @returns {boolean} True when go reports the module as not needed
|
|
714
|
+
*/
|
|
715
|
+
export function isGoModWhyNotNeeded(rawOutput) {
|
|
716
|
+
if (typeof rawOutput !== "string") {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
return rawOutput
|
|
720
|
+
.split("\n")
|
|
721
|
+
.some((line) => line.trim().startsWith("(main module does not need "));
|
|
722
|
+
}
|
|
723
|
+
|
|
686
724
|
/**
|
|
687
725
|
* Parse go sum data
|
|
688
726
|
* @param {string} gosumData Content of go.sum
|