@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
package/dist/launcher/env.js
CHANGED
|
@@ -1,53 +1,100 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { proxyUrlWithAuth, readProxyAuthToken } from "../proxy/auth.js";
|
|
3
|
+
// The client read timeout must exceed the proxy's per-artifact verdict ceiling
|
|
4
|
+
// (DG_INSTALL_VERDICT_TIMEOUT_MS, default 240s) plus download headroom, so the
|
|
5
|
+
// package manager never gives up on a legitimately-slow verify. Overridable for
|
|
6
|
+
// constrained environments via DG_CLIENT_READ_TIMEOUT_S.
|
|
7
|
+
function clientReadTimeoutSeconds(env) {
|
|
8
|
+
const override = Number(env.DG_CLIENT_READ_TIMEOUT_S);
|
|
9
|
+
if (Number.isFinite(override) && override > 0) {
|
|
10
|
+
return Math.ceil(override);
|
|
11
|
+
}
|
|
12
|
+
const verdictMs = Number(env.DG_INSTALL_VERDICT_TIMEOUT_MS);
|
|
13
|
+
const verdictSeconds = Number.isFinite(verdictMs) && verdictMs > 0 ? verdictMs / 1000 : 240;
|
|
14
|
+
return Math.ceil(verdictSeconds + 60);
|
|
15
|
+
}
|
|
1
16
|
export function buildProxyChildEnv(options) {
|
|
17
|
+
const authToken = readProxyAuthToken(dirname(options.caBundlePath));
|
|
18
|
+
const proxyUrl = authToken ? proxyUrlWithAuth(options.proxyUrl, authToken) : options.proxyUrl;
|
|
2
19
|
const env = {
|
|
3
20
|
...options.baseEnv,
|
|
4
21
|
DG_PROXY_ACTIVE: "1"
|
|
5
22
|
};
|
|
6
|
-
|
|
23
|
+
// The wrapped package manager and every lifecycle/postinstall script it runs
|
|
24
|
+
// are untrusted. The dg account credential must never reach them; only the
|
|
25
|
+
// trusted proxy worker (which makes the authenticated API calls) keeps it.
|
|
26
|
+
delete env.DG_API_KEY;
|
|
27
|
+
delete env.DG_API_TOKEN;
|
|
28
|
+
// dg fully controls NO_PROXY (loopback only). An inherited NO_PROXY that names
|
|
29
|
+
// the registry (or a `*`/`.org` glob) would route the manager straight past the
|
|
30
|
+
// firewall, so the inherited value is dropped, not merged.
|
|
31
|
+
const noProxy = (options.noProxyHosts ?? ["127.0.0.1", "localhost"]).join(",");
|
|
32
|
+
env.NO_PROXY = noProxy;
|
|
33
|
+
env.no_proxy = noProxy;
|
|
34
|
+
const readTimeoutSeconds = clientReadTimeoutSeconds(options.baseEnv);
|
|
7
35
|
if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(options.manager)) {
|
|
8
|
-
env.HTTP_PROXY =
|
|
9
|
-
env.HTTPS_PROXY =
|
|
10
|
-
env.npm_config_proxy =
|
|
11
|
-
env.npm_config_https_proxy =
|
|
12
|
-
env.NO_PROXY = noProxy;
|
|
36
|
+
env.HTTP_PROXY = proxyUrl;
|
|
37
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
38
|
+
env.npm_config_proxy = proxyUrl;
|
|
39
|
+
env.npm_config_https_proxy = proxyUrl;
|
|
13
40
|
env.NODE_EXTRA_CA_CERTS = options.caBundlePath;
|
|
41
|
+
// The proxy withholds every artifact byte until its verdict resolves, so the
|
|
42
|
+
// package manager sees no response data until then. Its own network read
|
|
43
|
+
// timeout must outlast the proxy's worst-case verdict wait or it aborts a
|
|
44
|
+
// legitimate install mid-verify (pip's 15s default is what made large cold
|
|
45
|
+
// installs fail). Set it explicitly so a user-lowered global config can't
|
|
46
|
+
// undercut the proxy.
|
|
47
|
+
env.npm_config_fetch_timeout = String(readTimeoutSeconds * 1000);
|
|
48
|
+
if (options.manager === "yarn") {
|
|
49
|
+
env.YARN_NETWORK_TIMEOUT = String(readTimeoutSeconds * 1000);
|
|
50
|
+
}
|
|
51
|
+
if (options.manager === "yarn") {
|
|
52
|
+
// Yarn Berry (2+) ignores HTTP(S)_PROXY/npm_config_*; without its own
|
|
53
|
+
// proxy + CA config it fetches straight from the registry, bypassing the
|
|
54
|
+
// firewall. Classic Yarn ignores these YARN_ vars, so set them for both.
|
|
55
|
+
env.YARN_HTTP_PROXY = proxyUrl;
|
|
56
|
+
env.YARN_HTTPS_PROXY = proxyUrl;
|
|
57
|
+
env.YARN_HTTPS_CA_FILE_PATH = options.caBundlePath;
|
|
58
|
+
}
|
|
14
59
|
if (options.cacheDir) {
|
|
15
60
|
env.npm_config_cache = options.cacheDir;
|
|
16
61
|
if (options.manager === "yarn") {
|
|
62
|
+
// Both: YARN_CACHE_FOLDER is Berry's per-project cache, but Berry 2+
|
|
63
|
+
// defaults to a shared global cache (~/.yarn/berry) that a warm install
|
|
64
|
+
// serves from without ever fetching, so redirect that too.
|
|
17
65
|
env.YARN_CACHE_FOLDER = options.cacheDir;
|
|
66
|
+
env.YARN_GLOBAL_FOLDER = options.cacheDir;
|
|
18
67
|
}
|
|
19
68
|
}
|
|
20
69
|
return env;
|
|
21
70
|
}
|
|
22
71
|
if (options.manager === "pip" || options.manager === "pipx") {
|
|
23
|
-
env.http_proxy =
|
|
24
|
-
env.https_proxy =
|
|
25
|
-
env.no_proxy = noProxy;
|
|
72
|
+
env.http_proxy = proxyUrl;
|
|
73
|
+
env.https_proxy = proxyUrl;
|
|
26
74
|
env.REQUESTS_CA_BUNDLE = options.caBundlePath;
|
|
27
75
|
env.PIP_CERT = options.caBundlePath;
|
|
28
76
|
env.PIP_NO_CACHE_DIR = "1";
|
|
77
|
+
env.PIP_DEFAULT_TIMEOUT = String(readTimeoutSeconds);
|
|
29
78
|
return env;
|
|
30
79
|
}
|
|
31
80
|
if (options.manager === "uv" || options.manager === "uvx") {
|
|
32
|
-
env.HTTP_PROXY =
|
|
33
|
-
env.HTTPS_PROXY =
|
|
34
|
-
env.ALL_PROXY =
|
|
35
|
-
env.NO_PROXY = noProxy;
|
|
81
|
+
env.HTTP_PROXY = proxyUrl;
|
|
82
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
83
|
+
env.ALL_PROXY = proxyUrl;
|
|
36
84
|
env.SSL_CERT_FILE = options.caBundlePath;
|
|
37
85
|
env.UV_NO_CACHE = "1";
|
|
86
|
+
env.UV_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
38
87
|
return env;
|
|
39
88
|
}
|
|
40
|
-
env.HTTPS_PROXY =
|
|
41
|
-
env.https_proxy =
|
|
42
|
-
env.http_proxy =
|
|
43
|
-
env.NO_PROXY = noProxy;
|
|
89
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
90
|
+
env.https_proxy = proxyUrl;
|
|
91
|
+
env.http_proxy = proxyUrl;
|
|
44
92
|
env.CARGO_HTTP_CAINFO = options.caBundlePath;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
values.add(host);
|
|
93
|
+
env.CARGO_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
94
|
+
if (options.cacheDir) {
|
|
95
|
+
// cargo serves a cached crate from ~/.cargo with zero network, skipping the
|
|
96
|
+
// firewall; pointing CARGO_HOME at the throwaway dir forces a re-fetch.
|
|
97
|
+
env.CARGO_HOME = options.cacheDir;
|
|
51
98
|
}
|
|
52
|
-
return
|
|
99
|
+
return env;
|
|
53
100
|
}
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { analyzePackages } from "../api/analyze.js";
|
|
4
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
5
|
+
import { matchDecision, packageKey } from "../decisions/apply.js";
|
|
6
|
+
import { offerRememberOnIo } from "../decisions/remember-prompt.js";
|
|
7
|
+
import { provenanceDowngradeLine } from "../presentation/provenance.js";
|
|
4
8
|
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
9
|
+
import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCooldownExempt, isCooldownExemptByDgFile } from "../policy/cooldown.js";
|
|
10
|
+
import { findProjectRoot, loadDgFile, resolveAcceptedBy, warnUnreadableDgFile } from "../project/dgfile.js";
|
|
5
11
|
import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
|
|
12
|
+
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
13
|
+
export function resolvePreflightCooldown(env, ecosystem) {
|
|
14
|
+
try {
|
|
15
|
+
const config = loadUserConfig(env);
|
|
16
|
+
const param = cooldownRequestParam(config, env, ecosystem, "");
|
|
17
|
+
return param ? { param, exempt: config.cooldown.exempt, ecosystem } : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isQuarantined(pkg, context, dgExemptions = []) {
|
|
24
|
+
if (!context || !pkg.cooldown) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (isCooldownExempt(pkg.name, context.exempt, context.ecosystem)
|
|
28
|
+
|| isCooldownExemptByDgFile(pkg.name, context.ecosystem, dgExemptions)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return pkg.cooldown.status === "quarantine"
|
|
32
|
+
|| (pkg.cooldown.status === "unknown" && context.param.onUnknown === "block");
|
|
33
|
+
}
|
|
34
|
+
export function resolvePreflightDecisions(ecosystem, cwd, env = process.env) {
|
|
35
|
+
const root = findProjectRoot(cwd, env);
|
|
36
|
+
if (!root) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const file = loadDgFile(root);
|
|
40
|
+
warnUnreadableDgFile(file);
|
|
41
|
+
if (!file.readable) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { root, file, ecosystem, env };
|
|
45
|
+
}
|
|
6
46
|
const PROCEED = { proceed: true };
|
|
7
47
|
const UNRESOLVED = { set: undefined, count: undefined };
|
|
8
48
|
const approvedFlagRanks = new Map();
|
|
@@ -32,6 +72,15 @@ export function cachedPipResolution(binary, args) {
|
|
|
32
72
|
function pipResolutionKey(binary, args) {
|
|
33
73
|
return JSON.stringify([binary, ...args]);
|
|
34
74
|
}
|
|
75
|
+
// pip's dry-run resolve downloads sdists/wheels it cannot get PEP 658
|
|
76
|
+
// metadata for, so big dependency trees legitimately take minutes on a cold
|
|
77
|
+
// pip cache. A short timeout here silently skips the preflight (and with it
|
|
78
|
+
// the batched analyze + preverified handoff), pushing every package onto the
|
|
79
|
+
// slow per-artifact verdict path.
|
|
80
|
+
function pipResolveTimeoutMs(env) {
|
|
81
|
+
const raw = Number(env.DG_PIP_RESOLVE_TIMEOUT_MS);
|
|
82
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 120_000;
|
|
83
|
+
}
|
|
35
84
|
function resolvePipInstallSet(binary, args, env) {
|
|
36
85
|
if (!binary)
|
|
37
86
|
return Promise.resolve(UNRESOLVED);
|
|
@@ -49,9 +98,11 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
49
98
|
};
|
|
50
99
|
let child;
|
|
51
100
|
try {
|
|
52
|
-
|
|
101
|
+
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
102
|
+
child = spawn(invocation.command, [...invocation.args], {
|
|
53
103
|
env,
|
|
54
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
104
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
105
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
55
106
|
});
|
|
56
107
|
}
|
|
57
108
|
catch {
|
|
@@ -64,7 +115,7 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
64
115
|
}
|
|
65
116
|
catch { /* already exited */ }
|
|
66
117
|
finish(UNRESOLVED);
|
|
67
|
-
},
|
|
118
|
+
}, pipResolveTimeoutMs(env));
|
|
68
119
|
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
69
120
|
child.on("error", () => finish(UNRESOLVED));
|
|
70
121
|
child.on("close", (code) => finish(code === 0
|
|
@@ -75,18 +126,48 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
75
126
|
function findingSummary(pkg) {
|
|
76
127
|
return pkg.reasons[0] ?? pkg.findings[0]?.title ?? pkg.findings[0]?.id ?? "flagged";
|
|
77
128
|
}
|
|
129
|
+
function cooldownSummary(pkg) {
|
|
130
|
+
const cooldown = pkg.cooldown;
|
|
131
|
+
if (!cooldown) {
|
|
132
|
+
return "release too new";
|
|
133
|
+
}
|
|
134
|
+
const window = cooldown.requiredDays !== undefined ? formatCooldownDuration(cooldown.requiredDays) : "your cooldown";
|
|
135
|
+
if (cooldown.ageDays === undefined) {
|
|
136
|
+
return `publish time unknown; cooldown ${window}`;
|
|
137
|
+
}
|
|
138
|
+
return `published ${formatPackageAge(cooldown.ageDays)}; cooldown ${window}`;
|
|
139
|
+
}
|
|
78
140
|
function renderPreflight(flagged, out) {
|
|
79
|
-
const blocks = flagged.filter((
|
|
141
|
+
const blocks = flagged.filter((entry) => entry.action === "block").length;
|
|
80
142
|
const noun = flagged.length === 1 ? "package" : "packages";
|
|
81
143
|
const tail = blocks > 0 ? ` (${blocks} blocked)` : "";
|
|
82
144
|
out.write(`\n DG flagged ${flagged.length} ${noun} before install${tail}:\n`);
|
|
83
|
-
for (const
|
|
84
|
-
const tag =
|
|
85
|
-
|
|
145
|
+
for (const entry of flagged) {
|
|
146
|
+
const tag = entry.viaCooldown ? "cooldown" : entry.action === "block" ? "block" : "warn";
|
|
147
|
+
const summary = entry.viaCooldown ? cooldownSummary(entry.pkg) : findingSummary(entry.pkg);
|
|
148
|
+
out.write(` ${entry.pkg.name}@${entry.pkg.version} ${tag} ${summary}${provenanceSuffix(entry.pkg)}\n`);
|
|
149
|
+
}
|
|
150
|
+
out.write("\n");
|
|
151
|
+
}
|
|
152
|
+
function provenanceSuffix(pkg) {
|
|
153
|
+
const prov = pkg.provenance;
|
|
154
|
+
if (!prov || prov.status === "unknown") {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
return ` provenance: ${prov.status}`;
|
|
158
|
+
}
|
|
159
|
+
export function renderProvenanceDowngrades(packages, out) {
|
|
160
|
+
const downgraded = packages.filter((pkg) => pkg.provenance?.downgrade);
|
|
161
|
+
if (downgraded.length === 0) {
|
|
162
|
+
return;
|
|
86
163
|
}
|
|
87
164
|
out.write("\n");
|
|
165
|
+
for (const pkg of downgraded) {
|
|
166
|
+
const line = provenanceDowngradeLine(pkg.version, pkg.provenance);
|
|
167
|
+
out.write(` ⚠ ${pkg.name}@${pkg.version}: ${line} (display only, verdict unchanged)\n`);
|
|
168
|
+
}
|
|
88
169
|
}
|
|
89
|
-
export async function runInstallPreflight(manager, binary, childArgs, env) {
|
|
170
|
+
export async function runInstallPreflight(manager, binary, childArgs, env, cwd = process.cwd(), beforeInteraction) {
|
|
90
171
|
if (manager !== "pip" || !binary) {
|
|
91
172
|
return PROCEED;
|
|
92
173
|
}
|
|
@@ -97,32 +178,101 @@ export async function runInstallPreflight(manager, binary, childArgs, env) {
|
|
|
97
178
|
pipResolutions.set(key, resolution);
|
|
98
179
|
}
|
|
99
180
|
const set = resolution.set;
|
|
100
|
-
if (!set
|
|
181
|
+
if (!set) {
|
|
182
|
+
process.stderr.write(" dg: could not resolve the install set ahead of time — verifying during install instead\n");
|
|
101
183
|
return PROCEED;
|
|
102
184
|
}
|
|
185
|
+
if (set.length === 0) {
|
|
186
|
+
return PROCEED;
|
|
187
|
+
}
|
|
188
|
+
const cooldownContext = resolvePreflightCooldown(env, "pypi");
|
|
103
189
|
let verdicts;
|
|
104
190
|
try {
|
|
105
|
-
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env });
|
|
191
|
+
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env, ...(cooldownContext ? { cooldown: cooldownContext.param } : {}) });
|
|
106
192
|
}
|
|
107
193
|
catch {
|
|
194
|
+
process.stderr.write(" dg: pre-install check unavailable — verifying during install instead\n");
|
|
108
195
|
return PROCEED;
|
|
109
196
|
}
|
|
110
|
-
|
|
197
|
+
beforeInteraction?.();
|
|
198
|
+
const decision = await decideFromVerdicts(verdicts.packages, defaultPromptIo(), cooldownContext, resolvePreflightDecisions("pypi", cwd, env));
|
|
199
|
+
if (!decision.proceed || decision.forceOverride) {
|
|
200
|
+
return decision;
|
|
201
|
+
}
|
|
202
|
+
const preverified = preverifiedEntries(verdicts.packages, cooldownContext !== undefined);
|
|
203
|
+
return preverified.length > 0 ? { ...decision, preverified } : decision;
|
|
204
|
+
}
|
|
205
|
+
export function preverifiedEntries(packages, cooldownRequested) {
|
|
206
|
+
return packages
|
|
207
|
+
.filter((pkg) => pkg.action === "pass" || pkg.action === "warn")
|
|
208
|
+
.map((pkg) => ({
|
|
209
|
+
ecosystem: "pypi",
|
|
210
|
+
name: pkg.name,
|
|
211
|
+
version: pkg.version,
|
|
212
|
+
action: pkg.action,
|
|
213
|
+
...(pkg.reasons[0] ? { reason: pkg.reasons[0] } : {}),
|
|
214
|
+
...(typeof pkg.artifactSha256 === "string" && pkg.artifactSha256.length > 0 ? { scannedSha256: pkg.artifactSha256 } : {}),
|
|
215
|
+
cooldownEvaluated: cooldownRequested && pkg.cooldown !== undefined
|
|
216
|
+
}));
|
|
111
217
|
}
|
|
112
|
-
export async function decideFromVerdicts(packages, io) {
|
|
113
|
-
|
|
114
|
-
|
|
218
|
+
export async function decideFromVerdicts(packages, io, cooldownContext, decisions = null) {
|
|
219
|
+
if (io.isTTY) {
|
|
220
|
+
renderProvenanceDowngrades(packages, io.output);
|
|
221
|
+
}
|
|
222
|
+
const covered = [];
|
|
223
|
+
const entries = packages
|
|
224
|
+
.map((pkg) => {
|
|
225
|
+
const viaCooldown = isQuarantined(pkg, cooldownContext, decisions?.file.cooldownExemptions ?? []) && pkg.action !== "block";
|
|
226
|
+
const action = viaCooldown ? "block" : pkg.action ?? "pass";
|
|
227
|
+
return { pkg, action, viaCooldown };
|
|
228
|
+
})
|
|
229
|
+
.filter((entry) => {
|
|
230
|
+
if ((entry.action !== "warn" && entry.action !== "block")
|
|
231
|
+
|| isPreflightApproved({ name: entry.pkg.name, version: entry.pkg.version, action: entry.action })) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (decisions && entry.action === "warn" && matchDecision(entry.pkg, decisions.ecosystem, decisions.file.decisions).acknowledged) {
|
|
235
|
+
covered.push(entry.pkg);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
});
|
|
240
|
+
if (covered.length > 0 && io.isTTY) {
|
|
241
|
+
renderCoveredWarns(covered, decisions, io.output);
|
|
242
|
+
recordPreflightApprovals(covered.map((pkg) => ({ name: pkg.name, version: pkg.version, action: "warn" })));
|
|
243
|
+
}
|
|
244
|
+
if (entries.length === 0 || !io.isTTY) {
|
|
115
245
|
return PROCEED;
|
|
116
246
|
}
|
|
117
|
-
renderPreflight(
|
|
118
|
-
const hasBlock =
|
|
247
|
+
renderPreflight(entries, io.output);
|
|
248
|
+
const hasBlock = entries.some((entry) => entry.action === "block");
|
|
119
249
|
const accepted = await promptPreflightYesNo(hasBlock ? " Override and install anyway?" : " Proceed?", io, false);
|
|
120
250
|
if (!accepted) {
|
|
121
251
|
return { proceed: false };
|
|
122
252
|
}
|
|
123
|
-
recordPreflightApprovals(
|
|
253
|
+
recordPreflightApprovals(entries.map((entry) => ({ name: entry.pkg.name, version: entry.pkg.version, action: entry.action })));
|
|
254
|
+
if (!hasBlock && decisions) {
|
|
255
|
+
await offerRememberOnIo({
|
|
256
|
+
io,
|
|
257
|
+
file: decisions.file,
|
|
258
|
+
packages: entries
|
|
259
|
+
.filter((entry) => entry.action === "warn")
|
|
260
|
+
.map((entry) => ({ ecosystem: decisions.ecosystem, name: entry.pkg.name, version: entry.pkg.version, findings: entry.pkg.findings })),
|
|
261
|
+
acceptedBy: resolveAcceptedBy(decisions.root, decisions.env ?? process.env),
|
|
262
|
+
surface: "install preflight",
|
|
263
|
+
env: decisions.env ?? process.env
|
|
264
|
+
});
|
|
265
|
+
}
|
|
124
266
|
return hasBlock ? { proceed: true, forceOverride: { force: true } } : PROCEED;
|
|
125
267
|
}
|
|
268
|
+
export function renderCoveredWarns(covered, decisions, out) {
|
|
269
|
+
for (const pkg of covered) {
|
|
270
|
+
const match = decisions ? matchDecision(pkg, decisions.ecosystem, decisions.file.decisions) : { acknowledged: false };
|
|
271
|
+
const who = match.acknowledged ? match.entry.acceptedBy : "dg.json";
|
|
272
|
+
const when = match.acknowledged && match.entry.acceptedAt ? ` on ${match.entry.acceptedAt.slice(0, 10)}` : "";
|
|
273
|
+
out.write(` ⚠ ${packageKey(pkg.name, pkg.version)} warn previously accepted by ${who}${when} — see 'dg decisions'\n`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
126
276
|
export async function promptPreflightYesNo(question, io, defaultYes) {
|
|
127
277
|
if (!io.isTTY) {
|
|
128
278
|
return false;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createLaunchPlan, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
1
|
+
import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
2
2
|
import { runInstallPreflight } from "./install-preflight.js";
|
|
3
3
|
import { isSupportedPackageManager } from "./classify.js";
|
|
4
4
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
5
|
+
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
5
6
|
const FALL_THROUGH = { handled: false };
|
|
6
7
|
export async function maybeRunLiveInstall(args, options = {}) {
|
|
7
8
|
const env = options.env ?? process.env;
|
|
@@ -17,24 +18,43 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
17
18
|
if (plan.classification.kind !== "protected" || !plan.realBinary.path) {
|
|
18
19
|
return FALL_THROUGH;
|
|
19
20
|
}
|
|
21
|
+
const appLoading = import("../install-ui/live-install-app.js");
|
|
22
|
+
appLoading.catch(() => undefined);
|
|
23
|
+
let prepared = forceOverride ? undefined : prepareProxyWorker(plan.classification, env);
|
|
20
24
|
let effectiveOverride = forceOverride;
|
|
25
|
+
let preverified;
|
|
21
26
|
if (!forceOverride) {
|
|
22
|
-
const
|
|
27
|
+
const spinner = startPrepSpinner("DG preparing…");
|
|
28
|
+
let preflight;
|
|
29
|
+
try {
|
|
30
|
+
preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env, process.cwd(), spinner.stop);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
spinner.stop();
|
|
34
|
+
}
|
|
23
35
|
if (!preflight.proceed) {
|
|
36
|
+
prepared?.discard();
|
|
24
37
|
return { handled: true, result: { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: " Install cancelled.\n" } };
|
|
25
38
|
}
|
|
26
39
|
effectiveOverride = preflight.forceOverride;
|
|
40
|
+
preverified = preflight.preverified;
|
|
41
|
+
if (effectiveOverride) {
|
|
42
|
+
prepared?.discard();
|
|
43
|
+
prepared = undefined;
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
46
|
const runOptions = {
|
|
29
47
|
env,
|
|
30
|
-
...(effectiveOverride ? { forceOverride: effectiveOverride } : {})
|
|
48
|
+
...(effectiveOverride ? { forceOverride: effectiveOverride } : {}),
|
|
49
|
+
...(preverified && preverified.length > 0 && !effectiveOverride ? { preverified } : {})
|
|
31
50
|
};
|
|
32
|
-
const { renderLiveInstall } = await import("../install-ui/live-install-app.js");
|
|
33
51
|
try {
|
|
34
|
-
const
|
|
52
|
+
const { renderLiveInstall } = await appLoading;
|
|
53
|
+
const result = await renderLiveInstall((onView) => runWithProductionProxyLive(plan, childArgs, runOptions, onView, prepared));
|
|
35
54
|
return { handled: true, result };
|
|
36
55
|
}
|
|
37
56
|
catch (error) {
|
|
57
|
+
prepared?.discard();
|
|
38
58
|
return {
|
|
39
59
|
handled: true,
|
|
40
60
|
result: {
|
|
@@ -1,23 +1,26 @@
|
|
|
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
|
-
|
|
3
|
+
// Matches both KEY=value and colon-form (KEY: value, YAML) secret assignments;
|
|
4
|
+
// the separator and any opening quote are preserved in the replacement.
|
|
5
|
+
const tokenAssignmentPattern = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret))(\s*[:=]\s*)("?)([^\s"]+)/gi;
|
|
4
6
|
const npmrcAuthPattern = /(_authToken|_auth|_password)\s*=\s*("[^"]*"|[^\s;,]+)/gi;
|
|
5
7
|
const jsonSecretPattern = /("[A-Za-z0-9_.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
|
|
6
8
|
const bearerPattern = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g;
|
|
7
|
-
|
|
9
|
+
// Bare tokens (not in URL userinfo or KEY=value form), by their published shape.
|
|
10
|
+
const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|hf_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|sk_(?:live|test)_[A-Za-z0-9]{20,}|rk_live_[A-Za-z0-9]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|xox[abdeprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
8
11
|
export function redactSecrets(text) {
|
|
9
12
|
return text
|
|
10
13
|
.replace(credentialUrlPattern, "$1<redacted>@")
|
|
11
14
|
.replace(authHeaderPattern, (_match, header) => `${header}: <redacted>`)
|
|
12
15
|
.replace(npmrcAuthPattern, "$1=<redacted>")
|
|
13
16
|
.replace(jsonSecretPattern, '$1"<redacted>"')
|
|
14
|
-
.replace(tokenAssignmentPattern, "$1
|
|
17
|
+
.replace(tokenAssignmentPattern, "$1$2$3<redacted>")
|
|
15
18
|
.replace(bearerPattern, "Bearer <redacted>")
|
|
16
19
|
.replace(knownTokenShapePattern, "<redacted>");
|
|
17
20
|
}
|
|
18
21
|
const STREAM_FLUSH_QUIET_MS = 80;
|
|
19
22
|
const SECRET_TAIL_SCAN_CHARS = 80;
|
|
20
|
-
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|pypi-[\w-]*|glpat-[\w-]*|xox[
|
|
23
|
+
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|github_pat_[A-Za-z0-9_]*|pypi-[\w-]*|glpat-[\w-]*|hf_[A-Za-z0-9]*|AKIA[0-9A-Z]*|ASIA[0-9A-Z]*|AIza[\w-]*|sk_(?:live|test)_[A-Za-z0-9]*|rk_live_[A-Za-z0-9]*|sk-(?:proj-)?[\w-]*|xox[abdeprs]-[\w-]*|[\w-]*(?:_authToken|_auth|_password|secret[_-]key|access[_-]key|api[_-]key|token|password|secret)\s*[:=]\s*"?\S*|"[\w.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*"?[^"\n]*)$/i;
|
|
21
24
|
export function createStreamRedactor(emit) {
|
|
22
25
|
let pending = "";
|
|
23
26
|
let timer;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { analyzePackages } from "../api/analyze.js";
|
|
2
2
|
import { isCiEnv } from "../presentation/mode.js";
|
|
3
|
+
import { matchDecision } from "../decisions/apply.js";
|
|
4
|
+
import { offerRememberOnIo } from "../decisions/remember-prompt.js";
|
|
3
5
|
import { renderInstallDecision } from "../install-ui/block-render.js";
|
|
4
6
|
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
7
|
+
import { normalizePypiName } from "../policy/pypi-name.js";
|
|
8
|
+
import { resolveAcceptedBy } from "../project/dgfile.js";
|
|
5
9
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
6
10
|
import { classifyPackageManagerInvocation, isSupportedPackageManager } from "./classify.js";
|
|
7
|
-
import { actionRank, promptPreflightYesNo, recordPreflightApprovals } from "./install-preflight.js";
|
|
11
|
+
import { actionRank, promptPreflightYesNo, recordPreflightApprovals, renderCoveredWarns, renderProvenanceDowngrades, resolvePreflightDecisions } from "./install-preflight.js";
|
|
8
12
|
import { redactSecrets } from "./output-redaction.js";
|
|
13
|
+
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
9
14
|
const ECOSYSTEM_BY_MANAGER = {
|
|
10
15
|
npm: "npm",
|
|
11
16
|
pnpm: "npm",
|
|
@@ -38,25 +43,42 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
38
43
|
if (specs.length === 0) {
|
|
39
44
|
return FALL_THROUGH;
|
|
40
45
|
}
|
|
46
|
+
const decisions = options.decisionsCwd ? resolvePreflightDecisions(ecosystem, options.decisionsCwd, env) : null;
|
|
41
47
|
const flagged = [];
|
|
48
|
+
const covered = [];
|
|
49
|
+
const spinner = startPrepSpinner("DG preparing…");
|
|
42
50
|
try {
|
|
43
51
|
const response = await (options.analyze ?? analyzePackages)(specs.map((spec) => ({ name: spec.name, version: spec.version })), { ecosystem, env });
|
|
52
|
+
spinner.stop();
|
|
53
|
+
renderProvenanceDowngrades(response.packages, io.output);
|
|
44
54
|
for (const spec of specs) {
|
|
45
|
-
const pkg = response.packages.find((entry) => entry.name
|
|
55
|
+
const pkg = response.packages.find((entry) => sameSpecName(entry.name, spec.name, ecosystem) && entry.version === spec.version);
|
|
46
56
|
const action = pkg?.action ?? "pass";
|
|
47
57
|
if (action === "pass") {
|
|
48
58
|
continue;
|
|
49
59
|
}
|
|
60
|
+
if (decisions && pkg && action === "warn" && matchDecision(pkg, decisions.ecosystem, decisions.file.decisions).acknowledged) {
|
|
61
|
+
covered.push(pkg);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
50
64
|
flagged.push({
|
|
51
65
|
action,
|
|
52
66
|
spec,
|
|
53
|
-
reason: pkg?.reasons[0] ?? pkg?.findings[0]?.title ?? "flagged by the scanner"
|
|
67
|
+
reason: pkg?.reasons[0] ?? pkg?.findings[0]?.title ?? "flagged by the scanner",
|
|
68
|
+
findings: pkg?.findings ?? []
|
|
54
69
|
});
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
catch {
|
|
58
73
|
return FALL_THROUGH;
|
|
59
74
|
}
|
|
75
|
+
finally {
|
|
76
|
+
spinner.stop();
|
|
77
|
+
}
|
|
78
|
+
if (covered.length > 0) {
|
|
79
|
+
renderCoveredWarns(covered, decisions, io.output);
|
|
80
|
+
recordPreflightApprovals(covered.map((pkg) => ({ name: pkg.name, version: pkg.version, action: "warn" })));
|
|
81
|
+
}
|
|
60
82
|
const worst = flagged.reduce((top, entry) => (!top || actionRank(entry.action) > actionRank(top.action) ? entry : top), undefined);
|
|
61
83
|
if (!worst) {
|
|
62
84
|
return FALL_THROUGH;
|
|
@@ -97,6 +119,18 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
97
119
|
const proceed = await promptPreflightYesNo(`⚠ DG flagged ${label} (warn) — ${worst.reason}. Proceed?`, io, false);
|
|
98
120
|
if (proceed) {
|
|
99
121
|
recordPreflightApprovals(flagged.map(asFlaggedPackage));
|
|
122
|
+
if (decisions) {
|
|
123
|
+
await offerRememberOnIo({
|
|
124
|
+
io,
|
|
125
|
+
file: decisions.file,
|
|
126
|
+
packages: flagged
|
|
127
|
+
.filter((entry) => entry.action === "warn")
|
|
128
|
+
.map((entry) => ({ ecosystem, name: entry.spec.name, version: entry.spec.version, findings: entry.findings })),
|
|
129
|
+
acceptedBy: resolveAcceptedBy(decisions.root, env),
|
|
130
|
+
surface: "install preflight",
|
|
131
|
+
env
|
|
132
|
+
});
|
|
133
|
+
}
|
|
100
134
|
return FALL_THROUGH;
|
|
101
135
|
}
|
|
102
136
|
return {
|
|
@@ -104,6 +138,12 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
104
138
|
result: { exitCode: 1, stdout: "", stderr: "Declined. Nothing was installed.\n" }
|
|
105
139
|
};
|
|
106
140
|
}
|
|
141
|
+
function sameSpecName(entryName, specName, ecosystem) {
|
|
142
|
+
if (ecosystem === "pypi") {
|
|
143
|
+
return normalizePypiName(entryName) === normalizePypiName(specName);
|
|
144
|
+
}
|
|
145
|
+
return entryName === specName;
|
|
146
|
+
}
|
|
107
147
|
function asFlaggedPackage(entry) {
|
|
108
148
|
return {
|
|
109
149
|
name: entry.spec.name,
|