@cdxgen/cdxgen 13.6.0 → 13.7.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 +15 -8
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +256 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +25 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +18 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +52 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +80 -1
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +609 -898
- package/lib/cli/jsBom.js +3 -0
- package/lib/cli/jvmBom.js +365 -40
- package/lib/cli/managedBom.js +19 -3
- package/lib/cli/nativeBom.js +10 -0
- package/lib/core/env.js +26 -0
- package/lib/core/fs.js +126 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/processTree.js +99 -12
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +15 -1
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1082 -6
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-python.js +89 -28
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalasem.js +2 -1
- package/lib/evinser/evinser.js +196 -34
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +112 -1
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +106 -13
- package/lib/inventory/dosai.js +236 -1
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +164 -14
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +132 -38
- package/lib/inventory/evidenceUtils.js +2 -1
- package/lib/inventory/formulationParsers.js +95 -13
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/source.js +65 -6
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/server/server.js +36 -34
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/validator/bomValidator.js +8 -1
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +48 -7
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +9 -9
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +16 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- 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 +2 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- 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/boundedFile.d.ts +30 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +10 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +16 -5
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +248 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- 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/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +61 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +19 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +22 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +15 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { chmodSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
|
|
7
7
|
import { build } from "@cdxgen/cdx-purl";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readEnvironmentVariable,
|
|
13
13
|
recordDecisionActivity,
|
|
14
14
|
} from "../core/activity.js";
|
|
15
|
+
import { readRegularTextFile } from "../core/boundedFile.js";
|
|
15
16
|
import {
|
|
16
17
|
commandOutputText,
|
|
17
18
|
isLedgerEnabled,
|
|
@@ -30,10 +31,13 @@ import {
|
|
|
30
31
|
noteBuildToolRateLimit,
|
|
31
32
|
} from "../inventory/buildToolRateLimit.js";
|
|
32
33
|
import { collectJarNS } from "../inventory/deps.js";
|
|
34
|
+
import { splitArtifactFileName } from "../inventory/jvmLocalRepos.js";
|
|
33
35
|
import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
|
|
34
36
|
import { gradleMirrorInitScriptArgs } from "../inventory/mavenCentralMirror.js";
|
|
35
|
-
import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
|
|
37
|
+
import { applyPurl, mavenPurl, tryBuildPurl } from "../inventory/purl.js";
|
|
38
|
+
import { xml2js } from "../parsers/xml.js";
|
|
36
39
|
import { parsePkgJson } from "./parsers-js.js";
|
|
40
|
+
import { isInVendoredDir } from "./vendoredCode.js";
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* True when the pre-generation stage pinned an exact JVM build tool version
|
|
@@ -1223,7 +1227,12 @@ function parseGradleTreeLine(line) {
|
|
|
1223
1227
|
}
|
|
1224
1228
|
}
|
|
1225
1229
|
const [requested, selected] = text.split(" -> ");
|
|
1226
|
-
|
|
1230
|
+
// Gradle 9.6 and later print the root project as `root project '<name>'`
|
|
1231
|
+
// where earlier versions print `project :`. Either way it is the root
|
|
1232
|
+
// project, whose path is `:`.
|
|
1233
|
+
if (requested.startsWith("root project ")) {
|
|
1234
|
+
parsed.projectPath = ":";
|
|
1235
|
+
} else if (requested.startsWith("project ")) {
|
|
1227
1236
|
parsed.projectPath = gradleProjectPath(requested.slice("project ".length));
|
|
1228
1237
|
} else {
|
|
1229
1238
|
const [group, name, ...versionParts] = requested.split(":");
|
|
@@ -1235,7 +1244,11 @@ function parseGradleTreeLine(line) {
|
|
|
1235
1244
|
parsed.requestedVersion = versionParts.join(":") || undefined;
|
|
1236
1245
|
parsed.version = gradleRequestedVersion(parsed.requestedVersion);
|
|
1237
1246
|
}
|
|
1238
|
-
if (selected?.startsWith("project ")) {
|
|
1247
|
+
if (selected?.startsWith("root project ")) {
|
|
1248
|
+
// A module substituted by the root project, as with
|
|
1249
|
+
// `substitute module('org.ex:main') using project(':')`.
|
|
1250
|
+
parsed.projectPath = ":";
|
|
1251
|
+
} else if (selected?.startsWith("project ")) {
|
|
1239
1252
|
parsed.projectPath = gradleProjectPath(selected.slice("project ".length));
|
|
1240
1253
|
} else if (selected?.includes(":")) {
|
|
1241
1254
|
// The selected module replaces the requested one, version and all.
|
|
@@ -1244,8 +1257,12 @@ function parseGradleTreeLine(line) {
|
|
|
1244
1257
|
parsed.name = name;
|
|
1245
1258
|
parsed.version = version;
|
|
1246
1259
|
parsed.requestedVersion = undefined;
|
|
1247
|
-
} else if (selected) {
|
|
1260
|
+
} else if (selected && !/\s/u.test(selected)) {
|
|
1261
|
+
// A selected version has no whitespace. Anything else is a form of the
|
|
1262
|
+
// report this parser does not know, and must not become a version.
|
|
1248
1263
|
parsed.version = selected;
|
|
1264
|
+
} else if (selected) {
|
|
1265
|
+
return { level: parsed.level, unknown: true };
|
|
1249
1266
|
}
|
|
1250
1267
|
if (parsed.projectPath === undefined && !parsed.group) {
|
|
1251
1268
|
return { level: parsed.level, unknown: true };
|
|
@@ -2433,6 +2450,14 @@ function catalogCoordinate(entry) {
|
|
|
2433
2450
|
return undefined;
|
|
2434
2451
|
}
|
|
2435
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* Gradle rich-version syntax that names no single version: a bracketed range,
|
|
2455
|
+
* an interval, a `!!` or `{strictly}` marker, a `->` transition, or a `..`
|
|
2456
|
+
* separator. A version text matching this is a declaration about versions, not
|
|
2457
|
+
* one version.
|
|
2458
|
+
*/
|
|
2459
|
+
const GRADLE_RICH_VERSION_PATTERN = /[[\](),!]|->|\.\./u;
|
|
2460
|
+
|
|
2436
2461
|
/**
|
|
2437
2462
|
* Resolve the version of one catalog entry.
|
|
2438
2463
|
*
|
|
@@ -2455,8 +2480,1059 @@ function resolveCatalogVersion(entry, versions) {
|
|
|
2455
2480
|
if (typeof raw !== "string" || !raw) {
|
|
2456
2481
|
return undefined;
|
|
2457
2482
|
}
|
|
2458
|
-
if (
|
|
2483
|
+
if (GRADLE_RICH_VERSION_PATTERN.test(raw)) {
|
|
2459
2484
|
return undefined;
|
|
2460
2485
|
}
|
|
2461
2486
|
return raw;
|
|
2462
2487
|
}
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* The largest Gradle lockfile read. Real lockfiles of large multi-project
|
|
2491
|
+
* builds stay well under a megabyte; the file is untrusted input, so a larger
|
|
2492
|
+
* one is skipped whole rather than read in part.
|
|
2493
|
+
*/
|
|
2494
|
+
const GRADLE_LOCKFILE_MAX_BYTES = 8 * 1024 * 1024;
|
|
2495
|
+
|
|
2496
|
+
/**
|
|
2497
|
+
* The most lines read from one Gradle lockfile. Lines past the cap are
|
|
2498
|
+
* ignored.
|
|
2499
|
+
*/
|
|
2500
|
+
const GRADLE_LOCKFILE_MAX_LINES = 100000;
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Gradle 6+ lockfiles that lock a buildscript classpath rather than the
|
|
2504
|
+
* project's own configurations.
|
|
2505
|
+
*/
|
|
2506
|
+
const GRADLE_BUILD_TOOLING_LOCKFILES = new Set([
|
|
2507
|
+
"buildscript-gradle.lockfile",
|
|
2508
|
+
"settings-gradle.lockfile",
|
|
2509
|
+
]);
|
|
2510
|
+
|
|
2511
|
+
const GRADLE_LOCKFILE_SUFFIX = ".lockfile";
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Work out which kind of Gradle lockfile a path names.
|
|
2515
|
+
*
|
|
2516
|
+
* Gradle 6 and later write one `gradle.lockfile` per project, plus
|
|
2517
|
+
* `buildscript-gradle.lockfile` and `settings-gradle.lockfile` for the
|
|
2518
|
+
* buildscript classpaths. Gradle 4.8 to 5.x wrote one
|
|
2519
|
+
* `gradle/dependency-locks/<configuration>.lockfile` per configuration, with
|
|
2520
|
+
* the buildscript configurations prefixed `buildscript-`.
|
|
2521
|
+
*
|
|
2522
|
+
* @param {string} lockFile Path to the lockfile
|
|
2523
|
+
* @returns {{legacy: boolean, buildTooling: boolean, configuration: (string|undefined)}|undefined}
|
|
2524
|
+
* The kind, or undefined when the name is not a lockfile name
|
|
2525
|
+
*/
|
|
2526
|
+
function gradleLockfileKind(lockFile) {
|
|
2527
|
+
const fileName = basename(lockFile);
|
|
2528
|
+
if (!fileName.endsWith(GRADLE_LOCKFILE_SUFFIX)) {
|
|
2529
|
+
return undefined;
|
|
2530
|
+
}
|
|
2531
|
+
const parentDir = dirname(lockFile);
|
|
2532
|
+
if (
|
|
2533
|
+
basename(parentDir) === "dependency-locks" &&
|
|
2534
|
+
basename(dirname(parentDir)) === "gradle"
|
|
2535
|
+
) {
|
|
2536
|
+
const configuration = fileName.slice(0, -GRADLE_LOCKFILE_SUFFIX.length);
|
|
2537
|
+
if (!configuration) {
|
|
2538
|
+
return undefined;
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
legacy: true,
|
|
2542
|
+
buildTooling: configuration.startsWith("buildscript-"),
|
|
2543
|
+
configuration,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
legacy: false,
|
|
2548
|
+
buildTooling: GRADLE_BUILD_TOOLING_LOCKFILES.has(fileName),
|
|
2549
|
+
configuration: undefined,
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
/**
|
|
2554
|
+
* Read a `group:name:version` coordinate from a lockfile entry.
|
|
2555
|
+
*
|
|
2556
|
+
* Gradle writes only resolved versions to a lockfile, so a version that is
|
|
2557
|
+
* blank, a range, a dynamic selector (`1.+`, `latest.release`) or any other
|
|
2558
|
+
* rich-version syntax means the line was not written by Gradle and is
|
|
2559
|
+
* skipped.
|
|
2560
|
+
*
|
|
2561
|
+
* @param {string} text Coordinate text, already trimmed
|
|
2562
|
+
* @returns {{group: string, name: string, version: string}|undefined}
|
|
2563
|
+
* The coordinate, or undefined for anything else
|
|
2564
|
+
*/
|
|
2565
|
+
function parseGradleLockCoordinate(text) {
|
|
2566
|
+
if (!text || /[\s=]/u.test(text)) {
|
|
2567
|
+
return undefined;
|
|
2568
|
+
}
|
|
2569
|
+
const parts = text.split(":");
|
|
2570
|
+
if (parts.length !== 3 || parts.some((part) => !part)) {
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
const [group, name, version] = parts;
|
|
2574
|
+
// Gradle writes only resolved versions to a lockfile, so a rich version or
|
|
2575
|
+
// a dynamic selector (`1.+`, `2.*`, `latest.release`) means the line was
|
|
2576
|
+
// not written by Gradle. The rich-version rejection is the one
|
|
2577
|
+
// `resolveCatalogVersion` applies; a trailing `+` and a `*` are the
|
|
2578
|
+
// dynamic selectors.
|
|
2579
|
+
if (
|
|
2580
|
+
version.startsWith("latest.") ||
|
|
2581
|
+
version.endsWith("+") ||
|
|
2582
|
+
version.includes("*") ||
|
|
2583
|
+
GRADLE_RICH_VERSION_PATTERN.test(version)
|
|
2584
|
+
) {
|
|
2585
|
+
return undefined;
|
|
2586
|
+
}
|
|
2587
|
+
return { group, name, version };
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Parse one Gradle dependency lockfile.
|
|
2592
|
+
*
|
|
2593
|
+
* Both layouts are read. A Gradle 6+ `gradle.lockfile` has one
|
|
2594
|
+
* `group:name:version=configuration,configuration` line per module and an
|
|
2595
|
+
* `empty=` line naming the configurations that resolved nothing. A legacy
|
|
2596
|
+
* `gradle/dependency-locks/<configuration>.lockfile` has one
|
|
2597
|
+
* `group:name:version` line per module, and the file name is the
|
|
2598
|
+
* configuration.
|
|
2599
|
+
*
|
|
2600
|
+
* The scope of a module is the strongest that `gradleConfigurationScope`
|
|
2601
|
+
* gives any of its configurations, so a module locked for both
|
|
2602
|
+
* `testRuntimeClasspath` and `runtimeClasspath` is `required`. Everything in
|
|
2603
|
+
* a buildscript or settings lockfile is build tooling and `excluded`.
|
|
2604
|
+
*
|
|
2605
|
+
* A lockfile records resolved versions, so components carry a purl identity
|
|
2606
|
+
* with confidence 1 and the lockfile path as the evidence value. The purl and
|
|
2607
|
+
* bom-ref match what `parseGradleDep` builds for the same coordinate, so a
|
|
2608
|
+
* component reads the same whether Gradle ran or not. A lockfile is flat: it
|
|
2609
|
+
* says nothing about which module depends on which, so no dependency graph is
|
|
2610
|
+
* returned.
|
|
2611
|
+
*
|
|
2612
|
+
* The file is untrusted input. It is read only when it is a regular file no
|
|
2613
|
+
* larger than 8 MiB, at most 100000 lines are read, and a malformed line
|
|
2614
|
+
* costs only itself. Line content never reaches a property.
|
|
2615
|
+
*
|
|
2616
|
+
* @param {string} lockFile Path to the lockfile
|
|
2617
|
+
* @param {Object} [options] Parse options
|
|
2618
|
+
* @param {string} [options.rootDir] Directory the lockfile must resolve inside
|
|
2619
|
+
* @returns {Object[]} Components, one per locked module
|
|
2620
|
+
*/
|
|
2621
|
+
export function parseGradleLockfile(lockFile, options = {}) {
|
|
2622
|
+
const kind = gradleLockfileKind(lockFile);
|
|
2623
|
+
if (!kind) {
|
|
2624
|
+
return [];
|
|
2625
|
+
}
|
|
2626
|
+
const text = readRegularTextFile(
|
|
2627
|
+
lockFile,
|
|
2628
|
+
GRADLE_LOCKFILE_MAX_BYTES,
|
|
2629
|
+
options?.rootDir,
|
|
2630
|
+
);
|
|
2631
|
+
if (text === undefined) {
|
|
2632
|
+
if (safeExistsSync(lockFile)) {
|
|
2633
|
+
console.warn(
|
|
2634
|
+
`Skipping ${lockFile}: it is not a regular file inside the project, or is larger than ${GRADLE_LOCKFILE_MAX_BYTES} bytes.`,
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
return [];
|
|
2638
|
+
}
|
|
2639
|
+
const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
2640
|
+
const pkgsByRef = new Map();
|
|
2641
|
+
let skippedLines = 0;
|
|
2642
|
+
let lineStart = 0;
|
|
2643
|
+
let lineCount = 0;
|
|
2644
|
+
while (lineStart <= body.length) {
|
|
2645
|
+
if (lineCount === GRADLE_LOCKFILE_MAX_LINES) {
|
|
2646
|
+
if (lineStart < body.length) {
|
|
2647
|
+
thoughtLog(
|
|
2648
|
+
`${lockFile} has more than ${GRADLE_LOCKFILE_MAX_LINES} lines; only the first ${GRADLE_LOCKFILE_MAX_LINES} are read.`,
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2653
|
+
let lineEnd = body.indexOf("\n", lineStart);
|
|
2654
|
+
if (lineEnd === -1) {
|
|
2655
|
+
lineEnd = body.length;
|
|
2656
|
+
}
|
|
2657
|
+
const line = body.slice(lineStart, lineEnd).trim();
|
|
2658
|
+
lineStart = lineEnd + 1;
|
|
2659
|
+
lineCount++;
|
|
2660
|
+
if (!line || line.startsWith("#")) {
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
let coordinateText = line;
|
|
2664
|
+
let configurations = [kind.configuration];
|
|
2665
|
+
if (!kind.legacy) {
|
|
2666
|
+
const separator = line.indexOf("=");
|
|
2667
|
+
if (separator === -1) {
|
|
2668
|
+
skippedLines++;
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
coordinateText = line.slice(0, separator).trim();
|
|
2672
|
+
if (coordinateText === "empty") {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
configurations = line
|
|
2676
|
+
.slice(separator + 1)
|
|
2677
|
+
.split(",")
|
|
2678
|
+
.map((configuration) => configuration.trim())
|
|
2679
|
+
.filter(Boolean);
|
|
2680
|
+
}
|
|
2681
|
+
const coordinate = parseGradleLockCoordinate(coordinateText);
|
|
2682
|
+
if (!coordinate || !configurations.length) {
|
|
2683
|
+
skippedLines++;
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
const { group, name, version } = coordinate;
|
|
2687
|
+
const purl = mavenPurl(group, name, version, { type: "jar" });
|
|
2688
|
+
if (!purl) {
|
|
2689
|
+
skippedLines++;
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
let scope = "excluded";
|
|
2693
|
+
if (!kind.buildTooling) {
|
|
2694
|
+
for (const configuration of configurations) {
|
|
2695
|
+
const configurationScope = gradleConfigurationScope(configuration);
|
|
2696
|
+
if (gradleScopeRank(configurationScope) > gradleScopeRank(scope)) {
|
|
2697
|
+
scope = configurationScope;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
const bomRef = decodeURIComponent(purl);
|
|
2702
|
+
const existing = pkgsByRef.get(bomRef);
|
|
2703
|
+
if (existing) {
|
|
2704
|
+
if (gradleScopeRank(scope) > gradleScopeRank(existing.scope)) {
|
|
2705
|
+
existing.scope = scope;
|
|
2706
|
+
}
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
pkgsByRef.set(bomRef, {
|
|
2710
|
+
group,
|
|
2711
|
+
name,
|
|
2712
|
+
version,
|
|
2713
|
+
qualifiers: { type: "jar" },
|
|
2714
|
+
type: "library",
|
|
2715
|
+
scope,
|
|
2716
|
+
purl,
|
|
2717
|
+
"bom-ref": bomRef,
|
|
2718
|
+
properties: [{ name: "internal:SrcFile", value: lockFile }],
|
|
2719
|
+
evidence: {
|
|
2720
|
+
identity: {
|
|
2721
|
+
field: "purl",
|
|
2722
|
+
confidence: 1,
|
|
2723
|
+
concludedValue: purl,
|
|
2724
|
+
methods: [
|
|
2725
|
+
{
|
|
2726
|
+
technique: "manifest-analysis",
|
|
2727
|
+
confidence: 1,
|
|
2728
|
+
value: lockFile,
|
|
2729
|
+
},
|
|
2730
|
+
],
|
|
2731
|
+
},
|
|
2732
|
+
},
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
if (skippedLines) {
|
|
2736
|
+
thoughtLog(
|
|
2737
|
+
`Skipped ${skippedLines} malformed or unresolved line(s) in ${lockFile}.`,
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
return [...pkgsByRef.values()];
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/**
|
|
2744
|
+
* Parse several Gradle lockfiles of one build and merge the result.
|
|
2745
|
+
*
|
|
2746
|
+
* Every project of a multi-project build has its own lockfile, so the same
|
|
2747
|
+
* module often appears in several of them with different configurations. The
|
|
2748
|
+
* copies collapse into one component with the strongest scope any lockfile
|
|
2749
|
+
* gave it (see `reconcileGradleComponents`), and its evidence lists every
|
|
2750
|
+
* lockfile that locked it. The first lockfile that locked a module stays its
|
|
2751
|
+
* `internal:SrcFile`.
|
|
2752
|
+
*
|
|
2753
|
+
* @param {string[]} lockFiles Paths to the lockfiles
|
|
2754
|
+
* @param {Object} [options] Parse options, as for `parseGradleLockfile`
|
|
2755
|
+
* @returns {Object[]} Components, one per locked module
|
|
2756
|
+
*/
|
|
2757
|
+
export function parseGradleLockfiles(lockFiles, options = {}) {
|
|
2758
|
+
const allPkgs = [];
|
|
2759
|
+
for (const lockFile of lockFiles || []) {
|
|
2760
|
+
allPkgs.push(...parseGradleLockfile(lockFile, options));
|
|
2761
|
+
}
|
|
2762
|
+
reconcileGradleComponents(allPkgs);
|
|
2763
|
+
const pkgsByRef = new Map();
|
|
2764
|
+
for (const pkg of allPkgs) {
|
|
2765
|
+
const existing = pkgsByRef.get(pkg["bom-ref"]);
|
|
2766
|
+
if (!existing) {
|
|
2767
|
+
pkgsByRef.set(pkg["bom-ref"], pkg);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const methods = existing.evidence.identity.methods;
|
|
2771
|
+
for (const method of pkg.evidence.identity.methods) {
|
|
2772
|
+
if (!methods.some((m) => m.value === method.value)) {
|
|
2773
|
+
methods.push(method);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return [...pkgsByRef.values()];
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* The build files that mark a directory as a Gradle project of its own. A
|
|
2782
|
+
* lockfile is accepted only beside one of these, so a lockfile copied into a
|
|
2783
|
+
* notes folder or a build's output is not read as an inventory source.
|
|
2784
|
+
*/
|
|
2785
|
+
const GRADLE_PROJECT_MARKERS = [
|
|
2786
|
+
"build.gradle",
|
|
2787
|
+
"build.gradle.kts",
|
|
2788
|
+
"settings.gradle",
|
|
2789
|
+
"settings.gradle.kts",
|
|
2790
|
+
];
|
|
2791
|
+
|
|
2792
|
+
/**
|
|
2793
|
+
* Find the Gradle dependency lockfiles of one build.
|
|
2794
|
+
*
|
|
2795
|
+
* Both layouts are searched below `rootPath`, recursively even when
|
|
2796
|
+
* `options.multiProject` is off: every subproject of a Gradle build locks its
|
|
2797
|
+
* own configurations, and the subprojects belong to the same build. The
|
|
2798
|
+
* Gradle 6+ names are `gradle.lockfile`, `buildscript-gradle.lockfile` and
|
|
2799
|
+
* `settings-gradle.lockfile`; the Gradle 4.8 to 5.x layout is
|
|
2800
|
+
* `gradle/dependency-locks/<configuration>.lockfile`.
|
|
2801
|
+
*
|
|
2802
|
+
* A lockfile is accepted only when the project directory that owns it holds a
|
|
2803
|
+
* Gradle build file, and never when it sits under `build/`, `.gradle/`,
|
|
2804
|
+
* `node_modules/` or a vendored directory. `getAllFiles` already leaves
|
|
2805
|
+
* `.git`, `.hg`, `.chen` and `node_modules` out; `build/` and `.gradle/` are
|
|
2806
|
+
* Gradle's own output and cache directories, which a lockfile in them cannot
|
|
2807
|
+
* belong to the project's source of truth.
|
|
2808
|
+
*
|
|
2809
|
+
* Custom `dependencyLocking.lockFile` paths, the Nebula `dependencies.lock`
|
|
2810
|
+
* and Palantir `versions.lock` formats are not looked for.
|
|
2811
|
+
*
|
|
2812
|
+
* @param {string} rootPath Directory to search
|
|
2813
|
+
* @param {Object} [options] CLI options
|
|
2814
|
+
* @returns {string[]} Lockfile paths, in a stable order
|
|
2815
|
+
*/
|
|
2816
|
+
export function findGradleLockfiles(rootPath, options = {}) {
|
|
2817
|
+
const lockFiles = new Set();
|
|
2818
|
+
for (const pattern of [
|
|
2819
|
+
"**/{gradle,buildscript-gradle,settings-gradle}.lockfile",
|
|
2820
|
+
"**/gradle/dependency-locks/*.lockfile",
|
|
2821
|
+
]) {
|
|
2822
|
+
for (const lockFile of getAllFiles(rootPath, pattern, options)) {
|
|
2823
|
+
// Only the segments below the scan root count: a scan root that sits
|
|
2824
|
+
// under a directory named `build` still has lockfiles of its own.
|
|
2825
|
+
const segments = relative(rootPath, lockFile).split(/[\\/]/);
|
|
2826
|
+
if (
|
|
2827
|
+
segments.some((segment) =>
|
|
2828
|
+
["build", ".gradle", "node_modules"].includes(segment),
|
|
2829
|
+
)
|
|
2830
|
+
) {
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (isInVendoredDir(rootPath, lockFile)) {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const kind = gradleLockfileKind(lockFile);
|
|
2837
|
+
if (!kind) {
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
// The project directory is the lockfile's own directory, and the
|
|
2841
|
+
// directory above `gradle` for a legacy dependency-locks file.
|
|
2842
|
+
const projectDir = kind.legacy
|
|
2843
|
+
? dirname(dirname(dirname(lockFile)))
|
|
2844
|
+
: dirname(lockFile);
|
|
2845
|
+
if (
|
|
2846
|
+
!GRADLE_PROJECT_MARKERS.some((marker) =>
|
|
2847
|
+
safeExistsSync(join(projectDir, marker)),
|
|
2848
|
+
)
|
|
2849
|
+
) {
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
lockFiles.add(lockFile);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return [...lockFiles].sort();
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/**
|
|
2859
|
+
* The most bytes read from a settings script when looking for the root
|
|
2860
|
+
* project name.
|
|
2861
|
+
*/
|
|
2862
|
+
const GRADLE_SETTINGS_MAX_BYTES = 1024 * 1024;
|
|
2863
|
+
|
|
2864
|
+
/**
|
|
2865
|
+
* Read the root project name a Gradle settings script assigns with
|
|
2866
|
+
* `rootProject.name = '<name>'` or `rootProject.name = "<name>"`, without
|
|
2867
|
+
* running Gradle.
|
|
2868
|
+
*
|
|
2869
|
+
* `settings.gradle` is read before `settings.gradle.kts`, and the last
|
|
2870
|
+
* assignment in the file wins, as it does in Gradle. Only a plain string
|
|
2871
|
+
* literal is accepted: a value built from a variable, a template
|
|
2872
|
+
* (`"${x}"`) or a method call is not something a static read can resolve.
|
|
2873
|
+
*
|
|
2874
|
+
* @param {string} gradleRootPath Root directory of the Gradle build
|
|
2875
|
+
* @returns {string|undefined} The root project name, or undefined when no
|
|
2876
|
+
* settings script assigns one as a plain literal
|
|
2877
|
+
*/
|
|
2878
|
+
export function readGradleRootProjectName(gradleRootPath) {
|
|
2879
|
+
for (const settingsName of ["settings.gradle", "settings.gradle.kts"]) {
|
|
2880
|
+
const settingsFile = join(gradleRootPath, settingsName);
|
|
2881
|
+
if (!safeExistsSync(settingsFile)) {
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const text = readRegularTextFile(
|
|
2885
|
+
settingsFile,
|
|
2886
|
+
GRADLE_SETTINGS_MAX_BYTES,
|
|
2887
|
+
gradleRootPath,
|
|
2888
|
+
);
|
|
2889
|
+
if (!text) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
let rootName;
|
|
2893
|
+
for (const rawLine of text.split("\n")) {
|
|
2894
|
+
const line = rawLine.trim();
|
|
2895
|
+
if (!line.startsWith("rootProject.name")) {
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
let rest = line.slice("rootProject.name".length).trim();
|
|
2899
|
+
if (!rest.startsWith("=")) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
rest = rest.slice(1).trim();
|
|
2903
|
+
const commentIndex = rest.indexOf("//");
|
|
2904
|
+
if (commentIndex !== -1) {
|
|
2905
|
+
rest = rest.slice(0, commentIndex).trim();
|
|
2906
|
+
}
|
|
2907
|
+
if (rest.endsWith(";")) {
|
|
2908
|
+
rest = rest.slice(0, -1).trim();
|
|
2909
|
+
}
|
|
2910
|
+
const quote = rest.charAt(0);
|
|
2911
|
+
if (
|
|
2912
|
+
rest.length < 3 ||
|
|
2913
|
+
!["'", '"'].includes(quote) ||
|
|
2914
|
+
!rest.endsWith(quote)
|
|
2915
|
+
) {
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const value = rest.slice(1, -1).trim();
|
|
2919
|
+
if (
|
|
2920
|
+
!value ||
|
|
2921
|
+
[..."'\"$\\"].some((ch) => value.includes(ch)) ||
|
|
2922
|
+
value.includes("/")
|
|
2923
|
+
) {
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
rootName = value;
|
|
2927
|
+
}
|
|
2928
|
+
if (rootName) {
|
|
2929
|
+
return rootName;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
return undefined;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* The largest gradle/verification-metadata.xml read. Real files of large
|
|
2937
|
+
* builds stay around half a megabyte; the file is untrusted input, so a
|
|
2938
|
+
* larger one is skipped whole rather than read in part.
|
|
2939
|
+
*/
|
|
2940
|
+
const GRADLE_VERIFICATION_METADATA_MAX_BYTES = 32 * 1024 * 1024;
|
|
2941
|
+
|
|
2942
|
+
/**
|
|
2943
|
+
* The most components read from one verification-metadata.xml. Components
|
|
2944
|
+
* past the cap are ignored.
|
|
2945
|
+
*/
|
|
2946
|
+
const GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS = 200000;
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* The most artifacts read for one component. A real entry names the jar, the
|
|
2950
|
+
* pom and the module metadata; a coordinate listing many more artifacts was
|
|
2951
|
+
* not written by Gradle.
|
|
2952
|
+
*/
|
|
2953
|
+
const GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS = 64;
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Checksum element of the verification schema, the CycloneDX hash algorithm
|
|
2957
|
+
* it maps to, and the exact hex digest length that algorithm produces. A
|
|
2958
|
+
* digest of any other length was not written by Gradle.
|
|
2959
|
+
*/
|
|
2960
|
+
const VERIFICATION_HASH_ALGORITHMS = [
|
|
2961
|
+
{ element: "sha512", alg: "SHA-512", length: 128 },
|
|
2962
|
+
{ element: "sha256", alg: "SHA-256", length: 64 },
|
|
2963
|
+
{ element: "sha1", alg: "SHA-1", length: 40 },
|
|
2964
|
+
{ element: "md5", alg: "MD5", length: 32 },
|
|
2965
|
+
];
|
|
2966
|
+
|
|
2967
|
+
const HEX_DIGEST_PATTERN = /^[0-9a-f]+$/iu;
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* A `pgp value` is either a 16-character long key id or a 40-character v4
|
|
2971
|
+
* fingerprint. Anything else is not a key id.
|
|
2972
|
+
*/
|
|
2973
|
+
const PGP_KEY_ID_LENGTHS = new Set([16, 40]);
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* The build stamp a timestamped SNAPSHOT file carries in place of
|
|
2977
|
+
* `-SNAPSHOT`, for example `-20240401.123456-1`.
|
|
2978
|
+
*/
|
|
2979
|
+
const SNAPSHOT_STAMP_PATTERN = /^-\d{8}\.\d{6}-\d+/u;
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* Children of one element in the compact XML form. A child that occurs once
|
|
2983
|
+
* parses as an object and a repeated child as an array; this accepts both.
|
|
2984
|
+
*
|
|
2985
|
+
* @param {object|object[]|undefined} value Compact-form child or children
|
|
2986
|
+
* @returns {object[]} The children as a list
|
|
2987
|
+
*/
|
|
2988
|
+
function verificationElementList(value) {
|
|
2989
|
+
if (value === undefined || value === null) {
|
|
2990
|
+
return [];
|
|
2991
|
+
}
|
|
2992
|
+
return Array.isArray(value) ? value : [value];
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
/**
|
|
2996
|
+
* One named child of an element in the compact XML form.
|
|
2997
|
+
*
|
|
2998
|
+
* A document whose root element carries a namespace prefix keeps that prefix
|
|
2999
|
+
* on the name of every element below it, so the child name is looked up with
|
|
3000
|
+
* the root's prefix in front of it. Attributes are never prefixed: the
|
|
3001
|
+
* schema's attributes are unqualified even when the elements are not.
|
|
3002
|
+
*
|
|
3003
|
+
* @param {object|undefined} element Compact-form element
|
|
3004
|
+
* @param {string} name Child element name without a prefix
|
|
3005
|
+
* @param {string} [prefix] Namespace prefix of the root element, with its
|
|
3006
|
+
* colon
|
|
3007
|
+
* @returns {object[]} The children as a list
|
|
3008
|
+
*/
|
|
3009
|
+
function verificationChildList(element, name, prefix = "") {
|
|
3010
|
+
return verificationElementList(element?.[`${prefix}${name}`]);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Read the `true` or `false` text of a configuration flag. A flag that is
|
|
3015
|
+
* absent, or whose text is neither, reads as undefined so the caller cannot
|
|
3016
|
+
* tell a written `false` from an unreadable one.
|
|
3017
|
+
*
|
|
3018
|
+
* @param {object|undefined} element Compact-form flag element
|
|
3019
|
+
* @returns {boolean|undefined} The flag value, or undefined
|
|
3020
|
+
*/
|
|
3021
|
+
function verificationBoolean(element) {
|
|
3022
|
+
const value = typeof element?._ === "string" ? element._.trim() : "";
|
|
3023
|
+
if (value === "true") {
|
|
3024
|
+
return true;
|
|
3025
|
+
}
|
|
3026
|
+
if (value === "false") {
|
|
3027
|
+
return false;
|
|
3028
|
+
}
|
|
3029
|
+
return undefined;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/**
|
|
3033
|
+
* Split an artifact file name into its classifier and extension, accepting
|
|
3034
|
+
* the timestamped file names Gradle writes for SNAPSHOT versions, for which
|
|
3035
|
+
* the build stamp replaces `-SNAPSHOT` in the file name only.
|
|
3036
|
+
*
|
|
3037
|
+
* @param {string} fileName File name from the verification metadata
|
|
3038
|
+
* @param {string} name Artifact ID of the coordinate
|
|
3039
|
+
* @param {string} version Version of the coordinate
|
|
3040
|
+
* @returns {{classifier: string, extension: string}|undefined} Undefined when
|
|
3041
|
+
* the name does not belong to the coordinate
|
|
3042
|
+
*/
|
|
3043
|
+
function splitVerificationArtifactName(fileName, name, version) {
|
|
3044
|
+
const split = splitArtifactFileName(fileName, name, version);
|
|
3045
|
+
if (split) {
|
|
3046
|
+
return split;
|
|
3047
|
+
}
|
|
3048
|
+
if (!version.endsWith("-SNAPSHOT")) {
|
|
3049
|
+
return undefined;
|
|
3050
|
+
}
|
|
3051
|
+
const base = version.slice(0, -"-SNAPSHOT".length);
|
|
3052
|
+
if (!fileName.startsWith(`${name}-${base}`)) {
|
|
3053
|
+
return undefined;
|
|
3054
|
+
}
|
|
3055
|
+
const stamp = SNAPSHOT_STAMP_PATTERN.exec(
|
|
3056
|
+
fileName.substring(`${name}-${base}`.length),
|
|
3057
|
+
);
|
|
3058
|
+
if (!stamp) {
|
|
3059
|
+
return undefined;
|
|
3060
|
+
}
|
|
3061
|
+
return splitArtifactFileName(fileName, name, `${base}${stamp[0]}`);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/**
|
|
3065
|
+
* Keep a checksum value only when it is a hex digest of the exact length its
|
|
3066
|
+
* algorithm produces. The `origin` and `reason` attributes, and the
|
|
3067
|
+
* `also-trust` alternates, are never read.
|
|
3068
|
+
*
|
|
3069
|
+
* @param {string} alg CycloneDX hash algorithm name
|
|
3070
|
+
* @param {number} length Expected hex digest length
|
|
3071
|
+
* @param {object|undefined} element Compact-form checksum element
|
|
3072
|
+
* @returns {{alg: string, content: string}|undefined} The hash, or undefined
|
|
3073
|
+
*/
|
|
3074
|
+
function verificationHash(alg, length, element) {
|
|
3075
|
+
const value = element?.$?.value;
|
|
3076
|
+
if (typeof value !== "string" || value.length !== length) {
|
|
3077
|
+
return undefined;
|
|
3078
|
+
}
|
|
3079
|
+
if (!HEX_DIGEST_PATTERN.test(value)) {
|
|
3080
|
+
return undefined;
|
|
3081
|
+
}
|
|
3082
|
+
return { alg, content: value.toLowerCase() };
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* Keep a `pgp value` only when it is a long key id or a v4 fingerprint, in
|
|
3087
|
+
* the upper-case form the verification schema itself uses.
|
|
3088
|
+
*
|
|
3089
|
+
* @param {object|undefined} element Compact-form pgp element
|
|
3090
|
+
* @returns {string|undefined} The key id, or undefined
|
|
3091
|
+
*/
|
|
3092
|
+
function verificationPgpKeyId(element) {
|
|
3093
|
+
const value = element?.$?.value;
|
|
3094
|
+
if (typeof value !== "string") {
|
|
3095
|
+
return undefined;
|
|
3096
|
+
}
|
|
3097
|
+
const keyId = value.trim();
|
|
3098
|
+
if (
|
|
3099
|
+
!PGP_KEY_ID_LENGTHS.has(keyId.length) ||
|
|
3100
|
+
!HEX_DIGEST_PATTERN.test(keyId)
|
|
3101
|
+
) {
|
|
3102
|
+
return undefined;
|
|
3103
|
+
}
|
|
3104
|
+
return keyId.toUpperCase();
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Parse a `gradle/verification-metadata.xml` file.
|
|
3109
|
+
*
|
|
3110
|
+
* The file is untrusted input. It records what Gradle verified when
|
|
3111
|
+
* `gradle --write-verification-metadata` last ran, which does not prove that
|
|
3112
|
+
* the artifacts in this BOM match: the file travels with the source, and
|
|
3113
|
+
* nothing here re-checks the digests. Trust rules (`trusted-artifacts`,
|
|
3114
|
+
* `trusted-keys`, `ignored-keys`) may be regular expressions, so they are
|
|
3115
|
+
* only counted, never evaluated, and no free text of the file (an `origin`, a
|
|
3116
|
+
* `reason`, a key-server URI) reaches the result. Only checksum digests,
|
|
3117
|
+
* PGP key ids and the configuration flags survive.
|
|
3118
|
+
*
|
|
3119
|
+
* The read is bounded: the file must be a regular file no larger than
|
|
3120
|
+
* 32 MiB inside `options.rootDir`, at most 200000 components and 64
|
|
3121
|
+
* artifacts per component are read, and a malformed file yields undefined
|
|
3122
|
+
* rather than an error.
|
|
3123
|
+
*
|
|
3124
|
+
* @param {string} metadataFile Path to verification-metadata.xml
|
|
3125
|
+
* @param {Object} [options] Parse options
|
|
3126
|
+
* @param {string} [options.rootDir] Directory the file must resolve inside
|
|
3127
|
+
* @returns {{configuration: {verifyMetadata: (boolean|undefined), verifySignatures: (boolean|undefined), trustedKeyCount: number, trustedArtifactRuleCount: number, ignoredKeyCount: number, keyServerCount: number}, components: Map<string, {group: string, name: string, version: string, artifacts: {fileName: string, classifier: string, extension: string, hashes: {alg: string, content: string}[], pgpKeyIds: string[]}[]}>}|undefined}
|
|
3128
|
+
* The parsed metadata, or undefined when the file could not be read
|
|
3129
|
+
*/
|
|
3130
|
+
export function parseGradleVerificationMetadata(metadataFile, options = {}) {
|
|
3131
|
+
const text = readRegularTextFile(
|
|
3132
|
+
metadataFile,
|
|
3133
|
+
GRADLE_VERIFICATION_METADATA_MAX_BYTES,
|
|
3134
|
+
options?.rootDir,
|
|
3135
|
+
);
|
|
3136
|
+
if (text === undefined) {
|
|
3137
|
+
if (safeExistsSync(metadataFile)) {
|
|
3138
|
+
console.warn(
|
|
3139
|
+
`Skipping ${metadataFile}: it is not a regular file inside the project, or is larger than ${GRADLE_VERIFICATION_METADATA_MAX_BYTES} bytes.`,
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
return undefined;
|
|
3143
|
+
}
|
|
3144
|
+
const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
3145
|
+
let parsed;
|
|
3146
|
+
try {
|
|
3147
|
+
parsed = xml2js(body, {
|
|
3148
|
+
compact: true,
|
|
3149
|
+
ignoreComment: true,
|
|
3150
|
+
ignoreDoctype: true,
|
|
3151
|
+
ignoreDeclaration: true,
|
|
3152
|
+
textKey: "_",
|
|
3153
|
+
attributesKey: "$",
|
|
3154
|
+
});
|
|
3155
|
+
} catch (err) {
|
|
3156
|
+
thoughtLog(
|
|
3157
|
+
`${metadataFile} is not well-formed XML, so it was skipped: ${
|
|
3158
|
+
err?.message || err
|
|
3159
|
+
}`,
|
|
3160
|
+
);
|
|
3161
|
+
return undefined;
|
|
3162
|
+
}
|
|
3163
|
+
// Gradle writes the root element in its own namespace, which a default
|
|
3164
|
+
// namespace leaves off element names and a prefix keeps; both are accepted.
|
|
3165
|
+
// A prefixed document keeps the prefix on the name of every element below
|
|
3166
|
+
// the root, so the prefix is carried into every child lookup below.
|
|
3167
|
+
const rootKey = Object.keys(parsed).find(
|
|
3168
|
+
(key) =>
|
|
3169
|
+
key === "verification-metadata" || key.endsWith(":verification-metadata"),
|
|
3170
|
+
);
|
|
3171
|
+
const root = verificationElementList(
|
|
3172
|
+
rootKey ? parsed[rootKey] : undefined,
|
|
3173
|
+
)[0];
|
|
3174
|
+
if (!root) {
|
|
3175
|
+
thoughtLog(
|
|
3176
|
+
`${metadataFile} has no verification-metadata root element, so it was skipped.`,
|
|
3177
|
+
);
|
|
3178
|
+
return undefined;
|
|
3179
|
+
}
|
|
3180
|
+
const prefix = rootKey?.includes(":")
|
|
3181
|
+
? rootKey.slice(0, rootKey.indexOf(":") + 1)
|
|
3182
|
+
: "";
|
|
3183
|
+
const configurationElement = verificationChildList(
|
|
3184
|
+
root,
|
|
3185
|
+
"configuration",
|
|
3186
|
+
prefix,
|
|
3187
|
+
)[0];
|
|
3188
|
+
const componentsElement = verificationChildList(
|
|
3189
|
+
root,
|
|
3190
|
+
"components",
|
|
3191
|
+
prefix,
|
|
3192
|
+
)[0];
|
|
3193
|
+
const trustedKeysElement = verificationChildList(
|
|
3194
|
+
configurationElement,
|
|
3195
|
+
"trusted-keys",
|
|
3196
|
+
prefix,
|
|
3197
|
+
)[0];
|
|
3198
|
+
const trustedArtifactsElement = verificationChildList(
|
|
3199
|
+
configurationElement,
|
|
3200
|
+
"trusted-artifacts",
|
|
3201
|
+
prefix,
|
|
3202
|
+
)[0];
|
|
3203
|
+
const ignoredKeysElement = verificationChildList(
|
|
3204
|
+
configurationElement,
|
|
3205
|
+
"ignored-keys",
|
|
3206
|
+
prefix,
|
|
3207
|
+
)[0];
|
|
3208
|
+
const keyServersElement = verificationChildList(
|
|
3209
|
+
configurationElement,
|
|
3210
|
+
"key-servers",
|
|
3211
|
+
prefix,
|
|
3212
|
+
)[0];
|
|
3213
|
+
const components = new Map();
|
|
3214
|
+
let skippedArtifacts = 0;
|
|
3215
|
+
const componentList = verificationChildList(
|
|
3216
|
+
componentsElement,
|
|
3217
|
+
"component",
|
|
3218
|
+
prefix,
|
|
3219
|
+
);
|
|
3220
|
+
for (const componentElement of componentList) {
|
|
3221
|
+
if (components.size === GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS) {
|
|
3222
|
+
thoughtLog(
|
|
3223
|
+
`${metadataFile} lists more than ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} components; only the first ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} are read.`,
|
|
3224
|
+
);
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
const attributes = componentElement?.$ || {};
|
|
3228
|
+
const group =
|
|
3229
|
+
typeof attributes.group === "string" ? attributes.group.trim() : "";
|
|
3230
|
+
const name =
|
|
3231
|
+
typeof attributes.name === "string" ? attributes.name.trim() : "";
|
|
3232
|
+
const version =
|
|
3233
|
+
typeof attributes.version === "string" ? attributes.version.trim() : "";
|
|
3234
|
+
// A coordinate without all three parts, or one no valid purl can be
|
|
3235
|
+
// built for, can never match a component of this BOM.
|
|
3236
|
+
if (!group || !name || !version) {
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (!mavenPurl(group, name, version)) {
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
const artifacts = [];
|
|
3243
|
+
const artifactList = verificationChildList(
|
|
3244
|
+
componentElement,
|
|
3245
|
+
"artifact",
|
|
3246
|
+
prefix,
|
|
3247
|
+
);
|
|
3248
|
+
if (artifactList.length > GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS) {
|
|
3249
|
+
skippedArtifacts +=
|
|
3250
|
+
artifactList.length - GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS;
|
|
3251
|
+
}
|
|
3252
|
+
for (const artifactElement of artifactList.slice(
|
|
3253
|
+
0,
|
|
3254
|
+
GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS,
|
|
3255
|
+
)) {
|
|
3256
|
+
const fileName =
|
|
3257
|
+
typeof artifactElement?.$?.name === "string"
|
|
3258
|
+
? artifactElement.$.name.trim()
|
|
3259
|
+
: "";
|
|
3260
|
+
const split = fileName
|
|
3261
|
+
? splitVerificationArtifactName(fileName, name, version)
|
|
3262
|
+
: undefined;
|
|
3263
|
+
if (!split) {
|
|
3264
|
+
skippedArtifacts++;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
const hashes = [];
|
|
3268
|
+
for (const { element, alg, length } of VERIFICATION_HASH_ALGORITHMS) {
|
|
3269
|
+
const hash = verificationHash(
|
|
3270
|
+
alg,
|
|
3271
|
+
length,
|
|
3272
|
+
verificationChildList(artifactElement, element, prefix)[0],
|
|
3273
|
+
);
|
|
3274
|
+
if (hash) {
|
|
3275
|
+
hashes.push(hash);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const pgpKeyIds = [];
|
|
3279
|
+
for (const pgpElement of verificationChildList(
|
|
3280
|
+
artifactElement,
|
|
3281
|
+
"pgp",
|
|
3282
|
+
prefix,
|
|
3283
|
+
)) {
|
|
3284
|
+
const keyId = verificationPgpKeyId(pgpElement);
|
|
3285
|
+
if (keyId && !pgpKeyIds.includes(keyId)) {
|
|
3286
|
+
pgpKeyIds.push(keyId);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
pgpKeyIds.sort();
|
|
3290
|
+
artifacts.push({
|
|
3291
|
+
fileName,
|
|
3292
|
+
classifier: split.classifier,
|
|
3293
|
+
extension: split.extension,
|
|
3294
|
+
hashes,
|
|
3295
|
+
pgpKeyIds,
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
const key = `${group}:${name}:${version}`;
|
|
3299
|
+
if (!components.has(key)) {
|
|
3300
|
+
components.set(key, { group, name, version, artifacts });
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
if (skippedArtifacts) {
|
|
3304
|
+
thoughtLog(
|
|
3305
|
+
`Skipped ${skippedArtifacts} artifact element(s) whose file name does not belong to their coordinate in ${metadataFile}.`,
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
return {
|
|
3309
|
+
configuration: {
|
|
3310
|
+
verifyMetadata: verificationBoolean(
|
|
3311
|
+
verificationChildList(
|
|
3312
|
+
configurationElement,
|
|
3313
|
+
"verify-metadata",
|
|
3314
|
+
prefix,
|
|
3315
|
+
)[0],
|
|
3316
|
+
),
|
|
3317
|
+
verifySignatures: verificationBoolean(
|
|
3318
|
+
verificationChildList(
|
|
3319
|
+
configurationElement,
|
|
3320
|
+
"verify-signatures",
|
|
3321
|
+
prefix,
|
|
3322
|
+
)[0],
|
|
3323
|
+
),
|
|
3324
|
+
trustedKeyCount: verificationChildList(
|
|
3325
|
+
trustedKeysElement,
|
|
3326
|
+
"trusted-key",
|
|
3327
|
+
prefix,
|
|
3328
|
+
).length,
|
|
3329
|
+
trustedArtifactRuleCount: verificationChildList(
|
|
3330
|
+
trustedArtifactsElement,
|
|
3331
|
+
"trust",
|
|
3332
|
+
prefix,
|
|
3333
|
+
).length,
|
|
3334
|
+
ignoredKeyCount: verificationChildList(
|
|
3335
|
+
ignoredKeysElement,
|
|
3336
|
+
"ignored-key",
|
|
3337
|
+
prefix,
|
|
3338
|
+
).length,
|
|
3339
|
+
keyServerCount: verificationChildList(
|
|
3340
|
+
keyServersElement,
|
|
3341
|
+
"key-server",
|
|
3342
|
+
prefix,
|
|
3343
|
+
).length,
|
|
3344
|
+
},
|
|
3345
|
+
components,
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
/**
|
|
3350
|
+
* Find the gradle/verification-metadata.xml of one Gradle build.
|
|
3351
|
+
*
|
|
3352
|
+
* Gradle reads one verification file per build, from the root build's
|
|
3353
|
+
* `gradle` directory; included builds' copies are ignored, so nothing is
|
|
3354
|
+
* searched below the root. The file is accepted only when the directory is a
|
|
3355
|
+
* Gradle project of its own, so a file copied into a nested folder is not
|
|
3356
|
+
* read as an inventory source.
|
|
3357
|
+
*
|
|
3358
|
+
* @param {string} gradleRootPath Root directory of the Gradle build
|
|
3359
|
+
* @returns {string|undefined} The metadata file path, or undefined
|
|
3360
|
+
*/
|
|
3361
|
+
export function findGradleVerificationMetadata(gradleRootPath) {
|
|
3362
|
+
const metadataFile = join(
|
|
3363
|
+
gradleRootPath,
|
|
3364
|
+
"gradle",
|
|
3365
|
+
"verification-metadata.xml",
|
|
3366
|
+
);
|
|
3367
|
+
if (!safeExistsSync(metadataFile)) {
|
|
3368
|
+
return undefined;
|
|
3369
|
+
}
|
|
3370
|
+
if (
|
|
3371
|
+
!GRADLE_PROJECT_MARKERS.some((marker) =>
|
|
3372
|
+
safeExistsSync(join(gradleRootPath, marker)),
|
|
3373
|
+
)
|
|
3374
|
+
) {
|
|
3375
|
+
return undefined;
|
|
3376
|
+
}
|
|
3377
|
+
return metadataFile;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
/**
|
|
3381
|
+
* The artifact of a coordinate whose hashes describe the component itself:
|
|
3382
|
+
* the `.pom` artifact for a platform or BOM component, else the main jar,
|
|
3383
|
+
* else the main aar. Sources, javadoc and other classifiers, and the
|
|
3384
|
+
* `.module` and `.pom` hashes of a jar component, are never used.
|
|
3385
|
+
*
|
|
3386
|
+
* @param {object} component Parsed verification metadata component
|
|
3387
|
+
* @param {boolean} wantsPom True for a component with purl qualifier type=pom
|
|
3388
|
+
* @returns {object|undefined} The artifact, or undefined
|
|
3389
|
+
*/
|
|
3390
|
+
export function gradleVerificationMainArtifact(component, wantsPom) {
|
|
3391
|
+
const wanted = wantsPom ? "pom" : "jar";
|
|
3392
|
+
const main = component.artifacts.find(
|
|
3393
|
+
(artifact) => !artifact.classifier && artifact.extension === wanted,
|
|
3394
|
+
);
|
|
3395
|
+
if (main || wantsPom) {
|
|
3396
|
+
return main;
|
|
3397
|
+
}
|
|
3398
|
+
return component.artifacts.find(
|
|
3399
|
+
(artifact) => !artifact.classifier && artifact.extension === "aar",
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
/**
|
|
3404
|
+
* Merge the checksums and PGP key ids a verification-metadata.xml records
|
|
3405
|
+
* into the components of a package list.
|
|
3406
|
+
*
|
|
3407
|
+
* A digest of the artifact itself, computed from the jar in the local cache
|
|
3408
|
+
* or found by another collector, is the stronger claim, so the recorded
|
|
3409
|
+
* digests only fill the algorithms a component still lacks. Where the file
|
|
3410
|
+
* records a different digest for an algorithm the component already has,
|
|
3411
|
+
* the recorded digest never wins: the component keeps its own, and the
|
|
3412
|
+
* component gains the cdx:gradle:verificationMismatch=true property so the
|
|
3413
|
+
* disagreement is visible. A released Maven version does not change after
|
|
3414
|
+
* publishing, so a stale file cannot explain a mismatch: a stale file names
|
|
3415
|
+
* old versions, which match nothing. The likely causes are an artifact
|
|
3416
|
+
* republished on a mirror or internal repository, or a corrupted or
|
|
3417
|
+
* tampered cache. SNAPSHOT versions and digests that are not hex are not
|
|
3418
|
+
* compared.
|
|
3419
|
+
*
|
|
3420
|
+
* Every matched component also gains two properties naming the verification
|
|
3421
|
+
* methods and the PGP key ids of its artifact. The evidence of the
|
|
3422
|
+
* component is left alone: a component found by a Gradle run or a lockfile
|
|
3423
|
+
* keeps the identity evidence of that source, and only the components the
|
|
3424
|
+
* verification-metadata fallback built carry the file as their evidence.
|
|
3425
|
+
*
|
|
3426
|
+
* @param {Object[]} pkgList Components to enrich in place
|
|
3427
|
+
* @param {Object} metadata Parsed metadata, as parseGradleVerificationMetadata
|
|
3428
|
+
* returns
|
|
3429
|
+
* @param {string} metadataFile Path to the verification-metadata.xml, named
|
|
3430
|
+
* in the debug reasoning
|
|
3431
|
+
* @returns {number} The number of components this call changed
|
|
3432
|
+
*/
|
|
3433
|
+
export function applyGradleVerificationMetadata(
|
|
3434
|
+
pkgList,
|
|
3435
|
+
metadata,
|
|
3436
|
+
metadataFile,
|
|
3437
|
+
) {
|
|
3438
|
+
if (!metadata?.components?.size) {
|
|
3439
|
+
return 0;
|
|
3440
|
+
}
|
|
3441
|
+
let changedCount = 0;
|
|
3442
|
+
for (const pkg of pkgList || []) {
|
|
3443
|
+
if (!pkg?.purl?.startsWith("pkg:maven/")) {
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
const component = metadata.components.get(
|
|
3447
|
+
`${pkg.group}:${pkg.name}:${pkg.version}`,
|
|
3448
|
+
);
|
|
3449
|
+
if (!component) {
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
const artifact = gradleVerificationMainArtifact(
|
|
3453
|
+
component,
|
|
3454
|
+
pkg.qualifiers?.type === "pom",
|
|
3455
|
+
);
|
|
3456
|
+
if (!artifact) {
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
const hashesBefore = pkg.hashes;
|
|
3460
|
+
const propertiesBefore = pkg.properties;
|
|
3461
|
+
// A SNAPSHOT version names a moving artifact: the file can record
|
|
3462
|
+
// several timestamped builds of it while the cache holds the newest, so
|
|
3463
|
+
// a differing digest there says nothing about tampering.
|
|
3464
|
+
const comparable = !String(pkg.version || "").endsWith("-SNAPSHOT");
|
|
3465
|
+
// The merged list keeps the order the component's own hashes came in
|
|
3466
|
+
// and appends the recorded ones in the schema's algorithm order.
|
|
3467
|
+
const hashes = Array.isArray(pkg.hashes) ? [...pkg.hashes] : [];
|
|
3468
|
+
let mismatch = false;
|
|
3469
|
+
for (const recorded of artifact.hashes) {
|
|
3470
|
+
const existing = hashes.find((hash) => hash.alg === recorded.alg);
|
|
3471
|
+
if (existing) {
|
|
3472
|
+
// Other collectors may carry an upper-case or base64 digest; only a
|
|
3473
|
+
// hex digest can be compared with the recorded one.
|
|
3474
|
+
const existingContent =
|
|
3475
|
+
typeof existing.content === "string" ? existing.content : "";
|
|
3476
|
+
if (
|
|
3477
|
+
comparable &&
|
|
3478
|
+
HEX_DIGEST_PATTERN.test(existingContent) &&
|
|
3479
|
+
existingContent.toLowerCase() !== recorded.content
|
|
3480
|
+
) {
|
|
3481
|
+
mismatch = true;
|
|
3482
|
+
thoughtLog(
|
|
3483
|
+
`${metadataFile} records a different ${recorded.alg} digest for ${pkg.group}:${pkg.name}:${pkg.version} than the artifact carries; the recorded digest is flagged, not applied.`,
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
hashes.push({ alg: recorded.alg, content: recorded.content });
|
|
3489
|
+
}
|
|
3490
|
+
if (hashes.length && hashes.length !== (pkg.hashes?.length || 0)) {
|
|
3491
|
+
pkg.hashes = hashes;
|
|
3492
|
+
}
|
|
3493
|
+
const methods = new Set();
|
|
3494
|
+
for (const { element, alg } of VERIFICATION_HASH_ALGORITHMS) {
|
|
3495
|
+
if (artifact.hashes.some((hash) => hash.alg === alg)) {
|
|
3496
|
+
methods.add(element);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
if (artifact.pgpKeyIds.length) {
|
|
3500
|
+
methods.add("pgp");
|
|
3501
|
+
}
|
|
3502
|
+
const properties = pkg.properties || [];
|
|
3503
|
+
if (
|
|
3504
|
+
methods.size &&
|
|
3505
|
+
!properties.some((p) => p.name === "cdx:gradle:verificationMethods")
|
|
3506
|
+
) {
|
|
3507
|
+
properties.push({
|
|
3508
|
+
name: "cdx:gradle:verificationMethods",
|
|
3509
|
+
value: [...methods].sort().join(","),
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
if (
|
|
3513
|
+
artifact.pgpKeyIds.length &&
|
|
3514
|
+
!properties.some((p) => p.name === "cdx:gradle:pgpKeyIds")
|
|
3515
|
+
) {
|
|
3516
|
+
properties.push({
|
|
3517
|
+
name: "cdx:gradle:pgpKeyIds",
|
|
3518
|
+
value: [...new Set(artifact.pgpKeyIds)].sort().slice(0, 10).join(","),
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
if (
|
|
3522
|
+
mismatch &&
|
|
3523
|
+
!properties.some((p) => p.name === "cdx:gradle:verificationMismatch")
|
|
3524
|
+
) {
|
|
3525
|
+
properties.push({
|
|
3526
|
+
name: "cdx:gradle:verificationMismatch",
|
|
3527
|
+
value: "true",
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
if (properties.length !== (pkg.properties?.length || 0)) {
|
|
3531
|
+
pkg.properties = properties;
|
|
3532
|
+
}
|
|
3533
|
+
if (pkg.hashes !== hashesBefore || pkg.properties !== propertiesBefore) {
|
|
3534
|
+
changedCount++;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return changedCount;
|
|
3538
|
+
}
|