@cdxgen/cdxgen 13.2.0 → 13.4.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 +33 -14
- package/bin/cdxgen.js +310 -138
- package/bin/evinse.js +22 -0
- package/bin/repl.js +86 -0
- package/bin/sign.js +119 -15
- package/bin/tracebom.js +69 -5
- package/bin/validate.js +107 -19
- package/bin/verify.js +172 -63
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +85 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +156 -58
- package/lib/cli/jsBom.js +99 -53
- package/lib/cli/jvmBom.js +708 -147
- package/lib/cli/managedBom.js +415 -224
- package/lib/cli/nativeBom.js +264 -44
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +22 -15
- package/lib/core/fs.js +176 -7
- package/lib/core/httpClient.js +159 -24
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +274 -108
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +76 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +399 -51
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1983 -793
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +356 -37
- package/lib/ecosystems/jsEvidence.js +104 -44
- package/lib/ecosystems/kosi.js +31 -7
- package/lib/ecosystems/npmutils.js +174 -9
- package/lib/ecosystems/parsers-dotnet.js +616 -159
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +371 -70
- package/lib/ecosystems/parsers-js.js +263 -76
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +929 -216
- package/lib/ecosystems/parsers-python.js +167 -24
- package/lib/ecosystems/parsers-rust.js +28 -22
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +1492 -182
- package/lib/ecosystems/rusi.js +54 -10
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +1008 -191
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +1101 -222
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +703 -43
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +798 -57
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +90 -6
- package/lib/inventory/evidenceUtils.js +166 -113
- package/lib/inventory/fetchBatch.js +690 -55
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +45 -27
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +295 -25
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +467 -46
- package/lib/managers/oci.js +63 -3
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +466 -0
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +83 -16
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +64 -11
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +2 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +23 -23
- package/types/lib/audit/index.d.ts.map +1 -1
- 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 +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -0
- 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/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.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/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.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/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.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/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.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 +28 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- 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 +70 -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 +57 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +54 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +16 -5
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- 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/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +164 -3
- 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/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +132 -15
- 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 +182 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +219 -5
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.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/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.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 +121 -9
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- 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/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +59 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- 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 +17 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +77 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.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/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +18 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
existsSync,
|
|
5
|
-
lstatSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
statSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { constants, lstatSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
4
|
import {
|
|
12
5
|
delimiter as _delimiter,
|
|
13
6
|
sep as _sep,
|
|
14
7
|
basename,
|
|
15
|
-
|
|
8
|
+
isAbsolute,
|
|
16
9
|
join,
|
|
17
10
|
relative,
|
|
11
|
+
resolve,
|
|
18
12
|
} from "node:path";
|
|
19
13
|
|
|
20
14
|
import { build } from "@cdxgen/cdx-purl";
|
|
21
|
-
import { globSync } from "glob";
|
|
22
15
|
import StreamZip from "node-stream-zip";
|
|
23
16
|
import {
|
|
24
17
|
clean,
|
|
@@ -32,49 +25,68 @@ import {
|
|
|
32
25
|
import {
|
|
33
26
|
cdxgenAgent,
|
|
34
27
|
DEBUG_MODE,
|
|
28
|
+
isSecureMode,
|
|
35
29
|
readEnvironmentVariable,
|
|
36
30
|
} from "../core/activity.js";
|
|
37
31
|
import {
|
|
38
32
|
SEARCH_MAVEN_ORG,
|
|
39
33
|
shouldFetchLicense,
|
|
40
34
|
shouldFetchPackageMetadata,
|
|
35
|
+
shouldFetchRegistryProvenance,
|
|
41
36
|
shouldFetchVCS,
|
|
42
37
|
} from "../core/env.js";
|
|
43
38
|
import {
|
|
44
39
|
checksumFile,
|
|
45
40
|
getAllFiles,
|
|
41
|
+
getTmpDir,
|
|
46
42
|
multiChecksumFile,
|
|
47
43
|
safeCopyFileSync,
|
|
48
44
|
safeExistsSync,
|
|
49
45
|
safeExtractArchive,
|
|
46
|
+
safeMkdtempSync,
|
|
50
47
|
safeRmSync,
|
|
51
48
|
} from "../core/fs.js";
|
|
52
49
|
import { thoughtLog } from "../core/logger.js";
|
|
53
50
|
import { mapWithConcurrency } from "../core/parallel.js";
|
|
54
51
|
import { isWin } from "../core/paths.js";
|
|
52
|
+
import { registerRunReset } from "../core/runState.js";
|
|
55
53
|
import { vendorAliases } from "../core/state.js";
|
|
56
|
-
import { isGitHubUrl, modulePathHost } from "../core/urls.js";
|
|
54
|
+
import { isGitHubUrl, modulePathHost, parseUrl } from "../core/urls.js";
|
|
57
55
|
import {
|
|
58
56
|
collectJarNS,
|
|
59
57
|
getPomPropertiesFromMavenDir,
|
|
60
58
|
inferJarGroupFromManifest,
|
|
59
|
+
isDocumentationJar,
|
|
61
60
|
parseJarManifest,
|
|
61
|
+
parseManifestLicenseList,
|
|
62
62
|
parsePomXml,
|
|
63
|
+
readEmbeddedMavenDescriptor,
|
|
63
64
|
trimJarGroupSuffix,
|
|
64
65
|
} from "../inventory/deps.js";
|
|
65
66
|
import {
|
|
67
|
+
gatedGet,
|
|
68
|
+
isHostCircuitOpen,
|
|
69
|
+
openHostCircuit,
|
|
66
70
|
prefetchEnabled,
|
|
67
71
|
prefetchedResponse,
|
|
68
72
|
prefetchJson,
|
|
69
73
|
withHostRateLimit,
|
|
70
74
|
} from "../inventory/fetchBatch.js";
|
|
75
|
+
import {
|
|
76
|
+
findLocalMavenArtifact,
|
|
77
|
+
findMavenCoordinatesBySha1,
|
|
78
|
+
inferMavenCoordinatesFromPath,
|
|
79
|
+
} from "../inventory/jvmLocalRepos.js";
|
|
71
80
|
import {
|
|
72
81
|
applyPurl,
|
|
73
|
-
encodeForPurl,
|
|
74
82
|
genericPurl,
|
|
75
83
|
nugetPurl,
|
|
76
84
|
pypiPurl,
|
|
77
85
|
} from "../inventory/purl.js";
|
|
86
|
+
import {
|
|
87
|
+
publishedArtifactId,
|
|
88
|
+
scalaCoordinateOfPurl,
|
|
89
|
+
} from "../inventory/scalaCoords.js";
|
|
78
90
|
import {
|
|
79
91
|
findLicenseId,
|
|
80
92
|
guessLicenseId,
|
|
@@ -86,6 +98,9 @@ import {
|
|
|
86
98
|
localCargoMetadataEnabled,
|
|
87
99
|
readCargoCacheMetadata,
|
|
88
100
|
} from "./cargoCache.js";
|
|
101
|
+
import { normalizeNpmRegistryUrl, resolveNpmLicense } from "./npmutils.js";
|
|
102
|
+
import { parseNuspecData } from "./parsers-dotnet.js";
|
|
103
|
+
import { isDefaultPypiRegistry } from "./pylockutils.js";
|
|
89
104
|
import {
|
|
90
105
|
applyPypiClassifierMetadata,
|
|
91
106
|
applyPypiModuleNames,
|
|
@@ -96,26 +111,63 @@ import {
|
|
|
96
111
|
collectPypiRegistryProvenanceProperties,
|
|
97
112
|
} from "./registryProvenance.js";
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
/**
|
|
115
|
+
* No longer used. Registry answers are kept per ecosystem and for one run; this
|
|
116
|
+
* object stays exported for callers that imported it.
|
|
117
|
+
*
|
|
118
|
+
* @deprecated
|
|
119
|
+
*/
|
|
120
|
+
export const metadata_cache = {};
|
|
121
|
+
|
|
122
|
+
// Registry answers kept for the rest of a run, one map per ecosystem, so that a
|
|
123
|
+
// package several lockfiles name is asked about once and one ecosystem's parser
|
|
124
|
+
// cannot empty another's. They are emptied when the run ends.
|
|
125
|
+
/** npm packument URL to the packument. */
|
|
126
|
+
const npmDocuments = new Map();
|
|
127
|
+
/** pkg.go.dev page URL to the licences or the repository URL read from it. */
|
|
128
|
+
const goPkgAnswers = new Map();
|
|
129
|
+
/** `name|version` to the NuGet catalog leaf, or `{error}` for a failed lookup. */
|
|
130
|
+
const nugetEntries = new Map();
|
|
131
|
+
|
|
132
|
+
registerRunReset("ecosystems:registry-answers", () => {
|
|
133
|
+
npmDocuments.clear();
|
|
134
|
+
goPkgAnswers.clear();
|
|
135
|
+
nugetEntries.clear();
|
|
136
|
+
repoLicensePrefetch = new Map();
|
|
137
|
+
goPkgPrefetch = new Map();
|
|
138
|
+
});
|
|
101
139
|
|
|
102
140
|
// Speed up lookup namespaces for a given jar
|
|
103
141
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
const MAX_SEARCH_MAVEN_ORG_ERRORS = 1;
|
|
142
|
+
// Maven Central's search API, used to identify a jar by its SHA-1.
|
|
143
|
+
const MAVEN_SEARCH_HOST = "central.sonatype.com";
|
|
107
144
|
|
|
108
|
-
// circuit breaker for get repo license
|
|
145
|
+
// circuit breaker for get repo license. After five failures the lookups pause
|
|
146
|
+
// for fifteen minutes, so a long-running server recovers once the API does.
|
|
109
147
|
let get_repo_license_errors = 0;
|
|
148
|
+
let get_repo_license_paused_until = 0;
|
|
110
149
|
const MAX_GET_REPO_LICENSE_ERRORS = 5;
|
|
150
|
+
const GET_REPO_LICENSE_PAUSE_MS = 15 * 60 * 1000;
|
|
111
151
|
|
|
112
152
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
153
|
+
* Whether repository licence lookups are currently paused.
|
|
154
|
+
*
|
|
155
|
+
* @returns {boolean}
|
|
116
156
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
function repoLicenseLookupsPaused() {
|
|
158
|
+
if (get_repo_license_errors < MAX_GET_REPO_LICENSE_ERRORS) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!get_repo_license_paused_until) {
|
|
162
|
+
get_repo_license_paused_until = Date.now() + GET_REPO_LICENSE_PAUSE_MS;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (Date.now() < get_repo_license_paused_until) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
get_repo_license_errors = 0;
|
|
169
|
+
get_repo_license_paused_until = 0;
|
|
170
|
+
return false;
|
|
119
171
|
}
|
|
120
172
|
|
|
121
173
|
/**
|
|
@@ -183,75 +235,286 @@ function npmRegistryKey(p) {
|
|
|
183
235
|
return p.name;
|
|
184
236
|
}
|
|
185
237
|
|
|
238
|
+
// The hosts of the public npm registry. A lockfile `resolved` URL on any other
|
|
239
|
+
// host was served by a mirror or a private registry.
|
|
240
|
+
const DEFAULT_NPM_REGISTRY_HOSTS = new Set([
|
|
241
|
+
"registry.npmjs.org",
|
|
242
|
+
"registry.npmjs.com",
|
|
243
|
+
"registry.yarnpkg.com",
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether a URL points at the public npm registry or at the registry the
|
|
248
|
+
* caller resolves against.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} url Registry or tarball URL.
|
|
251
|
+
* @param {string} defaultRegistryUrl The registry the caller resolves against.
|
|
252
|
+
* @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
function isDefaultNpmRegistry(url, defaultRegistryUrl) {
|
|
255
|
+
const host = parseUrl(url)?.host;
|
|
256
|
+
return (
|
|
257
|
+
!!host &&
|
|
258
|
+
(DEFAULT_NPM_REGISTRY_HOSTS.has(host) ||
|
|
259
|
+
host === parseUrl(defaultRegistryUrl)?.host)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The registry that served a tarball. Registries lay tarballs out as
|
|
265
|
+
* `<registry>/<name>/-/<file>`, so the registry is everything before the name.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} tarballUrl Lockfile `resolved` URL.
|
|
268
|
+
* @param {string} fullName Package name, with its scope.
|
|
269
|
+
* @returns {string|undefined} Registry base URL, or undefined when the URL has
|
|
270
|
+
* another layout.
|
|
271
|
+
*/
|
|
272
|
+
function npmTarballRegistry(tarballUrl, fullName) {
|
|
273
|
+
if (!tarballUrl.startsWith("https://") && !tarballUrl.startsWith("http://")) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const index = tarballUrl.indexOf(`/${fullName}/-/`);
|
|
277
|
+
return index > 0 ? tarballUrl.slice(0, index) : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Whether a lockfile `resolved` URL names a git source rather than a tarball.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} url Resolved URL.
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
function isNpmGitSource(url) {
|
|
287
|
+
return (
|
|
288
|
+
url.startsWith("git+") ||
|
|
289
|
+
url.startsWith("git://") ||
|
|
290
|
+
url.startsWith("git@") ||
|
|
291
|
+
url.startsWith("ssh://")
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The registry to look an npm package up at, or undefined when no registry
|
|
297
|
+
* lookup may be made for it.
|
|
298
|
+
*
|
|
299
|
+
* A public registry cannot hold the project itself, a workspace or link
|
|
300
|
+
* member, a file or git source, or a package another registry serves. Sending
|
|
301
|
+
* those names out leaks them and imports a look-alike's metadata, so the
|
|
302
|
+
* component keeps whatever the lockfile and the manifests already say.
|
|
303
|
+
*
|
|
304
|
+
* A scope that .npmrc maps to another registry, or whose tarball another
|
|
305
|
+
* registry served, is looked up at that registry. In secure mode cdxgen does
|
|
306
|
+
* not contact a host the scanned project names, so such a package is not
|
|
307
|
+
* looked up at all.
|
|
308
|
+
*
|
|
309
|
+
* @param {Object} p Package object carrying the properties the npm parsers record.
|
|
310
|
+
* @param {string} defaultRegistryUrl The registry the caller resolves against.
|
|
311
|
+
* @param {Object} [npmrcConfig] Merged .npmrc configuration, for scope mappings.
|
|
312
|
+
* @param {boolean} [secureMode] Whether secure mode is on.
|
|
313
|
+
* @returns {string|undefined} Registry base URL, or undefined to skip.
|
|
314
|
+
*/
|
|
315
|
+
function npmLookupRegistry(p, defaultRegistryUrl, npmrcConfig, secureMode) {
|
|
316
|
+
if (p.type === "application") {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
const props = {};
|
|
320
|
+
for (const prop of p.properties || []) {
|
|
321
|
+
props[prop.name] = prop.value;
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
props["cdx:npm:isWorkspace"] === "true" ||
|
|
325
|
+
props["cdx:npm:isLink"] === "true" ||
|
|
326
|
+
props["cdx:npm:isRegistryDependency"] === "false" ||
|
|
327
|
+
props["cdx:npm:resolvedPath"]
|
|
328
|
+
) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const manifestSourceType = props["cdx:npm:manifestSourceType"];
|
|
332
|
+
if (manifestSourceType && manifestSourceType !== "registry") {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const resolvedUrl =
|
|
336
|
+
props["internal:ResolvedUrl"] ||
|
|
337
|
+
p.externalReferences?.find((ref) => ref.type === "distribution")?.url;
|
|
338
|
+
if (resolvedUrl && isNpmGitSource(resolvedUrl)) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
if (p.group) {
|
|
342
|
+
const scope = p.group.startsWith("@") ? p.group : `@${p.group}`;
|
|
343
|
+
const scopeRegistry = normalizeNpmRegistryUrl(
|
|
344
|
+
npmrcConfig?.[`${scope}:registry`],
|
|
345
|
+
);
|
|
346
|
+
let registry;
|
|
347
|
+
if (scopeRegistry) {
|
|
348
|
+
registry = scopeRegistry;
|
|
349
|
+
} else if (resolvedUrl) {
|
|
350
|
+
registry = isDefaultNpmRegistry(resolvedUrl, defaultRegistryUrl)
|
|
351
|
+
? defaultRegistryUrl
|
|
352
|
+
: npmTarballRegistry(resolvedUrl, `${scope}/${p.name}`);
|
|
353
|
+
}
|
|
354
|
+
if (registry && !isDefaultNpmRegistry(registry, defaultRegistryUrl)) {
|
|
355
|
+
return secureMode ? undefined : registry;
|
|
356
|
+
}
|
|
357
|
+
if (resolvedUrl && !registry) {
|
|
358
|
+
// Served by another registry, but not in a layout that names it.
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return defaultRegistryUrl;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Whether every field the registry is asked for is already on the component.
|
|
367
|
+
*
|
|
368
|
+
* A lockfile or an installed package.json that already states the licence, the
|
|
369
|
+
* description and the repository has answered the question a packument would be
|
|
370
|
+
* asked, so the package is not looked up at all. A licence the project gave is
|
|
371
|
+
* never replaced by the registry's view even when a lookup happens for the
|
|
372
|
+
* other fields.
|
|
373
|
+
*
|
|
374
|
+
* @param {Object} p Package object
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function npmMetadataComplete(p) {
|
|
378
|
+
// A lockfile records the repository as a vcs external reference rather than
|
|
379
|
+
// the repository field the registry path sets, so both count.
|
|
380
|
+
const hasRepository =
|
|
381
|
+
p.repository?.url ||
|
|
382
|
+
(typeof p.repository === "string" && p.repository) ||
|
|
383
|
+
(p.externalReferences || []).some((ref) => ref.type === "vcs" && ref.url);
|
|
384
|
+
return Boolean(p.license && p.description && hasRepository);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The license an npm registry document declares for a version, read like the
|
|
389
|
+
* manifest it came from: the registry still serves the legacy `licenses` array
|
|
390
|
+
* of packages such as fuzzy 0.1.3 (issue 4466). The version's own manifest
|
|
391
|
+
* wins over the document-level field.
|
|
392
|
+
*
|
|
393
|
+
* @param {Object} body Registry document (packument).
|
|
394
|
+
* @param {string} version Version of the package.
|
|
395
|
+
* @returns {string|Object|Array|undefined} The declared license, if any.
|
|
396
|
+
*/
|
|
397
|
+
function npmRegistryLicense(body, version) {
|
|
398
|
+
return (
|
|
399
|
+
resolveNpmLicense(body?.versions?.[version]) || resolveNpmLicense(body)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
186
403
|
/**
|
|
187
404
|
* Method to retrieve metadata for npm packages by querying npmjs
|
|
188
405
|
*
|
|
406
|
+
* Packages a public registry cannot hold are skipped: the project itself,
|
|
407
|
+
* workspace and link members, file and git sources, and scopes served by
|
|
408
|
+
* another registry. See {@link npmLookupRegistry}. A package whose licence,
|
|
409
|
+
* description and repository the project or lockfile already supplied is left
|
|
410
|
+
* alone as well: the registry answers only what is still missing, and never
|
|
411
|
+
* overwrites a licence that is already set. The registry's licence is read
|
|
412
|
+
* with its legacy `licenses` field, and only a package for which neither the
|
|
413
|
+
* package nor the registry names one falls back to its repository's licence.
|
|
414
|
+
*
|
|
189
415
|
* @param {Array} pkgList Package list
|
|
416
|
+
* @param {string} [registryUrl] Registry to query instead of NPM_URL
|
|
417
|
+
* @param {Object} [options] Options
|
|
418
|
+
* @param {Object} [options.npmrcConfig] Merged .npmrc configuration, used to
|
|
419
|
+
* resolve a scope's own registry
|
|
420
|
+
* @param {boolean} [options.secureMode] Whether secure mode is on. Defaults to
|
|
421
|
+
* the process-wide setting.
|
|
190
422
|
*/
|
|
191
|
-
export async function getNpmMetadata(pkgList, registryUrl) {
|
|
423
|
+
export async function getNpmMetadata(pkgList, registryUrl, options = {}) {
|
|
192
424
|
const NPM_URL =
|
|
193
425
|
registryUrl ||
|
|
194
426
|
readEnvironmentVariable("NPM_URL") ||
|
|
195
427
|
"https://registry.npmjs.org/";
|
|
428
|
+
const secureMode = options.secureMode ?? isSecureMode;
|
|
196
429
|
const cdepList = [];
|
|
430
|
+
// The registry for every package is decided up front, so the batch and the
|
|
431
|
+
// loop below cannot disagree about who is skipped and who is asked where.
|
|
432
|
+
const lookupUrls = new Map();
|
|
433
|
+
// Provenance is never held locally, so when it is wanted every package is
|
|
434
|
+
// still asked about; the local fields survive the round either way.
|
|
435
|
+
const wantsProvenance = shouldFetchRegistryProvenance();
|
|
436
|
+
for (const p of pkgList) {
|
|
437
|
+
if (!wantsProvenance && npmMetadataComplete(p)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const registry = npmLookupRegistry(
|
|
441
|
+
p,
|
|
442
|
+
NPM_URL,
|
|
443
|
+
options.npmrcConfig,
|
|
444
|
+
secureMode,
|
|
445
|
+
);
|
|
446
|
+
if (registry) {
|
|
447
|
+
const base = registry.endsWith("/") ? registry : `${registry}/`;
|
|
448
|
+
lookupUrls.set(p, base + npmRegistryKey(p));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
197
451
|
// Every URL this loop needs is known up front, so they are fetched
|
|
198
452
|
// concurrently before the loop rather than one at a time inside it. The
|
|
199
453
|
// derivation below is unchanged and still reads `body`; only where `body`
|
|
200
|
-
// comes from differs.
|
|
454
|
+
// comes from differs. A packument this run already holds, from another
|
|
455
|
+
// lockfile or an earlier pass, is not fetched again.
|
|
201
456
|
const prefetched = await prefetchJson(
|
|
202
457
|
prefetchEnabled()
|
|
203
|
-
?
|
|
458
|
+
? [...lookupUrls.values()]
|
|
459
|
+
.filter((url) => !npmDocuments.has(url))
|
|
460
|
+
.map((url) => ({ url }))
|
|
204
461
|
: [],
|
|
205
462
|
);
|
|
206
463
|
// The licence fallback calls getRepoLicense with a URL that only exists once
|
|
207
464
|
// the registry document has arrived, so it needs a second batched round. The
|
|
208
465
|
// documents are already in hand here, which is why this can be done without a
|
|
209
466
|
// second pass over the network for the registry itself.
|
|
210
|
-
if (prefetched.size) {
|
|
467
|
+
if (prefetched.size || npmDocuments.size) {
|
|
211
468
|
const repoUrls = [];
|
|
212
469
|
for (const p of pkgList) {
|
|
213
|
-
const
|
|
214
|
-
|
|
470
|
+
const url = lookupUrls.get(p);
|
|
471
|
+
const entry = url ? prefetched.get(url) : undefined;
|
|
472
|
+
const body = entry?.ok ? entry.body : npmDocuments.get(url);
|
|
473
|
+
if (!body) {
|
|
215
474
|
continue;
|
|
216
475
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
476
|
+
if (
|
|
477
|
+
!p.license &&
|
|
478
|
+
!npmRegistryLicense(body, p.version) &&
|
|
479
|
+
body?.repository?.url
|
|
480
|
+
) {
|
|
220
481
|
repoUrls.push(body.repository.url);
|
|
221
482
|
}
|
|
222
483
|
}
|
|
223
484
|
await prefetchRepoLicenses(repoUrls);
|
|
224
485
|
}
|
|
225
486
|
for (const p of pkgList) {
|
|
487
|
+
const url = lookupUrls.get(p);
|
|
488
|
+
if (!url) {
|
|
489
|
+
cdepList.push(p);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
226
492
|
try {
|
|
227
|
-
const key = npmRegistryKey(p);
|
|
228
493
|
// Namespace the cache by registry so packages resolved from a
|
|
229
494
|
// non-default registry (e.g. jsr's npm mirror at npm.jsr.io) do not
|
|
230
495
|
// collide with identically-named packages on the default registry.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (metadata_cache[cacheKey]) {
|
|
234
|
-
body = metadata_cache[cacheKey];
|
|
235
|
-
} else {
|
|
236
|
-
const url = NPM_URL + key;
|
|
496
|
+
let body = npmDocuments.get(url);
|
|
497
|
+
if (!body) {
|
|
237
498
|
const res =
|
|
238
499
|
prefetchedResponse(prefetched, url) ||
|
|
239
|
-
(await
|
|
240
|
-
responseType: "json",
|
|
241
|
-
}));
|
|
500
|
+
(await gatedGet(url, { responseType: "json" }));
|
|
242
501
|
body = res.body;
|
|
243
|
-
|
|
502
|
+
npmDocuments.set(url, body);
|
|
244
503
|
}
|
|
504
|
+
// The registry fills only what the project, the lockfile or an
|
|
505
|
+
// installed manifest left open, so a local answer survives.
|
|
245
506
|
p.description =
|
|
246
|
-
|
|
507
|
+
p.description ||
|
|
508
|
+
body.versions?.[p.version]?.description ||
|
|
509
|
+
body.description;
|
|
247
510
|
p.license =
|
|
248
|
-
|
|
249
|
-
body.
|
|
511
|
+
p.license ||
|
|
512
|
+
npmRegistryLicense(body, p.version) ||
|
|
250
513
|
(await getRepoLicense(body.repository?.url, undefined));
|
|
251
|
-
if (body.repository?.url) {
|
|
514
|
+
if (body.repository?.url && !p.repository) {
|
|
252
515
|
p.repository = { url: body.repository.url };
|
|
253
516
|
}
|
|
254
|
-
if (body.homepage) {
|
|
517
|
+
if (body.homepage && !p.homepage) {
|
|
255
518
|
p.homepage = { url: body.homepage };
|
|
256
519
|
}
|
|
257
520
|
// Capture the resolved tarball as a distribution external reference when
|
|
@@ -296,445 +559,604 @@ export async function getNpmMetadata(pkgList, registryUrl) {
|
|
|
296
559
|
* @returns {Object|null} Object containing jarPath, sha1, and pomPath, or null
|
|
297
560
|
*/
|
|
298
561
|
export function findLocalMvnArtifact(group, name, version) {
|
|
299
|
-
|
|
562
|
+
const found = findLocalMavenArtifact(group, name, version);
|
|
563
|
+
if (!found) {
|
|
300
564
|
return null;
|
|
301
565
|
}
|
|
566
|
+
return {
|
|
567
|
+
jarPath: found.jarPath || null,
|
|
568
|
+
sha1: found.sha1 || null,
|
|
569
|
+
pomPath: found.pomPath || null,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
302
572
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
573
|
+
// Options for reading a POM with xml2js, shared by every POM reader here.
|
|
574
|
+
const POM_XML_OPTIONS = {
|
|
575
|
+
compact: true,
|
|
576
|
+
spaces: 4,
|
|
577
|
+
textKey: "_",
|
|
578
|
+
attributesKey: "$",
|
|
579
|
+
commentKey: "value",
|
|
580
|
+
};
|
|
310
581
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (existsSync(gradlePath)) {
|
|
314
|
-
try {
|
|
315
|
-
const subdirs = readdirSync(gradlePath);
|
|
316
|
-
let jarPath = null;
|
|
317
|
-
let sha1 = null;
|
|
318
|
-
let pomPath = null;
|
|
319
|
-
for (const subdir of subdirs) {
|
|
320
|
-
const subdirPath = join(gradlePath, subdir);
|
|
321
|
-
if (statSync(subdirPath).isDirectory()) {
|
|
322
|
-
const files = readdirSync(subdirPath);
|
|
323
|
-
for (const file of files) {
|
|
324
|
-
if (
|
|
325
|
-
file.endsWith(".jar") &&
|
|
326
|
-
!file.endsWith("-sources.jar") &&
|
|
327
|
-
!file.endsWith("-javadoc.jar")
|
|
328
|
-
) {
|
|
329
|
-
jarPath = join(subdirPath, file);
|
|
330
|
-
sha1 = subdir;
|
|
331
|
-
} else if (file.endsWith(".pom")) {
|
|
332
|
-
pomPath = join(subdirPath, file);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (jarPath || pomPath) {
|
|
338
|
-
return { jarPath, sha1, pomPath };
|
|
339
|
-
}
|
|
340
|
-
} catch (_err) {
|
|
341
|
-
// ignore
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
582
|
+
// A POM chain is walked at most this deep. Real hierarchies stop well short.
|
|
583
|
+
const MAX_POM_PARENT_DEPTH = 8;
|
|
345
584
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
585
|
+
// Groups published to Google's Maven repository rather than Maven Central.
|
|
586
|
+
const GOOGLE_MAVEN_GROUP_PREFIXES = [
|
|
587
|
+
"android.arch.",
|
|
588
|
+
"androidx.",
|
|
589
|
+
"com.android.",
|
|
590
|
+
"com.google.ads.mediation",
|
|
591
|
+
"com.google.android.",
|
|
592
|
+
"com.google.ar",
|
|
593
|
+
"com.google.firebase",
|
|
594
|
+
"com.google.gms",
|
|
595
|
+
"com.google.mlkit",
|
|
596
|
+
"com.google.testing.platform",
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A repository base URL with exactly one trailing slash, ready for
|
|
601
|
+
* {@link composePomXmlUrl}.
|
|
602
|
+
*
|
|
603
|
+
* @param {string} url Repository URL.
|
|
604
|
+
* @returns {string} Normalised URL.
|
|
605
|
+
*/
|
|
606
|
+
function withTrailingSlash(url) {
|
|
607
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The repository a package's POM should be fetched from, or undefined when no
|
|
612
|
+
* public repository can hold it: snapshots never reach Maven Central, the
|
|
613
|
+
* project's own modules are not published, and a file:// repository is local.
|
|
614
|
+
*
|
|
615
|
+
* @param {Object} p Package.
|
|
616
|
+
* @param {{MAVEN_CENTRAL_URL: string, ANDROID_MAVEN_URL: string}} repos Repository base URLs.
|
|
617
|
+
* @param {{skipPurls?: Set<string>}} context Packages the caller knows are its own.
|
|
618
|
+
* @returns {string|undefined} Repository base URL.
|
|
619
|
+
*/
|
|
620
|
+
function mavenRepositoryFor(p, repos, context) {
|
|
621
|
+
const group = p.group || "";
|
|
622
|
+
if (!group || !p.name || !p.version || p.version.endsWith("-SNAPSHOT")) {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
if (
|
|
626
|
+
context?.skipPurls?.has(p.purl) ||
|
|
627
|
+
context?.skipPurls?.has(p["bom-ref"])
|
|
628
|
+
) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
let repositoryUrl;
|
|
632
|
+
const query = p.purl?.includes("?") ? p.purl.split("?")[1] : "";
|
|
633
|
+
if (query) {
|
|
634
|
+
repositoryUrl = new URLSearchParams(query.split("#")[0]).get(
|
|
635
|
+
"repository_url",
|
|
636
|
+
);
|
|
366
637
|
}
|
|
638
|
+
if (repositoryUrl?.startsWith("file:")) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
let repositoryHost;
|
|
642
|
+
try {
|
|
643
|
+
repositoryHost = repositoryUrl ? new URL(repositoryUrl).hostname : "";
|
|
644
|
+
} catch (_err) {
|
|
645
|
+
repositoryHost = "";
|
|
646
|
+
}
|
|
647
|
+
if (
|
|
648
|
+
["maven.google.com", "dl.google.com"].includes(repositoryHost) ||
|
|
649
|
+
group.includes("android") ||
|
|
650
|
+
GOOGLE_MAVEN_GROUP_PREFIXES.some(
|
|
651
|
+
(prefix) => group === prefix || group.startsWith(prefix),
|
|
652
|
+
)
|
|
653
|
+
) {
|
|
654
|
+
return repos.ANDROID_MAVEN_URL;
|
|
655
|
+
}
|
|
656
|
+
return repos.MAVEN_CENTRAL_URL;
|
|
657
|
+
}
|
|
367
658
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
homedir(),
|
|
380
|
-
"AppData",
|
|
381
|
-
"Local",
|
|
382
|
-
"Coursier",
|
|
383
|
-
"Cache",
|
|
384
|
-
"v1",
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
} else {
|
|
388
|
-
cacheRoot = join(homedir(), ".cache", "coursier", "v1");
|
|
389
|
-
}
|
|
659
|
+
/**
|
|
660
|
+
* Whether a package already carries licence information, in either the
|
|
661
|
+
* `license` field cdxgen fills or the CycloneDX `licenses` array that the
|
|
662
|
+
* cyclonedx-maven-plugin and Quarkus emit.
|
|
663
|
+
*
|
|
664
|
+
* @param {Object} p Package.
|
|
665
|
+
* @returns {boolean}
|
|
666
|
+
*/
|
|
667
|
+
function hasMavenLicense(p) {
|
|
668
|
+
if (Array.isArray(p.license)) {
|
|
669
|
+
return p.license.some(Boolean);
|
|
390
670
|
}
|
|
671
|
+
return Boolean(p.license) || Boolean(p.licenses?.length);
|
|
672
|
+
}
|
|
391
673
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
!match.endsWith("-javadoc.jar")
|
|
405
|
-
) {
|
|
406
|
-
const localPath = join(cacheRoot, match);
|
|
407
|
-
if (existsSync(localPath)) {
|
|
408
|
-
jarPath = localPath;
|
|
409
|
-
}
|
|
410
|
-
} else if (match.endsWith(".pom")) {
|
|
411
|
-
const localPath = join(cacheRoot, match);
|
|
412
|
-
if (existsSync(localPath)) {
|
|
413
|
-
pomPath = localPath;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (jarPath || pomPath) {
|
|
418
|
-
return { jarPath, pomPath };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} catch (_err) {
|
|
422
|
-
// ignore
|
|
423
|
-
}
|
|
674
|
+
/**
|
|
675
|
+
* Set the licences a jar manifest's Bundle-License names on a package: one
|
|
676
|
+
* value as is, several as a list.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} p Package.
|
|
679
|
+
* @param {string[]|undefined} licenses Values from parseManifestLicenseList.
|
|
680
|
+
*/
|
|
681
|
+
function applyManifestLicenses(p, licenses) {
|
|
682
|
+
if (licenses?.length === 1) {
|
|
683
|
+
p.license = licenses[0];
|
|
684
|
+
} else if (licenses?.length > 1) {
|
|
685
|
+
p.license = licenses;
|
|
424
686
|
}
|
|
687
|
+
}
|
|
425
688
|
|
|
426
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Collapse the whitespace a POM description carries over from XML indentation.
|
|
691
|
+
*
|
|
692
|
+
* @param {string} description Raw description.
|
|
693
|
+
* @returns {string} Tidied description.
|
|
694
|
+
*/
|
|
695
|
+
function tidyPomDescription(description) {
|
|
696
|
+
return description
|
|
697
|
+
.replace(/[ \t]+/g, " ")
|
|
698
|
+
.replace(/^[ \t]+|[ \t]+$/gm, "")
|
|
699
|
+
.replace(/\n\s*\n/g, "\n")
|
|
700
|
+
.trim();
|
|
427
701
|
}
|
|
428
702
|
|
|
429
703
|
/**
|
|
430
|
-
*
|
|
704
|
+
* The licence a POM names in a comment ahead of its project element, as some
|
|
705
|
+
* projects do instead of declaring `<licenses>`.
|
|
431
706
|
*
|
|
432
|
-
* @param {
|
|
433
|
-
* @
|
|
434
|
-
|
|
707
|
+
* @param {string} pomXml POM text.
|
|
708
|
+
* @returns {string|undefined} Licence id.
|
|
709
|
+
*/
|
|
710
|
+
function licenseFromPomComment(pomXml) {
|
|
711
|
+
const match = /<!--([\s\S]*?)-->[\s\n]*<project/m.exec(pomXml || "");
|
|
712
|
+
return match?.[1] ? findLicenseId(match[1].trim()) : undefined;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Fill publisher, description, repository and licence from a merged POM,
|
|
717
|
+
* keeping any value the package already has.
|
|
435
718
|
*
|
|
436
|
-
* @
|
|
719
|
+
* @param {Object} p Package.
|
|
720
|
+
* @param {Object} pomJson Merged POM (xml2js compact form).
|
|
721
|
+
* @param {string} [ownPomXml] The package's own POM text, for a licence comment.
|
|
722
|
+
* @returns {void}
|
|
437
723
|
*/
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
force = false,
|
|
442
|
-
) {
|
|
443
|
-
const MAVEN_CENTRAL_URL =
|
|
444
|
-
readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
|
|
445
|
-
"https://repo1.maven.org/maven2/";
|
|
446
|
-
const ANDROID_MAVEN_URL =
|
|
447
|
-
readEnvironmentVariable("ANDROID_MAVEN_URL") || "https://maven.google.com/";
|
|
448
|
-
const cdepList = [];
|
|
449
|
-
if (!pkgList?.length) {
|
|
450
|
-
return pkgList;
|
|
724
|
+
function applyPomMetadata(p, pomJson, ownPomXml) {
|
|
725
|
+
if (!pomJson) {
|
|
726
|
+
return;
|
|
451
727
|
}
|
|
452
|
-
if (
|
|
453
|
-
|
|
728
|
+
if (!p.publisher && pomJson.organization?.name?._) {
|
|
729
|
+
p.publisher = pomJson.organization.name._;
|
|
454
730
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// inside fetchPomXmlAsJson) depend on the direct POM's content and stay
|
|
458
|
-
// serial. The conditions below mirror the loop's skip checks so the batch
|
|
459
|
-
// does not request anything the loop would skip.
|
|
460
|
-
const batchUrls = [];
|
|
461
|
-
const seenPomUrls = new Set();
|
|
462
|
-
for (const p of pkgList) {
|
|
463
|
-
const group = p.group || "";
|
|
464
|
-
if (
|
|
465
|
-
!p.version ||
|
|
466
|
-
(group && p.name && p.version && !shouldFetchLicense() && !force) ||
|
|
467
|
-
(group && p.name && p.version && p.license)
|
|
468
|
-
) {
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
if (!group || group === "") {
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
const urlPrefix =
|
|
475
|
-
group.indexOf("android") !== -1 ? ANDROID_MAVEN_URL : MAVEN_CENTRAL_URL;
|
|
476
|
-
const pomUrl = composePomXmlUrl({
|
|
477
|
-
urlPrefix,
|
|
478
|
-
group,
|
|
479
|
-
name: p.name,
|
|
480
|
-
version: p.version,
|
|
481
|
-
});
|
|
482
|
-
if (!seenPomUrls.has(pomUrl)) {
|
|
483
|
-
seenPomUrls.add(pomUrl);
|
|
484
|
-
batchUrls.push({ url: pomUrl, responseType: "text" });
|
|
485
|
-
}
|
|
731
|
+
if (!p.description && pomJson.description?._) {
|
|
732
|
+
p.description = tidyPomDescription(pomJson.description._);
|
|
486
733
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
repoUrls.push(scmUrl);
|
|
497
|
-
}
|
|
734
|
+
if (!p.repository && pomJson.scm?.url?._) {
|
|
735
|
+
p.repository = { url: pomJson.scm.url._ };
|
|
736
|
+
}
|
|
737
|
+
if (!hasMavenLicense(p)) {
|
|
738
|
+
const license =
|
|
739
|
+
parseLicenseEntryOrArrayFromPomXml(pomJson.licenses?.license) ||
|
|
740
|
+
licenseFromPomComment(ownPomXml);
|
|
741
|
+
if (license) {
|
|
742
|
+
p.license = license;
|
|
498
743
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Read a POM from the local Maven, Gradle or Coursier cache.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} group groupId.
|
|
751
|
+
* @param {string} name artifactId.
|
|
752
|
+
* @param {string} version Version.
|
|
753
|
+
* @returns {string|undefined} POM text.
|
|
754
|
+
*/
|
|
755
|
+
function readLocalPom(group, name, version) {
|
|
756
|
+
const pomPath = findLocalMavenArtifact(group, name, version)?.pomPath;
|
|
757
|
+
if (!pomPath) {
|
|
758
|
+
return undefined;
|
|
505
759
|
}
|
|
506
760
|
try {
|
|
507
|
-
return
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
});
|
|
511
|
-
} finally {
|
|
512
|
-
// Each body is read once, by the loop below. Holding them past that only
|
|
513
|
-
// grows the process, which matters in server mode where one process serves
|
|
514
|
-
// many scans. Only the documents this call fetched are dropped, so a scan
|
|
515
|
-
// running alongside this one keeps its own.
|
|
516
|
-
for (const pomUrl of ownPomUrls) {
|
|
517
|
-
prefetchedPoms.delete(pomUrl);
|
|
518
|
-
}
|
|
761
|
+
return readFileSync(pomPath, "utf-8");
|
|
762
|
+
} catch (_err) {
|
|
763
|
+
return undefined;
|
|
519
764
|
}
|
|
520
765
|
}
|
|
521
766
|
|
|
522
767
|
/**
|
|
523
|
-
*
|
|
524
|
-
* by {@link getMvnMetadata}'s batch round.
|
|
768
|
+
* Load a POM and its parents and merge them child over parent.
|
|
525
769
|
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
770
|
+
* Every level is read from the local caches first; a build has always
|
|
771
|
+
* downloaded the parents it needed. A level missing locally is fetched only
|
|
772
|
+
* when `remote` is set, and at most `remoteParents` parent levels are fetched,
|
|
773
|
+
* so a cache miss costs no more requests than it always did. The walk stops
|
|
774
|
+
* once licence, organization and scm are all known. A missing parent ends the
|
|
775
|
+
* walk without discarding what the child and nearer parents declared.
|
|
776
|
+
*
|
|
777
|
+
* @param {{urlPrefix?: string, group: string, name: string, version: string}} coordinates
|
|
778
|
+
* @param {{remote?: boolean, remoteParents?: number}} [opts]
|
|
779
|
+
* @returns {Promise<{merged: Object, ownXml: string}|undefined>}
|
|
533
780
|
*/
|
|
534
|
-
async function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
jarNSMapping,
|
|
538
|
-
force,
|
|
539
|
-
{ MAVEN_CENTRAL_URL, ANDROID_MAVEN_URL },
|
|
781
|
+
async function loadPomChain(
|
|
782
|
+
coordinates,
|
|
783
|
+
{ remote = false, remoteParents = 1 } = {},
|
|
540
784
|
) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
},
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
if (jarNSMapping[p.purl].hashes && !p?.hashes?.length) {
|
|
560
|
-
p.hashes = jarNSMapping[p.purl].hashes;
|
|
561
|
-
}
|
|
562
|
-
if (
|
|
563
|
-
jarNSMapping[p.purl].namespaces &&
|
|
564
|
-
jarNSMapping[p.purl].namespaces.length
|
|
565
|
-
) {
|
|
566
|
-
if (!p.properties) {
|
|
567
|
-
p.properties = [];
|
|
568
|
-
}
|
|
569
|
-
p.properties.push({
|
|
570
|
-
name: "internal:Namespaces",
|
|
571
|
-
value: jarNSMapping[p.purl].namespaces.join("\n"),
|
|
572
|
-
});
|
|
785
|
+
const chain = [];
|
|
786
|
+
let current = coordinates;
|
|
787
|
+
let remoteParentAttempts = 0;
|
|
788
|
+
for (let depth = 0; current && depth < MAX_POM_PARENT_DEPTH; depth++) {
|
|
789
|
+
let xml = readLocalPom(current.group, current.name, current.version);
|
|
790
|
+
if (
|
|
791
|
+
!xml &&
|
|
792
|
+
remote &&
|
|
793
|
+
coordinates.urlPrefix &&
|
|
794
|
+
(depth === 0 || remoteParentAttempts < remoteParents)
|
|
795
|
+
) {
|
|
796
|
+
if (depth > 0) {
|
|
797
|
+
remoteParentAttempts++;
|
|
573
798
|
}
|
|
799
|
+
xml = await fetchRemotePomXml({
|
|
800
|
+
urlPrefix: coordinates.urlPrefix,
|
|
801
|
+
group: current.group,
|
|
802
|
+
name: current.name,
|
|
803
|
+
version: current.version,
|
|
804
|
+
});
|
|
574
805
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (localArtifact.jarPath && (!p.hashes || p.hashes.length === 0)) {
|
|
578
|
-
try {
|
|
579
|
-
const hashValues = await multiChecksumFile(
|
|
580
|
-
["md5", "sha1", "sha256", "sha512"],
|
|
581
|
-
localArtifact.jarPath,
|
|
582
|
-
);
|
|
583
|
-
p.hashes = [
|
|
584
|
-
{ alg: "MD5", content: hashValues["md5"] },
|
|
585
|
-
{ alg: "SHA-1", content: hashValues["sha1"] },
|
|
586
|
-
{ alg: "SHA-256", content: hashValues["sha256"] },
|
|
587
|
-
{ alg: "SHA-512", content: hashValues["sha512"] },
|
|
588
|
-
];
|
|
589
|
-
} catch (_err) {
|
|
590
|
-
if (localArtifact.sha1) {
|
|
591
|
-
p.hashes = [{ alg: "SHA-1", content: localArtifact.sha1 }];
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
if (localArtifact.pomPath) {
|
|
596
|
-
try {
|
|
597
|
-
const xmlData = readFileSync(localArtifact.pomPath, "utf-8");
|
|
598
|
-
const pomData = parsePomXml(xmlData);
|
|
599
|
-
if (pomData) {
|
|
600
|
-
if (!p.publisher && pomData.organization?.name?._) {
|
|
601
|
-
p.publisher = pomData.organization.name._;
|
|
602
|
-
}
|
|
603
|
-
if (!p.description && pomData.description) {
|
|
604
|
-
p.description = pomData.description
|
|
605
|
-
.replace(/[ \t]+/g, " ")
|
|
606
|
-
.replace(/^[ \t]+|[ \t]+$/gm, "")
|
|
607
|
-
.replace(/\n\s*\n/g, "\n")
|
|
608
|
-
.trim();
|
|
609
|
-
}
|
|
610
|
-
if (!p.repository && pomData.scm) {
|
|
611
|
-
p.repository = { url: pomData.scm };
|
|
612
|
-
}
|
|
613
|
-
if (!p.license) {
|
|
614
|
-
const parsedLicense = parseLicenseEntryOrArrayFromPomXml(
|
|
615
|
-
pomData.licenses,
|
|
616
|
-
);
|
|
617
|
-
if (parsedLicense) {
|
|
618
|
-
p.license = parsedLicense;
|
|
619
|
-
} else {
|
|
620
|
-
const licenseRegex = /<!--([\s\S]*?)-->[\s\n]*<project/m;
|
|
621
|
-
const match = licenseRegex.exec(xmlData);
|
|
622
|
-
if (match?.[1]) {
|
|
623
|
-
const commentLicense = findLicenseId(match[1].trim());
|
|
624
|
-
if (commentLicense) {
|
|
625
|
-
p.license = commentLicense;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
} catch (_err) {
|
|
632
|
-
// ignore
|
|
633
|
-
}
|
|
634
|
-
}
|
|
806
|
+
if (!xml) {
|
|
807
|
+
break;
|
|
635
808
|
}
|
|
636
|
-
|
|
637
|
-
|
|
809
|
+
let json;
|
|
810
|
+
try {
|
|
811
|
+
json = xml2js(xml, POM_XML_OPTIONS).project;
|
|
812
|
+
} catch (_err) {
|
|
813
|
+
json = undefined;
|
|
814
|
+
}
|
|
815
|
+
if (!json) {
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
chain.push({ json, xml });
|
|
819
|
+
const merged = mergePomChain(chain);
|
|
638
820
|
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
821
|
+
parseLicenseEntryOrArrayFromPomXml(merged.licenses?.license) &&
|
|
822
|
+
merged.organization?.name?._ &&
|
|
823
|
+
merged.scm?.url?._
|
|
642
824
|
) {
|
|
643
|
-
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
const parent = json.parent;
|
|
828
|
+
current =
|
|
829
|
+
parent?.groupId?._ && parent?.artifactId?._ && parent?.version?._
|
|
830
|
+
? {
|
|
831
|
+
group: parent.groupId._,
|
|
832
|
+
name: parent.artifactId._,
|
|
833
|
+
version: parent.version._,
|
|
834
|
+
}
|
|
835
|
+
: undefined;
|
|
836
|
+
}
|
|
837
|
+
if (!chain.length) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
return { merged: mergePomChain(chain), ownXml: chain[0].xml };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Merge POM levels so that nearer levels win, as Maven inheritance does for
|
|
845
|
+
* the elements cdxgen reads.
|
|
846
|
+
*
|
|
847
|
+
* @param {Array<{json: Object}>} chain Levels, child first.
|
|
848
|
+
* @returns {Object} Merged POM.
|
|
849
|
+
*/
|
|
850
|
+
function mergePomChain(chain) {
|
|
851
|
+
let merged = {};
|
|
852
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
853
|
+
merged = { ...merged, ...chain[i].json };
|
|
854
|
+
}
|
|
855
|
+
return merged;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* A lookup of the jar namespace entry for a package. The jar data is keyed by the
|
|
860
|
+
* POM coordinates of the jar, while sbt (and the Mill and scala-cli paths)
|
|
861
|
+
* emit components with the Scala binary suffix stripped and a repository_url
|
|
862
|
+
* qualifier. Both spellings resolve to the same qualifier-free coordinate
|
|
863
|
+
* key, with the exact purl still winning when it matches. A cache can hold
|
|
864
|
+
* the same library for several Scala versions (`foo_2.13` and `foo_3`); the
|
|
865
|
+
* jar whose binary version matches the component's cdx:scala:compilerVersion
|
|
866
|
+
* speaks for it, and when that cannot be told only the namespaces, which the
|
|
867
|
+
* cross builds share, are taken.
|
|
868
|
+
*
|
|
869
|
+
* @param {Object} jarNSMapping Namespace and hash data from jar analysis.
|
|
870
|
+
* @returns {(p: Object) => {entry?: Object, exact?: Boolean}} Entry lookup.
|
|
871
|
+
*/
|
|
872
|
+
function scalaJarEntryLookup(jarNSMapping) {
|
|
873
|
+
const coordinateIndex = new Map();
|
|
874
|
+
for (const apurl of Object.keys(jarNSMapping || {})) {
|
|
875
|
+
const coordinate = scalaCoordinateOfPurl(apurl);
|
|
876
|
+
if (!coordinate) {
|
|
644
877
|
continue;
|
|
645
878
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
879
|
+
if (!coordinateIndex.has(coordinate.key)) {
|
|
880
|
+
coordinateIndex.set(coordinate.key, new Map());
|
|
881
|
+
}
|
|
882
|
+
const byBinaryVersion = coordinateIndex.get(coordinate.key);
|
|
883
|
+
const binaryVersion = coordinate.binaryVersion || "";
|
|
884
|
+
if (!byBinaryVersion.has(binaryVersion)) {
|
|
885
|
+
byBinaryVersion.set(binaryVersion, apurl);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return (pkg) => {
|
|
889
|
+
if (!pkg.purl) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
if (jarNSMapping[pkg.purl]) {
|
|
893
|
+
return { entry: jarNSMapping[pkg.purl], exact: true };
|
|
894
|
+
}
|
|
895
|
+
const byBinaryVersion = coordinateIndex.get(
|
|
896
|
+
scalaCoordinateOfPurl(pkg.purl)?.key,
|
|
897
|
+
);
|
|
898
|
+
if (!byBinaryVersion) {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
const wanted = pkg.properties?.find(
|
|
902
|
+
(prop) => prop.name === "cdx:scala:compilerVersion",
|
|
903
|
+
)?.value;
|
|
904
|
+
if (wanted && byBinaryVersion.has(wanted)) {
|
|
905
|
+
return { entry: jarNSMapping[byBinaryVersion.get(wanted)], exact: true };
|
|
906
|
+
}
|
|
907
|
+
const candidates = [...byBinaryVersion.values()];
|
|
908
|
+
return {
|
|
909
|
+
entry: jarNSMapping[candidates[0]],
|
|
910
|
+
exact: candidates.length === 1,
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Enrich one package from data already on disk: the jar namespace map, the
|
|
917
|
+
* POM that jar analysis parsed, the local jar's hashes and the local POM
|
|
918
|
+
* chain. No request is made.
|
|
919
|
+
*
|
|
920
|
+
* @param {Object} p Package.
|
|
921
|
+
* @param {Function} jarNSEntryFor Jar namespace entry lookup for a package.
|
|
922
|
+
* @returns {Promise<void>}
|
|
923
|
+
*/
|
|
924
|
+
async function enrichMvnPackageLocally(p, jarNSEntryFor) {
|
|
925
|
+
const { entry: nsEntry, exact } = jarNSEntryFor(p) || {};
|
|
926
|
+
if (nsEntry) {
|
|
927
|
+
// A jar copied into a temporary directory, as Maven --deep copies the
|
|
928
|
+
// dependencies, is deleted with the scan, and its path would make the BOM
|
|
929
|
+
// differ between two runs. The identity the dependency tree gave the
|
|
930
|
+
// package stands.
|
|
931
|
+
if (nsEntry.jarFile && exact && !isInsideTmpDir(nsEntry.jarFile)) {
|
|
932
|
+
p.evidence = {
|
|
933
|
+
identity: {
|
|
934
|
+
field: "purl",
|
|
935
|
+
confidence: 0.8,
|
|
936
|
+
methods: [
|
|
937
|
+
{
|
|
938
|
+
technique: "binary-analysis",
|
|
939
|
+
confidence: 0.8,
|
|
940
|
+
value: nsEntry.jarFile,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (nsEntry.hashes && exact && !p?.hashes?.length) {
|
|
947
|
+
p.hashes = nsEntry.hashes;
|
|
650
948
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
949
|
+
if (nsEntry.namespaces?.length) {
|
|
950
|
+
if (!p.properties) {
|
|
951
|
+
p.properties = [];
|
|
952
|
+
}
|
|
953
|
+
p.properties.push({
|
|
954
|
+
name: "internal:Namespaces",
|
|
955
|
+
value: nsEntry.namespaces.join("\n"),
|
|
956
|
+
});
|
|
655
957
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
};
|
|
662
|
-
try {
|
|
663
|
-
if (DEBUG_MODE) {
|
|
664
|
-
console.log(
|
|
665
|
-
`Querying ${pomMetadata.urlPrefix} for '${group}/${p.name}@${p.version}' ${composePomXmlUrl(
|
|
666
|
-
pomMetadata,
|
|
667
|
-
)}`,
|
|
668
|
-
);
|
|
958
|
+
// The POM jar analysis already parsed.
|
|
959
|
+
const nsPom = nsEntry.pom;
|
|
960
|
+
if (nsPom) {
|
|
961
|
+
if (!p.publisher && nsPom.organization?.name?._) {
|
|
962
|
+
p.publisher = nsPom.organization.name._;
|
|
669
963
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
: "";
|
|
681
|
-
if (bodyJson?.scm?.url) {
|
|
682
|
-
p.repository = { url: bodyJson.scm.url._ };
|
|
964
|
+
if (!p.description && nsPom.description) {
|
|
965
|
+
p.description = tidyPomDescription(nsPom.description);
|
|
966
|
+
}
|
|
967
|
+
if (!p.repository && nsPom.scm) {
|
|
968
|
+
p.repository = { url: nsPom.scm };
|
|
969
|
+
}
|
|
970
|
+
if (!hasMavenLicense(p)) {
|
|
971
|
+
const license = parseLicenseEntryOrArrayFromPomXml(nsPom.licenses);
|
|
972
|
+
if (license) {
|
|
973
|
+
p.license = license;
|
|
683
974
|
}
|
|
684
|
-
p.license =
|
|
685
|
-
parseLicenseEntryOrArrayFromPomXml(bodyJson?.licenses?.license) ||
|
|
686
|
-
(await extractLicenseCommentFromPomXml(pomMetadata)) ||
|
|
687
|
-
(await getRepoLicense(p.repository?.url, undefined));
|
|
688
975
|
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
976
|
+
}
|
|
977
|
+
// The jar's manifest names its licences even when no POM was parsed. A
|
|
978
|
+
// licence the package already has always wins.
|
|
979
|
+
if (!hasMavenLicense(p)) {
|
|
980
|
+
applyManifestLicenses(p, nsEntry.manifestLicenses);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Scala components are named without their Scala binary suffix, while the
|
|
984
|
+
// published artifact carries it. The published name is looked up first and
|
|
985
|
+
// the component name is the local fallback; a disk read costs nothing.
|
|
986
|
+
const publishedName = publishedArtifactId(p);
|
|
987
|
+
const primaryArtifact = findLocalMvnArtifact(
|
|
988
|
+
p.group,
|
|
989
|
+
publishedName,
|
|
990
|
+
p.version,
|
|
991
|
+
);
|
|
992
|
+
const localArtifact =
|
|
993
|
+
primaryArtifact ||
|
|
994
|
+
(publishedName !== p.name
|
|
995
|
+
? findLocalMvnArtifact(p.group, p.name, p.version)
|
|
996
|
+
: null);
|
|
997
|
+
const pomChainName = primaryArtifact ? publishedName : p.name;
|
|
998
|
+
if (localArtifact?.jarPath && (!p.hashes || p.hashes.length === 0)) {
|
|
999
|
+
try {
|
|
1000
|
+
const hashValues = await multiChecksumFile(
|
|
1001
|
+
["md5", "sha1", "sha256", "sha512"],
|
|
1002
|
+
localArtifact.jarPath,
|
|
1003
|
+
);
|
|
1004
|
+
p.hashes = [
|
|
1005
|
+
{ alg: "MD5", content: hashValues["md5"] },
|
|
1006
|
+
{ alg: "SHA-1", content: hashValues["sha1"] },
|
|
1007
|
+
{ alg: "SHA-256", content: hashValues["sha256"] },
|
|
1008
|
+
{ alg: "SHA-512", content: hashValues["sha512"] },
|
|
1009
|
+
];
|
|
1010
|
+
} catch (_err) {
|
|
1011
|
+
if (localArtifact.sha1) {
|
|
1012
|
+
p.hashes = [{ alg: "SHA-1", content: localArtifact.sha1 }];
|
|
695
1013
|
}
|
|
696
|
-
} finally {
|
|
697
|
-
cdepList.push(p);
|
|
698
1014
|
}
|
|
699
1015
|
}
|
|
700
|
-
|
|
1016
|
+
if (localArtifact?.pomPath) {
|
|
1017
|
+
const chain = await loadPomChain(
|
|
1018
|
+
{ group: p.group, name: pomChainName, version: p.version },
|
|
1019
|
+
{ remote: false },
|
|
1020
|
+
);
|
|
1021
|
+
applyPomMetadata(p, chain?.merged, chain?.ownXml);
|
|
1022
|
+
}
|
|
701
1023
|
}
|
|
702
1024
|
|
|
703
1025
|
/**
|
|
704
|
-
*
|
|
1026
|
+
* Method to retrieve metadata for maven packages, from the local caches first
|
|
1027
|
+
* and from Maven Central only for what remains.
|
|
705
1028
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
1029
|
+
* Every package is first enriched from data on disk. Only when license
|
|
1030
|
+
* fetching is enabled (FETCH_LICENSE) or `force` is set are the packages
|
|
1031
|
+
* still without a licence looked up remotely, and then only those a public
|
|
1032
|
+
* repository can hold.
|
|
1033
|
+
*
|
|
1034
|
+
* @param {Array} pkgList Package list
|
|
1035
|
+
* @param {Object} jarNSMapping Jar Namespace mapping object
|
|
1036
|
+
* @param {Boolean} force Force fetching of license
|
|
1037
|
+
* @param {{skipPurls?: Set<string>}} [context] Purls or bom-refs of the
|
|
1038
|
+
* project's own modules, which are never looked up remotely.
|
|
710
1039
|
*
|
|
711
|
-
* @
|
|
712
|
-
* @returns {String|undefined} The scm URL to prefetch a licence for, if any.
|
|
1040
|
+
* @returns {Array} Updated package list
|
|
713
1041
|
*/
|
|
714
|
-
function
|
|
715
|
-
|
|
716
|
-
|
|
1042
|
+
export async function getMvnMetadata(
|
|
1043
|
+
pkgList,
|
|
1044
|
+
jarNSMapping = {},
|
|
1045
|
+
force = false,
|
|
1046
|
+
context = {},
|
|
1047
|
+
) {
|
|
1048
|
+
if (!pkgList?.length) {
|
|
1049
|
+
return pkgList;
|
|
717
1050
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
1051
|
+
const repos = {
|
|
1052
|
+
MAVEN_CENTRAL_URL: withTrailingSlash(
|
|
1053
|
+
readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
|
|
1054
|
+
"https://repo1.maven.org/maven2/",
|
|
1055
|
+
),
|
|
1056
|
+
ANDROID_MAVEN_URL: withTrailingSlash(
|
|
1057
|
+
readEnvironmentVariable("ANDROID_MAVEN_URL") ||
|
|
1058
|
+
"https://maven.google.com/",
|
|
1059
|
+
),
|
|
1060
|
+
};
|
|
1061
|
+
const jarNSEntryFor = scalaJarEntryLookup(jarNSMapping);
|
|
1062
|
+
for (const p of pkgList) {
|
|
1063
|
+
await enrichMvnPackageLocally(p, jarNSEntryFor);
|
|
729
1064
|
}
|
|
730
|
-
if (
|
|
731
|
-
return
|
|
1065
|
+
if (!shouldFetchLicense() && !force) {
|
|
1066
|
+
return pkgList;
|
|
732
1067
|
}
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
1068
|
+
const remoteItems = [];
|
|
1069
|
+
for (const p of pkgList) {
|
|
1070
|
+
if (hasMavenLicense(p)) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const urlPrefix = mavenRepositoryFor(p, repos, context);
|
|
1074
|
+
if (urlPrefix) {
|
|
1075
|
+
// The remote round asks only for the published name: the component
|
|
1076
|
+
// name has no POM where the artifact is suffixed.
|
|
1077
|
+
remoteItems.push({
|
|
1078
|
+
p,
|
|
1079
|
+
coordinates: {
|
|
1080
|
+
urlPrefix,
|
|
1081
|
+
group: p.group,
|
|
1082
|
+
name: publishedArtifactId(p),
|
|
1083
|
+
version: p.version,
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (!remoteItems.length) {
|
|
1089
|
+
return pkgList;
|
|
1090
|
+
}
|
|
1091
|
+
if (DEBUG_MODE) {
|
|
1092
|
+
console.log(
|
|
1093
|
+
`About to query maven for ${remoteItems.length} of ${pkgList.length} packages`,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
// The direct POMs not on disk are fetched in one batched round. Parent URLs
|
|
1097
|
+
// depend on the direct POM's content, so parents are read in the loop below.
|
|
1098
|
+
const batchUrls = [];
|
|
1099
|
+
const seenPomUrls = new Set();
|
|
1100
|
+
for (const { coordinates } of remoteItems) {
|
|
1101
|
+
const pomUrl = composePomXmlUrl(coordinates);
|
|
1102
|
+
if (
|
|
1103
|
+
seenPomUrls.has(pomUrl) ||
|
|
1104
|
+
readLocalPom(coordinates.group, coordinates.name, coordinates.version)
|
|
1105
|
+
) {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
seenPomUrls.add(pomUrl);
|
|
1109
|
+
batchUrls.push({ url: pomUrl, responseType: "text" });
|
|
1110
|
+
}
|
|
1111
|
+
const ownPomUrls = [];
|
|
1112
|
+
if (batchUrls.length) {
|
|
1113
|
+
for (const [key, value] of await prefetchJson(batchUrls)) {
|
|
1114
|
+
prefetchedPoms.set(key, value);
|
|
1115
|
+
ownPomUrls.push(key);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
const needRepoLicense = [];
|
|
1120
|
+
for (const item of remoteItems) {
|
|
1121
|
+
try {
|
|
1122
|
+
const chain = await loadPomChain(item.coordinates, {
|
|
1123
|
+
remote: true,
|
|
1124
|
+
remoteParents: 1,
|
|
1125
|
+
});
|
|
1126
|
+
applyPomMetadata(item.p, chain?.merged, chain?.ownXml);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
if (DEBUG_MODE) {
|
|
1129
|
+
console.log(
|
|
1130
|
+
`An error occurred when trying to fetch metadata for ${item.p.group}/${item.p.name}@${item.p.version}`,
|
|
1131
|
+
err,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!hasMavenLicense(item.p) && item.p.repository?.url) {
|
|
1136
|
+
needRepoLicense.push(item.p);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// The repository licence is the last resort, asked for only once the POM
|
|
1140
|
+
// and all of its parents have been read, and batched.
|
|
1141
|
+
if (needRepoLicense.length) {
|
|
1142
|
+
await prefetchRepoLicenses(needRepoLicense.map((p) => p.repository.url));
|
|
1143
|
+
for (const p of needRepoLicense) {
|
|
1144
|
+
const license = await getRepoLicense(p.repository.url, undefined);
|
|
1145
|
+
if (license) {
|
|
1146
|
+
p.license = license;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} finally {
|
|
1151
|
+
// Each body is read once, by the loop above. Holding them past that only
|
|
1152
|
+
// grows the process, which matters in server mode where one process serves
|
|
1153
|
+
// many scans. Only the documents this call fetched are dropped, so a scan
|
|
1154
|
+
// running alongside this one keeps its own.
|
|
1155
|
+
for (const pomUrl of ownPomUrls) {
|
|
1156
|
+
prefetchedPoms.delete(pomUrl);
|
|
1157
|
+
}
|
|
736
1158
|
}
|
|
737
|
-
return
|
|
1159
|
+
return pkgList;
|
|
738
1160
|
}
|
|
739
1161
|
|
|
740
1162
|
/**
|
|
@@ -753,7 +1175,10 @@ export function composePomXmlUrl({ urlPrefix, group, name, version }) {
|
|
|
753
1175
|
}
|
|
754
1176
|
|
|
755
1177
|
/**
|
|
756
|
-
* Method to fetch pom.xml data and parse it to JSON
|
|
1178
|
+
* Method to fetch pom.xml data and parse it to JSON, merged with its parents.
|
|
1179
|
+
*
|
|
1180
|
+
* Each level is read from the local caches first. When a parent cannot be
|
|
1181
|
+
* found, the child and any nearer parents are still returned.
|
|
757
1182
|
*
|
|
758
1183
|
* @param {String} urlPrefix
|
|
759
1184
|
* @param {String} group
|
|
@@ -763,76 +1188,101 @@ export function composePomXmlUrl({ urlPrefix, group, name, version }) {
|
|
|
763
1188
|
* @return {Object|undefined}
|
|
764
1189
|
*/
|
|
765
1190
|
export async function fetchPomXmlAsJson({ urlPrefix, group, name, version }) {
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1191
|
+
const chain = await loadPomChain(
|
|
1192
|
+
{
|
|
1193
|
+
urlPrefix: urlPrefix ? withTrailingSlash(urlPrefix) : undefined,
|
|
1194
|
+
group,
|
|
1195
|
+
name,
|
|
1196
|
+
version,
|
|
1197
|
+
},
|
|
1198
|
+
{ remote: Boolean(urlPrefix), remoteParents: 1 },
|
|
1199
|
+
);
|
|
1200
|
+
return chain?.merged;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Fetch one document through the batch pool, so that it shares the pool's rate
|
|
1205
|
+
* gate, failure memo and disk cache, and make a gated request of its own only
|
|
1206
|
+
* when the pool could not reach the host.
|
|
1207
|
+
*
|
|
1208
|
+
* @param {string} url Document URL.
|
|
1209
|
+
* @param {"json"|"text"} [responseType] How to decode the body.
|
|
1210
|
+
* @returns {Promise<{body: *}>} The response.
|
|
1211
|
+
* @throws {Error} When the server answered with an error status.
|
|
1212
|
+
*/
|
|
1213
|
+
async function pooledDocument(url, responseType = "json") {
|
|
1214
|
+
if (prefetchEnabled()) {
|
|
1215
|
+
const res = prefetchedResponse(
|
|
1216
|
+
await prefetchJson([{ url, responseType }]),
|
|
1217
|
+
url,
|
|
1218
|
+
);
|
|
1219
|
+
if (res) {
|
|
1220
|
+
return res;
|
|
787
1221
|
}
|
|
788
|
-
const parentJson = xml2js(parentXml, options).project;
|
|
789
|
-
return { ...parentJson, ...pomJson };
|
|
790
1222
|
}
|
|
791
|
-
return
|
|
1223
|
+
return await gatedGet(url, { responseType });
|
|
792
1224
|
}
|
|
793
1225
|
|
|
794
1226
|
/**
|
|
795
1227
|
* Prefetched POM documents, populated by `getMvnMetadata` before its loop and
|
|
796
|
-
* read by `
|
|
797
|
-
*
|
|
798
|
-
* `extractLicenseCommentFromPomXml`, and threading a map through all of them
|
|
799
|
-
* would be a wide change for no gain.
|
|
1228
|
+
* read by `fetchRemotePomXml`. Module state rather than a parameter because
|
|
1229
|
+
* the POM readers are reached from several exported entry points.
|
|
800
1230
|
*
|
|
801
1231
|
* Only the *direct* POMs — the ones whose URLs are known before the loop — are
|
|
802
|
-
* prefetched.
|
|
803
|
-
*
|
|
804
|
-
* remain serial.
|
|
1232
|
+
* prefetched. Parent URLs depend on the direct POM's content, so they are not
|
|
1233
|
+
* known up front and are read serially.
|
|
805
1234
|
*/
|
|
806
1235
|
const prefetchedPoms = new Map();
|
|
807
1236
|
|
|
808
1237
|
/**
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
* @param {String} group
|
|
813
|
-
* @param {String} name
|
|
814
|
-
* @param {String} version
|
|
1238
|
+
* Fetch a POM from a remote repository, using a prefetched copy when there is
|
|
1239
|
+
* one. A POM that answered 404 or 410 is not asked for again this run, so
|
|
1240
|
+
* packages sharing a missing parent ask once.
|
|
815
1241
|
*
|
|
816
|
-
* @
|
|
1242
|
+
* @param {{urlPrefix: string, group: string, name: string, version: string}} coordinates
|
|
1243
|
+
* @returns {Promise<string|undefined>} POM text.
|
|
817
1244
|
*/
|
|
818
|
-
|
|
1245
|
+
async function fetchRemotePomXml({ urlPrefix, group, name, version }) {
|
|
1246
|
+
if (!urlPrefix || !group || !name || !version) {
|
|
1247
|
+
return undefined;
|
|
1248
|
+
}
|
|
819
1249
|
const fullUrl = composePomXmlUrl({ urlPrefix, group, name, version });
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
// and still counts against the same budget as the batch round, so it queues
|
|
825
|
-
// behind the same per-host gate rather than a second, ungoverned one.
|
|
1250
|
+
// Parent POMs, whose URLs are known only once the child is read, are
|
|
1251
|
+
// fetched one at a time but still through the pool: they count against
|
|
1252
|
+
// Maven Central's budget, and a release POM never changes, so the pool's
|
|
1253
|
+
// disk cache keeps it for every later run.
|
|
826
1254
|
try {
|
|
827
1255
|
const res =
|
|
828
1256
|
prefetchedResponse(prefetchedPoms, fullUrl) ||
|
|
829
|
-
(await
|
|
1257
|
+
(await pooledDocument(fullUrl, "text"));
|
|
830
1258
|
return res.body;
|
|
831
1259
|
} catch (_err) {
|
|
832
1260
|
return undefined;
|
|
833
1261
|
}
|
|
834
1262
|
}
|
|
835
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* Method to fetch pom.xml data, from the local caches when present.
|
|
1266
|
+
*
|
|
1267
|
+
* @param {String} urlPrefix
|
|
1268
|
+
* @param {String} group
|
|
1269
|
+
* @param {String} name
|
|
1270
|
+
* @param {String} version
|
|
1271
|
+
*
|
|
1272
|
+
* @return {Promise<String>}
|
|
1273
|
+
*/
|
|
1274
|
+
export async function fetchPomXml({ urlPrefix, group, name, version }) {
|
|
1275
|
+
return (
|
|
1276
|
+
readLocalPom(group, name, version) ||
|
|
1277
|
+
(await fetchRemotePomXml({
|
|
1278
|
+
urlPrefix: urlPrefix ? withTrailingSlash(urlPrefix) : undefined,
|
|
1279
|
+
group,
|
|
1280
|
+
name,
|
|
1281
|
+
version,
|
|
1282
|
+
}))
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
836
1286
|
/**
|
|
837
1287
|
* Method extract single or multiple license entries that might appear in pom.xml
|
|
838
1288
|
*
|
|
@@ -866,12 +1316,9 @@ export async function extractLicenseCommentFromPomXml({
|
|
|
866
1316
|
name,
|
|
867
1317
|
version,
|
|
868
1318
|
}) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (match?.[1]) {
|
|
873
|
-
return findLicenseId(match[1].trim());
|
|
874
|
-
}
|
|
1319
|
+
return licenseFromPomComment(
|
|
1320
|
+
await fetchPomXml({ urlPrefix, group, name, version }),
|
|
1321
|
+
);
|
|
875
1322
|
}
|
|
876
1323
|
|
|
877
1324
|
/**
|
|
@@ -906,6 +1353,63 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
906
1353
|
return maxSatisfying(versionsList, clean(versionSpecifiers, { loose: true }));
|
|
907
1354
|
}
|
|
908
1355
|
|
|
1356
|
+
/**
|
|
1357
|
+
* Whether PyPI, or the index PYPI_URL points at, can hold the package.
|
|
1358
|
+
*
|
|
1359
|
+
* The Python parsers record where each package came from. A virtual workspace
|
|
1360
|
+
* root, an editable install, and a git, URL or path source are not index
|
|
1361
|
+
* releases; Poetry records a private index the same way, as the package's
|
|
1362
|
+
* source URL. A package from another index is asked only when PYPI_URL points
|
|
1363
|
+
* at that index's server. Asking PyPI about any of them leaks the name and
|
|
1364
|
+
* attaches a look-alike's licence.
|
|
1365
|
+
*
|
|
1366
|
+
* @param {Object} p Package object with the properties the Python parsers record.
|
|
1367
|
+
* @param {string} pypiUrl The JSON API base cdxgen asks.
|
|
1368
|
+
* @returns {boolean}
|
|
1369
|
+
*/
|
|
1370
|
+
function canPypiServe(p, pypiUrl) {
|
|
1371
|
+
const props = {};
|
|
1372
|
+
for (const prop of p.properties || []) {
|
|
1373
|
+
props[prop.name] = prop.value;
|
|
1374
|
+
}
|
|
1375
|
+
if (props["internal:virtual_path"]) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const sourceType = props["cdx:pypi:manifestSourceType"];
|
|
1379
|
+
if (
|
|
1380
|
+
sourceType &&
|
|
1381
|
+
!(
|
|
1382
|
+
sourceType === "url" &&
|
|
1383
|
+
isDefaultPypiRegistry(props["cdx:pypi:manifestSource"])
|
|
1384
|
+
)
|
|
1385
|
+
) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
if ((p.externalReferences || []).some((ref) => ref.type === "vcs")) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
// The registry property is recorded only for a non-default index.
|
|
1392
|
+
const registry = props["cdx:pypi:registry"];
|
|
1393
|
+
if (registry && !isDefaultPypiRegistry(registry)) {
|
|
1394
|
+
const host = parseUrl(registry)?.host;
|
|
1395
|
+
return !!host && host === parseUrl(pypiUrl)?.host;
|
|
1396
|
+
}
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether a package already carries what a PyPI lookup would be asked for: a
|
|
1402
|
+
* licence and a version from the lockfile or an installed distribution. When
|
|
1403
|
+
* registry provenance is wanted it is still asked, since nothing local holds
|
|
1404
|
+
* that.
|
|
1405
|
+
*
|
|
1406
|
+
* @param {Object} p Package object.
|
|
1407
|
+
* @returns {boolean}
|
|
1408
|
+
*/
|
|
1409
|
+
function pypiAnsweredLocally(p) {
|
|
1410
|
+
return !!(p.license && p.version) && !shouldFetchRegistryProvenance();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
909
1413
|
/**
|
|
910
1414
|
* The PyPI path for a package, or `undefined` when the package will not be
|
|
911
1415
|
* queried at all.
|
|
@@ -916,14 +1420,19 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
916
1420
|
* JS path never makes, which is both wasted work and a divergence.
|
|
917
1421
|
*
|
|
918
1422
|
* @param {Object} p Package object.
|
|
1423
|
+
* @param {string} pypiUrl The JSON API base cdxgen asks.
|
|
919
1424
|
* @returns {string|undefined} Path to append to PYPI_URL.
|
|
920
1425
|
*/
|
|
921
|
-
function pyUrlAddition(p) {
|
|
1426
|
+
function pyUrlAddition(p, pypiUrl) {
|
|
922
1427
|
if (!p?.name) {
|
|
923
1428
|
return undefined;
|
|
924
1429
|
}
|
|
1430
|
+
// A package PyPI cannot hold is not queried.
|
|
1431
|
+
if (!canPypiServe(p, pypiUrl)) {
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
925
1434
|
// A URL as a name, or a package that already has both fields, is not queried.
|
|
926
|
-
if (p.name.includes("https") || (p
|
|
1435
|
+
if (p.name.includes("https") || pypiAnsweredLocally(p)) {
|
|
927
1436
|
return undefined;
|
|
928
1437
|
}
|
|
929
1438
|
// Extras (`requests[security]`) are not part of the PyPI path.
|
|
@@ -933,6 +1442,35 @@ function pyUrlAddition(p) {
|
|
|
933
1442
|
: `${name}/json`;
|
|
934
1443
|
}
|
|
935
1444
|
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether a failed PyPI request means the project or version does not exist,
|
|
1447
|
+
* as opposed to a rate limit, a timeout or a server error, which say nothing
|
|
1448
|
+
* about the name.
|
|
1449
|
+
*
|
|
1450
|
+
* @param {Error} err Error from the agent or the batch prefetch.
|
|
1451
|
+
* @returns {boolean} True for 404 and 410.
|
|
1452
|
+
*/
|
|
1453
|
+
function isPypiNotFound(err) {
|
|
1454
|
+
const status = err?.statusCode ?? err?.response?.statusCode;
|
|
1455
|
+
return status === 404 || status === 410;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Fetch a PyPI JSON document, from the batch prefetch when it holds one.
|
|
1460
|
+
* getPyMetadata runs more than once over the same packages, and a URL that
|
|
1461
|
+
* answered 404 or 410 fails at once on every later pass of the run.
|
|
1462
|
+
*
|
|
1463
|
+
* @param {string} url PyPI JSON API URL.
|
|
1464
|
+
* @param {Map} [prefetched] Results of the batch prefetch.
|
|
1465
|
+
* @returns {Promise<Object>} Response with `body`.
|
|
1466
|
+
*/
|
|
1467
|
+
async function getPypiDocument(url, prefetched) {
|
|
1468
|
+
return (
|
|
1469
|
+
prefetchedResponse(prefetched, url) ||
|
|
1470
|
+
(await gatedGet(url, { responseType: "json" }))
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
936
1474
|
/**
|
|
937
1475
|
* Method to retrieve metadata for python packages by querying pypi
|
|
938
1476
|
*
|
|
@@ -952,7 +1490,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
952
1490
|
const prefetched = await prefetchJson(
|
|
953
1491
|
prefetchEnabled()
|
|
954
1492
|
? pkgList
|
|
955
|
-
.map((p) => pyUrlAddition(p))
|
|
1493
|
+
.map((p) => pyUrlAddition(p, PYPI_URL))
|
|
956
1494
|
.filter(Boolean)
|
|
957
1495
|
.map((addition) => ({ url: PYPI_URL + addition }))
|
|
958
1496
|
: [],
|
|
@@ -966,8 +1504,13 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
966
1504
|
// map, so they are available without a registry round trip and are attached
|
|
967
1505
|
// before the skip below rather than after it.
|
|
968
1506
|
applyPypiModuleNames(p);
|
|
1507
|
+
// A package PyPI cannot hold keeps the answers the lockfile gave it.
|
|
1508
|
+
if (!canPypiServe(p, PYPI_URL)) {
|
|
1509
|
+
cdepList.push(p);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
969
1512
|
// If the package name has a url or already includes license and version skip it
|
|
970
|
-
if (p.name.includes("https") || (p
|
|
1513
|
+
if (p.name.includes("https") || pypiAnsweredLocally(p)) {
|
|
971
1514
|
cdepList.push(p);
|
|
972
1515
|
continue;
|
|
973
1516
|
}
|
|
@@ -984,30 +1527,36 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
984
1527
|
url_addition = `${p.name}/json`;
|
|
985
1528
|
}
|
|
986
1529
|
try {
|
|
987
|
-
res =
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
});
|
|
1530
|
+
res = await getPypiDocument(PYPI_URL + url_addition, prefetched);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
// Only a missing project means the name may be the Django plugin's.
|
|
1533
|
+
// A rate limit, a timeout or a server error says nothing about the
|
|
1534
|
+
// name, and renaming the package then would give it another
|
|
1535
|
+
// project's identity.
|
|
1536
|
+
if (!isPypiNotFound(err)) {
|
|
1537
|
+
throw err;
|
|
1538
|
+
}
|
|
1539
|
+
res = await getPypiDocument(`${PYPI_URL}django-${url_addition}`);
|
|
997
1540
|
p.name = `django-${p.name}`;
|
|
998
1541
|
}
|
|
999
1542
|
const body = res.body;
|
|
1543
|
+
// The registry fills only what the lockfile and the installed
|
|
1544
|
+
// distribution left open, so a local answer survives the round.
|
|
1545
|
+
let registryAuthor;
|
|
1000
1546
|
if (body.info.author && body.info.author.trim() !== "") {
|
|
1001
1547
|
if (body.info.author_email && body.info.author_email.trim() !== "") {
|
|
1002
|
-
|
|
1548
|
+
registryAuthor = `${body.info.author.trim()} <${body.info.author_email.trim()}>`;
|
|
1003
1549
|
} else {
|
|
1004
|
-
|
|
1550
|
+
registryAuthor = body.info.author.trim();
|
|
1005
1551
|
}
|
|
1006
1552
|
} else if (
|
|
1007
1553
|
body.info.author_email &&
|
|
1008
1554
|
body.info.author_email.trim() !== ""
|
|
1009
1555
|
) {
|
|
1010
|
-
|
|
1556
|
+
registryAuthor = body.info.author_email.trim();
|
|
1557
|
+
}
|
|
1558
|
+
if (registryAuthor && !p.author) {
|
|
1559
|
+
p.author = registryAuthor;
|
|
1011
1560
|
}
|
|
1012
1561
|
if (
|
|
1013
1562
|
p.name !== body.info?.name &&
|
|
@@ -1015,36 +1564,40 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
1015
1564
|
) {
|
|
1016
1565
|
p.name = body.info.name;
|
|
1017
1566
|
}
|
|
1018
|
-
p.description
|
|
1019
|
-
|
|
1567
|
+
if (!p.description) {
|
|
1568
|
+
p.description = body.info.summary;
|
|
1569
|
+
}
|
|
1570
|
+
const registryLicenses = [];
|
|
1020
1571
|
if (body.info.classifiers) {
|
|
1021
1572
|
for (const c of body.info.classifiers) {
|
|
1022
1573
|
if (c.startsWith("License :: ")) {
|
|
1023
1574
|
const licenseName = c.split("::").slice(-1)[0].trim();
|
|
1024
1575
|
const licenseId = findLicenseId(licenseName);
|
|
1025
|
-
if (licenseId && !
|
|
1026
|
-
|
|
1576
|
+
if (licenseId && !registryLicenses.includes(licenseId)) {
|
|
1577
|
+
registryLicenses.push(licenseId);
|
|
1027
1578
|
}
|
|
1028
1579
|
}
|
|
1029
1580
|
}
|
|
1030
1581
|
applyPypiClassifierMetadata(p, body.info.classifiers);
|
|
1031
1582
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1583
|
+
for (const declared of [
|
|
1584
|
+
body.info.license,
|
|
1585
|
+
body.info.license_expression,
|
|
1586
|
+
]) {
|
|
1587
|
+
const licenseId = declared ? findLicenseId(declared) : undefined;
|
|
1588
|
+
if (licenseId && !registryLicenses.includes(licenseId)) {
|
|
1589
|
+
registryLicenses.push(licenseId);
|
|
1036
1590
|
}
|
|
1037
1591
|
}
|
|
1038
|
-
if (
|
|
1039
|
-
|
|
1040
|
-
if (licenseId && !p.license.includes(licenseId)) {
|
|
1041
|
-
p.license.push(licenseId);
|
|
1042
|
-
}
|
|
1592
|
+
if (!(Array.isArray(p.license) ? p.license.length : p.license)) {
|
|
1593
|
+
p.license = registryLicenses;
|
|
1043
1594
|
}
|
|
1044
1595
|
if (body.info.home_page) {
|
|
1045
1596
|
if (body.info.home_page.includes("git")) {
|
|
1046
|
-
p.repository
|
|
1047
|
-
|
|
1597
|
+
if (!p.repository) {
|
|
1598
|
+
p.repository = { url: body.info.home_page };
|
|
1599
|
+
}
|
|
1600
|
+
} else if (!p.homepage) {
|
|
1048
1601
|
p.homepage = { url: body.info.home_page };
|
|
1049
1602
|
}
|
|
1050
1603
|
}
|
|
@@ -1482,7 +2035,13 @@ let repoLicensePrefetch = new Map();
|
|
|
1482
2035
|
* @returns {Promise<void>}
|
|
1483
2036
|
*/
|
|
1484
2037
|
export async function prefetchRepoLicenses(repoUrls) {
|
|
1485
|
-
|
|
2038
|
+
// While the lookups are paused, getRepoLicense asks nothing, so neither
|
|
2039
|
+
// does the batch.
|
|
2040
|
+
if (
|
|
2041
|
+
!prefetchEnabled() ||
|
|
2042
|
+
!Array.isArray(repoUrls) ||
|
|
2043
|
+
repoLicenseLookupsPaused()
|
|
2044
|
+
) {
|
|
1486
2045
|
return;
|
|
1487
2046
|
}
|
|
1488
2047
|
const requests = [];
|
|
@@ -1496,7 +2055,16 @@ export async function prefetchRepoLicenses(repoUrls) {
|
|
|
1496
2055
|
continue;
|
|
1497
2056
|
}
|
|
1498
2057
|
const licenseUrl = `${apiUrl}/license`;
|
|
1499
|
-
|
|
2058
|
+
// A licence this run already has an answer for, a licence or a 404, is
|
|
2059
|
+
// read from the earlier round. A lookup that failed, for instance on a
|
|
2060
|
+
// spent quota, is asked again.
|
|
2061
|
+
const known = repoLicensePrefetch.get(licenseUrl);
|
|
2062
|
+
if (
|
|
2063
|
+
seen.has(licenseUrl) ||
|
|
2064
|
+
known?.ok ||
|
|
2065
|
+
known?.status === 404 ||
|
|
2066
|
+
known?.status === 410
|
|
2067
|
+
) {
|
|
1500
2068
|
continue;
|
|
1501
2069
|
}
|
|
1502
2070
|
seen.add(licenseUrl);
|
|
@@ -1543,7 +2111,7 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1543
2111
|
}
|
|
1544
2112
|
const apiUrl = toGitHubApiUrl(repoUrl, repoMetadata);
|
|
1545
2113
|
// Perform github lookups
|
|
1546
|
-
if (apiUrl &&
|
|
2114
|
+
if (apiUrl && !repoLicenseLookupsPaused()) {
|
|
1547
2115
|
const licenseUrl = `${apiUrl}/license`;
|
|
1548
2116
|
const headers = {};
|
|
1549
2117
|
if (readEnvironmentVariable("GITHUB_TOKEN")) {
|
|
@@ -1553,9 +2121,9 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1553
2121
|
try {
|
|
1554
2122
|
const res =
|
|
1555
2123
|
prefetchedResponse(repoLicensePrefetch, licenseUrl) ||
|
|
1556
|
-
(await
|
|
2124
|
+
(await gatedGet(licenseUrl, {
|
|
1557
2125
|
responseType: "json",
|
|
1558
|
-
headers
|
|
2126
|
+
headers,
|
|
1559
2127
|
}));
|
|
1560
2128
|
if (res?.body) {
|
|
1561
2129
|
const license = res.body.license;
|
|
@@ -1582,20 +2150,18 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1582
2150
|
}
|
|
1583
2151
|
}
|
|
1584
2152
|
} catch (err) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
);
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
get_repo_license_errors++;
|
|
1598
|
-
}
|
|
2153
|
+
// A repository without a licence file answers 404. A spent quota is a
|
|
2154
|
+
// 429, or a 403 with nothing remaining; the request layer pauses
|
|
2155
|
+
// api.github.com until the quota resets and says so once, with the
|
|
2156
|
+
// GITHUB_TOKEN advice. Any other failure counts towards pausing these
|
|
2157
|
+
// lookups.
|
|
2158
|
+
const status = err?.response?.statusCode ?? err?.statusCode;
|
|
2159
|
+
const quotaSpent =
|
|
2160
|
+
status === 429 ||
|
|
2161
|
+
(status === 403 &&
|
|
2162
|
+
String(err?.response?.headers?.["x-ratelimit-remaining"]) === "0");
|
|
2163
|
+
if (status !== 404 && !quotaSpent) {
|
|
2164
|
+
get_repo_license_errors++;
|
|
1599
2165
|
}
|
|
1600
2166
|
}
|
|
1601
2167
|
}
|
|
@@ -1639,12 +2205,21 @@ export async function prefetchGoPkgMetadata(modules) {
|
|
|
1639
2205
|
if (!module?.name) {
|
|
1640
2206
|
continue;
|
|
1641
2207
|
}
|
|
2208
|
+
const fullName =
|
|
2209
|
+
module.fullName || getGoPkgFullName(module.group, module.name);
|
|
2210
|
+
// A module the environment marks private is never requested.
|
|
2211
|
+
// getGoPkgLicense applies the same test, so the batch cannot ask for a
|
|
2212
|
+
// module the serial path would skip.
|
|
2213
|
+
if (isPrivateGoModule(fullName)) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
1642
2216
|
const pkgUrl = getGoPkgUrl(module);
|
|
1643
2217
|
// The licence tab and the module page are distinct documents, read by
|
|
1644
2218
|
// getGoPkgLicense and getGoPkgVCSUrl respectively. Both are requested only
|
|
1645
2219
|
// when the corresponding flag is on, so the batch mirrors that.
|
|
1646
2220
|
const wanted = [];
|
|
1647
|
-
|
|
2221
|
+
// A licence notice read on this machine answers the licence tab.
|
|
2222
|
+
if (shouldFetchLicense() && !module.licenseKnown) {
|
|
1648
2223
|
wanted.push(`${pkgUrl}?tab=licenses`);
|
|
1649
2224
|
}
|
|
1650
2225
|
// getGoPkgVCSUrl derives github.com and gitlab.com URLs without asking
|
|
@@ -1653,7 +2228,7 @@ export async function prefetchGoPkgMetadata(modules) {
|
|
|
1653
2228
|
wanted.push(pkgUrl);
|
|
1654
2229
|
}
|
|
1655
2230
|
for (const url of wanted) {
|
|
1656
|
-
if (seen.has(url) ||
|
|
2231
|
+
if (seen.has(url) || goPkgAnswers.has(url)) {
|
|
1657
2232
|
continue;
|
|
1658
2233
|
}
|
|
1659
2234
|
seen.add(url);
|
|
@@ -1683,15 +2258,21 @@ export function resetGoPkgPrefetch() {
|
|
|
1683
2258
|
*/
|
|
1684
2259
|
export async function getGoPkgLicense(repoMetadata) {
|
|
1685
2260
|
const group = repoMetadata.group;
|
|
2261
|
+
// A module the environment marks private is never sent to the public site.
|
|
2262
|
+
const fullName =
|
|
2263
|
+
repoMetadata.fullName ||
|
|
2264
|
+
getGoPkgFullName(repoMetadata.group, repoMetadata.name);
|
|
2265
|
+
if (isPrivateGoModule(fullName)) {
|
|
2266
|
+
return undefined;
|
|
2267
|
+
}
|
|
1686
2268
|
const pkgUrl = `${getGoPkgUrl(repoMetadata)}?tab=licenses`;
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
return metadata_cache[pkgUrl];
|
|
2269
|
+
if (goPkgAnswers.has(pkgUrl)) {
|
|
2270
|
+
return goPkgAnswers.get(pkgUrl);
|
|
1690
2271
|
}
|
|
1691
2272
|
try {
|
|
1692
2273
|
const res =
|
|
1693
2274
|
prefetchedResponse(goPkgPrefetch, pkgUrl) ||
|
|
1694
|
-
(await
|
|
2275
|
+
(await gatedGet(pkgUrl, { responseType: "text" }));
|
|
1695
2276
|
if (res?.body) {
|
|
1696
2277
|
const licenses = extractLicenseText(res.body);
|
|
1697
2278
|
const licenseIds = licenses.split(", ");
|
|
@@ -1714,7 +2295,7 @@ export async function getGoPkgLicense(repoMetadata) {
|
|
|
1714
2295
|
licList.push(alicense);
|
|
1715
2296
|
}
|
|
1716
2297
|
}
|
|
1717
|
-
|
|
2298
|
+
goPkgAnswers.set(pkgUrl, licList);
|
|
1718
2299
|
return licList;
|
|
1719
2300
|
}
|
|
1720
2301
|
} catch (_err) {
|
|
@@ -1731,23 +2312,31 @@ export async function getGoPkgLicense(repoMetadata) {
|
|
|
1731
2312
|
*
|
|
1732
2313
|
* @param {String} group Package group
|
|
1733
2314
|
* @param {String} name Package name
|
|
2315
|
+
* @param {String} [version] Package version, part of the page URL
|
|
1734
2316
|
*/
|
|
1735
|
-
export async function getGoPkgVCSUrl(group, name) {
|
|
2317
|
+
export async function getGoPkgVCSUrl(group, name, version) {
|
|
1736
2318
|
const fullName = getGoPkgFullName(group, name);
|
|
1737
2319
|
if (isDirectlyResolvableGoVCS({ group, name })) {
|
|
1738
2320
|
return `https://${fullName}`;
|
|
1739
2321
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2322
|
+
// The repository URL of a private module follows from a github.com or
|
|
2323
|
+
// gitlab.com path without a request, but pkg.go.dev is never asked.
|
|
2324
|
+
if (isPrivateGoModule(fullName)) {
|
|
2325
|
+
return undefined;
|
|
2326
|
+
}
|
|
2327
|
+
// The versioned page matches the URL the prefetch requested, so a prefetched
|
|
2328
|
+
// document is found in the cache.
|
|
2329
|
+
const pkgUrl = getGoPkgUrl({ fullName, version });
|
|
2330
|
+
if (goPkgAnswers.has(pkgUrl)) {
|
|
2331
|
+
return goPkgAnswers.get(pkgUrl);
|
|
1743
2332
|
}
|
|
1744
2333
|
try {
|
|
1745
2334
|
const res =
|
|
1746
2335
|
prefetchedResponse(goPkgPrefetch, pkgUrl) ||
|
|
1747
|
-
(await
|
|
2336
|
+
(await gatedGet(pkgUrl, { responseType: "text" }));
|
|
1748
2337
|
if (res?.body) {
|
|
1749
2338
|
const vcs = extractRepoUrl(res.body);
|
|
1750
|
-
|
|
2339
|
+
goPkgAnswers.set(pkgUrl, vcs);
|
|
1751
2340
|
return vcs;
|
|
1752
2341
|
}
|
|
1753
2342
|
} catch (_err) {
|
|
@@ -1774,17 +2363,154 @@ function isDirectlyResolvableGoVCS(pkgMetadata) {
|
|
|
1774
2363
|
}
|
|
1775
2364
|
|
|
1776
2365
|
/**
|
|
1777
|
-
* Method to get go pkg url (go.dev site).
|
|
2366
|
+
* Method to get go pkg url (go.dev site).
|
|
2367
|
+
*
|
|
2368
|
+
* The version is part of the URL whenever it is known, so pkg.go.dev answers
|
|
2369
|
+
* for the release the project pinned rather than for the latest one.
|
|
2370
|
+
*
|
|
2371
|
+
* @param {Object} pkgMetadata pkg metadata
|
|
2372
|
+
*/
|
|
2373
|
+
function getGoPkgUrl(pkgMetadata) {
|
|
2374
|
+
const pkgUrlPrefix =
|
|
2375
|
+
readEnvironmentVariable("GO_PKG_URL") || "https://pkg.go.dev/";
|
|
2376
|
+
const fullName =
|
|
2377
|
+
pkgMetadata.fullName ||
|
|
2378
|
+
getGoPkgFullName(pkgMetadata.group, pkgMetadata.name);
|
|
2379
|
+
const version = pkgMetadata.version ? `@${pkgMetadata.version}` : "";
|
|
2380
|
+
return pkgUrlPrefix + fullName + version;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Match one path element of a Go module pattern against one element of a
|
|
2385
|
+
* module path, the way Go's `path.Match` does: `*` and `?` do not cross a `/`,
|
|
2386
|
+
* and a bracketed character class is allowed.
|
|
2387
|
+
*
|
|
2388
|
+
* @param {string} pattern One pattern element
|
|
2389
|
+
* @param {string} name One module path element
|
|
2390
|
+
* @returns {boolean}
|
|
2391
|
+
*/
|
|
2392
|
+
function matchGoPathElement(pattern, name) {
|
|
2393
|
+
let p = 0;
|
|
2394
|
+
let n = 0;
|
|
2395
|
+
let starP = -1;
|
|
2396
|
+
let starN = -1;
|
|
2397
|
+
while (n < name.length) {
|
|
2398
|
+
if (p < pattern.length) {
|
|
2399
|
+
const c = pattern[p];
|
|
2400
|
+
if (c === "*") {
|
|
2401
|
+
starP = p;
|
|
2402
|
+
starN = n;
|
|
2403
|
+
p++;
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
if (c === "?") {
|
|
2407
|
+
p++;
|
|
2408
|
+
n++;
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
if (c === "[") {
|
|
2412
|
+
let i = p + 1;
|
|
2413
|
+
let negate = false;
|
|
2414
|
+
if (i < pattern.length && "^".indexOf(pattern[i]) > -1) {
|
|
2415
|
+
negate = pattern[i] === "^";
|
|
2416
|
+
i++;
|
|
2417
|
+
}
|
|
2418
|
+
let matched = false;
|
|
2419
|
+
let first = true;
|
|
2420
|
+
while (i < pattern.length && (first || pattern[i] !== "]")) {
|
|
2421
|
+
first = false;
|
|
2422
|
+
let lo = pattern[i];
|
|
2423
|
+
if (lo === "\\" && i + 1 < pattern.length) {
|
|
2424
|
+
i++;
|
|
2425
|
+
lo = pattern[i];
|
|
2426
|
+
}
|
|
2427
|
+
let hi = lo;
|
|
2428
|
+
if (
|
|
2429
|
+
i + 2 < pattern.length &&
|
|
2430
|
+
pattern[i + 1] === "-" &&
|
|
2431
|
+
pattern[i + 2] !== "]"
|
|
2432
|
+
) {
|
|
2433
|
+
hi = pattern[i + 2];
|
|
2434
|
+
i += 2;
|
|
2435
|
+
}
|
|
2436
|
+
if (name[n] >= lo && name[n] <= hi) {
|
|
2437
|
+
matched = true;
|
|
2438
|
+
}
|
|
2439
|
+
i++;
|
|
2440
|
+
}
|
|
2441
|
+
if (i < pattern.length && pattern[i] === "]") {
|
|
2442
|
+
if (matched !== negate) {
|
|
2443
|
+
p = i + 1;
|
|
2444
|
+
n++;
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
// An unterminated class matches nothing rather than silently
|
|
2449
|
+
// swallowing the rest of the pattern.
|
|
2450
|
+
return false;
|
|
2451
|
+
}
|
|
2452
|
+
} else if (c === "\\" && p + 1 < pattern.length) {
|
|
2453
|
+
if (pattern[p + 1] === name[n]) {
|
|
2454
|
+
p += 2;
|
|
2455
|
+
n++;
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
} else if (c === name[n]) {
|
|
2459
|
+
p++;
|
|
2460
|
+
n++;
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
if (starP !== -1) {
|
|
2465
|
+
p = starP + 1;
|
|
2466
|
+
starN++;
|
|
2467
|
+
n = starN;
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
while (p < pattern.length && pattern[p] === "*") {
|
|
2473
|
+
p++;
|
|
2474
|
+
}
|
|
2475
|
+
return p === pattern.length;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Whether a module is private and must not be sent to pkg.go.dev.
|
|
1778
2480
|
*
|
|
1779
|
-
*
|
|
2481
|
+
* `GOPRIVATE`, and `GONOPROXY`/`GONOSUMDB` when they are set, hold
|
|
2482
|
+
* comma-separated globs. Like Go's `module.MatchPrefixPatterns`, a glob with N
|
|
2483
|
+
* path elements is matched against the first N elements of the module path,
|
|
2484
|
+
* so `example.com/org` matches `example.com/org/tool`. Sending a private
|
|
2485
|
+
* module to a public site leaks its path, and the site cannot hold it anyway.
|
|
2486
|
+
*
|
|
2487
|
+
* @param {string} modulePath Full module path
|
|
2488
|
+
* @returns {boolean}
|
|
1780
2489
|
*/
|
|
1781
|
-
function
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2490
|
+
function isPrivateGoModule(modulePath) {
|
|
2491
|
+
if (!modulePath) {
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
const targetParts = modulePath.split("/");
|
|
2495
|
+
for (const name of ["GOPRIVATE", "GONOPROXY", "GONOSUMDB"]) {
|
|
2496
|
+
for (const rawGlob of (readEnvironmentVariable(name) || "").split(",")) {
|
|
2497
|
+
const trimmed = rawGlob.trim();
|
|
2498
|
+
const glob = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2499
|
+
if (!glob) {
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
const globParts = glob.split("/");
|
|
2503
|
+
if (globParts.length > targetParts.length) {
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
if (
|
|
2507
|
+
globParts.every((part, i) => matchGoPathElement(part, targetParts[i]))
|
|
2508
|
+
) {
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return false;
|
|
1788
2514
|
}
|
|
1789
2515
|
|
|
1790
2516
|
/**
|
|
@@ -1797,6 +2523,35 @@ function getGoPkgFullName(group, name) {
|
|
|
1797
2523
|
return group && group !== "." && group !== name ? `${group}/${name}` : name;
|
|
1798
2524
|
}
|
|
1799
2525
|
|
|
2526
|
+
/**
|
|
2527
|
+
* Whether crates.io can hold the crate.
|
|
2528
|
+
*
|
|
2529
|
+
* The Cargo.lock parser records a `cdx:cargo:sourceKind` for every crate whose
|
|
2530
|
+
* source is not plain crates.io: the workspace and path crates, the project
|
|
2531
|
+
* itself, git crates, and crates an alternate registry serves. Asking crates.io
|
|
2532
|
+
* about those attaches a same-named crate's metadata to them, so only a crate
|
|
2533
|
+
* crates.io actually serves is looked up.
|
|
2534
|
+
*
|
|
2535
|
+
* @param {Object} p Package object with the parser's source properties.
|
|
2536
|
+
* @returns {boolean}
|
|
2537
|
+
*/
|
|
2538
|
+
function canCratesIoServe(p) {
|
|
2539
|
+
let sourceKind;
|
|
2540
|
+
let alternateRegistry = false;
|
|
2541
|
+
for (const prop of p.properties || []) {
|
|
2542
|
+
if (prop.name === "cdx:cargo:sourceKind") {
|
|
2543
|
+
sourceKind = prop.value;
|
|
2544
|
+
} else if (prop.name === "cdx:cargo:alternateRegistry") {
|
|
2545
|
+
alternateRegistry = prop.value === "true";
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (!sourceKind) {
|
|
2549
|
+
// The public registry is the norm and goes unrecorded.
|
|
2550
|
+
return true;
|
|
2551
|
+
}
|
|
2552
|
+
return sourceKind === "registry" && !alternateRegistry;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
1800
2555
|
/**
|
|
1801
2556
|
* Method to retrieve metadata for rust packages by querying crates
|
|
1802
2557
|
*
|
|
@@ -1807,7 +2562,13 @@ function getGoPkgFullName(group, name) {
|
|
|
1807
2562
|
*
|
|
1808
2563
|
* What it does not hold is publisher identity, so a caller that needs the
|
|
1809
2564
|
* publisher-drift and release-cadence signals — the predictive audit does —
|
|
1810
|
-
* passes `preferLocalCache: false` and takes the slower registry path.
|
|
2565
|
+
* passes `preferLocalCache: false` and takes the slower registry path. That is
|
|
2566
|
+
* also the only caller that needs the crate's owners, so the `/owners` request
|
|
2567
|
+
* is made for it alone.
|
|
2568
|
+
*
|
|
2569
|
+
* Crates a public registry cannot hold are skipped: the project itself, path
|
|
2570
|
+
* and git crates, and crates from an alternate registry. See
|
|
2571
|
+
* {@link canCratesIoServe}.
|
|
1811
2572
|
*
|
|
1812
2573
|
* @param {Array} pkgList Package list
|
|
1813
2574
|
* @param {Object} [options] Options
|
|
@@ -1824,12 +2585,26 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1824
2585
|
// reads; only the crates it cannot account for reach the network.
|
|
1825
2586
|
const servedLocally = new Set();
|
|
1826
2587
|
const preferLocalCache = options.preferLocalCache !== false;
|
|
2588
|
+
// The owners listing carries no licence, so only a caller that wants the
|
|
2589
|
+
// publisher identity behind it pays for the second request.
|
|
2590
|
+
const wantsOwners = !preferLocalCache;
|
|
1827
2591
|
if (preferLocalCache && localCargoMetadataEnabled()) {
|
|
1828
2592
|
for (const p of pkgList) {
|
|
1829
2593
|
if (!p?.name || !p?.version || p.version === "workspace") {
|
|
1830
2594
|
continue;
|
|
1831
2595
|
}
|
|
1832
|
-
|
|
2596
|
+
// A crate crates.io does not hold must not take a same-named crate's
|
|
2597
|
+
// place in the local registry cache either.
|
|
2598
|
+
if (!canCratesIoServe(p)) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
// A crate a caller already filled from its own local source, such as a
|
|
2602
|
+
// .crate archive the scan itself read, has nothing left to ask for.
|
|
2603
|
+
if (p.license && p.description) {
|
|
2604
|
+
servedLocally.add(p);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
if (await applyCargoCacheMetadata(p)) {
|
|
1833
2608
|
servedLocally.add(p);
|
|
1834
2609
|
}
|
|
1835
2610
|
}
|
|
@@ -1844,17 +2619,21 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1844
2619
|
p?.name &&
|
|
1845
2620
|
p?.version &&
|
|
1846
2621
|
p.version !== "workspace" &&
|
|
2622
|
+
canCratesIoServe(p) &&
|
|
1847
2623
|
!servedLocally.has(p),
|
|
1848
2624
|
);
|
|
1849
|
-
// Two URLs per crate — the crate document and
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
2625
|
+
// Two URLs per crate — the crate document and, for the caller that wants
|
|
2626
|
+
// publisher identity, its owners — fetched for the whole list at once. The
|
|
2627
|
+
// `workspace` and source guards below are mirrored here so the batch does
|
|
2628
|
+
// not request anything the loop would have skipped.
|
|
1852
2629
|
const prefetched = await prefetchJson(
|
|
1853
2630
|
prefetchEnabled()
|
|
1854
|
-
? needsRegistry.flatMap((p) =>
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2631
|
+
? needsRegistry.flatMap((p) => {
|
|
2632
|
+
const crateUrl = CRATES_URL + p.name;
|
|
2633
|
+
return wantsOwners
|
|
2634
|
+
? [{ url: crateUrl }, { url: `${crateUrl}/owners` }]
|
|
2635
|
+
: [{ url: crateUrl }];
|
|
2636
|
+
})
|
|
1858
2637
|
: [],
|
|
1859
2638
|
);
|
|
1860
2639
|
for (const p of pkgList) {
|
|
@@ -1863,6 +2642,10 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1863
2642
|
cdepList.push(p);
|
|
1864
2643
|
continue;
|
|
1865
2644
|
}
|
|
2645
|
+
if (!canCratesIoServe(p)) {
|
|
2646
|
+
cdepList.push(p);
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
1866
2649
|
if (servedLocally.has(p)) {
|
|
1867
2650
|
cdepList.push(p);
|
|
1868
2651
|
continue;
|
|
@@ -1877,32 +2660,32 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1877
2660
|
// this process issued it.
|
|
1878
2661
|
const res =
|
|
1879
2662
|
prefetchedResponse(prefetched, crateUrl) ||
|
|
1880
|
-
(await
|
|
1881
|
-
cdxgenAgent.get(crateUrl, {
|
|
1882
|
-
responseType: "json",
|
|
1883
|
-
}),
|
|
1884
|
-
));
|
|
2663
|
+
(await gatedGet(crateUrl, { responseType: "json" }));
|
|
1885
2664
|
let ownersRes;
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2665
|
+
if (wantsOwners) {
|
|
2666
|
+
try {
|
|
2667
|
+
const ownersUrl = `${crateUrl}/owners`;
|
|
2668
|
+
ownersRes =
|
|
2669
|
+
prefetchedResponse(prefetched, ownersUrl) ||
|
|
2670
|
+
(await gatedGet(ownersUrl, { responseType: "json" }));
|
|
2671
|
+
} catch (_err) {
|
|
2672
|
+
ownersRes = undefined;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
// A crate whose version crates.io does not list was not published
|
|
2676
|
+
// there: a path or git crate, or one from another registry, that
|
|
2677
|
+
// shares a crates.io name. Nothing in that document describes it.
|
|
2678
|
+
const versionToUse = res?.body?.versions?.find(
|
|
2679
|
+
(aversion) => aversion.num === p.version,
|
|
2680
|
+
);
|
|
2681
|
+
if (!versionToUse) {
|
|
2682
|
+
if (DEBUG_MODE) {
|
|
2683
|
+
console.log(
|
|
2684
|
+
`crates.io has no version ${p.version} of ${p.name}; leaving it unchanged.`,
|
|
2685
|
+
);
|
|
1905
2686
|
}
|
|
2687
|
+
cdepList.push(p);
|
|
2688
|
+
continue;
|
|
1906
2689
|
}
|
|
1907
2690
|
const body = res.body.crate;
|
|
1908
2691
|
p.description = body.description;
|
|
@@ -1915,10 +2698,6 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1915
2698
|
if (body.homepage && body.homepage !== body.repository) {
|
|
1916
2699
|
p.homepage = { url: body.homepage };
|
|
1917
2700
|
}
|
|
1918
|
-
// Use the latest version if none specified
|
|
1919
|
-
if (!p.version) {
|
|
1920
|
-
p.version = body.newest_version;
|
|
1921
|
-
}
|
|
1922
2701
|
if (!p._integrity && versionToUse.checksum) {
|
|
1923
2702
|
p._integrity = normalizeCargoIntegrity(versionToUse.checksum);
|
|
1924
2703
|
}
|
|
@@ -1978,12 +2757,17 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1978
2757
|
* @param {Object} p Package component to enrich in place.
|
|
1979
2758
|
* @returns {boolean} true when crates.io can be skipped for this crate.
|
|
1980
2759
|
*/
|
|
1981
|
-
function applyCargoCacheMetadata(p) {
|
|
1982
|
-
const local = readCargoCacheMetadata(p.name, p.version);
|
|
1983
|
-
if (!local?.license) {
|
|
2760
|
+
async function applyCargoCacheMetadata(p) {
|
|
2761
|
+
const local = await readCargoCacheMetadata(p.name, p.version);
|
|
2762
|
+
if (!local?.license && !local?.licenses) {
|
|
1984
2763
|
return false;
|
|
1985
2764
|
}
|
|
1986
|
-
|
|
2765
|
+
if (local.license) {
|
|
2766
|
+
p.license = local.license;
|
|
2767
|
+
}
|
|
2768
|
+
if (local.licenses && !p.licenses) {
|
|
2769
|
+
p.licenses = local.licenses;
|
|
2770
|
+
}
|
|
1987
2771
|
if (local.description) {
|
|
1988
2772
|
p.description = local.description;
|
|
1989
2773
|
}
|
|
@@ -2041,14 +2825,47 @@ function applyCargoCacheMetadata(p) {
|
|
|
2041
2825
|
return true;
|
|
2042
2826
|
}
|
|
2043
2827
|
|
|
2828
|
+
// pub.dev also answers under its old name, which lockfiles written before
|
|
2829
|
+
// Dart 2.19 record for every hosted package.
|
|
2830
|
+
const PUB_DEV_HOSTS = new Set(["pub.dev", "pub.dartlang.org"]);
|
|
2831
|
+
|
|
2832
|
+
/**
|
|
2833
|
+
* The registry a hosted Dart package records, or undefined when that is
|
|
2834
|
+
* pub.dev or the registry cdxgen is configured to ask.
|
|
2835
|
+
*
|
|
2836
|
+
* @param {Object} p Package object with the pubspec.lock properties.
|
|
2837
|
+
* @param {string} pubDevUrl The registry cdxgen asks by default.
|
|
2838
|
+
* @returns {string|undefined}
|
|
2839
|
+
*/
|
|
2840
|
+
function pubHostedRegistry(p, pubDevUrl) {
|
|
2841
|
+
const registry = p.properties?.find(
|
|
2842
|
+
(prop) => prop.name === "cdx:pub:registry" && prop.value,
|
|
2843
|
+
)?.value;
|
|
2844
|
+
const host = parseUrl(registry)?.host;
|
|
2845
|
+
if (!host || PUB_DEV_HOSTS.has(host) || host === parseUrl(pubDevUrl)?.host) {
|
|
2846
|
+
return undefined;
|
|
2847
|
+
}
|
|
2848
|
+
return registry;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2044
2851
|
/**
|
|
2045
2852
|
* Method to retrieve metadata for dart packages by querying pub.dev
|
|
2046
2853
|
*
|
|
2854
|
+
* A hosted package whose lockfile names another registry is looked up there;
|
|
2855
|
+
* the score endpoint that carries the licence tag is pub.dev's, so a private
|
|
2856
|
+
* registry is asked only for the package document. In secure mode cdxgen does
|
|
2857
|
+
* not contact a registry the scanned project names, so such a package is not
|
|
2858
|
+
* looked up at all.
|
|
2859
|
+
*
|
|
2047
2860
|
* @param {Array} pkgList Package list
|
|
2861
|
+
* @param {Object} [options] Options
|
|
2862
|
+
* @param {boolean} [options.secureMode] Whether secure mode is on. Defaults to
|
|
2863
|
+
* the process-wide setting.
|
|
2048
2864
|
*/
|
|
2049
|
-
export async function getDartMetadata(pkgList) {
|
|
2865
|
+
export async function getDartMetadata(pkgList, options = {}) {
|
|
2050
2866
|
const PUB_DEV_URL =
|
|
2051
2867
|
readEnvironmentVariable("PUB_DEV_URL") || "https://pub.dev";
|
|
2868
|
+
const secureMode = options.secureMode ?? isSecureMode;
|
|
2052
2869
|
const PUB_LICENSE_REGEX = /^license:/i;
|
|
2053
2870
|
const OPTIONS = {
|
|
2054
2871
|
responseType: "json",
|
|
@@ -2056,31 +2873,50 @@ export async function getDartMetadata(pkgList) {
|
|
|
2056
2873
|
Accept: PUB_ACCEPT,
|
|
2057
2874
|
},
|
|
2058
2875
|
};
|
|
2876
|
+
const apiBase = (url) => (url.endsWith("/") ? url.slice(0, -1) : url);
|
|
2059
2877
|
|
|
2060
2878
|
const cdepList = [];
|
|
2879
|
+
// Where each package is asked, decided once so the batch and the loop agree.
|
|
2061
2880
|
// pub.dev needs the package document and its score document; both carry the
|
|
2062
|
-
// vendor Accept header, which is part of the cache key on the Rust side.
|
|
2881
|
+
// vendor Accept header, which is part of the cache key on the Rust side. A
|
|
2882
|
+
// package hosted elsewhere is asked only for its package document.
|
|
2883
|
+
const lookups = new Map();
|
|
2884
|
+
for (const p of pkgList) {
|
|
2885
|
+
const registry = pubHostedRegistry(p, PUB_DEV_URL);
|
|
2886
|
+
if (registry && secureMode) {
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
lookups.set(p, {
|
|
2890
|
+
registry,
|
|
2891
|
+
url: `${apiBase(registry || PUB_DEV_URL)}/api/packages/${p.name}/versions/${p.version}`,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2063
2894
|
const prefetched = await prefetchJson(
|
|
2064
2895
|
prefetchEnabled()
|
|
2065
|
-
?
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2896
|
+
? [...lookups.values()].flatMap(({ registry, url }) =>
|
|
2897
|
+
registry
|
|
2898
|
+
? [{ url, accept: PUB_ACCEPT }]
|
|
2899
|
+
: [
|
|
2900
|
+
{ url, accept: PUB_ACCEPT },
|
|
2901
|
+
{ url: `${url}/score`, accept: PUB_ACCEPT },
|
|
2902
|
+
],
|
|
2903
|
+
)
|
|
2072
2904
|
: [],
|
|
2073
2905
|
);
|
|
2074
2906
|
for (const p of pkgList) {
|
|
2907
|
+
const lookup = lookups.get(p);
|
|
2908
|
+
if (!lookup) {
|
|
2909
|
+
cdepList.push(p);
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2075
2912
|
try {
|
|
2913
|
+
const { registry, url: PUB_PACKAGE_URL } = lookup;
|
|
2076
2914
|
if (DEBUG_MODE) {
|
|
2077
|
-
console.log(`Querying ${PUB_DEV_URL} for ${p.name}`);
|
|
2915
|
+
console.log(`Querying ${registry || PUB_DEV_URL} for ${p.name}`);
|
|
2078
2916
|
}
|
|
2079
|
-
const PUB_PACKAGE_URL = `${PUB_DEV_URL}/api/packages/${p.name}/versions/${p.version}`;
|
|
2080
|
-
const PUB_PACKAGE_SCORE_URL = `${PUB_PACKAGE_URL}/score`;
|
|
2081
2917
|
const res =
|
|
2082
2918
|
prefetchedResponse(prefetched, PUB_PACKAGE_URL) ||
|
|
2083
|
-
(await
|
|
2919
|
+
(await gatedGet(PUB_PACKAGE_URL, OPTIONS));
|
|
2084
2920
|
if (res?.body) {
|
|
2085
2921
|
const pubspec = res.body.pubspec;
|
|
2086
2922
|
p.description = pubspec.description;
|
|
@@ -2090,18 +2926,21 @@ export async function getDartMetadata(pkgList) {
|
|
|
2090
2926
|
if (pubspec.homepage) {
|
|
2091
2927
|
p.homepage = { url: pubspec.homepage };
|
|
2092
2928
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2929
|
+
if (!registry) {
|
|
2930
|
+
const PUB_PACKAGE_SCORE_URL = `${PUB_PACKAGE_URL}/score`;
|
|
2931
|
+
const score =
|
|
2932
|
+
prefetchedResponse(prefetched, PUB_PACKAGE_SCORE_URL) ||
|
|
2933
|
+
(await gatedGet(PUB_PACKAGE_SCORE_URL, OPTIONS));
|
|
2934
|
+
if (score?.body) {
|
|
2935
|
+
const tags = score.body.tags;
|
|
2936
|
+
const license = tags.find((tag) => PUB_LICENSE_REGEX.test(tag));
|
|
2937
|
+
if (license) {
|
|
2938
|
+
p.license = spdxLicenses.find(
|
|
2939
|
+
(spdxLicense) =>
|
|
2940
|
+
spdxLicense.toLowerCase() ===
|
|
2941
|
+
license.replace(PUB_LICENSE_REGEX, "").toLowerCase(),
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2105
2944
|
}
|
|
2106
2945
|
}
|
|
2107
2946
|
cdepList.push(p);
|
|
@@ -2152,6 +2991,60 @@ export function normalizeCargoIntegrity(integrity) {
|
|
|
2152
2991
|
return undefined;
|
|
2153
2992
|
}
|
|
2154
2993
|
|
|
2994
|
+
/**
|
|
2995
|
+
* The POM stored next to a jar, as in the Maven repository layout.
|
|
2996
|
+
*
|
|
2997
|
+
* @param {string} jarPath Path to a jar.
|
|
2998
|
+
* @returns {string|undefined} Path to `<name>.pom` when it exists.
|
|
2999
|
+
*/
|
|
3000
|
+
function siblingPomFile(jarPath) {
|
|
3001
|
+
if (!jarPath?.endsWith(".jar")) {
|
|
3002
|
+
return undefined;
|
|
3003
|
+
}
|
|
3004
|
+
const pomPath = `${jarPath.slice(0, -".jar".length)}.pom`;
|
|
3005
|
+
return safeExistsSync(pomPath) ? pomPath : undefined;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* Read the groupId, artifactId and version a POM declares, inheriting the
|
|
3010
|
+
* groupId and version from its parent element when they are omitted.
|
|
3011
|
+
*
|
|
3012
|
+
* @param {string} pomPath Path to a POM.
|
|
3013
|
+
* @returns {{groupId: string, artifactId: string, version: string}|undefined}
|
|
3014
|
+
*/
|
|
3015
|
+
function readPomCoordinates(pomPath) {
|
|
3016
|
+
try {
|
|
3017
|
+
const pomData = parsePomXml(readFileSync(pomPath, "utf-8"));
|
|
3018
|
+
if (pomData?.groupId && pomData.artifactId && pomData.version) {
|
|
3019
|
+
return {
|
|
3020
|
+
groupId: pomData.groupId,
|
|
3021
|
+
artifactId: pomData.artifactId,
|
|
3022
|
+
version: pomData.version,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
} catch (_err) {
|
|
3026
|
+
// An unreadable POM contributes nothing.
|
|
3027
|
+
}
|
|
3028
|
+
return undefined;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Whether a directory lies inside cdxgen's temp directory or the OS temp
|
|
3033
|
+
* directory. Cleanup in {@link extractJarArchive} is limited to such paths.
|
|
3034
|
+
*
|
|
3035
|
+
* @param {string} dir Directory to check.
|
|
3036
|
+
* @returns {boolean}
|
|
3037
|
+
*/
|
|
3038
|
+
function isInsideTmpDir(dir) {
|
|
3039
|
+
if (!dir) {
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
return [getTmpDir(), tmpdir()].some((root) => {
|
|
3043
|
+
const rel = relative(resolve(root), resolve(dir));
|
|
3044
|
+
return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2155
3048
|
/**
|
|
2156
3049
|
* Method to extract a war or ear file
|
|
2157
3050
|
*
|
|
@@ -2168,23 +3061,12 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2168
3061
|
// reported, as file components with zero-confidence identity evidence.
|
|
2169
3062
|
const unresolvedJars = [];
|
|
2170
3063
|
const fname = basename(jarFile);
|
|
2171
|
-
|
|
2172
|
-
//
|
|
2173
|
-
|
|
2174
|
-
//
|
|
2175
|
-
|
|
2176
|
-
// For now check for presence of both .pom and MANIFEST.MF files.
|
|
2177
|
-
if (jarFile.endsWith(".jar")) {
|
|
2178
|
-
pomname = jarFile.replace(".jar", ".pom");
|
|
2179
|
-
}
|
|
3064
|
+
// A POM next to the archive, as in the Maven repository layout, names its
|
|
3065
|
+
// coordinates. It is read where it is. The archive itself is always copied
|
|
3066
|
+
// into tempDir and extracted there, so nothing is ever written to or removed
|
|
3067
|
+
// from the directory being scanned.
|
|
3068
|
+
const siblingPom = siblingPomFile(jarFile);
|
|
2180
3069
|
if (
|
|
2181
|
-
pomname &&
|
|
2182
|
-
safeExistsSync(pomname) &&
|
|
2183
|
-
manifestname &&
|
|
2184
|
-
safeExistsSync(manifestname)
|
|
2185
|
-
) {
|
|
2186
|
-
tempDir = dirname(jarFile);
|
|
2187
|
-
} else if (
|
|
2188
3070
|
!safeExistsSync(join(tempDir, fname)) &&
|
|
2189
3071
|
safeExistsSync(jarFile) &&
|
|
2190
3072
|
lstatSync(jarFile).isFile()
|
|
@@ -2192,6 +3074,8 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2192
3074
|
// Only copy if the file doesn't exist
|
|
2193
3075
|
safeCopyFileSync(jarFile, join(tempDir, fname), constants.COPYFILE_FICLONE);
|
|
2194
3076
|
}
|
|
3077
|
+
const outerCopy = join(tempDir, fname);
|
|
3078
|
+
let outerExtracted = false;
|
|
2195
3079
|
const env = {
|
|
2196
3080
|
...process.env,
|
|
2197
3081
|
};
|
|
@@ -2207,11 +3091,11 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2207
3091
|
jarFile.endsWith(".hpi") ||
|
|
2208
3092
|
jarFile.endsWith(".jar")
|
|
2209
3093
|
) {
|
|
2210
|
-
if (safeExistsSync(
|
|
3094
|
+
if (safeExistsSync(outerCopy)) {
|
|
2211
3095
|
try {
|
|
2212
|
-
const zip = new StreamZip.async({ file:
|
|
3096
|
+
const zip = new StreamZip.async({ file: outerCopy });
|
|
2213
3097
|
const extracted = await safeExtractArchive(
|
|
2214
|
-
|
|
3098
|
+
outerCopy,
|
|
2215
3099
|
tempDir,
|
|
2216
3100
|
async () => {
|
|
2217
3101
|
await zip.extract(null, tempDir);
|
|
@@ -2221,6 +3105,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2221
3105
|
if (!extracted) {
|
|
2222
3106
|
return pkgList;
|
|
2223
3107
|
}
|
|
3108
|
+
outerExtracted = true;
|
|
2224
3109
|
} catch (e) {
|
|
2225
3110
|
console.log("Unable to extract %s. Skipping.", join(tempDir, fname), e);
|
|
2226
3111
|
return pkgList;
|
|
@@ -2251,32 +3136,41 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2251
3136
|
}
|
|
2252
3137
|
continue;
|
|
2253
3138
|
}
|
|
2254
|
-
pomname = jf.replace(".jar", ".pom");
|
|
2255
3139
|
const jarname = basename(jf);
|
|
2256
|
-
// Ignore test jars
|
|
3140
|
+
// Ignore test, sources and javadoc jars: none of them is a runtime
|
|
3141
|
+
// component, and identifying them only costs lookups.
|
|
2257
3142
|
if (
|
|
2258
3143
|
jarname.endsWith("-tests.jar") ||
|
|
2259
|
-
jarname.endsWith("-test-sources.jar")
|
|
3144
|
+
jarname.endsWith("-test-sources.jar") ||
|
|
3145
|
+
isDocumentationJar(jarname)
|
|
2260
3146
|
) {
|
|
2261
3147
|
if (DEBUG_MODE) {
|
|
2262
3148
|
console.log(`Skipping tests jar ${jarname}`);
|
|
2263
3149
|
}
|
|
2264
3150
|
continue;
|
|
2265
3151
|
}
|
|
2266
|
-
const
|
|
3152
|
+
const isOuterArchive = jf === outerCopy || jf === jarFile;
|
|
3153
|
+
// The outer archive is already extracted into tempDir. Every nested
|
|
3154
|
+
// archive gets a directory of its own, so the META-INF of the war or of
|
|
3155
|
+
// a previously read jar can never be mistaken for this jar's.
|
|
3156
|
+
const reuseOuter = jf === outerCopy && outerExtracted;
|
|
3157
|
+
const workDir = reuseOuter
|
|
3158
|
+
? tempDir
|
|
3159
|
+
: safeMkdtempSync(join(tempDir, "nested-"));
|
|
3160
|
+
const manifestDir = join(workDir, "META-INF");
|
|
2267
3161
|
const manifestFile = join(manifestDir, "MANIFEST.MF");
|
|
2268
3162
|
const mavenDir = join(manifestDir, "maven");
|
|
2269
3163
|
let jarResult = {
|
|
2270
3164
|
status: 1,
|
|
2271
3165
|
};
|
|
2272
|
-
if (
|
|
3166
|
+
if (reuseOuter) {
|
|
2273
3167
|
jarResult = { status: 0 };
|
|
2274
|
-
} else {
|
|
3168
|
+
} else if (workDir) {
|
|
2275
3169
|
// Unzip natively
|
|
2276
3170
|
try {
|
|
2277
3171
|
const zip = new StreamZip.async({ file: jf });
|
|
2278
|
-
const extracted = await safeExtractArchive(jf,
|
|
2279
|
-
await zip.extract(null,
|
|
3172
|
+
const extracted = await safeExtractArchive(jf, workDir, async () => {
|
|
3173
|
+
await zip.extract(null, workDir);
|
|
2280
3174
|
});
|
|
2281
3175
|
await zip.close();
|
|
2282
3176
|
jarResult = { status: extracted ? 0 : 1 };
|
|
@@ -2291,36 +3185,79 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2291
3185
|
// When maven descriptor is available take group, name and version from pom.properties
|
|
2292
3186
|
// META-INF/maven/${groupId}/${artifactId}/pom.properties
|
|
2293
3187
|
// see https://maven.apache.org/shared/maven-archiver/index.html
|
|
2294
|
-
const pomProperties = getPomPropertiesFromMavenDir(mavenDir);
|
|
3188
|
+
const pomProperties = getPomPropertiesFromMavenDir(mavenDir, jarname);
|
|
2295
3189
|
let group = pomProperties["groupId"];
|
|
2296
3190
|
let name = pomProperties["artifactId"];
|
|
2297
3191
|
let version = pomProperties["version"];
|
|
2298
3192
|
let confidence = 0.5;
|
|
2299
3193
|
let technique = "manifest-analysis";
|
|
3194
|
+
let classifier;
|
|
3195
|
+
// A POM stored next to the archive names the same coordinates.
|
|
3196
|
+
const identityPom = isOuterArchive ? siblingPom : siblingPomFile(jf);
|
|
3197
|
+
if ((!group || !name || !version) && identityPom) {
|
|
3198
|
+
const pomData = readPomCoordinates(identityPom);
|
|
3199
|
+
if (pomData) {
|
|
3200
|
+
group = pomData.groupId;
|
|
3201
|
+
name = pomData.artifactId;
|
|
3202
|
+
version = pomData.version;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
// A jar inside a local Maven repository or Gradle cache is named by
|
|
3206
|
+
// where it sits.
|
|
3207
|
+
if ((!group || !name || !version) && isOuterArchive) {
|
|
3208
|
+
const coordinates = inferMavenCoordinatesFromPath(jarFile);
|
|
3209
|
+
if (coordinates?.extension === "jar") {
|
|
3210
|
+
group = coordinates.group;
|
|
3211
|
+
name = coordinates.name;
|
|
3212
|
+
version = coordinates.version;
|
|
3213
|
+
classifier = coordinates.classifier;
|
|
3214
|
+
technique = "filename";
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
// A jar that was copied out of a local cache is known by its SHA-1,
|
|
3218
|
+
// which answers what the Maven Central search would be asked.
|
|
3219
|
+
let sha;
|
|
3220
|
+
if (!group || !name || !version) {
|
|
3221
|
+
sha = await checksumFile("sha1", jf);
|
|
3222
|
+
const indexed = findMavenCoordinatesBySha1(sha);
|
|
3223
|
+
if (indexed) {
|
|
3224
|
+
group = indexed.group;
|
|
3225
|
+
name = indexed.name;
|
|
3226
|
+
version = indexed.version;
|
|
3227
|
+
classifier = indexed.classifier;
|
|
3228
|
+
technique = "hash-comparison";
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
2300
3231
|
if (
|
|
2301
3232
|
(!group || !name || !version) &&
|
|
2302
3233
|
SEARCH_MAVEN_ORG &&
|
|
2303
|
-
|
|
3234
|
+
!isHostCircuitOpen(MAVEN_SEARCH_HOST)
|
|
2304
3235
|
) {
|
|
2305
3236
|
try {
|
|
2306
|
-
|
|
2307
|
-
const searchurl = `https
|
|
3237
|
+
sha = sha || (await checksumFile("sha1", jf));
|
|
3238
|
+
const searchurl = `https://${MAVEN_SEARCH_HOST}/solrsearch/select?q=1:%22${sha}%22&rows=20&wt=json`;
|
|
2308
3239
|
// One Solr query per unidentified jar, against Central's origin
|
|
2309
3240
|
// rather than its CDN. A directory of jars issues these back to
|
|
2310
3241
|
// back, so they go through the same per-host gate the batch pool
|
|
2311
|
-
// uses (500 ms, serial) instead of a second, ungoverned path.
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
3242
|
+
// uses (500 ms, serial) instead of a second, ungoverned path. A
|
|
3243
|
+
// repeated hash is answered from the response cache without
|
|
3244
|
+
// waiting for that gate.
|
|
3245
|
+
const res = await withHostRateLimit(
|
|
3246
|
+
searchurl,
|
|
3247
|
+
(gate) =>
|
|
3248
|
+
cdxgenAgent.get(searchurl, {
|
|
3249
|
+
...gate,
|
|
3250
|
+
responseType: "json",
|
|
3251
|
+
timeout: {
|
|
3252
|
+
lookup: 1000,
|
|
3253
|
+
connect: 5000,
|
|
3254
|
+
secureConnect: 5000,
|
|
3255
|
+
socket: 1000,
|
|
3256
|
+
send: 10000,
|
|
3257
|
+
response: 1000,
|
|
3258
|
+
},
|
|
3259
|
+
}),
|
|
3260
|
+
{ deferGate: true },
|
|
2324
3261
|
);
|
|
2325
3262
|
const data = res?.body ? res.body["response"] : undefined;
|
|
2326
3263
|
if (data && data["numFound"] === 1) {
|
|
@@ -2331,16 +3268,21 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2331
3268
|
technique = "hash-comparison";
|
|
2332
3269
|
}
|
|
2333
3270
|
} catch (err) {
|
|
3271
|
+
// Any failure other than "not found" pauses the search for a
|
|
3272
|
+
// while; a 429 has already done so. Only a rate limit or an
|
|
3273
|
+
// unanswered request is the host's doing and worth a warning; a
|
|
3274
|
+
// dry run or an allowlist block pauses it quietly.
|
|
2334
3275
|
if (err?.message && !err.message.includes("404")) {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
err.
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
3276
|
+
const rateLimited = err.message.includes("429");
|
|
3277
|
+
const unanswered =
|
|
3278
|
+
err.name === "RequestError" &&
|
|
3279
|
+
!err.options?.context?.activityBlocked;
|
|
3280
|
+
openHostCircuit(MAVEN_SEARCH_HOST, {
|
|
3281
|
+
cause: rateLimited
|
|
3282
|
+
? "answered HTTP 429 (rate limited)"
|
|
3283
|
+
: "did not answer",
|
|
3284
|
+
quiet: !rateLimited && !unanswered,
|
|
3285
|
+
});
|
|
2344
3286
|
}
|
|
2345
3287
|
}
|
|
2346
3288
|
}
|
|
@@ -2443,12 +3385,16 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2443
3385
|
value: jf,
|
|
2444
3386
|
},
|
|
2445
3387
|
];
|
|
3388
|
+
const qualifiers = { type: "jar" };
|
|
3389
|
+
if (classifier) {
|
|
3390
|
+
qualifiers.classifier = classifier;
|
|
3391
|
+
}
|
|
2446
3392
|
const purl = build({
|
|
2447
3393
|
type: "maven",
|
|
2448
3394
|
namespace: group || null,
|
|
2449
3395
|
name: name,
|
|
2450
3396
|
version: version || null,
|
|
2451
|
-
qualifiers
|
|
3397
|
+
qualifiers,
|
|
2452
3398
|
});
|
|
2453
3399
|
let namespaceValues;
|
|
2454
3400
|
let namespaceList;
|
|
@@ -2500,8 +3446,8 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2500
3446
|
}
|
|
2501
3447
|
}
|
|
2502
3448
|
const apkg = {
|
|
2503
|
-
group: group
|
|
2504
|
-
name: name
|
|
3449
|
+
group: group || "",
|
|
3450
|
+
name: name || "",
|
|
2505
3451
|
version,
|
|
2506
3452
|
purl,
|
|
2507
3453
|
evidence: {
|
|
@@ -2519,6 +3465,39 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2519
3465
|
},
|
|
2520
3466
|
properties,
|
|
2521
3467
|
};
|
|
3468
|
+
// The jar carries its own Maven descriptor, which holds the
|
|
3469
|
+
// description and licence no remote lookup is needed for, and a
|
|
3470
|
+
// manifest licence for the jars whose POM declares none.
|
|
3471
|
+
const descriptor = readEmbeddedMavenDescriptor(mavenDir, jarname);
|
|
3472
|
+
if (descriptor?.pomXml) {
|
|
3473
|
+
try {
|
|
3474
|
+
applyPomMetadata(
|
|
3475
|
+
apkg,
|
|
3476
|
+
xml2js(descriptor.pomXml, POM_XML_OPTIONS).project,
|
|
3477
|
+
descriptor.pomXml,
|
|
3478
|
+
);
|
|
3479
|
+
} catch (_err) {
|
|
3480
|
+
// An unparseable embedded POM contributes nothing.
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (!hasMavenLicense(apkg)) {
|
|
3484
|
+
let manifestText;
|
|
3485
|
+
try {
|
|
3486
|
+
manifestText =
|
|
3487
|
+
safeExistsSync(manifestFile) &&
|
|
3488
|
+
statSync(manifestFile).size <= 256 * 1024
|
|
3489
|
+
? readFileSync(manifestFile, { encoding: "utf-8" })
|
|
3490
|
+
: undefined;
|
|
3491
|
+
} catch (_err) {
|
|
3492
|
+
manifestText = undefined;
|
|
3493
|
+
}
|
|
3494
|
+
applyManifestLicenses(
|
|
3495
|
+
apkg,
|
|
3496
|
+
parseManifestLicenseList(
|
|
3497
|
+
parseJarManifest(manifestText)["Bundle-License"],
|
|
3498
|
+
),
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
2522
3501
|
pkgList.push(apkg);
|
|
2523
3502
|
} else {
|
|
2524
3503
|
unresolvedJars.push(jf);
|
|
@@ -2528,12 +3507,17 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2528
3507
|
}
|
|
2529
3508
|
}
|
|
2530
3509
|
try {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
3510
|
+
// Only directories this function created are removed. A caller's
|
|
3511
|
+
// tempDir is never the scanned directory, but the check keeps it that
|
|
3512
|
+
// way should a caller ever pass one.
|
|
3513
|
+
if (workDir && workDir !== tempDir && isInsideTmpDir(workDir)) {
|
|
3514
|
+
safeRmSync(workDir, { recursive: true, force: true });
|
|
3515
|
+
} else if (
|
|
3516
|
+
workDir === tempDir &&
|
|
3517
|
+
isInsideTmpDir(tempDir) &&
|
|
3518
|
+
safeExistsSync(manifestDir)
|
|
3519
|
+
) {
|
|
3520
|
+
safeRmSync(manifestDir, { recursive: true, force: true });
|
|
2537
3521
|
}
|
|
2538
3522
|
} catch (_err) {
|
|
2539
3523
|
// ignore cleanup errors
|
|
@@ -2629,18 +3613,51 @@ async function unresolvedJarComponents(unresolvedJars, jarFile, tempDir) {
|
|
|
2629
3613
|
*/
|
|
2630
3614
|
const METADATA_VERSION_PROP = "cdx:nuget:metadata_version";
|
|
2631
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Fetch one JSON document through the batch pool, so it shares the rate gate
|
|
3618
|
+
* and the disk cache with every other registry request. A direct request is
|
|
3619
|
+
* made only when batched fetching is off, which is the cassette replay the
|
|
3620
|
+
* golden corpus runs under.
|
|
3621
|
+
*
|
|
3622
|
+
* @param {string} url Document URL
|
|
3623
|
+
* @returns {Promise<{body: Object}|undefined>} The response, or undefined when
|
|
3624
|
+
* the batch answered definitely that the document is not available
|
|
3625
|
+
*/
|
|
3626
|
+
async function batchedJsonDocument(url) {
|
|
3627
|
+
if (prefetchEnabled()) {
|
|
3628
|
+
const fetched = await prefetchJson([{ url, responseType: "json" }]);
|
|
3629
|
+
const entry = fetched.get(url);
|
|
3630
|
+
return entry?.ok ? { body: entry.body } : undefined;
|
|
3631
|
+
}
|
|
3632
|
+
return await gatedGet(url, { responseType: "json" });
|
|
3633
|
+
}
|
|
3634
|
+
|
|
2632
3635
|
async function getNugetUrl() {
|
|
2633
3636
|
const req = "https://api.nuget.org/v3/index.json";
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3637
|
+
const fallbackUrl = "https://api.nuget.org/v3/registration3/";
|
|
3638
|
+
let res;
|
|
3639
|
+
try {
|
|
3640
|
+
res = await batchedJsonDocument(req);
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
// Package lookups already degrade to unenriched components on error. An
|
|
3643
|
+
// unreachable service index used to reject here instead and abort the
|
|
3644
|
+
// whole BOM, so fall back to the well-known registration base.
|
|
3645
|
+
thoughtLog(
|
|
3646
|
+
`Unable to read the NuGet service index (${err.code || err.message}). Using ${fallbackUrl}.`,
|
|
3647
|
+
);
|
|
3648
|
+
return fallbackUrl;
|
|
3649
|
+
}
|
|
3650
|
+
if (!res) {
|
|
3651
|
+
thoughtLog(`Unable to read the NuGet service index. Using ${fallbackUrl}.`);
|
|
3652
|
+
return fallbackUrl;
|
|
3653
|
+
}
|
|
3654
|
+
const urls = res?.body?.resources || [];
|
|
2638
3655
|
for (const resource of urls) {
|
|
2639
3656
|
if (resource["@type"] === "RegistrationsBaseUrl/3.6.0") {
|
|
2640
3657
|
return resource["@id"];
|
|
2641
3658
|
}
|
|
2642
3659
|
}
|
|
2643
|
-
return
|
|
3660
|
+
return fallbackUrl;
|
|
2644
3661
|
}
|
|
2645
3662
|
|
|
2646
3663
|
/**
|
|
@@ -2697,10 +3714,15 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2697
3714
|
const body = [];
|
|
2698
3715
|
const newBody = [];
|
|
2699
3716
|
const indexUrl = `${NUGET_URL + np.name.toLowerCase()}/index.json`;
|
|
2700
|
-
let res
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3717
|
+
let res;
|
|
3718
|
+
try {
|
|
3719
|
+
res =
|
|
3720
|
+
prefetchedResponse(prefetchedNugetIndex, indexUrl) ||
|
|
3721
|
+
(await batchedJsonDocument(indexUrl));
|
|
3722
|
+
} catch {
|
|
3723
|
+
res = undefined;
|
|
3724
|
+
}
|
|
3725
|
+
const items = res?.body?.items;
|
|
2704
3726
|
if (!items?.[0]) {
|
|
2705
3727
|
return [np, newBody, body];
|
|
2706
3728
|
}
|
|
@@ -2734,8 +3756,13 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2734
3756
|
// at a page that must be fetched.
|
|
2735
3757
|
let leaves = item.items;
|
|
2736
3758
|
if (!leaves) {
|
|
2737
|
-
|
|
2738
|
-
|
|
3759
|
+
// The page URL is only known once the index answered, but it still
|
|
3760
|
+
// goes through the batch pool, its rate gate and its disk cache.
|
|
3761
|
+
const page = await batchedJsonDocument(item["@id"]);
|
|
3762
|
+
if (!page) {
|
|
3763
|
+
return [np, newBody, body];
|
|
3764
|
+
}
|
|
3765
|
+
leaves = page.body.items;
|
|
2739
3766
|
}
|
|
2740
3767
|
for (const i of leaves.reverse()) {
|
|
2741
3768
|
if (
|
|
@@ -2751,6 +3778,200 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2751
3778
|
return [np, newBody];
|
|
2752
3779
|
}
|
|
2753
3780
|
|
|
3781
|
+
/**
|
|
3782
|
+
* The global NuGet packages folder of this machine.
|
|
3783
|
+
*
|
|
3784
|
+
* @returns {string} `$NUGET_PACKAGES`, or the default `~/.nuget/packages`.
|
|
3785
|
+
*/
|
|
3786
|
+
function nugetPackagesRoot() {
|
|
3787
|
+
return resolve(
|
|
3788
|
+
readEnvironmentVariable("NUGET_PACKAGES") ||
|
|
3789
|
+
join(homedir(), ".nuget", "packages"),
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Read the nuspec of a package version installed in the global packages
|
|
3795
|
+
* folder, without asking the NuGet service.
|
|
3796
|
+
*
|
|
3797
|
+
* The folder and the nuspec inside it are named by the lower-cased package id,
|
|
3798
|
+
* under a folder named by the lower-cased version.
|
|
3799
|
+
*
|
|
3800
|
+
* @param {Object} p Component with `name` and an exact `version`
|
|
3801
|
+
* @returns {{license: string|undefined, description: string|undefined,
|
|
3802
|
+
* author: string|undefined, repository: {url: string}|undefined,
|
|
3803
|
+
* source: string}|undefined} The nuspec's fields, or undefined when the
|
|
3804
|
+
* package is not installed here or states no exact version
|
|
3805
|
+
*/
|
|
3806
|
+
function readLocalNugetNuspec(p) {
|
|
3807
|
+
if (
|
|
3808
|
+
!p?.name ||
|
|
3809
|
+
!p.version ||
|
|
3810
|
+
["0.0.0", "latest"].includes(p.version) ||
|
|
3811
|
+
p.version === "workspace"
|
|
3812
|
+
) {
|
|
3813
|
+
return undefined;
|
|
3814
|
+
}
|
|
3815
|
+
// The folder holds the lower-cased id and version. A name or version that is
|
|
3816
|
+
// not a single path segment cannot be a package there.
|
|
3817
|
+
const id = p.name.toLowerCase();
|
|
3818
|
+
const version = p.version.trim().toLowerCase();
|
|
3819
|
+
if (
|
|
3820
|
+
[id, version].some(
|
|
3821
|
+
(segment) =>
|
|
3822
|
+
segment.includes("/") ||
|
|
3823
|
+
segment.includes("\\") ||
|
|
3824
|
+
segment === "." ||
|
|
3825
|
+
segment === "..",
|
|
3826
|
+
)
|
|
3827
|
+
) {
|
|
3828
|
+
return undefined;
|
|
3829
|
+
}
|
|
3830
|
+
const nuspecFile = join(nugetPackagesRoot(), id, version, `${id}.nuspec`);
|
|
3831
|
+
if (!safeExistsSync(nuspecFile)) {
|
|
3832
|
+
return undefined;
|
|
3833
|
+
}
|
|
3834
|
+
let nuspec;
|
|
3835
|
+
try {
|
|
3836
|
+
nuspec = parseNuspecData(nuspecFile, readFileSync(nuspecFile, "utf-8"));
|
|
3837
|
+
} catch {
|
|
3838
|
+
return undefined;
|
|
3839
|
+
}
|
|
3840
|
+
const pkg = nuspec?.pkgList?.[0];
|
|
3841
|
+
if (!pkg?.name) {
|
|
3842
|
+
return undefined;
|
|
3843
|
+
}
|
|
3844
|
+
return {
|
|
3845
|
+
license: pkg.license,
|
|
3846
|
+
description: pkg.description,
|
|
3847
|
+
author: pkg.author,
|
|
3848
|
+
repository: pkg.repository,
|
|
3849
|
+
source: nuspecFile,
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/**
|
|
3854
|
+
* Apply the installed nuspec of a package to its component, and say whether it
|
|
3855
|
+
* answered everything the registry round would have been asked for.
|
|
3856
|
+
*
|
|
3857
|
+
* @param {Object} p Component, enriched in place
|
|
3858
|
+
* @returns {boolean} true when the registry can be skipped for this package
|
|
3859
|
+
*/
|
|
3860
|
+
function applyLocalNugetNuspec(p) {
|
|
3861
|
+
const local = readLocalNugetNuspec(p);
|
|
3862
|
+
if (!local) {
|
|
3863
|
+
return false;
|
|
3864
|
+
}
|
|
3865
|
+
if (!p.license && local.license) {
|
|
3866
|
+
p.license = local.license;
|
|
3867
|
+
}
|
|
3868
|
+
if (!p.description && local.description) {
|
|
3869
|
+
p.description = local.description;
|
|
3870
|
+
}
|
|
3871
|
+
if (!p.author && local.author) {
|
|
3872
|
+
p.author = local.author;
|
|
3873
|
+
}
|
|
3874
|
+
if (!p.repository && local.repository) {
|
|
3875
|
+
p.repository = local.repository;
|
|
3876
|
+
}
|
|
3877
|
+
if (!p.license || !p.description) {
|
|
3878
|
+
return false;
|
|
3879
|
+
}
|
|
3880
|
+
p.properties = p.properties || [];
|
|
3881
|
+
if (!p.properties.some((prop) => prop.name === "cdx:nuget:metadataSource")) {
|
|
3882
|
+
p.properties.push({
|
|
3883
|
+
name: "cdx:nuget:metadataSource",
|
|
3884
|
+
value: "local-nuspec",
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
return true;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
/**
|
|
3891
|
+
* Whether a licence is still the URL a package named rather than an id.
|
|
3892
|
+
*
|
|
3893
|
+
* @param {*} license Licence value.
|
|
3894
|
+
* @returns {boolean}
|
|
3895
|
+
*/
|
|
3896
|
+
function isUnresolvedLicenseUrl(license) {
|
|
3897
|
+
return typeof license === "string" && license.includes("://");
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
/**
|
|
3901
|
+
* Fill a component from the NuGet catalog entry of its release, keeping every
|
|
3902
|
+
* value the project already gave it.
|
|
3903
|
+
*
|
|
3904
|
+
* When the entry names no licence id, only a GitHub licence URL or project URL,
|
|
3905
|
+
* the repository's licence is the remaining source. Those lookups are returned
|
|
3906
|
+
* rather than made here, so the caller can batch them for every package.
|
|
3907
|
+
*
|
|
3908
|
+
* @param {Object} p Component, enriched in place
|
|
3909
|
+
* @param {{leaf: Object, metadataVersion?: string}} entry The registration leaf
|
|
3910
|
+
* the service returned, and the release it describes when the component
|
|
3911
|
+
* states no version
|
|
3912
|
+
* @returns {string[]} GitHub URLs to ask for the licence, in order of preference
|
|
3913
|
+
*/
|
|
3914
|
+
function applyNugetCatalogEntry(p, { leaf, metadataVersion }) {
|
|
3915
|
+
const repoLicenseUrls = [];
|
|
3916
|
+
const catalogEntry = leaf.catalogEntry || {};
|
|
3917
|
+
// The component states no version, so the description and licence below
|
|
3918
|
+
// were read from whichever release the lookup fell back to. That release is
|
|
3919
|
+
// recorded rather than adopted as the version: a version cdxgen chose is not
|
|
3920
|
+
// a version the project declared.
|
|
3921
|
+
if (!p.version && metadataVersion) {
|
|
3922
|
+
p.properties = p.properties || [];
|
|
3923
|
+
if (!p.properties.some((prop) => prop.name === METADATA_VERSION_PROP)) {
|
|
3924
|
+
p.properties.push({
|
|
3925
|
+
name: METADATA_VERSION_PROP,
|
|
3926
|
+
value: metadataVersion,
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
if (catalogEntry.description && !p.description) {
|
|
3931
|
+
p.description = catalogEntry.description;
|
|
3932
|
+
}
|
|
3933
|
+
if (catalogEntry.authors && !p.author) {
|
|
3934
|
+
p.author = catalogEntry.authors.trim();
|
|
3935
|
+
}
|
|
3936
|
+
if (
|
|
3937
|
+
!p.license &&
|
|
3938
|
+
catalogEntry.licenseExpression &&
|
|
3939
|
+
catalogEntry.licenseExpression !== ""
|
|
3940
|
+
) {
|
|
3941
|
+
p.license = findLicenseId(catalogEntry.licenseExpression);
|
|
3942
|
+
} else if (!p.license && catalogEntry.licenseUrl) {
|
|
3943
|
+
p.license = findLicenseId(catalogEntry.licenseUrl);
|
|
3944
|
+
if (isUnresolvedLicenseUrl(p.license) && isGitHubUrl(p.license)) {
|
|
3945
|
+
repoLicenseUrls.push(p.license);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
// Capture the tags
|
|
3949
|
+
if (catalogEntry.tags?.length && Array.isArray(catalogEntry.tags)) {
|
|
3950
|
+
p.tags = catalogEntry.tags.map((t) => t.toLowerCase().replaceAll(" ", "-"));
|
|
3951
|
+
}
|
|
3952
|
+
if (catalogEntry.projectUrl && !p.repository) {
|
|
3953
|
+
p.repository = { url: catalogEntry.projectUrl };
|
|
3954
|
+
// A versionless component gets the package's landing page. Pasting an
|
|
3955
|
+
// undefined version into the path produced a 404 link.
|
|
3956
|
+
p.homepage = {
|
|
3957
|
+
url: p.version
|
|
3958
|
+
? `https://www.nuget.org/packages/${p.name}/${p.version}/`
|
|
3959
|
+
: `https://www.nuget.org/packages/${p.name}/`,
|
|
3960
|
+
};
|
|
3961
|
+
// Only a licence that is missing or still a URL is looked for in the
|
|
3962
|
+
// repository. An id the package declared is its own answer, and asking
|
|
3963
|
+
// GitHub about every package would spend its small anonymous quota.
|
|
3964
|
+
if (
|
|
3965
|
+
(!p.license || isUnresolvedLicenseUrl(p.license)) &&
|
|
3966
|
+
typeof p.repository.url === "string" &&
|
|
3967
|
+
isGitHubUrl(p.repository.url)
|
|
3968
|
+
) {
|
|
3969
|
+
repoLicenseUrls.push(p.repository.url);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return repoLicenseUrls;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
2754
3975
|
/**
|
|
2755
3976
|
* Method to retrieve metadata for nuget packages
|
|
2756
3977
|
*
|
|
@@ -2765,14 +3986,21 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2765
3986
|
// Batch the per-package registration index requests. queryNuget's first
|
|
2766
3987
|
// request for each package is the index; the follow-up to a specific
|
|
2767
3988
|
// registration page depends on the index response and stays serial. Only
|
|
2768
|
-
// packages that would actually be queried (
|
|
2769
|
-
//
|
|
3989
|
+
// packages that would actually be queried (no remembered answer or error) are
|
|
3990
|
+
// prefetched, matching the loop's skip conditions.
|
|
2770
3991
|
const batchUrls = [];
|
|
2771
3992
|
const seenNugetUrls = new Set();
|
|
3993
|
+
// Packages the installed nuspec answered, so neither the batch nor the loop
|
|
3994
|
+
// asks the service about them.
|
|
3995
|
+
const servedLocally = new Set();
|
|
2772
3996
|
for (const p of pkgList) {
|
|
2773
3997
|
// A cached entry — a body or a recorded error — means the loop will not
|
|
2774
3998
|
// reach the network for this package, so the batch must not either.
|
|
2775
|
-
if (
|
|
3999
|
+
if (nugetEntries.has(`${p.name}|${p.version}`)) {
|
|
4000
|
+
continue;
|
|
4001
|
+
}
|
|
4002
|
+
if (applyLocalNugetNuspec(p)) {
|
|
4003
|
+
servedLocally.add(p);
|
|
2776
4004
|
continue;
|
|
2777
4005
|
}
|
|
2778
4006
|
const indexUrl = `${NUGET_URL + p.name.toLowerCase()}/index.json`;
|
|
@@ -2789,110 +4017,58 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2789
4017
|
ownIndexUrls.push(key);
|
|
2790
4018
|
}
|
|
2791
4019
|
}
|
|
4020
|
+
// Packages whose licence can only come from their GitHub repository, with
|
|
4021
|
+
// the URLs to try, so those lookups are batched once the loop is done.
|
|
4022
|
+
const repoLicenseLookups = [];
|
|
2792
4023
|
try {
|
|
2793
4024
|
for (const p of pkgList) {
|
|
2794
|
-
|
|
4025
|
+
// If there is a version, we can safely use the cache to retrieve the license
|
|
4026
|
+
// See: https://github.com/cdxgen/cdxgen/issues/352
|
|
4027
|
+
const cacheKey = `${p.name}|${p.version}`;
|
|
2795
4028
|
try {
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
cacheKey = `${p.name}|${p.version}`;
|
|
2799
|
-
let body = metadata_cache[cacheKey];
|
|
2800
|
-
|
|
2801
|
-
if (body?.error) {
|
|
4029
|
+
let entry = nugetEntries.get(cacheKey);
|
|
4030
|
+
if (entry?.error) {
|
|
2802
4031
|
cdepList.push(p);
|
|
2803
4032
|
continue;
|
|
2804
4033
|
}
|
|
2805
|
-
if (!
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (p.version !== np.version) {
|
|
2810
|
-
const oldRef = p["bom-ref"];
|
|
2811
|
-
// The purl is rebuilt alongside the bom-ref. Leaving it behind let a
|
|
2812
|
-
// component state one version in `version` and another - or none - in
|
|
2813
|
-
// its purl, which is what a scanner range matches on. See #4359.
|
|
2814
|
-
p.version = np.version;
|
|
2815
|
-
applyPurl(p, nugetPurl(np.name, np.version));
|
|
2816
|
-
if (oldRef && p["bom-ref"] !== oldRef) {
|
|
2817
|
-
depRepList[oldRef] = p["bom-ref"];
|
|
2818
|
-
}
|
|
4034
|
+
if (!entry) {
|
|
4035
|
+
if (servedLocally.has(p)) {
|
|
4036
|
+
cdepList.push(p);
|
|
4037
|
+
continue;
|
|
2819
4038
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
4039
|
+
const [np, leaves] = await queryNuget(p, NUGET_URL);
|
|
4040
|
+
entry = {
|
|
4041
|
+
leaf: leaves?.[0],
|
|
4042
|
+
version: np.version,
|
|
4043
|
+
metadataVersion: np.metadataVersion,
|
|
4044
|
+
};
|
|
4045
|
+
if (entry.leaf) {
|
|
4046
|
+
nugetEntries.set(cacheKey, entry);
|
|
2822
4047
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
(prop) => prop.name === METADATA_VERSION_PROP,
|
|
2834
|
-
)
|
|
2835
|
-
) {
|
|
2836
|
-
p.properties.push({
|
|
2837
|
-
name: METADATA_VERSION_PROP,
|
|
2838
|
-
value: np.metadataVersion,
|
|
2839
|
-
});
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
p.description = body.catalogEntry.description;
|
|
2843
|
-
if (body.catalogEntry.authors) {
|
|
2844
|
-
p.author = body.catalogEntry.authors.trim();
|
|
2845
|
-
}
|
|
2846
|
-
if (
|
|
2847
|
-
body.catalogEntry.licenseExpression &&
|
|
2848
|
-
body.catalogEntry.licenseExpression !== ""
|
|
2849
|
-
) {
|
|
2850
|
-
p.license = findLicenseId(body.catalogEntry.licenseExpression);
|
|
2851
|
-
} else if (body.catalogEntry.licenseUrl) {
|
|
2852
|
-
p.license = findLicenseId(body.catalogEntry.licenseUrl);
|
|
2853
|
-
if (typeof p.license === "string" && isGitHubUrl(p.license)) {
|
|
2854
|
-
p.license =
|
|
2855
|
-
(await getRepoLicense(p.license, undefined)) || p.license;
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
// Capture the tags
|
|
2859
|
-
if (
|
|
2860
|
-
body.catalogEntry?.tags?.length &&
|
|
2861
|
-
Array.isArray(body.catalogEntry.tags)
|
|
2862
|
-
) {
|
|
2863
|
-
p.tags = body.catalogEntry.tags.map((t) =>
|
|
2864
|
-
t.toLowerCase().replaceAll(" ", "-"),
|
|
2865
|
-
);
|
|
2866
|
-
}
|
|
2867
|
-
if (body.catalogEntry.projectUrl) {
|
|
2868
|
-
p.repository = { url: body.catalogEntry.projectUrl };
|
|
2869
|
-
// A versionless component gets the package's landing page. Pasting
|
|
2870
|
-
// an undefined version into the path produced a 404 link.
|
|
2871
|
-
p.homepage = {
|
|
2872
|
-
url: p.version
|
|
2873
|
-
? `https://www.nuget.org/packages/${p.name}/${p.version}/`
|
|
2874
|
-
: `https://www.nuget.org/packages/${p.name}/`,
|
|
2875
|
-
};
|
|
2876
|
-
if (
|
|
2877
|
-
(!p.license || typeof p.license === "string") &&
|
|
2878
|
-
typeof p.repository.url === "string" &&
|
|
2879
|
-
isGitHubUrl(p.repository.url)
|
|
2880
|
-
) {
|
|
2881
|
-
// license couldn't be properly identified and is still a url,
|
|
2882
|
-
// therefore trying to resolve license via repository
|
|
2883
|
-
p.license =
|
|
2884
|
-
(await getRepoLicense(p.repository.url, undefined)) ||
|
|
2885
|
-
p.license;
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
cdepList.push(p);
|
|
4048
|
+
}
|
|
4049
|
+
if (p.version !== entry.version) {
|
|
4050
|
+
const oldRef = p["bom-ref"];
|
|
4051
|
+
// The purl is rebuilt alongside the bom-ref. Leaving it behind let a
|
|
4052
|
+
// component state one version in `version` and another - or none - in
|
|
4053
|
+
// its purl, which is what a scanner range matches on. See #4359.
|
|
4054
|
+
p.version = entry.version;
|
|
4055
|
+
applyPurl(p, nugetPurl(p.name, entry.version));
|
|
4056
|
+
if (oldRef && p["bom-ref"] !== oldRef) {
|
|
4057
|
+
depRepList[oldRef] = p["bom-ref"];
|
|
2889
4058
|
}
|
|
2890
4059
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
4060
|
+
// A package the service does not know, such as a private one, stays in
|
|
4061
|
+
// the BOM as the project described it.
|
|
4062
|
+
if (entry.leaf) {
|
|
4063
|
+
const repoLicenseUrls = applyNugetCatalogEntry(p, entry);
|
|
4064
|
+
if (repoLicenseUrls.length) {
|
|
4065
|
+
repoLicenseLookups.push({ p, repoLicenseUrls });
|
|
4066
|
+
}
|
|
2894
4067
|
}
|
|
2895
4068
|
cdepList.push(p);
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
nugetEntries.set(cacheKey, { error: err.code });
|
|
4071
|
+
cdepList.push(p);
|
|
2896
4072
|
}
|
|
2897
4073
|
}
|
|
2898
4074
|
} finally {
|
|
@@ -2904,6 +4080,20 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2904
4080
|
prefetchedNugetIndex.delete(indexUrl);
|
|
2905
4081
|
}
|
|
2906
4082
|
}
|
|
4083
|
+
// The first URL of each package is batched. A second is asked only when the
|
|
4084
|
+
// first gave no licence, as it was before.
|
|
4085
|
+
await prefetchRepoLicenses(
|
|
4086
|
+
repoLicenseLookups.map(({ repoLicenseUrls }) => repoLicenseUrls[0]),
|
|
4087
|
+
);
|
|
4088
|
+
for (const { p, repoLicenseUrls } of repoLicenseLookups) {
|
|
4089
|
+
for (const repoUrl of repoLicenseUrls) {
|
|
4090
|
+
const license = await getRepoLicense(repoUrl, undefined);
|
|
4091
|
+
if (license) {
|
|
4092
|
+
p.license = license;
|
|
4093
|
+
break;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
2907
4097
|
const newDependencies = [].concat(dependencies);
|
|
2908
4098
|
if (depRepList && newDependencies.length) {
|
|
2909
4099
|
const changed = Object.keys(depRepList);
|