@cdxgen/cdxgen 13.4.0 → 13.6.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 +9 -9
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +29 -5
- package/bin/evinse.js +8 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/bin/validate.js +6 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/license-aliases.json +24 -2
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +407 -1
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -0
- package/data/rules/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +120 -6
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +182 -6
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +363 -66
- package/lib/cli/managedBom.js +853 -87
- package/lib/cli/nativeBom.js +267 -33
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +38 -8
- package/lib/core/fs.js +40 -19
- package/lib/core/httpClient.js +172 -46
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -0
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/core/toolRegistry.js +170 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +858 -244
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +438 -64
- package/lib/ecosystems/parsers-python.js +100 -12
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +544 -34
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +4 -1
- package/lib/ecosystems/scalasem.js +798 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +472 -61
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +110 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +41 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +639 -63
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +475 -18
- package/lib/inventory/evidenceUtils.js +447 -23
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +32 -3
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +271 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +438 -0
- package/lib/inventory/phpHostProbe.js +185 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +256 -5
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +866 -186
- package/lib/managers/docker.js +245 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +62 -13
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +198 -2
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +71 -6
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +231 -62
- package/lib/validator/bomValidator.js +32 -6
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +27 -8
- package/lib/validator/generated/validate-1.7.mjs +32 -13
- package/lib/validator/generated/validate-2.0.mjs +31 -12
- package/lib/validator/index.js +1 -1
- package/package.json +19 -18
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +22 -3
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +1 -17
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +20 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +70 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -2
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +2 -0
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +57 -21
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +14 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +19 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +88 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +33 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +162 -7
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +92 -8
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +44 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +38 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +10 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +24 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +17 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +26 -9
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
basename,
|
|
4
|
+
dirname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
relative,
|
|
8
|
+
resolve,
|
|
9
|
+
sep,
|
|
10
|
+
} from "node:path";
|
|
3
11
|
import process from "node:process";
|
|
4
12
|
|
|
5
13
|
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
@@ -8,16 +16,17 @@ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
|
|
|
8
16
|
import {
|
|
9
17
|
getAllFiles,
|
|
10
18
|
getTmpDir,
|
|
11
|
-
safeCopyFileSync,
|
|
12
19
|
safeExistsSync,
|
|
13
20
|
safeMkdirSync,
|
|
14
21
|
safeMkdtempSync,
|
|
15
22
|
safeRmSync,
|
|
16
23
|
safeSpawnSync,
|
|
24
|
+
safeWriteSync,
|
|
17
25
|
} from "../core/fs.js";
|
|
18
26
|
import { dirNameStr } from "../core/paths.js";
|
|
19
27
|
import { resolvePluginSourceDir } from "../core/pluginRun.js";
|
|
20
28
|
import { formatDuration } from "../core/processTree.js";
|
|
29
|
+
import { isProjectToolingAllowed } from "../core/projectTooling.js";
|
|
21
30
|
import { parsetoolsFile } from "../inventory/atomUtils.js";
|
|
22
31
|
import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
|
|
23
32
|
import { getJarClasses } from "../inventory/deps.js";
|
|
@@ -146,22 +155,25 @@ function scalasemTimeoutMs(options = {}) {
|
|
|
146
155
|
const TIMEOUT_GRACE_MS = 30_000;
|
|
147
156
|
|
|
148
157
|
// The build files that identify a Scala project, for a source argument that
|
|
149
|
-
// names the working directory relative to itself
|
|
158
|
+
// names the working directory relative to itself, and that a persisted report
|
|
159
|
+
// must be newer than. sbt 0.13 builds may define everything under project/.
|
|
150
160
|
const SCALA_BUILD_MARKERS = [
|
|
151
161
|
"build.sbt",
|
|
152
162
|
"build.sc",
|
|
153
163
|
"build.mill",
|
|
154
164
|
"pom.xml",
|
|
155
165
|
"project.scala",
|
|
166
|
+
join("project", "build.properties"),
|
|
156
167
|
];
|
|
157
168
|
|
|
158
169
|
/**
|
|
159
170
|
* Where the scalasem report is persisted. An absolute --semantics-slices-file
|
|
160
171
|
* keeps its directory; anything else lands in the evinse output directory, the
|
|
161
|
-
* same rule the atom slice writer follows.
|
|
172
|
+
* same rule the atom slice writer follows. Without either there is nowhere to
|
|
173
|
+
* keep it, and the report lives only as long as the run.
|
|
162
174
|
*
|
|
163
175
|
* @param {Object} options CLI options carrying `output` and `semanticsSlicesFile`.
|
|
164
|
-
* @returns {string} Absolute report path.
|
|
176
|
+
* @returns {string|undefined} Absolute report path.
|
|
165
177
|
*/
|
|
166
178
|
export function scalasemOutputFile(options = {}) {
|
|
167
179
|
const requested = options.semanticsSlicesFile || "semantics.slices.json";
|
|
@@ -170,21 +182,18 @@ export function scalasemOutputFile(options = {}) {
|
|
|
170
182
|
if (isAbsolute(requested)) {
|
|
171
183
|
return requested;
|
|
172
184
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
185
|
-
} else {
|
|
186
|
-
outputDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
|
|
185
|
+
if (!options.output) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const resolvedOutput = resolve(options.output);
|
|
189
|
+
let isDir = false;
|
|
190
|
+
try {
|
|
191
|
+
isDir =
|
|
192
|
+
safeExistsSync(resolvedOutput) && lstatSync(resolvedOutput).isDirectory();
|
|
193
|
+
} catch (_err) {
|
|
194
|
+
isDir = false;
|
|
187
195
|
}
|
|
196
|
+
const outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
188
197
|
return join(outputDir, basename(requested));
|
|
189
198
|
}
|
|
190
199
|
|
|
@@ -223,6 +232,18 @@ export function isV1SemanticsSlice(parsed) {
|
|
|
223
232
|
return Object.keys(parsed).some((key) => key.endsWith(".scala"));
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
// Whether a path lies below a directory. A prefix test fails for a root
|
|
236
|
+
// directory (`/`, `C:\\`), whose path already ends in the separator.
|
|
237
|
+
function isInside(directory, path) {
|
|
238
|
+
const rel = relative(directory, path);
|
|
239
|
+
return (
|
|
240
|
+
Boolean(rel) &&
|
|
241
|
+
rel !== ".." &&
|
|
242
|
+
!rel.startsWith(`..${sep}`) &&
|
|
243
|
+
!isAbsolute(rel)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
226
247
|
/**
|
|
227
248
|
* Whether a version 1 slice describes this project: it names no project, so
|
|
228
249
|
* every source file it lists must exist under the project directory.
|
|
@@ -233,15 +254,104 @@ function isV1SliceOfProject(slice, projectDir) {
|
|
|
233
254
|
files.length > 0 &&
|
|
234
255
|
files.every((file) => {
|
|
235
256
|
const path = resolve(projectDir, file);
|
|
236
|
-
return
|
|
257
|
+
return isInside(projectDir, path) && safeExistsSync(path);
|
|
237
258
|
})
|
|
238
259
|
);
|
|
239
260
|
}
|
|
240
261
|
|
|
262
|
+
// The run settings a report depends on. cdxgen records them in the report it
|
|
263
|
+
// keeps, so a later run reuses that report only when it would ask for the
|
|
264
|
+
// same: tests included or not, the build run or not.
|
|
265
|
+
const RUN_SETTINGS_KEY = "cdxgenRunSettings";
|
|
266
|
+
|
|
267
|
+
function scalasemRunSettings(options) {
|
|
268
|
+
return {
|
|
269
|
+
build: isProjectToolingAllowed(options),
|
|
270
|
+
includeTests: Boolean(
|
|
271
|
+
options.scalasemIncludeTests && !options.requiredOnly,
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function canonicalPath(path) {
|
|
277
|
+
try {
|
|
278
|
+
return realpathSync(path);
|
|
279
|
+
} catch (_err) {
|
|
280
|
+
return resolve(path);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Whether two paths name one directory, through symbolic links such as
|
|
285
|
+
// /var and /private/var, and in any case on Windows.
|
|
286
|
+
function isSamePath(left, right) {
|
|
287
|
+
const a = canonicalPath(left);
|
|
288
|
+
const b = canonicalPath(right);
|
|
289
|
+
return process.platform === "win32"
|
|
290
|
+
? a.toLowerCase() === b.toLowerCase()
|
|
291
|
+
: a === b;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function modifiedAt(file) {
|
|
295
|
+
try {
|
|
296
|
+
return statSync(file).mtimeMs;
|
|
297
|
+
} catch (_err) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Why a persisted report cannot stand in for a new run: it describes another
|
|
304
|
+
* project, it predates the input BOM, cdxgen kept it from a run with other
|
|
305
|
+
* settings, or a build file or a source it describes changed or went away
|
|
306
|
+
* since it was written.
|
|
307
|
+
*
|
|
308
|
+
* @returns {string|undefined} The reason, or undefined when it can be reused.
|
|
309
|
+
*/
|
|
310
|
+
function reportReuseProblem(report, reportFile, analysisDir, options) {
|
|
311
|
+
const projectPath = report._meta?.projectPath;
|
|
312
|
+
if (!projectPath || !isSamePath(projectPath, analysisDir)) {
|
|
313
|
+
return "it describes another project";
|
|
314
|
+
}
|
|
315
|
+
if (!isNewerThan(reportFile, options.input)) {
|
|
316
|
+
return "it is older than the input BOM";
|
|
317
|
+
}
|
|
318
|
+
const recorded = report._meta[RUN_SETTINGS_KEY];
|
|
319
|
+
const wanted = scalasemRunSettings(options);
|
|
320
|
+
if (
|
|
321
|
+
recorded &&
|
|
322
|
+
(recorded.build !== wanted.build ||
|
|
323
|
+
recorded.includeTests !== wanted.includeTests)
|
|
324
|
+
) {
|
|
325
|
+
return "it was made with other settings";
|
|
326
|
+
}
|
|
327
|
+
const writtenAt = modifiedAt(reportFile);
|
|
328
|
+
if (writtenAt === undefined) {
|
|
329
|
+
return "its modification time is unknown";
|
|
330
|
+
}
|
|
331
|
+
for (const marker of SCALA_BUILD_MARKERS) {
|
|
332
|
+
const changedAt = modifiedAt(join(analysisDir, marker));
|
|
333
|
+
if (changedAt !== undefined && changedAt > writtenAt) {
|
|
334
|
+
return `${marker} changed since`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const key of Object.keys(report)) {
|
|
338
|
+
const file = isReportFileKey(key) ? resolve(analysisDir, key) : undefined;
|
|
339
|
+
if (!file || !isInside(analysisDir, file)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const changedAt = modifiedAt(file);
|
|
343
|
+
if (changedAt === undefined || changedAt > writtenAt) {
|
|
344
|
+
return `${key} changed since`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
241
350
|
/**
|
|
242
351
|
* Run scalasem over a Scala project and return its report, reusing a
|
|
243
|
-
* persisted version 2 report that belongs to this project
|
|
244
|
-
* the
|
|
352
|
+
* persisted version 2 report that belongs to this project, is newer than the
|
|
353
|
+
* input BOM and than the build files and sources it describes, and, when
|
|
354
|
+
* cdxgen kept it, was made with the settings this run asks for.
|
|
245
355
|
*
|
|
246
356
|
* Failures are visible rather than silent: the reason is printed once, the
|
|
247
357
|
* run's diagnostics reach the caller as `cdx:scalasem:diagnostic` properties,
|
|
@@ -279,10 +389,18 @@ export function analyzeScalaProject(src, options = {}) {
|
|
|
279
389
|
for (const candidate of candidates) {
|
|
280
390
|
const existing = readScalasemReport(candidate);
|
|
281
391
|
if (existing?._meta?.schemaVersion === "scalasem/2") {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
392
|
+
const problem = reportReuseProblem(
|
|
393
|
+
existing,
|
|
394
|
+
candidate,
|
|
395
|
+
analysisDir,
|
|
396
|
+
options,
|
|
397
|
+
);
|
|
398
|
+
if (problem && DEBUG_MODE) {
|
|
399
|
+
console.log(
|
|
400
|
+
`Not reusing the scalasem report "${candidate}": ${problem}.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (!problem) {
|
|
286
404
|
if (DEBUG_MODE) {
|
|
287
405
|
console.log(`Reusing the scalasem report "${candidate}".`);
|
|
288
406
|
}
|
|
@@ -339,12 +457,38 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
339
457
|
// earlier run (or another project's run) had left there, and a report
|
|
340
458
|
// written within the millisecond the run started passed for fresh.
|
|
341
459
|
const runDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
|
|
460
|
+
try {
|
|
461
|
+
return runScalasemIn(
|
|
462
|
+
runDir,
|
|
463
|
+
scalasem,
|
|
464
|
+
analysisDir,
|
|
465
|
+
options,
|
|
466
|
+
requestedFile,
|
|
467
|
+
v1Slice,
|
|
468
|
+
v1File,
|
|
469
|
+
);
|
|
470
|
+
} finally {
|
|
471
|
+
safeRmSync(runDir, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function runScalasemIn(
|
|
476
|
+
runDir,
|
|
477
|
+
scalasem,
|
|
478
|
+
analysisDir,
|
|
479
|
+
options,
|
|
480
|
+
requestedFile,
|
|
481
|
+
v1Slice,
|
|
482
|
+
v1File,
|
|
483
|
+
) {
|
|
342
484
|
const runReportFile = join(runDir, "scalasem.slices.json");
|
|
343
485
|
const args = [scalasem, analysisDir, runReportFile];
|
|
344
|
-
|
|
486
|
+
const settings = scalasemRunSettings(options);
|
|
487
|
+
// The build runs the project's sbt definitions
|
|
488
|
+
if (!settings.build) {
|
|
345
489
|
args.push("--no-build");
|
|
346
490
|
}
|
|
347
|
-
if (
|
|
491
|
+
if (settings.includeTests) {
|
|
348
492
|
args.push("--include-tests");
|
|
349
493
|
}
|
|
350
494
|
if (DEBUG_MODE) {
|
|
@@ -355,17 +499,24 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
355
499
|
// scalasem enforces the limit itself, so the builds it started stop with
|
|
356
500
|
// it; cdxgen's own timeout is the backstop. Naming cdxgen as the supervisor
|
|
357
501
|
// stops scalasem when cdxgen is killed.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
502
|
+
// A spawn that throws, such as a command the allow list refuses, degrades
|
|
503
|
+
// the evidence like any other run that could not start.
|
|
504
|
+
let result;
|
|
505
|
+
try {
|
|
506
|
+
result = safeSpawnSync(process.execPath, args, {
|
|
507
|
+
cwd: analysisDir,
|
|
508
|
+
shell: false,
|
|
509
|
+
killSignal: "SIGKILL",
|
|
510
|
+
timeout: timeoutMs + Math.min(TIMEOUT_GRACE_MS, timeoutMs),
|
|
511
|
+
env: {
|
|
512
|
+
...process.env,
|
|
513
|
+
ATOM_PARENT_PID: String(process.pid),
|
|
514
|
+
SCALASEM_TIMEOUT: String(timeoutMs),
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
} catch (err) {
|
|
518
|
+
result = { error: err };
|
|
519
|
+
}
|
|
369
520
|
const elapsedMs = Date.now() - startedAt;
|
|
370
521
|
const wroteReport = safeExistsSync(runReportFile);
|
|
371
522
|
const written = wroteReport ? readScalasemReport(runReportFile) : undefined;
|
|
@@ -429,7 +580,9 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
429
580
|
),
|
|
430
581
|
];
|
|
431
582
|
}
|
|
432
|
-
const summary = summarizeDiagnostics(
|
|
583
|
+
const summary = summarizeDiagnostics(
|
|
584
|
+
degradingDiagnostics(report._meta.diagnostics),
|
|
585
|
+
);
|
|
433
586
|
if (summary) {
|
|
434
587
|
console.warn(
|
|
435
588
|
`scalasem: the report is degraded: ${summary}. The cdx:scalasem:diagnostic:* metadata properties of the BOM say where.`,
|
|
@@ -441,19 +594,26 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
441
594
|
}
|
|
442
595
|
// Only a usable report replaces the one at the requested path, and a
|
|
443
596
|
// version 1 slice there stays as the user left it.
|
|
597
|
+
// The kept copy records the settings of this run, so a later run reuses it
|
|
598
|
+
// only when it asks for the same.
|
|
444
599
|
let reportFile;
|
|
445
|
-
if (usable && v1File !== requestedFile) {
|
|
600
|
+
if (usable && requestedFile && v1File !== requestedFile) {
|
|
446
601
|
try {
|
|
447
602
|
safeMkdirSync(dirname(requestedFile), { recursive: true });
|
|
448
|
-
|
|
449
|
-
|
|
603
|
+
safeWriteSync(
|
|
604
|
+
requestedFile,
|
|
605
|
+
JSON.stringify({
|
|
606
|
+
...report,
|
|
607
|
+
_meta: { ...report._meta, [RUN_SETTINGS_KEY]: settings },
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
reportFile = safeExistsSync(requestedFile) ? requestedFile : undefined;
|
|
450
611
|
} catch (err) {
|
|
451
612
|
console.warn(
|
|
452
613
|
`scalasem: the report could not be kept at ${requestedFile} (${err.message}); this run still uses it.`,
|
|
453
614
|
);
|
|
454
615
|
}
|
|
455
616
|
}
|
|
456
|
-
safeRmSync(runDir, { recursive: true, force: true });
|
|
457
617
|
return {
|
|
458
618
|
report,
|
|
459
619
|
reportFile,
|
|
@@ -462,6 +622,21 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
462
622
|
};
|
|
463
623
|
}
|
|
464
624
|
|
|
625
|
+
// Diagnostics scalasem counts on a healthy run: symbols its semantic pass
|
|
626
|
+
// could not resolve, and stale build outputs it correctly skipped. They say
|
|
627
|
+
// the analysis is incomplete, not that it is degraded, so they stay out of
|
|
628
|
+
// the BOM and out of the degraded flag.
|
|
629
|
+
const INFORMATIONAL_DIAGNOSTIC_CODES = new Set([
|
|
630
|
+
"unresolved-symbols",
|
|
631
|
+
"stale-output",
|
|
632
|
+
]);
|
|
633
|
+
|
|
634
|
+
function degradingDiagnostics(diagnostics = []) {
|
|
635
|
+
return (diagnostics || []).filter(
|
|
636
|
+
(diagnostic) => !INFORMATIONAL_DIAGNOSTIC_CODES.has(diagnostic?.code),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
465
640
|
function summarizeDiagnostics(diagnostics = []) {
|
|
466
641
|
const byCode = new Map();
|
|
467
642
|
for (const diagnostic of diagnostics || []) {
|
|
@@ -488,8 +663,9 @@ function summarizeDiagnostics(diagnostics = []) {
|
|
|
488
663
|
*/
|
|
489
664
|
export function scalasemMetadataProperties(
|
|
490
665
|
report,
|
|
491
|
-
|
|
666
|
+
allDiagnostics = report?._meta?.diagnostics || [],
|
|
492
667
|
) {
|
|
668
|
+
const diagnostics = degradingDiagnostics(allDiagnostics);
|
|
493
669
|
const properties = [];
|
|
494
670
|
const meta = report?._meta || {};
|
|
495
671
|
appendUniqueProperty(
|
|
@@ -551,20 +727,23 @@ export function sortedCsv(values) {
|
|
|
551
727
|
|
|
552
728
|
/**
|
|
553
729
|
* The platform a purl's artifact name targets: `_sjs1` is Scala.js,
|
|
554
|
-
* `_native0.x` is Scala Native, and neither is the JVM.
|
|
730
|
+
* `_native0.x` is Scala Native, and neither is the JVM. The platform suffix
|
|
731
|
+
* counts whether the Scala binary suffix follows it (`upickle_sjs1_3`, the
|
|
732
|
+
* published name a jar scan keeps) or not (`upickle_sjs1`, the sbt name).
|
|
555
733
|
*
|
|
556
734
|
* @param {string} purl Package URL.
|
|
557
735
|
* @returns {"jvm"|"js"|"native"} Platform key.
|
|
558
736
|
*/
|
|
559
737
|
export function scalaPurlPlatform(purl) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
738
|
+
return scalaNamePlatform(purlPurlName(purl));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function scalaNamePlatform(name) {
|
|
742
|
+
const { platformSuffix } = parseScalaArtifact(name);
|
|
743
|
+
if (platformSuffix.startsWith("_sjs")) {
|
|
565
744
|
return "js";
|
|
566
745
|
}
|
|
567
|
-
if (
|
|
746
|
+
if (platformSuffix.startsWith("_native")) {
|
|
568
747
|
return "native";
|
|
569
748
|
}
|
|
570
749
|
return "jvm";
|
|
@@ -582,6 +761,20 @@ function purlPurlName(purl) {
|
|
|
582
761
|
}
|
|
583
762
|
}
|
|
584
763
|
|
|
764
|
+
// A compiler-generated class: a `$`-separated part that is empty past the
|
|
765
|
+
// top-level name (`$$`) or numeric, or one of the anonymous and lambda markers.
|
|
766
|
+
function isSyntheticClassName(symbol, dollar) {
|
|
767
|
+
return symbol
|
|
768
|
+
.slice(dollar + 1)
|
|
769
|
+
.split("$")
|
|
770
|
+
.some(
|
|
771
|
+
(part, index, parts) =>
|
|
772
|
+
(part === "" && index < parts.length - 1) ||
|
|
773
|
+
/^\d+$/.test(part) ||
|
|
774
|
+
["anon", "anonfun", "Lambda", "lambda"].includes(part),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
585
778
|
/**
|
|
586
779
|
* The class and package index that joins scalasem symbols to component purls.
|
|
587
780
|
*
|
|
@@ -631,11 +824,22 @@ export class ScalaJoinIndex {
|
|
|
631
824
|
if (!symbol || symbol.includes(" ") || symbol.includes("/")) {
|
|
632
825
|
continue;
|
|
633
826
|
}
|
|
634
|
-
|
|
635
|
-
|
|
827
|
+
// A `$` inside the simple name separates nested classes; one that starts
|
|
828
|
+
// it belongs to an encoded operator name (`$colon$colon` for `::`).
|
|
829
|
+
const simpleName = symbol.lastIndexOf(".") + 1;
|
|
830
|
+
const dollar = symbol.startsWith("$", simpleName)
|
|
831
|
+
? -1
|
|
832
|
+
: symbol.indexOf("$", simpleName);
|
|
636
833
|
const topLevel = dollar > 0 ? symbol.slice(0, dollar) : symbol;
|
|
834
|
+
// Anonymous classes and lambdas (`Foo$$anon$1`, `Foo$$anonfun$bar$1`,
|
|
835
|
+
// `Foo$$Lambda$12`) are no names a source can reference; their outer
|
|
836
|
+
// class speaks for them.
|
|
837
|
+
const synthetic = dollar > 0 && isSyntheticClassName(symbol, dollar);
|
|
838
|
+
if (!synthetic) {
|
|
839
|
+
indexed.push(symbol);
|
|
840
|
+
}
|
|
637
841
|
this.addSymbol(this.classPurls, topLevel, purl, source);
|
|
638
|
-
if (dollar > 0) {
|
|
842
|
+
if (dollar > 0 && !synthetic) {
|
|
639
843
|
const nested = symbol.replaceAll("$", ".").replace(/\.+$/, "");
|
|
640
844
|
if (nested !== topLevel) {
|
|
641
845
|
this.addSymbol(this.classPurls, nested, purl, source);
|
|
@@ -732,15 +936,25 @@ export class ScalaJoinIndex {
|
|
|
732
936
|
* sits in. A package the project also fills is ambiguous and joins nothing,
|
|
733
937
|
* and `javax.` names join only through an exact class, since the JDK ships
|
|
734
938
|
* most of them.
|
|
939
|
+
*
|
|
940
|
+
* The package is the part of the symbol before its first type name, which
|
|
941
|
+
* starts with a capital letter (`io.circe` of `io.circe.Json.fromString`). A
|
|
942
|
+
* symbol without one names a package (`io.circe`) or a member of one, such
|
|
943
|
+
* as a package object member (`io.circe.syntax.asJson`). Parent packages
|
|
944
|
+
* never answer: a missing `io.circe.generic` is not circe-core.
|
|
735
945
|
*/
|
|
736
946
|
packageOwners(symbol) {
|
|
737
947
|
if (symbol.startsWith(JDK_EXTENSION_PREFIX)) {
|
|
738
948
|
return undefined;
|
|
739
949
|
}
|
|
740
950
|
const parts = symbol.replace(/\$+$/, "").split(".");
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
951
|
+
const firstType = parts.findIndex((part) => /^[A-Z]/.test(part));
|
|
952
|
+
const packages =
|
|
953
|
+
firstType >= 0
|
|
954
|
+
? [parts.slice(0, firstType).join(".")]
|
|
955
|
+
: [parts.join("."), parts.slice(0, -1).join(".")];
|
|
956
|
+
for (const name of packages) {
|
|
957
|
+
const owners = name ? this.packagePurls.get(name) : undefined;
|
|
744
958
|
if (owners?.size) {
|
|
745
959
|
return this.projectPackages.has(name) ? undefined : owners;
|
|
746
960
|
}
|
|
@@ -873,41 +1087,83 @@ function componentBinaryVersions(component, coordinate) {
|
|
|
873
1087
|
);
|
|
874
1088
|
}
|
|
875
1089
|
|
|
1090
|
+
// Which of two components standing for the same library is the one evidence
|
|
1091
|
+
// attaches to: the plain purl over one with qualifiers, then the shorter, then
|
|
1092
|
+
// the lexicographically first, so the choice never depends on BOM order.
|
|
1093
|
+
function preferredComponent(current, candidate) {
|
|
1094
|
+
const rank = (component) => [
|
|
1095
|
+
component.purl.includes("?") ? 1 : 0,
|
|
1096
|
+
component.purl.length,
|
|
1097
|
+
];
|
|
1098
|
+
const [currentQualified, currentLength] = rank(current);
|
|
1099
|
+
const [candidateQualified, candidateLength] = rank(candidate);
|
|
1100
|
+
if (currentQualified !== candidateQualified) {
|
|
1101
|
+
return candidateQualified < currentQualified ? candidate : current;
|
|
1102
|
+
}
|
|
1103
|
+
if (currentLength !== candidateLength) {
|
|
1104
|
+
return candidateLength < currentLength ? candidate : current;
|
|
1105
|
+
}
|
|
1106
|
+
return candidate.purl < current.purl ? candidate : current;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
876
1109
|
/**
|
|
877
1110
|
* Index the BOM components by their normalized Scala coordinates, both with
|
|
878
1111
|
* and without the version, so a classpath jar, a namespace property and a map
|
|
879
1112
|
* entry of the same library meet on one component. Each entry keeps the Scala
|
|
880
|
-
* binary
|
|
881
|
-
* artifacts of one library in a Maven build stay apart.
|
|
1113
|
+
* binary versions the component was built for, so the `_2.13` and `_3`
|
|
1114
|
+
* artifacts of one library in a Maven build stay apart. Components that share
|
|
1115
|
+
* a normalized coordinate and were built for an overlapping set of Scala
|
|
1116
|
+
* binary versions are one library listed more than once (a repository_url
|
|
1117
|
+
* qualifier, a `_3` suffix beside the sbt name built for 2.13 and 3, a stale
|
|
1118
|
+
* duplicate), so they share one entry, which the preferred component of the
|
|
1119
|
+
* group stands for.
|
|
882
1120
|
*/
|
|
883
1121
|
export function componentCoordinateIndex(components = []) {
|
|
884
|
-
const
|
|
885
|
-
const withoutVersion = new Map();
|
|
1122
|
+
const groupsByKey = new Map();
|
|
886
1123
|
for (const component of components) {
|
|
887
1124
|
const coordinate = scalaCoordinateOfPurl(component?.purl);
|
|
888
1125
|
if (!coordinate) {
|
|
889
1126
|
continue;
|
|
890
1127
|
}
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
1128
|
+
const binaryVersions = componentBinaryVersions(component, coordinate);
|
|
1129
|
+
const groups = groupsByKey.get(coordinate.key) || [];
|
|
1130
|
+
groupsByKey.set(coordinate.key, groups);
|
|
1131
|
+
// Every group this component overlaps becomes one with it.
|
|
1132
|
+
const overlapping = groups.filter((group) =>
|
|
1133
|
+
binaryVersions.size
|
|
1134
|
+
? [...binaryVersions].some((version) =>
|
|
1135
|
+
group.binaryVersions.has(version),
|
|
1136
|
+
)
|
|
1137
|
+
: !group.binaryVersions.size,
|
|
1138
|
+
);
|
|
1139
|
+
const merged = {
|
|
894
1140
|
component,
|
|
895
|
-
binaryVersions:
|
|
1141
|
+
binaryVersions: new Set(binaryVersions),
|
|
896
1142
|
};
|
|
897
|
-
|
|
898
|
-
|
|
1143
|
+
for (const group of overlapping) {
|
|
1144
|
+
merged.component = preferredComponent(merged.component, group.component);
|
|
1145
|
+
for (const version of group.binaryVersions) {
|
|
1146
|
+
merged.binaryVersions.add(version);
|
|
1147
|
+
}
|
|
1148
|
+
groups.splice(groups.indexOf(group), 1);
|
|
899
1149
|
}
|
|
900
|
-
|
|
901
|
-
|
|
1150
|
+
groups.push(merged);
|
|
1151
|
+
}
|
|
1152
|
+
const byVersion = new Map();
|
|
1153
|
+
const withoutVersion = new Map();
|
|
1154
|
+
for (const [key, groups] of groupsByKey) {
|
|
1155
|
+
const [type, group, artifactBase, platformSuffix] = key.split(":");
|
|
1156
|
+
byVersion.set(key, groups);
|
|
1157
|
+
const versionless = scalaCoordinateKey({
|
|
902
1158
|
type,
|
|
903
1159
|
group,
|
|
904
1160
|
artifactBase,
|
|
905
1161
|
platformSuffix,
|
|
906
1162
|
});
|
|
907
|
-
|
|
908
|
-
withoutVersion.
|
|
909
|
-
|
|
910
|
-
|
|
1163
|
+
withoutVersion.set(versionless, [
|
|
1164
|
+
...(withoutVersion.get(versionless) || []),
|
|
1165
|
+
...groups,
|
|
1166
|
+
]);
|
|
911
1167
|
}
|
|
912
1168
|
return { byVersion, withoutVersion };
|
|
913
1169
|
}
|
|
@@ -942,11 +1198,126 @@ function pickByBinaryVersion(entries, binaryVersion) {
|
|
|
942
1198
|
* @returns {{component: Object, exact: boolean}|undefined} The component, and
|
|
943
1199
|
* whether its version is the one on the classpath.
|
|
944
1200
|
*/
|
|
945
|
-
export function matchClasspathEntry(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1201
|
+
export function matchClasspathEntry(entry, coordinateIndex, platform) {
|
|
1202
|
+
const direct =
|
|
1203
|
+
entry?.group && entry?.artifact
|
|
1204
|
+
? matchCoordinates(entry, coordinateIndex, platform)
|
|
1205
|
+
: undefined;
|
|
1206
|
+
if (direct?.component || direct?.ambiguous) {
|
|
1207
|
+
return direct.component ? direct : undefined;
|
|
1208
|
+
}
|
|
1209
|
+
// The report reads coordinates from a Maven repository layout and can take
|
|
1210
|
+
// directories above the group for part of it (`maven.io.circe` for jars
|
|
1211
|
+
// under `<repository>/maven/io/circe/`), and reads none from the Gradle and
|
|
1212
|
+
// Ivy cache layouts. The coordinates the jar's own path spells are tried
|
|
1213
|
+
// then, and must still meet a component on artifact and version.
|
|
1214
|
+
for (const coordinates of jarPathCoordinates(entry)) {
|
|
1215
|
+
const byPath = matchCoordinates(
|
|
1216
|
+
{ ...entry, ...coordinates },
|
|
1217
|
+
coordinateIndex,
|
|
1218
|
+
platform,
|
|
1219
|
+
);
|
|
1220
|
+
if (byPath?.exact) {
|
|
1221
|
+
return byPath;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return undefined;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* The coordinates a classpath jar's path spells, for the layouts the report
|
|
1229
|
+
* does not read itself, most specific first:
|
|
1230
|
+
*
|
|
1231
|
+
* - a Maven layout `<group dirs>/<artifact>/<version>/<file>` whose group is
|
|
1232
|
+
* a dotted tail of the group the report recorded, so a group is only ever
|
|
1233
|
+
* shortened by the directories the report took for it, never guessed
|
|
1234
|
+
* - the Gradle cache `files-2.1/<group>/<artifact>/<version>/<sha1>/<file>`
|
|
1235
|
+
* - an Ivy cache `<org>/<artifact>/jars/<artifact>-<version>.jar`, as sbt 0.13
|
|
1236
|
+
* and sbt 1.0 to 1.2 keep it, and an Ivy local repository
|
|
1237
|
+
* `<org>/<artifact>/<version>/jars/<artifact>.jar`
|
|
1238
|
+
*
|
|
1239
|
+
* @param {Object} entry Classpath entry with `path` and, when the report read
|
|
1240
|
+
* them, `group`, `artifact` and `version`.
|
|
1241
|
+
* @returns {{group: string, artifact: string, version: string}[]}
|
|
1242
|
+
*/
|
|
1243
|
+
export function jarPathCoordinates(entry) {
|
|
1244
|
+
if (!entry?.path) {
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
const segments = String(entry.path).split(/[\\/]/).filter(Boolean);
|
|
1248
|
+
const total = segments.length;
|
|
1249
|
+
const file = segments[total - 1] || "";
|
|
1250
|
+
if (total < 4 || !file.toLowerCase().endsWith(".jar")) {
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
const found = [];
|
|
1254
|
+
if (entry.group && entry.artifact && entry.version) {
|
|
1255
|
+
const artifactIndex = total - 3;
|
|
1256
|
+
if (
|
|
1257
|
+
segments[artifactIndex] === entry.artifact &&
|
|
1258
|
+
segments[artifactIndex + 1] === entry.version
|
|
1259
|
+
) {
|
|
1260
|
+
const groupParts = entry.group.split(".");
|
|
1261
|
+
for (let start = 1; start < groupParts.length; start++) {
|
|
1262
|
+
const tail = groupParts.slice(start);
|
|
1263
|
+
const directories = segments.slice(
|
|
1264
|
+
artifactIndex - tail.length,
|
|
1265
|
+
artifactIndex,
|
|
1266
|
+
);
|
|
1267
|
+
if (
|
|
1268
|
+
artifactIndex - tail.length >= 0 &&
|
|
1269
|
+
directories.join("/") === tail.join("/")
|
|
1270
|
+
) {
|
|
1271
|
+
found.push({
|
|
1272
|
+
group: tail.join("."),
|
|
1273
|
+
artifact: entry.artifact,
|
|
1274
|
+
version: entry.version,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const gradle = segments.lastIndexOf("files-2.1");
|
|
1281
|
+
if (gradle >= 0 && gradle + 5 === total - 1) {
|
|
1282
|
+
found.push({
|
|
1283
|
+
group: segments[gradle + 1],
|
|
1284
|
+
artifact: segments[gradle + 2],
|
|
1285
|
+
version: segments[gradle + 3],
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
const stem = file.slice(0, -".jar".length);
|
|
1289
|
+
if (["jars", "bundles"].includes(segments[total - 2])) {
|
|
1290
|
+
// Ivy local: <org>/<artifact>/<version>/jars/<artifact>.jar
|
|
1291
|
+
if (total >= 5 && stem === segments[total - 4]) {
|
|
1292
|
+
found.push({
|
|
1293
|
+
group: segments[total - 5],
|
|
1294
|
+
artifact: segments[total - 4],
|
|
1295
|
+
version: segments[total - 3],
|
|
1296
|
+
});
|
|
1297
|
+
} else if (stem.startsWith(`${segments[total - 3]}-`)) {
|
|
1298
|
+
// Ivy cache: <org>/<artifact>/jars/<artifact>-<version>.jar
|
|
1299
|
+
found.push({
|
|
1300
|
+
group: segments[total - 4],
|
|
1301
|
+
artifact: segments[total - 3],
|
|
1302
|
+
version: stem.slice(segments[total - 3].length + 1),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return found.filter(
|
|
1307
|
+
(coordinates) =>
|
|
1308
|
+
coordinates.group &&
|
|
1309
|
+
coordinates.artifact &&
|
|
1310
|
+
coordinates.version &&
|
|
1311
|
+
(coordinates.group !== entry.group ||
|
|
1312
|
+
coordinates.artifact !== entry.artifact ||
|
|
1313
|
+
coordinates.version !== entry.version),
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// The component a set of coordinates names: `{component, exact}` for a single
|
|
1318
|
+
// owner, `{ambiguous: true}` when several components answer, undefined when
|
|
1319
|
+
// none does.
|
|
1320
|
+
function matchCoordinates(entry, { byVersion, withoutVersion }, platform) {
|
|
950
1321
|
const parsed = parseScalaArtifact(entry?.artifact);
|
|
951
1322
|
const parts = {
|
|
952
1323
|
group: entry?.group,
|
|
@@ -961,7 +1332,7 @@ export function matchClasspathEntry(
|
|
|
961
1332
|
return { component: exact[0].component, exact: true };
|
|
962
1333
|
}
|
|
963
1334
|
if (exact.length > 1) {
|
|
964
|
-
return
|
|
1335
|
+
return { ambiguous: true };
|
|
965
1336
|
}
|
|
966
1337
|
const candidates = pickByBinaryVersion(
|
|
967
1338
|
withoutVersion.get(scalaCoordinateKey(parts)),
|
|
@@ -971,9 +1342,10 @@ export function matchClasspathEntry(
|
|
|
971
1342
|
(candidate) => scalaPurlPlatform(candidate.component.purl) === platform,
|
|
972
1343
|
);
|
|
973
1344
|
const pool = matching.length ? matching : candidates;
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1345
|
+
if (pool.length === 1) {
|
|
1346
|
+
return { component: pool[0].component, exact: false };
|
|
1347
|
+
}
|
|
1348
|
+
return pool.length > 1 ? { ambiguous: true } : undefined;
|
|
977
1349
|
}
|
|
978
1350
|
|
|
979
1351
|
/**
|
|
@@ -1041,8 +1413,14 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1041
1413
|
}
|
|
1042
1414
|
}
|
|
1043
1415
|
for (const mapPurl of Object.keys(jarNSMapping || {})) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1416
|
+
// Maps written before every producer used `{ namespaces }` hold the
|
|
1417
|
+
// newline-joined namespaces of a jar-scanned component as a string.
|
|
1418
|
+
const value = jarNSMapping[mapPurl];
|
|
1419
|
+
const namespaces =
|
|
1420
|
+
typeof value === "string"
|
|
1421
|
+
? value.split("\n").filter(Boolean)
|
|
1422
|
+
: value?.namespaces;
|
|
1423
|
+
if (!Array.isArray(namespaces) || !namespaces.length) {
|
|
1046
1424
|
continue;
|
|
1047
1425
|
}
|
|
1048
1426
|
const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)@([^?#]*)/.exec(
|
|
@@ -1061,7 +1439,7 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1061
1439
|
scalaPurlPlatform(mapPurl),
|
|
1062
1440
|
);
|
|
1063
1441
|
if (matched) {
|
|
1064
|
-
index.addNamespaces(matched.component.purl,
|
|
1442
|
+
index.addNamespaces(matched.component.purl, namespaces, 3);
|
|
1065
1443
|
}
|
|
1066
1444
|
}
|
|
1067
1445
|
return index;
|
|
@@ -1308,10 +1686,11 @@ export function scalasemCryptoOidKey(name, keySize, mode, curve, padding) {
|
|
|
1308
1686
|
return undefined;
|
|
1309
1687
|
}
|
|
1310
1688
|
if (SCALASEM_AES_NAMES.has(name)) {
|
|
1689
|
+
const upperMode = mode?.toUpperCase();
|
|
1311
1690
|
const modeKey =
|
|
1312
|
-
SCALASEM_AES_MODE_KEYS[
|
|
1691
|
+
SCALASEM_AES_MODE_KEYS[upperMode] ||
|
|
1313
1692
|
SCALASEM_AES_MODE_KEYS[name.split("-")[1]];
|
|
1314
|
-
const wrap = name === "AES-KW" ||
|
|
1693
|
+
const wrap = name === "AES-KW" || upperMode === "KW";
|
|
1315
1694
|
if (keySize && (modeKey !== undefined || wrap)) {
|
|
1316
1695
|
const sizedKey = wrap ? `aes${keySize}-wrap` : `aes${keySize}-${modeKey}`;
|
|
1317
1696
|
const hit = SCALASEM_OID_KEYS.get(sizedKey.toLowerCase());
|
|
@@ -1454,6 +1833,7 @@ export async function collectScalasemEvidence(
|
|
|
1454
1833
|
componentPropertiesMap,
|
|
1455
1834
|
metadataProperties,
|
|
1456
1835
|
options,
|
|
1836
|
+
allowedPurlsByModule,
|
|
1457
1837
|
);
|
|
1458
1838
|
addJsModuleEvidence(
|
|
1459
1839
|
report,
|
|
@@ -1786,7 +2166,17 @@ function stackTargetPurls(sink, fileEntry, joinIndex, platform, allowed) {
|
|
|
1786
2166
|
return [...purls];
|
|
1787
2167
|
}
|
|
1788
2168
|
|
|
2169
|
+
// The package of a class the report names. A class nested in an object reads
|
|
2170
|
+
// `Outer$.Inner`, so the package ends before the first part that is a type:
|
|
2171
|
+
// one that carries a `$` or starts with a capital letter.
|
|
1789
2172
|
function packageOf(className) {
|
|
2173
|
+
const parts = String(className || "").split(".");
|
|
2174
|
+
const firstType = parts.findIndex(
|
|
2175
|
+
(part) => part.includes("$") || /^[A-Z]/.test(part),
|
|
2176
|
+
);
|
|
2177
|
+
if (firstType > 0) {
|
|
2178
|
+
return parts.slice(0, firstType).join(".");
|
|
2179
|
+
}
|
|
1790
2180
|
const text = String(className || "").replace(/\$+$/, "");
|
|
1791
2181
|
const dot = text.lastIndexOf(".");
|
|
1792
2182
|
return dot > 0 ? text.slice(0, dot) : "";
|
|
@@ -1846,9 +2236,22 @@ function providerAliases(purl, components = []) {
|
|
|
1846
2236
|
/**
|
|
1847
2237
|
* The component purl of the library a crypto finding names as its provider.
|
|
1848
2238
|
* Maven coordinates match on group and base artifact; the short aliases cover
|
|
1849
|
-
* the providers the rules emit for libraries without a coordinate.
|
|
2239
|
+
* the providers the rules emit for libraries without a coordinate. Of several
|
|
2240
|
+
* matching components, one on the referencing module's classpath wins, then
|
|
2241
|
+
* the artifact whose name extends the provider's least (`bcprov-jdk18on`
|
|
2242
|
+
* over `bcprov-ext-jdk18on`), then the first purl, so the choice never
|
|
2243
|
+
* depends on the order of the BOM.
|
|
2244
|
+
*
|
|
2245
|
+
* @param {string} provider Provider the finding names.
|
|
2246
|
+
* @param {Object[]} components BOM components.
|
|
2247
|
+
* @param {Set<string>} [classpathPurls] Purls on the module's classpath.
|
|
2248
|
+
* @returns {string|undefined} Component purl.
|
|
1850
2249
|
*/
|
|
1851
|
-
export function scalasemProviderPurl(
|
|
2250
|
+
export function scalasemProviderPurl(
|
|
2251
|
+
provider,
|
|
2252
|
+
components = [],
|
|
2253
|
+
classpathPurls = undefined,
|
|
2254
|
+
) {
|
|
1852
2255
|
if (!provider) {
|
|
1853
2256
|
return undefined;
|
|
1854
2257
|
}
|
|
@@ -1863,6 +2266,7 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1863
2266
|
return undefined;
|
|
1864
2267
|
}
|
|
1865
2268
|
const base = parseScalaArtifact(artifactPrefix).artifactBase;
|
|
2269
|
+
const matches = [];
|
|
1866
2270
|
for (const component of components) {
|
|
1867
2271
|
if (!component?.purl) {
|
|
1868
2272
|
continue;
|
|
@@ -1875,10 +2279,23 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1875
2279
|
}
|
|
1876
2280
|
const name = parseScalaArtifact(decodeSafe(segments[2])).artifactBase;
|
|
1877
2281
|
if (name === base || name.startsWith(`${base}-`)) {
|
|
1878
|
-
|
|
2282
|
+
matches.push({ purl: component.purl, name });
|
|
1879
2283
|
}
|
|
1880
2284
|
}
|
|
1881
|
-
|
|
2285
|
+
const rank = (match) => [
|
|
2286
|
+
classpathPurls?.has(match.purl) ? 0 : 1,
|
|
2287
|
+
match.name.length,
|
|
2288
|
+
];
|
|
2289
|
+
matches.sort((left, right) => {
|
|
2290
|
+
const [leftClasspath, leftLength] = rank(left);
|
|
2291
|
+
const [rightClasspath, rightLength] = rank(right);
|
|
2292
|
+
return (
|
|
2293
|
+
leftClasspath - rightClasspath ||
|
|
2294
|
+
leftLength - rightLength ||
|
|
2295
|
+
(left.purl < right.purl ? -1 : left.purl > right.purl ? 1 : 0)
|
|
2296
|
+
);
|
|
2297
|
+
});
|
|
2298
|
+
return matches[0]?.purl;
|
|
1882
2299
|
}
|
|
1883
2300
|
|
|
1884
2301
|
// Primitives a mode of operation applies to. RSA spells `RSA/ECB/...` in JCA,
|
|
@@ -1963,6 +2380,7 @@ function addCryptoEvidence(
|
|
|
1963
2380
|
componentPropertiesMap,
|
|
1964
2381
|
metadataProperties,
|
|
1965
2382
|
options = {},
|
|
2383
|
+
allowedPurlsByModule = new Map(),
|
|
1966
2384
|
) {
|
|
1967
2385
|
const includeTests = options.scalasemIncludeTests === true;
|
|
1968
2386
|
const nativeLibraryBySymbol = new Map();
|
|
@@ -1977,11 +2395,17 @@ function addCryptoEvidence(
|
|
|
1977
2395
|
if (scope === "test" && !includeTests) {
|
|
1978
2396
|
continue;
|
|
1979
2397
|
}
|
|
1980
|
-
const location =
|
|
2398
|
+
const location = finding.line
|
|
2399
|
+
? `${finding.file || ""}#${finding.line}`
|
|
2400
|
+
: finding.file || "";
|
|
1981
2401
|
const nativeLibrary = finding.kind?.startsWith("native")
|
|
1982
2402
|
? nativeLibraryBySymbol.get(finding.name)
|
|
1983
2403
|
: undefined;
|
|
1984
|
-
const providerPurl = scalasemProviderPurl(
|
|
2404
|
+
const providerPurl = scalasemProviderPurl(
|
|
2405
|
+
finding.provider,
|
|
2406
|
+
components,
|
|
2407
|
+
allowedPurlsFor(report[finding.file], allowedPurlsByModule),
|
|
2408
|
+
);
|
|
1985
2409
|
const asset =
|
|
1986
2410
|
finding.resolution === "unresolved"
|
|
1987
2411
|
? undefined
|
|
@@ -2052,12 +2476,18 @@ function addCryptoEvidence(
|
|
|
2052
2476
|
}
|
|
2053
2477
|
}
|
|
2054
2478
|
|
|
2479
|
+
// `file#line` as an occurrence. A location without a line, or whose `#` is
|
|
2480
|
+
// part of the file name, is the file alone.
|
|
2055
2481
|
function parseLocationString(location) {
|
|
2056
2482
|
const separator = location.lastIndexOf("#");
|
|
2057
|
-
const
|
|
2483
|
+
const suffix = separator >= 0 ? location.slice(separator + 1) : "";
|
|
2484
|
+
if (!/^\d+$/.test(suffix)) {
|
|
2485
|
+
return { location };
|
|
2486
|
+
}
|
|
2487
|
+
const line = Number.parseInt(suffix, 10);
|
|
2058
2488
|
return {
|
|
2059
2489
|
location: location.slice(0, separator),
|
|
2060
|
-
...(
|
|
2490
|
+
...(line > 0 ? { line } : {}),
|
|
2061
2491
|
};
|
|
2062
2492
|
}
|
|
2063
2493
|
|
|
@@ -2185,6 +2615,191 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2185
2615
|
return properties;
|
|
2186
2616
|
}
|
|
2187
2617
|
|
|
2618
|
+
// The characters that end a host list: a path, URL parameters, and the
|
|
2619
|
+
// `;key=value` properties some JDBC drivers take.
|
|
2620
|
+
const ADDRESS_END = ["/", "?", "#", ";"];
|
|
2621
|
+
|
|
2622
|
+
const HOST_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9._%-]+)(?::\d+)?$/;
|
|
2623
|
+
|
|
2624
|
+
function indexOfAny(text, characters, from = 0) {
|
|
2625
|
+
let found = -1;
|
|
2626
|
+
for (const character of characters) {
|
|
2627
|
+
const index = text.indexOf(character, from);
|
|
2628
|
+
if (index >= 0 && (found < 0 || index < found)) {
|
|
2629
|
+
found = index;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return found;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
function cutAt(text, characters) {
|
|
2636
|
+
const index = indexOfAny(text, characters);
|
|
2637
|
+
return index >= 0 ? text.slice(0, index) : text;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// `host`, `host:port`, `[ipv6]:port`, or a comma separated list of those.
|
|
2641
|
+
function isHostList(text) {
|
|
2642
|
+
return (
|
|
2643
|
+
Boolean(text) && text.split(",").every((host) => HOST_PATTERN.test(host))
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* Where the userinfo in front of a host ends, or -1 when there is none. A
|
|
2649
|
+
* password may hold any of `/ ? # ; @ =`, so an `@` ends the userinfo unless a
|
|
2650
|
+
* well-formed host list is followed by parameters that hold it
|
|
2651
|
+
* (`host:1433;user=a@b`). Of several `@`, the last one before the first
|
|
2652
|
+
* parameter ends it, and the last one of all when that leaves none: a host the
|
|
2653
|
+
* split gets wrong costs a service name, a split that keeps a password leaks it.
|
|
2654
|
+
*
|
|
2655
|
+
* @param {string} rest The text after `//`, or after a JDBC subprotocol.
|
|
2656
|
+
* @returns {number} Index of the `@` that ends the userinfo.
|
|
2657
|
+
*/
|
|
2658
|
+
function userinfoEnd(rest) {
|
|
2659
|
+
const firstAt = rest.indexOf("@");
|
|
2660
|
+
if (firstAt < 0) {
|
|
2661
|
+
return -1;
|
|
2662
|
+
}
|
|
2663
|
+
const end = indexOfAny(rest, ADDRESS_END);
|
|
2664
|
+
if (
|
|
2665
|
+
end >= 0 &&
|
|
2666
|
+
end < firstAt &&
|
|
2667
|
+
isHostList(rest.slice(0, end)) &&
|
|
2668
|
+
rest.slice(end + 1, firstAt).includes("=")
|
|
2669
|
+
) {
|
|
2670
|
+
return -1;
|
|
2671
|
+
}
|
|
2672
|
+
const firstEquals = rest.indexOf("=");
|
|
2673
|
+
const beforeParameters =
|
|
2674
|
+
firstEquals >= 0 ? rest.lastIndexOf("@", firstEquals) : -1;
|
|
2675
|
+
return beforeParameters >= 0 ? beforeParameters : rest.lastIndexOf("@");
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/**
|
|
2679
|
+
* The `host[:port]` list of a parenthesized address: MySQL
|
|
2680
|
+
* `address=(host=h)(port=3306)` and `(host=h,port=3306,user=u)`, Oracle
|
|
2681
|
+
* `(DESCRIPTION=(ADDRESS=(HOST=h)(PORT=1521)))`. Only host and port values are
|
|
2682
|
+
* read, so a user or password beside them never leaves.
|
|
2683
|
+
*/
|
|
2684
|
+
function parenthesizedHosts(text) {
|
|
2685
|
+
const hosts = [];
|
|
2686
|
+
const ports = [];
|
|
2687
|
+
for (const group of text.split(/[()]/)) {
|
|
2688
|
+
for (const pair of group.split(",")) {
|
|
2689
|
+
const equals = pair.indexOf("=");
|
|
2690
|
+
if (equals <= 0) {
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
const key = pair.slice(0, equals).trim().toLowerCase();
|
|
2694
|
+
const value = pair.slice(equals + 1).trim();
|
|
2695
|
+
if (key === "host" && HOST_PATTERN.test(value)) {
|
|
2696
|
+
hosts.push(value);
|
|
2697
|
+
} else if (key === "port" && /^\d+$/.test(value)) {
|
|
2698
|
+
ports[hosts.length ? hosts.length - 1 : 0] ??= value;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
return hosts.map((host, index) =>
|
|
2703
|
+
ports[index] && !host.includes(":") ? `${host}:${ports[index]}` : host,
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// The hosts and database path of an authority, after its userinfo is gone.
|
|
2708
|
+
function parseAuthority(text) {
|
|
2709
|
+
const parenthesized = text.startsWith("(") || /^[A-Za-z]+=\(/.test(text);
|
|
2710
|
+
if (parenthesized) {
|
|
2711
|
+
const close = text.lastIndexOf(")");
|
|
2712
|
+
const after = close >= 0 ? text.slice(close + 1) : "";
|
|
2713
|
+
return {
|
|
2714
|
+
hosts: parenthesizedHosts(text.slice(0, close + 1)),
|
|
2715
|
+
path: after.startsWith("/") ? cutAt(after, ["?", "#", ";", ":"]) : "",
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
const end = indexOfAny(text, ADDRESS_END);
|
|
2719
|
+
const hostPart = end >= 0 ? text.slice(0, end) : text;
|
|
2720
|
+
const hosts = isHostList(hostPart) ? hostPart.split(",") : [];
|
|
2721
|
+
// DB2 and Informix append `:key=value;` properties to the database name.
|
|
2722
|
+
const path =
|
|
2723
|
+
hosts.length && end >= 0 && text[end] === "/"
|
|
2724
|
+
? cutAt(text.slice(end), ["?", "#", ";", ":"])
|
|
2725
|
+
: "";
|
|
2726
|
+
return { hosts, path };
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* Split a JDBC URL into its subprotocol, hosts and database path, dropping
|
|
2731
|
+
* anything that may carry credentials: userinfo, the `user/password@` prefix
|
|
2732
|
+
* Oracle takes, URL parameters, and the `;key=value` and `:key=value`
|
|
2733
|
+
* properties some drivers take. Host values that do not look like hosts are
|
|
2734
|
+
* dropped too, rather than passed on.
|
|
2735
|
+
*
|
|
2736
|
+
* @param {string} value JDBC URL.
|
|
2737
|
+
* @returns {{scheme: string, hosts: string[], address: string}|undefined}
|
|
2738
|
+
* The first subprotocol, the hosts, and the address rebuilt from those
|
|
2739
|
+
* parts; undefined for anything but a JDBC URL.
|
|
2740
|
+
*/
|
|
2741
|
+
function parseJdbcUrl(value) {
|
|
2742
|
+
const text = String(value || "").trim();
|
|
2743
|
+
if (!/^jdbc:/i.test(text)) {
|
|
2744
|
+
return undefined;
|
|
2745
|
+
}
|
|
2746
|
+
let rest = text.slice(5);
|
|
2747
|
+
const tokens = [];
|
|
2748
|
+
for (
|
|
2749
|
+
let token = /^[A-Za-z][\w+.-]*:/.exec(rest);
|
|
2750
|
+
token && !rest.startsWith("//");
|
|
2751
|
+
token = /^[A-Za-z][\w+.-]*:/.exec(rest)
|
|
2752
|
+
) {
|
|
2753
|
+
tokens.push(token[0]);
|
|
2754
|
+
rest = rest.slice(token[0].length);
|
|
2755
|
+
}
|
|
2756
|
+
const scheme = (tokens[0] || "jdbc:").slice(0, -1).toLowerCase();
|
|
2757
|
+
if (rest.startsWith("//")) {
|
|
2758
|
+
const authority = rest.slice(2);
|
|
2759
|
+
const { hosts, path } = parseAuthority(
|
|
2760
|
+
authority.slice(userinfoEnd(authority) + 1),
|
|
2761
|
+
);
|
|
2762
|
+
return {
|
|
2763
|
+
scheme,
|
|
2764
|
+
hosts,
|
|
2765
|
+
address: `jdbc:${tokens.join("")}//${hosts.join(",")}${path}`,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
const at = userinfoEnd(rest);
|
|
2769
|
+
if (at < 0) {
|
|
2770
|
+
return {
|
|
2771
|
+
scheme,
|
|
2772
|
+
hosts: [],
|
|
2773
|
+
address: `jdbc:${tokens.join("")}${cutAt(rest, ["?", "#", ";"])}`,
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
// `user/password@host:port:sid`: a word before the credentials can read as
|
|
2777
|
+
// a subprotocol (`jdbc:db:user:password@host`), so only the driver keeps
|
|
2778
|
+
// its tokens, and Oracle its driver type.
|
|
2779
|
+
const kept = tokens.slice(0, scheme === "oracle" ? 2 : 1).join("");
|
|
2780
|
+
const target = rest.slice(at + 1);
|
|
2781
|
+
if (target.startsWith("//")) {
|
|
2782
|
+
const { hosts, path } = parseAuthority(target.slice(2));
|
|
2783
|
+
return {
|
|
2784
|
+
scheme,
|
|
2785
|
+
hosts,
|
|
2786
|
+
address: `jdbc:${kept}@//${hosts.join(",")}${path}`,
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
if (target.startsWith("(")) {
|
|
2790
|
+
const hosts = parenthesizedHosts(target);
|
|
2791
|
+
return { scheme, hosts, address: `jdbc:${kept}@${hosts.join(",")}` };
|
|
2792
|
+
}
|
|
2793
|
+
const address = cutAt(target, ["?", "#", ";", "/"]);
|
|
2794
|
+
const host = address.split(":")[0];
|
|
2795
|
+
const hosts = HOST_PATTERN.test(host) ? [host] : [];
|
|
2796
|
+
return {
|
|
2797
|
+
scheme,
|
|
2798
|
+
hosts,
|
|
2799
|
+
address: `jdbc:${kept}@${hosts.length ? address : ""}`,
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2188
2803
|
/**
|
|
2189
2804
|
* A JDBC address without what may carry credentials: userinfo, the Oracle
|
|
2190
2805
|
* `user/password@` prefix, URL parameters and the `;key=value` properties
|
|
@@ -2195,49 +2810,58 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2195
2810
|
* @returns {string} The address part.
|
|
2196
2811
|
*/
|
|
2197
2812
|
export function sanitizeJdbcUrl(value) {
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
const slashes = text.indexOf("//");
|
|
2210
|
-
if (slashes >= 0) {
|
|
2211
|
-
const authorityEnd = text.indexOf("/", slashes + 2);
|
|
2212
|
-
const authority = text.slice(
|
|
2213
|
-
slashes + 2,
|
|
2214
|
-
authorityEnd >= 0 ? authorityEnd : undefined,
|
|
2215
|
-
);
|
|
2216
|
-
const at = authority.lastIndexOf("@");
|
|
2217
|
-
if (at >= 0) {
|
|
2218
|
-
text =
|
|
2219
|
-
text.slice(0, slashes + 2) +
|
|
2220
|
-
authority.slice(at + 1) +
|
|
2221
|
-
(authorityEnd >= 0 ? text.slice(authorityEnd) : "");
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
// Oracle thin: jdbc:oracle:thin:user/password@host:port:sid
|
|
2225
|
-
const thin = /^(jdbc:oracle:thin:)[^@]*@/i.exec(text);
|
|
2226
|
-
if (thin) {
|
|
2227
|
-
text = `${thin[1]}@${text.slice(thin[0].length)}`;
|
|
2813
|
+
const parsed = parseJdbcUrl(value);
|
|
2814
|
+
return parsed ? parsed.address : String(value || "").trim();
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// The host name of `host`, `host:port` or `[ipv6]:port`.
|
|
2818
|
+
function hostName(host) {
|
|
2819
|
+
if (host.startsWith("[")) {
|
|
2820
|
+
const close = host.indexOf("]");
|
|
2821
|
+
return close > 0 ? host.slice(1, close) : undefined;
|
|
2228
2822
|
}
|
|
2229
|
-
return
|
|
2823
|
+
return host.split(":")[0] || undefined;
|
|
2230
2824
|
}
|
|
2231
2825
|
|
|
2232
2826
|
function jdbcServiceName(value) {
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2827
|
+
const { scheme, hosts } = parseJdbcUrl(value);
|
|
2828
|
+
return [scheme, hosts.length ? hostName(hosts[0]) : undefined]
|
|
2829
|
+
.filter(Boolean)
|
|
2830
|
+
.join("-");
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// A host value as `host[:port]`, without a scheme, userinfo, path or
|
|
2834
|
+
// parameters, or undefined when what is left is not a host.
|
|
2835
|
+
function bareHost(value) {
|
|
2836
|
+
let text = String(value || "").trim();
|
|
2837
|
+
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.exec(text);
|
|
2838
|
+
if (scheme) {
|
|
2839
|
+
text = text.slice(scheme[0].length);
|
|
2840
|
+
}
|
|
2841
|
+
text = cutAt(text.slice(userinfoEnd(text) + 1), ADDRESS_END);
|
|
2842
|
+
return HOST_PATTERN.test(text) ? text : undefined;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// The `host[:port]` a URL names. The userinfo is dropped before the URL is
|
|
2846
|
+
// parsed: a password holding `/` would otherwise end the authority early and
|
|
2847
|
+
// leave the user name to read as the host.
|
|
2848
|
+
function urlHost(value) {
|
|
2849
|
+
const separator = value.indexOf("://");
|
|
2850
|
+
if (separator <= 0) {
|
|
2851
|
+
return undefined;
|
|
2852
|
+
}
|
|
2853
|
+
const rest = value.slice(separator + 3);
|
|
2854
|
+
const withoutUserinfo = rest.slice(userinfoEnd(rest) + 1);
|
|
2855
|
+
try {
|
|
2856
|
+
const host = new URL(`${value.slice(0, separator)}://${withoutUserinfo}`)
|
|
2857
|
+
.host;
|
|
2858
|
+
if (host) {
|
|
2859
|
+
return host;
|
|
2860
|
+
}
|
|
2861
|
+
} catch (_err) {
|
|
2862
|
+
// Not a URL the WHATWG parser takes; the host is read by hand.
|
|
2239
2863
|
}
|
|
2240
|
-
return
|
|
2864
|
+
return bareHost(withoutUserinfo);
|
|
2241
2865
|
}
|
|
2242
2866
|
|
|
2243
2867
|
/**
|
|
@@ -2257,13 +2881,11 @@ export function scalasemServiceName(service) {
|
|
|
2257
2881
|
return jdbcServiceName(value);
|
|
2258
2882
|
}
|
|
2259
2883
|
if (service?.host) {
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
try {
|
|
2263
|
-
return new URL(value).hostname || undefined;
|
|
2264
|
-
} catch (_err) {
|
|
2265
|
-
return undefined;
|
|
2884
|
+
const host = bareHost(service.host);
|
|
2885
|
+
return host ? hostName(host) : undefined;
|
|
2266
2886
|
}
|
|
2887
|
+
const host = urlHost(value);
|
|
2888
|
+
return host ? hostName(host) : undefined;
|
|
2267
2889
|
}
|
|
2268
2890
|
|
|
2269
2891
|
/**
|
|
@@ -2285,15 +2907,12 @@ export function scalasemServiceEndpoint(service) {
|
|
|
2285
2907
|
return sanitizeJdbcUrl(value);
|
|
2286
2908
|
}
|
|
2287
2909
|
if (service?.host) {
|
|
2288
|
-
return service.host;
|
|
2289
|
-
}
|
|
2290
|
-
try {
|
|
2291
|
-
const parsed = new URL(value);
|
|
2292
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2293
|
-
} catch (_err) {
|
|
2294
|
-
const hostMatch = /:\/\/([^/?#@]+@)?([^/?#]+)/.exec(value);
|
|
2295
|
-
return hostMatch ? `${value.split("://")[0]}://${hostMatch[2]}` : undefined;
|
|
2910
|
+
return bareHost(service.host);
|
|
2296
2911
|
}
|
|
2912
|
+
const host = urlHost(value);
|
|
2913
|
+
return host
|
|
2914
|
+
? `${value.slice(0, value.indexOf("://")).toLowerCase()}://${host}`
|
|
2915
|
+
: undefined;
|
|
2297
2916
|
}
|
|
2298
2917
|
|
|
2299
2918
|
function serviceLocation(service) {
|
|
@@ -2457,7 +3076,10 @@ export async function collectScalaJsNpmComponents(
|
|
|
2457
3076
|
) {
|
|
2458
3077
|
const isScalaJs = pkgList.some((pkg) => {
|
|
2459
3078
|
const name = pkg?.name || purlPurlName(pkg?.purl) || "";
|
|
2460
|
-
return
|
|
3079
|
+
return (
|
|
3080
|
+
scalaNamePlatform(name) === "js" ||
|
|
3081
|
+
parseScalaArtifact(name).artifactBase === "scalajs-library"
|
|
3082
|
+
);
|
|
2461
3083
|
});
|
|
2462
3084
|
const empty = { components: [], roots: [], dependencies: [] };
|
|
2463
3085
|
if (!isScalaJs) {
|
|
@@ -2546,6 +3168,24 @@ function npmNameFromPurl(purl) {
|
|
|
2546
3168
|
}
|
|
2547
3169
|
}
|
|
2548
3170
|
|
|
3171
|
+
function nodeModulesDepth(component) {
|
|
3172
|
+
const location = (component.properties || []).find(
|
|
3173
|
+
(property) => property.name === "internal:LocalNodeModulesPath",
|
|
3174
|
+
)?.value;
|
|
3175
|
+
return location
|
|
3176
|
+
? location.split("node_modules/").length - 1
|
|
3177
|
+
: Number.POSITIVE_INFINITY;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function isPreferredNpmCopy(candidate, known) {
|
|
3181
|
+
const candidateDepth = nodeModulesDepth(candidate);
|
|
3182
|
+
const knownDepth = nodeModulesDepth(known);
|
|
3183
|
+
if (candidateDepth !== knownDepth) {
|
|
3184
|
+
return candidateDepth < knownDepth;
|
|
3185
|
+
}
|
|
3186
|
+
return candidate.purl < known.purl;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
2549
3189
|
/**
|
|
2550
3190
|
* Attribute the report's JavaScript module imports to the npm components the
|
|
2551
3191
|
* build bundles. A module no component owns is reported on the project
|
|
@@ -2562,10 +3202,17 @@ function addJsModuleEvidence(
|
|
|
2562
3202
|
if (!jsModules.length) {
|
|
2563
3203
|
return;
|
|
2564
3204
|
}
|
|
3205
|
+
// Of several versions of one package, the one a bare import resolves to is
|
|
3206
|
+
// the hoisted copy, the shallowest under node_modules; the first purl
|
|
3207
|
+
// breaks a tie, so the choice never depends on the order of the BOM.
|
|
2565
3208
|
const npmByName = new Map();
|
|
2566
3209
|
for (const component of components) {
|
|
2567
3210
|
const name = npmNameFromPurl(component?.purl);
|
|
2568
|
-
if (name) {
|
|
3211
|
+
if (!name) {
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
const known = npmByName.get(name);
|
|
3215
|
+
if (!known || isPreferredNpmCopy(component, known)) {
|
|
2569
3216
|
npmByName.set(name, component);
|
|
2570
3217
|
}
|
|
2571
3218
|
}
|