@westbayberry/dg 2.0.11 → 2.2.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 +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +6 -4
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +26 -10
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +169 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +8 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +3 -2
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/status.js +5 -2
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +154 -65
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +92 -0
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/install-ui/prompt.js +14 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +167 -17
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +43 -3
- package/dist/launcher/run.js +100 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +117 -0
- package/dist/policy/evaluate.js +5 -21
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/provenance.js +23 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +446 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +57 -17
- package/dist/proxy/metadata-map.js +66 -4
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +473 -45
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +51 -17
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +63 -8
- package/dist/scan/scanner-report.js +42 -9
- package/dist/scan/staged.js +71 -11
- package/dist/scan-ui/LegacyApp.js +12 -16
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +46 -4
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +14 -4
- package/dist/scripts/detect.js +158 -0
- package/dist/scripts/gate.js +170 -0
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +98 -29
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/json-file.js +24 -0
- package/dist/util/report-writer.js +57 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +86 -18
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +14 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -116
- package/dist/commands/explain.js +0 -232
- package/dist/commands/unavailable.js +0 -11
- package/dist/telemetry/events.js +0 -40
package/dist/verify/preflight.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, 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([
|
|
@@ -21,6 +21,28 @@ const REMOTE_SPEC_PREFIXES = [
|
|
|
21
21
|
"ssh://",
|
|
22
22
|
"github:"
|
|
23
23
|
];
|
|
24
|
+
const MAX_LOCKFILE_BYTES = 64 * 1024 * 1024;
|
|
25
|
+
const NPM_LOCKFILE_NAMES = new Set([
|
|
26
|
+
"npm-shrinkwrap.json",
|
|
27
|
+
"package-lock.json",
|
|
28
|
+
"pnpm-lock.yaml",
|
|
29
|
+
"yarn.lock"
|
|
30
|
+
]);
|
|
31
|
+
const PYPI_LOCKFILE_NAMES = new Set([
|
|
32
|
+
"Pipfile.lock",
|
|
33
|
+
"poetry.lock",
|
|
34
|
+
"requirements.txt",
|
|
35
|
+
"uv.lock"
|
|
36
|
+
]);
|
|
37
|
+
const DEFAULT_NPM_REGISTRY_HOSTS = ["registry.npmjs.org", "registry.yarnpkg.com"];
|
|
38
|
+
const DEFAULT_PYPI_REGISTRY_HOSTS = ["pypi.org", "files.pythonhosted.org"];
|
|
39
|
+
function readLockfileText(path) {
|
|
40
|
+
const sizeBytes = statSync(path).size;
|
|
41
|
+
if (sizeBytes > MAX_LOCKFILE_BYTES) {
|
|
42
|
+
throw new Error(`lockfile is ${sizeBytes} bytes, above the ${MAX_LOCKFILE_BYTES} byte parse limit`);
|
|
43
|
+
}
|
|
44
|
+
return readFileSync(path, "utf8");
|
|
45
|
+
}
|
|
24
46
|
export function isSupportedLockfilePath(target) {
|
|
25
47
|
return LOCKFILE_NAMES.has(basename(target));
|
|
26
48
|
}
|
|
@@ -62,7 +84,7 @@ export function verifyLockfile(targetPath, options = {}) {
|
|
|
62
84
|
}
|
|
63
85
|
let text;
|
|
64
86
|
try {
|
|
65
|
-
text =
|
|
87
|
+
text = readLockfileText(absoluteTarget);
|
|
66
88
|
}
|
|
67
89
|
catch (error) {
|
|
68
90
|
return preflightReport({
|
|
@@ -88,7 +110,7 @@ export function parseLockfilePackages(targetPath) {
|
|
|
88
110
|
const fileName = basename(absoluteTarget);
|
|
89
111
|
let text;
|
|
90
112
|
try {
|
|
91
|
-
text =
|
|
113
|
+
text = readLockfileText(absoluteTarget);
|
|
92
114
|
}
|
|
93
115
|
catch (error) {
|
|
94
116
|
return {
|
|
@@ -114,10 +136,67 @@ function createParseContext(fileName, filePath) {
|
|
|
114
136
|
return {
|
|
115
137
|
fileName,
|
|
116
138
|
filePath,
|
|
139
|
+
registryHosts: expectedRegistryHosts(fileName, filePath),
|
|
117
140
|
skipped: [],
|
|
118
141
|
errors: []
|
|
119
142
|
};
|
|
120
143
|
}
|
|
144
|
+
function expectedRegistryHosts(fileName, filePath) {
|
|
145
|
+
const hosts = new Set();
|
|
146
|
+
if (NPM_LOCKFILE_NAMES.has(fileName)) {
|
|
147
|
+
for (const host of DEFAULT_NPM_REGISTRY_HOSTS) {
|
|
148
|
+
hosts.add(host);
|
|
149
|
+
}
|
|
150
|
+
for (const host of npmrcRegistryHosts(filePath)) {
|
|
151
|
+
hosts.add(host);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (PYPI_LOCKFILE_NAMES.has(fileName)) {
|
|
155
|
+
for (const host of DEFAULT_PYPI_REGISTRY_HOSTS) {
|
|
156
|
+
hosts.add(host);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return hosts;
|
|
160
|
+
}
|
|
161
|
+
function npmrcRegistryHosts(lockfilePath) {
|
|
162
|
+
if (!lockfilePath) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
let text;
|
|
166
|
+
try {
|
|
167
|
+
text = readLockfileText(join(dirname(lockfilePath), ".npmrc"));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const hosts = [];
|
|
173
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
174
|
+
const value = /^\s*(?:@[^=\s:]+:)?registry\s*=\s*(\S+)/u.exec(line)?.[1];
|
|
175
|
+
if (!value) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
hosts.push(new URL(value).hostname.toLowerCase());
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return hosts;
|
|
186
|
+
}
|
|
187
|
+
function untrustedResolvedHost(value, context) {
|
|
188
|
+
if (context.registryHosts.size === 0 || !/^https?:\/\//iu.test(value)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
let host;
|
|
192
|
+
try {
|
|
193
|
+
host = new URL(value).hostname.toLowerCase();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
return context.registryHosts.has(host) ? null : host;
|
|
199
|
+
}
|
|
121
200
|
function recordSkip(context, name, reason, location) {
|
|
122
201
|
context.skipped.push({ name, reason, location });
|
|
123
202
|
}
|
|
@@ -136,7 +215,7 @@ function preflightReport(input) {
|
|
|
136
215
|
reason: "dg verify command allowlist",
|
|
137
216
|
trustedBy: "user"
|
|
138
217
|
}));
|
|
139
|
-
const deniedLicenses = new Set((input.options.denyLicenses ?? []).map(
|
|
218
|
+
const deniedLicenses = new Set((input.options.denyLicenses ?? []).map(denyListLicenseKey));
|
|
140
219
|
const findings = [];
|
|
141
220
|
for (const observation of input.observations) {
|
|
142
221
|
const packageName = packageDisplayName(observation.identity);
|
|
@@ -212,10 +291,10 @@ function parseLockfile(text, context) {
|
|
|
212
291
|
return parseRequirements(text, context);
|
|
213
292
|
}
|
|
214
293
|
if (name === "Cargo.lock") {
|
|
215
|
-
return parseCargoLock(text);
|
|
294
|
+
return parseCargoLock(text, context);
|
|
216
295
|
}
|
|
217
296
|
if (name === "poetry.lock") {
|
|
218
|
-
return parsePoetryLock(text);
|
|
297
|
+
return parsePoetryLock(text, context);
|
|
219
298
|
}
|
|
220
299
|
if (name === "uv.lock") {
|
|
221
300
|
return parseUvLock(text, context);
|
|
@@ -304,14 +383,25 @@ function parsePackageLock(text, context) {
|
|
|
304
383
|
continue;
|
|
305
384
|
}
|
|
306
385
|
const resolvedIsGit = resolved !== null && isUnsafeResolvedUrl(resolved);
|
|
386
|
+
const resolvedOffRegistry = resolved !== null && untrustedResolvedHost(resolved, context) !== null;
|
|
387
|
+
// A registry `resolved` URL is how npm actually fetched this entry; it must
|
|
388
|
+
// be scanned regardless of how a same-named sibling declares the dependency
|
|
389
|
+
// (a `file:`/`git:` spec elsewhere must not suppress a registry install).
|
|
390
|
+
// The name-keyed cross-package hint only applies when this entry has no
|
|
391
|
+
// registry resolved of its own.
|
|
392
|
+
const resolvedIsRegistry = !!resolved && !resolvedIsGit && !resolvedOffRegistry && !resolved.toLowerCase().startsWith("file:");
|
|
307
393
|
const skipReason = resolvedIsGit
|
|
308
394
|
? "git"
|
|
309
395
|
: resolved?.toLowerCase().startsWith("file:")
|
|
310
396
|
? "local"
|
|
311
|
-
:
|
|
397
|
+
: resolvedOffRegistry
|
|
398
|
+
? "direct-url"
|
|
399
|
+
: resolvedIsRegistry
|
|
400
|
+
? null
|
|
401
|
+
: specKinds.get(name) ?? null;
|
|
312
402
|
if (skipReason) {
|
|
313
403
|
recordSkip(context, name, skipReason, path);
|
|
314
|
-
if (!resolvedIsGit) {
|
|
404
|
+
if (!resolvedIsGit && !resolvedOffRegistry) {
|
|
315
405
|
continue;
|
|
316
406
|
}
|
|
317
407
|
}
|
|
@@ -324,7 +414,7 @@ function parsePackageLock(text, context) {
|
|
|
324
414
|
resolvedUrl: resolved,
|
|
325
415
|
integrity: stringOrNull(rawPackage.integrity),
|
|
326
416
|
license: stringOrNull(rawPackage.license)
|
|
327
|
-
}));
|
|
417
|
+
}, context));
|
|
328
418
|
}
|
|
329
419
|
return observations;
|
|
330
420
|
}
|
|
@@ -333,7 +423,11 @@ function parsePackageLock(text, context) {
|
|
|
333
423
|
}
|
|
334
424
|
return observations;
|
|
335
425
|
}
|
|
336
|
-
|
|
426
|
+
const MAX_LEGACY_DEPENDENCY_DEPTH = 512;
|
|
427
|
+
function walkLegacyDependencies(dependencies, observations, seen, context, depth = 0) {
|
|
428
|
+
if (depth > MAX_LEGACY_DEPENDENCY_DEPTH) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
337
431
|
for (const [name, rawPackage] of Object.entries(dependencies)) {
|
|
338
432
|
if (!isRecord(rawPackage) || rawPackage.bundled === true) {
|
|
339
433
|
continue;
|
|
@@ -341,8 +435,13 @@ function walkLegacyDependencies(dependencies, observations, seen, context) {
|
|
|
341
435
|
const rawVersion = stringOrNull(rawPackage.version);
|
|
342
436
|
const resolved = stringOrNull(rawPackage.resolved);
|
|
343
437
|
const resolvedIsGit = resolved !== null && isUnsafeResolvedUrl(resolved);
|
|
438
|
+
const resolvedOffRegistry = resolved !== null && untrustedResolvedHost(resolved, context) !== null;
|
|
344
439
|
const versionKind = rawVersion && !rawVersion.startsWith("npm:") ? specSourceKind(rawVersion) : null;
|
|
345
|
-
|
|
440
|
+
// Prefer the registry `resolved` URL over the version field: an entry npm
|
|
441
|
+
// fetched from the registry must be scanned even if its `version` reads
|
|
442
|
+
// file:/git:/workspace: (a contradiction an attacker can craft to hide it).
|
|
443
|
+
const resolvedIsRegistry = resolved !== null && !resolvedIsGit && !resolvedOffRegistry && !resolved.toLowerCase().startsWith("file:");
|
|
444
|
+
const skipReason = resolvedIsGit ? "git" : resolvedOffRegistry ? "direct-url" : resolvedIsRegistry ? null : versionKind;
|
|
346
445
|
const alias = npmAliasVersion(rawVersion);
|
|
347
446
|
const resolvedName = alias?.name ?? name;
|
|
348
447
|
const version = alias?.version ?? rawVersion;
|
|
@@ -350,7 +449,7 @@ function walkLegacyDependencies(dependencies, observations, seen, context) {
|
|
|
350
449
|
if (skipReason) {
|
|
351
450
|
recordSkip(context, resolvedName, skipReason, name);
|
|
352
451
|
}
|
|
353
|
-
if (!seen.has(key) && (!skipReason || resolvedIsGit)) {
|
|
452
|
+
if (!seen.has(key) && (!skipReason || resolvedIsGit || resolvedOffRegistry)) {
|
|
354
453
|
seen.add(key);
|
|
355
454
|
observations.push(lockfileObservation({
|
|
356
455
|
ecosystem: "npm",
|
|
@@ -361,10 +460,10 @@ function walkLegacyDependencies(dependencies, observations, seen, context) {
|
|
|
361
460
|
resolvedUrl: resolved,
|
|
362
461
|
integrity: stringOrNull(rawPackage.integrity),
|
|
363
462
|
license: stringOrNull(rawPackage.license)
|
|
364
|
-
}));
|
|
463
|
+
}, context));
|
|
365
464
|
}
|
|
366
465
|
if (isRecord(rawPackage.dependencies)) {
|
|
367
|
-
walkLegacyDependencies(rawPackage.dependencies, observations, seen, context);
|
|
466
|
+
walkLegacyDependencies(rawPackage.dependencies, observations, seen, context, depth + 1);
|
|
368
467
|
}
|
|
369
468
|
}
|
|
370
469
|
}
|
|
@@ -394,10 +493,14 @@ function parseYarnLock(text, context) {
|
|
|
394
493
|
const requested = header.split(",")[0]?.trim().replace(/^"|"$/gu, "") ?? header;
|
|
395
494
|
const name = packageNameFromYarnDescriptor(requested);
|
|
396
495
|
const resolved = quotedValue(lines, "resolved");
|
|
397
|
-
const
|
|
496
|
+
const resolution = quotedValue(lines, "resolution");
|
|
497
|
+
const skipReason = specSourceKind(yarnDescriptorSpec(requested))
|
|
498
|
+
?? (resolution ? specSourceKind(yarnDescriptorSpec(resolution)) : null);
|
|
499
|
+
const resolvedUnsafe = resolved !== null
|
|
500
|
+
&& (isUnsafeResolvedUrl(resolved) || untrustedResolvedHost(resolved, context) !== null);
|
|
398
501
|
if (skipReason) {
|
|
399
502
|
recordSkip(context, name ?? requested, skipReason, requested);
|
|
400
|
-
if (!
|
|
503
|
+
if (!resolvedUnsafe) {
|
|
401
504
|
continue;
|
|
402
505
|
}
|
|
403
506
|
}
|
|
@@ -414,7 +517,7 @@ function parseYarnLock(text, context) {
|
|
|
414
517
|
resolvedUrl: resolved,
|
|
415
518
|
integrity: quotedValue(lines, "integrity") ?? quotedValue(lines, "checksum"),
|
|
416
519
|
license: null
|
|
417
|
-
}));
|
|
520
|
+
}, context));
|
|
418
521
|
}
|
|
419
522
|
return observations;
|
|
420
523
|
}
|
|
@@ -425,7 +528,7 @@ function parsePnpmLock(text, context) {
|
|
|
425
528
|
let current = null;
|
|
426
529
|
const flush = () => {
|
|
427
530
|
if (current) {
|
|
428
|
-
observations.push(lockfileObservation(current));
|
|
531
|
+
observations.push(lockfileObservation(current, context));
|
|
429
532
|
current = null;
|
|
430
533
|
}
|
|
431
534
|
};
|
|
@@ -590,6 +693,33 @@ function collectRequirements(text, baseDir, state, context, observations) {
|
|
|
590
693
|
}
|
|
591
694
|
const hash = /--hash=([A-Za-z0-9:_-]+)/u.exec(line)?.[1] ?? null;
|
|
592
695
|
const requirement = line.replace(/\s*--hash=[^\s]+/gu, "").trim();
|
|
696
|
+
const directReference = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?\s*@\s*([^;\s]+)/u.exec(requirement);
|
|
697
|
+
if (directReference?.[1] && directReference[2]) {
|
|
698
|
+
const referenceName = directReference[1];
|
|
699
|
+
const referenceUrl = directReference[2];
|
|
700
|
+
const referenceKind = specSourceKind(referenceUrl);
|
|
701
|
+
if (referenceKind === "local" || /^\.{0,2}\//u.test(referenceUrl)) {
|
|
702
|
+
recordSkip(context, referenceName, "local", line);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
recordSkip(context, referenceName, referenceKind ?? "direct-url", line);
|
|
706
|
+
observations.push(packageObservation({
|
|
707
|
+
ecosystem: "pypi",
|
|
708
|
+
name: referenceName,
|
|
709
|
+
version: null,
|
|
710
|
+
requested: line,
|
|
711
|
+
sourceKind: "lockfile-url-fallback",
|
|
712
|
+
resolvedUrl: referenceUrl,
|
|
713
|
+
integrity: hash,
|
|
714
|
+
license: null
|
|
715
|
+
}, "block", {
|
|
716
|
+
id: "unverified-lockfile-url",
|
|
717
|
+
title: "Unverified lockfile URL",
|
|
718
|
+
message: "lockfile resolved artifact uses a direct URL or git source that requires proxy hash verification",
|
|
719
|
+
location: line
|
|
720
|
+
}));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
593
723
|
const match = /^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?(?:\s*={2,3}\s*([^;\s]+))?/u.exec(requirement);
|
|
594
724
|
if (!match?.[1]) {
|
|
595
725
|
observations.push(blockedUnknownSpec(line));
|
|
@@ -609,7 +739,7 @@ function collectRequirements(text, baseDir, state, context, observations) {
|
|
|
609
739
|
resolvedUrl: null,
|
|
610
740
|
integrity: hash,
|
|
611
741
|
license: null
|
|
612
|
-
}));
|
|
742
|
+
}, context));
|
|
613
743
|
}
|
|
614
744
|
}
|
|
615
745
|
function followRequirementInclude(target, baseDir, state, context, observations) {
|
|
@@ -628,7 +758,7 @@ function followRequirementInclude(target, baseDir, state, context, observations)
|
|
|
628
758
|
state.visited.add(includePath);
|
|
629
759
|
let text;
|
|
630
760
|
try {
|
|
631
|
-
text =
|
|
761
|
+
text = readLockfileText(includePath);
|
|
632
762
|
}
|
|
633
763
|
catch (error) {
|
|
634
764
|
recordParseError(context, target, error);
|
|
@@ -652,27 +782,46 @@ function joinRequirementContinuations(lines) {
|
|
|
652
782
|
}
|
|
653
783
|
return joined;
|
|
654
784
|
}
|
|
655
|
-
function parseCargoLock(text) {
|
|
656
|
-
|
|
657
|
-
|
|
785
|
+
function parseCargoLock(text, context) {
|
|
786
|
+
const observations = [];
|
|
787
|
+
for (const block of lockBlocks(text, "[[package]]")) {
|
|
788
|
+
const name = tomlString(block, "name");
|
|
789
|
+
if (!name) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
658
792
|
const version = tomlString(block, "version");
|
|
659
|
-
|
|
793
|
+
const source = tomlString(block, "source");
|
|
794
|
+
if (source === null) {
|
|
795
|
+
recordSkip(context, name, "local", `${name}@${version ?? "unknown"}`);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
observations.push(lockfileObservation({
|
|
660
799
|
ecosystem: "cargo",
|
|
661
800
|
name,
|
|
662
801
|
version,
|
|
663
802
|
requested: `${name}@${version ?? "unknown"}`,
|
|
664
803
|
sourceKind: "lockfile",
|
|
665
|
-
resolvedUrl:
|
|
804
|
+
resolvedUrl: source,
|
|
666
805
|
integrity: tomlString(block, "checksum"),
|
|
667
806
|
license: null
|
|
668
|
-
});
|
|
669
|
-
}
|
|
807
|
+
}, context));
|
|
808
|
+
}
|
|
809
|
+
return observations;
|
|
670
810
|
}
|
|
671
|
-
function parsePoetryLock(text) {
|
|
672
|
-
|
|
673
|
-
|
|
811
|
+
function parsePoetryLock(text, context) {
|
|
812
|
+
const observations = [];
|
|
813
|
+
for (const block of lockBlocks(text, "[[package]]")) {
|
|
814
|
+
const name = tomlString(block, "name");
|
|
815
|
+
if (!name) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
674
818
|
const version = tomlString(block, "version");
|
|
675
|
-
|
|
819
|
+
const skipReason = poetrySourceSkipReason(block);
|
|
820
|
+
if (skipReason) {
|
|
821
|
+
recordSkip(context, name, skipReason, `${name}${version ? `==${version}` : ""}`);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
observations.push(lockfileObservation({
|
|
676
825
|
ecosystem: "pypi",
|
|
677
826
|
name,
|
|
678
827
|
version,
|
|
@@ -681,8 +830,26 @@ function parsePoetryLock(text) {
|
|
|
681
830
|
resolvedUrl: null,
|
|
682
831
|
integrity: /hash\s*=\s*"([^"]+)"/u.exec(block)?.[1] ?? null,
|
|
683
832
|
license: tomlString(block, "license")
|
|
684
|
-
});
|
|
685
|
-
}
|
|
833
|
+
}, context));
|
|
834
|
+
}
|
|
835
|
+
return observations;
|
|
836
|
+
}
|
|
837
|
+
function poetrySourceSkipReason(block) {
|
|
838
|
+
const sourceTable = /\[package\.source\]([\s\S]*?)(?=\n\[|$)/u.exec(block)?.[1];
|
|
839
|
+
if (sourceTable === undefined) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const type = /\btype\s*=\s*"([^"]+)"/u.exec(sourceTable)?.[1];
|
|
843
|
+
if (type === "git") {
|
|
844
|
+
return "git";
|
|
845
|
+
}
|
|
846
|
+
if (type === "url") {
|
|
847
|
+
return "direct-url";
|
|
848
|
+
}
|
|
849
|
+
if (type === "directory" || type === "file") {
|
|
850
|
+
return "local";
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
686
853
|
}
|
|
687
854
|
function parseUvLock(text, context) {
|
|
688
855
|
const observations = [];
|
|
@@ -707,7 +874,7 @@ function parseUvLock(text, context) {
|
|
|
707
874
|
resolvedUrl: null,
|
|
708
875
|
integrity: /hash\s*=\s*"([^"]+)"/u.exec(block)?.[1] ?? null,
|
|
709
876
|
license: null
|
|
710
|
-
}));
|
|
877
|
+
}, context));
|
|
711
878
|
}
|
|
712
879
|
return observations;
|
|
713
880
|
}
|
|
@@ -744,24 +911,41 @@ function parsePipfileLock(text, context) {
|
|
|
744
911
|
}
|
|
745
912
|
return ["default", "develop"].flatMap((section) => {
|
|
746
913
|
const packages = isRecord(parsed[section]) ? parsed[section] : {};
|
|
747
|
-
return Object.entries(packages).
|
|
914
|
+
return Object.entries(packages).flatMap(([name, rawPackage]) => {
|
|
748
915
|
const record = isRecord(rawPackage) ? rawPackage : {};
|
|
749
916
|
const version = stringOrNull(record.version)?.replace(/^==/u, "") ?? null;
|
|
917
|
+
const skipReason = pipfileSourceSkipReason(record);
|
|
918
|
+
if (skipReason) {
|
|
919
|
+
recordSkip(context, name, skipReason, `${name}${version ? `==${version}` : ""}`);
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
750
922
|
const hashes = Array.isArray(record.hashes) ? record.hashes.filter((hash) => typeof hash === "string") : [];
|
|
751
|
-
return lockfileObservation({
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
923
|
+
return [lockfileObservation({
|
|
924
|
+
ecosystem: "pypi",
|
|
925
|
+
name,
|
|
926
|
+
version,
|
|
927
|
+
requested: `${name}${version ? `==${version}` : ""}`,
|
|
928
|
+
sourceKind: "lockfile",
|
|
929
|
+
resolvedUrl: null,
|
|
930
|
+
integrity: hashes[0] ?? null,
|
|
931
|
+
license: null
|
|
932
|
+
}, context)];
|
|
761
933
|
});
|
|
762
934
|
});
|
|
763
935
|
}
|
|
764
|
-
function
|
|
936
|
+
function pipfileSourceSkipReason(record) {
|
|
937
|
+
if (typeof record.git === "string") {
|
|
938
|
+
return "git";
|
|
939
|
+
}
|
|
940
|
+
if (typeof record.file === "string") {
|
|
941
|
+
return "direct-url";
|
|
942
|
+
}
|
|
943
|
+
if (record.editable === true || typeof record.path === "string") {
|
|
944
|
+
return "local";
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
function lockfileObservation(identity, context) {
|
|
765
949
|
if (identity.resolvedUrl && isUnsafeResolvedUrl(identity.resolvedUrl)) {
|
|
766
950
|
return packageObservation(identity, "block", {
|
|
767
951
|
id: "unverified-lockfile-url",
|
|
@@ -770,6 +954,15 @@ function lockfileObservation(identity) {
|
|
|
770
954
|
location: identity.requested
|
|
771
955
|
});
|
|
772
956
|
}
|
|
957
|
+
const untrustedHost = identity.resolvedUrl ? untrustedResolvedHost(identity.resolvedUrl, context) : null;
|
|
958
|
+
if (untrustedHost !== null) {
|
|
959
|
+
return packageObservation(identity, "block", {
|
|
960
|
+
id: "untrusted-registry-host",
|
|
961
|
+
title: "Untrusted lockfile registry host",
|
|
962
|
+
message: `${packageDisplayName(identity)} resolves from '${untrustedHost}', which is not an expected registry host for this lockfile`,
|
|
963
|
+
location: identity.requested
|
|
964
|
+
});
|
|
965
|
+
}
|
|
773
966
|
const integrityFinding = integrityPolicyFinding(identity);
|
|
774
967
|
if (integrityFinding) {
|
|
775
968
|
return packageObservation(identity, "warn", integrityFinding);
|
|
@@ -788,7 +981,7 @@ function integrityPolicyFinding(identity) {
|
|
|
788
981
|
};
|
|
789
982
|
}
|
|
790
983
|
function deniedLicenseFinding(identity, deniedLicenses) {
|
|
791
|
-
if (!identity.license || !deniedLicenses.has(
|
|
984
|
+
if (!identity.license || !deniedLicenses.has(denyListLicenseKey(identity.license))) {
|
|
792
985
|
return null;
|
|
793
986
|
}
|
|
794
987
|
return {
|
|
@@ -956,7 +1149,7 @@ function stringOrNull(value) {
|
|
|
956
1149
|
function stripQuotes(value) {
|
|
957
1150
|
return value.replace(/^["']|["']$/gu, "");
|
|
958
1151
|
}
|
|
959
|
-
function
|
|
1152
|
+
function denyListLicenseKey(value) {
|
|
960
1153
|
return value.trim().toLowerCase();
|
|
961
1154
|
}
|
|
962
1155
|
function displayPath(root, path) {
|
package/dist/verify/render.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createTheme } from "../presentation/theme.js";
|
|
2
|
+
import { packagePageUrl } from "../presentation/package-page.js";
|
|
3
|
+
import { sanitizeDeep } from "../security/sanitize.js";
|
|
2
4
|
const STATUS_ROLE = {
|
|
3
5
|
pass: "pass",
|
|
4
6
|
warn: "warn",
|
|
@@ -14,7 +16,8 @@ const STATUS_ACTION = {
|
|
|
14
16
|
error: "block"
|
|
15
17
|
};
|
|
16
18
|
export function renderVerifyText(report, theme = createTheme(false), verbose = false) {
|
|
17
|
-
|
|
19
|
+
const safe = sanitizeDeep(report);
|
|
20
|
+
return verbose ? renderVerifyVerbose(safe, theme) : renderVerifyCompact(safe, theme);
|
|
18
21
|
}
|
|
19
22
|
function renderVerifyCompact(report, theme) {
|
|
20
23
|
const badge = report.status === "error" ? theme.paint("block", "✘ ERROR") : theme.badge(STATUS_ACTION[report.status]);
|
|
@@ -30,8 +33,19 @@ function renderVerifyCompact(report, theme) {
|
|
|
30
33
|
for (const error of report.errors) {
|
|
31
34
|
lines.push(` ${theme.paint("block", "✘")} ${error}`);
|
|
32
35
|
}
|
|
36
|
+
const page = packageReportUrl(report);
|
|
37
|
+
if (page) {
|
|
38
|
+
lines.push(theme.paint("muted", ` → ${page}`));
|
|
39
|
+
}
|
|
33
40
|
return `${lines.join("\n")}\n`;
|
|
34
41
|
}
|
|
42
|
+
function packageReportUrl(report) {
|
|
43
|
+
if (report.packages.length !== 1) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const identity = report.packages[0];
|
|
47
|
+
return packagePageUrl(identity.ecosystem, identity.name);
|
|
48
|
+
}
|
|
35
49
|
function targetLabel(report) {
|
|
36
50
|
if (report.packages.length === 1) {
|
|
37
51
|
const identity = report.packages[0];
|