@westbayberry/dg 1.3.2 → 2.0.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/LICENSE +1 -201
- package/NOTICE +1 -4
- package/README.md +293 -0
- package/dist/api/analyze.js +210 -0
- package/dist/audit/deep.js +180 -0
- package/dist/audit/detectors.js +247 -0
- package/dist/audit/events.js +41 -0
- package/dist/audit/rules.js +426 -0
- package/dist/audit-ui/AuditApp.js +39 -0
- package/dist/audit-ui/components/AuditHeader.js +24 -0
- package/dist/audit-ui/components/AuditResultsView.js +307 -0
- package/dist/audit-ui/components/DeepStatusRow.js +11 -0
- package/dist/audit-ui/export.js +85 -0
- package/dist/audit-ui/format.js +34 -0
- package/dist/audit-ui/launch.js +34 -0
- package/dist/auth/device-login.js +271 -0
- package/dist/auth/env-token.js +6 -0
- package/dist/auth/login-app.js +156 -0
- package/dist/auth/store.js +147 -0
- package/dist/bin/dg.js +71 -0
- package/dist/commands/audit.js +357 -0
- package/dist/commands/completion.js +116 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/doctor.js +39 -0
- package/dist/commands/explain.js +100 -0
- package/dist/commands/guard-commit.js +158 -0
- package/dist/commands/help.js +74 -0
- package/dist/commands/licenses.js +435 -0
- package/dist/commands/login.js +81 -0
- package/dist/commands/logout.js +37 -0
- package/dist/commands/router.js +98 -0
- package/dist/commands/scan.js +18 -0
- package/dist/commands/service.js +475 -0
- package/dist/commands/setup.js +302 -0
- package/dist/commands/status.js +115 -0
- package/dist/commands/suggest.js +35 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/unavailable.js +11 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/commands/update.js +210 -0
- package/dist/commands/verify.js +151 -0
- package/dist/commands/version.js +22 -0
- package/dist/commands/wrap.js +55 -0
- package/dist/config/settings.js +302 -0
- package/dist/install-ui/LiveInstall.js +24 -0
- package/dist/install-ui/block-render.js +83 -0
- package/dist/install-ui/live-install-app.js +48 -0
- package/dist/install-ui/prompt.js +24 -0
- package/dist/launcher/classify.js +116 -0
- package/dist/launcher/env.js +53 -0
- package/dist/launcher/live-install.js +50 -0
- package/dist/launcher/output-redaction.js +77 -0
- package/dist/launcher/preflight-prompt.js +139 -0
- package/dist/launcher/resolve-real-binary.js +73 -0
- package/dist/launcher/run.js +417 -0
- package/dist/policy/evaluate.js +128 -0
- package/dist/presentation/mode.js +52 -0
- package/dist/presentation/theme.js +29 -0
- package/dist/proxy/buffer-budget.js +64 -0
- package/dist/proxy/ca.js +126 -0
- package/dist/proxy/classify-host.js +26 -0
- package/dist/proxy/enforcement.js +102 -0
- package/dist/proxy/metadata-map.js +336 -0
- package/dist/proxy/server.js +909 -0
- package/dist/proxy/upstream-proxy.js +102 -0
- package/dist/proxy/worker.js +39 -0
- package/dist/publish-set/collect.js +51 -0
- package/dist/publish-set/no-exec-shell.js +19 -0
- package/dist/publish-set/npm.js +109 -0
- package/dist/publish-set/pack.js +36 -0
- package/dist/publish-set/pypi.js +59 -0
- package/dist/runtime/cli.js +17 -0
- package/dist/runtime/first-run.js +60 -0
- package/dist/runtime/node-version.js +58 -0
- package/dist/runtime/nudges.js +105 -0
- package/dist/scan/analyze-worker.js +21 -0
- package/dist/scan/collect.js +153 -0
- package/dist/scan/command.js +159 -0
- package/dist/scan/discovery.js +209 -0
- package/dist/scan/render.js +240 -0
- package/dist/scan/scanner-report.js +82 -0
- package/dist/scan/staged.js +173 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan-ui/LegacyApp.js +156 -0
- package/dist/scan-ui/alt-screen.js +84 -0
- package/dist/scan-ui/api-aliases.js +1 -0
- package/dist/scan-ui/components/ErrorView.js +23 -0
- package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
- package/dist/scan-ui/components/ProgressBar.js +89 -0
- package/dist/scan-ui/components/ProjectSelector.js +62 -0
- package/dist/scan-ui/components/ScoreHeader.js +20 -0
- package/dist/scan-ui/components/SetupBanner.js +13 -0
- package/dist/scan-ui/components/Spinner.js +4 -0
- package/dist/scan-ui/format-helpers.js +40 -0
- package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
- package/dist/scan-ui/hooks/useScan.js +113 -0
- package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
- package/dist/scan-ui/launch.js +27 -0
- package/dist/scan-ui/logo.js +91 -0
- package/dist/scan-ui/shims.js +30 -0
- package/dist/security/sanitize.js +28 -0
- package/dist/service/state.js +837 -0
- package/dist/service/trust-store.js +234 -0
- package/dist/service/worker.js +88 -0
- package/dist/setup/git-hook.js +244 -0
- package/dist/setup/optional-support.js +58 -0
- package/dist/setup/plan.js +899 -0
- package/dist/state/cleanup-registry.js +60 -0
- package/dist/state/index.js +5 -0
- package/dist/state/locks.js +161 -0
- package/dist/state/paths.js +24 -0
- package/dist/state/sessions.js +170 -0
- package/dist/state/store.js +50 -0
- package/dist/telemetry/events.js +40 -0
- package/dist/util/git.js +20 -0
- package/dist/util/tty-prompt.js +43 -0
- package/dist/verify/local.js +400 -0
- package/dist/verify/package-check.js +240 -0
- package/dist/verify/preflight.js +698 -0
- package/dist/verify/render.js +184 -0
- package/dist/verify/types.js +1 -0
- package/package.json +33 -50
- package/dist/index.mjs +0 -54141
- package/dist/postinstall.mjs +0 -731
- package/dist/python-hook/dg_pip_hook.pth +0 -1
- package/dist/python-hook/dg_pip_hook.py +0 -130
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
5
|
+
export const CONFIG_KEYS = Object.freeze([
|
|
6
|
+
"api.baseUrl",
|
|
7
|
+
"org.id",
|
|
8
|
+
"policy.mode",
|
|
9
|
+
"policy.trustProjectAllowlists",
|
|
10
|
+
"policy.allowForceOverride",
|
|
11
|
+
"policy.scriptHardening",
|
|
12
|
+
"gitHook.onWarn",
|
|
13
|
+
"gitHook.onIncomplete",
|
|
14
|
+
"audit.upload",
|
|
15
|
+
"telemetry.enabled",
|
|
16
|
+
"webhooks.enabled"
|
|
17
|
+
]);
|
|
18
|
+
export const DEFAULT_CONFIG = Object.freeze({
|
|
19
|
+
version: 1,
|
|
20
|
+
api: {
|
|
21
|
+
baseUrl: "https://api.westbayberry.com"
|
|
22
|
+
},
|
|
23
|
+
org: {
|
|
24
|
+
id: ""
|
|
25
|
+
},
|
|
26
|
+
policy: {
|
|
27
|
+
mode: "block",
|
|
28
|
+
trustProjectAllowlists: false,
|
|
29
|
+
allowForceOverride: true,
|
|
30
|
+
scriptHardening: false
|
|
31
|
+
},
|
|
32
|
+
gitHook: {
|
|
33
|
+
onWarn: "prompt",
|
|
34
|
+
onIncomplete: "allow"
|
|
35
|
+
},
|
|
36
|
+
audit: {
|
|
37
|
+
upload: false
|
|
38
|
+
},
|
|
39
|
+
telemetry: {
|
|
40
|
+
enabled: false
|
|
41
|
+
},
|
|
42
|
+
webhooks: {
|
|
43
|
+
enabled: true
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
export class ConfigError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "ConfigError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function userConfigPath(paths) {
|
|
53
|
+
return join(paths.configDir, "config.json");
|
|
54
|
+
}
|
|
55
|
+
export function loadUserConfig(env = process.env) {
|
|
56
|
+
const paths = resolveDgPaths(env);
|
|
57
|
+
const path = userConfigPath(paths);
|
|
58
|
+
if (!existsSync(path)) {
|
|
59
|
+
return DEFAULT_CONFIG;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
+
return normalizeConfig(parsed);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new ConfigError(`Malformed dg config at ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function saveUserConfig(config, env = process.env) {
|
|
70
|
+
const paths = resolveDgPaths(env);
|
|
71
|
+
writeJsonAtomic(userConfigPath(paths), config);
|
|
72
|
+
}
|
|
73
|
+
export function getConfigValue(config, key) {
|
|
74
|
+
if (key === "api.baseUrl") {
|
|
75
|
+
return config.api.baseUrl;
|
|
76
|
+
}
|
|
77
|
+
if (key === "org.id") {
|
|
78
|
+
return config.org.id;
|
|
79
|
+
}
|
|
80
|
+
if (key === "policy.mode") {
|
|
81
|
+
return config.policy.mode;
|
|
82
|
+
}
|
|
83
|
+
if (key === "policy.trustProjectAllowlists") {
|
|
84
|
+
return String(config.policy.trustProjectAllowlists);
|
|
85
|
+
}
|
|
86
|
+
if (key === "policy.allowForceOverride") {
|
|
87
|
+
return String(config.policy.allowForceOverride);
|
|
88
|
+
}
|
|
89
|
+
if (key === "policy.scriptHardening") {
|
|
90
|
+
return String(config.policy.scriptHardening);
|
|
91
|
+
}
|
|
92
|
+
if (key === "gitHook.onWarn") {
|
|
93
|
+
return config.gitHook.onWarn;
|
|
94
|
+
}
|
|
95
|
+
if (key === "gitHook.onIncomplete") {
|
|
96
|
+
return config.gitHook.onIncomplete;
|
|
97
|
+
}
|
|
98
|
+
if (key === "audit.upload") {
|
|
99
|
+
return String(config.audit.upload);
|
|
100
|
+
}
|
|
101
|
+
if (key === "telemetry.enabled") {
|
|
102
|
+
return String(config.telemetry.enabled);
|
|
103
|
+
}
|
|
104
|
+
return String(config.webhooks.enabled);
|
|
105
|
+
}
|
|
106
|
+
export function listConfig(config) {
|
|
107
|
+
return CONFIG_KEYS.map((key) => ({
|
|
108
|
+
key,
|
|
109
|
+
value: getConfigValue(config, key)
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
export function setConfigValue(config, key, rawValue) {
|
|
113
|
+
if (key === "api.baseUrl") {
|
|
114
|
+
return {
|
|
115
|
+
...config,
|
|
116
|
+
api: {
|
|
117
|
+
baseUrl: parseUrl(rawValue)
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (key === "org.id") {
|
|
122
|
+
return {
|
|
123
|
+
...config,
|
|
124
|
+
org: {
|
|
125
|
+
id: rawValue.trim()
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (key === "policy.mode") {
|
|
130
|
+
return {
|
|
131
|
+
...config,
|
|
132
|
+
policy: {
|
|
133
|
+
...config.policy,
|
|
134
|
+
mode: parsePolicyMode(rawValue)
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (key === "policy.trustProjectAllowlists") {
|
|
139
|
+
return withPolicyBoolean(config, "trustProjectAllowlists", rawValue);
|
|
140
|
+
}
|
|
141
|
+
if (key === "policy.allowForceOverride") {
|
|
142
|
+
return withPolicyBoolean(config, "allowForceOverride", rawValue);
|
|
143
|
+
}
|
|
144
|
+
if (key === "policy.scriptHardening") {
|
|
145
|
+
return withPolicyBoolean(config, "scriptHardening", rawValue);
|
|
146
|
+
}
|
|
147
|
+
if (key === "gitHook.onWarn") {
|
|
148
|
+
return {
|
|
149
|
+
...config,
|
|
150
|
+
gitHook: {
|
|
151
|
+
...config.gitHook,
|
|
152
|
+
onWarn: parseOnWarn(rawValue)
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (key === "gitHook.onIncomplete") {
|
|
157
|
+
return {
|
|
158
|
+
...config,
|
|
159
|
+
gitHook: {
|
|
160
|
+
...config.gitHook,
|
|
161
|
+
onIncomplete: parseOnIncomplete(rawValue)
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (key === "audit.upload") {
|
|
166
|
+
return {
|
|
167
|
+
...config,
|
|
168
|
+
audit: {
|
|
169
|
+
upload: parseBoolean(rawValue)
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (key === "telemetry.enabled") {
|
|
174
|
+
return {
|
|
175
|
+
...config,
|
|
176
|
+
telemetry: {
|
|
177
|
+
enabled: parseBoolean(rawValue)
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...config,
|
|
183
|
+
webhooks: {
|
|
184
|
+
enabled: parseBoolean(rawValue)
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function unsetConfigValue(config, key) {
|
|
189
|
+
return setConfigValue(config, key, getConfigValue(DEFAULT_CONFIG, key));
|
|
190
|
+
}
|
|
191
|
+
export function isConfigKey(value) {
|
|
192
|
+
return CONFIG_KEYS.includes(value);
|
|
193
|
+
}
|
|
194
|
+
function normalizeConfig(raw) {
|
|
195
|
+
if (raw.version !== undefined && raw.version !== 1) {
|
|
196
|
+
throw new ConfigError("unsupported config version");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
version: 1,
|
|
200
|
+
api: {
|
|
201
|
+
baseUrl: parseUrl(raw.api?.baseUrl ?? DEFAULT_CONFIG.api.baseUrl)
|
|
202
|
+
},
|
|
203
|
+
org: {
|
|
204
|
+
id: raw.org?.id ?? DEFAULT_CONFIG.org.id
|
|
205
|
+
},
|
|
206
|
+
policy: {
|
|
207
|
+
mode: parsePolicyMode(raw.policy?.mode ?? DEFAULT_CONFIG.policy.mode),
|
|
208
|
+
trustProjectAllowlists: raw.policy?.trustProjectAllowlists ?? DEFAULT_CONFIG.policy.trustProjectAllowlists,
|
|
209
|
+
allowForceOverride: raw.policy?.allowForceOverride ?? DEFAULT_CONFIG.policy.allowForceOverride,
|
|
210
|
+
scriptHardening: raw.policy?.scriptHardening ?? DEFAULT_CONFIG.policy.scriptHardening
|
|
211
|
+
},
|
|
212
|
+
gitHook: {
|
|
213
|
+
onWarn: parseOnWarn(raw.gitHook?.onWarn ?? DEFAULT_CONFIG.gitHook.onWarn),
|
|
214
|
+
onIncomplete: parseOnIncomplete(raw.gitHook?.onIncomplete ?? DEFAULT_CONFIG.gitHook.onIncomplete)
|
|
215
|
+
},
|
|
216
|
+
audit: {
|
|
217
|
+
upload: raw.audit?.upload ?? DEFAULT_CONFIG.audit.upload
|
|
218
|
+
},
|
|
219
|
+
telemetry: {
|
|
220
|
+
enabled: raw.telemetry?.enabled ?? DEFAULT_CONFIG.telemetry.enabled
|
|
221
|
+
},
|
|
222
|
+
webhooks: {
|
|
223
|
+
enabled: raw.webhooks?.enabled ?? DEFAULT_CONFIG.webhooks.enabled
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function withPolicyBoolean(config, key, rawValue) {
|
|
228
|
+
if (key === "mode") {
|
|
229
|
+
return config;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
...config,
|
|
233
|
+
policy: {
|
|
234
|
+
...config.policy,
|
|
235
|
+
[key]: parseBoolean(rawValue)
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parsePolicyMode(value) {
|
|
240
|
+
if (value === "off" || value === "warn" || value === "block" || value === "strict") {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
throw new ConfigError("policy.mode must be one of: off, warn, block, strict");
|
|
244
|
+
}
|
|
245
|
+
function parseOnWarn(value) {
|
|
246
|
+
if (value === "prompt" || value === "allow" || value === "block") {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
throw new ConfigError("gitHook.onWarn must be one of: prompt, allow, block");
|
|
250
|
+
}
|
|
251
|
+
function parseOnIncomplete(value) {
|
|
252
|
+
if (value === "allow" || value === "block") {
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
throw new ConfigError("gitHook.onIncomplete must be one of: allow, block");
|
|
256
|
+
}
|
|
257
|
+
function parseBoolean(value) {
|
|
258
|
+
if (value === "true") {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (value === "false") {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
throw new ConfigError("boolean config values must be true or false");
|
|
265
|
+
}
|
|
266
|
+
function parseUrl(value) {
|
|
267
|
+
const trimmed = value.trim();
|
|
268
|
+
try {
|
|
269
|
+
const url = new URL(trimmed);
|
|
270
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
271
|
+
throw new ConfigError("api.baseUrl must use https unless it targets localhost");
|
|
272
|
+
}
|
|
273
|
+
return url.toString().replace(/\/$/, "");
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error instanceof ConfigError) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
throw new ConfigError("api.baseUrl must be an absolute URL");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function writeJsonAtomic(path, value) {
|
|
283
|
+
mkdirSync(dirname(path), {
|
|
284
|
+
recursive: true,
|
|
285
|
+
mode: 0o700
|
|
286
|
+
});
|
|
287
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
288
|
+
try {
|
|
289
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
flag: "wx",
|
|
292
|
+
mode: 0o600
|
|
293
|
+
});
|
|
294
|
+
renameSync(tempPath, path);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
rmSync(tempPath, {
|
|
298
|
+
force: true
|
|
299
|
+
});
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import InkSpinner from "ink-spinner";
|
|
4
|
+
function packageCount(n) {
|
|
5
|
+
return n === 1 ? "1 package" : `${n} packages`;
|
|
6
|
+
}
|
|
7
|
+
function rule() {
|
|
8
|
+
const width = Math.max(20, Math.min((process.stdout.columns ?? 80) - 4, 56));
|
|
9
|
+
return "─".repeat(width);
|
|
10
|
+
}
|
|
11
|
+
export const LiveInstall = ({ view }) => {
|
|
12
|
+
if (view.total === 0 && !view.blocked) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (view.phase === "scanning") {
|
|
16
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" DG verifying ", packageCount(view.total), "\u2026"] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
|
|
17
|
+
}
|
|
18
|
+
if (view.blocked) {
|
|
19
|
+
const blocked = view.blocked;
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: rule() }), blocked.kind === "blocked" ? (_jsxs(Text, { color: "red", children: ["\u2718 DG blocked install \u2014 ", blocked.headline] })) : (_jsxs(Text, { color: "yellow", children: ["? DG could not verify ", blocked.packageName, " \u2014 ", blocked.headline] })), _jsxs(Text, { children: [" ", blocked.packageName, " ", blocked.reason] }), blocked.override ? _jsxs(Text, { dimColor: true, children: [" ", "Override: ", blocked.override] }) : null, blocked.nextStep ? _jsxs(Text, { dimColor: true, children: [" ", "Next: ", blocked.nextStep] }) : null] }));
|
|
21
|
+
}
|
|
22
|
+
const total = view.verified + view.flagged;
|
|
23
|
+
return (_jsx(Box, { paddingX: 1, children: view.flagged > 0 ? (_jsxs(Text, { color: "yellow", children: ["\u26A0 DG verified ", packageCount(total), " \u2014 ", view.flagged, " flagged"] })) : (_jsxs(Text, { color: "green", children: ["\u2713 DG verified ", packageCount(total), " \u2014 clean"] })) }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const VERIFIED_BAD = new Set([
|
|
2
|
+
"malware",
|
|
3
|
+
"policy",
|
|
4
|
+
"license",
|
|
5
|
+
"hash-mismatch",
|
|
6
|
+
"private-upload-disabled"
|
|
7
|
+
]);
|
|
8
|
+
const HEADLINES = {
|
|
9
|
+
pass: "clean",
|
|
10
|
+
warn: "flagged",
|
|
11
|
+
malware: "confirmed malware",
|
|
12
|
+
policy: "blocked by policy",
|
|
13
|
+
license: "license policy",
|
|
14
|
+
"hash-mismatch": "artifact integrity mismatch",
|
|
15
|
+
"private-upload-disabled": "private artifact not scanned",
|
|
16
|
+
"api-unavailable": "scanner unavailable",
|
|
17
|
+
"quota-exceeded": "monthly scan limit reached",
|
|
18
|
+
"api-timeout": "scanner timed out",
|
|
19
|
+
"registry-timeout": "registry timed out",
|
|
20
|
+
"analysis-incomplete": "analysis incomplete",
|
|
21
|
+
"unsupported-manager": "unsupported package manager",
|
|
22
|
+
"proxy-setup-failure": "protection unavailable"
|
|
23
|
+
};
|
|
24
|
+
const NEXT_STEP = {
|
|
25
|
+
malware: "Do not install. Remove the dependency or pin a known-safe version.",
|
|
26
|
+
policy: "Adjust the dependency to satisfy policy, or ask your admin.",
|
|
27
|
+
license: "Replace the dependency or update your license policy.",
|
|
28
|
+
"hash-mismatch": "Clear your package cache and retry. If it persists, do not install.",
|
|
29
|
+
"private-upload-disabled": "Enable private artifact scanning to verify this package.",
|
|
30
|
+
"quota-exceeded": "Upgrade your plan or wait for your monthly limit to reset. See westbayberry.com/pricing."
|
|
31
|
+
};
|
|
32
|
+
export function describeBlockedInstall(decision) {
|
|
33
|
+
const verifiedBad = VERIFIED_BAD.has(decision.cause);
|
|
34
|
+
const override = decision.forceOverride && !decision.forceOverride.allowed
|
|
35
|
+
? "not allowed by your policy"
|
|
36
|
+
: "re-run with --dg-force-install";
|
|
37
|
+
const nextStep = verifiedBad
|
|
38
|
+
? NEXT_STEP[decision.cause]
|
|
39
|
+
: "Re-check later with 'dg verify', or override if you accept the risk.";
|
|
40
|
+
return {
|
|
41
|
+
kind: verifiedBad ? "blocked" : "unverified",
|
|
42
|
+
packageName: decision.packageName,
|
|
43
|
+
headline: HEADLINES[decision.cause],
|
|
44
|
+
reason: decision.reason,
|
|
45
|
+
...(nextStep ? { nextStep } : {}),
|
|
46
|
+
...(verifiedBad ? {} : { override })
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function renderInstallDecision(decision) {
|
|
50
|
+
if (decision.action === "pass") {
|
|
51
|
+
return `✓ DG verified ${decision.packageName} — clean\n`;
|
|
52
|
+
}
|
|
53
|
+
if (decision.action === "warn") {
|
|
54
|
+
if (decision.forceOverride?.allowed) {
|
|
55
|
+
return `⚠ DG override — installing ${decision.packageName} despite block (--dg-force-install)\n`;
|
|
56
|
+
}
|
|
57
|
+
return `⚠ DG flagged ${decision.packageName} (warn) — ${decision.reason}\n`;
|
|
58
|
+
}
|
|
59
|
+
const verifiedBad = VERIFIED_BAD.has(decision.cause);
|
|
60
|
+
const headline = HEADLINES[decision.cause];
|
|
61
|
+
const lines = [
|
|
62
|
+
verifiedBad
|
|
63
|
+
? `✘ DG blocked install — ${headline}`
|
|
64
|
+
: `? DG could not verify ${decision.packageName} — ${headline}`,
|
|
65
|
+
` ${decision.packageName} ${decision.reason}`
|
|
66
|
+
];
|
|
67
|
+
if (decision.dashboardUrl) {
|
|
68
|
+
lines.push(` Evidence: ${decision.dashboardUrl}`);
|
|
69
|
+
}
|
|
70
|
+
if (decision.unauthenticated) {
|
|
71
|
+
lines.push(" Auth: local policy only (run 'dg login' for full coverage)");
|
|
72
|
+
}
|
|
73
|
+
lines.push(decision.forceOverride && !decision.forceOverride.allowed
|
|
74
|
+
? " Override: not allowed by your policy"
|
|
75
|
+
: " Override: re-run with --dg-force-install");
|
|
76
|
+
const next = verifiedBad
|
|
77
|
+
? NEXT_STEP[decision.cause]
|
|
78
|
+
: "Re-check later with 'dg verify', or override if you accept the risk.";
|
|
79
|
+
if (next) {
|
|
80
|
+
lines.push(` Next: ${next}`);
|
|
81
|
+
}
|
|
82
|
+
return `${lines.join("\n")}\n`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, useApp } from "ink";
|
|
3
|
+
import { LiveInstall } from "./LiveInstall.js";
|
|
4
|
+
function createViewStore(initial) {
|
|
5
|
+
let current = initial;
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
return {
|
|
8
|
+
get: () => current,
|
|
9
|
+
set: (view) => {
|
|
10
|
+
current = view;
|
|
11
|
+
for (const listener of listeners) {
|
|
12
|
+
listener();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
subscribe: (listener) => {
|
|
16
|
+
listeners.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
listeners.delete(listener);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const App = ({ store }) => {
|
|
24
|
+
const view = React.useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
25
|
+
const { exit } = useApp();
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (view.phase !== "done") {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const timer = setTimeout(() => exit(), 30);
|
|
31
|
+
return () => clearTimeout(timer);
|
|
32
|
+
}, [view.phase, exit]);
|
|
33
|
+
return React.createElement(LiveInstall, { view });
|
|
34
|
+
};
|
|
35
|
+
export async function renderLiveInstall(run) {
|
|
36
|
+
const store = createViewStore({ phase: "scanning", total: 0, verified: 0, flagged: 0 });
|
|
37
|
+
const restoreCursor = () => process.stdout.write(String.fromCharCode(27) + "[?25h");
|
|
38
|
+
process.once("exit", restoreCursor);
|
|
39
|
+
const instance = render(React.createElement(App, { store }));
|
|
40
|
+
try {
|
|
41
|
+
return await run((view) => store.set(view));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
store.set({ ...store.get(), phase: "done" });
|
|
45
|
+
await instance.waitUntilExit().catch(() => undefined);
|
|
46
|
+
process.off("exit", restoreCursor);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
export function defaultPromptIo() {
|
|
3
|
+
return {
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stderr,
|
|
6
|
+
isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export async function promptYesNo(question, io) {
|
|
10
|
+
if (!io.isTTY) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const rl = createInterface({ input: io.input, output: io.output });
|
|
14
|
+
try {
|
|
15
|
+
const answer = await new Promise((resolve) => {
|
|
16
|
+
rl.question(`${question} [y/N] `, resolve);
|
|
17
|
+
});
|
|
18
|
+
const normalized = answer.trim().toLowerCase();
|
|
19
|
+
return normalized === "y" || normalized === "yes";
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
rl.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { optionalPackageManagerNames, optionalSupportGate } from "../setup/optional-support.js";
|
|
2
|
+
const supportedManagers = ["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"];
|
|
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"]);
|
|
7
|
+
const pipProtected = new Set(["install", "download", "wheel"]);
|
|
8
|
+
const pipxProtected = new Set(["install", "upgrade", "inject", "run"]);
|
|
9
|
+
const uvProtected = new Set(["add", "sync"]);
|
|
10
|
+
const cargoProtected = new Set(["add", "install", "fetch", "update", "build", "test", "check", "run"]);
|
|
11
|
+
const passthrough = new Set(["help", "--help", "-h", "version", "--version", "-v", "list", "ls", "show", "freeze"]);
|
|
12
|
+
export function packageManagerNames() {
|
|
13
|
+
return [...supportedManagers, ...gatedManagers];
|
|
14
|
+
}
|
|
15
|
+
export function isSupportedPackageManager(manager) {
|
|
16
|
+
return supportedManagers.includes(manager);
|
|
17
|
+
}
|
|
18
|
+
export function classifyPackageManagerInvocation(manager, args) {
|
|
19
|
+
if (!isSupportedPackageManager(manager)) {
|
|
20
|
+
return unsupportedClassification(manager, args);
|
|
21
|
+
}
|
|
22
|
+
if (manager === "npx" || manager === "pnpx" || manager === "uvx") {
|
|
23
|
+
return protectedClassification(manager, args, manager, `${manager} fetches and runs package artifacts`);
|
|
24
|
+
}
|
|
25
|
+
if (manager === "npm") {
|
|
26
|
+
return classifyByCommand(manager, args, "npm", jsProtected, "npm install/fetch command", "javascript");
|
|
27
|
+
}
|
|
28
|
+
if (manager === "pnpm") {
|
|
29
|
+
return classifyByCommand(manager, args, "pnpm", pnpmProtected, "pnpm install/fetch command", "javascript");
|
|
30
|
+
}
|
|
31
|
+
if (manager === "yarn") {
|
|
32
|
+
return classifyByCommand(manager, args, "yarn", yarnProtected, "Yarn classic install/fetch command", "javascript");
|
|
33
|
+
}
|
|
34
|
+
if (manager === "pip") {
|
|
35
|
+
return classifyByCommand(manager, args, "pip", pipProtected, "pip install/fetch command", "python");
|
|
36
|
+
}
|
|
37
|
+
if (manager === "pipx") {
|
|
38
|
+
return classifyByCommand(manager, args, "pipx", pipxProtected, "pipx install/fetch command", "python");
|
|
39
|
+
}
|
|
40
|
+
if (manager === "uv") {
|
|
41
|
+
return classifyUv(args);
|
|
42
|
+
}
|
|
43
|
+
return classifyByCommand(manager, args, "cargo", cargoProtected, "Cargo command can fetch crates", "rust");
|
|
44
|
+
}
|
|
45
|
+
function classifyByCommand(manager, args, realBinaryName, protectedCommands, protectedReason, ecosystem) {
|
|
46
|
+
const action = firstCommand(args);
|
|
47
|
+
if (protectedCommands.has(action) || containsFetchSpec(args)) {
|
|
48
|
+
return {
|
|
49
|
+
manager,
|
|
50
|
+
ecosystem,
|
|
51
|
+
kind: "protected",
|
|
52
|
+
action,
|
|
53
|
+
reason: protectedReason,
|
|
54
|
+
realBinaryName,
|
|
55
|
+
args
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
manager,
|
|
60
|
+
ecosystem,
|
|
61
|
+
kind: "passthrough",
|
|
62
|
+
action,
|
|
63
|
+
reason: passthrough.has(action) ? "read-only or local package-manager command" : "not classified as an install/fetch command",
|
|
64
|
+
realBinaryName,
|
|
65
|
+
args
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function classifyUv(args) {
|
|
69
|
+
const action = firstCommand(args);
|
|
70
|
+
if (action === "pip" && pipProtected.has(args[1] ?? "")) {
|
|
71
|
+
return protectedClassification("uv", args, "uv", "uv pip install/fetch command");
|
|
72
|
+
}
|
|
73
|
+
if (action === "tool" && args[1] === "run") {
|
|
74
|
+
return protectedClassification("uv", args, "uv", "uv tool run fetches package artifacts");
|
|
75
|
+
}
|
|
76
|
+
if (uvProtected.has(action) || containsFetchSpec(args)) {
|
|
77
|
+
return protectedClassification("uv", args, "uv", "uv install/fetch command");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
manager: "uv",
|
|
81
|
+
ecosystem: "python",
|
|
82
|
+
kind: "passthrough",
|
|
83
|
+
action,
|
|
84
|
+
reason: passthrough.has(action) ? "read-only or local uv command" : "not classified as an install/fetch command",
|
|
85
|
+
realBinaryName: "uv",
|
|
86
|
+
args
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function protectedClassification(manager, args, realBinaryName, reason) {
|
|
90
|
+
return {
|
|
91
|
+
manager,
|
|
92
|
+
ecosystem: manager === "cargo" ? "rust" : manager === "pip" || manager === "pipx" || manager === "uv" || manager === "uvx" ? "python" : "javascript",
|
|
93
|
+
kind: "protected",
|
|
94
|
+
action: firstCommand(args),
|
|
95
|
+
reason,
|
|
96
|
+
realBinaryName,
|
|
97
|
+
args
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function unsupportedClassification(manager, args) {
|
|
101
|
+
return {
|
|
102
|
+
manager,
|
|
103
|
+
ecosystem: "gated",
|
|
104
|
+
kind: "unsupported",
|
|
105
|
+
action: firstCommand(args),
|
|
106
|
+
reason: optionalSupportGate(manager).message,
|
|
107
|
+
realBinaryName: manager,
|
|
108
|
+
args
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function firstCommand(args) {
|
|
112
|
+
return args.find((arg) => !arg.startsWith("-")) ?? "";
|
|
113
|
+
}
|
|
114
|
+
function containsFetchSpec(args) {
|
|
115
|
+
return args.some((arg) => /^https?:\/\//.test(arg) || /^git\+https?:\/\//.test(arg) || /\.t(ar\.)?gz(?:$|[#?])/.test(arg));
|
|
116
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function buildProxyChildEnv(options) {
|
|
2
|
+
const env = {
|
|
3
|
+
...options.baseEnv,
|
|
4
|
+
DG_PROXY_ACTIVE: "1"
|
|
5
|
+
};
|
|
6
|
+
const noProxy = mergeNoProxy(options.baseEnv.NO_PROXY ?? options.baseEnv.no_proxy, options.noProxyHosts ?? ["127.0.0.1", "localhost"]);
|
|
7
|
+
if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(options.manager)) {
|
|
8
|
+
env.HTTP_PROXY = options.proxyUrl;
|
|
9
|
+
env.HTTPS_PROXY = options.proxyUrl;
|
|
10
|
+
env.npm_config_proxy = options.proxyUrl;
|
|
11
|
+
env.npm_config_https_proxy = options.proxyUrl;
|
|
12
|
+
env.NO_PROXY = noProxy;
|
|
13
|
+
env.NODE_EXTRA_CA_CERTS = options.caBundlePath;
|
|
14
|
+
if (options.cacheDir) {
|
|
15
|
+
env.npm_config_cache = options.cacheDir;
|
|
16
|
+
if (options.manager === "yarn") {
|
|
17
|
+
env.YARN_CACHE_FOLDER = options.cacheDir;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
22
|
+
if (options.manager === "pip" || options.manager === "pipx") {
|
|
23
|
+
env.http_proxy = options.proxyUrl;
|
|
24
|
+
env.https_proxy = options.proxyUrl;
|
|
25
|
+
env.no_proxy = noProxy;
|
|
26
|
+
env.REQUESTS_CA_BUNDLE = options.caBundlePath;
|
|
27
|
+
env.PIP_CERT = options.caBundlePath;
|
|
28
|
+
env.PIP_NO_CACHE_DIR = "1";
|
|
29
|
+
return env;
|
|
30
|
+
}
|
|
31
|
+
if (options.manager === "uv" || options.manager === "uvx") {
|
|
32
|
+
env.HTTP_PROXY = options.proxyUrl;
|
|
33
|
+
env.HTTPS_PROXY = options.proxyUrl;
|
|
34
|
+
env.ALL_PROXY = options.proxyUrl;
|
|
35
|
+
env.NO_PROXY = noProxy;
|
|
36
|
+
env.SSL_CERT_FILE = options.caBundlePath;
|
|
37
|
+
env.UV_NO_CACHE = "1";
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
40
|
+
env.HTTPS_PROXY = options.proxyUrl;
|
|
41
|
+
env.https_proxy = options.proxyUrl;
|
|
42
|
+
env.http_proxy = options.proxyUrl;
|
|
43
|
+
env.NO_PROXY = noProxy;
|
|
44
|
+
env.CARGO_HTTP_CAINFO = options.caBundlePath;
|
|
45
|
+
return env;
|
|
46
|
+
}
|
|
47
|
+
function mergeNoProxy(existing, required) {
|
|
48
|
+
const values = new Set((existing ?? "").split(",").map((part) => part.trim()).filter(Boolean));
|
|
49
|
+
for (const host of required) {
|
|
50
|
+
values.add(host);
|
|
51
|
+
}
|
|
52
|
+
return [...values].join(",");
|
|
53
|
+
}
|