@westbayberry/dg 2.0.4 → 2.0.5
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/dist/api/analyze.js
CHANGED
|
@@ -6,6 +6,7 @@ import { envAuthToken } from "../auth/env-token.js";
|
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
7
|
import { sanitize, sanitizeResponse } from "../security/sanitize.js";
|
|
8
8
|
import { resolveDgPaths } from "../state/index.js";
|
|
9
|
+
import { dgVersion } from "../commands/version.js";
|
|
9
10
|
export class AnalyzeError extends Error {
|
|
10
11
|
statusCode;
|
|
11
12
|
body;
|
|
@@ -29,13 +30,15 @@ export async function analyzePackages(packages, options) {
|
|
|
29
30
|
const token = resolveToken(env);
|
|
30
31
|
const deviceId = getOrCreateDeviceId(env);
|
|
31
32
|
const url = `${baseUrl}${ANALYZE_PATHS[options.ecosystem]}`;
|
|
32
|
-
|
|
33
|
+
const batchCount = Math.max(1, Math.ceil(packages.length / BATCH_SIZE));
|
|
34
|
+
options.onProgress?.({ done: 0, total: packages.length, batchIndex: 0, batchCount });
|
|
33
35
|
const responses = [];
|
|
34
36
|
for (let index = 0; index < packages.length; index += BATCH_SIZE) {
|
|
35
37
|
const batch = packages.slice(index, index + BATCH_SIZE);
|
|
36
|
-
|
|
38
|
+
const batchIndex = Math.floor(index / BATCH_SIZE) + 1;
|
|
39
|
+
options.onProgress?.({ done: index, total: packages.length, batchIndex, batchCount });
|
|
37
40
|
responses.push(await analyzeBatchWithRetry(url, batch, token, deviceId, fetchImpl, options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
38
|
-
options.onProgress?.(Math.min(index + batch.length, packages.length), packages.length,
|
|
41
|
+
options.onProgress?.({ done: Math.min(index + batch.length, packages.length), total: packages.length, batchIndex, batchCount });
|
|
39
42
|
}
|
|
40
43
|
return mergeAnalyzeResponses(responses);
|
|
41
44
|
}
|
|
@@ -86,6 +89,7 @@ async function analyzeBatch(url, batch, token, deviceId, fetchImpl, timeoutMs) {
|
|
|
86
89
|
headers: {
|
|
87
90
|
"Content-Type": "application/json",
|
|
88
91
|
"X-Device-Id": deviceId,
|
|
92
|
+
"X-Dg-Version": dgVersion(),
|
|
89
93
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
90
94
|
},
|
|
91
95
|
body: JSON.stringify({
|
|
@@ -128,8 +128,8 @@ export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialVi
|
|
|
128
128
|
case "selecting":
|
|
129
129
|
return (_jsx(ProjectSelector, { projects: state.projects, onConfirm: scanSelectedProjects, onCancel: () => { process.exitCode = 0; leaveAltScreen(); exit(); }, userStatus: userStatus }));
|
|
130
130
|
case "scanning":
|
|
131
|
-
return (_jsx(ProgressBar, { value: state.done, total: state.total, label: state.
|
|
132
|
-
? state.
|
|
131
|
+
return (_jsx(ProgressBar, { value: state.done, total: state.total, label: state.batchCount > 1 && state.batchIndex >= 1
|
|
132
|
+
? `batch ${state.batchIndex}/${state.batchCount}`
|
|
133
133
|
: undefined }));
|
|
134
134
|
case "results":
|
|
135
135
|
return (_jsx(InteractiveResultsView, { result: state.result, config: config, durationMs: state.durationMs, onExit: handleResultsExit, onBack: restartSelection ?? undefined, discoveredTotal: state.discoveredTotal, userStatus: userStatus, scanUsage: scanUsage, initialView: initialView }));
|
|
@@ -47,7 +47,7 @@ function isYankedIncomplete(pkg) {
|
|
|
47
47
|
}
|
|
48
48
|
export function packageBadge(pkg) {
|
|
49
49
|
if (isYankedIncomplete(pkg))
|
|
50
|
-
return { label: "
|
|
50
|
+
return { label: "Unverified", color: chalk.yellow };
|
|
51
51
|
return actionBadge(pkg.action);
|
|
52
52
|
}
|
|
53
53
|
const EVIDENCE_LIMIT = 2;
|
|
@@ -21,9 +21,10 @@ export function truncate(s, max) {
|
|
|
21
21
|
export function groupPackages(packages, keyBy = "name") {
|
|
22
22
|
const map = new Map();
|
|
23
23
|
for (const pkg of packages) {
|
|
24
|
+
const action = pkg.action ?? "pass";
|
|
24
25
|
const fingerprint = pkg.findings.length === 0
|
|
25
|
-
?
|
|
26
|
-
: pkg.findings
|
|
26
|
+
? `${action}|${pkg.name}@${pkg.version ?? ""}|score:${pkg.score}`
|
|
27
|
+
: `${action}|` + pkg.findings
|
|
27
28
|
.map((f) => `${f.category ?? ""}:${f.severity}`)
|
|
28
29
|
.sort()
|
|
29
30
|
.join("|") + `|score:${pkg.score}`;
|
|
@@ -9,11 +9,11 @@ function reducer(_state, action) {
|
|
|
9
9
|
case "DISCOVERY_PROGRESS":
|
|
10
10
|
return { phase: "discovering", path: action.path, found: action.found };
|
|
11
11
|
case "DISCOVERY_COMPLETE":
|
|
12
|
-
return { phase: "scanning", done: 0, total: action.total,
|
|
12
|
+
return { phase: "scanning", done: 0, total: action.total, batchIndex: 0, batchCount: 1 };
|
|
13
13
|
case "DISCOVERY_EMPTY":
|
|
14
14
|
return { phase: "empty", message: action.message };
|
|
15
15
|
case "SCAN_PROGRESS":
|
|
16
|
-
return { phase: "scanning", done: action.done, total: action.total,
|
|
16
|
+
return { phase: "scanning", done: action.done, total: action.total, batchIndex: action.batchIndex, batchCount: action.batchCount };
|
|
17
17
|
case "SCAN_COMPLETE":
|
|
18
18
|
return {
|
|
19
19
|
phase: "results",
|
|
@@ -83,8 +83,8 @@ async function scanProjects(projects, dispatch) {
|
|
|
83
83
|
const base = completed;
|
|
84
84
|
responses.push(await analyzePackages(packages, {
|
|
85
85
|
ecosystem,
|
|
86
|
-
onProgress: (
|
|
87
|
-
dispatch({ type: "SCAN_PROGRESS", done: base + done, total,
|
|
86
|
+
onProgress: (progress) => {
|
|
87
|
+
dispatch({ type: "SCAN_PROGRESS", done: base + progress.done, total, batchIndex: progress.batchIndex, batchCount: progress.batchCount });
|
|
88
88
|
}
|
|
89
89
|
}));
|
|
90
90
|
completed = base + packages.length;
|
package/dist/verify/preflight.js
CHANGED
|
@@ -245,44 +245,73 @@ function parsePackageLock(text) {
|
|
|
245
245
|
return [malformedLockfileObservation("package-lock.json", new Error("root must be an object"))];
|
|
246
246
|
}
|
|
247
247
|
const observations = [];
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
if (isRecord(parsed.packages)) {
|
|
249
|
+
for (const [path, rawPackage] of Object.entries(parsed.packages)) {
|
|
250
|
+
if (path.length === 0 || !isRecord(rawPackage) || rawPackage.link === true) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const declaredName = typeof rawPackage.name === "string" ? rawPackage.name : packageNameFromNodeModulesPath(path);
|
|
254
|
+
const alias = npmAliasVersion(stringOrNull(rawPackage.version));
|
|
255
|
+
const name = alias?.name ?? declaredName;
|
|
256
|
+
const version = alias?.version ?? (typeof rawPackage.version === "string" ? rawPackage.version : null);
|
|
257
|
+
if (!name) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
observations.push(lockfileObservation({
|
|
261
|
+
ecosystem: "npm",
|
|
262
|
+
name,
|
|
263
|
+
version,
|
|
264
|
+
requested: path,
|
|
265
|
+
sourceKind: "lockfile",
|
|
266
|
+
resolvedUrl: stringOrNull(rawPackage.resolved),
|
|
267
|
+
integrity: stringOrNull(rawPackage.integrity),
|
|
268
|
+
license: stringOrNull(rawPackage.license)
|
|
269
|
+
}));
|
|
257
270
|
}
|
|
258
|
-
observations
|
|
259
|
-
ecosystem: "npm",
|
|
260
|
-
name,
|
|
261
|
-
version,
|
|
262
|
-
requested: path,
|
|
263
|
-
sourceKind: "lockfile",
|
|
264
|
-
resolvedUrl: stringOrNull(rawPackage.resolved),
|
|
265
|
-
integrity: stringOrNull(rawPackage.integrity),
|
|
266
|
-
license: stringOrNull(rawPackage.license)
|
|
267
|
-
}));
|
|
271
|
+
return observations;
|
|
268
272
|
}
|
|
269
|
-
|
|
273
|
+
if (isRecord(parsed.dependencies)) {
|
|
274
|
+
walkLegacyDependencies(parsed.dependencies, observations, new Set());
|
|
275
|
+
}
|
|
276
|
+
return observations;
|
|
277
|
+
}
|
|
278
|
+
function walkLegacyDependencies(dependencies, observations, seen) {
|
|
270
279
|
for (const [name, rawPackage] of Object.entries(dependencies)) {
|
|
271
|
-
if (!isRecord(rawPackage) ||
|
|
280
|
+
if (!isRecord(rawPackage) || rawPackage.bundled === true) {
|
|
272
281
|
continue;
|
|
273
282
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
const alias = npmAliasVersion(stringOrNull(rawPackage.version));
|
|
284
|
+
const resolvedName = alias?.name ?? name;
|
|
285
|
+
const version = alias?.version ?? stringOrNull(rawPackage.version);
|
|
286
|
+
const key = `${resolvedName}@${version ?? ""}`;
|
|
287
|
+
if (!seen.has(key)) {
|
|
288
|
+
seen.add(key);
|
|
289
|
+
observations.push(lockfileObservation({
|
|
290
|
+
ecosystem: "npm",
|
|
291
|
+
name: resolvedName,
|
|
292
|
+
version,
|
|
293
|
+
requested: name,
|
|
294
|
+
sourceKind: "lockfile",
|
|
295
|
+
resolvedUrl: stringOrNull(rawPackage.resolved),
|
|
296
|
+
integrity: stringOrNull(rawPackage.integrity),
|
|
297
|
+
license: stringOrNull(rawPackage.license)
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
if (isRecord(rawPackage.dependencies)) {
|
|
301
|
+
walkLegacyDependencies(rawPackage.dependencies, observations, seen);
|
|
302
|
+
}
|
|
284
303
|
}
|
|
285
|
-
|
|
304
|
+
}
|
|
305
|
+
function npmAliasVersion(version) {
|
|
306
|
+
if (!version || !version.startsWith("npm:")) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const spec = version.slice(4);
|
|
310
|
+
const at = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.indexOf("@");
|
|
311
|
+
if (at <= 0) {
|
|
312
|
+
return { name: spec, version: null };
|
|
313
|
+
}
|
|
314
|
+
return { name: spec.slice(0, at), version: spec.slice(at + 1) || null };
|
|
286
315
|
}
|
|
287
316
|
function parseYarnLock(text) {
|
|
288
317
|
const observations = [];
|
|
@@ -293,22 +322,23 @@ function parseYarnLock(text) {
|
|
|
293
322
|
if (!header) {
|
|
294
323
|
continue;
|
|
295
324
|
}
|
|
325
|
+
if (header.startsWith("#") || header === "__metadata") {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
296
328
|
const requested = header.split(",")[0]?.trim().replace(/^"|"$/gu, "") ?? header;
|
|
297
329
|
const name = packageNameFromYarnDescriptor(requested);
|
|
298
|
-
|
|
330
|
+
const version = quotedValue(lines, "version");
|
|
331
|
+
if (!name || !version) {
|
|
299
332
|
continue;
|
|
300
333
|
}
|
|
301
|
-
const version = quotedValue(lines, "version");
|
|
302
|
-
const resolvedUrl = quotedValue(lines, "resolved");
|
|
303
|
-
const integrity = quotedValue(lines, "integrity");
|
|
304
334
|
observations.push(lockfileObservation({
|
|
305
335
|
ecosystem: "npm",
|
|
306
336
|
name,
|
|
307
337
|
version,
|
|
308
338
|
requested,
|
|
309
339
|
sourceKind: "lockfile",
|
|
310
|
-
resolvedUrl,
|
|
311
|
-
integrity,
|
|
340
|
+
resolvedUrl: quotedValue(lines, "resolved"),
|
|
341
|
+
integrity: quotedValue(lines, "integrity") ?? quotedValue(lines, "checksum"),
|
|
312
342
|
license: null
|
|
313
343
|
}));
|
|
314
344
|
}
|
|
@@ -317,23 +347,33 @@ function parseYarnLock(text) {
|
|
|
317
347
|
function parsePnpmLock(text) {
|
|
318
348
|
const observations = [];
|
|
319
349
|
const lines = text.split(/\r?\n/u);
|
|
350
|
+
let inPackages = false;
|
|
320
351
|
let current = null;
|
|
352
|
+
const flush = () => {
|
|
353
|
+
if (current) {
|
|
354
|
+
observations.push(lockfileObservation(current));
|
|
355
|
+
current = null;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
321
358
|
for (const line of lines) {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
359
|
+
const sectionMatch = /^([A-Za-z][\w-]*):\s*$/u.exec(line);
|
|
360
|
+
if (sectionMatch) {
|
|
361
|
+
flush();
|
|
362
|
+
// The `snapshots:` section keys the resolved peer graph, e.g.
|
|
363
|
+
// `eslint-utils@4.9.1(eslint@9.39.4)` — the peer suffix is not a real
|
|
364
|
+
// registry version, so scanning it yields a false "removed from
|
|
365
|
+
// registry" verdict. `packages:` is the canonical inventory (carries
|
|
366
|
+
// integrity); take identity only from there.
|
|
367
|
+
inPackages = sectionMatch[1] === "packages";
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!inPackages) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const keyMatch = /^\s{2}(\S.*?):\s*$/u.exec(line);
|
|
374
|
+
if (keyMatch?.[1]) {
|
|
375
|
+
flush();
|
|
376
|
+
current = parsePnpmPackageKey(keyMatch[1]);
|
|
337
377
|
continue;
|
|
338
378
|
}
|
|
339
379
|
if (!current) {
|
|
@@ -354,24 +394,69 @@ function parsePnpmLock(text) {
|
|
|
354
394
|
};
|
|
355
395
|
}
|
|
356
396
|
}
|
|
357
|
-
|
|
358
|
-
observations.push(lockfileObservation(current));
|
|
359
|
-
}
|
|
397
|
+
flush();
|
|
360
398
|
return observations;
|
|
361
399
|
}
|
|
400
|
+
function parsePnpmPackageKey(rawKey) {
|
|
401
|
+
const key = stripQuotes(rawKey.trim());
|
|
402
|
+
if (/(?:file|link|workspace|git\+|git:|https?):/u.test(key)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const hadSlash = key.startsWith("/");
|
|
406
|
+
const body = hadSlash ? key.slice(1) : key;
|
|
407
|
+
let name = null;
|
|
408
|
+
let version = null;
|
|
409
|
+
if (hadSlash) {
|
|
410
|
+
// lockfileVersion 5.x keys are /<name>/<version>[_peer]; the version
|
|
411
|
+
// follows the final slash and the name may itself be scoped (two slashes).
|
|
412
|
+
const lastSlash = body.lastIndexOf("/");
|
|
413
|
+
if (lastSlash > 0) {
|
|
414
|
+
name = body.slice(0, lastSlash);
|
|
415
|
+
version = stripPnpmPeerSuffix(body.slice(lastSlash + 1));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const atForm = /^((?:@[^/\s]+\/)?[^@\s]+)@(.+)$/u.exec(body);
|
|
420
|
+
if (atForm?.[1] && atForm[2]) {
|
|
421
|
+
name = atForm[1];
|
|
422
|
+
version = stripPnpmPeerSuffix(atForm[2]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!name || !version) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
ecosystem: "npm",
|
|
430
|
+
name,
|
|
431
|
+
version,
|
|
432
|
+
requested: `${name}@${version}`,
|
|
433
|
+
sourceKind: "lockfile",
|
|
434
|
+
resolvedUrl: null,
|
|
435
|
+
integrity: null,
|
|
436
|
+
license: null
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function stripPnpmPeerSuffix(version) {
|
|
440
|
+
const peerStart = version.search(/[(_]/u);
|
|
441
|
+
return peerStart === -1 ? version : version.slice(0, peerStart);
|
|
442
|
+
}
|
|
362
443
|
function parseRequirements(text) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
|
|
444
|
+
const observations = [];
|
|
445
|
+
for (const logical of joinRequirementContinuations(text.split(/\r?\n/u))) {
|
|
446
|
+
const line = logical.trim();
|
|
447
|
+
if (line.length === 0 || line.startsWith("#")) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const editable = /^(?:-e|--editable)[=\s]+(.+)$/u.exec(line);
|
|
451
|
+
const target = (editable?.[1] ?? line).trim();
|
|
452
|
+
if (REMOTE_SPEC_PREFIXES.some((prefix) => target.toLowerCase().startsWith(prefix))) {
|
|
453
|
+
observations.push(packageObservation({
|
|
369
454
|
ecosystem: "pypi",
|
|
370
|
-
name:
|
|
455
|
+
name: target,
|
|
371
456
|
version: null,
|
|
372
457
|
requested: line,
|
|
373
458
|
sourceKind: "lockfile-url-fallback",
|
|
374
|
-
resolvedUrl:
|
|
459
|
+
resolvedUrl: target,
|
|
375
460
|
integrity: null,
|
|
376
461
|
license: null
|
|
377
462
|
}, "block", {
|
|
@@ -379,15 +464,20 @@ function parseRequirements(text) {
|
|
|
379
464
|
title: "Lockfile URL fallback identity",
|
|
380
465
|
message: "lockfile entry uses a URL without package identity or hash metadata",
|
|
381
466
|
location: line
|
|
382
|
-
});
|
|
467
|
+
}));
|
|
468
|
+
continue;
|
|
383
469
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
470
|
+
if (line.startsWith("-") || /^\.{0,2}\//u.test(target)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const hash = /--hash=([A-Za-z0-9:_-]+)/u.exec(line)?.[1] ?? null;
|
|
474
|
+
const requirement = line.replace(/\s*--hash=[^\s]+/gu, "").trim();
|
|
475
|
+
const match = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?(?:\s*==\s*([^;\s]+))?/u.exec(requirement);
|
|
387
476
|
if (!match?.[1]) {
|
|
388
|
-
|
|
477
|
+
observations.push(blockedUnknownSpec(line));
|
|
478
|
+
continue;
|
|
389
479
|
}
|
|
390
|
-
|
|
480
|
+
observations.push(lockfileObservation({
|
|
391
481
|
ecosystem: "pypi",
|
|
392
482
|
name: match[1],
|
|
393
483
|
version: match[2] ?? null,
|
|
@@ -396,8 +486,25 @@ function parseRequirements(text) {
|
|
|
396
486
|
resolvedUrl: null,
|
|
397
487
|
integrity: hash,
|
|
398
488
|
license: null
|
|
399
|
-
});
|
|
400
|
-
}
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
return observations;
|
|
492
|
+
}
|
|
493
|
+
function joinRequirementContinuations(lines) {
|
|
494
|
+
const joined = [];
|
|
495
|
+
let buffer = "";
|
|
496
|
+
for (const line of lines) {
|
|
497
|
+
if (line.endsWith("\\")) {
|
|
498
|
+
buffer += `${line.slice(0, -1)} `;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
joined.push(buffer + line);
|
|
502
|
+
buffer = "";
|
|
503
|
+
}
|
|
504
|
+
if (buffer.length > 0) {
|
|
505
|
+
joined.push(buffer);
|
|
506
|
+
}
|
|
507
|
+
return joined;
|
|
401
508
|
}
|
|
402
509
|
function parseCargoLock(text) {
|
|
403
510
|
return lockBlocks(text, "[[package]]").map((block) => {
|
|
@@ -426,7 +533,7 @@ function parsePoetryLock(text) {
|
|
|
426
533
|
requested: `${name}==${version ?? "unknown"}`,
|
|
427
534
|
sourceKind: "lockfile",
|
|
428
535
|
resolvedUrl: null,
|
|
429
|
-
integrity: null,
|
|
536
|
+
integrity: /hash\s*=\s*"([^"]+)"/u.exec(block)?.[1] ?? null,
|
|
430
537
|
license: tomlString(block, "license")
|
|
431
538
|
});
|
|
432
539
|
}).filter((observation) => observation.identity.name !== "unknown");
|
|
@@ -588,7 +695,8 @@ function isExactVersion(version) {
|
|
|
588
695
|
}
|
|
589
696
|
function isSupportedIntegrity(value) {
|
|
590
697
|
return /^(?:sha256|sha384|sha512)-[A-Za-z0-9+/=]+$/u.test(value)
|
|
591
|
-
|| /^(?:sha256:)?[a-f0-9]{64}$/iu.test(value)
|
|
698
|
+
|| /^(?:sha256:)?[a-f0-9]{64}$/iu.test(value)
|
|
699
|
+
|| /^[0-9a-f]+\/[0-9a-f]{64,}$/iu.test(value);
|
|
592
700
|
}
|
|
593
701
|
function isUnsafeResolvedUrl(value) {
|
|
594
702
|
const lower = value.toLowerCase();
|
|
@@ -657,13 +765,18 @@ function packageNameFromNodeModulesPath(path) {
|
|
|
657
765
|
return first;
|
|
658
766
|
}
|
|
659
767
|
function packageNameFromYarnDescriptor(descriptor) {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
768
|
+
const scoped = descriptor.startsWith("@");
|
|
769
|
+
const body = scoped ? descriptor.slice(1) : descriptor;
|
|
770
|
+
const at = body.indexOf("@");
|
|
771
|
+
const name = at === -1 ? descriptor : scoped ? `@${body.slice(0, at)}` : body.slice(0, at);
|
|
772
|
+
const spec = at === -1 ? "" : body.slice(at + 1);
|
|
773
|
+
// npm: alias descriptors (`alias@npm:real-pkg@range`) resolve to the alias
|
|
774
|
+
// target — that is the registry artifact actually fetched and scanned.
|
|
775
|
+
const alias = /^npm:((?:@[^/\s]+\/)?[^@\s]+)@/u.exec(spec);
|
|
776
|
+
return alias?.[1] ?? (name || null);
|
|
664
777
|
}
|
|
665
778
|
function quotedValue(lines, key) {
|
|
666
|
-
const pattern = new RegExp(`^\\s*${key}
|
|
779
|
+
const pattern = new RegExp(`^\\s*${key}:?\\s+"?([^"\\n]+?)"?\\s*$`, "u");
|
|
667
780
|
for (const line of lines) {
|
|
668
781
|
const match = pattern.exec(line);
|
|
669
782
|
if (match?.[1]) {
|