@westbayberry/dg 2.0.8 → 2.0.10
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 +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/launcher/install-preflight.js +81 -12
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
package/dist/verify/preflight.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { loadUserConfig } from "../config/settings.js";
|
|
4
4
|
import { evaluatePackagePolicy, resolveEffectivePolicy } from "../policy/evaluate.js";
|
|
5
5
|
const LOCKFILE_NAMES = new Set([
|
|
6
6
|
"Cargo.lock",
|
|
7
7
|
"Pipfile.lock",
|
|
8
|
+
"npm-shrinkwrap.json",
|
|
8
9
|
"package-lock.json",
|
|
9
10
|
"pnpm-lock.yaml",
|
|
10
11
|
"poetry.lock",
|
|
11
12
|
"requirements.txt",
|
|
13
|
+
"uv.lock",
|
|
12
14
|
"yarn.lock"
|
|
13
15
|
]);
|
|
14
16
|
const REMOTE_SPEC_PREFIXES = [
|
|
@@ -22,6 +24,10 @@ const REMOTE_SPEC_PREFIXES = [
|
|
|
22
24
|
export function isSupportedLockfilePath(target) {
|
|
23
25
|
return LOCKFILE_NAMES.has(basename(target));
|
|
24
26
|
}
|
|
27
|
+
export function isRemotePackageSpec(spec) {
|
|
28
|
+
const lowered = spec.trim().toLowerCase();
|
|
29
|
+
return REMOTE_SPEC_PREFIXES.some((prefix) => lowered.startsWith(prefix)) || lowered.startsWith("file:");
|
|
30
|
+
}
|
|
25
31
|
export function verifyPackageSpec(spec, options = {}) {
|
|
26
32
|
const parsed = parsePackageSpec(spec);
|
|
27
33
|
const observations = parsed
|
|
@@ -68,7 +74,7 @@ export function verifyLockfile(targetPath, options = {}) {
|
|
|
68
74
|
errors: [`could not read lockfile: ${error instanceof Error ? error.message : "unknown read error"}`]
|
|
69
75
|
});
|
|
70
76
|
}
|
|
71
|
-
const observations = parseLockfile(basename(absoluteTarget),
|
|
77
|
+
const observations = parseLockfile(text, createParseContext(basename(absoluteTarget), absoluteTarget));
|
|
72
78
|
return preflightReport({
|
|
73
79
|
target: displayTarget,
|
|
74
80
|
inputKind: "lockfile",
|
|
@@ -77,6 +83,50 @@ export function verifyLockfile(targetPath, options = {}) {
|
|
|
77
83
|
options
|
|
78
84
|
});
|
|
79
85
|
}
|
|
86
|
+
export function parseLockfilePackages(targetPath) {
|
|
87
|
+
const absoluteTarget = resolve(targetPath);
|
|
88
|
+
const fileName = basename(absoluteTarget);
|
|
89
|
+
let text;
|
|
90
|
+
try {
|
|
91
|
+
text = readFileSync(absoluteTarget, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
packages: [],
|
|
96
|
+
skipped: [],
|
|
97
|
+
parseError: {
|
|
98
|
+
file: fileName,
|
|
99
|
+
reason: error instanceof Error ? error.message : "could not read lockfile"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const context = createParseContext(fileName, absoluteTarget);
|
|
104
|
+
const observations = parseLockfile(text, context);
|
|
105
|
+
return {
|
|
106
|
+
packages: observations
|
|
107
|
+
.filter((observation) => observation.verdict !== "block" && observation.identity.sourceKind === "lockfile")
|
|
108
|
+
.map((observation) => observation.identity),
|
|
109
|
+
skipped: context.skipped,
|
|
110
|
+
parseError: context.errors[0] ?? null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function createParseContext(fileName, filePath) {
|
|
114
|
+
return {
|
|
115
|
+
fileName,
|
|
116
|
+
filePath,
|
|
117
|
+
skipped: [],
|
|
118
|
+
errors: []
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function recordSkip(context, name, reason, location) {
|
|
122
|
+
context.skipped.push({ name, reason, location });
|
|
123
|
+
}
|
|
124
|
+
function recordParseError(context, file, error) {
|
|
125
|
+
context.errors.push({
|
|
126
|
+
file,
|
|
127
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
80
130
|
function preflightReport(input) {
|
|
81
131
|
const policy = resolveEffectivePolicy({
|
|
82
132
|
userConfig: loadUserConfig()
|
|
@@ -128,99 +178,38 @@ function parsePackageSpec(spec) {
|
|
|
128
178
|
if (trimmed.length === 0 || /[\u0000-\u001f\u007f]/u.test(trimmed)) {
|
|
129
179
|
return null;
|
|
130
180
|
}
|
|
131
|
-
|
|
132
|
-
if (REMOTE_SPEC_PREFIXES.some((prefix) => lowered.startsWith(prefix)) || lowered.startsWith("file:")) {
|
|
133
|
-
return packageObservation({
|
|
134
|
-
ecosystem: "unknown",
|
|
135
|
-
name: trimmed,
|
|
136
|
-
version: null,
|
|
137
|
-
requested: spec,
|
|
138
|
-
sourceKind: "package-spec",
|
|
139
|
-
resolvedUrl: trimmed,
|
|
140
|
-
integrity: null,
|
|
141
|
-
license: null
|
|
142
|
-
}, "block", {
|
|
143
|
-
id: "unverified-network-spec",
|
|
144
|
-
title: "Unverified network package spec",
|
|
145
|
-
message: "direct URL, git, and file package specs require artifact verification before install",
|
|
146
|
-
location: spec
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
if (lowered.startsWith("npm:")) {
|
|
150
|
-
return parseNpmSpec(trimmed.slice(4), spec);
|
|
151
|
-
}
|
|
152
|
-
if (lowered.startsWith("pypi:")) {
|
|
153
|
-
return parsePypiSpec(trimmed.slice(5), spec);
|
|
154
|
-
}
|
|
155
|
-
if (lowered.startsWith("cargo:")) {
|
|
156
|
-
return parseCargoSpec(trimmed.slice(6), spec);
|
|
157
|
-
}
|
|
158
|
-
if (trimmed.includes("==")) {
|
|
159
|
-
return parsePypiSpec(trimmed, spec);
|
|
160
|
-
}
|
|
161
|
-
return parseNpmSpec(trimmed, spec);
|
|
162
|
-
}
|
|
163
|
-
function parseNpmSpec(value, requested) {
|
|
164
|
-
const parsed = splitNameVersion(value);
|
|
165
|
-
if (!parsed || !isValidPackageName(parsed.name)) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
return packageObservation({
|
|
169
|
-
ecosystem: "npm",
|
|
170
|
-
name: parsed.name,
|
|
171
|
-
version: parsed.version,
|
|
172
|
-
requested,
|
|
173
|
-
sourceKind: "package-spec",
|
|
174
|
-
resolvedUrl: null,
|
|
175
|
-
integrity: null,
|
|
176
|
-
license: null
|
|
177
|
-
}, exactVersionVerdict(parsed.version), exactVersionFinding(parsed.name, parsed.version, requested));
|
|
178
|
-
}
|
|
179
|
-
function parsePypiSpec(value, requested) {
|
|
180
|
-
const match = /^([A-Za-z0-9_.-]+)(?:==([^<>=!~\s]+))?$/.exec(value.trim());
|
|
181
|
-
if (!match?.[1]) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
const version = match[2] ?? null;
|
|
185
|
-
return packageObservation({
|
|
186
|
-
ecosystem: "pypi",
|
|
187
|
-
name: match[1],
|
|
188
|
-
version,
|
|
189
|
-
requested,
|
|
190
|
-
sourceKind: "package-spec",
|
|
191
|
-
resolvedUrl: null,
|
|
192
|
-
integrity: null,
|
|
193
|
-
license: null
|
|
194
|
-
}, exactVersionVerdict(version), exactVersionFinding(match[1], version, requested));
|
|
195
|
-
}
|
|
196
|
-
function parseCargoSpec(value, requested) {
|
|
197
|
-
const parsed = splitNameVersion(value);
|
|
198
|
-
if (!parsed || !/^[A-Za-z0-9_-]+$/.test(parsed.name)) {
|
|
181
|
+
if (!isRemotePackageSpec(trimmed)) {
|
|
199
182
|
return null;
|
|
200
183
|
}
|
|
201
184
|
return packageObservation({
|
|
202
|
-
ecosystem: "
|
|
203
|
-
name:
|
|
204
|
-
version:
|
|
205
|
-
requested,
|
|
185
|
+
ecosystem: "unknown",
|
|
186
|
+
name: trimmed,
|
|
187
|
+
version: null,
|
|
188
|
+
requested: spec,
|
|
206
189
|
sourceKind: "package-spec",
|
|
207
|
-
resolvedUrl:
|
|
190
|
+
resolvedUrl: trimmed,
|
|
208
191
|
integrity: null,
|
|
209
192
|
license: null
|
|
210
|
-
},
|
|
193
|
+
}, "block", {
|
|
194
|
+
id: "unverified-network-spec",
|
|
195
|
+
title: "Unverified network package spec",
|
|
196
|
+
message: "direct URL, git, and file package specs require artifact verification before install",
|
|
197
|
+
location: spec
|
|
198
|
+
});
|
|
211
199
|
}
|
|
212
|
-
function parseLockfile(
|
|
213
|
-
|
|
214
|
-
|
|
200
|
+
function parseLockfile(text, context) {
|
|
201
|
+
const name = context.fileName;
|
|
202
|
+
if (name === "package-lock.json" || name === "npm-shrinkwrap.json") {
|
|
203
|
+
return parsePackageLock(text, context);
|
|
215
204
|
}
|
|
216
205
|
if (name === "yarn.lock") {
|
|
217
|
-
return parseYarnLock(text);
|
|
206
|
+
return parseYarnLock(text, context);
|
|
218
207
|
}
|
|
219
208
|
if (name === "pnpm-lock.yaml") {
|
|
220
|
-
return parsePnpmLock(text);
|
|
209
|
+
return parsePnpmLock(text, context);
|
|
221
210
|
}
|
|
222
211
|
if (name === "requirements.txt") {
|
|
223
|
-
return parseRequirements(text);
|
|
212
|
+
return parseRequirements(text, context);
|
|
224
213
|
}
|
|
225
214
|
if (name === "Cargo.lock") {
|
|
226
215
|
return parseCargoLock(text);
|
|
@@ -228,42 +217,111 @@ function parseLockfile(name, text) {
|
|
|
228
217
|
if (name === "poetry.lock") {
|
|
229
218
|
return parsePoetryLock(text);
|
|
230
219
|
}
|
|
220
|
+
if (name === "uv.lock") {
|
|
221
|
+
return parseUvLock(text, context);
|
|
222
|
+
}
|
|
231
223
|
if (name === "Pipfile.lock") {
|
|
232
|
-
return parsePipfileLock(text);
|
|
224
|
+
return parsePipfileLock(text, context);
|
|
233
225
|
}
|
|
234
226
|
return [];
|
|
235
227
|
}
|
|
236
|
-
function
|
|
228
|
+
function specSourceKind(spec) {
|
|
229
|
+
const lower = spec.trim().toLowerCase();
|
|
230
|
+
if (lower.startsWith("workspace:")) {
|
|
231
|
+
return "workspace";
|
|
232
|
+
}
|
|
233
|
+
if (lower.startsWith("portal:") || lower.startsWith("link:") || lower.startsWith("file:")) {
|
|
234
|
+
return "local";
|
|
235
|
+
}
|
|
236
|
+
if (lower.startsWith("git+") || lower.startsWith("git://") || lower.startsWith("github:") || lower.startsWith("ssh://")) {
|
|
237
|
+
return "git";
|
|
238
|
+
}
|
|
239
|
+
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
|
240
|
+
return lower.includes(".git") ? "git" : "direct-url";
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const NPM_LOCK_DEPENDENCY_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
245
|
+
function npmLockSpecKinds(packages) {
|
|
246
|
+
const kinds = new Map();
|
|
247
|
+
for (const rawPackage of Object.values(packages)) {
|
|
248
|
+
if (!isRecord(rawPackage)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
for (const section of NPM_LOCK_DEPENDENCY_SECTIONS) {
|
|
252
|
+
const dependencies = isRecord(rawPackage[section]) ? rawPackage[section] : {};
|
|
253
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
254
|
+
if (typeof spec !== "string") {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const kind = specSourceKind(spec);
|
|
258
|
+
if (kind) {
|
|
259
|
+
kinds.set(name, kind);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return kinds;
|
|
265
|
+
}
|
|
266
|
+
function parsePackageLock(text, context) {
|
|
237
267
|
let parsed;
|
|
238
268
|
try {
|
|
239
269
|
parsed = JSON.parse(text);
|
|
240
270
|
}
|
|
241
271
|
catch (error) {
|
|
242
|
-
|
|
272
|
+
recordParseError(context, context.fileName, error);
|
|
273
|
+
return [malformedLockfileObservation(context.fileName, error)];
|
|
243
274
|
}
|
|
244
275
|
if (!isRecord(parsed)) {
|
|
245
|
-
|
|
276
|
+
const error = new Error("root must be an object");
|
|
277
|
+
recordParseError(context, context.fileName, error);
|
|
278
|
+
return [malformedLockfileObservation(context.fileName, error)];
|
|
246
279
|
}
|
|
247
280
|
const observations = [];
|
|
248
281
|
if (isRecord(parsed.packages)) {
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
const packagesMap = parsed.packages;
|
|
283
|
+
const specKinds = npmLockSpecKinds(packagesMap);
|
|
284
|
+
for (const [path, rawPackage] of Object.entries(packagesMap)) {
|
|
285
|
+
if (path.length === 0 || !isRecord(rawPackage)) {
|
|
251
286
|
continue;
|
|
252
287
|
}
|
|
253
288
|
const declaredName = typeof rawPackage.name === "string" ? rawPackage.name : packageNameFromNodeModulesPath(path);
|
|
289
|
+
const resolved = stringOrNull(rawPackage.resolved);
|
|
290
|
+
if (rawPackage.link === true) {
|
|
291
|
+
if (!(resolved && isRecord(packagesMap[resolved]))) {
|
|
292
|
+
recordSkip(context, declaredName ?? packageNameFromWorkspacePath(path), "local", path);
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!path.includes("node_modules")) {
|
|
297
|
+
recordSkip(context, declaredName ?? packageNameFromWorkspacePath(path), "workspace", path);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
254
300
|
const alias = npmAliasVersion(stringOrNull(rawPackage.version));
|
|
255
301
|
const name = alias?.name ?? declaredName;
|
|
256
302
|
const version = alias?.version ?? (typeof rawPackage.version === "string" ? rawPackage.version : null);
|
|
257
303
|
if (!name) {
|
|
258
304
|
continue;
|
|
259
305
|
}
|
|
306
|
+
const resolvedIsGit = resolved !== null && isUnsafeResolvedUrl(resolved);
|
|
307
|
+
const skipReason = resolvedIsGit
|
|
308
|
+
? "git"
|
|
309
|
+
: resolved?.toLowerCase().startsWith("file:")
|
|
310
|
+
? "local"
|
|
311
|
+
: specKinds.get(name) ?? null;
|
|
312
|
+
if (skipReason) {
|
|
313
|
+
recordSkip(context, name, skipReason, path);
|
|
314
|
+
if (!resolvedIsGit) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
260
318
|
observations.push(lockfileObservation({
|
|
261
319
|
ecosystem: "npm",
|
|
262
320
|
name,
|
|
263
321
|
version,
|
|
264
322
|
requested: path,
|
|
265
323
|
sourceKind: "lockfile",
|
|
266
|
-
resolvedUrl:
|
|
324
|
+
resolvedUrl: resolved,
|
|
267
325
|
integrity: stringOrNull(rawPackage.integrity),
|
|
268
326
|
license: stringOrNull(rawPackage.license)
|
|
269
327
|
}));
|
|
@@ -271,20 +329,28 @@ function parsePackageLock(text) {
|
|
|
271
329
|
return observations;
|
|
272
330
|
}
|
|
273
331
|
if (isRecord(parsed.dependencies)) {
|
|
274
|
-
walkLegacyDependencies(parsed.dependencies, observations, new Set());
|
|
332
|
+
walkLegacyDependencies(parsed.dependencies, observations, new Set(), context);
|
|
275
333
|
}
|
|
276
334
|
return observations;
|
|
277
335
|
}
|
|
278
|
-
function walkLegacyDependencies(dependencies, observations, seen) {
|
|
336
|
+
function walkLegacyDependencies(dependencies, observations, seen, context) {
|
|
279
337
|
for (const [name, rawPackage] of Object.entries(dependencies)) {
|
|
280
338
|
if (!isRecord(rawPackage) || rawPackage.bundled === true) {
|
|
281
339
|
continue;
|
|
282
340
|
}
|
|
283
|
-
const
|
|
341
|
+
const rawVersion = stringOrNull(rawPackage.version);
|
|
342
|
+
const resolved = stringOrNull(rawPackage.resolved);
|
|
343
|
+
const resolvedIsGit = resolved !== null && isUnsafeResolvedUrl(resolved);
|
|
344
|
+
const versionKind = rawVersion && !rawVersion.startsWith("npm:") ? specSourceKind(rawVersion) : null;
|
|
345
|
+
const skipReason = resolvedIsGit ? "git" : versionKind;
|
|
346
|
+
const alias = npmAliasVersion(rawVersion);
|
|
284
347
|
const resolvedName = alias?.name ?? name;
|
|
285
|
-
const version = alias?.version ??
|
|
348
|
+
const version = alias?.version ?? rawVersion;
|
|
286
349
|
const key = `${resolvedName}@${version ?? ""}`;
|
|
287
|
-
if (
|
|
350
|
+
if (skipReason) {
|
|
351
|
+
recordSkip(context, resolvedName, skipReason, name);
|
|
352
|
+
}
|
|
353
|
+
if (!seen.has(key) && (!skipReason || resolvedIsGit)) {
|
|
288
354
|
seen.add(key);
|
|
289
355
|
observations.push(lockfileObservation({
|
|
290
356
|
ecosystem: "npm",
|
|
@@ -292,13 +358,13 @@ function walkLegacyDependencies(dependencies, observations, seen) {
|
|
|
292
358
|
version,
|
|
293
359
|
requested: name,
|
|
294
360
|
sourceKind: "lockfile",
|
|
295
|
-
resolvedUrl:
|
|
361
|
+
resolvedUrl: resolved,
|
|
296
362
|
integrity: stringOrNull(rawPackage.integrity),
|
|
297
363
|
license: stringOrNull(rawPackage.license)
|
|
298
364
|
}));
|
|
299
365
|
}
|
|
300
366
|
if (isRecord(rawPackage.dependencies)) {
|
|
301
|
-
walkLegacyDependencies(rawPackage.dependencies, observations, seen);
|
|
367
|
+
walkLegacyDependencies(rawPackage.dependencies, observations, seen, context);
|
|
302
368
|
}
|
|
303
369
|
}
|
|
304
370
|
}
|
|
@@ -313,7 +379,7 @@ function npmAliasVersion(version) {
|
|
|
313
379
|
}
|
|
314
380
|
return { name: spec.slice(0, at), version: spec.slice(at + 1) || null };
|
|
315
381
|
}
|
|
316
|
-
function parseYarnLock(text) {
|
|
382
|
+
function parseYarnLock(text, context) {
|
|
317
383
|
const observations = [];
|
|
318
384
|
const blocks = text.split(/\n(?=(?:"?@?[^"\s].*"?):\n)/u);
|
|
319
385
|
for (const block of blocks) {
|
|
@@ -327,6 +393,14 @@ function parseYarnLock(text) {
|
|
|
327
393
|
}
|
|
328
394
|
const requested = header.split(",")[0]?.trim().replace(/^"|"$/gu, "") ?? header;
|
|
329
395
|
const name = packageNameFromYarnDescriptor(requested);
|
|
396
|
+
const resolved = quotedValue(lines, "resolved");
|
|
397
|
+
const skipReason = specSourceKind(yarnDescriptorSpec(requested));
|
|
398
|
+
if (skipReason) {
|
|
399
|
+
recordSkip(context, name ?? requested, skipReason, requested);
|
|
400
|
+
if (!(resolved && isUnsafeResolvedUrl(resolved))) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
330
404
|
const version = quotedValue(lines, "version");
|
|
331
405
|
if (!name || !version) {
|
|
332
406
|
continue;
|
|
@@ -337,14 +411,14 @@ function parseYarnLock(text) {
|
|
|
337
411
|
version,
|
|
338
412
|
requested,
|
|
339
413
|
sourceKind: "lockfile",
|
|
340
|
-
resolvedUrl:
|
|
414
|
+
resolvedUrl: resolved,
|
|
341
415
|
integrity: quotedValue(lines, "integrity") ?? quotedValue(lines, "checksum"),
|
|
342
416
|
license: null
|
|
343
417
|
}));
|
|
344
418
|
}
|
|
345
419
|
return observations;
|
|
346
420
|
}
|
|
347
|
-
function parsePnpmLock(text) {
|
|
421
|
+
function parsePnpmLock(text, context) {
|
|
348
422
|
const observations = [];
|
|
349
423
|
const lines = text.split(/\r?\n/u);
|
|
350
424
|
let inPackages = false;
|
|
@@ -373,7 +447,15 @@ function parsePnpmLock(text) {
|
|
|
373
447
|
const keyMatch = /^\s{2}(\S.*?):\s*$/u.exec(line);
|
|
374
448
|
if (keyMatch?.[1]) {
|
|
375
449
|
flush();
|
|
376
|
-
|
|
450
|
+
const key = stripQuotes(keyMatch[1].trim());
|
|
451
|
+
const skipReason = pnpmKeySkipReason(key);
|
|
452
|
+
if (skipReason) {
|
|
453
|
+
recordSkip(context, pnpmKeyName(key), skipReason, key);
|
|
454
|
+
current = null;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
current = parsePnpmPackageKey(key);
|
|
458
|
+
}
|
|
377
459
|
continue;
|
|
378
460
|
}
|
|
379
461
|
if (!current) {
|
|
@@ -397,29 +479,45 @@ function parsePnpmLock(text) {
|
|
|
397
479
|
flush();
|
|
398
480
|
return observations;
|
|
399
481
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (
|
|
482
|
+
const PNPM_NON_REGISTRY_MARKER = /(?:file|link|workspace|git\+|git:|ssh|https?):/u;
|
|
483
|
+
function pnpmKeySkipReason(key) {
|
|
484
|
+
if (!PNPM_NON_REGISTRY_MARKER.test(key)) {
|
|
403
485
|
return null;
|
|
404
486
|
}
|
|
487
|
+
if (key.includes("workspace:")) {
|
|
488
|
+
return "workspace";
|
|
489
|
+
}
|
|
490
|
+
if (/git\+|git:/u.test(key)) {
|
|
491
|
+
return "git";
|
|
492
|
+
}
|
|
493
|
+
if (/file:|link:/u.test(key)) {
|
|
494
|
+
return "local";
|
|
495
|
+
}
|
|
496
|
+
return "direct-url";
|
|
497
|
+
}
|
|
498
|
+
function pnpmKeyName(key) {
|
|
499
|
+
const named = /^\/?((?:@[^/\s]+\/)?[^@\s/]+)@/u.exec(key);
|
|
500
|
+
return named?.[1] ?? key;
|
|
501
|
+
}
|
|
502
|
+
function parsePnpmPackageKey(key) {
|
|
405
503
|
const hadSlash = key.startsWith("/");
|
|
406
504
|
const body = hadSlash ? key.slice(1) : key;
|
|
505
|
+
const parenStart = body.indexOf("(");
|
|
506
|
+
const core = parenStart === -1 ? body : body.slice(0, parenStart);
|
|
407
507
|
let name = null;
|
|
408
508
|
let version = null;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
509
|
+
// lockfileVersion 5.x keys are /<name>/<version>[_peer]; 6.0 keys are
|
|
510
|
+
// /<name>@<version>[(peer)]; 9.0 keys drop the leading slash.
|
|
511
|
+
const atForm = /^((?:@[^/\s]+\/)?[^/@\s]+)@([^/\s]+)$/u.exec(core);
|
|
512
|
+
if (atForm?.[1] && atForm[2]) {
|
|
513
|
+
name = atForm[1];
|
|
514
|
+
version = stripPnpmPeerSuffix(atForm[2]);
|
|
515
|
+
}
|
|
516
|
+
else if (hadSlash) {
|
|
517
|
+
const lastSlash = core.lastIndexOf("/");
|
|
413
518
|
if (lastSlash > 0) {
|
|
414
|
-
name =
|
|
415
|
-
version = stripPnpmPeerSuffix(
|
|
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]);
|
|
519
|
+
name = core.slice(0, lastSlash);
|
|
520
|
+
version = stripPnpmPeerSuffix(core.slice(lastSlash + 1));
|
|
423
521
|
}
|
|
424
522
|
}
|
|
425
523
|
if (!name || !version) {
|
|
@@ -440,16 +538,32 @@ function stripPnpmPeerSuffix(version) {
|
|
|
440
538
|
const peerStart = version.search(/[(_]/u);
|
|
441
539
|
return peerStart === -1 ? version : version.slice(0, peerStart);
|
|
442
540
|
}
|
|
443
|
-
function parseRequirements(text) {
|
|
541
|
+
function parseRequirements(text, context) {
|
|
444
542
|
const observations = [];
|
|
543
|
+
const baseDir = context.filePath ? dirname(context.filePath) : null;
|
|
544
|
+
const state = {
|
|
545
|
+
rootDir: baseDir,
|
|
546
|
+
visited: new Set(context.filePath ? [resolve(context.filePath)] : []),
|
|
547
|
+
seen: new Set()
|
|
548
|
+
};
|
|
549
|
+
collectRequirements(text, baseDir, state, context, observations);
|
|
550
|
+
return observations;
|
|
551
|
+
}
|
|
552
|
+
function collectRequirements(text, baseDir, state, context, observations) {
|
|
445
553
|
for (const logical of joinRequirementContinuations(text.split(/\r?\n/u))) {
|
|
446
554
|
const line = logical.trim();
|
|
447
555
|
if (line.length === 0 || line.startsWith("#")) {
|
|
448
556
|
continue;
|
|
449
557
|
}
|
|
558
|
+
const include = /^(?:-r|--requirement|-c|--constraint)[=\s]+(\S+)/u.exec(line);
|
|
559
|
+
if (include?.[1]) {
|
|
560
|
+
followRequirementInclude(include[1], baseDir, state, context, observations);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
450
563
|
const editable = /^(?:-e|--editable)[=\s]+(.+)$/u.exec(line);
|
|
451
564
|
const target = (editable?.[1] ?? line).trim();
|
|
452
565
|
if (REMOTE_SPEC_PREFIXES.some((prefix) => target.toLowerCase().startsWith(prefix))) {
|
|
566
|
+
recordSkip(context, target, specSourceKind(target) ?? "direct-url", line);
|
|
453
567
|
observations.push(packageObservation({
|
|
454
568
|
ecosystem: "pypi",
|
|
455
569
|
name: target,
|
|
@@ -467,16 +581,25 @@ function parseRequirements(text) {
|
|
|
467
581
|
}));
|
|
468
582
|
continue;
|
|
469
583
|
}
|
|
470
|
-
if (
|
|
584
|
+
if (editable !== null || /^\.{0,2}\//u.test(target)) {
|
|
585
|
+
recordSkip(context, target, "local", line);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (line.startsWith("-")) {
|
|
471
589
|
continue;
|
|
472
590
|
}
|
|
473
591
|
const hash = /--hash=([A-Za-z0-9:_-]+)/u.exec(line)?.[1] ?? null;
|
|
474
592
|
const requirement = line.replace(/\s*--hash=[^\s]+/gu, "").trim();
|
|
475
|
-
const match = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?(?:\s
|
|
593
|
+
const match = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?(?:\s*={2,3}\s*([^;\s]+))?/u.exec(requirement);
|
|
476
594
|
if (!match?.[1]) {
|
|
477
595
|
observations.push(blockedUnknownSpec(line));
|
|
478
596
|
continue;
|
|
479
597
|
}
|
|
598
|
+
const pinKey = `${match[1].toLowerCase()}@${match[2] ?? ""}`;
|
|
599
|
+
if (state.seen.has(pinKey)) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
state.seen.add(pinKey);
|
|
480
603
|
observations.push(lockfileObservation({
|
|
481
604
|
ecosystem: "pypi",
|
|
482
605
|
name: match[1],
|
|
@@ -488,7 +611,30 @@ function parseRequirements(text) {
|
|
|
488
611
|
license: null
|
|
489
612
|
}));
|
|
490
613
|
}
|
|
491
|
-
|
|
614
|
+
}
|
|
615
|
+
function followRequirementInclude(target, baseDir, state, context, observations) {
|
|
616
|
+
if (!baseDir || !state.rootDir) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const includePath = resolve(baseDir, target);
|
|
620
|
+
const containment = relative(state.rootDir, includePath);
|
|
621
|
+
if (containment.startsWith("..") || isAbsolute(containment)) {
|
|
622
|
+
recordParseError(context, target, new Error("requirements include escapes the project directory"));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (state.visited.has(includePath)) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
state.visited.add(includePath);
|
|
629
|
+
let text;
|
|
630
|
+
try {
|
|
631
|
+
text = readFileSync(includePath, "utf8");
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
recordParseError(context, target, error);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
collectRequirements(text, dirname(includePath), state, context, observations);
|
|
492
638
|
}
|
|
493
639
|
function joinRequirementContinuations(lines) {
|
|
494
640
|
const joined = [];
|
|
@@ -538,15 +684,62 @@ function parsePoetryLock(text) {
|
|
|
538
684
|
});
|
|
539
685
|
}).filter((observation) => observation.identity.name !== "unknown");
|
|
540
686
|
}
|
|
541
|
-
function
|
|
687
|
+
function parseUvLock(text, context) {
|
|
688
|
+
const observations = [];
|
|
689
|
+
for (const block of lockBlocks(text, "[[package]]")) {
|
|
690
|
+
const name = tomlString(block, "name");
|
|
691
|
+
if (!name) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const version = tomlString(block, "version");
|
|
695
|
+
const source = /^source\s*=\s*\{([^}]*)\}/mu.exec(block)?.[1] ?? "";
|
|
696
|
+
const skipReason = uvSourceSkipReason(source);
|
|
697
|
+
if (skipReason) {
|
|
698
|
+
recordSkip(context, name, skipReason, `${name}${version ? `==${version}` : ""}`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
observations.push(lockfileObservation({
|
|
702
|
+
ecosystem: "pypi",
|
|
703
|
+
name,
|
|
704
|
+
version,
|
|
705
|
+
requested: `${name}==${version ?? "unknown"}`,
|
|
706
|
+
sourceKind: "lockfile",
|
|
707
|
+
resolvedUrl: null,
|
|
708
|
+
integrity: /hash\s*=\s*"([^"]+)"/u.exec(block)?.[1] ?? null,
|
|
709
|
+
license: null
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
return observations;
|
|
713
|
+
}
|
|
714
|
+
function uvSourceSkipReason(source) {
|
|
715
|
+
if (source.length === 0 || /\bregistry\s*=/u.test(source)) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (/\bgit\s*=/u.test(source)) {
|
|
719
|
+
return "git";
|
|
720
|
+
}
|
|
721
|
+
if (/\burl\s*=/u.test(source)) {
|
|
722
|
+
return "direct-url";
|
|
723
|
+
}
|
|
724
|
+
if (/\b(?:editable|virtual)\s*=/u.test(source)) {
|
|
725
|
+
return "workspace";
|
|
726
|
+
}
|
|
727
|
+
if (/\b(?:path|directory)\s*=/u.test(source)) {
|
|
728
|
+
return "local";
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
function parsePipfileLock(text, context) {
|
|
542
733
|
let parsed;
|
|
543
734
|
try {
|
|
544
735
|
parsed = JSON.parse(text);
|
|
545
736
|
}
|
|
546
737
|
catch (error) {
|
|
547
|
-
|
|
738
|
+
recordParseError(context, context.fileName, error);
|
|
739
|
+
return [malformedLockfileObservation(context.fileName, error)];
|
|
548
740
|
}
|
|
549
741
|
if (!isRecord(parsed)) {
|
|
742
|
+
recordParseError(context, context.fileName, new Error("root must be an object"));
|
|
550
743
|
return [];
|
|
551
744
|
}
|
|
552
745
|
return ["default", "develop"].flatMap((section) => {
|
|
@@ -594,20 +787,6 @@ function integrityPolicyFinding(identity) {
|
|
|
594
787
|
location: identity.requested
|
|
595
788
|
};
|
|
596
789
|
}
|
|
597
|
-
function exactVersionFinding(name, version, location) {
|
|
598
|
-
if (version && isExactVersion(version)) {
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
id: "unpinned-package-spec",
|
|
603
|
-
title: "Unpinned package spec",
|
|
604
|
-
message: `${name} is not pinned to an exact package version`,
|
|
605
|
-
location
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
function exactVersionVerdict(version) {
|
|
609
|
-
return version && isExactVersion(version) ? "pass" : "warn";
|
|
610
|
-
}
|
|
611
790
|
function deniedLicenseFinding(identity, deniedLicenses) {
|
|
612
791
|
if (!identity.license || !deniedLicenses.has(normalizeLicense(identity.license))) {
|
|
613
792
|
return null;
|
|
@@ -660,41 +839,8 @@ function packageObservation(identity, verdict, finding) {
|
|
|
660
839
|
finding
|
|
661
840
|
};
|
|
662
841
|
}
|
|
663
|
-
function splitNameVersion(value) {
|
|
664
|
-
const trimmed = value.trim();
|
|
665
|
-
if (trimmed.startsWith("@")) {
|
|
666
|
-
const index = trimmed.lastIndexOf("@");
|
|
667
|
-
if (index <= 0) {
|
|
668
|
-
return {
|
|
669
|
-
name: trimmed,
|
|
670
|
-
version: null
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
return {
|
|
674
|
-
name: trimmed.slice(0, index),
|
|
675
|
-
version: trimmed.slice(index + 1) || null
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
const index = trimmed.lastIndexOf("@");
|
|
679
|
-
if (index > 0) {
|
|
680
|
-
return {
|
|
681
|
-
name: trimmed.slice(0, index),
|
|
682
|
-
version: trimmed.slice(index + 1) || null
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
return {
|
|
686
|
-
name: trimmed,
|
|
687
|
-
version: null
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
function isValidPackageName(name) {
|
|
691
|
-
return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/iu.test(name) && !/[\\\s]/u.test(name);
|
|
692
|
-
}
|
|
693
|
-
function isExactVersion(version) {
|
|
694
|
-
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version);
|
|
695
|
-
}
|
|
696
842
|
function isSupportedIntegrity(value) {
|
|
697
|
-
return /^(?:sha256|sha384|sha512)-[A-Za-z0-9+/=]+$/u.test(value)
|
|
843
|
+
return /^(?:sha1|sha256|sha384|sha512)-[A-Za-z0-9+/=]+$/u.test(value)
|
|
698
844
|
|| /^(?:sha256:)?[a-f0-9]{64}$/iu.test(value)
|
|
699
845
|
|| /^[0-9a-f]+\/[0-9a-f]{64,}$/iu.test(value);
|
|
700
846
|
}
|
|
@@ -749,6 +895,12 @@ function packageDisplayName(identity) {
|
|
|
749
895
|
const version = identity.version ? `@${identity.version}` : "";
|
|
750
896
|
return `${identity.ecosystem}:${identity.name}${version}`;
|
|
751
897
|
}
|
|
898
|
+
function packageNameFromWorkspacePath(path) {
|
|
899
|
+
const parts = path.split("/").filter((part) => part.length > 0);
|
|
900
|
+
const last = parts[parts.length - 1] ?? path;
|
|
901
|
+
const prior = parts[parts.length - 2];
|
|
902
|
+
return prior?.startsWith("@") ? `${prior}/${last}` : last;
|
|
903
|
+
}
|
|
752
904
|
function packageNameFromNodeModulesPath(path) {
|
|
753
905
|
const parts = path.split("/");
|
|
754
906
|
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
@@ -764,6 +916,12 @@ function packageNameFromNodeModulesPath(path) {
|
|
|
764
916
|
}
|
|
765
917
|
return first;
|
|
766
918
|
}
|
|
919
|
+
function yarnDescriptorSpec(descriptor) {
|
|
920
|
+
const scoped = descriptor.startsWith("@");
|
|
921
|
+
const body = scoped ? descriptor.slice(1) : descriptor;
|
|
922
|
+
const at = body.indexOf("@");
|
|
923
|
+
return at === -1 ? "" : body.slice(at + 1);
|
|
924
|
+
}
|
|
767
925
|
function packageNameFromYarnDescriptor(descriptor) {
|
|
768
926
|
const scoped = descriptor.startsWith("@");
|
|
769
927
|
const body = scoped ? descriptor.slice(1) : descriptor;
|