@westbayberry/dg 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +123 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +118 -0
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +351 -0
- package/dist/agents/registry.js +128 -0
- package/dist/agents/routing.js +118 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit/rules.js +6 -2
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +17 -5
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +275 -0
- package/dist/commands/audit.js +28 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +196 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +49 -48
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +5 -4
- package/dist/install-ui/block-render.js +18 -14
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +896 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +39 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +44 -7
- package/dist/launcher/env.js +105 -26
- package/dist/launcher/install-preflight.js +64 -18
- package/dist/launcher/live-install.js +29 -8
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +10 -4
- package/dist/launcher/preflight-prompt.js +18 -4
- package/dist/launcher/run.js +178 -103
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +146 -6
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +66 -12
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +568 -53
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +14 -12
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +132 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +33 -9
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +19 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +28 -9
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +227 -49
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +233 -0
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +242 -44
- package/dist/verify/package-check.js +87 -29
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +7 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
package/dist/launcher/run.js
CHANGED
|
@@ -3,46 +3,77 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { constants as osConstants } from "node:os";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { EXIT_UNAVAILABLE } from "../commands/types.js";
|
|
6
|
-
import { loadUserConfig } from "../config/settings.js";
|
|
6
|
+
import { loadUserConfig, trustsProjectOverrides } from "../config/settings.js";
|
|
7
|
+
import { findProjectRoot, loadDgFile } from "../project/dgfile.js";
|
|
8
|
+
import { honoredOverrides } from "../project/override-trust.js";
|
|
9
|
+
import { writeCooldownExemptionsFile } from "../proxy/cooldown-exemptions-file.js";
|
|
10
|
+
import { writePreverifiedFile } from "../proxy/preverified.js";
|
|
7
11
|
import { describeBlockedInstall, describeFlaggedWarn, renderInstallDecision } from "../install-ui/block-render.js";
|
|
8
12
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
9
13
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
14
|
+
import { maybeSetupNudge } from "../runtime/nudges.js";
|
|
10
15
|
import { createTheme } from "../presentation/theme.js";
|
|
11
16
|
import { readProxySessionState } from "../proxy/server.js";
|
|
17
|
+
import { readServiceState } from "../service/state.js";
|
|
12
18
|
import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
|
|
13
19
|
import { classifyPackageManagerInvocation } from "./classify.js";
|
|
14
|
-
import { buildProxyChildEnv } from "./env.js";
|
|
20
|
+
import { buildProxyChildEnv, scrubChildSecrets } from "./env.js";
|
|
21
|
+
import { prepareCargoHome, userCargoHome } from "./cargo-cache.js";
|
|
15
22
|
import { cachedPipResolution } from "./install-preflight.js";
|
|
16
|
-
import { parsePipReportInstallCount } from "./pip-report.js";
|
|
17
23
|
import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
|
|
24
|
+
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
18
25
|
import { resolveRealBinary } from "./resolve-real-binary.js";
|
|
19
|
-
import { runScriptGateAfterInstall } from "../scripts/gate.js";
|
|
26
|
+
import { runScriptGateAfterInstall, scriptGateChildEnv, scriptGateInstallArgs } from "../scripts/gate.js";
|
|
20
27
|
export const EXIT_INSTALL_BLOCKED = 2;
|
|
21
|
-
|
|
22
|
-
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
23
|
-
export function resolveSpawnInvocation(binary, args, platform = process.platform) {
|
|
24
|
-
if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
|
|
25
|
-
return { command: binary, args, windowsVerbatimArguments: false };
|
|
26
|
-
}
|
|
27
|
-
const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
|
|
28
|
-
return {
|
|
29
|
-
command: process.env.comspec ?? "cmd.exe",
|
|
30
|
-
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
31
|
-
windowsVerbatimArguments: true
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function escapeCmdCommand(command) {
|
|
35
|
-
return command.replace(CMD_META_CHARS, "^$1");
|
|
36
|
-
}
|
|
37
|
-
// cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
|
|
38
|
-
function escapeCmdArgument(argument) {
|
|
39
|
-
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
40
|
-
return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
|
|
41
|
-
}
|
|
28
|
+
export { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
42
29
|
export function shimDepth(env) {
|
|
43
30
|
const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
|
|
44
31
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
45
32
|
}
|
|
33
|
+
// A genuine parent dg proxy sets DG_PROXY_ACTIVE=1 and routes the child through a
|
|
34
|
+
// loopback proxy URL that carries the proxy's auth token (buildProxyChildEnv ->
|
|
35
|
+
// proxyUrlWithAuth). All of these must hold for a nested invocation to safely run
|
|
36
|
+
// the real package manager directly (its traffic still flows through the live
|
|
37
|
+
// parent proxy). DG_PROXY_ACTIVE + a bare loopback URL are both forgeable, so:
|
|
38
|
+
// - the URL must carry a dg auth token (a forged HTTPS_PROXY=http://127.0.0.1:1
|
|
39
|
+
// with no credential is rejected), and
|
|
40
|
+
// - when a persistent dg service is the parent (the agent-routing case), the URL
|
|
41
|
+
// must match that live service's proxy, so a forged token cannot impersonate it.
|
|
42
|
+
// If neither matches, we start our own verifying proxy rather than trust the env.
|
|
43
|
+
export function inheritedDgProxyActive(env) {
|
|
44
|
+
if (env.DG_PROXY_ACTIVE !== "1") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const proxyUrl = env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
|
|
48
|
+
if (!proxyUrl) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(proxyUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const host = url.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
59
|
+
if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (!url.username && !url.password) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { state } = readServiceState(env);
|
|
67
|
+
if (state.running && state.proxy) {
|
|
68
|
+
const live = new URL(state.proxy.proxyUrl);
|
|
69
|
+
return live.hostname.replace(/^\[(.*)\]$/, "$1") === host && live.port === url.port;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// No readable service state — fall through to the session-proxy path below.
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
46
77
|
export function rootUnprotectedNotice(env, uid = process.getuid?.()) {
|
|
47
78
|
if (uid !== 0) {
|
|
48
79
|
return "";
|
|
@@ -74,11 +105,11 @@ export function createLaunchPlan(manager, args, env = process.env) {
|
|
|
74
105
|
classification,
|
|
75
106
|
realBinary,
|
|
76
107
|
startsProxy: classification.kind === "protected",
|
|
77
|
-
childEnv: {
|
|
108
|
+
childEnv: scrubChildSecrets({
|
|
78
109
|
...env,
|
|
79
110
|
DG_SHIM_ACTIVE: shimNonce(manager, env),
|
|
80
111
|
DG_SHIM_DEPTH: String(shimDepth(env) + 1)
|
|
81
|
-
}
|
|
112
|
+
})
|
|
82
113
|
};
|
|
83
114
|
}
|
|
84
115
|
export async function runPackageManager(manager, args, options = {}) {
|
|
@@ -99,15 +130,20 @@ export async function runPackageManager(manager, args, options = {}) {
|
|
|
99
130
|
stderr: `dg: ${manager} shim exec loop detected (DG_SHIM_DEPTH=${depth}) — refusing to re-enter\n`
|
|
100
131
|
};
|
|
101
132
|
}
|
|
102
|
-
if (depth === 1 && plan.startsProxy) {
|
|
103
|
-
const child = await spawnPackageManager(plan, args, options);
|
|
104
|
-
return {
|
|
105
|
-
exitCode: child.exitCode,
|
|
106
|
-
stdout: streamedOut(child.stdout, options),
|
|
107
|
-
stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
133
|
if (plan.startsProxy) {
|
|
134
|
+
// Reuse a parent dg proxy ONLY when one is genuinely live in this environment
|
|
135
|
+
// (the parent set DG_PROXY_ACTIVE and a loopback proxy URL). An env var such as
|
|
136
|
+
// a forged or stale DG_SHIM_DEPTH must never be what decides to skip
|
|
137
|
+
// verification: if no live dg proxy is detected, start one rather than run the
|
|
138
|
+
// install unproxied. DG_SHIM_DEPTH is only the >=2 infinite-recursion backstop.
|
|
139
|
+
if (inheritedDgProxyActive(options.env ?? process.env)) {
|
|
140
|
+
const child = await spawnPackageManager(plan, args, options);
|
|
141
|
+
return {
|
|
142
|
+
exitCode: child.exitCode,
|
|
143
|
+
stdout: streamedOut(child.stdout, options),
|
|
144
|
+
stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
111
147
|
if (!options.proxyVerdict) {
|
|
112
148
|
return runWithProductionProxy(plan, args, options);
|
|
113
149
|
}
|
|
@@ -130,7 +166,7 @@ export async function runPackageManager(manager, args, options = {}) {
|
|
|
130
166
|
return {
|
|
131
167
|
exitCode: child.exitCode,
|
|
132
168
|
stdout: streamedOut(child.stdout, options),
|
|
133
|
-
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
|
|
169
|
+
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
|
|
134
170
|
};
|
|
135
171
|
}
|
|
136
172
|
const child = await spawnPackageManager(plan, args, options);
|
|
@@ -152,6 +188,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
152
188
|
}
|
|
153
189
|
const restoreSignalHandlers = installProxySignalHandlers(proxy);
|
|
154
190
|
try {
|
|
191
|
+
const cacheDir = prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env);
|
|
155
192
|
const proxiedPlan = {
|
|
156
193
|
...plan,
|
|
157
194
|
childEnv: buildProxyChildEnv({
|
|
@@ -159,7 +196,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
159
196
|
baseEnv: plan.childEnv,
|
|
160
197
|
proxyUrl: proxy.proxyUrl,
|
|
161
198
|
caBundlePath: proxy.session.files.ca,
|
|
162
|
-
cacheDir
|
|
199
|
+
cacheDir
|
|
163
200
|
})
|
|
164
201
|
};
|
|
165
202
|
const child = await spawnPackageManager(proxiedPlan, args, options);
|
|
@@ -196,7 +233,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
196
233
|
return {
|
|
197
234
|
exitCode: child.exitCode,
|
|
198
235
|
stdout: streamedOut(child.stdout, options),
|
|
199
|
-
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
|
|
236
|
+
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
|
|
200
237
|
};
|
|
201
238
|
}
|
|
202
239
|
finally {
|
|
@@ -214,6 +251,12 @@ function scriptGateLine(plan, exitCode, options) {
|
|
|
214
251
|
...(options.projectDir ? { projectDir: options.projectDir } : {})
|
|
215
252
|
});
|
|
216
253
|
}
|
|
254
|
+
function setupNudgeAfterInstall(plan, exitCode, options) {
|
|
255
|
+
if (exitCode !== 0) {
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
return maybeSetupNudge(plan.classification.manager, { env: options.env ?? process.env });
|
|
259
|
+
}
|
|
217
260
|
export function deriveLiveView(state, phase, resolvedTotal) {
|
|
218
261
|
const verified = state.decisions.filter((decision) => decision.action === "pass").length;
|
|
219
262
|
const warnDecisions = state.decisions.filter((decision) => decision.action === "warn");
|
|
@@ -232,57 +275,43 @@ export function deriveLiveView(state, phase, resolvedTotal) {
|
|
|
232
275
|
...(blocked ? { blocked: describeBlockedInstall(blocked) } : {})
|
|
233
276
|
};
|
|
234
277
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const finish = (value) => {
|
|
243
|
-
if (settled)
|
|
278
|
+
export function prepareProxyWorker(classification, env) {
|
|
279
|
+
const worker = startProxyWorker(classification, env, undefined);
|
|
280
|
+
let discarded = false;
|
|
281
|
+
return {
|
|
282
|
+
worker,
|
|
283
|
+
discard: () => {
|
|
284
|
+
if (discarded) {
|
|
244
285
|
return;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
253
|
-
child = spawn(invocation.command, [...invocation.args], {
|
|
254
|
-
env,
|
|
255
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
256
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
286
|
+
}
|
|
287
|
+
discarded = true;
|
|
288
|
+
void worker.then((proxy) => {
|
|
289
|
+
if (!("decision" in proxy)) {
|
|
290
|
+
stopProxyWorker(proxy);
|
|
291
|
+
}
|
|
257
292
|
});
|
|
258
293
|
}
|
|
259
|
-
|
|
260
|
-
finish(undefined);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
register(child);
|
|
264
|
-
timer = setTimeout(() => {
|
|
265
|
-
try {
|
|
266
|
-
child.kill();
|
|
267
|
-
}
|
|
268
|
-
catch { /* already exited */ }
|
|
269
|
-
finish(undefined);
|
|
270
|
-
}, 30_000);
|
|
271
|
-
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
272
|
-
child.on("error", () => finish(undefined));
|
|
273
|
-
child.on("close", (code) => finish(code === 0 ? parsePipReportInstallCount(stdout) : undefined));
|
|
274
|
-
});
|
|
294
|
+
};
|
|
275
295
|
}
|
|
276
|
-
export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
296
|
+
export async function runWithProductionProxyLive(plan, args, options, onView, prepared) {
|
|
277
297
|
if (options.onStdout || options.onStderr) {
|
|
278
298
|
throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
|
|
279
299
|
}
|
|
280
300
|
const env = options.env ?? process.env;
|
|
281
301
|
maybeWarnRootWithoutState(env);
|
|
282
|
-
|
|
302
|
+
if (prepared && options.forceOverride) {
|
|
303
|
+
prepared.discard();
|
|
304
|
+
prepared = undefined;
|
|
305
|
+
}
|
|
306
|
+
const proxy = prepared
|
|
307
|
+
? await prepared.worker
|
|
308
|
+
: await startProxyWorker(plan.classification, env, options.forceOverride);
|
|
283
309
|
if ("decision" in proxy) {
|
|
284
310
|
return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
|
|
285
311
|
}
|
|
312
|
+
if (options.preverified && options.preverified.length > 0) {
|
|
313
|
+
writePreverifiedFile(proxy.session.dir, options.preverified);
|
|
314
|
+
}
|
|
286
315
|
const restoreSignalHandlers = installProxySignalHandlers(proxy);
|
|
287
316
|
try {
|
|
288
317
|
const childEnv = buildProxyChildEnv({
|
|
@@ -290,22 +319,11 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
290
319
|
baseEnv: plan.childEnv,
|
|
291
320
|
proxyUrl: proxy.proxyUrl,
|
|
292
321
|
caBundlePath: proxy.session.files.ca,
|
|
293
|
-
cacheDir:
|
|
322
|
+
cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
|
|
294
323
|
});
|
|
295
324
|
const spawner = options.spawner ?? defaultSpawner;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (plan.classification.manager === "pip") {
|
|
299
|
-
const cached = cachedPipResolution(plan.realBinary.path ?? "", args);
|
|
300
|
-
if (cached) {
|
|
301
|
-
resolvedTotal = cached.count;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
void resolvePipInstallTotal(plan.realBinary.path ?? "", args, childEnv, (child) => { dryRunChild = child; })
|
|
305
|
-
.then((count) => { resolvedTotal = count; })
|
|
306
|
-
.catch(() => undefined);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
325
|
+
const hardened = applyScriptGateHardening(plan, args, childEnv, env);
|
|
326
|
+
const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
|
|
309
327
|
const poll = setInterval(() => {
|
|
310
328
|
onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
|
|
311
329
|
}, 90);
|
|
@@ -313,18 +331,12 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
313
331
|
try {
|
|
314
332
|
finished = await spawner({
|
|
315
333
|
binary: plan.realBinary.path ?? "",
|
|
316
|
-
args,
|
|
317
|
-
env:
|
|
334
|
+
args: hardened.args,
|
|
335
|
+
env: hardened.env
|
|
318
336
|
});
|
|
319
337
|
}
|
|
320
338
|
finally {
|
|
321
339
|
clearInterval(poll);
|
|
322
|
-
if (dryRunChild) {
|
|
323
|
-
try {
|
|
324
|
-
dryRunChild.kill();
|
|
325
|
-
}
|
|
326
|
-
catch { /* already exited */ }
|
|
327
|
-
}
|
|
328
340
|
}
|
|
329
341
|
const proxyState = readProxySessionState(proxy.session);
|
|
330
342
|
onView(deriveLiveView(proxyState, "done", resolvedTotal));
|
|
@@ -342,13 +354,20 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
342
354
|
const outcome = installOutcome(finished.stdout);
|
|
343
355
|
return { exitCode: EXIT_INSTALL_BLOCKED, stdout: outcome, stderr: cacheOnlyNotice(outcome.length > 0) };
|
|
344
356
|
}
|
|
345
|
-
return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: scriptGateLine(plan, finished.exitCode, options) };
|
|
357
|
+
return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: `${scriptGateLine(plan, finished.exitCode, options)}${setupNudgeAfterInstall(plan, finished.exitCode, options)}` };
|
|
346
358
|
}
|
|
347
359
|
finally {
|
|
348
360
|
restoreSignalHandlers();
|
|
349
361
|
stopProxyWorker(proxy);
|
|
350
362
|
}
|
|
351
363
|
}
|
|
364
|
+
function prepareProxyCacheDir(manager, sessionDir, env) {
|
|
365
|
+
const cacheDir = `${sessionDir}/pm-cache`;
|
|
366
|
+
if (manager === "cargo") {
|
|
367
|
+
prepareCargoHome(cacheDir, userCargoHome(env));
|
|
368
|
+
}
|
|
369
|
+
return cacheDir;
|
|
370
|
+
}
|
|
352
371
|
function cacheOnlyNotice(installedFresh) {
|
|
353
372
|
const theme = createTheme(resolvePresentation().color);
|
|
354
373
|
const reason = installedFresh ? "used a saved copy" : "already installed";
|
|
@@ -359,6 +378,22 @@ function installOutcome(stdout) {
|
|
|
359
378
|
/^(added|changed|removed|updated) \d/.test(line.trim()));
|
|
360
379
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
361
380
|
}
|
|
381
|
+
// scriptGate.mode === "enforce" is applied HERE, at the spawn boundary: it
|
|
382
|
+
// rewrites the install args (--ignore-scripts) and child env
|
|
383
|
+
// (npm_config_ignore_scripts) for npm/yarn so a reputation-clean package can't
|
|
384
|
+
// run a lifecycle script. observe/off leave the install untouched. Without this
|
|
385
|
+
// the mode setting was inert — the verdict screens what is fetched, this gates
|
|
386
|
+
// what the fetched package is allowed to execute.
|
|
387
|
+
function applyScriptGateHardening(plan, args, childEnv, configEnv) {
|
|
388
|
+
const mode = loadUserConfig(configEnv).scriptGate.mode;
|
|
389
|
+
const manager = plan.classification.manager;
|
|
390
|
+
const hardenedArgs = scriptGateInstallArgs({ mode, manager, args, env: childEnv });
|
|
391
|
+
const gateEnv = scriptGateChildEnv({ mode, manager, args, env: childEnv });
|
|
392
|
+
return {
|
|
393
|
+
args: hardenedArgs,
|
|
394
|
+
env: Object.keys(gateEnv).length > 0 ? { ...childEnv, ...gateEnv } : childEnv
|
|
395
|
+
};
|
|
396
|
+
}
|
|
362
397
|
async function spawnPackageManager(plan, args, options) {
|
|
363
398
|
if (!plan.realBinary.path) {
|
|
364
399
|
return {
|
|
@@ -368,10 +403,11 @@ async function spawnPackageManager(plan, args, options) {
|
|
|
368
403
|
};
|
|
369
404
|
}
|
|
370
405
|
const spawner = options.spawner ?? defaultSpawner;
|
|
406
|
+
const hardened = applyScriptGateHardening(plan, args, plan.childEnv, options.env ?? process.env);
|
|
371
407
|
return spawner({
|
|
372
408
|
binary: plan.realBinary.path,
|
|
373
|
-
args,
|
|
374
|
-
env:
|
|
409
|
+
args: hardened.args,
|
|
410
|
+
env: hardened.env,
|
|
375
411
|
onStdout: options.onStdout,
|
|
376
412
|
onStderr: options.onStderr
|
|
377
413
|
});
|
|
@@ -436,6 +472,41 @@ function renderDecisions(decisions, env) {
|
|
|
436
472
|
const visible = isCiEnv(env) ? decisions.filter((decision) => decision.action !== "pass") : decisions;
|
|
437
473
|
return visible.map((decision) => redactSecrets(renderInstallDecision(decision))).join("");
|
|
438
474
|
}
|
|
475
|
+
export function loadProjectCooldownExemptions(env, cwd = process.cwd()) {
|
|
476
|
+
try {
|
|
477
|
+
const root = findProjectRoot(cwd, env);
|
|
478
|
+
if (!root) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
const file = loadDgFile(root);
|
|
482
|
+
if (!file.readable) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
return honoredOverrides(file, root, env, trustsProjectOverrides(env)).exemptions;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
export function dgFileExemptionsNotice(exemptions) {
|
|
492
|
+
if (exemptions.length === 0) {
|
|
493
|
+
return "";
|
|
494
|
+
}
|
|
495
|
+
const plural = exemptions.length === 1 ? "exemption" : "exemptions";
|
|
496
|
+
return `dg: applying ${exemptions.length} cooldown ${plural} from this project's dg.json\n`;
|
|
497
|
+
}
|
|
498
|
+
let dgFileExemptionNoticeWritten = false;
|
|
499
|
+
function maybeNoticeDgFileExemptions(exemptions) {
|
|
500
|
+
if (dgFileExemptionNoticeWritten) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const notice = dgFileExemptionsNotice(exemptions);
|
|
504
|
+
if (!notice) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
dgFileExemptionNoticeWritten = true;
|
|
508
|
+
process.stderr.write(notice);
|
|
509
|
+
}
|
|
439
510
|
async function startProxyWorker(classification, env, forceOverride) {
|
|
440
511
|
const paths = resolveDgPaths(env);
|
|
441
512
|
const session = createSessionSync(paths);
|
|
@@ -450,11 +521,15 @@ async function startProxyWorker(classification, env, forceOverride) {
|
|
|
450
521
|
throw new Error("production proxy worker is not built");
|
|
451
522
|
}
|
|
452
523
|
const config = loadUserConfig(env);
|
|
524
|
+
const exemptions = loadProjectCooldownExemptions(env);
|
|
525
|
+
maybeNoticeDgFileExemptions(exemptions);
|
|
526
|
+
const exemptionsEnv = writeCooldownExemptionsFile(session.dir, exemptions);
|
|
453
527
|
const child = spawn(process.execPath, [workerPath, sessionBootstrapPath, config.api.baseUrl], {
|
|
454
528
|
env: {
|
|
455
529
|
...env,
|
|
456
530
|
DG_PROXY_CLASSIFICATION: JSON.stringify(classification),
|
|
457
|
-
...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {})
|
|
531
|
+
...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {}),
|
|
532
|
+
...exemptionsEnv
|
|
458
533
|
},
|
|
459
534
|
stdio: ["pipe", "pipe", "pipe"]
|
|
460
535
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
|
|
2
|
+
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
3
|
+
export function resolveSpawnInvocation(binary, args, platform = process.platform) {
|
|
4
|
+
if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
|
|
5
|
+
return { command: binary, args, windowsVerbatimArguments: false };
|
|
6
|
+
}
|
|
7
|
+
const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
|
|
8
|
+
return {
|
|
9
|
+
command: process.env.comspec ?? "cmd.exe",
|
|
10
|
+
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
11
|
+
windowsVerbatimArguments: true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function escapeCmdCommand(command) {
|
|
15
|
+
return command.replace(CMD_META_CHARS, "^$1");
|
|
16
|
+
}
|
|
17
|
+
// cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
|
|
18
|
+
function escapeCmdArgument(argument) {
|
|
19
|
+
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
20
|
+
return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
|
|
21
|
+
}
|
package/dist/policy/cooldown.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { parseCooldownAge } from "../config/settings.js";
|
|
2
|
+
import { cooldownExemptionActive } from "../project/dgfile.js";
|
|
3
|
+
import { canonicalCooldownName, normalizePypiName } from "./pypi-name.js";
|
|
4
|
+
export { normalizePypiName };
|
|
2
5
|
export function durationToDays(value) {
|
|
3
6
|
const trimmed = value.trim();
|
|
4
7
|
if (trimmed === "" || trimmed === "0" || trimmed === "off") {
|
|
@@ -28,23 +31,33 @@ export function effectiveCooldownAge(config, env, ecosystem) {
|
|
|
28
31
|
: config.cooldown.cargoAge;
|
|
29
32
|
return perEcosystem !== "" ? perEcosystem : config.cooldown.age;
|
|
30
33
|
}
|
|
31
|
-
function normalizePypiName(name) {
|
|
32
|
-
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
33
|
-
}
|
|
34
34
|
export function isCooldownExempt(packageName, exempt, ecosystem) {
|
|
35
|
-
const name = ecosystem
|
|
35
|
+
const name = ecosystem ? canonicalCooldownName(ecosystem, packageName) : packageName;
|
|
36
36
|
return exempt
|
|
37
37
|
.split(",")
|
|
38
38
|
.map((entry) => entry.trim())
|
|
39
39
|
.filter(Boolean)
|
|
40
40
|
.some((pattern) => {
|
|
41
|
-
const candidate = ecosystem
|
|
41
|
+
const candidate = ecosystem ? canonicalCooldownName(ecosystem, pattern) : pattern;
|
|
42
42
|
if (candidate.endsWith("*")) {
|
|
43
43
|
return name.startsWith(candidate.slice(0, -1));
|
|
44
44
|
}
|
|
45
45
|
return name === candidate;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
+
export function isCooldownExemptByDgFile(packageName, ecosystem, exemptions, now = new Date()) {
|
|
49
|
+
if (ecosystem !== "npm" && ecosystem !== "pypi" && ecosystem !== "cargo") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const target = canonicalCooldownName(ecosystem, packageName);
|
|
53
|
+
return exemptions.some((e) => {
|
|
54
|
+
if (e.ecosystem !== ecosystem) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const candidate = canonicalCooldownName(ecosystem, e.name);
|
|
58
|
+
return candidate === target && cooldownExemptionActive(e, now);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
48
61
|
export function cooldownRequestParam(config, env, ecosystem, packageName) {
|
|
49
62
|
const minAgeDays = durationToDays(effectiveCooldownAge(config, env, ecosystem));
|
|
50
63
|
if (minAgeDays <= 0) {
|
package/dist/policy/evaluate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { recordAuditEvent } from "../audit/events.js";
|
|
2
2
|
import { DEFAULT_CONFIG } from "../config/settings.js";
|
|
3
3
|
export function resolveEffectivePolicy(options) {
|
|
4
4
|
if (options.orgPolicy) {
|
|
@@ -69,21 +69,21 @@ export function applyForceOverride(options, env = process.env) {
|
|
|
69
69
|
return {
|
|
70
70
|
allowed: false,
|
|
71
71
|
reason: "force override was not requested",
|
|
72
|
-
|
|
72
|
+
auditRecorded: false
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
if (options.currentAction !== "block") {
|
|
76
76
|
return {
|
|
77
77
|
allowed: false,
|
|
78
78
|
reason: "force override is only valid for block decisions",
|
|
79
|
-
|
|
79
|
+
auditRecorded: false
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
if (!options.policy.allowForceOverride) {
|
|
83
83
|
return {
|
|
84
84
|
allowed: false,
|
|
85
85
|
reason: "force override is disabled by policy",
|
|
86
|
-
|
|
86
|
+
auditRecorded: false
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
const event = {
|
|
@@ -93,11 +93,10 @@ export function applyForceOverride(options, env = process.env) {
|
|
|
93
93
|
policyMode: options.policy.mode,
|
|
94
94
|
createdAt: (options.now ?? new Date()).toISOString()
|
|
95
95
|
};
|
|
96
|
-
recordAuditEvent(event, env);
|
|
97
96
|
return {
|
|
98
97
|
allowed: true,
|
|
99
98
|
reason: event.reason,
|
|
100
|
-
|
|
99
|
+
auditRecorded: recordAuditEvent(event, env)
|
|
101
100
|
};
|
|
102
101
|
}
|
|
103
102
|
function findTrustedAllowlist(packageName, policy, allowlists) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizePypiName(name) {
|
|
2
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
3
|
+
}
|
|
4
|
+
// crates.io is case-insensitive and treats '-' and '_' as equivalent, so fold
|
|
5
|
+
// both to a single canonical form for exemption matching.
|
|
6
|
+
export function normalizeCargoName(name) {
|
|
7
|
+
return name.toLowerCase().replace(/_/g, "-");
|
|
8
|
+
}
|
|
9
|
+
export function canonicalCooldownName(ecosystem, name) {
|
|
10
|
+
if (ecosystem === "pypi") {
|
|
11
|
+
return normalizePypiName(name);
|
|
12
|
+
}
|
|
13
|
+
if (ecosystem === "cargo") {
|
|
14
|
+
return normalizeCargoName(name);
|
|
15
|
+
}
|
|
16
|
+
return name;
|
|
17
|
+
}
|
|
@@ -19,8 +19,9 @@ export function colorEnabled(input) {
|
|
|
19
19
|
if (input.forceColorFlag) {
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const forceColor = input.env.FORCE_COLOR;
|
|
23
|
+
if (forceColor !== undefined) {
|
|
24
|
+
return forceColor !== "0" && forceColor !== "false";
|
|
24
25
|
}
|
|
25
26
|
if (input.noColorFlag) {
|
|
26
27
|
return false;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const SITE = "https://westbayberry.com";
|
|
2
|
+
// Only npm and pypi have public, indexable per-package pages today; cargo and
|
|
3
|
+
// other ecosystems have none, so return null rather than link to a 404.
|
|
4
|
+
export function packagePageUrl(ecosystem, name) {
|
|
5
|
+
if (ecosystem !== "npm" && ecosystem !== "pypi") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return `${SITE}/${ecosystem}/${name}`;
|
|
9
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const ANSI = {
|
|
2
|
-
block: "
|
|
3
|
-
warn: "
|
|
4
|
-
pass: "
|
|
5
|
-
unknown: "
|
|
6
|
-
muted: "
|
|
7
|
-
accent: "
|
|
2
|
+
block: "\u001b[31m",
|
|
3
|
+
warn: "\u001b[33m",
|
|
4
|
+
pass: "\u001b[32m",
|
|
5
|
+
unknown: "\u001b[36m",
|
|
6
|
+
muted: "\u001b[2m",
|
|
7
|
+
accent: "\u001b[1m"
|
|
8
8
|
};
|
|
9
|
-
const RESET = "
|
|
9
|
+
const RESET = "\u001b[0m";
|
|
10
10
|
const BADGES = {
|
|
11
11
|
block: { word: "BLOCK", glyph: "✘", role: "block" },
|
|
12
12
|
warn: { word: "WARN", glyph: "⚠", role: "warn" },
|