@westbayberry/dg 2.0.3 → 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 +7 -3
- package/dist/scan-ui/LegacyApp.js +2 -2
- package/dist/scan-ui/components/InteractiveResultsView.js +1 -1
- package/dist/scan-ui/format-helpers.js +3 -2
- package/dist/scan-ui/hooks/useScan.js +4 -4
- package/dist/setup/plan.js +24 -5
- package/dist/verify/preflight.js +192 -79
- package/package.json +1 -1
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/setup/plan.js
CHANGED
|
@@ -14,6 +14,8 @@ import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
|
|
|
14
14
|
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
|
|
15
15
|
export const SHIM_SENTINEL = "dg-shim-v1";
|
|
16
16
|
export const RC_SENTINEL = "dg-shell-rc-v1";
|
|
17
|
+
export const RC_FUNCTIONS_SENTINEL = "dg-shim-functions-v1";
|
|
18
|
+
const RC_SHIM_HELPER = "__dg_shim";
|
|
17
19
|
export const GUARD_HOOK_SENTINEL = "dg-git-hook-v1";
|
|
18
20
|
export const RC_BEGIN = "# >>> dg setup >>>";
|
|
19
21
|
export const RC_END = "# <<< dg setup <<<";
|
|
@@ -273,6 +275,7 @@ export function doctorReport(options = {}) {
|
|
|
273
275
|
});
|
|
274
276
|
const rcEntries = registryRead.registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "rc");
|
|
275
277
|
const missingRc = rcEntries.filter((entry) => !readText(entry.path).includes(RC_SENTINEL));
|
|
278
|
+
const functionsPresent = rcEntries.some((entry) => readText(entry.path).includes(RC_FUNCTIONS_SENTINEL));
|
|
276
279
|
checks.push({
|
|
277
280
|
name: "shell-rc",
|
|
278
281
|
status: rcEntries.length > 0 && missingRc.length === 0 ? "pass" : "warn",
|
|
@@ -286,7 +289,7 @@ export function doctorReport(options = {}) {
|
|
|
286
289
|
? "No legacy dg pip hooks in user site-packages"
|
|
287
290
|
: `Legacy dg pip hooks break pip in: ${staleHookSites.join(", ")}`
|
|
288
291
|
});
|
|
289
|
-
checks.push(pathPrecedenceCheck(env, shimDir));
|
|
292
|
+
checks.push(pathPrecedenceCheck(env, shimDir, functionsPresent));
|
|
290
293
|
checks.push({
|
|
291
294
|
name: "stale-sessions",
|
|
292
295
|
status: staleSessions.length === 0 ? "pass" : "warn",
|
|
@@ -510,11 +513,20 @@ function withRcBlock(existing, plan) {
|
|
|
510
513
|
const prefix = withoutExisting.length > 0 && !withoutExisting.endsWith("\n") ? `${withoutExisting}\n` : withoutExisting;
|
|
511
514
|
return `${prefix}${block}`;
|
|
512
515
|
}
|
|
516
|
+
// The PATH export covers child processes that inherit it; the shell functions
|
|
517
|
+
// win even when a virtualenv prepends its own bin ahead of the shim dir, since
|
|
518
|
+
// a function is resolved before PATH. Each delegates to the fail-open shim and
|
|
519
|
+
// falls back to the real command if the shim is gone.
|
|
513
520
|
function posixRcBlock(shimDir) {
|
|
514
|
-
|
|
521
|
+
const dir = escapeDoubleQuotedSh(shimDir);
|
|
522
|
+
const helper = `${RC_SHIM_HELPER}() { local __dg_c="$1"; shift; if [ -x "${dir}/$__dg_c" ]; then "${dir}/$__dg_c" "$@"; else command "$__dg_c" "$@"; fi; }`;
|
|
523
|
+
const fns = SHIM_COMMANDS.map((command) => `${command}() { ${RC_SHIM_HELPER} ${command} "$@"; }`).join("\n");
|
|
524
|
+
return `${RC_BEGIN}\n# ${RC_SENTINEL}\n# ${RC_FUNCTIONS_SENTINEL}\nexport PATH="${dir}:$PATH"\n${helper}\n${fns}\n${RC_END}\n`;
|
|
515
525
|
}
|
|
516
526
|
function fishRcBlock(shimDir) {
|
|
517
|
-
|
|
527
|
+
const dir = escapeDoubleQuotedFish(shimDir);
|
|
528
|
+
const fns = SHIM_COMMANDS.map((command) => `function ${command}; if test -x "${dir}/${command}"; "${dir}/${command}" $argv; else; command ${command} $argv; end; end`).join("\n");
|
|
529
|
+
return `${RC_BEGIN}\n# ${RC_SENTINEL}\n# ${RC_FUNCTIONS_SENTINEL}\nfish_add_path -p "${dir}"\n${fns}\n${RC_END}\n`;
|
|
518
530
|
}
|
|
519
531
|
function stripRcBlock(existing) {
|
|
520
532
|
const pattern = new RegExp(`${escapeRegex(RC_BEGIN)}\\n[\\s\\S]*?${escapeRegex(RC_END)}\\n?`, "g");
|
|
@@ -778,7 +790,7 @@ function serviceCheck(env) {
|
|
|
778
790
|
message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
|
|
779
791
|
};
|
|
780
792
|
}
|
|
781
|
-
function pathPrecedenceCheck(env, shimDir) {
|
|
793
|
+
function pathPrecedenceCheck(env, shimDir, functionsPresent) {
|
|
782
794
|
const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
783
795
|
const shimIndex = pathEntries.indexOf(shimDir);
|
|
784
796
|
const activateFix = `activate this shell: ${currentShellActivation(env)} — or open a new terminal`;
|
|
@@ -812,11 +824,18 @@ function pathPrecedenceCheck(env, shimDir) {
|
|
|
812
824
|
};
|
|
813
825
|
}
|
|
814
826
|
if (offender) {
|
|
827
|
+
if (functionsPresent) {
|
|
828
|
+
return {
|
|
829
|
+
name: "path",
|
|
830
|
+
status: "pass",
|
|
831
|
+
message: `${offender.dir} resolves ${offender.command} first (e.g. an active virtualenv); dg shell functions intercept bare installs regardless`
|
|
832
|
+
};
|
|
833
|
+
}
|
|
815
834
|
return {
|
|
816
835
|
name: "path",
|
|
817
836
|
status: "warn",
|
|
818
837
|
message: `${shimDir} is on PATH but ${offender.dir} resolves ${offender.command} first`,
|
|
819
|
-
fix: activateFix
|
|
838
|
+
fix: `re-run dg setup to intercept inside virtualenvs — or ${activateFix}`
|
|
820
839
|
};
|
|
821
840
|
}
|
|
822
841
|
return {
|
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]) {
|