@westbayberry/dg 2.0.7 → 2.0.10
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 +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +158 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/pip-report.js +18 -2
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
package/dist/config/settings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { resolveDgPaths } from "../state/index.js";
|
|
4
|
+
import { acquireLockSyncWithRetry, resolveDgPaths } from "../state/index.js";
|
|
5
5
|
export const CONFIG_KEYS = Object.freeze([
|
|
6
6
|
"api.baseUrl",
|
|
7
7
|
"org.id",
|
|
@@ -59,10 +59,12 @@ export function loadUserConfig(env = process.env) {
|
|
|
59
59
|
return DEFAULT_CONFIG;
|
|
60
60
|
}
|
|
61
61
|
try {
|
|
62
|
-
|
|
63
|
-
return normalizeConfig(parsed);
|
|
62
|
+
return normalizeConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
64
63
|
}
|
|
65
64
|
catch (error) {
|
|
65
|
+
if (error instanceof ConfigError) {
|
|
66
|
+
throw new ConfigError(`Invalid dg config at ${path}: ${error.message}`);
|
|
67
|
+
}
|
|
66
68
|
throw new ConfigError(`Malformed dg config at ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -70,6 +72,28 @@ export function saveUserConfig(config, env = process.env) {
|
|
|
70
72
|
const paths = resolveDgPaths(env);
|
|
71
73
|
writeJsonAtomic(userConfigPath(paths), config);
|
|
72
74
|
}
|
|
75
|
+
export const USER_CONFIG_LOCK = "user-config";
|
|
76
|
+
const USER_CONFIG_LOCK_TIMEOUT_MS = 5_000;
|
|
77
|
+
const USER_CONFIG_LOCK_STALE_MS = 60_000;
|
|
78
|
+
export function withUserConfigLock(env, action) {
|
|
79
|
+
const lock = acquireLockSyncWithRetry(resolveDgPaths(env), USER_CONFIG_LOCK, {
|
|
80
|
+
staleMs: USER_CONFIG_LOCK_STALE_MS,
|
|
81
|
+
timeoutMs: USER_CONFIG_LOCK_TIMEOUT_MS
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
return action();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
lock.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function updateUserConfig(apply, env = process.env) {
|
|
91
|
+
return withUserConfigLock(env, () => {
|
|
92
|
+
const next = apply(loadUserConfig(env));
|
|
93
|
+
saveUserConfig(next, env);
|
|
94
|
+
return next;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
73
97
|
export function getConfigValue(config, key) {
|
|
74
98
|
if (key === "api.baseUrl") {
|
|
75
99
|
return config.api.baseUrl;
|
|
@@ -166,7 +190,7 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
166
190
|
return {
|
|
167
191
|
...config,
|
|
168
192
|
audit: {
|
|
169
|
-
upload: parseBoolean(rawValue)
|
|
193
|
+
upload: parseBoolean(rawValue, key)
|
|
170
194
|
}
|
|
171
195
|
};
|
|
172
196
|
}
|
|
@@ -174,14 +198,14 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
174
198
|
return {
|
|
175
199
|
...config,
|
|
176
200
|
telemetry: {
|
|
177
|
-
enabled: parseBoolean(rawValue)
|
|
201
|
+
enabled: parseBoolean(rawValue, key)
|
|
178
202
|
}
|
|
179
203
|
};
|
|
180
204
|
}
|
|
181
205
|
return {
|
|
182
206
|
...config,
|
|
183
207
|
webhooks: {
|
|
184
|
-
enabled: parseBoolean(rawValue)
|
|
208
|
+
enabled: parseBoolean(rawValue, key)
|
|
185
209
|
}
|
|
186
210
|
};
|
|
187
211
|
}
|
|
@@ -192,38 +216,93 @@ export function isConfigKey(value) {
|
|
|
192
216
|
return CONFIG_KEYS.includes(value);
|
|
193
217
|
}
|
|
194
218
|
function normalizeConfig(raw) {
|
|
219
|
+
if (!isPlainObject(raw)) {
|
|
220
|
+
throw new ConfigError(`config must be a JSON object, got ${describeJsonType(raw)}`);
|
|
221
|
+
}
|
|
195
222
|
if (raw.version !== undefined && raw.version !== 1) {
|
|
196
223
|
throw new ConfigError("unsupported config version");
|
|
197
224
|
}
|
|
225
|
+
const api = fieldObject(raw, "api");
|
|
226
|
+
const org = fieldObject(raw, "org");
|
|
227
|
+
const policy = fieldObject(raw, "policy");
|
|
228
|
+
const gitHook = fieldObject(raw, "gitHook");
|
|
229
|
+
const audit = fieldObject(raw, "audit");
|
|
230
|
+
const telemetry = fieldObject(raw, "telemetry");
|
|
231
|
+
const webhooks = fieldObject(raw, "webhooks");
|
|
198
232
|
return {
|
|
199
233
|
version: 1,
|
|
200
234
|
api: {
|
|
201
|
-
baseUrl: parseUrl(
|
|
235
|
+
baseUrl: parseUrl(fieldString(api, "api.baseUrl", "baseUrl") ?? DEFAULT_CONFIG.api.baseUrl)
|
|
202
236
|
},
|
|
203
237
|
org: {
|
|
204
|
-
id:
|
|
238
|
+
id: fieldString(org, "org.id", "id") ?? DEFAULT_CONFIG.org.id
|
|
205
239
|
},
|
|
206
240
|
policy: {
|
|
207
|
-
mode: parsePolicyMode(
|
|
208
|
-
trustProjectAllowlists:
|
|
209
|
-
allowForceOverride:
|
|
210
|
-
scriptHardening:
|
|
241
|
+
mode: parsePolicyMode(fieldString(policy, "policy.mode", "mode") ?? DEFAULT_CONFIG.policy.mode),
|
|
242
|
+
trustProjectAllowlists: fieldBoolean(policy, "policy.trustProjectAllowlists", "trustProjectAllowlists") ?? DEFAULT_CONFIG.policy.trustProjectAllowlists,
|
|
243
|
+
allowForceOverride: fieldBoolean(policy, "policy.allowForceOverride", "allowForceOverride") ?? DEFAULT_CONFIG.policy.allowForceOverride,
|
|
244
|
+
scriptHardening: fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening
|
|
211
245
|
},
|
|
212
246
|
gitHook: {
|
|
213
|
-
onWarn: parseOnWarn(
|
|
214
|
-
onIncomplete: parseOnIncomplete(
|
|
247
|
+
onWarn: parseOnWarn(fieldString(gitHook, "gitHook.onWarn", "onWarn") ?? DEFAULT_CONFIG.gitHook.onWarn),
|
|
248
|
+
onIncomplete: parseOnIncomplete(fieldString(gitHook, "gitHook.onIncomplete", "onIncomplete") ?? DEFAULT_CONFIG.gitHook.onIncomplete)
|
|
215
249
|
},
|
|
216
250
|
audit: {
|
|
217
|
-
upload:
|
|
251
|
+
upload: fieldBoolean(audit, "audit.upload", "upload") ?? DEFAULT_CONFIG.audit.upload
|
|
218
252
|
},
|
|
219
253
|
telemetry: {
|
|
220
|
-
enabled:
|
|
254
|
+
enabled: fieldBoolean(telemetry, "telemetry.enabled", "enabled") ?? DEFAULT_CONFIG.telemetry.enabled
|
|
221
255
|
},
|
|
222
256
|
webhooks: {
|
|
223
|
-
enabled:
|
|
257
|
+
enabled: fieldBoolean(webhooks, "webhooks.enabled", "enabled") ?? DEFAULT_CONFIG.webhooks.enabled
|
|
224
258
|
}
|
|
225
259
|
};
|
|
226
260
|
}
|
|
261
|
+
function fieldObject(root, field) {
|
|
262
|
+
const value = root[field];
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
if (!isPlainObject(value)) {
|
|
267
|
+
throw new ConfigError(`${field} must be a JSON object, got ${describeJsonType(value)}`);
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function fieldBoolean(section, field, key) {
|
|
272
|
+
const value = section[key];
|
|
273
|
+
if (value === undefined) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (typeof value === "boolean") {
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
throw new ConfigError(`${field} must be a JSON boolean (true or false), got ${describeJsonType(value)}`);
|
|
280
|
+
}
|
|
281
|
+
function fieldString(section, field, key) {
|
|
282
|
+
const value = section[key];
|
|
283
|
+
if (value === undefined) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
if (typeof value === "string") {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
throw new ConfigError(`${field} must be a string, got ${describeJsonType(value)}`);
|
|
290
|
+
}
|
|
291
|
+
function isPlainObject(value) {
|
|
292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
293
|
+
}
|
|
294
|
+
function describeJsonType(value) {
|
|
295
|
+
if (value === null) {
|
|
296
|
+
return "null";
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
return "an array";
|
|
300
|
+
}
|
|
301
|
+
if (typeof value === "string") {
|
|
302
|
+
return `the string "${value.length > 32 ? `${value.slice(0, 32)}…` : value}"`;
|
|
303
|
+
}
|
|
304
|
+
return `a ${typeof value}`;
|
|
305
|
+
}
|
|
227
306
|
function withPolicyBoolean(config, key, rawValue) {
|
|
228
307
|
if (key === "mode") {
|
|
229
308
|
return config;
|
|
@@ -232,7 +311,7 @@ function withPolicyBoolean(config, key, rawValue) {
|
|
|
232
311
|
...config,
|
|
233
312
|
policy: {
|
|
234
313
|
...config.policy,
|
|
235
|
-
[key]: parseBoolean(rawValue)
|
|
314
|
+
[key]: parseBoolean(rawValue, `policy.${key}`)
|
|
236
315
|
}
|
|
237
316
|
};
|
|
238
317
|
}
|
|
@@ -254,21 +333,22 @@ function parseOnIncomplete(value) {
|
|
|
254
333
|
}
|
|
255
334
|
throw new ConfigError("gitHook.onIncomplete must be one of: allow, block");
|
|
256
335
|
}
|
|
257
|
-
function parseBoolean(value) {
|
|
336
|
+
function parseBoolean(value, field) {
|
|
258
337
|
if (value === "true") {
|
|
259
338
|
return true;
|
|
260
339
|
}
|
|
261
340
|
if (value === "false") {
|
|
262
341
|
return false;
|
|
263
342
|
}
|
|
264
|
-
throw new ConfigError(
|
|
343
|
+
throw new ConfigError(`${field} must be true or false`);
|
|
265
344
|
}
|
|
266
345
|
function parseUrl(value) {
|
|
267
346
|
const trimmed = value.trim();
|
|
268
347
|
try {
|
|
269
348
|
const url = new URL(trimmed);
|
|
270
|
-
|
|
271
|
-
|
|
349
|
+
const localhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
350
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && localhost)) {
|
|
351
|
+
throw new ConfigError("api.baseUrl must use https, or http for localhost");
|
|
272
352
|
}
|
|
273
353
|
return url.toString().replace(/\/$/, "");
|
|
274
354
|
}
|
|
@@ -6,16 +6,19 @@ export function defaultPromptIo() {
|
|
|
6
6
|
isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
-
export async function promptYesNo(question, io) {
|
|
9
|
+
export async function promptYesNo(question, io, defaultYes = false) {
|
|
10
10
|
if (!io.isTTY) {
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
13
|
const rl = createInterface({ input: io.input, output: io.output });
|
|
14
14
|
try {
|
|
15
15
|
const answer = await new Promise((resolve) => {
|
|
16
|
-
rl.question(`${question} [y/N] `, resolve);
|
|
16
|
+
rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `, resolve);
|
|
17
17
|
});
|
|
18
18
|
const normalized = answer.trim().toLowerCase();
|
|
19
|
+
if (normalized === "") {
|
|
20
|
+
return defaultYes;
|
|
21
|
+
}
|
|
19
22
|
return normalized === "y" || normalized === "yes";
|
|
20
23
|
}
|
|
21
24
|
finally {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { analyzePackages } from "../api/analyze.js";
|
|
4
|
+
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
5
|
+
import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
|
|
6
|
+
const PROCEED = { proceed: true };
|
|
7
|
+
const UNRESOLVED = { set: undefined, count: undefined };
|
|
8
|
+
const approvedFlagRanks = new Map();
|
|
9
|
+
const pipResolutions = new Map();
|
|
10
|
+
export function resetInstallPreflightSession() {
|
|
11
|
+
approvedFlagRanks.clear();
|
|
12
|
+
pipResolutions.clear();
|
|
13
|
+
}
|
|
14
|
+
export function actionRank(action) {
|
|
15
|
+
return { pass: 0, analysis_incomplete: 1, warn: 2, block: 3 }[action];
|
|
16
|
+
}
|
|
17
|
+
export function recordPreflightApprovals(packages) {
|
|
18
|
+
for (const pkg of packages) {
|
|
19
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
20
|
+
const rank = actionRank(pkg.action);
|
|
21
|
+
if ((approvedFlagRanks.get(key) ?? -1) < rank) {
|
|
22
|
+
approvedFlagRanks.set(key, rank);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isPreflightApproved(pkg) {
|
|
27
|
+
return (approvedFlagRanks.get(`${pkg.name}@${pkg.version}`) ?? -1) >= actionRank(pkg.action);
|
|
28
|
+
}
|
|
29
|
+
export function cachedPipResolution(binary, args) {
|
|
30
|
+
return pipResolutions.get(pipResolutionKey(binary, args));
|
|
31
|
+
}
|
|
32
|
+
function pipResolutionKey(binary, args) {
|
|
33
|
+
return JSON.stringify([binary, ...args]);
|
|
34
|
+
}
|
|
35
|
+
function resolvePipInstallSet(binary, args, env) {
|
|
36
|
+
if (!binary)
|
|
37
|
+
return Promise.resolve(UNRESOLVED);
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let stdout = "";
|
|
40
|
+
let settled = false;
|
|
41
|
+
let timer;
|
|
42
|
+
const finish = (value) => {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
settled = true;
|
|
46
|
+
if (timer)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(value);
|
|
49
|
+
};
|
|
50
|
+
let child;
|
|
51
|
+
try {
|
|
52
|
+
child = spawn(binary, [...args, "--dry-run", "--report", "-", "--quiet"], {
|
|
53
|
+
env,
|
|
54
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
finish(UNRESOLVED);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
timer = setTimeout(() => {
|
|
62
|
+
try {
|
|
63
|
+
child.kill();
|
|
64
|
+
}
|
|
65
|
+
catch { /* already exited */ }
|
|
66
|
+
finish(UNRESOLVED);
|
|
67
|
+
}, 30_000);
|
|
68
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
69
|
+
child.on("error", () => finish(UNRESOLVED));
|
|
70
|
+
child.on("close", (code) => finish(code === 0
|
|
71
|
+
? { set: parsePipReportInstallSet(stdout), count: parsePipReportInstallCount(stdout) }
|
|
72
|
+
: UNRESOLVED));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function findingSummary(pkg) {
|
|
76
|
+
return pkg.reasons[0] ?? pkg.findings[0]?.title ?? pkg.findings[0]?.id ?? "flagged";
|
|
77
|
+
}
|
|
78
|
+
function renderPreflight(flagged, out) {
|
|
79
|
+
const blocks = flagged.filter((pkg) => pkg.action === "block").length;
|
|
80
|
+
const noun = flagged.length === 1 ? "package" : "packages";
|
|
81
|
+
const tail = blocks > 0 ? ` (${blocks} blocked)` : "";
|
|
82
|
+
out.write(`\n DG flagged ${flagged.length} ${noun} before install${tail}:\n`);
|
|
83
|
+
for (const pkg of flagged) {
|
|
84
|
+
const tag = pkg.action === "block" ? "block" : "warn";
|
|
85
|
+
out.write(` ${pkg.name}@${pkg.version} ${tag} ${findingSummary(pkg)}\n`);
|
|
86
|
+
}
|
|
87
|
+
out.write("\n");
|
|
88
|
+
}
|
|
89
|
+
export async function runInstallPreflight(manager, binary, childArgs, env) {
|
|
90
|
+
if (manager !== "pip" || !binary) {
|
|
91
|
+
return PROCEED;
|
|
92
|
+
}
|
|
93
|
+
const key = pipResolutionKey(binary, childArgs);
|
|
94
|
+
let resolution = pipResolutions.get(key);
|
|
95
|
+
if (!resolution) {
|
|
96
|
+
resolution = await resolvePipInstallSet(binary, childArgs, env);
|
|
97
|
+
pipResolutions.set(key, resolution);
|
|
98
|
+
}
|
|
99
|
+
const set = resolution.set;
|
|
100
|
+
if (!set || set.length === 0) {
|
|
101
|
+
return PROCEED;
|
|
102
|
+
}
|
|
103
|
+
let verdicts;
|
|
104
|
+
try {
|
|
105
|
+
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return PROCEED;
|
|
109
|
+
}
|
|
110
|
+
return decideFromVerdicts(verdicts.packages, defaultPromptIo());
|
|
111
|
+
}
|
|
112
|
+
export async function decideFromVerdicts(packages, io) {
|
|
113
|
+
const flagged = packages.filter((pkg) => (pkg.action === "warn" || pkg.action === "block") && !isPreflightApproved({ name: pkg.name, version: pkg.version, action: pkg.action }));
|
|
114
|
+
if (flagged.length === 0 || !io.isTTY) {
|
|
115
|
+
return PROCEED;
|
|
116
|
+
}
|
|
117
|
+
renderPreflight(flagged, io.output);
|
|
118
|
+
const hasBlock = flagged.some((pkg) => pkg.action === "block");
|
|
119
|
+
const accepted = await promptPreflightYesNo(hasBlock ? " Override and install anyway?" : " Proceed?", io, false);
|
|
120
|
+
if (!accepted) {
|
|
121
|
+
return { proceed: false };
|
|
122
|
+
}
|
|
123
|
+
recordPreflightApprovals(flagged.map((pkg) => ({ name: pkg.name, version: pkg.version, action: pkg.action ?? "warn" })));
|
|
124
|
+
return hasBlock ? { proceed: true, forceOverride: { force: true } } : PROCEED;
|
|
125
|
+
}
|
|
126
|
+
export async function promptPreflightYesNo(question, io, defaultYes) {
|
|
127
|
+
if (!io.isTTY) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const rl = createInterface({ input: io.input, output: io.output });
|
|
131
|
+
try {
|
|
132
|
+
const answer = await new Promise((resolve) => {
|
|
133
|
+
rl.once("SIGINT", () => resolve(undefined));
|
|
134
|
+
rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `, resolve);
|
|
135
|
+
});
|
|
136
|
+
if (answer === undefined) {
|
|
137
|
+
rl.close();
|
|
138
|
+
restoreRawInput(io.input);
|
|
139
|
+
io.output.write("\n");
|
|
140
|
+
process.exit(130);
|
|
141
|
+
}
|
|
142
|
+
const normalized = answer.trim().toLowerCase();
|
|
143
|
+
if (normalized === "") {
|
|
144
|
+
return defaultYes;
|
|
145
|
+
}
|
|
146
|
+
return normalized === "y" || normalized === "yes";
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
rl.close();
|
|
150
|
+
restoreRawInput(io.input);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function restoreRawInput(input) {
|
|
154
|
+
const stream = input;
|
|
155
|
+
if (stream.isTTY && typeof stream.setRawMode === "function") {
|
|
156
|
+
stream.setRawMode(false);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createLaunchPlan, runWithProductionProxyLive } from "./run.js";
|
|
1
|
+
import { createLaunchPlan, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
2
|
+
import { runInstallPreflight } from "./install-preflight.js";
|
|
2
3
|
import { isSupportedPackageManager } from "./classify.js";
|
|
3
4
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
4
5
|
const FALL_THROUGH = { handled: false };
|
|
@@ -16,9 +17,17 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
16
17
|
if (plan.classification.kind !== "protected" || !plan.realBinary.path) {
|
|
17
18
|
return FALL_THROUGH;
|
|
18
19
|
}
|
|
20
|
+
let effectiveOverride = forceOverride;
|
|
21
|
+
if (!forceOverride) {
|
|
22
|
+
const preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env);
|
|
23
|
+
if (!preflight.proceed) {
|
|
24
|
+
return { handled: true, result: { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: " Install cancelled.\n" } };
|
|
25
|
+
}
|
|
26
|
+
effectiveOverride = preflight.forceOverride;
|
|
27
|
+
}
|
|
19
28
|
const runOptions = {
|
|
20
29
|
env,
|
|
21
|
-
...(
|
|
30
|
+
...(effectiveOverride ? { forceOverride: effectiveOverride } : {})
|
|
22
31
|
};
|
|
23
32
|
const { renderLiveInstall } = await import("../install-ui/live-install-app.js");
|
|
24
33
|
try {
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
const credentialUrlPattern = /\b([a-z][a-z0-9+.-]{0,31}:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
|
|
2
2
|
const authHeaderPattern = /\b(proxy-authorization|authorization):\s*[^\r\n]+/gi;
|
|
3
|
-
const tokenAssignmentPattern =
|
|
3
|
+
const tokenAssignmentPattern = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret))=([^\s]+)/gi;
|
|
4
4
|
const npmrcAuthPattern = /(_authToken|_auth|_password)\s*=\s*("[^"]*"|[^\s;,]+)/gi;
|
|
5
|
+
const jsonSecretPattern = /("[A-Za-z0-9_.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
|
|
5
6
|
const bearerPattern = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g;
|
|
6
|
-
const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|pypi-[A-Za-z0-9_-]{20,})\b/g;
|
|
7
|
+
const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[bp]-[A-Za-z0-9-]{10,})\b/g;
|
|
7
8
|
export function redactSecrets(text) {
|
|
8
9
|
return text
|
|
9
10
|
.replace(credentialUrlPattern, "$1<redacted>@")
|
|
10
11
|
.replace(authHeaderPattern, (_match, header) => `${header}: <redacted>`)
|
|
11
12
|
.replace(npmrcAuthPattern, "$1=<redacted>")
|
|
13
|
+
.replace(jsonSecretPattern, '$1"<redacted>"')
|
|
12
14
|
.replace(tokenAssignmentPattern, "$1=<redacted>")
|
|
13
15
|
.replace(bearerPattern, "Bearer <redacted>")
|
|
14
16
|
.replace(knownTokenShapePattern, "<redacted>");
|
|
15
17
|
}
|
|
16
18
|
const STREAM_FLUSH_QUIET_MS = 80;
|
|
17
19
|
const SECRET_TAIL_SCAN_CHARS = 80;
|
|
18
|
-
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|pypi-[\w-]*|(?:_authToken|_auth|_password|token|
|
|
20
|
+
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|pypi-[\w-]*|glpat-[\w-]*|xox[bp]-[\w-]*|[\w-]*(?:_authToken|_auth|_password|secret[_-]key|access[_-]key|api[_-]key|token|password|secret)=\S*|"[\w.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*"?[^"\n]*)$/i;
|
|
19
21
|
export function createStreamRedactor(emit) {
|
|
20
22
|
let pending = "";
|
|
21
23
|
let timer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
function parseInstallArray(stdout) {
|
|
2
2
|
const trimmed = stdout.trim();
|
|
3
3
|
if (!trimmed)
|
|
4
4
|
return undefined;
|
|
@@ -10,7 +10,7 @@ export function parsePipReportInstallCount(stdout) {
|
|
|
10
10
|
try {
|
|
11
11
|
const parsed = JSON.parse(candidate);
|
|
12
12
|
if (Array.isArray(parsed.install)) {
|
|
13
|
-
return parsed.install
|
|
13
|
+
return parsed.install;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
catch {
|
|
@@ -19,3 +19,19 @@ export function parsePipReportInstallCount(stdout) {
|
|
|
19
19
|
}
|
|
20
20
|
return undefined;
|
|
21
21
|
}
|
|
22
|
+
export function parsePipReportInstallCount(stdout) {
|
|
23
|
+
return parseInstallArray(stdout)?.length;
|
|
24
|
+
}
|
|
25
|
+
export function parsePipReportInstallSet(stdout) {
|
|
26
|
+
const install = parseInstallArray(stdout);
|
|
27
|
+
if (!install)
|
|
28
|
+
return undefined;
|
|
29
|
+
const set = [];
|
|
30
|
+
for (const entry of install) {
|
|
31
|
+
const metadata = entry.metadata;
|
|
32
|
+
if (metadata && typeof metadata.name === "string" && typeof metadata.version === "string") {
|
|
33
|
+
set.push({ name: metadata.name, version: metadata.version });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return set;
|
|
37
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { analyzePackages } from "../api/analyze.js";
|
|
2
2
|
import { isCiEnv } from "../presentation/mode.js";
|
|
3
3
|
import { renderInstallDecision } from "../install-ui/block-render.js";
|
|
4
|
-
import {
|
|
4
|
+
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
5
5
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
6
6
|
import { classifyPackageManagerInvocation, isSupportedPackageManager } from "./classify.js";
|
|
7
|
+
import { actionRank, promptPreflightYesNo, recordPreflightApprovals } from "./install-preflight.js";
|
|
7
8
|
import { redactSecrets } from "./output-redaction.js";
|
|
8
9
|
const ECOSYSTEM_BY_MANAGER = {
|
|
9
10
|
npm: "npm",
|
|
@@ -37,25 +38,27 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
37
38
|
if (specs.length === 0) {
|
|
38
39
|
return FALL_THROUGH;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
const flagged = [];
|
|
41
42
|
try {
|
|
42
43
|
const response = await (options.analyze ?? analyzePackages)(specs.map((spec) => ({ name: spec.name, version: spec.version })), { ecosystem, env });
|
|
43
44
|
for (const spec of specs) {
|
|
44
45
|
const pkg = response.packages.find((entry) => entry.name === spec.name && entry.version === spec.version);
|
|
45
46
|
const action = pkg?.action ?? "pass";
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
action,
|
|
49
|
-
spec,
|
|
50
|
-
reason: pkg?.reasons[0] ?? pkg?.findings[0]?.title ?? "flagged by the scanner"
|
|
51
|
-
};
|
|
47
|
+
if (action === "pass") {
|
|
48
|
+
continue;
|
|
52
49
|
}
|
|
50
|
+
flagged.push({
|
|
51
|
+
action,
|
|
52
|
+
spec,
|
|
53
|
+
reason: pkg?.reasons[0] ?? pkg?.findings[0]?.title ?? "flagged by the scanner"
|
|
54
|
+
});
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
catch {
|
|
56
58
|
return FALL_THROUGH;
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
const worst = flagged.reduce((top, entry) => (!top || actionRank(entry.action) > actionRank(top.action) ? entry : top), undefined);
|
|
61
|
+
if (!worst) {
|
|
59
62
|
return FALL_THROUGH;
|
|
60
63
|
}
|
|
61
64
|
if (worst.action === "block") {
|
|
@@ -80,8 +83,20 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
80
83
|
return FALL_THROUGH;
|
|
81
84
|
}
|
|
82
85
|
const label = `${worst.spec.name}@${worst.spec.version}`;
|
|
83
|
-
|
|
86
|
+
if (worst.action === "analysis_incomplete") {
|
|
87
|
+
const proceed = await promptPreflightYesNo(`? DG could not fully analyze ${label} (analysis incomplete) — ${worst.reason}. Proceed?`, io, true);
|
|
88
|
+
if (proceed) {
|
|
89
|
+
recordPreflightApprovals(flagged.map(asFlaggedPackage));
|
|
90
|
+
return FALL_THROUGH;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
handled: true,
|
|
94
|
+
result: { exitCode: 4, stdout: "", stderr: "Declined. Nothing was installed.\n" }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const proceed = await promptPreflightYesNo(`⚠ DG flagged ${label} (warn) — ${worst.reason}. Proceed?`, io, false);
|
|
84
98
|
if (proceed) {
|
|
99
|
+
recordPreflightApprovals(flagged.map(asFlaggedPackage));
|
|
85
100
|
return FALL_THROUGH;
|
|
86
101
|
}
|
|
87
102
|
return {
|
|
@@ -89,8 +104,12 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
89
104
|
result: { exitCode: 1, stdout: "", stderr: "Declined. Nothing was installed.\n" }
|
|
90
105
|
};
|
|
91
106
|
}
|
|
92
|
-
function
|
|
93
|
-
return {
|
|
107
|
+
function asFlaggedPackage(entry) {
|
|
108
|
+
return {
|
|
109
|
+
name: entry.spec.name,
|
|
110
|
+
version: entry.spec.version,
|
|
111
|
+
action: entry.action
|
|
112
|
+
};
|
|
94
113
|
}
|
|
95
114
|
function stripControlArgs(args) {
|
|
96
115
|
const childArgs = [];
|