@westbayberry/dg 2.3.3 → 2.3.4
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 +94 -217
- package/dist/agents/copilot-cli.js +7 -2
- package/dist/agents/persistence.js +83 -8
- package/dist/agents/registry.js +5 -2
- package/dist/agents/routing.js +31 -14
- package/dist/api/analyze.js +138 -41
- package/dist/audit/deep.js +2 -1
- package/dist/audit/detectors.js +23 -2
- package/dist/audit/rules.js +4 -1
- package/dist/audit-ui/export.js +5 -4
- package/dist/auth/device-login.js +33 -11
- package/dist/auth/login-app.js +17 -12
- package/dist/auth/store.js +65 -9
- package/dist/bin/dg.js +1 -1
- package/dist/commands/audit.js +12 -20
- package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
- package/dist/commands/cooldown.js +2 -1
- package/dist/commands/decisions.js +8 -7
- package/dist/commands/help.js +11 -3
- package/dist/commands/licenses.js +17 -8
- package/dist/commands/login.js +58 -21
- package/dist/commands/router.js +12 -2
- package/dist/commands/sbom.js +20 -5
- package/dist/commands/scan.js +5 -4
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +12 -12
- package/dist/commands/status.js +4 -4
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/update.js +20 -0
- package/dist/commands/verify.js +4 -4
- package/dist/config/settings.js +81 -12
- package/dist/export-ui/ExportDialog.js +1 -1
- package/dist/gate/cooldown-request.js +22 -0
- package/dist/gate/egress-guard.js +185 -0
- package/dist/gate/host-match.js +48 -0
- package/dist/gate/verdict-core.js +101 -0
- package/dist/gate/verdict-fetch.js +214 -0
- package/dist/launcher/agent-check.js +1096 -42
- package/dist/launcher/agent-hook-exec.js +8 -1
- package/dist/launcher/agent-hook-io.js +5 -3
- package/dist/launcher/classify.js +110 -26
- package/dist/launcher/env.js +84 -18
- package/dist/launcher/install-preflight.js +26 -3
- package/dist/launcher/live-install.js +4 -2
- package/dist/launcher/manifest-screen.js +88 -7
- package/dist/launcher/output-redaction.js +153 -15
- package/dist/launcher/preflight-prompt.js +1 -1
- package/dist/launcher/resolve-real-binary.js +8 -3
- package/dist/launcher/run.js +237 -40
- package/dist/origin/artifact-cache.js +55 -0
- package/dist/origin/cargo-config.js +12 -0
- package/dist/origin/gate-mode.js +7 -0
- package/dist/origin/platform-paths.js +14 -0
- package/dist/origin/precedence.js +345 -0
- package/dist/origin/server.js +611 -0
- package/dist/origin/spike-worker.js +35 -0
- package/dist/origin/worker.js +116 -0
- package/dist/presentation/package-page.js +21 -1
- package/dist/presentation/safe-version.js +11 -0
- package/dist/presentation/sarif.js +109 -0
- package/dist/project/dgfile.js +54 -17
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/enforcement.js +27 -7
- package/dist/proxy/metadata-map.js +33 -7
- package/dist/proxy/preverified.js +0 -3
- package/dist/proxy/server.js +104 -446
- package/dist/proxy/upstream-proxy.js +23 -4
- package/dist/proxy/worker.js +3 -2
- package/dist/publish-set/collect.js +6 -0
- package/dist/runtime/fatal.js +2 -1
- package/dist/runtime/first-run.js +5 -1
- package/dist/runtime/node-version.js +51 -0
- package/dist/runtime/nudges.js +27 -1
- package/dist/sbom/cyclonedx.js +120 -33
- package/dist/sbom/graph.js +236 -0
- package/dist/sbom/spdx.js +53 -0
- package/dist/sbom-ui/SbomApp.js +5 -3
- package/dist/sbom-ui/inventory.js +8 -10
- package/dist/scan/analyze-worker.js +3 -1
- package/dist/scan/collect.js +38 -8
- package/dist/scan/command.js +77 -27
- package/dist/scan/discovery.js +18 -7
- package/dist/scan/installed.js +202 -0
- package/dist/scan/manifest-coverage.js +128 -0
- package/dist/scan/render.js +125 -40
- package/dist/scan/scanner-report.js +92 -22
- package/dist/scan/staged.js +68 -15
- package/dist/scan/types.js +7 -1
- package/dist/scan-ui/LegacyApp.js +3 -3
- package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
- package/dist/scan-ui/hooks/useScan.js +37 -11
- package/dist/scan-ui/launch.js +2 -2
- package/dist/scan-ui/logo.js +6 -2
- package/dist/scan-ui/shims.js +6 -6
- package/dist/scripts/gate.js +53 -35
- package/dist/security/csv.js +9 -0
- package/dist/security/sanitize.js +11 -2
- package/dist/service/state.js +120 -37
- package/dist/service/trust-refresh.js +12 -1
- package/dist/service/trust-store.js +36 -5
- package/dist/service/worker.js +5 -7
- package/dist/setup/git-hook.js +2 -2
- package/dist/setup/install-method.js +36 -0
- package/dist/setup/plan.js +119 -57
- package/dist/setup/uninstall-standalone.js +7 -2
- package/dist/setup-ui/gate.js +1 -8
- package/dist/setup-ui/offer.js +14 -7
- package/dist/setup-ui/selector.js +1 -1
- package/dist/setup-ui/wizard.js +73 -78
- package/dist/standalone/uninstall.mjs +1114 -386
- package/dist/state/cleanup-registry.js +14 -3
- package/dist/state/cooldown-held.js +36 -13
- package/dist/state/locks.js +239 -34
- package/dist/state/sessions.js +2 -2
- package/dist/util/sh-escape.js +6 -0
- package/dist/util/tty-prompt.js +13 -4
- package/dist/verify/local.js +60 -10
- package/dist/verify/package-check.js +35 -49
- package/dist/verify/preflight.js +59 -33
- package/dist/verify/render.js +11 -12
- package/npm-shrinkwrap.json +714 -213
- package/package.json +3 -2
package/dist/commands/sbom.js
CHANGED
|
@@ -2,7 +2,9 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { writeReportAtomic } from "../util/report-writer.js";
|
|
4
4
|
import { discoverScanProjects, SBOM_LOCKFILE_ECOSYSTEMS } from "../scan/collect.js";
|
|
5
|
-
import { buildCycloneDxSbom, collectSbomComponents, readRootComponent } from "../sbom/cyclonedx.js";
|
|
5
|
+
import { bomRefOf, buildCycloneDxSbom, collectSbomComponents, readRootComponent } from "../sbom/cyclonedx.js";
|
|
6
|
+
import { buildDependencyGraph, SBOM_ROOT_REF } from "../sbom/graph.js";
|
|
7
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
6
8
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
7
9
|
import { createTheme } from "../presentation/theme.js";
|
|
8
10
|
import { buildSbomRows } from "../sbom-ui/inventory.js";
|
|
@@ -17,12 +19,12 @@ export const sbomCommand = {
|
|
|
17
19
|
flags: [
|
|
18
20
|
{ flag: "--output", value: "<path>", summary: "Write the document to a file without opening the interactive view (alias -o)." },
|
|
19
21
|
{ flag: "--json", summary: "Print the raw CycloneDX document to stdout instead of the interactive view." },
|
|
20
|
-
{ flag: "--reproducible", summary: "Byte-stable output: derive the serial number from the components and
|
|
22
|
+
{ flag: "--reproducible", summary: "Byte-stable output: derive the serial number from the components and pin the timestamp to SOURCE_DATE_EPOCH (or drop it when that is unset)." }
|
|
21
23
|
],
|
|
22
24
|
examples: ["dg sbom", "dg sbom -o sbom.cdx.json", "dg sbom --json | cyclonedx validate", "dg sbom --reproducible"],
|
|
23
25
|
details: [
|
|
24
|
-
"Reads the project's lockfiles and inventories
|
|
25
|
-
"In a terminal it opens an interactive view: the inventory appears immediately, then dg's scanner runs over the npm and PyPI components and fills in a BLOCK/WARN/PASS verdict per package — malware, provenance downgrades, and cooldown — while cargo stays inventory only. Browse with the arrow keys, search with /, filter with f, and press
|
|
26
|
+
"Reads the project's lockfiles and inventories each resolved dependency as a CycloneDX 1.5 component with its purl, plus the declared license and integrity hash when the lockfile records them. npm and Cargo lockfiles also yield the dependency graph (which package pulls in which); unpinned dependencies are reported on stderr and left out.",
|
|
27
|
+
"In a terminal it opens an interactive view: the inventory appears immediately, then dg's scanner runs over the npm and PyPI components and fills in a BLOCK/WARN/PASS verdict per package — malware, provenance downgrades, and cooldown — while cargo stays inventory only. Browse with the arrow keys, search with /, filter with f, and press e to export the document. Signing in unlocks verdicts; without it the view shows the inventory alone.",
|
|
26
28
|
"Piped, with -o, or with --json it stays offline and prints the raw CycloneDX document so you can attach it to a release or feed any CycloneDX-aware tool — no scan, no account needed. Use --reproducible (or set SOURCE_DATE_EPOCH) for identical bytes across runs so a committed SBOM diffs cleanly."
|
|
27
29
|
],
|
|
28
30
|
handler: (context) => runSbomCommand(context)
|
|
@@ -37,11 +39,24 @@ export function runSbomCommand(context, cwd = process.cwd(), now = new Date(), e
|
|
|
37
39
|
const { components, dropped } = collectSbomComponents(projects);
|
|
38
40
|
const rootComponent = readRootComponent(root);
|
|
39
41
|
const reproducible = parsed.reproducible || env.SOURCE_DATE_EPOCH !== undefined;
|
|
42
|
+
const componentRefs = new Set(components.map((component) => bomRefOf(component)));
|
|
43
|
+
const dependencies = buildDependencyGraph(projects, componentRefs);
|
|
44
|
+
const graphable = projects.length > 0 &&
|
|
45
|
+
projects.every((project) => project.depFile === "package-lock.json" ||
|
|
46
|
+
project.depFile === "npm-shrinkwrap.json" ||
|
|
47
|
+
project.depFile === "Cargo.lock");
|
|
48
|
+
const completeness = dropped.length === 0 && graphable && dependencies.length > 0 ? "complete" : "incomplete";
|
|
49
|
+
const rootSubject = rootComponent.component ??
|
|
50
|
+
(dependencies.some((dependency) => dependency.ref === SBOM_ROOT_REF)
|
|
51
|
+
? { name: sanitizeLine(basename(root)) || "root" }
|
|
52
|
+
: undefined);
|
|
40
53
|
const bom = buildCycloneDxSbom(components, {
|
|
41
54
|
...timestampOption(reproducible, now, env),
|
|
42
55
|
serialNumber: "urn:uuid:00000000-0000-0000-0000-000000000000",
|
|
43
56
|
toolVersion: dgVersion(),
|
|
44
|
-
|
|
57
|
+
dependencies,
|
|
58
|
+
completeness,
|
|
59
|
+
...(rootSubject ? { rootComponent: rootSubject } : {})
|
|
45
60
|
});
|
|
46
61
|
const finalBom = {
|
|
47
62
|
...bom,
|
package/dist/commands/scan.js
CHANGED
|
@@ -2,18 +2,19 @@ import { runScanCommand } from "../scan/command.js";
|
|
|
2
2
|
export const scanCommand = {
|
|
3
3
|
name: "scan",
|
|
4
4
|
summary: "Scan a project and show Dependency Guardian findings.",
|
|
5
|
-
usage: "dg scan [path] [--json|--sarif] [--output <path>]",
|
|
5
|
+
usage: "dg scan [path] [--json|--sarif] [--output <path>] [--fail-on <block|warn|never>]",
|
|
6
6
|
args: [{ name: "[path]", summary: "Project directory or a package.json to scan (default: current directory)." }],
|
|
7
7
|
flags: [
|
|
8
8
|
{ flag: "--json", summary: "Machine-readable JSON report." },
|
|
9
9
|
{ flag: "--sarif", summary: "SARIF report for code-scanning tools." },
|
|
10
10
|
{ flag: "--output", value: "<path>", summary: "Write the report to a file instead of stdout (alias -o)." },
|
|
11
|
-
{ flag: "--
|
|
11
|
+
{ flag: "--fail-on", value: "<block|warn|never>", summary: "Exit non-zero at or above this level (default: block; never suppresses block/warn exit codes, but analysis-incomplete still exits 4)." },
|
|
12
|
+
{ flag: "--staged", summary: "Scan only the git-staged lockfile changes (what dg commit-guard runs)." },
|
|
12
13
|
{ flag: "--no-decisions", summary: "Ignore acceptances remembered in dg.json (see dg decisions)." }
|
|
13
14
|
],
|
|
14
|
-
examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --
|
|
15
|
+
examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --sarif -o scan.sarif --fail-on never"],
|
|
15
16
|
details: [
|
|
16
|
-
"Reads lockfiles, scores each dependency through the scanner, and never changes project files. In a terminal it opens the full-screen results browser; piped or with --json/--sarif it prints machine output. Exit codes: 0 clean, 1 warn, 2 block, 4 analysis incomplete, 10 nothing to scan, 64 usage error."
|
|
17
|
+
"Reads lockfiles, scores each dependency through the scanner, and never changes project files. Requires sign-in — run dg login. In a terminal it opens the full-screen results browser; piped or with --json/--sarif it prints machine output. Exit codes: 0 clean (warn counts as clean under the default --fail-on block), 1 warn with --fail-on warn, 2 block, 4 analysis incomplete, 10 nothing to scan, 64 usage error. Use --fail-on never so a block or warn verdict exits 0; analysis-incomplete (4), nothing-to-scan (10), and a rejected key (64) still exit non-zero, so a broken scan fails loudly instead of uploading an empty report. Add if: always() to the SARIF-upload step so it runs even when dg exits non-zero."
|
|
17
18
|
],
|
|
18
19
|
handler: runScanCommand
|
|
19
20
|
};
|
package/dist/commands/service.js
CHANGED
|
@@ -497,7 +497,7 @@ function serviceDoctorChecks(state) {
|
|
|
497
497
|
{
|
|
498
498
|
name: "service-proxy",
|
|
499
499
|
status: state.running && state.proxy ? "pass" : state.lastError?.startsWith("stale service runtime state") ? "fail" : "warn",
|
|
500
|
-
message: state.running && state.proxy ? `
|
|
500
|
+
message: state.running && state.proxy ? `Service proxy listening at ${state.proxy.proxyUrl} for this login session (not restarted after reboot or logout)` : state.lastError ?? "Service proxy is not running"
|
|
501
501
|
},
|
|
502
502
|
{
|
|
503
503
|
name: "health-endpoint",
|
package/dist/commands/setup.js
CHANGED
|
@@ -15,19 +15,19 @@ import { markSecurityNotesShown, securityNotesShown } from "../setup-ui/gate.js"
|
|
|
15
15
|
export const setupCommand = {
|
|
16
16
|
name: "setup",
|
|
17
17
|
summary: "Protect this machine — shell installs, AI-agent installs, and a repo's commits.",
|
|
18
|
-
usage: "dg setup [--print] [--yes] [--agents] [--guard
|
|
18
|
+
usage: "dg setup [--print] [--yes] [--agents] [--commit-guard] [--shell <auto|zsh|bash|fish>] [--service]",
|
|
19
19
|
flags: [
|
|
20
20
|
{ flag: "--print", summary: "Preview the exact write plan and change nothing." },
|
|
21
|
-
{ flag: "--yes", summary: "Apply the shell firewall only, without prompts (add --agents / --guard
|
|
21
|
+
{ flag: "--yes", summary: "Apply the shell firewall only, without prompts (add --agents / --commit-guard to opt in the rest)." },
|
|
22
22
|
{ flag: "--agents", summary: "With --yes: also route every detected AI agent's installs through dg." },
|
|
23
|
-
{ flag: "--guard
|
|
23
|
+
{ flag: "--commit-guard", summary: "With --yes: also scan this repo's commits (per-repo)." },
|
|
24
24
|
{ flag: "--shell", value: "<auto|zsh|bash|fish>", summary: "Target shell rc to write (default: auto-detect)." },
|
|
25
|
-
{ flag: "--service", summary: "Set up service mode (Pro/Team;
|
|
25
|
+
{ flag: "--service", summary: "Set up service mode (Pro/Team; long-running local proxy for this login session + managed CA)." }
|
|
26
26
|
],
|
|
27
27
|
examples: ["dg setup", "dg setup --print", "dg setup --yes --agents", "dg setup --shell zsh --yes"],
|
|
28
28
|
details: [
|
|
29
29
|
"In a terminal it shows one consent screen listing everything it will protect — your shell's installs plus every detected AI agent, with the exact files it writes — and applies them on a single yes. Commits in the current repo are a separate per-repo question.",
|
|
30
|
-
"Non-interactively, --yes applies just the shell firewall; add --agents and/or --guard
|
|
30
|
+
"Non-interactively, --yes applies just the shell firewall; add --agents and/or --commit-guard to opt those in. --print previews. Writes only dg-owned reversible files; undo it all with dg uninstall."
|
|
31
31
|
],
|
|
32
32
|
handler: (context) => setupHandler(context.args)
|
|
33
33
|
};
|
|
@@ -234,7 +234,7 @@ async function applyRequestedSurfaces(parsed) {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
if (parsed.
|
|
237
|
+
if (parsed.commitGuard) {
|
|
238
238
|
const repo = commitGuardOffer();
|
|
239
239
|
if (repo) {
|
|
240
240
|
try {
|
|
@@ -254,7 +254,7 @@ function printableSurfaceOffers() {
|
|
|
254
254
|
lines.push(`would offer: route ${offer.label} installs through dg (--agents)`);
|
|
255
255
|
}
|
|
256
256
|
if (commitGuardOffer()) {
|
|
257
|
-
lines.push("would offer: scan this repo's commits (--guard
|
|
257
|
+
lines.push("would offer: scan this repo's commits (--commit-guard)");
|
|
258
258
|
}
|
|
259
259
|
return lines.length > 0 ? `\n${lines.join("\n")}\n` : "";
|
|
260
260
|
}
|
|
@@ -335,7 +335,7 @@ function parseSetupArgs(args) {
|
|
|
335
335
|
let shell = "auto";
|
|
336
336
|
let service = false;
|
|
337
337
|
let agents = false;
|
|
338
|
-
let
|
|
338
|
+
let commitGuard = false;
|
|
339
339
|
for (let index = 0; index < args.length; index += 1) {
|
|
340
340
|
const arg = args[index];
|
|
341
341
|
if (arg === "--print") {
|
|
@@ -350,8 +350,8 @@ function parseSetupArgs(args) {
|
|
|
350
350
|
else if (arg === "--agents") {
|
|
351
351
|
agents = true;
|
|
352
352
|
}
|
|
353
|
-
else if (arg === "--guard
|
|
354
|
-
|
|
353
|
+
else if (arg === "--commit-guard") {
|
|
354
|
+
commitGuard = true;
|
|
355
355
|
}
|
|
356
356
|
else if (arg === "--python-hook" || arg?.startsWith("--python-hook=")) {
|
|
357
357
|
return {
|
|
@@ -362,7 +362,7 @@ function parseSetupArgs(args) {
|
|
|
362
362
|
else if (arg === "--git-hooks") {
|
|
363
363
|
return {
|
|
364
364
|
exitCode: EXIT_USAGE,
|
|
365
|
-
error: "dg setup --git-hooks moved — run 'dg guard
|
|
365
|
+
error: "dg setup --git-hooks moved — run 'dg commit-guard' inside the repo you want to protect.\n"
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
368
|
else if (arg === "--shell") {
|
|
@@ -419,7 +419,7 @@ function parseSetupArgs(args) {
|
|
|
419
419
|
shell,
|
|
420
420
|
service,
|
|
421
421
|
agents,
|
|
422
|
-
|
|
422
|
+
commitGuard
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
425
|
function parseShell(value) {
|
package/dist/commands/status.js
CHANGED
|
@@ -64,7 +64,7 @@ async function buildStatusReport(io) {
|
|
|
64
64
|
async function fetchStatusUsage(connected, io) {
|
|
65
65
|
const token = connected ? resolveStatusToken() : undefined;
|
|
66
66
|
if (!token) {
|
|
67
|
-
return { state: "
|
|
67
|
+
return { state: "signed-out" };
|
|
68
68
|
}
|
|
69
69
|
const account = await fetchAccountStatus(token, process.env, io.fetchImpl ?? fetch, io.usageTimeoutMs ?? STATUS_USAGE_TIMEOUT_MS);
|
|
70
70
|
if (!account || account.scansUsed === null) {
|
|
@@ -131,7 +131,7 @@ function renderStatus(report, theme) {
|
|
|
131
131
|
return `${lines.join("\n")}\n`;
|
|
132
132
|
}
|
|
133
133
|
function usageLine(report, theme) {
|
|
134
|
-
if (report.usage.state === "
|
|
134
|
+
if (report.usage.state === "signed-out") {
|
|
135
135
|
return theme.paint("muted", "sign in to see usage");
|
|
136
136
|
}
|
|
137
137
|
if (report.usage.state === "unavailable") {
|
|
@@ -147,7 +147,7 @@ function commitGuardLine(state, theme) {
|
|
|
147
147
|
return `${theme.paint("pass", "✓")} this repo's commits are scanned`;
|
|
148
148
|
}
|
|
149
149
|
if (state === "dead") {
|
|
150
|
-
return `${theme.paint("warn", "⚠")} installed but git isn't using it — run ${theme.paint("accent", "dg guard
|
|
150
|
+
return `${theme.paint("warn", "⚠")} installed but git isn't using it — run ${theme.paint("accent", "dg commit-guard --check")}`;
|
|
151
151
|
}
|
|
152
|
-
return `${theme.paint("warn", "⚠")} off — run ${theme.paint("accent", "dg guard
|
|
152
|
+
return `${theme.paint("warn", "⚠")} off — run ${theme.paint("accent", "dg commit-guard")}`;
|
|
153
153
|
}
|
|
@@ -8,7 +8,7 @@ export const uninstallCommand = {
|
|
|
8
8
|
usage: "dg uninstall [--yes] [--service] [--all] [--keep-config]",
|
|
9
9
|
flags: [
|
|
10
10
|
{ flag: "--yes", summary: "Remove without the confirmation prompt." },
|
|
11
|
-
{ flag: "--keep-config", summary: "Keep
|
|
11
|
+
{ flag: "--keep-config", summary: "Keep the saved login too (the default removes it; config.json is kept either way unless --all)." },
|
|
12
12
|
{ flag: "--all", summary: "Also remove the config directory — a full wipe." },
|
|
13
13
|
{ flag: "--service", summary: "Reverse only service-mode writes." }
|
|
14
14
|
],
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { toolInvocation } from "../util/external-tool.js";
|
|
5
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
3
6
|
import { EXIT_USAGE } from "./types.js";
|
|
4
7
|
import { dgVersion } from "./version.js";
|
|
8
|
+
// `dg update` only prints the npm command; the user runs the upgrade manually,
|
|
9
|
+
// during which dg is briefly unavailable. Drop a freshness-stamped sentinel the
|
|
10
|
+
// shim's fail-open self-cleanup honors (15-min window) so a slow `npm install -g`
|
|
11
|
+
// is not mistaken for an uninstall and self-cleaned mid-upgrade.
|
|
12
|
+
function markUpgradeWindow() {
|
|
13
|
+
try {
|
|
14
|
+
const dgHome = join(resolveDgPaths().homeDir, ".dg");
|
|
15
|
+
mkdirSync(dgHome, { recursive: true, mode: 0o700 });
|
|
16
|
+
writeFileSync(join(dgHome, ".upgrading"), "", { encoding: "utf8", mode: 0o600 });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// best-effort: a missing sentinel only restores the prior 60s-debounce behavior
|
|
20
|
+
}
|
|
21
|
+
}
|
|
5
22
|
const PACKAGE_NAME = "@westbayberry/dg";
|
|
6
23
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/u;
|
|
7
24
|
export const updateCommand = {
|
|
@@ -24,6 +41,9 @@ function runUpdateCommand(args, commandName) {
|
|
|
24
41
|
}
|
|
25
42
|
const latestVersion = readLatestVersion();
|
|
26
43
|
const report = buildUpdateReport(latestVersion);
|
|
44
|
+
if (report.updateCommand) {
|
|
45
|
+
markUpgradeWindow();
|
|
46
|
+
}
|
|
27
47
|
if (parsed.format === "json") {
|
|
28
48
|
return {
|
|
29
49
|
exitCode: report.status === "unknown" ? 1 : 0,
|
package/dist/commands/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
1
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_TOOL_ERROR, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
2
2
|
import { writeFileSync } from "node:fs";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
@@ -22,8 +22,8 @@ export const verifyCommand = {
|
|
|
22
22
|
],
|
|
23
23
|
examples: ["dg verify npm:react", "dg verify pypi:requests@2.31.0", "dg verify ./pkg.tgz --verbose", "dg verify package-lock.json --json"],
|
|
24
24
|
details: [
|
|
25
|
-
"dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it.
|
|
26
|
-
"Local paths, workspaces, tgz/zip/wheel artifacts, and lockfiles are verified offline as advisory preflight (
|
|
25
|
+
"dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. Registry checks require sign-in — run dg login.",
|
|
26
|
+
"Local paths, workspaces, tgz/zip/wheel artifacts, and lockfiles are verified offline as advisory preflight (no account needed); proxy enforcement remains authoritative for network artifact fetches."
|
|
27
27
|
],
|
|
28
28
|
handler: (context) => {
|
|
29
29
|
const parsed = parseVerifyArgs(context.args);
|
|
@@ -52,7 +52,7 @@ export const verifyCommand = {
|
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
return {
|
|
55
|
-
exitCode:
|
|
55
|
+
exitCode: EXIT_TOOL_ERROR,
|
|
56
56
|
stdout: "",
|
|
57
57
|
stderr: `dg verify could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
58
58
|
};
|
package/dist/config/settings.js
CHANGED
|
@@ -11,8 +11,10 @@ export const CONFIG_KEYS = Object.freeze([
|
|
|
11
11
|
"policy.scriptHardening",
|
|
12
12
|
"policy.shimFailClosed",
|
|
13
13
|
"policy.strictEgress",
|
|
14
|
+
"policy.allowNonVendorApiHost",
|
|
15
|
+
"policy.provenanceDowngrade",
|
|
14
16
|
"scriptGate.mode",
|
|
15
|
-
"scriptGate.
|
|
17
|
+
"scriptGate.persistObservations",
|
|
16
18
|
"gitHook.onWarn",
|
|
17
19
|
"gitHook.onIncomplete",
|
|
18
20
|
"cooldown.age",
|
|
@@ -37,11 +39,13 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
37
39
|
allowForceOverride: true,
|
|
38
40
|
scriptHardening: false,
|
|
39
41
|
shimFailClosed: false,
|
|
40
|
-
strictEgress: false
|
|
42
|
+
strictEgress: false,
|
|
43
|
+
allowNonVendorApiHost: false,
|
|
44
|
+
provenanceDowngrade: "off"
|
|
41
45
|
},
|
|
42
46
|
scriptGate: {
|
|
43
47
|
mode: "observe",
|
|
44
|
-
|
|
48
|
+
persistObservations: false
|
|
45
49
|
},
|
|
46
50
|
gitHook: {
|
|
47
51
|
onWarn: "prompt",
|
|
@@ -146,11 +150,17 @@ export function getConfigValue(config, key) {
|
|
|
146
150
|
if (key === "policy.strictEgress") {
|
|
147
151
|
return String(config.policy.strictEgress);
|
|
148
152
|
}
|
|
153
|
+
if (key === "policy.allowNonVendorApiHost") {
|
|
154
|
+
return String(config.policy.allowNonVendorApiHost);
|
|
155
|
+
}
|
|
156
|
+
if (key === "policy.provenanceDowngrade") {
|
|
157
|
+
return config.policy.provenanceDowngrade;
|
|
158
|
+
}
|
|
149
159
|
if (key === "scriptGate.mode") {
|
|
150
160
|
return config.scriptGate.mode;
|
|
151
161
|
}
|
|
152
|
-
if (key === "scriptGate.
|
|
153
|
-
return String(config.scriptGate.
|
|
162
|
+
if (key === "scriptGate.persistObservations") {
|
|
163
|
+
return String(config.scriptGate.persistObservations);
|
|
154
164
|
}
|
|
155
165
|
if (key === "gitHook.onWarn") {
|
|
156
166
|
return config.gitHook.onWarn;
|
|
@@ -183,7 +193,8 @@ export const ADVANCED_CONFIG_KEYS = new Set([
|
|
|
183
193
|
"policy.scriptHardening",
|
|
184
194
|
"policy.shimFailClosed",
|
|
185
195
|
"policy.strictEgress",
|
|
186
|
-
"
|
|
196
|
+
"policy.provenanceDowngrade",
|
|
197
|
+
"scriptGate.persistObservations",
|
|
187
198
|
"cooldown.npm.age",
|
|
188
199
|
"cooldown.pypi.age",
|
|
189
200
|
"cooldown.cargo.age"
|
|
@@ -199,7 +210,7 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
199
210
|
return {
|
|
200
211
|
...config,
|
|
201
212
|
api: {
|
|
202
|
-
baseUrl: parseUrl(rawValue)
|
|
213
|
+
baseUrl: assertVendorApiBaseUrl(parseUrl(rawValue), config.policy.allowNonVendorApiHost)
|
|
203
214
|
}
|
|
204
215
|
};
|
|
205
216
|
}
|
|
@@ -235,6 +246,25 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
235
246
|
if (key === "policy.strictEgress") {
|
|
236
247
|
return withPolicyBoolean(config, "strictEgress", rawValue);
|
|
237
248
|
}
|
|
249
|
+
if (key === "policy.allowNonVendorApiHost") {
|
|
250
|
+
const next = withPolicyBoolean(config, "allowNonVendorApiHost", rawValue);
|
|
251
|
+
if (!next.policy.allowNonVendorApiHost) {
|
|
252
|
+
const host = new URL(next.api.baseUrl).hostname;
|
|
253
|
+
if (!isVendorApiHost(host)) {
|
|
254
|
+
throw new ConfigError(`cannot disable policy.allowNonVendorApiHost while api.baseUrl points at the self-hosted host ${host}; reset api.baseUrl to a vendor host (or unset it) first`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
if (key === "policy.provenanceDowngrade") {
|
|
260
|
+
return {
|
|
261
|
+
...config,
|
|
262
|
+
policy: {
|
|
263
|
+
...config.policy,
|
|
264
|
+
provenanceDowngrade: parseProvenanceDowngradeMode(rawValue)
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
238
268
|
if (key === "scriptGate.mode") {
|
|
239
269
|
return {
|
|
240
270
|
...config,
|
|
@@ -244,12 +274,12 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
244
274
|
}
|
|
245
275
|
};
|
|
246
276
|
}
|
|
247
|
-
if (key === "scriptGate.
|
|
277
|
+
if (key === "scriptGate.persistObservations") {
|
|
248
278
|
return {
|
|
249
279
|
...config,
|
|
250
280
|
scriptGate: {
|
|
251
281
|
...config.scriptGate,
|
|
252
|
-
|
|
282
|
+
persistObservations: parseBoolean(rawValue, key)
|
|
253
283
|
}
|
|
254
284
|
};
|
|
255
285
|
}
|
|
@@ -317,7 +347,7 @@ function normalizeConfig(raw) {
|
|
|
317
347
|
const cooldown = fieldObject(raw, "cooldown");
|
|
318
348
|
const audit = fieldObject(raw, "audit");
|
|
319
349
|
const scriptHardening = fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening;
|
|
320
|
-
|
|
350
|
+
const normalized = {
|
|
321
351
|
version: 1,
|
|
322
352
|
api: {
|
|
323
353
|
baseUrl: parseUrl(fieldString(api, "api.baseUrl", "baseUrl") ?? DEFAULT_CONFIG.api.baseUrl)
|
|
@@ -331,11 +361,15 @@ function normalizeConfig(raw) {
|
|
|
331
361
|
allowForceOverride: fieldBoolean(policy, "policy.allowForceOverride", "allowForceOverride") ?? DEFAULT_CONFIG.policy.allowForceOverride,
|
|
332
362
|
scriptHardening,
|
|
333
363
|
shimFailClosed: fieldBoolean(policy, "policy.shimFailClosed", "shimFailClosed") ?? DEFAULT_CONFIG.policy.shimFailClosed,
|
|
334
|
-
strictEgress: fieldBoolean(policy, "policy.strictEgress", "strictEgress") ?? DEFAULT_CONFIG.policy.strictEgress
|
|
364
|
+
strictEgress: fieldBoolean(policy, "policy.strictEgress", "strictEgress") ?? DEFAULT_CONFIG.policy.strictEgress,
|
|
365
|
+
allowNonVendorApiHost: fieldBoolean(policy, "policy.allowNonVendorApiHost", "allowNonVendorApiHost") ?? DEFAULT_CONFIG.policy.allowNonVendorApiHost,
|
|
366
|
+
provenanceDowngrade: parseProvenanceDowngradeMode(fieldString(policy, "policy.provenanceDowngrade", "provenanceDowngrade") ?? DEFAULT_CONFIG.policy.provenanceDowngrade)
|
|
335
367
|
},
|
|
336
368
|
scriptGate: {
|
|
337
369
|
mode: parseScriptGateMode(fieldString(scriptGate, "scriptGate.mode", "mode") ?? (scriptHardening ? "enforce" : DEFAULT_CONFIG.scriptGate.mode)),
|
|
338
|
-
|
|
370
|
+
persistObservations: fieldBoolean(scriptGate, "scriptGate.persistObservations", "persistObservations") ??
|
|
371
|
+
fieldBoolean(scriptGate, "scriptGate.observe", "observe") ??
|
|
372
|
+
DEFAULT_CONFIG.scriptGate.persistObservations
|
|
339
373
|
},
|
|
340
374
|
gitHook: {
|
|
341
375
|
onWarn: parseOnWarn(fieldString(gitHook, "gitHook.onWarn", "onWarn") ?? DEFAULT_CONFIG.gitHook.onWarn),
|
|
@@ -353,6 +387,8 @@ function normalizeConfig(raw) {
|
|
|
353
387
|
upload: fieldBoolean(audit, "audit.upload", "upload") ?? DEFAULT_CONFIG.audit.upload
|
|
354
388
|
}
|
|
355
389
|
};
|
|
390
|
+
assertVendorApiBaseUrl(normalized.api.baseUrl, normalized.policy.allowNonVendorApiHost);
|
|
391
|
+
return normalized;
|
|
356
392
|
}
|
|
357
393
|
function fieldObject(root, field) {
|
|
358
394
|
const value = root[field];
|
|
@@ -417,6 +453,12 @@ function parsePolicyMode(value) {
|
|
|
417
453
|
}
|
|
418
454
|
throw new ConfigError("policy.mode must be one of: off, warn, block, strict");
|
|
419
455
|
}
|
|
456
|
+
function parseProvenanceDowngradeMode(value) {
|
|
457
|
+
if (value === "off" || value === "warn" || value === "block") {
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
throw new ConfigError("policy.provenanceDowngrade must be one of: off, warn, block");
|
|
461
|
+
}
|
|
420
462
|
function parseScriptGateMode(value) {
|
|
421
463
|
if (value === "observe" || value === "enforce" || value === "off") {
|
|
422
464
|
return value;
|
|
@@ -500,3 +542,30 @@ export function parseUrl(value) {
|
|
|
500
542
|
throw new ConfigError("api.baseUrl must be an absolute URL");
|
|
501
543
|
}
|
|
502
544
|
}
|
|
545
|
+
// The vendor API host the CLI ships to — derived from the default so there is one
|
|
546
|
+
// source of truth, never a duplicated literal. localhost stays usable for dev.
|
|
547
|
+
export const VENDOR_API_HOST = new URL(DEFAULT_CONFIG.api.baseUrl).hostname;
|
|
548
|
+
export function isVendorApiHost(host) {
|
|
549
|
+
return host === VENDOR_API_HOST || host === "localhost" || host === "127.0.0.1";
|
|
550
|
+
}
|
|
551
|
+
// The CLI attaches a Bearer token to and trusts scan verdicts from this base URL,
|
|
552
|
+
// so a non-vendor host (a tampered config / auth file, an env-set login base, an
|
|
553
|
+
// unpinned MITM) would leak the token and could return action:pass for malware.
|
|
554
|
+
// Refuse it unless the operator opted into a self-hosted API in their own global
|
|
555
|
+
// config (policy.allowNonVendorApiHost) — a project/repo cannot set that field.
|
|
556
|
+
export function assertVendorApiBaseUrl(baseUrl, allowNonVendor) {
|
|
557
|
+
if (allowNonVendor) {
|
|
558
|
+
return baseUrl;
|
|
559
|
+
}
|
|
560
|
+
let host;
|
|
561
|
+
try {
|
|
562
|
+
host = new URL(baseUrl).hostname;
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
throw new ConfigError("api.baseUrl must be an absolute URL");
|
|
566
|
+
}
|
|
567
|
+
if (!isVendorApiHost(host)) {
|
|
568
|
+
throw new ConfigError(`api.baseUrl host ${host} is not a Dependency Guardian vendor host; set policy.allowNonVendorApiHost=true to use a self-hosted API`);
|
|
569
|
+
}
|
|
570
|
+
return baseUrl;
|
|
571
|
+
}
|
|
@@ -6,7 +6,7 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
6
6
|
import { basename, join, resolve, sep } from "node:path";
|
|
7
7
|
import { exportDestinations, resolveExportPath, userHomeDir, writeReportAtomic } from "../util/report-writer.js";
|
|
8
8
|
export function loginRequiredToast() {
|
|
9
|
-
return `${chalk.yellow("Sign in to export:")} ${chalk.cyan.bold("dg login")}
|
|
9
|
+
return `${chalk.yellow("Sign in to export:")} ${chalk.cyan.bold("dg login")}`;
|
|
10
10
|
}
|
|
11
11
|
const PATH_DISPLAY_MAX = 48;
|
|
12
12
|
function cursorBeforeExtension(name) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cooldownRequestParam, isCooldownExemptByDgFile } from "../policy/cooldown.js";
|
|
2
|
+
import { DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
|
|
3
|
+
export function resolveCooldownRequest(options, identity) {
|
|
4
|
+
if (identity.ecosystem === "unknown" || identity.version === "unknown") {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (isCooldownExemptByDgFile(identity.name, identity.ecosystem, options.cooldownExemptions ?? [])) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
// A corrupt user config must not silently disable the cooldown gate — fall back
|
|
11
|
+
// to defaults (24h age + onUnknown=block) so the gate still applies, consistent
|
|
12
|
+
// with the rest of the policy stack. undefined stays reserved for the legitimate
|
|
13
|
+
// disabled/exempt cases cooldownRequestParam itself returns.
|
|
14
|
+
let config;
|
|
15
|
+
try {
|
|
16
|
+
config = loadUserConfig(options.env);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
config = DEFAULT_CONFIG;
|
|
20
|
+
}
|
|
21
|
+
return cooldownRequestParam(config, options.env, identity.ecosystem, identity.name);
|
|
22
|
+
}
|