@westbayberry/dg 2.0.11 → 2.2.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 +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +6 -4
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- 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 +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +26 -10
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +169 -0
- 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 +8 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +3 -2
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/status.js +5 -2
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +154 -65
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +92 -0
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/install-ui/prompt.js +14 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +167 -17
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +43 -3
- package/dist/launcher/run.js +100 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +117 -0
- package/dist/policy/evaluate.js +5 -21
- 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/provenance.js +23 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +446 -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 +57 -17
- package/dist/proxy/metadata-map.js +66 -4
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +473 -45
- 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 +12 -11
- 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 +128 -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 +51 -17
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +63 -8
- package/dist/scan/scanner-report.js +42 -9
- package/dist/scan/staged.js +71 -11
- package/dist/scan-ui/LegacyApp.js +12 -16
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
- 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 +46 -4
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +14 -4
- package/dist/scripts/detect.js +158 -0
- package/dist/scripts/gate.js +170 -0
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- 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 +98 -29
- 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 +225 -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/json-file.js +24 -0
- package/dist/util/report-writer.js +57 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +86 -18
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +14 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -116
- package/dist/commands/explain.js +0 -232
- package/dist/commands/unavailable.js +0 -11
- package/dist/telemetry/events.js +0 -40
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { analyzePackages, AnalyzeError } from "../api/analyze.js";
|
|
2
|
+
import { classifyPackageManagerInvocation, isSupportedPackageManager, packageManagerNames, } from "./classify.js";
|
|
3
|
+
import { resolveLatest } from "../verify/package-check.js";
|
|
4
|
+
import { matchDecision } from "../decisions/apply.js";
|
|
5
|
+
import { isCooldownExempt, isCooldownExemptByDgFile } from "../policy/cooldown.js";
|
|
6
|
+
import { resolvePreflightCooldown, resolvePreflightDecisions } from "./install-preflight.js";
|
|
7
|
+
const ALLOW = { decision: "allow" };
|
|
8
|
+
// Flags that consume the following token as their value, so it must not be
|
|
9
|
+
// mistaken for a package name (the `-r requirements.txt` trap).
|
|
10
|
+
const PIP_VALUE_FLAGS = new Set([
|
|
11
|
+
"-r", "--requirement", "-c", "--constraint", "-e", "--editable", "-i", "--index-url",
|
|
12
|
+
"--extra-index-url", "-f", "--find-links", "-t", "--target", "--platform",
|
|
13
|
+
"--python-version", "--implementation", "--abi", "--prefix", "--root", "--no-binary",
|
|
14
|
+
"--only-binary", "--progress-bar",
|
|
15
|
+
]);
|
|
16
|
+
const NPM_VALUE_FLAGS = new Set(["--registry", "--prefix", "-C", "--workspace", "-w", "--tag", "--otp"]);
|
|
17
|
+
function analyzeEcosystem(eco) {
|
|
18
|
+
if (eco === "javascript")
|
|
19
|
+
return "npm";
|
|
20
|
+
if (eco === "python")
|
|
21
|
+
return "pypi";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function splitSegments(line) {
|
|
25
|
+
return line.split(/&&|\|\||[;|&\n\r]/).map((s) => s.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
function substitutionBodies(line) {
|
|
28
|
+
const bodies = [];
|
|
29
|
+
const patterns = [/\$\(([^)]*)\)/g, /`([^`]*)`/g];
|
|
30
|
+
for (const pattern of patterns) {
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = pattern.exec(line)) !== null) {
|
|
33
|
+
const body = (m[1] ?? "").trim();
|
|
34
|
+
if (body) {
|
|
35
|
+
bodies.push(body);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return bodies;
|
|
40
|
+
}
|
|
41
|
+
function collectSegments(line) {
|
|
42
|
+
const texts = [];
|
|
43
|
+
const pending = [line];
|
|
44
|
+
while (pending.length > 0) {
|
|
45
|
+
const current = pending.pop();
|
|
46
|
+
if (current === undefined || texts.includes(current)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
texts.push(current);
|
|
50
|
+
pending.push(...substitutionBodies(current));
|
|
51
|
+
}
|
|
52
|
+
const segments = [];
|
|
53
|
+
for (const segment of texts.flatMap(splitSegments)) {
|
|
54
|
+
if (!segments.includes(segment)) {
|
|
55
|
+
segments.push(segment);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return segments;
|
|
59
|
+
}
|
|
60
|
+
const ANSI_C_ESCAPES = {
|
|
61
|
+
n: "\n", t: "\t", r: "\r", a: "\x07", b: "\b", f: "\f", v: "\v",
|
|
62
|
+
"\\": "\\", "'": "'", '"': '"', "?": "?", e: "\x1b", E: "\x1b",
|
|
63
|
+
};
|
|
64
|
+
function decodeAnsiC(body) {
|
|
65
|
+
let out = "";
|
|
66
|
+
let i = 0;
|
|
67
|
+
while (i < body.length) {
|
|
68
|
+
const c = body[i] ?? "";
|
|
69
|
+
if (c !== "\\" || i + 1 >= body.length) {
|
|
70
|
+
out += c;
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const next = body[i + 1] ?? "";
|
|
75
|
+
if (next === "x") {
|
|
76
|
+
const m = /^[0-9a-fA-F]{1,2}/.exec(body.slice(i + 2));
|
|
77
|
+
if (m) {
|
|
78
|
+
out += String.fromCharCode(parseInt(m[0], 16));
|
|
79
|
+
i += 2 + m[0].length;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (next === "u" || next === "U") {
|
|
84
|
+
const m = new RegExp(`^[0-9a-fA-F]{1,${next === "u" ? 4 : 8}}`).exec(body.slice(i + 2));
|
|
85
|
+
if (m) {
|
|
86
|
+
out += String.fromCodePoint(parseInt(m[0], 16));
|
|
87
|
+
i += 2 + m[0].length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (next >= "0" && next <= "7") {
|
|
92
|
+
const m = /^[0-7]{1,3}/.exec(body.slice(i + 1));
|
|
93
|
+
if (m) {
|
|
94
|
+
out += String.fromCharCode(parseInt(m[0], 8) & 0xff);
|
|
95
|
+
i += 1 + m[0].length;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out += next in ANSI_C_ESCAPES ? ANSI_C_ESCAPES[next] : next;
|
|
100
|
+
i += 2;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
// A POSIX-style word splitter that performs quote removal, escape processing,
|
|
105
|
+
// ANSI-C ($'…') decoding, and adjacent-fragment joining the way the executing
|
|
106
|
+
// shell would, so quote/backslash obfuscation (np""m, n"p"m, i"n"stall, np\m,
|
|
107
|
+
// $'\x69'nstall) collapses to the real command before classification.
|
|
108
|
+
function lexSegment(segment) {
|
|
109
|
+
const tokens = [];
|
|
110
|
+
let cur = "";
|
|
111
|
+
let started = false;
|
|
112
|
+
let i = 0;
|
|
113
|
+
const n = segment.length;
|
|
114
|
+
const flush = () => {
|
|
115
|
+
if (started) {
|
|
116
|
+
tokens.push(cur);
|
|
117
|
+
cur = "";
|
|
118
|
+
started = false;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
while (i < n) {
|
|
122
|
+
const ch = segment[i] ?? "";
|
|
123
|
+
if (ch === " " || ch === "\t") {
|
|
124
|
+
flush();
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === "#" && !started) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (ch === "'") {
|
|
132
|
+
started = true;
|
|
133
|
+
i += 1;
|
|
134
|
+
let closed = false;
|
|
135
|
+
while (i < n) {
|
|
136
|
+
if (segment[i] === "'") {
|
|
137
|
+
closed = true;
|
|
138
|
+
i += 1;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
cur += segment[i];
|
|
142
|
+
i += 1;
|
|
143
|
+
}
|
|
144
|
+
if (!closed) {
|
|
145
|
+
flush();
|
|
146
|
+
return { tokens, ok: false };
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '"') {
|
|
151
|
+
started = true;
|
|
152
|
+
i += 1;
|
|
153
|
+
let closed = false;
|
|
154
|
+
while (i < n) {
|
|
155
|
+
const c = segment[i] ?? "";
|
|
156
|
+
if (c === '"') {
|
|
157
|
+
closed = true;
|
|
158
|
+
i += 1;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (c === "\\" && i + 1 < n) {
|
|
162
|
+
const nx = segment[i + 1] ?? "";
|
|
163
|
+
if (nx === '"' || nx === "\\" || nx === "`" || nx === "$" || nx === "\n") {
|
|
164
|
+
cur += nx;
|
|
165
|
+
i += 2;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
cur += c;
|
|
169
|
+
i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
cur += c;
|
|
173
|
+
i += 1;
|
|
174
|
+
}
|
|
175
|
+
if (!closed) {
|
|
176
|
+
flush();
|
|
177
|
+
return { tokens, ok: false };
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch === "$" && segment[i + 1] === "'") {
|
|
182
|
+
started = true;
|
|
183
|
+
i += 2;
|
|
184
|
+
let closed = false;
|
|
185
|
+
let body = "";
|
|
186
|
+
while (i < n) {
|
|
187
|
+
const c = segment[i] ?? "";
|
|
188
|
+
if (c === "\\" && i + 1 < n) {
|
|
189
|
+
body += c + (segment[i + 1] ?? "");
|
|
190
|
+
i += 2;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (c === "'") {
|
|
194
|
+
closed = true;
|
|
195
|
+
i += 1;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
body += c;
|
|
199
|
+
i += 1;
|
|
200
|
+
}
|
|
201
|
+
if (!closed) {
|
|
202
|
+
flush();
|
|
203
|
+
return { tokens, ok: false };
|
|
204
|
+
}
|
|
205
|
+
cur += decodeAnsiC(body);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch === "\\") {
|
|
209
|
+
if (i + 1 < n) {
|
|
210
|
+
cur += segment[i + 1];
|
|
211
|
+
started = true;
|
|
212
|
+
i += 2;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
flush();
|
|
216
|
+
return { tokens, ok: false };
|
|
217
|
+
}
|
|
218
|
+
cur += ch;
|
|
219
|
+
started = true;
|
|
220
|
+
i += 1;
|
|
221
|
+
}
|
|
222
|
+
flush();
|
|
223
|
+
return { tokens, ok: true };
|
|
224
|
+
}
|
|
225
|
+
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
226
|
+
const WRAPPER_COMMANDS = new Set(["sudo", "command", "exec", "env", "nice", "nohup", "time", "xargs"]);
|
|
227
|
+
function commandBasename(token) {
|
|
228
|
+
const slash = token.lastIndexOf("/");
|
|
229
|
+
return slash === -1 ? token : token.slice(slash + 1);
|
|
230
|
+
}
|
|
231
|
+
function commandTokens(segment) {
|
|
232
|
+
const lexed = lexSegment(segment);
|
|
233
|
+
let tokens = lexed.tokens;
|
|
234
|
+
for (;;) {
|
|
235
|
+
while (tokens.length > 0 && ENV_ASSIGNMENT.test(tokens[0] ?? "")) {
|
|
236
|
+
tokens = tokens.slice(1);
|
|
237
|
+
}
|
|
238
|
+
const head = tokens[0];
|
|
239
|
+
if (head === undefined || !WRAPPER_COMMANDS.has(commandBasename(head))) {
|
|
240
|
+
return { tokens, ok: lexed.ok };
|
|
241
|
+
}
|
|
242
|
+
tokens = tokens.slice(1);
|
|
243
|
+
while (tokens.length > 0 && (tokens[0] ?? "").startsWith("-")) {
|
|
244
|
+
tokens = tokens.slice(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function parseSpecToken(eco, token) {
|
|
249
|
+
if (eco === "npm") {
|
|
250
|
+
const at = token.lastIndexOf("@");
|
|
251
|
+
if (at > 0) {
|
|
252
|
+
return { name: token.slice(0, at), version: token.slice(at + 1) || null };
|
|
253
|
+
}
|
|
254
|
+
return { name: token, version: null };
|
|
255
|
+
}
|
|
256
|
+
// pypi: only an exact `name==version` is a pinned version; ranges are unpinned.
|
|
257
|
+
const exact = /^([A-Za-z0-9._-]+)==([^,;\s]+)$/.exec(token);
|
|
258
|
+
if (exact && exact[1] && exact[2]) {
|
|
259
|
+
return { name: exact[1], version: exact[2] };
|
|
260
|
+
}
|
|
261
|
+
const ranged = /^([A-Za-z0-9._-]+)\s*(?:===|>=|<=|~=|!=|<|>)/.exec(token);
|
|
262
|
+
if (ranged && ranged[1]) {
|
|
263
|
+
return { name: ranged[1], version: null };
|
|
264
|
+
}
|
|
265
|
+
return { name: token, version: null };
|
|
266
|
+
}
|
|
267
|
+
function isLocalSpecToken(t) {
|
|
268
|
+
return (t.startsWith(".") ||
|
|
269
|
+
t.startsWith("/") ||
|
|
270
|
+
t.startsWith("file:") ||
|
|
271
|
+
t.startsWith("link:") ||
|
|
272
|
+
t.startsWith("workspace:"));
|
|
273
|
+
}
|
|
274
|
+
function extractSpecs(manager, eco, rawArgs) {
|
|
275
|
+
const valueFlags = eco === "pypi" ? PIP_VALUE_FLAGS : NPM_VALUE_FLAGS;
|
|
276
|
+
const noVerb = manager === "npx" || manager === "pnpx" || manager === "uvx";
|
|
277
|
+
const positionals = [];
|
|
278
|
+
let seenVerb = noVerb;
|
|
279
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
280
|
+
const a = rawArgs[i];
|
|
281
|
+
if (a === undefined) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (a.startsWith("-")) {
|
|
285
|
+
if (valueFlags.has(a)) {
|
|
286
|
+
i += 1;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!seenVerb) {
|
|
291
|
+
seenVerb = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
positionals.push(a);
|
|
295
|
+
}
|
|
296
|
+
const tokens = noVerb ? positionals.slice(0, 1) : positionals;
|
|
297
|
+
const specs = [];
|
|
298
|
+
const remoteUnverifiable = [];
|
|
299
|
+
for (const t of tokens) {
|
|
300
|
+
if (t.length === 0) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (t.includes("://") || t.startsWith("git+")) {
|
|
304
|
+
remoteUnverifiable.push(t);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (isLocalSpecToken(t)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const spec = parseSpecToken(eco, t);
|
|
311
|
+
if (spec.name.length > 0) {
|
|
312
|
+
specs.push(spec);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { specs, remoteUnverifiable };
|
|
316
|
+
}
|
|
317
|
+
function normalizeName(eco, name) {
|
|
318
|
+
const lower = name.toLowerCase();
|
|
319
|
+
return eco === "pypi" ? lower.replace(/[._-]+/g, "-") : lower;
|
|
320
|
+
}
|
|
321
|
+
function quarantined(pkg, ctx, eco, dgExemptions = []) {
|
|
322
|
+
if (!ctx || !pkg.cooldown)
|
|
323
|
+
return false;
|
|
324
|
+
if (isCooldownExempt(pkg.name, ctx.exempt, eco) || isCooldownExemptByDgFile(pkg.name, eco, dgExemptions)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return (pkg.cooldown.status === "quarantine" ||
|
|
328
|
+
(pkg.cooldown.status === "unknown" && ctx.param.onUnknown === "block"));
|
|
329
|
+
}
|
|
330
|
+
function whyFor(pkg, action) {
|
|
331
|
+
if (action === "cooldown") {
|
|
332
|
+
return "release too new (cooldown)";
|
|
333
|
+
}
|
|
334
|
+
return pkg.reasons[0] ?? pkg.findings[0]?.title ?? pkg.findings[0]?.id ?? String(action);
|
|
335
|
+
}
|
|
336
|
+
function combinePackages(verdicts) {
|
|
337
|
+
if (verdicts.length === 0) {
|
|
338
|
+
return ALLOW;
|
|
339
|
+
}
|
|
340
|
+
const blocking = verdicts.filter((v) => v.action === "block" || v.action === "cooldown" || v.action === "analysis_incomplete");
|
|
341
|
+
if (blocking.length > 0) {
|
|
342
|
+
const list = blocking.map((v) => `${v.name}@${v.version} (${v.action}: ${v.why})`).join("; ");
|
|
343
|
+
return { decision: "deny", reason: `DG blocked install — ${list}`, packages: verdicts };
|
|
344
|
+
}
|
|
345
|
+
const list = verdicts.map((v) => `${v.name}@${v.version} (${v.why})`).join("; ");
|
|
346
|
+
return { decision: "ask", reason: `DG flagged for review — ${list}`, packages: verdicts };
|
|
347
|
+
}
|
|
348
|
+
function combine(results) {
|
|
349
|
+
return results.find((v) => v.decision === "deny") ?? results.find((v) => v.decision === "ask") ?? ALLOW;
|
|
350
|
+
}
|
|
351
|
+
async function checkSegment(segment, env, cwd, fetchImpl) {
|
|
352
|
+
const { tokens, ok } = commandTokens(segment);
|
|
353
|
+
const manager = commandBasename(tokens[0] ?? "");
|
|
354
|
+
if (!manager || !packageManagerNames().includes(manager) || !isSupportedPackageManager(manager)) {
|
|
355
|
+
return ALLOW;
|
|
356
|
+
}
|
|
357
|
+
if (!ok) {
|
|
358
|
+
// Head is a package manager but the segment could not be parsed safely
|
|
359
|
+
// (unbalanced quote / trailing escape). Never wave it through.
|
|
360
|
+
return { decision: "ask", reason: `dg could not safely parse this ${manager} command; proceed only if you trust it` };
|
|
361
|
+
}
|
|
362
|
+
const args = tokens.slice(1);
|
|
363
|
+
const classification = classifyPackageManagerInvocation(manager, args);
|
|
364
|
+
if (classification.kind !== "protected") {
|
|
365
|
+
return ALLOW;
|
|
366
|
+
}
|
|
367
|
+
const eco = analyzeEcosystem(classification.ecosystem);
|
|
368
|
+
if (eco === null) {
|
|
369
|
+
return { decision: "ask", reason: `dg cannot yet verify ${manager} packages; proceed manually if you trust them` };
|
|
370
|
+
}
|
|
371
|
+
const { specs, remoteUnverifiable } = extractSpecs(manager, eco, args);
|
|
372
|
+
const unverifiable = remoteUnverifiable.length > 0
|
|
373
|
+
? {
|
|
374
|
+
decision: "ask",
|
|
375
|
+
reason: `dg cannot verify non-registry source${remoteUnverifiable.length > 1 ? "s" : ""}: ${remoteUnverifiable.join(", ")}; proceed only if you trust ${remoteUnverifiable.length > 1 ? "them" : "it"}`,
|
|
376
|
+
}
|
|
377
|
+
: null;
|
|
378
|
+
if (specs.length === 0) {
|
|
379
|
+
return unverifiable ?? ALLOW;
|
|
380
|
+
}
|
|
381
|
+
const resolved = [];
|
|
382
|
+
for (const spec of specs) {
|
|
383
|
+
let version = spec.version;
|
|
384
|
+
if (!version) {
|
|
385
|
+
version = await resolveLatest(eco, spec.name, fetchImpl);
|
|
386
|
+
if (!version) {
|
|
387
|
+
return {
|
|
388
|
+
decision: "deny",
|
|
389
|
+
reason: `could not resolve a version to verify ${spec.name} on ${eco}; refusing under the firewall`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
resolved.push({ name: spec.name, version });
|
|
394
|
+
}
|
|
395
|
+
const cooldown = resolvePreflightCooldown(env, eco);
|
|
396
|
+
let decisions;
|
|
397
|
+
try {
|
|
398
|
+
decisions = resolvePreflightDecisions(eco, cwd, env);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
decisions = null;
|
|
402
|
+
}
|
|
403
|
+
let response;
|
|
404
|
+
try {
|
|
405
|
+
response = await analyzePackages(resolved, {
|
|
406
|
+
ecosystem: eco,
|
|
407
|
+
env,
|
|
408
|
+
fetchImpl,
|
|
409
|
+
...(cooldown ? { cooldown: cooldown.param } : {}),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
const message = error instanceof AnalyzeError ? error.message : "the dg scanner could not be reached";
|
|
414
|
+
return {
|
|
415
|
+
decision: "deny",
|
|
416
|
+
reason: `could not verify ${resolved.map((r) => r.name).join(", ")}: ${message}; blocked under the firewall (disable: dg hook <agent> off)`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const verdicts = [];
|
|
420
|
+
for (const pkg of response.packages) {
|
|
421
|
+
const isQuar = quarantined(pkg, cooldown, eco, decisions?.file.cooldownExemptions ?? []);
|
|
422
|
+
// A per-package result with no action is a coverage gap, not a clean pass:
|
|
423
|
+
// treat it as analysis_incomplete (blocking) so the firewall fails closed.
|
|
424
|
+
let action = isQuar && pkg.action !== "block" ? "cooldown" : pkg.action ?? "analysis_incomplete";
|
|
425
|
+
if (action === "warn" &&
|
|
426
|
+
decisions &&
|
|
427
|
+
matchDecision(pkg, decisions.ecosystem, decisions.file.decisions).acknowledged) {
|
|
428
|
+
action = "pass";
|
|
429
|
+
}
|
|
430
|
+
if (action !== "pass") {
|
|
431
|
+
verdicts.push({ name: pkg.name, version: pkg.version, action, why: whyFor(pkg, action) });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Every requested spec must come back with a verdict. A package the scanner
|
|
435
|
+
// omitted (partial/truncated/tampered response) must not be silently allowed.
|
|
436
|
+
const returned = new Set(response.packages.map((p) => normalizeName(eco, p.name)));
|
|
437
|
+
for (const spec of resolved) {
|
|
438
|
+
if (!returned.has(normalizeName(eco, spec.name))) {
|
|
439
|
+
verdicts.push({
|
|
440
|
+
name: spec.name,
|
|
441
|
+
version: spec.version,
|
|
442
|
+
action: "analysis_incomplete",
|
|
443
|
+
why: "scanner returned no verdict for this package",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const base = combinePackages(verdicts);
|
|
448
|
+
if (unverifiable && base.decision === "allow") {
|
|
449
|
+
return unverifiable;
|
|
450
|
+
}
|
|
451
|
+
return base;
|
|
452
|
+
}
|
|
453
|
+
export async function agentCheckCommand(input) {
|
|
454
|
+
const env = input.env ?? process.env;
|
|
455
|
+
const cwd = input.cwd ?? process.cwd();
|
|
456
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
457
|
+
const segments = collectSegments(input.commandLine);
|
|
458
|
+
if (segments.length === 0) {
|
|
459
|
+
return ALLOW;
|
|
460
|
+
}
|
|
461
|
+
const results = [];
|
|
462
|
+
for (const seg of segments) {
|
|
463
|
+
results.push(await checkSegment(seg, env, cwd, fetchImpl));
|
|
464
|
+
}
|
|
465
|
+
return combine(results);
|
|
466
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { runAgentHookExec } from "./agent-hook-io.js";
|
|
3
|
+
import { getAgent, isAgentId } from "../agents/registry.js";
|
|
4
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
5
|
+
import { writeJsonAtomic } from "../util/json-file.js";
|
|
6
|
+
async function readStdin(stream = process.stdin) {
|
|
7
|
+
if (stream.isTTY) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
chunks.push(chunk);
|
|
13
|
+
}
|
|
14
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
15
|
+
}
|
|
16
|
+
function recordFixture(agent, stdin) {
|
|
17
|
+
const path = join(resolveDgPaths().stateDir, "fixtures", `${agent}-${Date.now()}.json`);
|
|
18
|
+
writeJsonAtomic(path, { agent, capturedAt: new Date().toISOString(), stdin });
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
// Short-circuited before any banner/nudge so stdout carries only the decision
|
|
22
|
+
// JSON the agent parses.
|
|
23
|
+
export async function maybeAgentHookExec(args, opts = {}) {
|
|
24
|
+
const noop = { exitCode: 0, stdout: "", stderr: "" };
|
|
25
|
+
if (args[0] !== "hook-exec") {
|
|
26
|
+
return { handled: false, result: noop };
|
|
27
|
+
}
|
|
28
|
+
const agent = args[1];
|
|
29
|
+
if (!agent || !isAgentId(agent)) {
|
|
30
|
+
// Unknown agent (e.g. a hook written by a newer dg, run by an older one):
|
|
31
|
+
// fail closed. We can't emit this agent's allow shape, and a non-zero exit
|
|
32
|
+
// with no decision blocks across the agents we support, so an out-of-date
|
|
33
|
+
// dg refuses the install with a clear message rather than waving it through.
|
|
34
|
+
return {
|
|
35
|
+
handled: true,
|
|
36
|
+
result: {
|
|
37
|
+
exitCode: 2,
|
|
38
|
+
stdout: "",
|
|
39
|
+
stderr: `dg hook-exec: unknown agent '${agent ?? ""}' — blocked under the firewall; update dg (npm i -g @westbayberry/dg) or run 'dg agents off'\n`
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let stdin;
|
|
44
|
+
try {
|
|
45
|
+
stdin = await readStdin(opts.stdin);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A stdin stream error must not escape to the fatal handler (exit 70 with
|
|
49
|
+
// empty stdout reads as allow to every agent). Emit this agent's deny shape.
|
|
50
|
+
const emitted = getAgent(agent).emitDecision({
|
|
51
|
+
decision: "deny",
|
|
52
|
+
reason: "dg hook: could not read the tool payload from stdin; blocked under the firewall",
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
handled: true,
|
|
56
|
+
result: { exitCode: emitted.exitCode || 2, stdout: emitted.stdout, stderr: "dg hook: stdin read error\n" },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let fixtureNote = "";
|
|
60
|
+
if (args[2] === "--record-fixture") {
|
|
61
|
+
try {
|
|
62
|
+
fixtureNote = `dg hook-exec: recorded fixture ${recordFixture(agent, stdin)}\n`;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
fixtureNote = "dg hook-exec: could not record the fixture\n";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const result = await runAgentHookExec(agent, stdin);
|
|
69
|
+
return { handled: true, result: { exitCode: result.exitCode, stdout: result.stdout, stderr: `${fixtureNote}${result.stderr}` } };
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { agentCheckCommand } from "./agent-check.js";
|
|
2
|
+
import { getAgent } from "../agents/registry.js";
|
|
3
|
+
export async function runAgentHookExec(agent, stdin, deps = {}) {
|
|
4
|
+
const integration = getAgent(agent);
|
|
5
|
+
const parsed = integration.parseInput(stdin);
|
|
6
|
+
if (!parsed) {
|
|
7
|
+
// Unreadable payload -> fail closed (deny) so a malformed hook input can
|
|
8
|
+
// never slip an install through unverified.
|
|
9
|
+
const verdict = { decision: "deny", reason: "dg hook: could not read the tool command; blocked under the firewall" };
|
|
10
|
+
const emitted = integration.emitDecision(verdict);
|
|
11
|
+
return { stdout: emitted.stdout, stderr: "dg hook: malformed hook payload\n", exitCode: emitted.exitCode };
|
|
12
|
+
}
|
|
13
|
+
let verdict;
|
|
14
|
+
try {
|
|
15
|
+
verdict = await agentCheckCommand({
|
|
16
|
+
commandLine: parsed.command,
|
|
17
|
+
...(parsed.cwd ? { cwd: parsed.cwd } : {}),
|
|
18
|
+
...(deps.env ? { env: deps.env } : {}),
|
|
19
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Any failure computing the verdict must fail closed: an unhandled throw
|
|
24
|
+
// would otherwise exit non-zero with no decision, which most agents read
|
|
25
|
+
// as allow.
|
|
26
|
+
verdict = { decision: "deny", reason: "dg hook: firewall check failed; blocked under the firewall (disable: dg agents off)" };
|
|
27
|
+
}
|
|
28
|
+
const emitted = integration.emitDecision(verdict);
|
|
29
|
+
const stderr = verdict.decision === "allow" ? "" : ` ${verdict.reason ?? "DG firewall"}\n`;
|
|
30
|
+
return { stdout: emitted.stdout, stderr, exitCode: emitted.exitCode };
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const PRESERVED_ENTRIES = ["config.toml", "config", "credentials.toml", "credentials"];
|
|
5
|
+
export function userCargoHome(env) {
|
|
6
|
+
const explicit = env.CARGO_HOME?.trim();
|
|
7
|
+
return explicit ? explicit : join(homedir(), ".cargo");
|
|
8
|
+
}
|
|
9
|
+
// Cargo keeps registry config and auth tokens (config.toml / credentials.toml)
|
|
10
|
+
// inside CARGO_HOME, next to the crate cache. dg redirects CARGO_HOME to an empty
|
|
11
|
+
// per-session dir so cached crates re-fetch through the firewall, which would also
|
|
12
|
+
// hide a private-registry user's config and tokens — so link those back in.
|
|
13
|
+
export function prepareCargoHome(cacheDir, source) {
|
|
14
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
15
|
+
const linked = [];
|
|
16
|
+
for (const name of PRESERVED_ENTRIES) {
|
|
17
|
+
const target = join(source, name);
|
|
18
|
+
const link = join(cacheDir, name);
|
|
19
|
+
if (!existsSync(target) || pathPresent(link)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
symlinkSync(target, link);
|
|
24
|
+
linked.push(name);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return linked;
|
|
31
|
+
}
|
|
32
|
+
function pathPresent(path) {
|
|
33
|
+
try {
|
|
34
|
+
lstatSync(path);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
const jsProtected = new Set(["install", "i", "ci", "add", "update", "dedupe", "exec"]);
|
|
5
|
-
const pnpmProtected = new Set(["install", "i", "add", "update", "dlx", "exec"]);
|
|
6
|
-
const yarnProtected = new Set(["add", "install", "upgrade", "dlx"]);
|
|
4
|
+
const jsProtected = new Set(["install", "i", "ci", "add", "update", "dedupe", "exec", "create"]);
|
|
5
|
+
const pnpmProtected = new Set(["install", "i", "add", "update", "dlx", "exec", "create"]);
|
|
6
|
+
const yarnProtected = new Set(["add", "install", "upgrade", "dlx", "create"]);
|
|
7
7
|
const pipProtected = new Set(["install", "download", "wheel"]);
|
|
8
8
|
const pipxProtected = new Set(["install", "upgrade", "inject", "run"]);
|
|
9
9
|
const uvProtected = new Set(["add", "sync"]);
|
|
@@ -44,7 +44,7 @@ export function classifyPackageManagerInvocation(manager, args) {
|
|
|
44
44
|
}
|
|
45
45
|
function classifyByCommand(manager, args, realBinaryName, protectedCommands, protectedReason, ecosystem) {
|
|
46
46
|
const action = firstCommand(args);
|
|
47
|
-
if (protectedCommands.has(action) || containsFetchSpec(args)) {
|
|
47
|
+
if (protectedCommands.has(action) || containsFetchSpec(args) || initFetchesPackage(action, args, protectedCommands)) {
|
|
48
48
|
return {
|
|
49
49
|
manager,
|
|
50
50
|
ecosystem,
|
|
@@ -70,8 +70,8 @@ function classifyUv(args) {
|
|
|
70
70
|
if (action === "pip" && pipProtected.has(args[1] ?? "")) {
|
|
71
71
|
return protectedClassification("uv", args, "uv", "uv pip install/fetch command");
|
|
72
72
|
}
|
|
73
|
-
if (action === "tool" && args[1]
|
|
74
|
-
return protectedClassification("uv", args, "uv", "uv tool run fetches package artifacts");
|
|
73
|
+
if (action === "tool" && ["run", "install", "upgrade"].includes(args[1] ?? "")) {
|
|
74
|
+
return protectedClassification("uv", args, "uv", "uv tool run/install/upgrade fetches package artifacts");
|
|
75
75
|
}
|
|
76
76
|
if (uvProtected.has(action) || containsFetchSpec(args)) {
|
|
77
77
|
return protectedClassification("uv", args, "uv", "uv install/fetch command");
|
|
@@ -111,6 +111,17 @@ function unsupportedClassification(manager, args) {
|
|
|
111
111
|
function firstCommand(args) {
|
|
112
112
|
return args.find((arg) => !arg.startsWith("-")) ?? "";
|
|
113
113
|
}
|
|
114
|
+
// `npm/pnpm/yarn init <initializer>` aliases to fetching and running
|
|
115
|
+
// create-<initializer>, the same as the `create` form; bare `init` / `init -y`
|
|
116
|
+
// is a local scaffold with no fetch. Gated to the JS managers (their protected
|
|
117
|
+
// set carries "create"); cargo/pip `init` are local-only.
|
|
118
|
+
function initFetchesPackage(action, args, protectedCommands) {
|
|
119
|
+
if (action !== "init" || !protectedCommands.has("create")) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const commandIndex = args.findIndex((arg) => !arg.startsWith("-"));
|
|
123
|
+
return args.slice(commandIndex + 1).some((arg) => arg !== "--" && !arg.startsWith("-"));
|
|
124
|
+
}
|
|
114
125
|
function containsFetchSpec(args) {
|
|
115
126
|
return args.some((arg) => /^https?:\/\//.test(arg) || /^git\+https?:\/\//.test(arg) || /\.t(ar\.)?gz(?:$|[#?])/.test(arg));
|
|
116
127
|
}
|