@westbayberry/dg 2.0.7 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +158 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/pip-report.js +18 -2
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
package/dist/launcher/run.js
CHANGED
|
@@ -12,10 +12,57 @@ import { readProxySessionState } from "../proxy/server.js";
|
|
|
12
12
|
import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
|
|
13
13
|
import { classifyPackageManagerInvocation } from "./classify.js";
|
|
14
14
|
import { buildProxyChildEnv } from "./env.js";
|
|
15
|
+
import { cachedPipResolution } from "./install-preflight.js";
|
|
15
16
|
import { parsePipReportInstallCount } from "./pip-report.js";
|
|
16
17
|
import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
|
|
17
18
|
import { resolveRealBinary } from "./resolve-real-binary.js";
|
|
18
19
|
export const EXIT_INSTALL_BLOCKED = 2;
|
|
20
|
+
const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
|
|
21
|
+
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
22
|
+
export function resolveSpawnInvocation(binary, args, platform = process.platform) {
|
|
23
|
+
if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
|
|
24
|
+
return { command: binary, args, windowsVerbatimArguments: false };
|
|
25
|
+
}
|
|
26
|
+
const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
|
|
27
|
+
return {
|
|
28
|
+
command: process.env.comspec ?? "cmd.exe",
|
|
29
|
+
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
30
|
+
windowsVerbatimArguments: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function escapeCmdCommand(command) {
|
|
34
|
+
return command.replace(CMD_META_CHARS, "^$1");
|
|
35
|
+
}
|
|
36
|
+
// cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
|
|
37
|
+
function escapeCmdArgument(argument) {
|
|
38
|
+
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
39
|
+
return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
|
|
40
|
+
}
|
|
41
|
+
export function shimDepth(env) {
|
|
42
|
+
const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
|
|
43
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
44
|
+
}
|
|
45
|
+
export function rootUnprotectedNotice(env, uid = process.getuid?.()) {
|
|
46
|
+
if (uid !== 0) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
if (existsSync(resolveDgPaths(env).stateDir)) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
return "dg: running as root without dg state — bare package-manager installs by root are not protected\n";
|
|
53
|
+
}
|
|
54
|
+
let rootNoticeWritten = false;
|
|
55
|
+
function maybeWarnRootWithoutState(env) {
|
|
56
|
+
if (rootNoticeWritten) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const notice = rootUnprotectedNotice(env);
|
|
60
|
+
if (!notice) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
rootNoticeWritten = true;
|
|
64
|
+
process.stderr.write(notice);
|
|
65
|
+
}
|
|
19
66
|
export function createLaunchPlan(manager, args, env = process.env) {
|
|
20
67
|
const classification = classifyPackageManagerInvocation(manager, args);
|
|
21
68
|
const realBinary = resolveRealBinary({
|
|
@@ -28,7 +75,8 @@ export function createLaunchPlan(manager, args, env = process.env) {
|
|
|
28
75
|
startsProxy: classification.kind === "protected",
|
|
29
76
|
childEnv: {
|
|
30
77
|
...env,
|
|
31
|
-
DG_SHIM_ACTIVE: shimNonce(manager, env)
|
|
78
|
+
DG_SHIM_ACTIVE: shimNonce(manager, env),
|
|
79
|
+
DG_SHIM_DEPTH: String(shimDepth(env) + 1)
|
|
32
80
|
}
|
|
33
81
|
};
|
|
34
82
|
}
|
|
@@ -41,6 +89,23 @@ export async function runPackageManager(manager, args, options = {}) {
|
|
|
41
89
|
if (!plan.realBinary.path) {
|
|
42
90
|
return unavailable(invoked, `real ${plan.classification.realBinaryName} binary was not found outside dg shims`);
|
|
43
91
|
}
|
|
92
|
+
maybeWarnRootWithoutState(options.env ?? process.env);
|
|
93
|
+
const depth = shimDepth(options.env ?? process.env);
|
|
94
|
+
if (depth >= 2) {
|
|
95
|
+
return {
|
|
96
|
+
exitCode: EXIT_UNAVAILABLE,
|
|
97
|
+
stdout: "",
|
|
98
|
+
stderr: `dg: ${manager} shim exec loop detected (DG_SHIM_DEPTH=${depth}) — refusing to re-enter\n`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (depth === 1 && plan.startsProxy) {
|
|
102
|
+
const child = await spawnPackageManager(plan, args, options);
|
|
103
|
+
return {
|
|
104
|
+
exitCode: child.exitCode,
|
|
105
|
+
stdout: streamedOut(child.stdout, options),
|
|
106
|
+
stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
44
109
|
if (plan.startsProxy) {
|
|
45
110
|
if (!options.proxyVerdict) {
|
|
46
111
|
return runWithProductionProxy(plan, args, options);
|
|
@@ -173,9 +238,11 @@ function resolvePipInstallTotal(binary, args, env, register) {
|
|
|
173
238
|
};
|
|
174
239
|
let child;
|
|
175
240
|
try {
|
|
176
|
-
|
|
241
|
+
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
242
|
+
child = spawn(invocation.command, [...invocation.args], {
|
|
177
243
|
env,
|
|
178
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
244
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
245
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
179
246
|
});
|
|
180
247
|
}
|
|
181
248
|
catch {
|
|
@@ -200,6 +267,7 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
200
267
|
throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
|
|
201
268
|
}
|
|
202
269
|
const env = options.env ?? process.env;
|
|
270
|
+
maybeWarnRootWithoutState(env);
|
|
203
271
|
const proxy = await startProxyWorker(plan.classification, env, options.forceOverride);
|
|
204
272
|
if ("decision" in proxy) {
|
|
205
273
|
return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
|
|
@@ -217,9 +285,15 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
217
285
|
let resolvedTotal;
|
|
218
286
|
let dryRunChild;
|
|
219
287
|
if (plan.classification.manager === "pip") {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
288
|
+
const cached = cachedPipResolution(plan.realBinary.path ?? "", args);
|
|
289
|
+
if (cached) {
|
|
290
|
+
resolvedTotal = cached.count;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
void resolvePipInstallTotal(plan.realBinary.path ?? "", args, childEnv, (child) => { dryRunChild = child; })
|
|
294
|
+
.then((count) => { resolvedTotal = count; })
|
|
295
|
+
.catch(() => undefined);
|
|
296
|
+
}
|
|
223
297
|
}
|
|
224
298
|
const poll = setInterval(() => {
|
|
225
299
|
onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
|
|
@@ -292,9 +366,11 @@ async function spawnPackageManager(plan, args, options) {
|
|
|
292
366
|
});
|
|
293
367
|
}
|
|
294
368
|
const defaultSpawner = (request) => new Promise((resolve) => {
|
|
295
|
-
const
|
|
369
|
+
const invocation = resolveSpawnInvocation(request.binary, request.args);
|
|
370
|
+
const child = spawn(invocation.command, [...invocation.args], {
|
|
296
371
|
env: request.env,
|
|
297
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
372
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
373
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
298
374
|
});
|
|
299
375
|
const stdout = [];
|
|
300
376
|
const stderr = [];
|
|
@@ -421,6 +497,9 @@ function stopProxyWorker(proxy) {
|
|
|
421
497
|
function installProxySignalHandlers(proxy) {
|
|
422
498
|
const registrations = ["SIGINT", "SIGTERM"].map((signal) => {
|
|
423
499
|
const handler = () => {
|
|
500
|
+
if (process.stdin.isTTY) {
|
|
501
|
+
process.stdin.setRawMode(false);
|
|
502
|
+
}
|
|
424
503
|
stopProxyWorker(proxy);
|
|
425
504
|
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
426
505
|
};
|
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,6 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { parseLockfilePackages } from "../verify/preflight.js";
|
|
4
4
|
export const LOCKFILE_ECOSYSTEMS = {
|
|
5
5
|
"package-lock.json": "npm",
|
|
6
6
|
"npm-shrinkwrap.json": "npm",
|
|
@@ -8,6 +8,7 @@ export const LOCKFILE_ECOSYSTEMS = {
|
|
|
8
8
|
"pnpm-lock.yaml": "npm",
|
|
9
9
|
"Pipfile.lock": "pypi",
|
|
10
10
|
"poetry.lock": "pypi",
|
|
11
|
+
"uv.lock": "pypi",
|
|
11
12
|
"requirements.txt": "pypi"
|
|
12
13
|
};
|
|
13
14
|
export function isLockfileName(name) {
|
|
@@ -34,13 +35,19 @@ function readDirents(directory) {
|
|
|
34
35
|
return [];
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
function
|
|
38
|
+
function lockfilesPerEcosystem(entries) {
|
|
39
|
+
const matches = [];
|
|
40
|
+
const claimed = new Set();
|
|
38
41
|
for (const [lockfile, ecosystem] of Object.entries(LOCKFILE_ECOSYSTEMS)) {
|
|
42
|
+
if (claimed.has(ecosystem)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
39
45
|
if (entries.some((entry) => entry.name === lockfile && entry.isFile())) {
|
|
40
|
-
|
|
46
|
+
matches.push([lockfile, ecosystem]);
|
|
47
|
+
claimed.add(ecosystem);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
|
-
return
|
|
50
|
+
return matches;
|
|
44
51
|
}
|
|
45
52
|
function shouldDescend(entry) {
|
|
46
53
|
return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith(".");
|
|
@@ -54,14 +61,13 @@ export function discoverScanProjects(root) {
|
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
63
|
const entries = readDirents(directory);
|
|
57
|
-
const
|
|
58
|
-
if (match) {
|
|
64
|
+
for (const [depFile, ecosystem] of lockfilesPerEcosystem(entries)) {
|
|
59
65
|
projects.push({
|
|
60
66
|
path: directory,
|
|
61
67
|
relativePath: relative(root, directory) || ".",
|
|
62
|
-
ecosystem
|
|
63
|
-
depFile
|
|
64
|
-
packageCount: countLockfilePackages(join(directory,
|
|
68
|
+
ecosystem,
|
|
69
|
+
depFile,
|
|
70
|
+
packageCount: countLockfilePackages(join(directory, depFile))
|
|
65
71
|
});
|
|
66
72
|
}
|
|
67
73
|
for (const entry of entries) {
|
|
@@ -85,17 +91,16 @@ export async function discoverScanProjectsAsync(root, onProgress) {
|
|
|
85
91
|
lastYield = Date.now();
|
|
86
92
|
}
|
|
87
93
|
const entries = readDirents(directory);
|
|
88
|
-
const
|
|
89
|
-
if (match) {
|
|
94
|
+
for (const [depFile, ecosystem] of lockfilesPerEcosystem(entries)) {
|
|
90
95
|
const relativePath = relative(root, directory) || ".";
|
|
91
96
|
projects.push({
|
|
92
97
|
path: directory,
|
|
93
98
|
relativePath,
|
|
94
|
-
ecosystem
|
|
95
|
-
depFile
|
|
96
|
-
packageCount: countLockfilePackages(join(directory,
|
|
99
|
+
ecosystem,
|
|
100
|
+
depFile,
|
|
101
|
+
packageCount: countLockfilePackages(join(directory, depFile))
|
|
97
102
|
});
|
|
98
|
-
onProgress?.({ path: relativePath === "." ?
|
|
103
|
+
onProgress?.({ path: relativePath === "." ? depFile : `${relativePath}/${depFile}`, found: projects.length });
|
|
99
104
|
}
|
|
100
105
|
for (const entry of entries) {
|
|
101
106
|
if (shouldDescend(entry)) {
|
|
@@ -110,27 +115,29 @@ function yieldToEventLoop() {
|
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
117
|
function countLockfilePackages(lockfilePath) {
|
|
113
|
-
|
|
114
|
-
return verifyLockfile(lockfilePath).packages.length;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
118
|
+
return parseLockfilePackages(lockfilePath).packages.length;
|
|
119
119
|
}
|
|
120
120
|
export function collectScanPackages(projects) {
|
|
121
121
|
const byEcosystem = new Map();
|
|
122
122
|
const seen = new Set();
|
|
123
|
+
const skippedPackages = [];
|
|
124
|
+
const parseErrors = [];
|
|
123
125
|
let skipped = 0;
|
|
124
126
|
for (const project of projects) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const depFilePath = project.relativePath === "." ? project.depFile : `${project.relativePath}/${project.depFile}`;
|
|
128
|
+
const parsed = parseLockfilePackages(join(project.path, project.depFile));
|
|
129
|
+
for (const skippedPackage of parsed.skipped) {
|
|
130
|
+
skippedPackages.push({ ...skippedPackage, location: `${depFilePath}: ${skippedPackage.location}` });
|
|
131
|
+
skipped += 1;
|
|
128
132
|
}
|
|
129
|
-
|
|
133
|
+
if (parsed.parseError) {
|
|
134
|
+
parseErrors.push({
|
|
135
|
+
file: parsed.parseError.file === project.depFile ? depFilePath : parsed.parseError.file,
|
|
136
|
+
reason: parsed.parseError.reason
|
|
137
|
+
});
|
|
130
138
|
skipped += 1;
|
|
131
|
-
continue;
|
|
132
139
|
}
|
|
133
|
-
for (const identity of
|
|
140
|
+
for (const identity of parsed.packages) {
|
|
134
141
|
if (identity.ecosystem !== "npm" && identity.ecosystem !== "pypi") {
|
|
135
142
|
skipped += 1;
|
|
136
143
|
continue;
|
|
@@ -149,5 +156,5 @@ export function collectScanPackages(projects) {
|
|
|
149
156
|
byEcosystem.set(identity.ecosystem, list);
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
|
-
return { byEcosystem, skipped };
|
|
159
|
+
return { byEcosystem, skipped, skippedPackages, parseErrors };
|
|
153
160
|
}
|