@westbayberry/dg 1.3.3 → 2.0.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.
Files changed (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54116
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,698 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, relative, resolve, sep } from "node:path";
3
+ import { loadUserConfig } from "../config/settings.js";
4
+ import { evaluatePackagePolicy, resolveEffectivePolicy } from "../policy/evaluate.js";
5
+ const LOCKFILE_NAMES = new Set([
6
+ "Cargo.lock",
7
+ "Pipfile.lock",
8
+ "package-lock.json",
9
+ "pnpm-lock.yaml",
10
+ "poetry.lock",
11
+ "requirements.txt",
12
+ "yarn.lock"
13
+ ]);
14
+ const REMOTE_SPEC_PREFIXES = [
15
+ "http://",
16
+ "https://",
17
+ "git+",
18
+ "git://",
19
+ "ssh://",
20
+ "github:"
21
+ ];
22
+ export function isSupportedLockfilePath(target) {
23
+ return LOCKFILE_NAMES.has(basename(target));
24
+ }
25
+ export function verifyPackageSpec(spec, options = {}) {
26
+ const parsed = parsePackageSpec(spec);
27
+ const observations = parsed
28
+ ? [parsed]
29
+ : [blockedUnknownSpec(spec)];
30
+ return preflightReport({
31
+ target: spec,
32
+ inputKind: "package-spec",
33
+ preflight: {
34
+ advisory: true,
35
+ packageCount: observations.length,
36
+ identitySource: "package-spec",
37
+ message: "Package spec verification is advisory preflight; proxy enforcement remains authoritative for network fetches."
38
+ },
39
+ observations,
40
+ options
41
+ });
42
+ }
43
+ export function verifyLockfile(targetPath, options = {}) {
44
+ const cwd = resolve(options.cwd ?? process.cwd());
45
+ const absoluteTarget = resolve(cwd, targetPath);
46
+ const displayTarget = displayPath(cwd, absoluteTarget);
47
+ if (!existsSync(absoluteTarget)) {
48
+ return preflightReport({
49
+ target: displayTarget,
50
+ inputKind: "lockfile",
51
+ preflight: lockfileSummary(0),
52
+ observations: [],
53
+ options,
54
+ errors: [`lockfile does not exist: ${displayTarget}`]
55
+ });
56
+ }
57
+ let text;
58
+ try {
59
+ text = readFileSync(absoluteTarget, "utf8");
60
+ }
61
+ catch (error) {
62
+ return preflightReport({
63
+ target: displayTarget,
64
+ inputKind: "lockfile",
65
+ preflight: lockfileSummary(0),
66
+ observations: [],
67
+ options,
68
+ errors: [`could not read lockfile: ${error instanceof Error ? error.message : "unknown read error"}`]
69
+ });
70
+ }
71
+ const observations = parseLockfile(basename(absoluteTarget), text);
72
+ return preflightReport({
73
+ target: displayTarget,
74
+ inputKind: "lockfile",
75
+ preflight: lockfileSummary(observations.length),
76
+ observations,
77
+ options
78
+ });
79
+ }
80
+ function preflightReport(input) {
81
+ const policy = resolveEffectivePolicy({
82
+ userConfig: loadUserConfig()
83
+ });
84
+ const allowlists = (input.options.allowPackages ?? []).map((packageName) => ({
85
+ packageName,
86
+ reason: "dg verify command allowlist",
87
+ trustedBy: "user"
88
+ }));
89
+ const deniedLicenses = new Set((input.options.denyLicenses ?? []).map(normalizeLicense));
90
+ const findings = [];
91
+ for (const observation of input.observations) {
92
+ const packageName = packageDisplayName(observation.identity);
93
+ const licenseFinding = deniedLicenseFinding(observation.identity, deniedLicenses);
94
+ const packageVerdict = strongerVerdict(observation.verdict, licenseFinding ? "block" : "pass");
95
+ const evaluation = evaluatePackagePolicy({
96
+ verdict: packageVerdict,
97
+ packageName,
98
+ policy,
99
+ allowlists
100
+ });
101
+ const baseFinding = licenseFinding ?? observation.finding;
102
+ if (baseFinding && evaluation.action !== "pass") {
103
+ findings.push({
104
+ ...baseFinding,
105
+ severity: evaluation.action === "block" ? "block" : "warn",
106
+ message: `${baseFinding.message} (${evaluation.reason})`
107
+ });
108
+ }
109
+ }
110
+ const errors = [...(input.errors ?? [])];
111
+ return {
112
+ target: input.target,
113
+ inputKind: input.inputKind,
114
+ status: statusFor(policyActionFor(findings), errors),
115
+ sha256: null,
116
+ sizeBytes: null,
117
+ archive: null,
118
+ workspaceScan: null,
119
+ preflight: input.preflight,
120
+ packages: input.observations.map((observation) => observation.identity),
121
+ findings,
122
+ errors,
123
+ summary: summarize(findings, errors)
124
+ };
125
+ }
126
+ function parsePackageSpec(spec) {
127
+ const trimmed = spec.trim();
128
+ if (trimmed.length === 0 || /[\u0000-\u001f\u007f]/u.test(trimmed)) {
129
+ return null;
130
+ }
131
+ const lowered = trimmed.toLowerCase();
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)) {
199
+ return null;
200
+ }
201
+ return packageObservation({
202
+ ecosystem: "cargo",
203
+ name: parsed.name,
204
+ version: parsed.version,
205
+ requested,
206
+ sourceKind: "package-spec",
207
+ resolvedUrl: null,
208
+ integrity: null,
209
+ license: null
210
+ }, exactVersionVerdict(parsed.version), exactVersionFinding(parsed.name, parsed.version, requested));
211
+ }
212
+ function parseLockfile(name, text) {
213
+ if (name === "package-lock.json") {
214
+ return parsePackageLock(text);
215
+ }
216
+ if (name === "yarn.lock") {
217
+ return parseYarnLock(text);
218
+ }
219
+ if (name === "pnpm-lock.yaml") {
220
+ return parsePnpmLock(text);
221
+ }
222
+ if (name === "requirements.txt") {
223
+ return parseRequirements(text);
224
+ }
225
+ if (name === "Cargo.lock") {
226
+ return parseCargoLock(text);
227
+ }
228
+ if (name === "poetry.lock") {
229
+ return parsePoetryLock(text);
230
+ }
231
+ if (name === "Pipfile.lock") {
232
+ return parsePipfileLock(text);
233
+ }
234
+ return [];
235
+ }
236
+ function parsePackageLock(text) {
237
+ let parsed;
238
+ try {
239
+ parsed = JSON.parse(text);
240
+ }
241
+ catch (error) {
242
+ return [malformedLockfileObservation("package-lock.json", error)];
243
+ }
244
+ if (!isRecord(parsed)) {
245
+ return [malformedLockfileObservation("package-lock.json", new Error("root must be an object"))];
246
+ }
247
+ const observations = [];
248
+ const packages = isRecord(parsed.packages) ? parsed.packages : {};
249
+ for (const [path, rawPackage] of Object.entries(packages)) {
250
+ if (path.length === 0 || !isRecord(rawPackage)) {
251
+ continue;
252
+ }
253
+ const name = typeof rawPackage.name === "string" ? rawPackage.name : packageNameFromNodeModulesPath(path);
254
+ const version = typeof rawPackage.version === "string" ? rawPackage.version : null;
255
+ if (!name) {
256
+ continue;
257
+ }
258
+ observations.push(lockfileObservation({
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
+ }));
268
+ }
269
+ const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
270
+ for (const [name, rawPackage] of Object.entries(dependencies)) {
271
+ if (!isRecord(rawPackage) || observations.some((observation) => observation.identity.name === name)) {
272
+ continue;
273
+ }
274
+ observations.push(lockfileObservation({
275
+ ecosystem: "npm",
276
+ name,
277
+ version: stringOrNull(rawPackage.version),
278
+ requested: name,
279
+ sourceKind: "lockfile",
280
+ resolvedUrl: stringOrNull(rawPackage.resolved),
281
+ integrity: stringOrNull(rawPackage.integrity),
282
+ license: stringOrNull(rawPackage.license)
283
+ }));
284
+ }
285
+ return observations;
286
+ }
287
+ function parseYarnLock(text) {
288
+ const observations = [];
289
+ const blocks = text.split(/\n(?=(?:"?@?[^"\s].*"?):\n)/u);
290
+ for (const block of blocks) {
291
+ const lines = block.split(/\r?\n/u);
292
+ const header = lines[0]?.trim().replace(/:$/u, "");
293
+ if (!header) {
294
+ continue;
295
+ }
296
+ const requested = header.split(",")[0]?.trim().replace(/^"|"$/gu, "") ?? header;
297
+ const name = packageNameFromYarnDescriptor(requested);
298
+ if (!name) {
299
+ continue;
300
+ }
301
+ const version = quotedValue(lines, "version");
302
+ const resolvedUrl = quotedValue(lines, "resolved");
303
+ const integrity = quotedValue(lines, "integrity");
304
+ observations.push(lockfileObservation({
305
+ ecosystem: "npm",
306
+ name,
307
+ version,
308
+ requested,
309
+ sourceKind: "lockfile",
310
+ resolvedUrl,
311
+ integrity,
312
+ license: null
313
+ }));
314
+ }
315
+ return observations;
316
+ }
317
+ function parsePnpmLock(text) {
318
+ const observations = [];
319
+ const lines = text.split(/\r?\n/u);
320
+ let current = null;
321
+ for (const line of lines) {
322
+ const packageMatch = /^\s{2}['"]?(?:\/)?((?:@[^/\s]+\/)?[^@\s:'"]+)@([^:\s'"]+)['"]?:\s*$/u.exec(line);
323
+ if (packageMatch?.[1] && packageMatch[2]) {
324
+ if (current) {
325
+ observations.push(lockfileObservation(current));
326
+ }
327
+ current = {
328
+ ecosystem: "npm",
329
+ name: packageMatch[1],
330
+ version: packageMatch[2],
331
+ requested: `${packageMatch[1]}@${packageMatch[2]}`,
332
+ sourceKind: "lockfile",
333
+ resolvedUrl: null,
334
+ integrity: null,
335
+ license: null
336
+ };
337
+ continue;
338
+ }
339
+ if (!current) {
340
+ continue;
341
+ }
342
+ const integrityMatch = /integrity:\s*([^,}\s]+)/u.exec(line);
343
+ const tarballMatch = /tarball:\s*([^,}\s]+)/u.exec(line);
344
+ if (integrityMatch?.[1]) {
345
+ current = {
346
+ ...current,
347
+ integrity: stripQuotes(integrityMatch[1])
348
+ };
349
+ }
350
+ if (tarballMatch?.[1]) {
351
+ current = {
352
+ ...current,
353
+ resolvedUrl: stripQuotes(tarballMatch[1])
354
+ };
355
+ }
356
+ }
357
+ if (current) {
358
+ observations.push(lockfileObservation(current));
359
+ }
360
+ return observations;
361
+ }
362
+ function parseRequirements(text) {
363
+ return text.split(/\r?\n/u)
364
+ .map((line) => line.trim())
365
+ .filter((line) => line.length > 0 && !line.startsWith("#"))
366
+ .map((line) => {
367
+ if (REMOTE_SPEC_PREFIXES.some((prefix) => line.toLowerCase().startsWith(prefix))) {
368
+ return packageObservation({
369
+ ecosystem: "pypi",
370
+ name: line,
371
+ version: null,
372
+ requested: line,
373
+ sourceKind: "lockfile-url-fallback",
374
+ resolvedUrl: line,
375
+ integrity: null,
376
+ license: null
377
+ }, "block", {
378
+ id: "lockfile-url-fallback",
379
+ title: "Lockfile URL fallback identity",
380
+ message: "lockfile entry uses a URL without package identity or hash metadata",
381
+ location: line
382
+ });
383
+ }
384
+ const hash = /--hash=([A-Za-z0-9:-]+)/u.exec(line)?.[1] ?? null;
385
+ const cleanLine = line.replace(/\s+--hash=[^\s]+/gu, "");
386
+ const match = /^([A-Za-z0-9_.-]+)(?:==([^;\s]+))?/u.exec(cleanLine);
387
+ if (!match?.[1]) {
388
+ return blockedUnknownSpec(line);
389
+ }
390
+ return lockfileObservation({
391
+ ecosystem: "pypi",
392
+ name: match[1],
393
+ version: match[2] ?? null,
394
+ requested: line,
395
+ sourceKind: "lockfile",
396
+ resolvedUrl: null,
397
+ integrity: hash,
398
+ license: null
399
+ });
400
+ });
401
+ }
402
+ function parseCargoLock(text) {
403
+ return lockBlocks(text, "[[package]]").map((block) => {
404
+ const name = tomlString(block, "name") ?? "unknown";
405
+ const version = tomlString(block, "version");
406
+ return lockfileObservation({
407
+ ecosystem: "cargo",
408
+ name,
409
+ version,
410
+ requested: `${name}@${version ?? "unknown"}`,
411
+ sourceKind: "lockfile",
412
+ resolvedUrl: tomlString(block, "source"),
413
+ integrity: tomlString(block, "checksum"),
414
+ license: null
415
+ });
416
+ }).filter((observation) => observation.identity.name !== "unknown");
417
+ }
418
+ function parsePoetryLock(text) {
419
+ return lockBlocks(text, "[[package]]").map((block) => {
420
+ const name = tomlString(block, "name") ?? "unknown";
421
+ const version = tomlString(block, "version");
422
+ return lockfileObservation({
423
+ ecosystem: "pypi",
424
+ name,
425
+ version,
426
+ requested: `${name}==${version ?? "unknown"}`,
427
+ sourceKind: "lockfile",
428
+ resolvedUrl: null,
429
+ integrity: null,
430
+ license: tomlString(block, "license")
431
+ });
432
+ }).filter((observation) => observation.identity.name !== "unknown");
433
+ }
434
+ function parsePipfileLock(text) {
435
+ let parsed;
436
+ try {
437
+ parsed = JSON.parse(text);
438
+ }
439
+ catch (error) {
440
+ return [malformedLockfileObservation("Pipfile.lock", error)];
441
+ }
442
+ if (!isRecord(parsed)) {
443
+ return [];
444
+ }
445
+ return ["default", "develop"].flatMap((section) => {
446
+ const packages = isRecord(parsed[section]) ? parsed[section] : {};
447
+ return Object.entries(packages).map(([name, rawPackage]) => {
448
+ const record = isRecord(rawPackage) ? rawPackage : {};
449
+ const version = stringOrNull(record.version)?.replace(/^==/u, "") ?? null;
450
+ const hashes = Array.isArray(record.hashes) ? record.hashes.filter((hash) => typeof hash === "string") : [];
451
+ return lockfileObservation({
452
+ ecosystem: "pypi",
453
+ name,
454
+ version,
455
+ requested: `${name}${version ? `==${version}` : ""}`,
456
+ sourceKind: "lockfile",
457
+ resolvedUrl: null,
458
+ integrity: hashes[0] ?? null,
459
+ license: null
460
+ });
461
+ });
462
+ });
463
+ }
464
+ function lockfileObservation(identity) {
465
+ if (identity.resolvedUrl && isUnsafeResolvedUrl(identity.resolvedUrl)) {
466
+ return packageObservation(identity, "block", {
467
+ id: "unverified-lockfile-url",
468
+ title: "Unverified lockfile URL",
469
+ message: "lockfile resolved artifact uses a direct URL or git source that requires proxy hash verification",
470
+ location: identity.requested
471
+ });
472
+ }
473
+ const integrityFinding = integrityPolicyFinding(identity);
474
+ if (integrityFinding) {
475
+ return packageObservation(identity, "warn", integrityFinding);
476
+ }
477
+ return packageObservation(identity, "pass", null);
478
+ }
479
+ function integrityPolicyFinding(identity) {
480
+ if (identity.integrity && isSupportedIntegrity(identity.integrity)) {
481
+ return null;
482
+ }
483
+ return {
484
+ id: "missing-artifact-integrity",
485
+ title: "Missing artifact integrity",
486
+ message: `${packageDisplayName(identity)} has no supported lockfile integrity or checksum metadata`,
487
+ location: identity.requested
488
+ };
489
+ }
490
+ function exactVersionFinding(name, version, location) {
491
+ if (version && isExactVersion(version)) {
492
+ return null;
493
+ }
494
+ return {
495
+ id: "unpinned-package-spec",
496
+ title: "Unpinned package spec",
497
+ message: `${name} is not pinned to an exact package version`,
498
+ location
499
+ };
500
+ }
501
+ function exactVersionVerdict(version) {
502
+ return version && isExactVersion(version) ? "pass" : "warn";
503
+ }
504
+ function deniedLicenseFinding(identity, deniedLicenses) {
505
+ if (!identity.license || !deniedLicenses.has(normalizeLicense(identity.license))) {
506
+ return null;
507
+ }
508
+ return {
509
+ id: "license-policy-denied",
510
+ title: "Denied package license",
511
+ message: `${packageDisplayName(identity)} declares denied license '${identity.license}'`,
512
+ location: identity.requested
513
+ };
514
+ }
515
+ function malformedLockfileObservation(lockfile, error) {
516
+ return packageObservation({
517
+ ecosystem: "unknown",
518
+ name: lockfile,
519
+ version: null,
520
+ requested: lockfile,
521
+ sourceKind: "lockfile",
522
+ resolvedUrl: null,
523
+ integrity: null,
524
+ license: null
525
+ }, "block", {
526
+ id: "malformed-lockfile",
527
+ title: "Malformed lockfile",
528
+ message: error instanceof Error ? error.message : "lockfile could not be parsed",
529
+ location: lockfile
530
+ });
531
+ }
532
+ function blockedUnknownSpec(spec) {
533
+ return packageObservation({
534
+ ecosystem: "unknown",
535
+ name: spec,
536
+ version: null,
537
+ requested: spec,
538
+ sourceKind: "package-spec",
539
+ resolvedUrl: null,
540
+ integrity: null,
541
+ license: null
542
+ }, "block", {
543
+ id: "unsupported-package-spec",
544
+ title: "Unsupported package spec",
545
+ message: "package spec could not be parsed without guessing identity",
546
+ location: spec
547
+ });
548
+ }
549
+ function packageObservation(identity, verdict, finding) {
550
+ return {
551
+ identity,
552
+ verdict,
553
+ finding
554
+ };
555
+ }
556
+ function splitNameVersion(value) {
557
+ const trimmed = value.trim();
558
+ if (trimmed.startsWith("@")) {
559
+ const index = trimmed.lastIndexOf("@");
560
+ if (index <= 0) {
561
+ return {
562
+ name: trimmed,
563
+ version: null
564
+ };
565
+ }
566
+ return {
567
+ name: trimmed.slice(0, index),
568
+ version: trimmed.slice(index + 1) || null
569
+ };
570
+ }
571
+ const index = trimmed.lastIndexOf("@");
572
+ if (index > 0) {
573
+ return {
574
+ name: trimmed.slice(0, index),
575
+ version: trimmed.slice(index + 1) || null
576
+ };
577
+ }
578
+ return {
579
+ name: trimmed,
580
+ version: null
581
+ };
582
+ }
583
+ function isValidPackageName(name) {
584
+ return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/iu.test(name) && !/[\\\s]/u.test(name);
585
+ }
586
+ function isExactVersion(version) {
587
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version);
588
+ }
589
+ function isSupportedIntegrity(value) {
590
+ return /^(?:sha256|sha384|sha512)-[A-Za-z0-9+/=]+$/u.test(value)
591
+ || /^(?:sha256:)?[a-f0-9]{64}$/iu.test(value);
592
+ }
593
+ function isUnsafeResolvedUrl(value) {
594
+ const lower = value.toLowerCase();
595
+ return lower.startsWith("git+")
596
+ || lower.startsWith("git://")
597
+ || lower.startsWith("ssh://")
598
+ || lower.startsWith("github:");
599
+ }
600
+ function strongerVerdict(left, right) {
601
+ if (left === "block" || right === "block") {
602
+ return "block";
603
+ }
604
+ if (left === "warn" || right === "warn") {
605
+ return "warn";
606
+ }
607
+ return "pass";
608
+ }
609
+ function policyActionFor(findings) {
610
+ if (findings.some((finding) => finding.severity === "block")) {
611
+ return "block";
612
+ }
613
+ if (findings.some((finding) => finding.severity === "warn")) {
614
+ return "warn";
615
+ }
616
+ return "pass";
617
+ }
618
+ function statusFor(action, errors) {
619
+ if (errors.length > 0) {
620
+ return "error";
621
+ }
622
+ return action;
623
+ }
624
+ function summarize(findings, errors) {
625
+ return {
626
+ findingCount: findings.length,
627
+ warnCount: findings.filter((finding) => finding.severity === "warn").length,
628
+ blockCount: findings.filter((finding) => finding.severity === "block").length,
629
+ errorCount: errors.length
630
+ };
631
+ }
632
+ function lockfileSummary(packageCount) {
633
+ return {
634
+ advisory: true,
635
+ packageCount,
636
+ identitySource: "lockfile",
637
+ message: "Lockfile verification maps package identity and integrity for preflight only; proxy enforcement remains authoritative for network fetches."
638
+ };
639
+ }
640
+ function packageDisplayName(identity) {
641
+ const version = identity.version ? `@${identity.version}` : "";
642
+ return `${identity.ecosystem}:${identity.name}${version}`;
643
+ }
644
+ function packageNameFromNodeModulesPath(path) {
645
+ const parts = path.split("/");
646
+ const nodeModulesIndex = parts.lastIndexOf("node_modules");
647
+ if (nodeModulesIndex === -1) {
648
+ return null;
649
+ }
650
+ const first = parts[nodeModulesIndex + 1];
651
+ if (!first) {
652
+ return null;
653
+ }
654
+ if (first.startsWith("@") && parts[nodeModulesIndex + 2]) {
655
+ return `${first}/${parts[nodeModulesIndex + 2]}`;
656
+ }
657
+ return first;
658
+ }
659
+ function packageNameFromYarnDescriptor(descriptor) {
660
+ const value = descriptor.startsWith("@") ? descriptor.slice(1) : descriptor;
661
+ const index = value.lastIndexOf("@");
662
+ const name = descriptor.startsWith("@") ? `@${value.slice(0, index)}` : value.slice(0, index);
663
+ return name || null;
664
+ }
665
+ function quotedValue(lines, key) {
666
+ const pattern = new RegExp(`^\\s*${key}\\s+\"?([^\"\\n]+)\"?\\s*$`, "u");
667
+ for (const line of lines) {
668
+ const match = pattern.exec(line);
669
+ if (match?.[1]) {
670
+ return match[1];
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+ function lockBlocks(text, marker) {
676
+ return text.split(marker).slice(1).map((block) => `${marker}${block}`);
677
+ }
678
+ function tomlString(block, key) {
679
+ const match = new RegExp(`^${key}\\s*=\\s*\"([^\"]*)\"`, "mu").exec(block);
680
+ return match?.[1] ?? null;
681
+ }
682
+ function stringOrNull(value) {
683
+ return typeof value === "string" && value.length > 0 ? value : null;
684
+ }
685
+ function stripQuotes(value) {
686
+ return value.replace(/^["']|["']$/gu, "");
687
+ }
688
+ function normalizeLicense(value) {
689
+ return value.trim().toLowerCase();
690
+ }
691
+ function displayPath(root, path) {
692
+ const relativePath = relative(resolve(root), resolve(path));
693
+ const display = relativePath.length === 0 ? "." : relativePath;
694
+ return display.split(sep).join("/");
695
+ }
696
+ function isRecord(value) {
697
+ return typeof value === "object" && value !== null && !Array.isArray(value);
698
+ }