@westbayberry/dg 2.0.8 → 2.0.11

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 (50) hide show
  1. package/README.md +17 -12
  2. package/dist/api/analyze.js +134 -34
  3. package/dist/audit-ui/export.js +3 -4
  4. package/dist/auth/device-login.js +13 -9
  5. package/dist/auth/store.js +43 -26
  6. package/dist/bin/dg.js +5 -0
  7. package/dist/commands/audit.js +14 -4
  8. package/dist/commands/config.js +3 -5
  9. package/dist/commands/doctor.js +3 -3
  10. package/dist/commands/explain.js +138 -6
  11. package/dist/commands/licenses.js +37 -24
  12. package/dist/commands/login.js +12 -3
  13. package/dist/commands/logout.js +15 -4
  14. package/dist/commands/scan.js +1 -1
  15. package/dist/commands/service.js +76 -24
  16. package/dist/commands/status.js +38 -4
  17. package/dist/commands/types.js +1 -0
  18. package/dist/config/settings.js +102 -22
  19. package/dist/launcher/install-preflight.js +81 -12
  20. package/dist/launcher/output-redaction.js +5 -3
  21. package/dist/launcher/preflight-prompt.js +31 -12
  22. package/dist/launcher/run.js +87 -8
  23. package/dist/proxy/ca.js +69 -29
  24. package/dist/proxy/enforcement.js +41 -3
  25. package/dist/proxy/worker.js +21 -9
  26. package/dist/runtime/first-run.js +33 -2
  27. package/dist/runtime/nudges.js +9 -2
  28. package/dist/scan/analyze-worker.js +18 -8
  29. package/dist/scan/collect.js +45 -32
  30. package/dist/scan/command.js +80 -40
  31. package/dist/scan/discovery.js +75 -7
  32. package/dist/scan/render.js +22 -6
  33. package/dist/scan/scanner-report.js +89 -12
  34. package/dist/scan/staged.js +69 -7
  35. package/dist/scan-ui/LegacyApp.js +10 -48
  36. package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
  37. package/dist/scan-ui/components/ProjectSelector.js +3 -3
  38. package/dist/scan-ui/components/ScoreHeader.js +8 -4
  39. package/dist/scan-ui/hooks/useScan.js +74 -27
  40. package/dist/scan-ui/launch.js +21 -4
  41. package/dist/service/state.js +15 -4
  42. package/dist/service/trust-store.js +23 -2
  43. package/dist/setup/git-hook.js +28 -17
  44. package/dist/setup/plan.js +302 -18
  45. package/dist/state/cleanup-registry.js +65 -8
  46. package/dist/state/locks.js +95 -9
  47. package/dist/state/sessions.js +66 -2
  48. package/dist/verify/package-check.js +22 -3
  49. package/dist/verify/preflight.js +328 -170
  50. package/package.json +1 -1
package/dist/proxy/ca.js CHANGED
@@ -1,9 +1,43 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { isIP } from "node:net";
5
5
  import forge from "node-forge";
6
- export function createEphemeralCertificateAuthority(caPath) {
6
+ export const CA_LIFETIME_MS = 24 * 60 * 60 * 1_000;
7
+ export const CA_ROTATE_AFTER_FRACTION = 0.75;
8
+ const ROTATION_CHECK_INTERVAL_MS = 60_000;
9
+ export function createEphemeralCertificateAuthority(caPath, options = {}) {
10
+ const lifetimeMs = options.lifetimeMs ?? CA_LIFETIME_MS;
11
+ const now = options.now ?? Date.now;
12
+ let active = issueAuthority(caPath, lifetimeMs, now(), undefined);
13
+ const leafs = new Map();
14
+ const rotateIfDue = () => {
15
+ if (now() < active.rotateAtMs) {
16
+ return;
17
+ }
18
+ active = issueAuthority(caPath, lifetimeMs, now(), active.certPem);
19
+ leafs.clear();
20
+ };
21
+ setInterval(rotateIfDue, ROTATION_CHECK_INTERVAL_MS).unref();
22
+ return {
23
+ get caCertPem() {
24
+ return active.certPem;
25
+ },
26
+ caPath,
27
+ leafForHost(host) {
28
+ rotateIfDue();
29
+ const normalized = normalizeHost(host);
30
+ const existing = leafs.get(normalized);
31
+ if (existing) {
32
+ return existing;
33
+ }
34
+ const leaf = createLeafCertificate(normalized, active, lifetimeMs, now());
35
+ leafs.set(normalized, leaf);
36
+ return leaf;
37
+ }
38
+ };
39
+ }
40
+ function issueAuthority(caPath, lifetimeMs, nowMs, previousCertPem) {
7
41
  const keys = forge.pki.rsa.generateKeyPair({
8
42
  bits: 2048,
9
43
  workers: -1
@@ -11,8 +45,8 @@ export function createEphemeralCertificateAuthority(caPath) {
11
45
  const cert = forge.pki.createCertificate();
12
46
  cert.publicKey = keys.publicKey;
13
47
  cert.serialNumber = serialNumber();
14
- cert.validity.notBefore = new Date(Date.now() - 60_000);
15
- cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1_000);
48
+ cert.validity.notBefore = new Date(nowMs - 60_000);
49
+ cert.validity.notAfter = new Date(nowMs + lifetimeMs);
16
50
  const attrs = [{
17
51
  name: "commonName",
18
52
  value: "Dependency Guardian per-session proxy CA"
@@ -36,32 +70,38 @@ export function createEphemeralCertificateAuthority(caPath) {
36
70
  }
37
71
  ]);
38
72
  cert.sign(keys.privateKey, forge.md.sha256.create());
39
- const caCertPem = forge.pki.certificateToPem(cert);
73
+ const certPem = forge.pki.certificateToPem(cert);
74
+ writeCaBundleAtomic(caPath, previousCertPem ? `${certPem}${previousCertPem}` : certPem);
75
+ return {
76
+ cert,
77
+ privateKey: keys.privateKey,
78
+ certPem,
79
+ notAfterMs: nowMs + lifetimeMs,
80
+ rotateAtMs: nowMs + Math.floor(lifetimeMs * CA_ROTATE_AFTER_FRACTION)
81
+ };
82
+ }
83
+ function writeCaBundleAtomic(caPath, bundle) {
40
84
  mkdirSync(dirname(caPath), {
41
85
  recursive: true,
42
86
  mode: 0o700
43
87
  });
44
- writeFileSync(caPath, caCertPem, {
45
- encoding: "utf8",
46
- mode: 0o600
47
- });
48
- const leafs = new Map();
49
- return {
50
- caCertPem,
51
- caPath,
52
- leafForHost(host) {
53
- const normalized = normalizeHost(host);
54
- const existing = leafs.get(normalized);
55
- if (existing) {
56
- return existing;
57
- }
58
- const leaf = createLeafCertificate(normalized, cert, keys.privateKey);
59
- leafs.set(normalized, leaf);
60
- return leaf;
61
- }
62
- };
88
+ const tempPath = `${caPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
89
+ try {
90
+ writeFileSync(tempPath, bundle, {
91
+ encoding: "utf8",
92
+ flag: "wx",
93
+ mode: 0o600
94
+ });
95
+ renameSync(tempPath, caPath);
96
+ }
97
+ catch (error) {
98
+ rmSync(tempPath, {
99
+ force: true
100
+ });
101
+ throw error;
102
+ }
63
103
  }
64
- function createLeafCertificate(host, issuerCert, issuerKey) {
104
+ function createLeafCertificate(host, issuer, lifetimeMs, nowMs) {
65
105
  const keys = forge.pki.rsa.generateKeyPair({
66
106
  bits: 2048,
67
107
  workers: -1
@@ -69,13 +109,13 @@ function createLeafCertificate(host, issuerCert, issuerKey) {
69
109
  const cert = forge.pki.createCertificate();
70
110
  cert.publicKey = keys.publicKey;
71
111
  cert.serialNumber = serialNumber();
72
- cert.validity.notBefore = new Date(Date.now() - 60_000);
73
- cert.validity.notAfter = new Date(Date.now() + 24 * 60 * 60 * 1_000);
112
+ cert.validity.notBefore = new Date(nowMs - 60_000);
113
+ cert.validity.notAfter = new Date(Math.min(nowMs + lifetimeMs, issuer.notAfterMs));
74
114
  cert.setSubject([{
75
115
  name: "commonName",
76
116
  value: host
77
117
  }]);
78
- cert.setIssuer(issuerCert.subject.attributes);
118
+ cert.setIssuer(issuer.cert.subject.attributes);
79
119
  cert.setExtensions([
80
120
  {
81
121
  name: "basicConstraints",
@@ -97,7 +137,7 @@ function createLeafCertificate(host, issuerCert, issuerKey) {
97
137
  altNames: subjectAlternativeNames(host)
98
138
  }
99
139
  ]);
100
- cert.sign(issuerKey, forge.md.sha256.create());
140
+ cert.sign(issuer.privateKey, forge.md.sha256.create());
101
141
  return {
102
142
  certPem: forge.pki.certificateToPem(cert),
103
143
  keyPem: forge.pki.privateKeyToPem(keys.privateKey)
@@ -83,9 +83,47 @@ function failClosedVerdict(classification) {
83
83
  reason: "per-invocation proxy enforcement is not available, so protected installs fail closed"
84
84
  };
85
85
  }
86
- function derivePackageName(classification) {
87
- const spec = classification.args.find((arg) => !arg.startsWith("-") && arg !== classification.action);
88
- return spec ?? `${classification.manager}:${classification.action || "install"}`;
86
+ const VALUE_CONSUMING_FLAGS = {
87
+ javascript: new Set([
88
+ "--registry", "--userconfig", "--globalconfig", "--cache", "--prefix", "--loglevel",
89
+ "--tag", "--workspace", "-w", "--filter", "-C", "--dir", "--cwd", "--modules-folder",
90
+ "--otp", "--script-shell", "--network-concurrency", "--mutex"
91
+ ]),
92
+ python: new Set([
93
+ "--index-url", "-i", "--extra-index-url", "--find-links", "-f", "--trusted-host",
94
+ "--proxy", "--requirement", "-r", "--constraint", "-c", "--target", "-t", "--prefix",
95
+ "--root", "--src", "--platform", "--python", "--python-version", "--implementation",
96
+ "--abi", "--cache-dir", "--timeout", "--retries", "--cert", "--client-cert", "--log",
97
+ "--progress-bar", "--upgrade-strategy", "--no-binary", "--only-binary", "--report",
98
+ "--editable", "-e", "--index", "--default-index"
99
+ ]),
100
+ rust: new Set([
101
+ "--registry", "--index", "--git", "--branch", "--tag", "--rev", "--path", "--vers",
102
+ "--version", "--features", "-F", "--package", "-p", "--manifest-path", "--target-dir",
103
+ "--profile", "--jobs", "-j", "--target", "--config", "-Z"
104
+ ]),
105
+ gated: new Set()
106
+ };
107
+ export function derivePackageName(classification) {
108
+ const valueFlags = VALUE_CONSUMING_FLAGS[classification.ecosystem];
109
+ const args = classification.args;
110
+ for (let index = 0; index < args.length; index += 1) {
111
+ const arg = args[index];
112
+ if (!arg) {
113
+ continue;
114
+ }
115
+ if (arg.startsWith("-")) {
116
+ if (valueFlags.has(arg)) {
117
+ index += 1;
118
+ }
119
+ continue;
120
+ }
121
+ if (arg === classification.action) {
122
+ continue;
123
+ }
124
+ return arg;
125
+ }
126
+ return `${classification.manager}:${classification.action || "install"}`;
89
127
  }
90
128
  function causeFromVerdict(verdict, action) {
91
129
  if (verdict === "pass" && action === "pass") {
@@ -1,6 +1,8 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { startProductionHttpProxy } from "./server.js";
3
3
  import { parseForceOverrideRequest } from "./enforcement.js";
4
+ import { cleanupSessionSync } from "../state/index.js";
5
+ const PARENT_POLL_MS = 500;
4
6
  const sessionPath = process.argv[2];
5
7
  const apiBaseUrl = process.argv[3];
6
8
  const classificationJson = process.env.DG_PROXY_CLASSIFICATION;
@@ -11,6 +13,10 @@ if (!sessionPath || !apiBaseUrl || !classificationJson) {
11
13
  const session = JSON.parse(readFileSync(sessionPath, "utf8"));
12
14
  const classification = JSON.parse(classificationJson);
13
15
  const forceOverride = parseForceOverrideRequest(process.env.DG_FORCE_OVERRIDE_REQUEST);
16
+ writeFileSync(session.files.pid, `${process.pid}\n`, {
17
+ encoding: "utf8",
18
+ mode: 0o600
19
+ });
14
20
  let handle = null;
15
21
  let closed = false;
16
22
  async function close() {
@@ -19,16 +25,22 @@ async function close() {
19
25
  }
20
26
  closed = true;
21
27
  await handle?.close();
28
+ cleanupSessionSync(session);
22
29
  }
23
- process.stdin.on("end", () => {
24
- close().finally(() => process.exit(0));
25
- });
26
- process.on("SIGTERM", () => {
27
- close().finally(() => process.exit(0));
28
- });
29
- process.on("SIGINT", () => {
30
+ function shutdown() {
30
31
  close().finally(() => process.exit(0));
31
- });
32
+ }
33
+ process.stdin.resume();
34
+ process.stdin.on("end", shutdown);
35
+ process.stdin.on("error", shutdown);
36
+ process.on("SIGTERM", shutdown);
37
+ process.on("SIGINT", shutdown);
38
+ const parentWatch = setInterval(() => {
39
+ if (process.ppid === 1) {
40
+ shutdown();
41
+ }
42
+ }, PARENT_POLL_MS);
43
+ parentWatch.unref();
32
44
  handle = await startProductionHttpProxy({
33
45
  session,
34
46
  apiBaseUrl,
@@ -1,7 +1,9 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { dgVersion } from "../commands/version.js";
3
4
  import { isCiEnv } from "../presentation/mode.js";
4
5
  import { createTheme } from "../presentation/theme.js";
6
+ import { sweepLegacyPythonHooks } from "../setup/plan.js";
5
7
  import { resolveDgPaths } from "../state/index.js";
6
8
  const SKIP_COMMANDS = new Set([
7
9
  "help",
@@ -18,17 +20,46 @@ const SKIP_COMMANDS = new Set([
18
20
  "upgrade",
19
21
  "uninstall"
20
22
  ]);
23
+ const MACHINE_OUTPUT_FLAGS = new Set([
24
+ "--json",
25
+ "--sarif",
26
+ "--csv",
27
+ "--markdown",
28
+ "--output",
29
+ "-o",
30
+ "--quiet"
31
+ ]);
21
32
  export function firstRunMarkerPath(env = process.env) {
22
33
  return join(resolveDgPaths(env).stateDir, "first-run-shown");
23
34
  }
35
+ export function lastRunVersionMarkerPath(env = process.env) {
36
+ return join(resolveDgPaths(env).stateDir, "last-run-version");
37
+ }
38
+ export function sweepLegacyHooksOnVersionChange(env = process.env, version = dgVersion()) {
39
+ try {
40
+ const marker = lastRunVersionMarkerPath(env);
41
+ const recorded = existsSync(marker) ? readFileSync(marker, "utf8").trim() : undefined;
42
+ if (recorded === version) {
43
+ return false;
44
+ }
45
+ sweepLegacyPythonHooks(resolveDgPaths(env).homeDir, [], []);
46
+ mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
47
+ writeFileSync(marker, `${version}\n`, { encoding: "utf8", mode: 0o600 });
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
24
54
  export function maybeShowFirstRun(args, options = {}) {
25
55
  const env = options.env ?? process.env;
26
56
  const stderr = options.stderr ?? process.stderr;
27
57
  const command = args[0] ?? "";
58
+ sweepLegacyHooksOnVersionChange(env);
28
59
  if (!stderr.isTTY || isCiEnv(env)) {
29
60
  return false;
30
61
  }
31
- if (SKIP_COMMANDS.has(command) || args.some((arg) => arg === "--json" || arg === "--sarif" || arg === "--quiet")) {
62
+ if (SKIP_COMMANDS.has(command) || args.some((arg) => MACHINE_OUTPUT_FLAGS.has(arg))) {
32
63
  return false;
33
64
  }
34
65
  const marker = firstRunMarkerPath(env);
@@ -46,11 +46,11 @@ export function maybeShowNudges(args, options = {}) {
46
46
  const lines = [];
47
47
  let stateChanged = false;
48
48
  if (due(state.updateCheckedAt, UPDATE_THROTTLE_MS, now)) {
49
- state.updateCheckedAt = now.toISOString();
50
- stateChanged = true;
51
49
  const latest = readLatestVersion(LATEST_LOOKUP_TIMEOUT_MS);
52
50
  if (latest) {
51
+ state.updateCheckedAt = now.toISOString();
53
52
  state.updateLatest = latest;
53
+ stateChanged = true;
54
54
  if (compareVersions(latest, dgVersion()) > 0) {
55
55
  lines.push(`${theme.paint("warn", "⚠")} ${theme.paint("muted", `Update available: ${dgVersion()} → ${latest}. Run`)} ${theme.paint("accent", "dg update")}${theme.paint("muted", ".")}`);
56
56
  }
@@ -68,6 +68,13 @@ export function maybeShowNudges(args, options = {}) {
68
68
  writeState(statePath, state);
69
69
  }
70
70
  }
71
+ export function pendingUpdate(env = process.env) {
72
+ const latest = readState(nudgeStatePath(env)).updateLatest;
73
+ if (!latest || compareVersions(latest, dgVersion()) <= 0) {
74
+ return null;
75
+ }
76
+ return { current: dgVersion(), latest };
77
+ }
71
78
  function isAuthenticated(env) {
72
79
  try {
73
80
  return authStatus(env).authenticated;
@@ -1,21 +1,31 @@
1
- import { analyzePackages, mergeAnalyzeResponses } from "../api/analyze.js";
1
+ import { analyzePackages, mergeAnalyzeResponses, scannerErrorFromUnknown } from "../api/analyze.js";
2
+ async function readStdin() {
3
+ process.stdin.setEncoding("utf8");
4
+ let raw = "";
5
+ for await (const chunk of process.stdin) {
6
+ raw += chunk;
7
+ }
8
+ return raw;
9
+ }
2
10
  async function main() {
3
- const raw = process.argv[2];
11
+ const raw = (await readStdin()).trim();
4
12
  if (!raw) {
5
- process.stderr.write("analyze-worker: missing input payload\n");
6
- process.exit(2);
13
+ throw new Error("analyze-worker: missing input payload");
7
14
  }
8
- const groups = JSON.parse(raw);
15
+ const payload = JSON.parse(raw);
9
16
  const responses = [];
10
- for (const group of groups) {
17
+ for (const group of payload.groups) {
11
18
  if (group.packages.length === 0) {
12
19
  continue;
13
20
  }
14
- responses.push(await analyzePackages(group.packages, { ecosystem: group.ecosystem }));
21
+ responses.push(await analyzePackages(group.packages, {
22
+ ecosystem: group.ecosystem,
23
+ ...(payload.scanId ? { scanId: payload.scanId } : {})
24
+ }));
15
25
  }
16
26
  process.stdout.write(JSON.stringify(mergeAnalyzeResponses(responses)));
17
27
  }
18
28
  main().catch((error) => {
19
- process.stderr.write(`analyze-worker: ${error instanceof Error ? error.message : "unknown error"}\n`);
29
+ process.stdout.write(JSON.stringify({ scannerError: scannerErrorFromUnknown(error) }));
20
30
  process.exit(1);
21
31
  });
@@ -1,6 +1,7 @@
1
1
  import { readdirSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
- import { verifyLockfile } from "../verify/preflight.js";
3
+ import { gitIgnoredDirectories } from "./discovery.js";
4
+ import { parseLockfilePackages } from "../verify/preflight.js";
4
5
  export const LOCKFILE_ECOSYSTEMS = {
5
6
  "package-lock.json": "npm",
6
7
  "npm-shrinkwrap.json": "npm",
@@ -8,6 +9,7 @@ export const LOCKFILE_ECOSYSTEMS = {
8
9
  "pnpm-lock.yaml": "npm",
9
10
  "Pipfile.lock": "pypi",
10
11
  "poetry.lock": "pypi",
12
+ "uv.lock": "pypi",
11
13
  "requirements.txt": "pypi"
12
14
  };
13
15
  export function isLockfileName(name) {
@@ -34,19 +36,29 @@ function readDirents(directory) {
34
36
  return [];
35
37
  }
36
38
  }
37
- function firstLockfile(entries) {
39
+ function lockfilesPerEcosystem(entries) {
40
+ const matches = [];
41
+ const claimed = new Set();
38
42
  for (const [lockfile, ecosystem] of Object.entries(LOCKFILE_ECOSYSTEMS)) {
43
+ if (claimed.has(ecosystem)) {
44
+ continue;
45
+ }
39
46
  if (entries.some((entry) => entry.name === lockfile && entry.isFile())) {
40
- return [lockfile, ecosystem];
47
+ matches.push([lockfile, ecosystem]);
48
+ claimed.add(ecosystem);
41
49
  }
42
50
  }
43
- return null;
51
+ return matches;
44
52
  }
45
- function shouldDescend(entry) {
46
- return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith(".");
53
+ function shouldDescend(entry, directory, gitIgnored) {
54
+ return (entry.isDirectory() &&
55
+ !IGNORED_DIRECTORIES.has(entry.name) &&
56
+ !entry.name.startsWith(".") &&
57
+ !gitIgnored.has(join(directory, entry.name)));
47
58
  }
48
59
  export function discoverScanProjects(root) {
49
60
  const projects = [];
61
+ const gitIgnored = gitIgnoredDirectories(root);
50
62
  walk(root, 0);
51
63
  return projects;
52
64
  function walk(directory, depth) {
@@ -54,18 +66,17 @@ export function discoverScanProjects(root) {
54
66
  return;
55
67
  }
56
68
  const entries = readDirents(directory);
57
- const match = firstLockfile(entries);
58
- if (match) {
69
+ for (const [depFile, ecosystem] of lockfilesPerEcosystem(entries)) {
59
70
  projects.push({
60
71
  path: directory,
61
72
  relativePath: relative(root, directory) || ".",
62
- ecosystem: match[1],
63
- depFile: match[0],
64
- packageCount: countLockfilePackages(join(directory, match[0]))
73
+ ecosystem,
74
+ depFile,
75
+ packageCount: countLockfilePackages(join(directory, depFile))
65
76
  });
66
77
  }
67
78
  for (const entry of entries) {
68
- if (shouldDescend(entry)) {
79
+ if (shouldDescend(entry, directory, gitIgnored)) {
69
80
  walk(join(directory, entry.name), depth + 1);
70
81
  }
71
82
  }
@@ -73,6 +84,7 @@ export function discoverScanProjects(root) {
73
84
  }
74
85
  export async function discoverScanProjectsAsync(root, onProgress) {
75
86
  const projects = [];
87
+ const gitIgnored = gitIgnoredDirectories(root);
76
88
  let lastYield = Date.now();
77
89
  await walk(root, 0);
78
90
  return projects;
@@ -85,20 +97,19 @@ export async function discoverScanProjectsAsync(root, onProgress) {
85
97
  lastYield = Date.now();
86
98
  }
87
99
  const entries = readDirents(directory);
88
- const match = firstLockfile(entries);
89
- if (match) {
100
+ for (const [depFile, ecosystem] of lockfilesPerEcosystem(entries)) {
90
101
  const relativePath = relative(root, directory) || ".";
91
102
  projects.push({
92
103
  path: directory,
93
104
  relativePath,
94
- ecosystem: match[1],
95
- depFile: match[0],
96
- packageCount: countLockfilePackages(join(directory, match[0]))
105
+ ecosystem,
106
+ depFile,
107
+ packageCount: countLockfilePackages(join(directory, depFile))
97
108
  });
98
- onProgress?.({ path: relativePath === "." ? match[0] : `${relativePath}/${match[0]}`, found: projects.length });
109
+ onProgress?.({ path: relativePath === "." ? depFile : `${relativePath}/${depFile}`, found: projects.length });
99
110
  }
100
111
  for (const entry of entries) {
101
- if (shouldDescend(entry)) {
112
+ if (shouldDescend(entry, directory, gitIgnored)) {
102
113
  await walk(join(directory, entry.name), depth + 1);
103
114
  }
104
115
  }
@@ -110,27 +121,29 @@ function yieldToEventLoop() {
110
121
  });
111
122
  }
112
123
  function countLockfilePackages(lockfilePath) {
113
- try {
114
- return verifyLockfile(lockfilePath).packages.length;
115
- }
116
- catch {
117
- return 0;
118
- }
124
+ return parseLockfilePackages(lockfilePath).packages.length;
119
125
  }
120
126
  export function collectScanPackages(projects) {
121
127
  const byEcosystem = new Map();
122
128
  const seen = new Set();
129
+ const skippedPackages = [];
130
+ const parseErrors = [];
123
131
  let skipped = 0;
124
132
  for (const project of projects) {
125
- let identities;
126
- try {
127
- identities = verifyLockfile(join(project.path, project.depFile)).packages;
133
+ const depFilePath = project.relativePath === "." ? project.depFile : `${project.relativePath}/${project.depFile}`;
134
+ const parsed = parseLockfilePackages(join(project.path, project.depFile));
135
+ for (const skippedPackage of parsed.skipped) {
136
+ skippedPackages.push({ ...skippedPackage, location: `${depFilePath}: ${skippedPackage.location}` });
137
+ skipped += 1;
128
138
  }
129
- catch {
139
+ if (parsed.parseError) {
140
+ parseErrors.push({
141
+ file: parsed.parseError.file === project.depFile ? depFilePath : parsed.parseError.file,
142
+ reason: parsed.parseError.reason
143
+ });
130
144
  skipped += 1;
131
- continue;
132
145
  }
133
- for (const identity of identities) {
146
+ for (const identity of parsed.packages) {
134
147
  if (identity.ecosystem !== "npm" && identity.ecosystem !== "pypi") {
135
148
  skipped += 1;
136
149
  continue;
@@ -149,5 +162,5 @@ export function collectScanPackages(projects) {
149
162
  byEcosystem.set(identity.ecosystem, list);
150
163
  }
151
164
  }
152
- return { byEcosystem, skipped };
165
+ return { byEcosystem, skipped, skippedPackages, parseErrors };
153
166
  }