@westbayberry/dg 2.0.6 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +89 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/pip-report.js +18 -2
- package/dist/proxy/server.js +1 -1
- package/dist/scan-ui/components/InteractiveResultsView.js +2 -2
- package/package.json +1 -1
|
@@ -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,89 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { analyzePackages } from "../api/analyze.js";
|
|
3
|
+
import { defaultPromptIo, promptYesNo } from "../install-ui/prompt.js";
|
|
4
|
+
import { parsePipReportInstallSet } from "./pip-report.js";
|
|
5
|
+
const PROCEED = { proceed: true };
|
|
6
|
+
function resolvePipInstallSet(binary, args, env) {
|
|
7
|
+
if (!binary)
|
|
8
|
+
return Promise.resolve(undefined);
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
let stdout = "";
|
|
11
|
+
let settled = false;
|
|
12
|
+
let timer;
|
|
13
|
+
const finish = (value) => {
|
|
14
|
+
if (settled)
|
|
15
|
+
return;
|
|
16
|
+
settled = true;
|
|
17
|
+
if (timer)
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve(value);
|
|
20
|
+
};
|
|
21
|
+
let child;
|
|
22
|
+
try {
|
|
23
|
+
child = spawn(binary, [...args, "--dry-run", "--report", "-", "--quiet"], {
|
|
24
|
+
env,
|
|
25
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
finish(undefined);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
try {
|
|
34
|
+
child.kill();
|
|
35
|
+
}
|
|
36
|
+
catch { /* already exited */ }
|
|
37
|
+
finish(undefined);
|
|
38
|
+
}, 30_000);
|
|
39
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
40
|
+
child.on("error", () => finish(undefined));
|
|
41
|
+
child.on("close", (code) => finish(code === 0 ? parsePipReportInstallSet(stdout) : undefined));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function findingSummary(pkg) {
|
|
45
|
+
return pkg.reasons[0] ?? pkg.findings[0]?.title ?? pkg.findings[0]?.id ?? "flagged";
|
|
46
|
+
}
|
|
47
|
+
function renderPreflight(flagged, out) {
|
|
48
|
+
const blocks = flagged.filter((pkg) => pkg.action === "block").length;
|
|
49
|
+
const noun = flagged.length === 1 ? "package" : "packages";
|
|
50
|
+
const tail = blocks > 0 ? ` (${blocks} blocked)` : "";
|
|
51
|
+
out.write(`\n DG flagged ${flagged.length} ${noun} before install${tail}:\n`);
|
|
52
|
+
for (const pkg of flagged) {
|
|
53
|
+
const tag = pkg.action === "block" ? "block" : "warn";
|
|
54
|
+
out.write(` ${pkg.name}@${pkg.version} ${tag} ${findingSummary(pkg)}\n`);
|
|
55
|
+
}
|
|
56
|
+
out.write("\n");
|
|
57
|
+
}
|
|
58
|
+
export async function runInstallPreflight(manager, binary, childArgs, env) {
|
|
59
|
+
if (manager !== "pip" || !binary) {
|
|
60
|
+
return PROCEED;
|
|
61
|
+
}
|
|
62
|
+
const set = await resolvePipInstallSet(binary, childArgs, env);
|
|
63
|
+
if (!set || set.length === 0) {
|
|
64
|
+
return PROCEED;
|
|
65
|
+
}
|
|
66
|
+
let verdicts;
|
|
67
|
+
try {
|
|
68
|
+
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return PROCEED;
|
|
72
|
+
}
|
|
73
|
+
return decideFromVerdicts(verdicts.packages, defaultPromptIo());
|
|
74
|
+
}
|
|
75
|
+
export async function decideFromVerdicts(packages, io) {
|
|
76
|
+
const flagged = packages.filter((pkg) => pkg.action === "warn" || pkg.action === "block");
|
|
77
|
+
if (flagged.length === 0 || !io.isTTY) {
|
|
78
|
+
return PROCEED;
|
|
79
|
+
}
|
|
80
|
+
renderPreflight(flagged, io.output);
|
|
81
|
+
const hasBlock = flagged.some((pkg) => pkg.action === "block");
|
|
82
|
+
const accepted = hasBlock
|
|
83
|
+
? await promptYesNo(" Override and install anyway?", io, false)
|
|
84
|
+
: await promptYesNo(" Proceed?", io, true);
|
|
85
|
+
if (!accepted) {
|
|
86
|
+
return { proceed: false };
|
|
87
|
+
}
|
|
88
|
+
return hasBlock ? { proceed: true, forceOverride: { force: true } } : PROCEED;
|
|
89
|
+
}
|
|
@@ -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,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
|
+
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -792,7 +792,7 @@ function scanTarballUploadPolicy(env) {
|
|
|
792
792
|
};
|
|
793
793
|
}
|
|
794
794
|
function installVerdictTimeoutMs(env) {
|
|
795
|
-
return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS,
|
|
795
|
+
return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS, 240_000);
|
|
796
796
|
}
|
|
797
797
|
function parsePositiveInteger(value, fallback) {
|
|
798
798
|
if (!value) {
|
|
@@ -1088,7 +1088,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1088
1088
|
const dpAbove = dpScroll;
|
|
1089
1089
|
const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
|
|
1090
1090
|
const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
|
|
1091
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed
|
|
1091
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] }) }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })] }));
|
|
1092
1092
|
}
|
|
1093
1093
|
}
|
|
1094
1094
|
// ── List mode ──
|
|
@@ -1108,7 +1108,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1108
1108
|
: chalk.yellow;
|
|
1109
1109
|
const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
|
|
1110
1110
|
return (_jsxs(Box, { flexDirection: "column", children: [isCursor ? (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("\u258C"), " ", chalk.cyan(arrow), " ", ` `, color(pad(label, 8)), chalk.bold(pad(truncate(names, nameCol - 2), nameCol)), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3)), " "] })) : (_jsxs(Text, { children: [` ${chalk.dim(arrow)} `, color(pad(label, 8)), pad(truncate(names, nameCol - 2), nameCol), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3))] })), level === "summary" && (_jsx(FindingsSummary, { group: group, maxWidth: innerWidth - 8, maxLines: globalIdx === view.expandedIndex ? animVisibleLines : undefined }))] }, group.key));
|
|
1111
|
-
}), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No flagged packages match "${searchQuery}"` })), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed
|
|
1111
|
+
}), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No flagged packages match "${searchQuery}"` })), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : (_jsxs(Text, { children: [" ", allGroupCount > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit"), " ", chalk.dim("\u00B7 Ctrl+C or q to exit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
|
|
1112
1112
|
};
|
|
1113
1113
|
const T = {
|
|
1114
1114
|
branch: chalk.dim("\u251C\u2500\u2500"),
|