@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.
- 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 +45 -32
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +75 -7
- 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 +21 -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/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
|
|
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(
|
|
15
|
-
cert.validity.notAfter = new Date(
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
caPath
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
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(
|
|
73
|
-
cert.validity.notAfter = new Date(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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") {
|
package/dist/proxy/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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);
|
package/dist/runtime/nudges.js
CHANGED
|
@@ -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 =
|
|
11
|
+
const raw = (await readStdin()).trim();
|
|
4
12
|
if (!raw) {
|
|
5
|
-
|
|
6
|
-
process.exit(2);
|
|
13
|
+
throw new Error("analyze-worker: missing input payload");
|
|
7
14
|
}
|
|
8
|
-
const
|
|
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, {
|
|
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.
|
|
29
|
+
process.stdout.write(JSON.stringify({ scannerError: scannerErrorFromUnknown(error) }));
|
|
20
30
|
process.exit(1);
|
|
21
31
|
});
|
package/dist/scan/collect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
47
|
+
matches.push([lockfile, ecosystem]);
|
|
48
|
+
claimed.add(ecosystem);
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
|
-
return
|
|
51
|
+
return matches;
|
|
44
52
|
}
|
|
45
|
-
function shouldDescend(entry) {
|
|
46
|
-
return entry.isDirectory() &&
|
|
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
|
|
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
|
|
63
|
-
depFile
|
|
64
|
-
packageCount: countLockfilePackages(join(directory,
|
|
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
|
|
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
|
|
95
|
-
depFile
|
|
96
|
-
packageCount: countLockfilePackages(join(directory,
|
|
105
|
+
ecosystem,
|
|
106
|
+
depFile,
|
|
107
|
+
packageCount: countLockfilePackages(join(directory, depFile))
|
|
97
108
|
});
|
|
98
|
-
onProgress?.({ path: relativePath === "." ?
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|