@cdxgen/cdxgen 13.5.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 +6 -6
- package/bin/cdxgen.js +4 -0
- package/bin/evinse.js +3 -0
- package/bin/validate.js +6 -0
- package/data/license-aliases.json +24 -2
- package/data/remediations.json +124 -0
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -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/cli/bomAssembly.js +92 -3
- package/lib/cli/index.js +108 -1
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +237 -46
- package/lib/cli/managedBom.js +142 -16
- package/lib/cli/nativeBom.js +42 -5
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +8 -0
- package/lib/core/fs.js +12 -0
- package/lib/core/httpClient.js +98 -1
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -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/gradleutils.js +828 -224
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-misc.js +209 -20
- package/lib/ecosystems/parsers-python.js +89 -2
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +538 -31
- package/lib/ecosystems/scalacliutils.js +2 -0
- package/lib/ecosystems/scalasem.js +796 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +346 -43
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/atomUtils.js +77 -0
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/deps.js +35 -0
- package/lib/inventory/dosai.js +139 -5
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/envcontext.js +307 -13
- package/lib/inventory/evidenceUtils.js +11 -0
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +31 -3
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -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/osPackageResolver.js +182 -0
- package/lib/inventory/phpBuildInfo.js +10 -59
- package/lib/inventory/phpHostProbe.js +32 -488
- 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 +49 -2
- package/lib/managers/binary.js +337 -3
- package/lib/managers/docker.js +79 -0
- package/lib/parsers/largeJson.js +28 -11
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +12 -1
- package/lib/stages/postgen/ruleEngine.js +13 -5
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +114 -57
- package/lib/validator/bomValidator.js +6 -1
- package/lib/validator/generated/validate-1.6.mjs +1 -1
- package/lib/validator/generated/validate-1.7.mjs +1 -1
- package/lib/validator/generated/validate-2.0.mjs +1 -1
- package/lib/validator/index.js +1 -1
- package/package.json +3 -2
- package/types/lib/cli/bomAssembly.d.ts +13 -1
- package/types/lib/cli/bomAssembly.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.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 +4 -0
- 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/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/gradleutils.d.ts +47 -13
- 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-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +10 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +20 -2
- 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/scalacliutils.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 +63 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +19 -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/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +17 -1
- 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/envcontext.d.ts +52 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/gtfobins.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/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/osPackageResolver.d.ts +38 -0
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
- 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 +10 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +4 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +12 -8
- package/types/lib/stages/pregen/pregen.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,12 +16,12 @@ 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";
|
|
@@ -147,22 +155,25 @@ function scalasemTimeoutMs(options = {}) {
|
|
|
147
155
|
const TIMEOUT_GRACE_MS = 30_000;
|
|
148
156
|
|
|
149
157
|
// The build files that identify a Scala project, for a source argument that
|
|
150
|
-
// 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/.
|
|
151
160
|
const SCALA_BUILD_MARKERS = [
|
|
152
161
|
"build.sbt",
|
|
153
162
|
"build.sc",
|
|
154
163
|
"build.mill",
|
|
155
164
|
"pom.xml",
|
|
156
165
|
"project.scala",
|
|
166
|
+
join("project", "build.properties"),
|
|
157
167
|
];
|
|
158
168
|
|
|
159
169
|
/**
|
|
160
170
|
* Where the scalasem report is persisted. An absolute --semantics-slices-file
|
|
161
171
|
* keeps its directory; anything else lands in the evinse output directory, the
|
|
162
|
-
* 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.
|
|
163
174
|
*
|
|
164
175
|
* @param {Object} options CLI options carrying `output` and `semanticsSlicesFile`.
|
|
165
|
-
* @returns {string} Absolute report path.
|
|
176
|
+
* @returns {string|undefined} Absolute report path.
|
|
166
177
|
*/
|
|
167
178
|
export function scalasemOutputFile(options = {}) {
|
|
168
179
|
const requested = options.semanticsSlicesFile || "semantics.slices.json";
|
|
@@ -171,21 +182,18 @@ export function scalasemOutputFile(options = {}) {
|
|
|
171
182
|
if (isAbsolute(requested)) {
|
|
172
183
|
return requested;
|
|
173
184
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
186
|
-
} else {
|
|
187
|
-
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;
|
|
188
195
|
}
|
|
196
|
+
const outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
189
197
|
return join(outputDir, basename(requested));
|
|
190
198
|
}
|
|
191
199
|
|
|
@@ -224,6 +232,18 @@ export function isV1SemanticsSlice(parsed) {
|
|
|
224
232
|
return Object.keys(parsed).some((key) => key.endsWith(".scala"));
|
|
225
233
|
}
|
|
226
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
|
+
|
|
227
247
|
/**
|
|
228
248
|
* Whether a version 1 slice describes this project: it names no project, so
|
|
229
249
|
* every source file it lists must exist under the project directory.
|
|
@@ -234,15 +254,104 @@ function isV1SliceOfProject(slice, projectDir) {
|
|
|
234
254
|
files.length > 0 &&
|
|
235
255
|
files.every((file) => {
|
|
236
256
|
const path = resolve(projectDir, file);
|
|
237
|
-
return
|
|
257
|
+
return isInside(projectDir, path) && safeExistsSync(path);
|
|
238
258
|
})
|
|
239
259
|
);
|
|
240
260
|
}
|
|
241
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
|
+
|
|
242
350
|
/**
|
|
243
351
|
* Run scalasem over a Scala project and return its report, reusing a
|
|
244
|
-
* persisted version 2 report that belongs to this project
|
|
245
|
-
* 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.
|
|
246
355
|
*
|
|
247
356
|
* Failures are visible rather than silent: the reason is printed once, the
|
|
248
357
|
* run's diagnostics reach the caller as `cdx:scalasem:diagnostic` properties,
|
|
@@ -280,10 +389,18 @@ export function analyzeScalaProject(src, options = {}) {
|
|
|
280
389
|
for (const candidate of candidates) {
|
|
281
390
|
const existing = readScalasemReport(candidate);
|
|
282
391
|
if (existing?._meta?.schemaVersion === "scalasem/2") {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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) {
|
|
287
404
|
if (DEBUG_MODE) {
|
|
288
405
|
console.log(`Reusing the scalasem report "${candidate}".`);
|
|
289
406
|
}
|
|
@@ -340,13 +457,38 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
340
457
|
// earlier run (or another project's run) had left there, and a report
|
|
341
458
|
// written within the millisecond the run started passed for fresh.
|
|
342
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
|
+
) {
|
|
343
484
|
const runReportFile = join(runDir, "scalasem.slices.json");
|
|
344
485
|
const args = [scalasem, analysisDir, runReportFile];
|
|
486
|
+
const settings = scalasemRunSettings(options);
|
|
345
487
|
// The build runs the project's sbt definitions
|
|
346
|
-
if (!
|
|
488
|
+
if (!settings.build) {
|
|
347
489
|
args.push("--no-build");
|
|
348
490
|
}
|
|
349
|
-
if (
|
|
491
|
+
if (settings.includeTests) {
|
|
350
492
|
args.push("--include-tests");
|
|
351
493
|
}
|
|
352
494
|
if (DEBUG_MODE) {
|
|
@@ -357,17 +499,24 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
357
499
|
// scalasem enforces the limit itself, so the builds it started stop with
|
|
358
500
|
// it; cdxgen's own timeout is the backstop. Naming cdxgen as the supervisor
|
|
359
501
|
// stops scalasem when cdxgen is killed.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
520
|
const elapsedMs = Date.now() - startedAt;
|
|
372
521
|
const wroteReport = safeExistsSync(runReportFile);
|
|
373
522
|
const written = wroteReport ? readScalasemReport(runReportFile) : undefined;
|
|
@@ -431,7 +580,9 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
431
580
|
),
|
|
432
581
|
];
|
|
433
582
|
}
|
|
434
|
-
const summary = summarizeDiagnostics(
|
|
583
|
+
const summary = summarizeDiagnostics(
|
|
584
|
+
degradingDiagnostics(report._meta.diagnostics),
|
|
585
|
+
);
|
|
435
586
|
if (summary) {
|
|
436
587
|
console.warn(
|
|
437
588
|
`scalasem: the report is degraded: ${summary}. The cdx:scalasem:diagnostic:* metadata properties of the BOM say where.`,
|
|
@@ -443,19 +594,26 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
443
594
|
}
|
|
444
595
|
// Only a usable report replaces the one at the requested path, and a
|
|
445
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.
|
|
446
599
|
let reportFile;
|
|
447
|
-
if (usable && v1File !== requestedFile) {
|
|
600
|
+
if (usable && requestedFile && v1File !== requestedFile) {
|
|
448
601
|
try {
|
|
449
602
|
safeMkdirSync(dirname(requestedFile), { recursive: true });
|
|
450
|
-
|
|
451
|
-
|
|
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;
|
|
452
611
|
} catch (err) {
|
|
453
612
|
console.warn(
|
|
454
613
|
`scalasem: the report could not be kept at ${requestedFile} (${err.message}); this run still uses it.`,
|
|
455
614
|
);
|
|
456
615
|
}
|
|
457
616
|
}
|
|
458
|
-
safeRmSync(runDir, { recursive: true, force: true });
|
|
459
617
|
return {
|
|
460
618
|
report,
|
|
461
619
|
reportFile,
|
|
@@ -464,6 +622,21 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
464
622
|
};
|
|
465
623
|
}
|
|
466
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
|
+
|
|
467
640
|
function summarizeDiagnostics(diagnostics = []) {
|
|
468
641
|
const byCode = new Map();
|
|
469
642
|
for (const diagnostic of diagnostics || []) {
|
|
@@ -490,8 +663,9 @@ function summarizeDiagnostics(diagnostics = []) {
|
|
|
490
663
|
*/
|
|
491
664
|
export function scalasemMetadataProperties(
|
|
492
665
|
report,
|
|
493
|
-
|
|
666
|
+
allDiagnostics = report?._meta?.diagnostics || [],
|
|
494
667
|
) {
|
|
668
|
+
const diagnostics = degradingDiagnostics(allDiagnostics);
|
|
495
669
|
const properties = [];
|
|
496
670
|
const meta = report?._meta || {};
|
|
497
671
|
appendUniqueProperty(
|
|
@@ -553,20 +727,23 @@ export function sortedCsv(values) {
|
|
|
553
727
|
|
|
554
728
|
/**
|
|
555
729
|
* The platform a purl's artifact name targets: `_sjs1` is Scala.js,
|
|
556
|
-
* `_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).
|
|
557
733
|
*
|
|
558
734
|
* @param {string} purl Package URL.
|
|
559
735
|
* @returns {"jvm"|"js"|"native"} Platform key.
|
|
560
736
|
*/
|
|
561
737
|
export function scalaPurlPlatform(purl) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
738
|
+
return scalaNamePlatform(purlPurlName(purl));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function scalaNamePlatform(name) {
|
|
742
|
+
const { platformSuffix } = parseScalaArtifact(name);
|
|
743
|
+
if (platformSuffix.startsWith("_sjs")) {
|
|
567
744
|
return "js";
|
|
568
745
|
}
|
|
569
|
-
if (
|
|
746
|
+
if (platformSuffix.startsWith("_native")) {
|
|
570
747
|
return "native";
|
|
571
748
|
}
|
|
572
749
|
return "jvm";
|
|
@@ -584,6 +761,20 @@ function purlPurlName(purl) {
|
|
|
584
761
|
}
|
|
585
762
|
}
|
|
586
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
|
+
|
|
587
778
|
/**
|
|
588
779
|
* The class and package index that joins scalasem symbols to component purls.
|
|
589
780
|
*
|
|
@@ -633,11 +824,22 @@ export class ScalaJoinIndex {
|
|
|
633
824
|
if (!symbol || symbol.includes(" ") || symbol.includes("/")) {
|
|
634
825
|
continue;
|
|
635
826
|
}
|
|
636
|
-
|
|
637
|
-
|
|
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);
|
|
638
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
|
+
}
|
|
639
841
|
this.addSymbol(this.classPurls, topLevel, purl, source);
|
|
640
|
-
if (dollar > 0) {
|
|
842
|
+
if (dollar > 0 && !synthetic) {
|
|
641
843
|
const nested = symbol.replaceAll("$", ".").replace(/\.+$/, "");
|
|
642
844
|
if (nested !== topLevel) {
|
|
643
845
|
this.addSymbol(this.classPurls, nested, purl, source);
|
|
@@ -734,15 +936,25 @@ export class ScalaJoinIndex {
|
|
|
734
936
|
* sits in. A package the project also fills is ambiguous and joins nothing,
|
|
735
937
|
* and `javax.` names join only through an exact class, since the JDK ships
|
|
736
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.
|
|
737
945
|
*/
|
|
738
946
|
packageOwners(symbol) {
|
|
739
947
|
if (symbol.startsWith(JDK_EXTENSION_PREFIX)) {
|
|
740
948
|
return undefined;
|
|
741
949
|
}
|
|
742
950
|
const parts = symbol.replace(/\$+$/, "").split(".");
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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;
|
|
746
958
|
if (owners?.size) {
|
|
747
959
|
return this.projectPackages.has(name) ? undefined : owners;
|
|
748
960
|
}
|
|
@@ -875,41 +1087,83 @@ function componentBinaryVersions(component, coordinate) {
|
|
|
875
1087
|
);
|
|
876
1088
|
}
|
|
877
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
|
+
|
|
878
1109
|
/**
|
|
879
1110
|
* Index the BOM components by their normalized Scala coordinates, both with
|
|
880
1111
|
* and without the version, so a classpath jar, a namespace property and a map
|
|
881
1112
|
* entry of the same library meet on one component. Each entry keeps the Scala
|
|
882
|
-
* binary
|
|
883
|
-
* 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.
|
|
884
1120
|
*/
|
|
885
1121
|
export function componentCoordinateIndex(components = []) {
|
|
886
|
-
const
|
|
887
|
-
const withoutVersion = new Map();
|
|
1122
|
+
const groupsByKey = new Map();
|
|
888
1123
|
for (const component of components) {
|
|
889
1124
|
const coordinate = scalaCoordinateOfPurl(component?.purl);
|
|
890
1125
|
if (!coordinate) {
|
|
891
1126
|
continue;
|
|
892
1127
|
}
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
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 = {
|
|
896
1140
|
component,
|
|
897
|
-
binaryVersions:
|
|
1141
|
+
binaryVersions: new Set(binaryVersions),
|
|
898
1142
|
};
|
|
899
|
-
|
|
900
|
-
|
|
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);
|
|
901
1149
|
}
|
|
902
|
-
|
|
903
|
-
|
|
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({
|
|
904
1158
|
type,
|
|
905
1159
|
group,
|
|
906
1160
|
artifactBase,
|
|
907
1161
|
platformSuffix,
|
|
908
1162
|
});
|
|
909
|
-
|
|
910
|
-
withoutVersion.
|
|
911
|
-
|
|
912
|
-
|
|
1163
|
+
withoutVersion.set(versionless, [
|
|
1164
|
+
...(withoutVersion.get(versionless) || []),
|
|
1165
|
+
...groups,
|
|
1166
|
+
]);
|
|
913
1167
|
}
|
|
914
1168
|
return { byVersion, withoutVersion };
|
|
915
1169
|
}
|
|
@@ -944,11 +1198,126 @@ function pickByBinaryVersion(entries, binaryVersion) {
|
|
|
944
1198
|
* @returns {{component: Object, exact: boolean}|undefined} The component, and
|
|
945
1199
|
* whether its version is the one on the classpath.
|
|
946
1200
|
*/
|
|
947
|
-
export function matchClasspathEntry(
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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) {
|
|
952
1321
|
const parsed = parseScalaArtifact(entry?.artifact);
|
|
953
1322
|
const parts = {
|
|
954
1323
|
group: entry?.group,
|
|
@@ -963,7 +1332,7 @@ export function matchClasspathEntry(
|
|
|
963
1332
|
return { component: exact[0].component, exact: true };
|
|
964
1333
|
}
|
|
965
1334
|
if (exact.length > 1) {
|
|
966
|
-
return
|
|
1335
|
+
return { ambiguous: true };
|
|
967
1336
|
}
|
|
968
1337
|
const candidates = pickByBinaryVersion(
|
|
969
1338
|
withoutVersion.get(scalaCoordinateKey(parts)),
|
|
@@ -973,9 +1342,10 @@ export function matchClasspathEntry(
|
|
|
973
1342
|
(candidate) => scalaPurlPlatform(candidate.component.purl) === platform,
|
|
974
1343
|
);
|
|
975
1344
|
const pool = matching.length ? matching : candidates;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1345
|
+
if (pool.length === 1) {
|
|
1346
|
+
return { component: pool[0].component, exact: false };
|
|
1347
|
+
}
|
|
1348
|
+
return pool.length > 1 ? { ambiguous: true } : undefined;
|
|
979
1349
|
}
|
|
980
1350
|
|
|
981
1351
|
/**
|
|
@@ -1043,8 +1413,14 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1043
1413
|
}
|
|
1044
1414
|
}
|
|
1045
1415
|
for (const mapPurl of Object.keys(jarNSMapping || {})) {
|
|
1046
|
-
|
|
1047
|
-
|
|
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) {
|
|
1048
1424
|
continue;
|
|
1049
1425
|
}
|
|
1050
1426
|
const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)@([^?#]*)/.exec(
|
|
@@ -1063,7 +1439,7 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1063
1439
|
scalaPurlPlatform(mapPurl),
|
|
1064
1440
|
);
|
|
1065
1441
|
if (matched) {
|
|
1066
|
-
index.addNamespaces(matched.component.purl,
|
|
1442
|
+
index.addNamespaces(matched.component.purl, namespaces, 3);
|
|
1067
1443
|
}
|
|
1068
1444
|
}
|
|
1069
1445
|
return index;
|
|
@@ -1310,10 +1686,11 @@ export function scalasemCryptoOidKey(name, keySize, mode, curve, padding) {
|
|
|
1310
1686
|
return undefined;
|
|
1311
1687
|
}
|
|
1312
1688
|
if (SCALASEM_AES_NAMES.has(name)) {
|
|
1689
|
+
const upperMode = mode?.toUpperCase();
|
|
1313
1690
|
const modeKey =
|
|
1314
|
-
SCALASEM_AES_MODE_KEYS[
|
|
1691
|
+
SCALASEM_AES_MODE_KEYS[upperMode] ||
|
|
1315
1692
|
SCALASEM_AES_MODE_KEYS[name.split("-")[1]];
|
|
1316
|
-
const wrap = name === "AES-KW" ||
|
|
1693
|
+
const wrap = name === "AES-KW" || upperMode === "KW";
|
|
1317
1694
|
if (keySize && (modeKey !== undefined || wrap)) {
|
|
1318
1695
|
const sizedKey = wrap ? `aes${keySize}-wrap` : `aes${keySize}-${modeKey}`;
|
|
1319
1696
|
const hit = SCALASEM_OID_KEYS.get(sizedKey.toLowerCase());
|
|
@@ -1456,6 +1833,7 @@ export async function collectScalasemEvidence(
|
|
|
1456
1833
|
componentPropertiesMap,
|
|
1457
1834
|
metadataProperties,
|
|
1458
1835
|
options,
|
|
1836
|
+
allowedPurlsByModule,
|
|
1459
1837
|
);
|
|
1460
1838
|
addJsModuleEvidence(
|
|
1461
1839
|
report,
|
|
@@ -1788,7 +2166,17 @@ function stackTargetPurls(sink, fileEntry, joinIndex, platform, allowed) {
|
|
|
1788
2166
|
return [...purls];
|
|
1789
2167
|
}
|
|
1790
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.
|
|
1791
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
|
+
}
|
|
1792
2180
|
const text = String(className || "").replace(/\$+$/, "");
|
|
1793
2181
|
const dot = text.lastIndexOf(".");
|
|
1794
2182
|
return dot > 0 ? text.slice(0, dot) : "";
|
|
@@ -1848,9 +2236,22 @@ function providerAliases(purl, components = []) {
|
|
|
1848
2236
|
/**
|
|
1849
2237
|
* The component purl of the library a crypto finding names as its provider.
|
|
1850
2238
|
* Maven coordinates match on group and base artifact; the short aliases cover
|
|
1851
|
-
* 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.
|
|
1852
2249
|
*/
|
|
1853
|
-
export function scalasemProviderPurl(
|
|
2250
|
+
export function scalasemProviderPurl(
|
|
2251
|
+
provider,
|
|
2252
|
+
components = [],
|
|
2253
|
+
classpathPurls = undefined,
|
|
2254
|
+
) {
|
|
1854
2255
|
if (!provider) {
|
|
1855
2256
|
return undefined;
|
|
1856
2257
|
}
|
|
@@ -1865,6 +2266,7 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1865
2266
|
return undefined;
|
|
1866
2267
|
}
|
|
1867
2268
|
const base = parseScalaArtifact(artifactPrefix).artifactBase;
|
|
2269
|
+
const matches = [];
|
|
1868
2270
|
for (const component of components) {
|
|
1869
2271
|
if (!component?.purl) {
|
|
1870
2272
|
continue;
|
|
@@ -1877,10 +2279,23 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1877
2279
|
}
|
|
1878
2280
|
const name = parseScalaArtifact(decodeSafe(segments[2])).artifactBase;
|
|
1879
2281
|
if (name === base || name.startsWith(`${base}-`)) {
|
|
1880
|
-
|
|
2282
|
+
matches.push({ purl: component.purl, name });
|
|
1881
2283
|
}
|
|
1882
2284
|
}
|
|
1883
|
-
|
|
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;
|
|
1884
2299
|
}
|
|
1885
2300
|
|
|
1886
2301
|
// Primitives a mode of operation applies to. RSA spells `RSA/ECB/...` in JCA,
|
|
@@ -1965,6 +2380,7 @@ function addCryptoEvidence(
|
|
|
1965
2380
|
componentPropertiesMap,
|
|
1966
2381
|
metadataProperties,
|
|
1967
2382
|
options = {},
|
|
2383
|
+
allowedPurlsByModule = new Map(),
|
|
1968
2384
|
) {
|
|
1969
2385
|
const includeTests = options.scalasemIncludeTests === true;
|
|
1970
2386
|
const nativeLibraryBySymbol = new Map();
|
|
@@ -1979,11 +2395,17 @@ function addCryptoEvidence(
|
|
|
1979
2395
|
if (scope === "test" && !includeTests) {
|
|
1980
2396
|
continue;
|
|
1981
2397
|
}
|
|
1982
|
-
const location =
|
|
2398
|
+
const location = finding.line
|
|
2399
|
+
? `${finding.file || ""}#${finding.line}`
|
|
2400
|
+
: finding.file || "";
|
|
1983
2401
|
const nativeLibrary = finding.kind?.startsWith("native")
|
|
1984
2402
|
? nativeLibraryBySymbol.get(finding.name)
|
|
1985
2403
|
: undefined;
|
|
1986
|
-
const providerPurl = scalasemProviderPurl(
|
|
2404
|
+
const providerPurl = scalasemProviderPurl(
|
|
2405
|
+
finding.provider,
|
|
2406
|
+
components,
|
|
2407
|
+
allowedPurlsFor(report[finding.file], allowedPurlsByModule),
|
|
2408
|
+
);
|
|
1987
2409
|
const asset =
|
|
1988
2410
|
finding.resolution === "unresolved"
|
|
1989
2411
|
? undefined
|
|
@@ -2054,12 +2476,18 @@ function addCryptoEvidence(
|
|
|
2054
2476
|
}
|
|
2055
2477
|
}
|
|
2056
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.
|
|
2057
2481
|
function parseLocationString(location) {
|
|
2058
2482
|
const separator = location.lastIndexOf("#");
|
|
2059
|
-
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);
|
|
2060
2488
|
return {
|
|
2061
2489
|
location: location.slice(0, separator),
|
|
2062
|
-
...(
|
|
2490
|
+
...(line > 0 ? { line } : {}),
|
|
2063
2491
|
};
|
|
2064
2492
|
}
|
|
2065
2493
|
|
|
@@ -2187,6 +2615,191 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2187
2615
|
return properties;
|
|
2188
2616
|
}
|
|
2189
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
|
+
|
|
2190
2803
|
/**
|
|
2191
2804
|
* A JDBC address without what may carry credentials: userinfo, the Oracle
|
|
2192
2805
|
* `user/password@` prefix, URL parameters and the `;key=value` properties
|
|
@@ -2197,49 +2810,58 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2197
2810
|
* @returns {string} The address part.
|
|
2198
2811
|
*/
|
|
2199
2812
|
export function sanitizeJdbcUrl(value) {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
const slashes = text.indexOf("//");
|
|
2212
|
-
if (slashes >= 0) {
|
|
2213
|
-
const authorityEnd = text.indexOf("/", slashes + 2);
|
|
2214
|
-
const authority = text.slice(
|
|
2215
|
-
slashes + 2,
|
|
2216
|
-
authorityEnd >= 0 ? authorityEnd : undefined,
|
|
2217
|
-
);
|
|
2218
|
-
const at = authority.lastIndexOf("@");
|
|
2219
|
-
if (at >= 0) {
|
|
2220
|
-
text =
|
|
2221
|
-
text.slice(0, slashes + 2) +
|
|
2222
|
-
authority.slice(at + 1) +
|
|
2223
|
-
(authorityEnd >= 0 ? text.slice(authorityEnd) : "");
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
// Oracle thin: jdbc:oracle:thin:user/password@host:port:sid
|
|
2227
|
-
const thin = /^(jdbc:oracle:thin:)[^@]*@/i.exec(text);
|
|
2228
|
-
if (thin) {
|
|
2229
|
-
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;
|
|
2230
2822
|
}
|
|
2231
|
-
return
|
|
2823
|
+
return host.split(":")[0] || undefined;
|
|
2232
2824
|
}
|
|
2233
2825
|
|
|
2234
2826
|
function jdbcServiceName(value) {
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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.
|
|
2241
2863
|
}
|
|
2242
|
-
return
|
|
2864
|
+
return bareHost(withoutUserinfo);
|
|
2243
2865
|
}
|
|
2244
2866
|
|
|
2245
2867
|
/**
|
|
@@ -2259,13 +2881,11 @@ export function scalasemServiceName(service) {
|
|
|
2259
2881
|
return jdbcServiceName(value);
|
|
2260
2882
|
}
|
|
2261
2883
|
if (service?.host) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
try {
|
|
2265
|
-
return new URL(value).hostname || undefined;
|
|
2266
|
-
} catch (_err) {
|
|
2267
|
-
return undefined;
|
|
2884
|
+
const host = bareHost(service.host);
|
|
2885
|
+
return host ? hostName(host) : undefined;
|
|
2268
2886
|
}
|
|
2887
|
+
const host = urlHost(value);
|
|
2888
|
+
return host ? hostName(host) : undefined;
|
|
2269
2889
|
}
|
|
2270
2890
|
|
|
2271
2891
|
/**
|
|
@@ -2287,15 +2907,12 @@ export function scalasemServiceEndpoint(service) {
|
|
|
2287
2907
|
return sanitizeJdbcUrl(value);
|
|
2288
2908
|
}
|
|
2289
2909
|
if (service?.host) {
|
|
2290
|
-
return service.host;
|
|
2291
|
-
}
|
|
2292
|
-
try {
|
|
2293
|
-
const parsed = new URL(value);
|
|
2294
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2295
|
-
} catch (_err) {
|
|
2296
|
-
const hostMatch = /:\/\/([^/?#@]+@)?([^/?#]+)/.exec(value);
|
|
2297
|
-
return hostMatch ? `${value.split("://")[0]}://${hostMatch[2]}` : undefined;
|
|
2910
|
+
return bareHost(service.host);
|
|
2298
2911
|
}
|
|
2912
|
+
const host = urlHost(value);
|
|
2913
|
+
return host
|
|
2914
|
+
? `${value.slice(0, value.indexOf("://")).toLowerCase()}://${host}`
|
|
2915
|
+
: undefined;
|
|
2299
2916
|
}
|
|
2300
2917
|
|
|
2301
2918
|
function serviceLocation(service) {
|
|
@@ -2459,7 +3076,10 @@ export async function collectScalaJsNpmComponents(
|
|
|
2459
3076
|
) {
|
|
2460
3077
|
const isScalaJs = pkgList.some((pkg) => {
|
|
2461
3078
|
const name = pkg?.name || purlPurlName(pkg?.purl) || "";
|
|
2462
|
-
return
|
|
3079
|
+
return (
|
|
3080
|
+
scalaNamePlatform(name) === "js" ||
|
|
3081
|
+
parseScalaArtifact(name).artifactBase === "scalajs-library"
|
|
3082
|
+
);
|
|
2463
3083
|
});
|
|
2464
3084
|
const empty = { components: [], roots: [], dependencies: [] };
|
|
2465
3085
|
if (!isScalaJs) {
|
|
@@ -2548,6 +3168,24 @@ function npmNameFromPurl(purl) {
|
|
|
2548
3168
|
}
|
|
2549
3169
|
}
|
|
2550
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
|
+
|
|
2551
3189
|
/**
|
|
2552
3190
|
* Attribute the report's JavaScript module imports to the npm components the
|
|
2553
3191
|
* build bundles. A module no component owns is reported on the project
|
|
@@ -2564,10 +3202,17 @@ function addJsModuleEvidence(
|
|
|
2564
3202
|
if (!jsModules.length) {
|
|
2565
3203
|
return;
|
|
2566
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.
|
|
2567
3208
|
const npmByName = new Map();
|
|
2568
3209
|
for (const component of components) {
|
|
2569
3210
|
const name = npmNameFromPurl(component?.purl);
|
|
2570
|
-
if (name) {
|
|
3211
|
+
if (!name) {
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
const known = npmByName.get(name);
|
|
3215
|
+
if (!known || isPreferredNpmCopy(component, known)) {
|
|
2571
3216
|
npmByName.set(name, component);
|
|
2572
3217
|
}
|
|
2573
3218
|
}
|