@westbayberry/dg 2.2.0 → 2.3.1
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/dist/agents/claude-code.js +10 -0
- package/dist/agents/codex.js +1 -1
- package/dist/agents/cursor.js +6 -1
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/persistence.js +68 -2
- package/dist/agents/registry.js +4 -3
- package/dist/agents/routing.js +118 -0
- package/dist/agents/windsurf.js +1 -1
- package/dist/audit/rules.js +6 -2
- package/dist/auth/store.js +9 -2
- package/dist/commands/agents.js +45 -1
- package/dist/commands/audit.js +6 -0
- package/dist/commands/setup.js +1 -2
- package/dist/commands/uninstall.js +2 -1
- package/dist/config/settings.js +35 -4
- package/dist/install-ui/LiveInstall.js +3 -2
- package/dist/install-ui/block-render.js +17 -13
- package/dist/launcher/agent-check.js +455 -25
- package/dist/launcher/agent-hook-exec.js +1 -1
- package/dist/launcher/agent-hook-io.js +12 -4
- package/dist/launcher/classify.js +27 -1
- package/dist/launcher/env.js +39 -7
- package/dist/launcher/install-preflight.js +15 -6
- package/dist/launcher/live-install.js +4 -3
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +4 -1
- package/dist/launcher/preflight-prompt.js +4 -3
- package/dist/launcher/run.js +90 -18
- package/dist/project/dgfile.js +2 -1
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/metadata-map.js +29 -13
- package/dist/proxy/server.js +130 -14
- package/dist/runtime/first-run.js +2 -1
- package/dist/sbom-ui/inventory.js +5 -1
- package/dist/scan/staged.js +31 -8
- package/dist/scan-ui/hooks/useScan.js +4 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +1 -1
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +3 -3
- package/dist/setup/plan.js +156 -12
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/wizard.js +9 -1
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/verify/local.js +2 -2
- package/dist/verify/package-check.js +3 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { formatScreenedNote } from "../launcher/agent-check.js";
|
|
2
3
|
import { dirExists, mergedJsonHook, LEGACY_AGENT_HOOK_SENTINEL } from "./persistence.js";
|
|
3
4
|
const SIGNATURE = "hook-exec claude-code";
|
|
4
5
|
function configPath(home) {
|
|
@@ -74,6 +75,15 @@ function parseInput(stdin) {
|
|
|
74
75
|
// silent ({}) on allow so we never override the user's normal permission flow.
|
|
75
76
|
function emitDecision(verdict) {
|
|
76
77
|
if (verdict.decision === "allow") {
|
|
78
|
+
const note = verdict.screened ? formatScreenedNote(verdict.screened) : "";
|
|
79
|
+
if (note) {
|
|
80
|
+
return {
|
|
81
|
+
stdout: JSON.stringify({
|
|
82
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: note },
|
|
83
|
+
}),
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
77
87
|
return { stdout: "{}", exitCode: 0 };
|
|
78
88
|
}
|
|
79
89
|
return {
|
package/dist/agents/codex.js
CHANGED
|
@@ -21,7 +21,7 @@ function emitDecision(verdict) {
|
|
|
21
21
|
return { stdout: "", exitCode: 0 };
|
|
22
22
|
}
|
|
23
23
|
const reason = verdict.reason ?? "Dependency Guardian firewall";
|
|
24
|
-
const suffix = verdict.decision === "ask" ? " (flagged for review —
|
|
24
|
+
const suffix = verdict.decision === "ask" ? " (dg flagged this for human review — not auto-approved)" : "";
|
|
25
25
|
return {
|
|
26
26
|
stdout: JSON.stringify({ decision: "block", reason: `${reason}${suffix}` }),
|
|
27
27
|
exitCode: 0,
|
package/dist/agents/cursor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { formatScreenedNote } from "../launcher/agent-check.js";
|
|
2
3
|
import { dirExists, mergedJsonHook, readSettings } from "./persistence.js";
|
|
3
4
|
const SIGNATURE = "hook-exec cursor";
|
|
4
5
|
function configPath(home) {
|
|
@@ -73,7 +74,11 @@ function parseInput(stdin) {
|
|
|
73
74
|
}
|
|
74
75
|
function emitDecision(verdict) {
|
|
75
76
|
if (verdict.decision === "allow") {
|
|
76
|
-
|
|
77
|
+
const note = verdict.screened ? formatScreenedNote(verdict.screened) : "";
|
|
78
|
+
return {
|
|
79
|
+
stdout: JSON.stringify(note ? { permission: "allow", agent_message: note } : { permission: "allow" }),
|
|
80
|
+
exitCode: 0,
|
|
81
|
+
};
|
|
77
82
|
}
|
|
78
83
|
const reason = verdict.reason ?? "Dependency Guardian firewall";
|
|
79
84
|
return {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readServiceState } from "../service/state.js";
|
|
2
|
+
export function readNetworkGatePosture(env = process.env) {
|
|
3
|
+
try {
|
|
4
|
+
const { state } = readServiceState(env);
|
|
5
|
+
if (state.running && state.proxy) {
|
|
6
|
+
return { live: true, proxyUrl: state.proxy.proxyUrl };
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// fall through to the off posture; a missing/unreadable service state means
|
|
11
|
+
// the gate is not live, which is exactly what we report.
|
|
12
|
+
}
|
|
13
|
+
return { live: false };
|
|
14
|
+
}
|
|
15
|
+
export const GATE_OFF_COVERAGE_GAPS = [
|
|
16
|
+
"absolute-path installs (e.g. /usr/local/bin/npm install evil)",
|
|
17
|
+
"manifest-only installs (npm ci, npm install with no package named)",
|
|
18
|
+
"dynamically-built or stdin-fed commands the static parser defers on",
|
|
19
|
+
"recognized-but-unsupported managers (bun, deno, poetry, pdm)",
|
|
20
|
+
];
|
|
21
|
+
export const GATE_ENABLE_HINT = "dg setup --service --yes && dg service start";
|
|
@@ -81,12 +81,16 @@ async function recordHookEntry(ctx, legacySentinels, created) {
|
|
|
81
81
|
for (const sentinel of legacySentinels) {
|
|
82
82
|
await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel });
|
|
83
83
|
}
|
|
84
|
+
// Preserve the dg-created provenance on re-apply: `created` is false the second
|
|
85
|
+
// time the hook is installed, but if dg created the file originally, uninstall
|
|
86
|
+
// must still remove it, so keep the marker.
|
|
87
|
+
const dgCreated = created || (await wasCreatedByDg(ctx));
|
|
84
88
|
await recordCleanupEntry(ctx.paths, {
|
|
85
89
|
kind: "agent-hook",
|
|
86
90
|
path: ctx.settingsPath,
|
|
87
91
|
mode: "mode1",
|
|
88
92
|
sentinel: agentHookSentinel(ctx.agent),
|
|
89
|
-
...(
|
|
93
|
+
...(dgCreated ? { original: "dg-created" } : {}),
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
async function removeHookEntries(ctx, legacySentinels) {
|
|
@@ -95,6 +99,48 @@ async function removeHookEntries(ctx, legacySentinels) {
|
|
|
95
99
|
await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel });
|
|
96
100
|
}
|
|
97
101
|
}
|
|
102
|
+
function findHookCommand(node, signature) {
|
|
103
|
+
if (typeof node === "string") {
|
|
104
|
+
return node.includes(signature) ? node : null;
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(node)) {
|
|
107
|
+
for (const value of node) {
|
|
108
|
+
const found = findHookCommand(value, signature);
|
|
109
|
+
if (found) {
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (node && typeof node === "object") {
|
|
116
|
+
for (const value of Object.values(node)) {
|
|
117
|
+
const found = findHookCommand(value, signature);
|
|
118
|
+
if (found) {
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function hookResolvesCheck(settings, agent) {
|
|
127
|
+
const command = findHookCommand(settings, `hook-exec ${agent}`);
|
|
128
|
+
if (!command) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const head = command.split(/\s+hook-exec\s+/)[0] ?? command;
|
|
132
|
+
const broken = head
|
|
133
|
+
.split(/\s+/)
|
|
134
|
+
.filter((token) => token.startsWith("/"))
|
|
135
|
+
.filter((token) => !existsSync(token));
|
|
136
|
+
return {
|
|
137
|
+
name: "hook command resolves",
|
|
138
|
+
ok: broken.length === 0,
|
|
139
|
+
detail: broken.length === 0
|
|
140
|
+
? "dg path in the hook resolves"
|
|
141
|
+
: `hook references a missing path (${broken.join(", ")}); re-run 'dg agents on ${agent}'`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
98
144
|
export function mergedJsonHook(config) {
|
|
99
145
|
const legacy = config.legacySentinels ?? [];
|
|
100
146
|
return {
|
|
@@ -138,14 +184,22 @@ export function mergedJsonHook(config) {
|
|
|
138
184
|
if (!present) {
|
|
139
185
|
return checks;
|
|
140
186
|
}
|
|
187
|
+
let settings = null;
|
|
141
188
|
let installed = false;
|
|
142
189
|
try {
|
|
143
|
-
|
|
190
|
+
settings = readSettings(ctx.settingsPath).settings;
|
|
191
|
+
installed = config.isInstalled(settings);
|
|
144
192
|
}
|
|
145
193
|
catch {
|
|
146
194
|
installed = false;
|
|
147
195
|
}
|
|
148
196
|
checks.push({ name: config.checkName, ok: installed, detail: installed ? "installed" : "not installed" });
|
|
197
|
+
if (installed && settings) {
|
|
198
|
+
const resolves = hookResolvesCheck(settings, ctx.agent);
|
|
199
|
+
if (resolves) {
|
|
200
|
+
checks.push(resolves);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
149
203
|
return checks;
|
|
150
204
|
},
|
|
151
205
|
reverseEntry(entry, removed, missing, warnings) {
|
|
@@ -227,6 +281,18 @@ export function ownedJsonHook(config) {
|
|
|
227
281
|
}
|
|
228
282
|
const installed = ownsFile(ctx.settingsPath);
|
|
229
283
|
checks.push({ name: config.checkName, ok: installed, detail: installed ? "installed" : "present but not dg-owned" });
|
|
284
|
+
if (installed) {
|
|
285
|
+
try {
|
|
286
|
+
const settings = JSON.parse(readFileSync(ctx.settingsPath, "utf8"));
|
|
287
|
+
const resolves = hookResolvesCheck(settings, ctx.agent);
|
|
288
|
+
if (resolves) {
|
|
289
|
+
checks.push(resolves);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// unreadable file: the installed check already reflects the broken state
|
|
294
|
+
}
|
|
295
|
+
}
|
|
230
296
|
return checks;
|
|
231
297
|
},
|
|
232
298
|
reverseEntry(entry, removed, missing, warnings) {
|
package/dist/agents/registry.js
CHANGED
|
@@ -37,9 +37,10 @@ export function defaultDgCommand(agent) {
|
|
|
37
37
|
catch {
|
|
38
38
|
// keep argv[1]; the hook still resolves via PATH as a fallback
|
|
39
39
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
40
|
+
// Run the dg script via its own shebang rather than baking process.execPath:
|
|
41
|
+
// a version-pinned node path (e.g. .../Cellar/node/<ver>/bin/node) vanishes on
|
|
42
|
+
// a node upgrade and would silently disable the hook.
|
|
43
|
+
return `${bin} ${agentHookSignature(agent)}`;
|
|
43
44
|
}
|
|
44
45
|
export function resolveAgentHookContext(agent, options = {}) {
|
|
45
46
|
const env = options.env ?? process.env;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { buildAgentRoutingEnv } from "../launcher/env.js";
|
|
4
|
+
import { readServiceState } from "../service/state.js";
|
|
5
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
6
|
+
import { readSettings, writeSettingsAtomic } from "./persistence.js";
|
|
7
|
+
import { getAgent } from "./registry.js";
|
|
8
|
+
// The agent's install hook screens statically; this routes the agent's ACTUAL
|
|
9
|
+
// fetches through dg's proxy so a wrapped/dynamic install (eval, $VAR, python -m
|
|
10
|
+
// pip, …) the static hook can't decode is still screened at fetch time. Only
|
|
11
|
+
// applied when `dg service` is running, so the proxy endpoint baked into the
|
|
12
|
+
// agent's env is always live — never a dead HTTPS_PROXY that breaks installs.
|
|
13
|
+
const CODEX_BEGIN = "# >>> dg routing >>>";
|
|
14
|
+
const CODEX_END = "# <<< dg routing <<<";
|
|
15
|
+
function backupPath(agent, env) {
|
|
16
|
+
return join(resolveDgPaths(env).stateDir, `routing-${agent}.json`);
|
|
17
|
+
}
|
|
18
|
+
export function resolveServiceRoutingEnv(env = process.env) {
|
|
19
|
+
const { state } = readServiceState(env);
|
|
20
|
+
if (!state.running || !state.proxy) {
|
|
21
|
+
return { error: "dg service is not running — run 'dg service start' so the proxy gate is live before routing agents through it" };
|
|
22
|
+
}
|
|
23
|
+
return { env: buildAgentRoutingEnv(state.proxy.proxyUrl, state.proxy.caPath), proxyUrl: state.proxy.proxyUrl };
|
|
24
|
+
}
|
|
25
|
+
function escapeRegex(value) {
|
|
26
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
}
|
|
28
|
+
function stripCodexBlock(content) {
|
|
29
|
+
const block = new RegExp(`\\n?${escapeRegex(CODEX_BEGIN)}[\\s\\S]*?${escapeRegex(CODEX_END)}\\n?`, "g");
|
|
30
|
+
return content.replace(block, "\n").replace(/\n{3,}/g, "\n\n");
|
|
31
|
+
}
|
|
32
|
+
function applyClaudeRouting(settingsPath, routing, backup) {
|
|
33
|
+
const { settings } = readSettings(settingsPath);
|
|
34
|
+
const envObj = typeof settings.env === "object" && settings.env !== null ? { ...settings.env } : {};
|
|
35
|
+
const prior = {};
|
|
36
|
+
for (const [k, v] of Object.entries(routing)) {
|
|
37
|
+
prior[k] = k in envObj ? String(envObj[k]) : null;
|
|
38
|
+
envObj[k] = v;
|
|
39
|
+
}
|
|
40
|
+
settings.env = envObj;
|
|
41
|
+
writeSettingsAtomic(settingsPath, settings);
|
|
42
|
+
mkdirSync(dirname(backup), { recursive: true, mode: 0o700 });
|
|
43
|
+
writeFileSync(backup, `${JSON.stringify({ kind: "claude-env", path: settingsPath, prior }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
function removeClaudeRouting(settingsPath, backup) {
|
|
46
|
+
if (!existsSync(backup)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let prior = {};
|
|
50
|
+
try {
|
|
51
|
+
prior = JSON.parse(readFileSync(backup, "utf8")).prior ?? {};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
prior = {};
|
|
55
|
+
}
|
|
56
|
+
if (existsSync(settingsPath)) {
|
|
57
|
+
const { settings } = readSettings(settingsPath);
|
|
58
|
+
const envObj = typeof settings.env === "object" && settings.env !== null ? { ...settings.env } : {};
|
|
59
|
+
for (const [k, v] of Object.entries(prior)) {
|
|
60
|
+
if (v === null) {
|
|
61
|
+
delete envObj[k];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
envObj[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(envObj).length === 0) {
|
|
68
|
+
delete settings.env;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
settings.env = envObj;
|
|
72
|
+
}
|
|
73
|
+
writeSettingsAtomic(settingsPath, settings);
|
|
74
|
+
}
|
|
75
|
+
rmSync(backup, { force: true });
|
|
76
|
+
}
|
|
77
|
+
function applyCodexRouting(configPath, routing) {
|
|
78
|
+
const content = existsSync(configPath) ? stripCodexBlock(readFileSync(configPath, "utf8")) : "";
|
|
79
|
+
if (/^\s*\[shell_environment_policy\]/m.test(content)) {
|
|
80
|
+
return { applied: false, detail: "~/.codex/config.toml already defines [shell_environment_policy] — add dg's proxy vars (HTTPS_PROXY, NODE_EXTRA_CA_CERTS, …) to its `set` table manually" };
|
|
81
|
+
}
|
|
82
|
+
const set = Object.entries(routing).map(([k, v]) => `${k} = ${JSON.stringify(v)}`).join(", ");
|
|
83
|
+
const block = `${CODEX_BEGIN}\n[shell_environment_policy]\nset = { ${set} }\n${CODEX_END}\n`;
|
|
84
|
+
const next = content === "" || content.endsWith("\n") ? `${content}${block}` : `${content}\n${block}`;
|
|
85
|
+
writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
|
|
86
|
+
return { applied: true, detail: configPath };
|
|
87
|
+
}
|
|
88
|
+
function removeCodexRouting(configPath) {
|
|
89
|
+
if (!existsSync(configPath)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(configPath, stripCodexBlock(readFileSync(configPath, "utf8")), { encoding: "utf8" });
|
|
93
|
+
}
|
|
94
|
+
function codexConfigPath(home) {
|
|
95
|
+
return join(home, ".codex", "config.toml");
|
|
96
|
+
}
|
|
97
|
+
export function applyAgentRouting(agent, routing, home, env = process.env) {
|
|
98
|
+
if (agent === "codex") {
|
|
99
|
+
return applyCodexRouting(codexConfigPath(home), routing);
|
|
100
|
+
}
|
|
101
|
+
// Every other supported agent reads a JSON settings file with an `env` block.
|
|
102
|
+
applyClaudeRouting(getAgent(agent).configPath(home), routing, backupPath(agent, env));
|
|
103
|
+
return { applied: true, detail: getAgent(agent).configPath(home) };
|
|
104
|
+
}
|
|
105
|
+
export function removeAgentRouting(agent, home, env = process.env) {
|
|
106
|
+
if (agent === "codex") {
|
|
107
|
+
removeCodexRouting(codexConfigPath(home));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
removeClaudeRouting(getAgent(agent).configPath(home), backupPath(agent, env));
|
|
111
|
+
}
|
|
112
|
+
export function routingInstalled(agent, home, env = process.env) {
|
|
113
|
+
if (agent === "codex") {
|
|
114
|
+
const p = codexConfigPath(home);
|
|
115
|
+
return existsSync(p) && readFileSync(p, "utf8").includes(CODEX_BEGIN);
|
|
116
|
+
}
|
|
117
|
+
return existsSync(backupPath(agent, env));
|
|
118
|
+
}
|
package/dist/agents/windsurf.js
CHANGED
|
@@ -64,7 +64,7 @@ function emitDecision(verdict) {
|
|
|
64
64
|
return { stdout: "", exitCode: 0 };
|
|
65
65
|
}
|
|
66
66
|
const reason = verdict.reason ?? "Dependency Guardian firewall";
|
|
67
|
-
const suffix = verdict.decision === "ask" ? " (flagged for review —
|
|
67
|
+
const suffix = verdict.decision === "ask" ? " (dg flagged this for human review — not auto-approved)" : "";
|
|
68
68
|
return { stdout: `${reason}${suffix}\n`, exitCode: 2 };
|
|
69
69
|
}
|
|
70
70
|
function probeHookSupport(home) {
|
package/dist/audit/rules.js
CHANGED
|
@@ -384,7 +384,7 @@ export const CONTENT_RULES = [
|
|
|
384
384
|
{
|
|
385
385
|
id: "db-connection-string",
|
|
386
386
|
re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:@\s/]+:[^@\s/]+@[^/\s]+/u,
|
|
387
|
-
allow: /:\/\/[^:@/]+:(password|pass|user|example|changeme|xxx+|\*+)@|@(localhost|127\.0\.0\.1|example\.|host)/iu,
|
|
387
|
+
allow: /:\/\/[^:@/]+:(password|pass|user|example|changeme|xxx+|\*+)@|@(localhost|127\.0\.0\.1|example\.|host\b)/iu,
|
|
388
388
|
category: "bundled-secret",
|
|
389
389
|
severity: 4,
|
|
390
390
|
title: "Database connection string with password embedded in a published file",
|
|
@@ -421,6 +421,10 @@ export const RISKY_SCRIPT_NAMES = [
|
|
|
421
421
|
"preuninstall",
|
|
422
422
|
"uninstall"
|
|
423
423
|
];
|
|
424
|
-
|
|
424
|
+
// Best-effort heuristic on the LOCAL audit only; a public regex is evadable, so
|
|
425
|
+
// the authoritative verdict is the behavioral scanner. Covers pipe-to-shell, eval,
|
|
426
|
+
// node -e, python -c, base64 -d, curl|sh, AND download-then-execute where the fetch
|
|
427
|
+
// is chained (&& / ;) to a shell or a freshly-downloaded file.
|
|
428
|
+
export const DANGEROUS_SCRIPT_RE = /\|\s*(?:sh|bash|zsh)\b|\beval\s|node\s+-e\b|python[0-9.]*\s+-c\b|base64\s+-d|(?:curl|wget|fetch)\b[^\n]*\|\s*\w*sh\b|(?:curl|wget|fetch)\b[^\n]*(?:&&|;)\s*(?:sh|bash|zsh|source|\.\/)/iu;
|
|
425
429
|
export const INVISIBLE_UNICODE_RE = /[\u200B-\u200F\u2060-\u2064\u202A-\u202E\u2066-\u2069\uFEFF]/u;
|
|
426
430
|
export const BIDI_OVERRIDE_RE = /[\u202A-\u202E\u2066-\u2069]/u;
|
package/dist/auth/store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { loadUserConfig, saveUserConfig, withUserConfigLock } from "../config/settings.js";
|
|
4
|
+
import { loadUserConfig, parseUrl, saveUserConfig, withUserConfigLock } from "../config/settings.js";
|
|
5
5
|
import { resolveDgPaths } from "../state/index.js";
|
|
6
6
|
import { envAuthToken } from "./env-token.js";
|
|
7
7
|
export class AuthError extends Error {
|
|
@@ -24,6 +24,13 @@ export function readAuthState(env = process.env) {
|
|
|
24
24
|
if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl || parsed.loggedInAt === undefined) {
|
|
25
25
|
throw new AuthError("unsupported auth state");
|
|
26
26
|
}
|
|
27
|
+
let apiBaseUrl;
|
|
28
|
+
try {
|
|
29
|
+
apiBaseUrl = parseUrl(parsed.apiBaseUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new AuthError("auth state has an invalid api base URL; run 'dg login' again");
|
|
33
|
+
}
|
|
27
34
|
const email = typeof parsed.email === "string" && parsed.email.length > 0 ? parsed.email : undefined;
|
|
28
35
|
const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? parsed.tier : undefined;
|
|
29
36
|
const name = typeof parsed.name === "string" && parsed.name.length > 0 ? parsed.name : undefined;
|
|
@@ -31,7 +38,7 @@ export function readAuthState(env = process.env) {
|
|
|
31
38
|
version: 1,
|
|
32
39
|
token: parsed.token,
|
|
33
40
|
tokenPreview: parsed.tokenPreview ?? redactToken(parsed.token),
|
|
34
|
-
apiBaseUrl
|
|
41
|
+
apiBaseUrl,
|
|
35
42
|
orgId: parsed.orgId ?? "",
|
|
36
43
|
loggedInAt: parsed.loggedInAt,
|
|
37
44
|
...(email ? { email } : {}),
|
package/dist/commands/agents.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { AGENT_IDS, AGENTS, agentLabel, collectAgentOffers, getAgent, isAgentId, resolveAgentHookContext, } from "../agents/registry.js";
|
|
3
3
|
import { AgentHookError } from "../agents/persistence.js";
|
|
4
|
+
import { applyAgentRouting, removeAgentRouting, resolveServiceRoutingEnv, routingInstalled } from "../agents/routing.js";
|
|
5
|
+
import { GATE_ENABLE_HINT, GATE_OFF_COVERAGE_GAPS, readNetworkGatePosture } from "../agents/gate-posture.js";
|
|
4
6
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
5
7
|
import { createTheme } from "../presentation/theme.js";
|
|
6
8
|
import { tildifyPath as tildify } from "../setup/plan.js";
|
|
@@ -101,6 +103,23 @@ function renderStatusCard(rows, theme) {
|
|
|
101
103
|
lines.push("", ` ${muted("protect all:")} ${accent("dg agents on")} ${muted("·")} ${muted("remove:")} ${accent("dg agents off")}`, "");
|
|
102
104
|
return lines.join("\n");
|
|
103
105
|
}
|
|
106
|
+
function gatePostureLines(env, theme) {
|
|
107
|
+
const accent = (text) => theme.paint("accent", text);
|
|
108
|
+
const muted = (text) => theme.paint("muted", text);
|
|
109
|
+
const posture = readNetworkGatePosture(env);
|
|
110
|
+
if (posture.live) {
|
|
111
|
+
return [` ${theme.paint("pass", "✓ network gate live")} ${muted(`at ${posture.proxyUrl} — installs are screened at fetch by artifact hash, however the command is written`)}`];
|
|
112
|
+
}
|
|
113
|
+
const lines = [
|
|
114
|
+
` ${theme.paint("warn", "⚠ network gate OFF — static pre-screen only")}`,
|
|
115
|
+
` ${muted("these are NOT screened until the gate is on:")}`,
|
|
116
|
+
];
|
|
117
|
+
for (const gap of GATE_OFF_COVERAGE_GAPS) {
|
|
118
|
+
lines.push(` ${muted(`· ${gap}`)}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(` ${muted("enable full fetch-time screening:")} ${accent(GATE_ENABLE_HINT)}`);
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
104
123
|
async function applyAgents(targets, env, home, theme, recordFixture) {
|
|
105
124
|
const accent = (text) => theme.paint("accent", text);
|
|
106
125
|
const muted = (text) => theme.paint("muted", text);
|
|
@@ -119,6 +138,16 @@ async function applyAgents(targets, env, home, theme, recordFixture) {
|
|
|
119
138
|
try {
|
|
120
139
|
await integration.apply({ ...ctx, dgCommand });
|
|
121
140
|
lines.push(` ${theme.paint("pass", `✓ ${integration.label} installs route through dg`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`);
|
|
141
|
+
// When the persistent proxy is live, also route the agent's real fetches
|
|
142
|
+
// through it so a wrapped/dynamic install the static hook can't decode is
|
|
143
|
+
// still screened at fetch time.
|
|
144
|
+
const routing = resolveServiceRoutingEnv(env);
|
|
145
|
+
if (!("error" in routing)) {
|
|
146
|
+
const r = applyAgentRouting(agent, routing.env, home, env);
|
|
147
|
+
lines.push(r.applied
|
|
148
|
+
? ` ${muted(" ↳ fetches also routed through the live dg proxy (fetch-time screening)")}`
|
|
149
|
+
: ` ${muted(` ↳ routing skipped: ${r.detail}`)}`);
|
|
150
|
+
}
|
|
122
151
|
if (integration.maturity === "unverified") {
|
|
123
152
|
lines.push(` ${muted(` hook installed from ${integration.label}'s documented schema; payload format not yet verified against a live ${integration.label} run`)}`);
|
|
124
153
|
}
|
|
@@ -135,7 +164,7 @@ async function applyAgents(targets, env, home, theme, recordFixture) {
|
|
|
135
164
|
if (lines.length === 0) {
|
|
136
165
|
return { exitCode: 0, stdout: ` ${muted("No unprotected agents detected.")} ${muted("See")} ${accent("dg agents")}\n`, stderr: "" };
|
|
137
166
|
}
|
|
138
|
-
lines.push(` ${muted("Reverse:")} ${accent("dg agents off")}`);
|
|
167
|
+
lines.push("", ...gatePostureLines(env, theme), "", ` ${muted("Reverse:")} ${accent("dg agents off")}`);
|
|
139
168
|
return { exitCode: failures > 0 && failures === targets.length ? 1 : 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
140
169
|
}
|
|
141
170
|
async function removeAgents(targets, env, home, theme) {
|
|
@@ -146,6 +175,7 @@ async function removeAgents(targets, env, home, theme) {
|
|
|
146
175
|
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
147
176
|
try {
|
|
148
177
|
const result = await integration.remove(ctx);
|
|
178
|
+
removeAgentRouting(agent, home, env);
|
|
149
179
|
lines.push(result.removed
|
|
150
180
|
? ` ${theme.paint("pass", `✓ ${integration.label} hook removed`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`
|
|
151
181
|
: ` ${muted(`· ${integration.label} — no dg hook was installed`)}`);
|
|
@@ -171,6 +201,20 @@ function checkAgents(targets, env, home) {
|
|
|
171
201
|
lines.push(` ${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
|
|
172
202
|
allOk = allOk && check.ok;
|
|
173
203
|
}
|
|
204
|
+
const routed = routingInstalled(agent, home, env);
|
|
205
|
+
lines.push(` ${routed ? "✓" : "·"} fetch-time routing: ${routed ? "installs routed through the dg proxy" : "not routed (start dg service, then re-run dg agents on)"}`);
|
|
206
|
+
}
|
|
207
|
+
const posture = readNetworkGatePosture(env);
|
|
208
|
+
lines.push("", "network gate:");
|
|
209
|
+
if (posture.live) {
|
|
210
|
+
lines.push(` ✓ live at ${posture.proxyUrl} — installs screened at fetch by artifact hash, however the command is written`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
lines.push(" ⚠ OFF — static pre-screen only. NOT screened until the gate is on:");
|
|
214
|
+
for (const gap of GATE_OFF_COVERAGE_GAPS) {
|
|
215
|
+
lines.push(` · ${gap}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push(` enable: ${GATE_ENABLE_HINT}`);
|
|
174
218
|
}
|
|
175
219
|
return { exitCode: allOk ? 0 : 1, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
176
220
|
}
|
package/dist/commands/audit.js
CHANGED
|
@@ -131,6 +131,12 @@ export async function maybeAudit(args) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
// Org policy must gate every upload, not just the consent prompt: a pre-granted
|
|
135
|
+
// consent (DG_AUDIT_UPLOAD=1 / config) must not transmit the tarball when the
|
|
136
|
+
// organization has disabled artifact upload.
|
|
137
|
+
if (decision.upload && (await teamPolicyBlocksUpload())) {
|
|
138
|
+
decision = { upload: false, reason: "deep audit upload is disabled by your organization's policy" };
|
|
139
|
+
}
|
|
134
140
|
if (shouldLaunchAuditTui({ format: gathered.parsed.format, outputPath: gathered.parsed.outputPath })) {
|
|
135
141
|
const uploadAbort = new AbortController();
|
|
136
142
|
const deepPromise = decision.upload
|
package/dist/commands/setup.js
CHANGED
|
@@ -270,8 +270,7 @@ function renderSecurityNotesText(theme) {
|
|
|
270
270
|
"",
|
|
271
271
|
" 1. dg can make mistakes.",
|
|
272
272
|
" A PASS verdict does not guarantee a package is safe. You are",
|
|
273
|
-
" responsible for what you install
|
|
274
|
-
" dependencies.",
|
|
273
|
+
" responsible for what you install.",
|
|
275
274
|
"",
|
|
276
275
|
" 2. By continuing you confirm you have read and understand the",
|
|
277
276
|
" Terms of Service and Privacy Policy.",
|
|
@@ -14,7 +14,8 @@ export const uninstallCommand = {
|
|
|
14
14
|
],
|
|
15
15
|
examples: ["dg uninstall", "dg uninstall --yes --keep-config", "dg uninstall --all --yes"],
|
|
16
16
|
details: [
|
|
17
|
-
"Removes only dg-owned writes (shims, shell-rc block, git hooks), tolerates missing or malformed state, runs twice safely, and preserves user content."
|
|
17
|
+
"Removes only dg-owned writes (shims, shell-rc block, git hooks), tolerates missing or malformed state, runs twice safely, and preserves user content.",
|
|
18
|
+
"Running 'npm uninstall -g @westbayberry/dg' on its own leaves these writes behind; the next npm or pip command then clears them automatically. Run this to remove everything immediately."
|
|
18
19
|
],
|
|
19
20
|
handler: (context) => uninstallHandler(context.args)
|
|
20
21
|
};
|
package/dist/config/settings.js
CHANGED
|
@@ -9,6 +9,8 @@ export const CONFIG_KEYS = Object.freeze([
|
|
|
9
9
|
"policy.trustProjectAllowlists",
|
|
10
10
|
"policy.allowForceOverride",
|
|
11
11
|
"policy.scriptHardening",
|
|
12
|
+
"policy.shimFailClosed",
|
|
13
|
+
"policy.strictEgress",
|
|
12
14
|
"scriptGate.mode",
|
|
13
15
|
"scriptGate.observe",
|
|
14
16
|
"gitHook.onWarn",
|
|
@@ -33,7 +35,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
33
35
|
mode: "block",
|
|
34
36
|
trustProjectAllowlists: false,
|
|
35
37
|
allowForceOverride: true,
|
|
36
|
-
scriptHardening: false
|
|
38
|
+
scriptHardening: false,
|
|
39
|
+
shimFailClosed: false,
|
|
40
|
+
strictEgress: false
|
|
37
41
|
},
|
|
38
42
|
scriptGate: {
|
|
39
43
|
mode: "observe",
|
|
@@ -48,7 +52,7 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
48
52
|
npmAge: "",
|
|
49
53
|
pypiAge: "",
|
|
50
54
|
cargoAge: "",
|
|
51
|
-
onUnknown: "
|
|
55
|
+
onUnknown: "block",
|
|
52
56
|
exempt: ""
|
|
53
57
|
},
|
|
54
58
|
audit: {
|
|
@@ -106,6 +110,17 @@ export function updateUserConfig(apply, env = process.env) {
|
|
|
106
110
|
return next;
|
|
107
111
|
});
|
|
108
112
|
}
|
|
113
|
+
// Whether repo-supplied project overrides (allowlists, cooldown exemptions,
|
|
114
|
+
// warn-decisions) are trusted wholesale. A corrupt config fails closed to
|
|
115
|
+
// untrusted so a hostile repo can never relax screening by breaking the config.
|
|
116
|
+
export function trustsProjectOverrides(env = process.env) {
|
|
117
|
+
try {
|
|
118
|
+
return loadUserConfig(env).policy.trustProjectAllowlists;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
export function getConfigValue(config, key) {
|
|
110
125
|
if (key === "api.baseUrl") {
|
|
111
126
|
return config.api.baseUrl;
|
|
@@ -125,6 +140,12 @@ export function getConfigValue(config, key) {
|
|
|
125
140
|
if (key === "policy.scriptHardening") {
|
|
126
141
|
return String(config.policy.scriptHardening);
|
|
127
142
|
}
|
|
143
|
+
if (key === "policy.shimFailClosed") {
|
|
144
|
+
return String(config.policy.shimFailClosed);
|
|
145
|
+
}
|
|
146
|
+
if (key === "policy.strictEgress") {
|
|
147
|
+
return String(config.policy.strictEgress);
|
|
148
|
+
}
|
|
128
149
|
if (key === "scriptGate.mode") {
|
|
129
150
|
return config.scriptGate.mode;
|
|
130
151
|
}
|
|
@@ -160,6 +181,8 @@ export function getConfigValue(config, key) {
|
|
|
160
181
|
export const ADVANCED_CONFIG_KEYS = new Set([
|
|
161
182
|
"org.id",
|
|
162
183
|
"policy.scriptHardening",
|
|
184
|
+
"policy.shimFailClosed",
|
|
185
|
+
"policy.strictEgress",
|
|
163
186
|
"scriptGate.observe",
|
|
164
187
|
"cooldown.npm.age",
|
|
165
188
|
"cooldown.pypi.age",
|
|
@@ -206,6 +229,12 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
206
229
|
if (key === "policy.scriptHardening") {
|
|
207
230
|
return withPolicyBoolean(config, "scriptHardening", rawValue);
|
|
208
231
|
}
|
|
232
|
+
if (key === "policy.shimFailClosed") {
|
|
233
|
+
return withPolicyBoolean(config, "shimFailClosed", rawValue);
|
|
234
|
+
}
|
|
235
|
+
if (key === "policy.strictEgress") {
|
|
236
|
+
return withPolicyBoolean(config, "strictEgress", rawValue);
|
|
237
|
+
}
|
|
209
238
|
if (key === "scriptGate.mode") {
|
|
210
239
|
return {
|
|
211
240
|
...config,
|
|
@@ -300,7 +329,9 @@ function normalizeConfig(raw) {
|
|
|
300
329
|
mode: parsePolicyMode(fieldString(policy, "policy.mode", "mode") ?? DEFAULT_CONFIG.policy.mode),
|
|
301
330
|
trustProjectAllowlists: fieldBoolean(policy, "policy.trustProjectAllowlists", "trustProjectAllowlists") ?? DEFAULT_CONFIG.policy.trustProjectAllowlists,
|
|
302
331
|
allowForceOverride: fieldBoolean(policy, "policy.allowForceOverride", "allowForceOverride") ?? DEFAULT_CONFIG.policy.allowForceOverride,
|
|
303
|
-
scriptHardening
|
|
332
|
+
scriptHardening,
|
|
333
|
+
shimFailClosed: fieldBoolean(policy, "policy.shimFailClosed", "shimFailClosed") ?? DEFAULT_CONFIG.policy.shimFailClosed,
|
|
334
|
+
strictEgress: fieldBoolean(policy, "policy.strictEgress", "strictEgress") ?? DEFAULT_CONFIG.policy.strictEgress
|
|
304
335
|
},
|
|
305
336
|
scriptGate: {
|
|
306
337
|
mode: parseScriptGateMode(fieldString(scriptGate, "scriptGate.mode", "mode") ?? (scriptHardening ? "enforce" : DEFAULT_CONFIG.scriptGate.mode)),
|
|
@@ -452,7 +483,7 @@ function parseBoolean(value, field) {
|
|
|
452
483
|
}
|
|
453
484
|
throw new ConfigError(`${field} must be true or false`);
|
|
454
485
|
}
|
|
455
|
-
function parseUrl(value) {
|
|
486
|
+
export function parseUrl(value) {
|
|
456
487
|
const trimmed = value.trim();
|
|
457
488
|
try {
|
|
458
489
|
const url = new URL(trimmed);
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import InkSpinner from "ink-spinner";
|
|
4
4
|
import { formatResetDate } from "./block-render.js";
|
|
5
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
5
6
|
function packageCount(n) {
|
|
6
7
|
return n === 1 ? "1 package" : `${n} packages`;
|
|
7
8
|
}
|
|
@@ -15,7 +16,7 @@ export const LiveInstall = ({ view }) => {
|
|
|
15
16
|
? "DG starting protection…"
|
|
16
17
|
: view.resolvedTotal !== undefined && view.total <= view.resolvedTotal
|
|
17
18
|
? `DG verifying ${view.total}/${view.resolvedTotal}…`
|
|
18
|
-
: `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
|
|
19
|
+
: `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", sanitizeLine(view.current)] }) : null] }));
|
|
19
20
|
}
|
|
20
21
|
if (view.total === 0 && !view.blocked) {
|
|
21
22
|
return null;
|
|
@@ -30,7 +31,7 @@ export const LiveInstall = ({ view }) => {
|
|
|
30
31
|
}
|
|
31
32
|
const total = view.verified + view.flagged;
|
|
32
33
|
if (view.flagged > 0) {
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ["\u26A0 DG verified ", packageCount(total), " \u2014 ", view.flagged, " flagged"] }), (view.flaggedItems ?? []).map((item, index) => (_jsxs(Text, { dimColor: true, children: [" ", item.packageName, " ", item.reason] }, `${item.packageName}-${index}`)))] }));
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ["\u26A0 DG verified ", packageCount(total), " \u2014 ", view.flagged, " flagged"] }), (view.flaggedItems ?? []).map((item, index) => (_jsxs(Text, { dimColor: true, children: [" ", sanitizeLine(item.packageName), " ", sanitizeLine(item.reason)] }, `${item.packageName}-${index}`)))] }));
|
|
34
35
|
}
|
|
35
36
|
return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 DG verified ", packageCount(total), " \u2014 clean"] }) }));
|
|
36
37
|
};
|