@westbayberry/dg 2.3.3 → 2.3.4
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 +94 -217
- package/dist/agents/copilot-cli.js +7 -2
- package/dist/agents/persistence.js +83 -8
- package/dist/agents/registry.js +5 -2
- package/dist/agents/routing.js +31 -14
- package/dist/api/analyze.js +138 -41
- package/dist/audit/deep.js +2 -1
- package/dist/audit/detectors.js +23 -2
- package/dist/audit/rules.js +4 -1
- package/dist/audit-ui/export.js +5 -4
- package/dist/auth/device-login.js +33 -11
- package/dist/auth/login-app.js +17 -12
- package/dist/auth/store.js +65 -9
- package/dist/bin/dg.js +1 -1
- package/dist/commands/audit.js +12 -20
- package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
- package/dist/commands/cooldown.js +2 -1
- package/dist/commands/decisions.js +8 -7
- package/dist/commands/help.js +11 -3
- package/dist/commands/licenses.js +17 -8
- package/dist/commands/login.js +58 -21
- package/dist/commands/router.js +12 -2
- package/dist/commands/sbom.js +20 -5
- package/dist/commands/scan.js +5 -4
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +12 -12
- package/dist/commands/status.js +4 -4
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/update.js +20 -0
- package/dist/commands/verify.js +4 -4
- package/dist/config/settings.js +81 -12
- package/dist/export-ui/ExportDialog.js +1 -1
- package/dist/gate/cooldown-request.js +22 -0
- package/dist/gate/egress-guard.js +185 -0
- package/dist/gate/host-match.js +48 -0
- package/dist/gate/verdict-core.js +101 -0
- package/dist/gate/verdict-fetch.js +214 -0
- package/dist/launcher/agent-check.js +1096 -42
- package/dist/launcher/agent-hook-exec.js +8 -1
- package/dist/launcher/agent-hook-io.js +5 -3
- package/dist/launcher/classify.js +110 -26
- package/dist/launcher/env.js +84 -18
- package/dist/launcher/install-preflight.js +26 -3
- package/dist/launcher/live-install.js +4 -2
- package/dist/launcher/manifest-screen.js +88 -7
- package/dist/launcher/output-redaction.js +153 -15
- package/dist/launcher/preflight-prompt.js +1 -1
- package/dist/launcher/resolve-real-binary.js +8 -3
- package/dist/launcher/run.js +237 -40
- package/dist/origin/artifact-cache.js +55 -0
- package/dist/origin/cargo-config.js +12 -0
- package/dist/origin/gate-mode.js +7 -0
- package/dist/origin/platform-paths.js +14 -0
- package/dist/origin/precedence.js +345 -0
- package/dist/origin/server.js +611 -0
- package/dist/origin/spike-worker.js +35 -0
- package/dist/origin/worker.js +116 -0
- package/dist/presentation/package-page.js +21 -1
- package/dist/presentation/safe-version.js +11 -0
- package/dist/presentation/sarif.js +109 -0
- package/dist/project/dgfile.js +54 -17
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/enforcement.js +27 -7
- package/dist/proxy/metadata-map.js +33 -7
- package/dist/proxy/preverified.js +0 -3
- package/dist/proxy/server.js +104 -446
- package/dist/proxy/upstream-proxy.js +23 -4
- package/dist/proxy/worker.js +3 -2
- package/dist/publish-set/collect.js +6 -0
- package/dist/runtime/fatal.js +2 -1
- package/dist/runtime/first-run.js +5 -1
- package/dist/runtime/node-version.js +51 -0
- package/dist/runtime/nudges.js +27 -1
- package/dist/sbom/cyclonedx.js +120 -33
- package/dist/sbom/graph.js +236 -0
- package/dist/sbom/spdx.js +53 -0
- package/dist/sbom-ui/SbomApp.js +5 -3
- package/dist/sbom-ui/inventory.js +8 -10
- package/dist/scan/analyze-worker.js +3 -1
- package/dist/scan/collect.js +38 -8
- package/dist/scan/command.js +77 -27
- package/dist/scan/discovery.js +18 -7
- package/dist/scan/installed.js +202 -0
- package/dist/scan/manifest-coverage.js +128 -0
- package/dist/scan/render.js +125 -40
- package/dist/scan/scanner-report.js +92 -22
- package/dist/scan/staged.js +68 -15
- package/dist/scan/types.js +7 -1
- package/dist/scan-ui/LegacyApp.js +3 -3
- package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
- package/dist/scan-ui/hooks/useScan.js +37 -11
- package/dist/scan-ui/launch.js +2 -2
- package/dist/scan-ui/logo.js +6 -2
- package/dist/scan-ui/shims.js +6 -6
- package/dist/scripts/gate.js +53 -35
- package/dist/security/csv.js +9 -0
- package/dist/security/sanitize.js +11 -2
- package/dist/service/state.js +120 -37
- package/dist/service/trust-refresh.js +12 -1
- package/dist/service/trust-store.js +36 -5
- package/dist/service/worker.js +5 -7
- package/dist/setup/git-hook.js +2 -2
- package/dist/setup/install-method.js +36 -0
- package/dist/setup/plan.js +119 -57
- package/dist/setup/uninstall-standalone.js +7 -2
- package/dist/setup-ui/gate.js +1 -8
- package/dist/setup-ui/offer.js +14 -7
- package/dist/setup-ui/selector.js +1 -1
- package/dist/setup-ui/wizard.js +73 -78
- package/dist/standalone/uninstall.mjs +1114 -386
- package/dist/state/cleanup-registry.js +14 -3
- package/dist/state/cooldown-held.js +36 -13
- package/dist/state/locks.js +239 -34
- package/dist/state/sessions.js +2 -2
- package/dist/util/sh-escape.js +6 -0
- package/dist/util/tty-prompt.js +13 -4
- package/dist/verify/local.js +60 -10
- package/dist/verify/package-check.js +35 -49
- package/dist/verify/preflight.js +59 -33
- package/dist/verify/render.js +11 -12
- package/npm-shrinkwrap.json +714 -213
- package/package.json +3 -2
|
@@ -3,13 +3,20 @@ import { runAgentHookExec } from "./agent-hook-io.js";
|
|
|
3
3
|
import { getAgent, isAgentId } from "../agents/registry.js";
|
|
4
4
|
import { resolveDgPaths } from "../state/index.js";
|
|
5
5
|
import { writeJsonAtomic } from "../util/json-file.js";
|
|
6
|
+
const MAX_HOOK_STDIN_BYTES = 8 * 1024 * 1024;
|
|
6
7
|
async function readStdin(stream = process.stdin) {
|
|
7
8
|
if (stream.isTTY) {
|
|
8
9
|
return "";
|
|
9
10
|
}
|
|
10
11
|
const chunks = [];
|
|
12
|
+
let total = 0;
|
|
11
13
|
for await (const chunk of stream) {
|
|
12
|
-
|
|
14
|
+
const buffer = chunk;
|
|
15
|
+
total += buffer.length;
|
|
16
|
+
if (total > MAX_HOOK_STDIN_BYTES) {
|
|
17
|
+
throw new Error(`hook stdin exceeded ${MAX_HOOK_STDIN_BYTES} bytes`);
|
|
18
|
+
}
|
|
19
|
+
chunks.push(buffer);
|
|
13
20
|
}
|
|
14
21
|
return Buffer.concat(chunks).toString("utf8");
|
|
15
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { agentCheckCommand, ESCALATE, formatScreenedNote } from "./agent-check.js";
|
|
2
2
|
import { getAgent } from "../agents/registry.js";
|
|
3
3
|
import { redactSecrets } from "./output-redaction.js";
|
|
4
|
+
import { maybeUpdateNudge } from "../runtime/nudges.js";
|
|
4
5
|
export async function runAgentHookExec(agent, stdin, deps = {}) {
|
|
5
6
|
const integration = getAgent(agent);
|
|
6
7
|
const parsed = integration.parseInput(stdin);
|
|
@@ -27,13 +28,14 @@ export async function runAgentHookExec(agent, stdin, deps = {}) {
|
|
|
27
28
|
verdict = { decision: "deny", reason: `dg hook: the firewall check failed, so the install is blocked. ${ESCALATE}` };
|
|
28
29
|
}
|
|
29
30
|
const emitted = integration.emitDecision(verdict);
|
|
30
|
-
const stderr = allowStderr(verdict);
|
|
31
|
+
const stderr = allowStderr(verdict, deps.env);
|
|
31
32
|
return { stdout: emitted.stdout, stderr, exitCode: emitted.exitCode };
|
|
32
33
|
}
|
|
33
|
-
function allowStderr(verdict) {
|
|
34
|
+
function allowStderr(verdict, env) {
|
|
34
35
|
if (verdict.decision !== "allow") {
|
|
35
36
|
return ` ${redactSecrets(verdict.reason ?? "DG firewall")}\n`;
|
|
36
37
|
}
|
|
37
38
|
const note = verdict.screened ? formatScreenedNote(verdict.screened) : "";
|
|
38
|
-
|
|
39
|
+
const noteLine = note ? ` ${redactSecrets(note)}\n` : "";
|
|
40
|
+
return noteLine + maybeUpdateNudge(env ? { env } : {});
|
|
39
41
|
}
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { optionalPackageManagerNames, optionalSupportGate } from "../setup/optional-support.js";
|
|
2
2
|
const supportedManagers = ["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"];
|
|
3
3
|
const gatedManagers = optionalPackageManagerNames();
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// npm resolves many spellings to the same fetch/run command via its cmd-list.js
|
|
5
|
+
// alias table — `x`=exec (remote fetch-and-run, like npx), `in`/`ins`/`isntall`/…
|
|
6
|
+
// =install, `it`=install-test, `cit`/`sit`=install-ci-test, `ic`=ci, `up`=update.
|
|
7
|
+
// Each fetches or runs package code, so each must gate exactly like its canonical
|
|
8
|
+
// verb; otherwise `npm x evil` / `npm in evil` install ungated. Spelled out (not
|
|
9
|
+
// abbrev-derived) so the gate set stays reviewable; classify-aliases.test.ts pins
|
|
10
|
+
// it against npm's own table.
|
|
11
|
+
const jsProtected = new Set([
|
|
12
|
+
"install", "i", "in", "ins", "inst", "insta", "instal", "isnt", "isnta", "isntal", "isntall",
|
|
13
|
+
"ci", "ic", "clean-install", "install-clean", "isntall-clean",
|
|
14
|
+
"install-test", "it",
|
|
15
|
+
"install-ci-test", "cit", "sit", "clean-install-test",
|
|
16
|
+
"add", "update", "up", "upgrade", "udpate", "dedupe", "ddp",
|
|
17
|
+
"exec", "x", "create",
|
|
18
|
+
]);
|
|
19
|
+
const pnpmProtected = new Set(["install", "i", "add", "update", "up", "upgrade", "dlx", "fetch", "exec", "create"]);
|
|
20
|
+
const yarnProtected = new Set(["add", "install", "up", "upgrade", "dlx", "create"]);
|
|
7
21
|
const pipProtected = new Set(["install", "download", "wheel"]);
|
|
8
|
-
|
|
22
|
+
// `upgrade-all`/`reinstall`/`reinstall-all`/`install-all` each re-fetch and run
|
|
23
|
+
// package code just like `upgrade`/`install`; without them the `-all` and
|
|
24
|
+
// `reinstall` forms slip past as passthrough (the prefix gap that let
|
|
25
|
+
// `pipx upgrade` gate but `pipx upgrade-all` install unscanned).
|
|
26
|
+
const pipxProtected = new Set(["install", "install-all", "upgrade", "upgrade-all", "reinstall", "reinstall-all", "inject", "run"]);
|
|
9
27
|
const uvProtected = new Set(["add", "sync"]);
|
|
10
28
|
// Only the dependency-set-changing verbs. `build`/`test`/`run`/`check` compile
|
|
11
29
|
// local code (the npm `run`/`build` analog, which is passthrough) and crates are
|
|
12
30
|
// not analyzable yet, so gating them just interrupts every Rust build with no
|
|
13
31
|
// screening value.
|
|
14
|
-
const cargoProtected = new Set(["add", "install", "fetch", "update"]);
|
|
32
|
+
const cargoProtected = new Set(["add", "install", "fetch", "update", "binstall"]);
|
|
15
33
|
const passthrough = new Set(["help", "--help", "-h", "version", "--version", "-v", "list", "ls", "show", "freeze"]);
|
|
16
34
|
// Recognized read-only / local / script subcommands across the managers. Used
|
|
17
35
|
// only to locate the real subcommand when a global value-flag puts a non-verb
|
|
@@ -29,20 +47,44 @@ const READ_ONLY_VERBS = new Set([
|
|
|
29
47
|
"logout", "login", "adduser", "owner", "set", "init", "licenses",
|
|
30
48
|
"freeze", "check", "uninstall", "debug", "hash", "inspect", "index", "tree", "lock", "venv", "export", "python", "self",
|
|
31
49
|
]);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
// Verbs/subcommands that always indicate a uv install/fetch path — the guard set
|
|
51
|
+
// for uv so positional resolution never skips over a real install token.
|
|
52
|
+
const UV_GUARD = new Set(["pip", "tool", "run", "build", "add", "sync", "install", "upgrade", "download", "wheel"]);
|
|
53
|
+
// Positional (non-flag) tokens, with each flag's value argument skipped so a
|
|
54
|
+
// flag VALUE can't masquerade as the subcommand/verb (`npm --user-agent run
|
|
55
|
+
// install x`, `uv --cache-dir /tmp tool install x`). We conservatively treat
|
|
56
|
+
// EVERY non-attached flag as potentially value-consuming rather than maintain a
|
|
57
|
+
// per-manager flag allowlist (which always drifts and re-opens the bypass). Two
|
|
58
|
+
// invariants keep this fail-closed: never skip a token that is itself a protected
|
|
59
|
+
// (install) command, and never skip another flag — so a misjudged boolean flag
|
|
60
|
+
// can at worst over-screen the rare `<flag> run <install-named-script>` form, it
|
|
61
|
+
// can never wave a real install through.
|
|
62
|
+
function positionalArgs(args, protectedCommands) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (let i = 0; i < args.length; i++) {
|
|
65
|
+
const a = args[i];
|
|
66
|
+
if (a === undefined) {
|
|
36
67
|
continue;
|
|
37
68
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
69
|
+
if (a.startsWith("-")) {
|
|
70
|
+
const next = args[i + 1];
|
|
71
|
+
if (!a.includes("=") && next !== undefined && !next.startsWith("-") && !protectedCommands.has(next)) {
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
40
75
|
}
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
out.push(a);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function findAction(args, protectedCommands) {
|
|
81
|
+
const positionals = positionalArgs(args, protectedCommands);
|
|
82
|
+
for (const token of positionals) {
|
|
83
|
+
if (protectedCommands.has(token) || READ_ONLY_VERBS.has(token)) {
|
|
84
|
+
return token;
|
|
43
85
|
}
|
|
44
86
|
}
|
|
45
|
-
return
|
|
87
|
+
return positionals[0] ?? "";
|
|
46
88
|
}
|
|
47
89
|
// pip3, pip3.12, python3, python3.11 are the canonical install commands on
|
|
48
90
|
// macOS/Homebrew and many Linux distros (see BINARY_FALLBACKS in
|
|
@@ -51,8 +93,16 @@ function findAction(args, protectedCommands) {
|
|
|
51
93
|
// through to a silent allow. `pipx`/`pipenv` are left untouched — only a bare
|
|
52
94
|
// numeric/dotted version suffix is stripped.
|
|
53
95
|
export function normalizeManagerName(name) {
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
// The OS resolves a binary name case-insensitively on macOS, so `NPM`/`Pip3`
|
|
97
|
+
// run the real lowercase manager; fold the head toward the lowercase manager
|
|
98
|
+
// tables (the single source of truth) or it short-circuits to an unscreened
|
|
99
|
+
// ALLOW. Only the manager head is folded — package names keep their case.
|
|
100
|
+
// On Windows the manager is invoked through a launcher with an executable
|
|
101
|
+
// extension (npm.cmd, pnpm.cmd, yarn.cmd, pip.exe); strip it or the head never
|
|
102
|
+
// matches the manager tables and short-circuits to an unscreened ALLOW.
|
|
103
|
+
const lower = name.toLowerCase().replace(/\.(?:cmd|exe|bat|ps1)$/, "");
|
|
104
|
+
const m = /^(pip|python)(\d+(?:\.\d+)*)?$/.exec(lower);
|
|
105
|
+
return m && m[1] ? m[1] : lower;
|
|
56
106
|
}
|
|
57
107
|
export function packageManagerNames() {
|
|
58
108
|
return [...supportedManagers, ...gatedManagers];
|
|
@@ -79,7 +129,7 @@ export function classifyPackageManagerInvocation(rawManager, args) {
|
|
|
79
129
|
// dependency; classify it protected rather than passthrough. Read-only
|
|
80
130
|
// version/help invocations stay passthrough.
|
|
81
131
|
const readOnlyFlag = args.some((a) => a === "--version" || a === "-v" || a === "--help" || a === "-h");
|
|
82
|
-
if (
|
|
132
|
+
if (positionalArgs(args, yarnProtected).length === 0 && !readOnlyFlag) {
|
|
83
133
|
return protectedClassification("yarn", args, "yarn", "bare yarn installs all lockfile dependencies");
|
|
84
134
|
}
|
|
85
135
|
return classifyByCommand(manager, args, "yarn", yarnProtected, "Yarn classic install/fetch command", "javascript");
|
|
@@ -119,17 +169,25 @@ function classifyByCommand(manager, args, realBinaryName, protectedCommands, pro
|
|
|
119
169
|
};
|
|
120
170
|
}
|
|
121
171
|
function classifyUv(args) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
// Skip value-flags so a flag value can't masquerade as the subcommand/verb
|
|
173
|
+
// (`uv --cache-dir /tmp tool install x`, `uv -q tool install x`).
|
|
174
|
+
const positionals = positionalArgs(args, UV_GUARD);
|
|
175
|
+
const action = positionals[0] ?? "";
|
|
176
|
+
const subVerb = positionals[1] ?? "";
|
|
177
|
+
// `uv pip sync` installs/removes packages to match a requirements set — a
|
|
178
|
+
// uv-only verb regular pip lacks, so it isn't in pipProtected — and must gate
|
|
179
|
+
// like `uv pip install`.
|
|
180
|
+
if (action === "pip" && (pipProtected.has(subVerb) || subVerb === "sync")) {
|
|
181
|
+
return protectedClassification("uv", args, "uv", "uv pip install/sync/fetch command");
|
|
125
182
|
}
|
|
126
|
-
if (action === "tool" && ["run", "install", "upgrade"].includes(
|
|
183
|
+
if (action === "tool" && ["run", "install", "upgrade"].includes(subVerb)) {
|
|
127
184
|
return protectedClassification("uv", args, "uv", "uv tool run/install/upgrade fetches package artifacts");
|
|
128
185
|
}
|
|
129
|
-
if (action === "run" &&
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
186
|
+
if (action === "run" && !uvRunSkipsDependencyFetch(args)) {
|
|
187
|
+
return protectedClassification("uv", args, "uv", "uv run syncs project/script dependencies from PyPI before executing");
|
|
188
|
+
}
|
|
189
|
+
if (action === "build" && !uvArgPresent(args, "--no-build-isolation")) {
|
|
190
|
+
return protectedClassification("uv", args, "uv", "uv build fetches PEP 517 build backends into an isolated environment");
|
|
133
191
|
}
|
|
134
192
|
if (uvProtected.has(action)) {
|
|
135
193
|
return protectedClassification("uv", args, "uv", "uv install/fetch command");
|
|
@@ -181,5 +239,31 @@ function initFetchesPackage(action, args, protectedCommands) {
|
|
|
181
239
|
return args.slice(commandIndex + 1).some((arg) => arg !== "--" && !arg.startsWith("-"));
|
|
182
240
|
}
|
|
183
241
|
export function uvRunFetchesPackage(args) {
|
|
184
|
-
|
|
242
|
+
// uv accepts the glued `--flag=value` form for every long flag, so the
|
|
243
|
+
// attached spellings must gate exactly like the space-separated ones or
|
|
244
|
+
// `uv run --with-requirements=evil.txt cmd` fetches and runs unscanned.
|
|
245
|
+
return args.some((a) => a === "--with" ||
|
|
246
|
+
a.startsWith("--with=") ||
|
|
247
|
+
a === "--with-editable" ||
|
|
248
|
+
a.startsWith("--with-editable=") ||
|
|
249
|
+
a === "--with-requirements" ||
|
|
250
|
+
a.startsWith("--with-requirements="));
|
|
251
|
+
}
|
|
252
|
+
function uvArgPresent(args, flag) {
|
|
253
|
+
return args.some((a) => a === flag || a.startsWith(`${flag}=`));
|
|
254
|
+
}
|
|
255
|
+
// `uv run` syncs the project's declared dependencies (pyproject.toml/uv.lock) and
|
|
256
|
+
// any PEP 723 inline-script deps from PyPI before executing, so it is protected by
|
|
257
|
+
// default. `--no-sync` alone still lets the project load; only dropping the project
|
|
258
|
+
// too (`--no-project`) removes the last sync path, and `--offline` forbids any
|
|
259
|
+
// fetch outright. An explicit `--with`/`--script` names remote deps that those
|
|
260
|
+
// suppressors do not cover, so it stays gated regardless.
|
|
261
|
+
export function uvRunSkipsDependencyFetch(args) {
|
|
262
|
+
if (uvArgPresent(args, "--script") || uvRunFetchesPackage(args)) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (uvArgPresent(args, "--offline")) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return uvArgPresent(args, "--no-sync") && uvArgPresent(args, "--no-project");
|
|
185
269
|
}
|
package/dist/launcher/env.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
2
|
import { proxyUrlWithAuth, readProxyAuthToken } from "../proxy/auth.js";
|
|
3
3
|
// The client read timeout must exceed the proxy's per-artifact verdict ceiling
|
|
4
4
|
// (DG_INSTALL_VERDICT_TIMEOUT_MS, default 240s) plus download headroom, so the
|
|
@@ -50,6 +50,85 @@ export function buildAgentRoutingEnv(proxyUrl, caPath) {
|
|
|
50
50
|
CARGO_HTTP_CAINFO: caPath,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
// Point a manager at the local registry origin, env-only — never the user's
|
|
54
|
+
// real ~/.npmrc / pip.conf / ~/.cargo/config.toml. npm keeps dist.tarball
|
|
55
|
+
// canonical and disables host rewriting so committed lockfiles stay portable
|
|
56
|
+
// (the tarball is gated by the proxy tier — the npm hybrid the P0 spike settled);
|
|
57
|
+
// pip/uv get the loopback simple index; cargo routes through a throwaway
|
|
58
|
+
// CARGO_HOME whose config.toml is written separately.
|
|
59
|
+
export function buildRegistryOriginEnv(options) {
|
|
60
|
+
const env = scrubChildSecrets({ ...options.baseEnv, DG_REGISTRY_ORIGIN_ACTIVE: "1" });
|
|
61
|
+
const noProxy = "127.0.0.1,localhost";
|
|
62
|
+
env.NO_PROXY = noProxy;
|
|
63
|
+
env.no_proxy = noProxy;
|
|
64
|
+
const origin = ensureTrailingSlash(options.originUrl);
|
|
65
|
+
const host = new URL(options.originUrl).hostname;
|
|
66
|
+
if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(options.manager)) {
|
|
67
|
+
env.npm_config_registry = origin;
|
|
68
|
+
env.npm_config_replace_registry_host = "never";
|
|
69
|
+
if (options.manager === "yarn") {
|
|
70
|
+
env.YARN_NPM_REGISTRY_SERVER = options.originUrl;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (options.manager === "pip" || options.manager === "pipx") {
|
|
74
|
+
env.PIP_INDEX_URL = `${origin}simple/`;
|
|
75
|
+
env.PIP_TRUSTED_HOST = host;
|
|
76
|
+
scrubSupplementaryIndexes(env);
|
|
77
|
+
}
|
|
78
|
+
else if (options.manager === "uv" || options.manager === "uvx") {
|
|
79
|
+
env.UV_DEFAULT_INDEX = `${origin}simple/`;
|
|
80
|
+
scrubSupplementaryIndexes(env);
|
|
81
|
+
}
|
|
82
|
+
else if (options.cargoHome) {
|
|
83
|
+
env.CARGO_HOME = options.cargoHome;
|
|
84
|
+
}
|
|
85
|
+
applyCacheBust(env, options.manager, options.cacheDir);
|
|
86
|
+
return env;
|
|
87
|
+
}
|
|
88
|
+
function ensureTrailingSlash(url) {
|
|
89
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
90
|
+
}
|
|
91
|
+
// Precedence already steers an install with a supplementary index to the proxy
|
|
92
|
+
// door, but when the origin door IS used the pip/uv child must fetch ONLY from the
|
|
93
|
+
// origin — drop any inherited extra-index / find-links source so a precedence gap
|
|
94
|
+
// can never leave a second, un-fronted package source active. Defense in depth.
|
|
95
|
+
function scrubSupplementaryIndexes(env) {
|
|
96
|
+
for (const key of ["PIP_EXTRA_INDEX_URL", "PIP_FIND_LINKS", "UV_EXTRA_INDEX_URL", "UV_INDEX", "UV_FIND_LINKS"]) {
|
|
97
|
+
delete env[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// A warm package-manager cache serves an artifact with no network, skipping
|
|
101
|
+
// whichever door is active, so both env builders force a re-fetch through a
|
|
102
|
+
// throwaway cache (or PIP_NO_CACHE_DIR / UV_NO_CACHE where the manager has no
|
|
103
|
+
// redirectable cache dir). Shared so the proxy and origin paths cannot drift.
|
|
104
|
+
function applyCacheBust(env, manager, cacheDir) {
|
|
105
|
+
if (manager === "pip" || manager === "pipx") {
|
|
106
|
+
env.PIP_NO_CACHE_DIR = "1";
|
|
107
|
+
}
|
|
108
|
+
if (manager === "uv" || manager === "uvx") {
|
|
109
|
+
env.UV_NO_CACHE = "1";
|
|
110
|
+
}
|
|
111
|
+
if (!cacheDir) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(manager)) {
|
|
115
|
+
env.npm_config_cache = cacheDir;
|
|
116
|
+
if (manager === "pnpm" || manager === "pnpx") {
|
|
117
|
+
// pnpm links packages from a global content-addressable store, not the
|
|
118
|
+
// npm cache; a package already in the user's ~/.pnpm-store installs with
|
|
119
|
+
// zero network and skips the origin entirely. Point the store at a
|
|
120
|
+
// dg-owned dir so every package is fetched (and gated) at least once.
|
|
121
|
+
env.npm_config_store_dir = join(cacheDir, "pnpm-store");
|
|
122
|
+
}
|
|
123
|
+
if (manager === "yarn") {
|
|
124
|
+
env.YARN_CACHE_FOLDER = cacheDir;
|
|
125
|
+
env.YARN_GLOBAL_FOLDER = cacheDir;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (manager === "cargo") {
|
|
129
|
+
env.CARGO_HOME = cacheDir;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
53
132
|
export function buildProxyChildEnv(options) {
|
|
54
133
|
const authToken = readProxyAuthToken(dirname(options.caBundlePath));
|
|
55
134
|
const proxyUrl = authToken ? proxyUrlWithAuth(options.proxyUrl, authToken) : options.proxyUrl;
|
|
@@ -88,16 +167,7 @@ export function buildProxyChildEnv(options) {
|
|
|
88
167
|
env.YARN_HTTPS_PROXY = proxyUrl;
|
|
89
168
|
env.YARN_HTTPS_CA_FILE_PATH = options.caBundlePath;
|
|
90
169
|
}
|
|
91
|
-
|
|
92
|
-
env.npm_config_cache = options.cacheDir;
|
|
93
|
-
if (options.manager === "yarn") {
|
|
94
|
-
// Both: YARN_CACHE_FOLDER is Berry's per-project cache, but Berry 2+
|
|
95
|
-
// defaults to a shared global cache (~/.yarn/berry) that a warm install
|
|
96
|
-
// serves from without ever fetching, so redirect that too.
|
|
97
|
-
env.YARN_CACHE_FOLDER = options.cacheDir;
|
|
98
|
-
env.YARN_GLOBAL_FOLDER = options.cacheDir;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
170
|
+
applyCacheBust(env, options.manager, options.cacheDir);
|
|
101
171
|
return env;
|
|
102
172
|
}
|
|
103
173
|
if (options.manager === "pip" || options.manager === "pipx") {
|
|
@@ -105,8 +175,8 @@ export function buildProxyChildEnv(options) {
|
|
|
105
175
|
env.https_proxy = proxyUrl;
|
|
106
176
|
env.REQUESTS_CA_BUNDLE = options.caBundlePath;
|
|
107
177
|
env.PIP_CERT = options.caBundlePath;
|
|
108
|
-
env.PIP_NO_CACHE_DIR = "1";
|
|
109
178
|
env.PIP_DEFAULT_TIMEOUT = String(readTimeoutSeconds);
|
|
179
|
+
applyCacheBust(env, options.manager, options.cacheDir);
|
|
110
180
|
return env;
|
|
111
181
|
}
|
|
112
182
|
if (options.manager === "uv" || options.manager === "uvx") {
|
|
@@ -114,8 +184,8 @@ export function buildProxyChildEnv(options) {
|
|
|
114
184
|
env.HTTPS_PROXY = proxyUrl;
|
|
115
185
|
env.ALL_PROXY = proxyUrl;
|
|
116
186
|
env.SSL_CERT_FILE = options.caBundlePath;
|
|
117
|
-
env.UV_NO_CACHE = "1";
|
|
118
187
|
env.UV_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
188
|
+
applyCacheBust(env, options.manager, options.cacheDir);
|
|
119
189
|
return env;
|
|
120
190
|
}
|
|
121
191
|
env.HTTPS_PROXY = proxyUrl;
|
|
@@ -123,10 +193,6 @@ export function buildProxyChildEnv(options) {
|
|
|
123
193
|
env.http_proxy = proxyUrl;
|
|
124
194
|
env.CARGO_HTTP_CAINFO = options.caBundlePath;
|
|
125
195
|
env.CARGO_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
126
|
-
|
|
127
|
-
// cargo serves a cached crate from ~/.cargo with zero network, skipping the
|
|
128
|
-
// firewall; pointing CARGO_HOME at the throwaway dir forces a re-fetch.
|
|
129
|
-
env.CARGO_HOME = options.cacheDir;
|
|
130
|
-
}
|
|
196
|
+
applyCacheBust(env, options.manager, options.cacheDir);
|
|
131
197
|
return env;
|
|
132
198
|
}
|
|
@@ -11,6 +11,7 @@ import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCoold
|
|
|
11
11
|
import { findProjectRoot, loadDgFile, resolveAcceptedBy, warnUnreadableDgFile } from "../project/dgfile.js";
|
|
12
12
|
import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
|
|
13
13
|
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
14
|
+
import { scrubChildSecrets } from "./env.js";
|
|
14
15
|
export function resolvePreflightCooldown(env, ecosystem) {
|
|
15
16
|
// A corrupt user config must not silently disable the cooldown gate; fall back
|
|
16
17
|
// to defaults so any env-configured cooldown is still applied.
|
|
@@ -54,6 +55,10 @@ export function resolvePreflightDecisions(ecosystem, cwd, env = process.env) {
|
|
|
54
55
|
}
|
|
55
56
|
const PROCEED = { proceed: true };
|
|
56
57
|
const UNRESOLVED = { set: undefined, count: undefined };
|
|
58
|
+
// pip controls the --report shape, but a hostile index can inflate it; cap the
|
|
59
|
+
// captured bytes so the preflight degrades to the per-artifact proxy path rather
|
|
60
|
+
// than buffering an unbounded report on the dev/CI machine.
|
|
61
|
+
const MAX_PIP_REPORT_BYTES = 16 * 1024 * 1024;
|
|
57
62
|
const approvedFlagRanks = new Map();
|
|
58
63
|
const pipResolutions = new Map();
|
|
59
64
|
export function resetInstallPreflightSession() {
|
|
@@ -108,8 +113,12 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
108
113
|
let child;
|
|
109
114
|
try {
|
|
110
115
|
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
116
|
+
// pip's dry-run resolve builds sdists without a wheel / PEP 658 metadata,
|
|
117
|
+
// invoking arbitrary PEP 517 build backends from the package being resolved.
|
|
118
|
+
// Scrub the dg credential first — the same protection every other spawn path
|
|
119
|
+
// applies — so DG_API_KEY/DG_API_TOKEN never reaches untrusted build code.
|
|
111
120
|
child = spawn(invocation.command, [...invocation.args], {
|
|
112
|
-
env,
|
|
121
|
+
env: scrubChildSecrets({ ...env }),
|
|
113
122
|
stdio: ["ignore", "pipe", "ignore"],
|
|
114
123
|
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
115
124
|
});
|
|
@@ -125,7 +134,21 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
125
134
|
catch { /* already exited */ }
|
|
126
135
|
finish(UNRESOLVED);
|
|
127
136
|
}, pipResolveTimeoutMs(env));
|
|
128
|
-
|
|
137
|
+
let stdoutBytes = 0;
|
|
138
|
+
child.stdout?.on("data", (chunk) => {
|
|
139
|
+
if (settled)
|
|
140
|
+
return;
|
|
141
|
+
stdoutBytes += chunk.length;
|
|
142
|
+
if (stdoutBytes > MAX_PIP_REPORT_BYTES) {
|
|
143
|
+
try {
|
|
144
|
+
child.kill();
|
|
145
|
+
}
|
|
146
|
+
catch { /* already exited */ }
|
|
147
|
+
finish(UNRESOLVED);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
stdout += chunk.toString("utf8");
|
|
151
|
+
});
|
|
129
152
|
child.on("error", () => finish(UNRESOLVED));
|
|
130
153
|
child.on("close", (code) => finish(code === 0
|
|
131
154
|
? { set: parsePipReportInstallSet(stdout), count: parsePipReportInstallCount(stdout) }
|
|
@@ -197,7 +220,7 @@ export async function runInstallPreflight(manager, binary, childArgs, env, cwd =
|
|
|
197
220
|
const cooldownContext = resolvePreflightCooldown(env, "pypi");
|
|
198
221
|
let verdicts;
|
|
199
222
|
try {
|
|
200
|
-
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env, ...(cooldownContext ? { cooldown: cooldownContext.param } : {}) });
|
|
223
|
+
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env, publicFallback: true, ...(cooldownContext ? { cooldown: cooldownContext.param } : {}) });
|
|
201
224
|
}
|
|
202
225
|
catch {
|
|
203
226
|
process.stderr.write(" dg: pre-install check unavailable — verifying during install instead\n");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
1
|
+
import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, meterInstallTree, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
2
2
|
import { runInstallPreflight } from "./install-preflight.js";
|
|
3
3
|
import { isSupportedPackageManager, normalizeManagerName } from "./classify.js";
|
|
4
4
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
5
5
|
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
6
|
+
import { redactSecrets } from "./output-redaction.js";
|
|
6
7
|
const FALL_THROUGH = { handled: false };
|
|
7
8
|
export async function maybeRunLiveInstall(args, options = {}) {
|
|
8
9
|
const env = options.env ?? process.env;
|
|
@@ -52,6 +53,7 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
52
53
|
try {
|
|
53
54
|
const { renderLiveInstall } = await appLoading;
|
|
54
55
|
const result = await renderLiveInstall((onView) => runWithProductionProxyLive(plan, childArgs, runOptions, onView, prepared));
|
|
56
|
+
meterInstallTree(plan, runOptions, result.exitCode);
|
|
55
57
|
return { handled: true, result };
|
|
56
58
|
}
|
|
57
59
|
catch (error) {
|
|
@@ -61,7 +63,7 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
61
63
|
result: {
|
|
62
64
|
exitCode: 1,
|
|
63
65
|
stdout: "",
|
|
64
|
-
stderr: `dg protection failed: ${error instanceof Error ? error.message : "unknown error"}\n`
|
|
66
|
+
stderr: `dg protection failed: ${redactSecrets(error instanceof Error ? error.message : "unknown error")}\n`
|
|
65
67
|
}
|
|
66
68
|
};
|
|
67
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
// A bare `npm install` resolves the whole dependency tree, but enumerating every
|
|
4
4
|
// transitive package in the hook would blow its time budget — that breadth is
|
|
5
5
|
// the runtime network gate's job. The static hook screens the DIRECT
|
|
@@ -8,6 +8,9 @@ import { isAbsolute, join } from "node:path";
|
|
|
8
8
|
// instead of silently passing.
|
|
9
9
|
const MAX_MANIFEST_SPECS = 100;
|
|
10
10
|
const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
// Cap the requirements include graph so a deep / fan-out `-r` chain defers rather
|
|
12
|
+
// than walking unbounded inside the hook's time budget.
|
|
13
|
+
const MAX_MANIFEST_FILES = 50;
|
|
11
14
|
function readBoundedFile(path) {
|
|
12
15
|
try {
|
|
13
16
|
if (statSync(path).size > MAX_MANIFEST_BYTES) {
|
|
@@ -130,15 +133,51 @@ export function readPipRequirementSpecs(args, cwd) {
|
|
|
130
133
|
else if (a.startsWith("-r=")) {
|
|
131
134
|
files.push(a.slice("-r=".length));
|
|
132
135
|
}
|
|
136
|
+
else if (a.startsWith("-r") && a.length > 2) {
|
|
137
|
+
// pip accepts the glued short form `-rrequirements.txt`.
|
|
138
|
+
files.push(a.slice(2));
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
141
|
if (files.length === 0) {
|
|
135
142
|
return null;
|
|
136
143
|
}
|
|
137
144
|
const specs = [];
|
|
138
145
|
const seen = new Set();
|
|
146
|
+
const alternateIndexes = [];
|
|
139
147
|
let truncated = false;
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
// pip resolves a nested `-r`/`-c` include relative to the directory of the file
|
|
149
|
+
// that contains it; a command-line file is relative to cwd. Walk the include
|
|
150
|
+
// graph (cycle-guarded, depth-capped) so a `requirements.txt` whose only line
|
|
151
|
+
// is `-r evil.txt` does not pass clean with the pinned packages unscreened.
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
const queue = files.map((file) => ({ file, base: cwd }));
|
|
154
|
+
let filesProcessed = 0;
|
|
155
|
+
outer: while (queue.length > 0) {
|
|
156
|
+
const next = queue.shift();
|
|
157
|
+
if (next === undefined) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (next.file.includes("://")) {
|
|
161
|
+
// A remote -r URL the static hook cannot read; defer.
|
|
162
|
+
truncated = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const path = isAbsolute(next.file) ? next.file : resolve(next.base, next.file);
|
|
166
|
+
const rel = relative(cwd, path);
|
|
167
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
168
|
+
// A -r target outside the project root is not ours to open. Defer the
|
|
169
|
+
// install (truncated) rather than read an arbitrary path or pass clean.
|
|
170
|
+
truncated = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (visited.has(path)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
visited.add(path);
|
|
177
|
+
if (++filesProcessed > MAX_MANIFEST_FILES) {
|
|
178
|
+
truncated = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
142
181
|
if (!existsSync(path)) {
|
|
143
182
|
// A missing requirements file means the install will fail anyway — nothing
|
|
144
183
|
// to screen, and no reason to nag the user. Leave it alone.
|
|
@@ -152,20 +191,62 @@ export function readPipRequirementSpecs(args, cwd) {
|
|
|
152
191
|
continue;
|
|
153
192
|
}
|
|
154
193
|
for (const line of raw.split(/\r?\n/)) {
|
|
194
|
+
const include = parseRequirementInclude(line);
|
|
195
|
+
if (include !== null) {
|
|
196
|
+
queue.push({ file: include, base: dirname(path) });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const index = parseRequirementIndexDirective(line);
|
|
200
|
+
if (index !== null) {
|
|
201
|
+
alternateIndexes.push(index);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
155
204
|
const spec = parseRequirementLine(line);
|
|
156
205
|
if (!spec || seen.has(spec.name)) {
|
|
157
206
|
continue;
|
|
158
207
|
}
|
|
159
208
|
if (specs.length >= MAX_MANIFEST_SPECS) {
|
|
160
209
|
truncated = true;
|
|
161
|
-
break;
|
|
210
|
+
break outer;
|
|
162
211
|
}
|
|
163
212
|
seen.add(spec.name);
|
|
164
213
|
specs.push(spec);
|
|
165
214
|
}
|
|
166
215
|
}
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
const indexes = alternateIndexes.length > 0 ? alternateIndexes : undefined;
|
|
217
|
+
if (specs.length === 0 && !truncated && !indexes) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return { specs, truncated, ...(indexes ? { alternateIndexes: indexes } : {}) };
|
|
221
|
+
}
|
|
222
|
+
// An index / find-links directive declared inside a requirements/constraints
|
|
223
|
+
// file. pip honors `--index-url` / `--extra-index-url` / `-i` / `-f` /
|
|
224
|
+
// `--find-links` there, so the static screen must surface them the same as the
|
|
225
|
+
// command-line forms or a hostile in-file index passes clean.
|
|
226
|
+
function parseRequirementIndexDirective(line) {
|
|
227
|
+
const stripped = line.split(/(?<!\\)#/)[0]?.trim() ?? "";
|
|
228
|
+
const spaced = /^(?:-i|--index-url|--extra-index-url|-f|--find-links)[=\s]+(\S.*)$/.exec(stripped);
|
|
229
|
+
if (spaced && spaced[1]) {
|
|
230
|
+
return spaced[1].trim();
|
|
169
231
|
}
|
|
170
|
-
|
|
232
|
+
const glued = /^-([if])(\S.+)$/.exec(stripped);
|
|
233
|
+
if (glued && glued[2] && glued[2][0] !== "=") {
|
|
234
|
+
return glued[2].trim();
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
// A nested `-r FILE` / `--requirement FILE` / `-c FILE` / `--constraint FILE`
|
|
239
|
+
// include line; pip reads its pins, so the static hook must follow it too.
|
|
240
|
+
function parseRequirementInclude(line) {
|
|
241
|
+
const stripped = line.split(/(?<!\\)#/)[0]?.trim() ?? "";
|
|
242
|
+
const spaced = /^(?:-r|--requirement|-c|--constraint)[=\s]+(\S.*)$/.exec(stripped);
|
|
243
|
+
if (spaced && spaced[1]) {
|
|
244
|
+
return spaced[1].trim();
|
|
245
|
+
}
|
|
246
|
+
// Glued short form: -rnested.txt / -cconstraints.txt
|
|
247
|
+
const glued = /^-([rc])(\S.+)$/.exec(stripped);
|
|
248
|
+
if (glued && glued[2] && glued[2][0] !== "=") {
|
|
249
|
+
return glued[2].trim();
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
171
252
|
}
|