@westbayberry/dg 2.3.2 → 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 +95 -208
- 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 +4 -2
package/dist/bin/dg.js
CHANGED
|
@@ -8,7 +8,7 @@ process.on("uncaughtException", (error) => {
|
|
|
8
8
|
}
|
|
9
9
|
exitOnFatal(error);
|
|
10
10
|
});
|
|
11
|
-
process.on("unhandledRejection", exitOnFatal);
|
|
11
|
+
process.on("unhandledRejection", (reason) => exitOnFatal(reason));
|
|
12
12
|
process.stdout.on("error", (error) => {
|
|
13
13
|
if (error.code === "EPIPE") {
|
|
14
14
|
process.exit(process.exitCode ?? 0);
|
package/dist/commands/audit.js
CHANGED
|
@@ -4,8 +4,9 @@ import { writeReportAtomic } from "../util/report-writer.js";
|
|
|
4
4
|
import { EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
5
5
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
6
6
|
import { createTheme } from "../presentation/theme.js";
|
|
7
|
-
import { actionForFindings, detectFindings, findingLocation } from "../audit/detectors.js";
|
|
7
|
+
import { actionForFindings, detectFindings, findingEvidenceSafe, findingLocation, findingTitleSafe } from "../audit/detectors.js";
|
|
8
8
|
import { buildAuditFile } from "../publish-set/collect.js";
|
|
9
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
9
10
|
import { npmPublishSet } from "../publish-set/npm.js";
|
|
10
11
|
import { pypiPublishSet } from "../publish-set/pypi.js";
|
|
11
12
|
import { deepDecision, runDeepUpload, consentGiven, deepSummary, teamPolicyBlocksUpload } from "../audit/deep.js";
|
|
@@ -33,11 +34,11 @@ export const auditCommand = {
|
|
|
33
34
|
"Audits exactly what you're about to publish — the resolved publish set of one package, never the whole repo.",
|
|
34
35
|
"Basic checks run 100% locally and never upload anything. If you're on a paid plan (and your org allows it), it also runs a deep behavioral scan of your package on the scanner; raw bytes are never retained. Exit codes: 0 clean (warn counts as clean under the default --fail-on block), 1 warn with --fail-on warn, 2 block, 3 deep audit required but unavailable (--require-deep), 4 analysis incomplete, 64 usage error."
|
|
35
36
|
],
|
|
36
|
-
handler: (context) =>
|
|
37
|
+
handler: async (context) => (await maybeAudit(["audit", ...context.args])).result
|
|
37
38
|
};
|
|
38
39
|
function privatePackageResult(scope) {
|
|
39
40
|
const theme = createTheme(resolvePresentation().color);
|
|
40
|
-
const name = typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root);
|
|
41
|
+
const name = sanitizeLine(typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root));
|
|
41
42
|
return {
|
|
42
43
|
exitCode: 0,
|
|
43
44
|
stdout: `${theme.paint("pass", "✓")} ${name} is private — npm refuses to publish it, so there is nothing to audit ${theme.paint("muted", "(remove \"private\": true to audit it for publish)")}\n`,
|
|
@@ -97,17 +98,6 @@ function finalize(gathered, deep) {
|
|
|
97
98
|
function gatherError(gathered) {
|
|
98
99
|
return gathered.usage ? usageError(gathered.error) : { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg audit: ${gathered.error}.\n` };
|
|
99
100
|
}
|
|
100
|
-
function runAuditCommand(args) {
|
|
101
|
-
const gathered = gather(args);
|
|
102
|
-
if ("error" in gathered) {
|
|
103
|
-
return gatherError(gathered);
|
|
104
|
-
}
|
|
105
|
-
if ("privatePackage" in gathered) {
|
|
106
|
-
return privatePackageResult(gathered.privatePackage);
|
|
107
|
-
}
|
|
108
|
-
const decision = deepDecision(gathered.scope, gathered.parsed.local);
|
|
109
|
-
return finalize(gathered, { ran: false, reason: decision.reason });
|
|
110
|
-
}
|
|
111
101
|
export async function maybeAudit(args) {
|
|
112
102
|
if (args[0] !== "audit") {
|
|
113
103
|
return { handled: false, result: { exitCode: 0, stdout: "", stderr: "" } };
|
|
@@ -234,15 +224,17 @@ function resolveScope(target) {
|
|
|
234
224
|
const packageJson = safeReadJson(resolve(root, "package.json"));
|
|
235
225
|
const name = packageJson && typeof packageJson.name === "string" ? packageJson.name : basename(root);
|
|
236
226
|
const version = packageJson && typeof packageJson.version === "string" ? packageJson.version : "(unknown)";
|
|
237
|
-
|
|
227
|
+
// name/version are free-text from a (possibly hostile) package.json rendered
|
|
228
|
+
// into the header and report; strip control bytes so they cannot inject ANSI.
|
|
229
|
+
return { root, ecosystem: "npm", packageJson, artifact: sanitizeLine(`${name}@${version}`) };
|
|
238
230
|
}
|
|
239
231
|
if (existsSync(resolve(root, "pyproject.toml")) || existsSync(resolve(root, "setup.py")) || existsSync(resolve(root, "setup.cfg"))) {
|
|
240
|
-
return { root, ecosystem: "pypi", packageJson: null, artifact: basename(root) };
|
|
232
|
+
return { root, ecosystem: "pypi", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
241
233
|
}
|
|
242
234
|
if (existsSync(resolve(root, "Cargo.toml"))) {
|
|
243
|
-
return { root, ecosystem: "cargo", packageJson: null, artifact: basename(root) };
|
|
235
|
+
return { root, ecosystem: "cargo", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
244
236
|
}
|
|
245
|
-
return { root, ecosystem: "unknown", packageJson: null, artifact: basename(root) };
|
|
237
|
+
return { root, ecosystem: "unknown", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
246
238
|
}
|
|
247
239
|
function findPackageRoot(start) {
|
|
248
240
|
let current = start;
|
|
@@ -292,9 +284,9 @@ function renderReport(report, theme) {
|
|
|
292
284
|
? theme.paint("warn", WARN_GLYPH)
|
|
293
285
|
: theme.paint("muted", "·");
|
|
294
286
|
lines.push(` ${tag} ${accent(findingLocation(finding))}`);
|
|
295
|
-
lines.push(` ${finding
|
|
287
|
+
lines.push(` ${findingTitleSafe(finding)}`);
|
|
296
288
|
if (finding.evidence && finding.evidence !== `path: ${finding.location}` && finding.evidence !== finding.location) {
|
|
297
|
-
lines.push(` ${muted(finding
|
|
289
|
+
lines.push(` ${muted(findingEvidenceSafe(finding))}`);
|
|
298
290
|
}
|
|
299
291
|
lines.push(` ${accent("→")} ${finding.recommendation}`);
|
|
300
292
|
lines.push("");
|
|
@@ -3,17 +3,17 @@ import { resolvePresentation } from "../presentation/mode.js";
|
|
|
3
3
|
import { createTheme } from "../presentation/theme.js";
|
|
4
4
|
import { promptYesNo } from "../util/tty-prompt.js";
|
|
5
5
|
import { applyGitHook, gitHookState, planGitHook, removeGitHookForRepo, resolveGitRepo, verifyGitHook } from "../setup/git-hook.js";
|
|
6
|
-
export const
|
|
7
|
-
name: "guard
|
|
6
|
+
export const commitGuardCommand = {
|
|
7
|
+
name: "commit-guard",
|
|
8
8
|
summary: "Scan staged dependencies before every commit in this repo.",
|
|
9
|
-
usage: "dg guard
|
|
9
|
+
usage: "dg commit-guard [off | --check | --print] [--yes]",
|
|
10
10
|
args: [{ name: "[off]", summary: "Remove the hook and restore any hook it chained (alias remove/uninstall)." }],
|
|
11
11
|
flags: [
|
|
12
12
|
{ flag: "--check", summary: "Verify the hook is installed and will actually fire." },
|
|
13
13
|
{ flag: "--print", summary: "Preview what it will write, change nothing." },
|
|
14
14
|
{ flag: "--yes", summary: "Install without the confirmation prompt (alias -y)." }
|
|
15
15
|
],
|
|
16
|
-
examples: ["dg guard
|
|
16
|
+
examples: ["dg commit-guard", "dg commit-guard --check", "dg commit-guard off"],
|
|
17
17
|
details: [
|
|
18
18
|
"Installs a git pre-commit hook that scans staged lockfile changes and blocks a commit that would add a malicious dependency. Override a block with 'git commit --no-verify'.",
|
|
19
19
|
"It resolves the real hooks directory (honouring core.hooksPath/husky and worktrees) and chains any existing pre-commit hook. Tune behaviour with the gitHook.onWarn / gitHook.onIncomplete config keys."
|
|
@@ -53,13 +53,13 @@ function run(context) {
|
|
|
53
53
|
return {
|
|
54
54
|
exitCode: EXIT_USAGE,
|
|
55
55
|
stdout: "",
|
|
56
|
-
stderr: `dg guard
|
|
56
|
+
stderr: `dg commit-guard: ${parsed.error}. Usage: ${commitGuardCommand.usage}\n`
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
const theme = createTheme(resolvePresentation().color);
|
|
60
60
|
const repo = resolveGitRepo();
|
|
61
61
|
if ("error" in repo) {
|
|
62
|
-
return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg guard
|
|
62
|
+
return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg commit-guard: ${repo.error}.\n` };
|
|
63
63
|
}
|
|
64
64
|
if (parsed.off) {
|
|
65
65
|
return remove(repo, theme);
|
|
@@ -82,7 +82,7 @@ function install(repo, theme, yes) {
|
|
|
82
82
|
if (plan.willChain) {
|
|
83
83
|
err.write(` ${muted("Your existing pre-commit hook will be kept and run after dg.")}\n`);
|
|
84
84
|
}
|
|
85
|
-
err.write(` ${muted("Reversible with")} ${accent("dg guard
|
|
85
|
+
err.write(` ${muted("Reversible with")} ${accent("dg commit-guard off")}${muted(".")}\n\n`);
|
|
86
86
|
const proceed = promptYesNo(` ${accent("Proceed?")}`, true);
|
|
87
87
|
if (proceed === false) {
|
|
88
88
|
return { exitCode: 0, stdout: "", stderr: ` ${muted("cancelled — nothing written")}\n` };
|
|
@@ -107,7 +107,7 @@ function check(repo, theme) {
|
|
|
107
107
|
return {
|
|
108
108
|
exitCode: 1,
|
|
109
109
|
stdout: "",
|
|
110
|
-
stderr: ` ${theme.paint("warn", "⚠")} not set up in this repo — run ${theme.paint("accent", "dg guard
|
|
110
|
+
stderr: ` ${theme.paint("warn", "⚠")} not set up in this repo — run ${theme.paint("accent", "dg commit-guard")}\n`
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
const checks = verifyGitHook(repo);
|
|
@@ -120,9 +120,9 @@ function check(repo, theme) {
|
|
|
120
120
|
function print(repo) {
|
|
121
121
|
const plan = planGitHook(repo);
|
|
122
122
|
const lines = [
|
|
123
|
-
"dg guard
|
|
123
|
+
"dg commit-guard write plan",
|
|
124
124
|
"",
|
|
125
|
-
"No files are changed until you run dg guard
|
|
125
|
+
"No files are changed until you run dg commit-guard.",
|
|
126
126
|
`- write pre-commit hook: ${plan.context.hookTarget}`
|
|
127
127
|
];
|
|
128
128
|
if (plan.willChain) {
|
|
@@ -131,7 +131,7 @@ function print(repo) {
|
|
|
131
131
|
else if (plan.state === "managed") {
|
|
132
132
|
lines.push("- (a dg hook is already installed; it will be refreshed)");
|
|
133
133
|
}
|
|
134
|
-
lines.push("- record it for 'dg guard
|
|
134
|
+
lines.push("- record it for 'dg commit-guard off' / 'dg uninstall'");
|
|
135
135
|
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
136
136
|
}
|
|
137
137
|
function remove(repo, theme) {
|
|
@@ -140,7 +140,7 @@ function remove(repo, theme) {
|
|
|
140
140
|
return {
|
|
141
141
|
exitCode: 0,
|
|
142
142
|
stdout: "",
|
|
143
|
-
stderr: ` ${theme.paint("muted", "no dg guard
|
|
143
|
+
stderr: ` ${theme.paint("muted", "no dg commit-guard hook in this repo")}\n`
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
const restored = result.removed.some((path) => path.includes(".dg-chained-"));
|
|
@@ -4,6 +4,7 @@ import { canonicalCooldownName } from "../policy/pypi-name.js";
|
|
|
4
4
|
import { ConfigError, loadUserConfig, setConfigValue, updateUserConfig } from "../config/settings.js";
|
|
5
5
|
import { appendCooldownExemptions, cooldownExemptionActive, CooldownExemptionCapError, findProjectRoot, loadDgFile, mutateDgFile, removeCooldownExemptions, resolveAcceptedBy } from "../project/dgfile.js";
|
|
6
6
|
import { EXIT_USAGE } from "./types.js";
|
|
7
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
7
8
|
export const cooldownCommand = {
|
|
8
9
|
name: "cooldown",
|
|
9
10
|
summary: "Hold new releases for a window before they can install, with per-package exemptions.",
|
|
@@ -342,7 +343,7 @@ function listExemptions(cwd, env, json, now) {
|
|
|
342
343
|
}
|
|
343
344
|
const rows = file.cooldownExemptions.map((e) => [
|
|
344
345
|
`${e.ecosystem}:${e.name}`,
|
|
345
|
-
e.reason || "-",
|
|
346
|
+
sanitizeLine(e.reason) || "-",
|
|
346
347
|
e.acceptedBy || "-",
|
|
347
348
|
e.acceptedAt.slice(0, 10) || "-",
|
|
348
349
|
e.expiresAt ? formatExpiryDisplay(e.expiresAt, e.acceptedAt) : "-",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordDecisionEvents } from "../decisions/remember-prompt.js";
|
|
2
2
|
import { packageKey } from "../decisions/apply.js";
|
|
3
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
3
4
|
import { findProjectRoot, loadDgFile, mutateDgFile, removeDecisions } from "../project/dgfile.js";
|
|
4
5
|
import { EXIT_USAGE } from "./types.js";
|
|
5
6
|
export const decisionsCommand = {
|
|
@@ -63,11 +64,11 @@ function listDecisions(cwd, env, json) {
|
|
|
63
64
|
}
|
|
64
65
|
const rows = file.decisions.map((entry) => [
|
|
65
66
|
entry.id.slice(0, 8),
|
|
66
|
-
`${entry.ecosystem}:${entry.name}@${scopeLabel(entry)}`,
|
|
67
|
+
`${entry.ecosystem}:${sanitizeLine(entry.name)}@${scopeLabel(entry)}`,
|
|
67
68
|
findingsLabel(entry),
|
|
68
|
-
entry.acceptedBy,
|
|
69
|
-
entry.acceptedAt.slice(0, 10) || "-",
|
|
70
|
-
entry.expiresAt ? entry.expiresAt.slice(0, 10) : "-",
|
|
69
|
+
sanitizeLine(entry.acceptedBy),
|
|
70
|
+
sanitizeLine(entry.acceptedAt).slice(0, 10) || "-",
|
|
71
|
+
entry.expiresAt ? sanitizeLine(entry.expiresAt).slice(0, 10) : "-",
|
|
71
72
|
isExpired(entry) ? "expired" : "active"
|
|
72
73
|
]);
|
|
73
74
|
const header = ["ID", "PACKAGE", "ACCEPTED FINDINGS", "BY", "WHEN", "EXPIRES", "STATUS"];
|
|
@@ -103,7 +104,7 @@ function revokeDecisions(selector, cwd, env) {
|
|
|
103
104
|
return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${located.file.path}.\n` };
|
|
104
105
|
}
|
|
105
106
|
recordDecisionEvents("decision.revoked", matched.map((entry) => `${entry.ecosystem}:${packageKey(entry.name, scopeLabel(entry))}`), `revoked via dg decisions (${selector})`, env);
|
|
106
|
-
const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${entry.name}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
|
|
107
|
+
const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${sanitizeLine(entry.name)}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
|
|
107
108
|
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
108
109
|
}
|
|
109
110
|
function locateDgFile(cwd, env) {
|
|
@@ -133,10 +134,10 @@ function matchesSelector(entry, selector) {
|
|
|
133
134
|
return entry.name === selector;
|
|
134
135
|
}
|
|
135
136
|
function scopeLabel(entry) {
|
|
136
|
-
return entry.scope.kind === "exact" ? entry.scope.version : "*";
|
|
137
|
+
return entry.scope.kind === "exact" ? sanitizeLine(entry.scope.version) : "*";
|
|
137
138
|
}
|
|
138
139
|
function findingsLabel(entry) {
|
|
139
|
-
const pairs = Object.entries(entry.findings).map(([category, severity]) => `${category}:${severity}`);
|
|
140
|
+
const pairs = Object.entries(entry.findings).map(([category, severity]) => `${sanitizeLine(category)}:${severity}`);
|
|
140
141
|
return pairs.length > 0 ? pairs.sort().join(",") : "(action-only warn)";
|
|
141
142
|
}
|
|
142
143
|
function isExpired(entry) {
|
package/dist/commands/help.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { optionalPackageManagerNames } from "../setup/optional-support.js";
|
|
1
2
|
const PRODUCT_DESCRIPTION = "Dependency Guardian";
|
|
2
|
-
const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "guard
|
|
3
|
-
const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v
|
|
3
|
+
const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "commit-guard", "doctor", "login"];
|
|
4
|
+
const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v";
|
|
4
5
|
function isWrapper(command) {
|
|
5
6
|
return command.summary.includes("prefix-mode routing");
|
|
6
7
|
}
|
|
@@ -37,7 +38,14 @@ export function renderRootHelp(version, commands, options = {}) {
|
|
|
37
38
|
for (const command of standalone) {
|
|
38
39
|
lines.push(` ${command.name.padEnd(14)} ${command.summary}`);
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
const gated = new Set(optionalPackageManagerNames());
|
|
42
|
+
const wrapperNames = wrappers.map((command) => command.name);
|
|
43
|
+
const claimed = wrapperNames.filter((name) => !gated.has(name));
|
|
44
|
+
const gatedNames = wrapperNames.filter((name) => gated.has(name));
|
|
45
|
+
lines.push("", "Package-manager prefix mode (run any through dg):", ` ${claimed.join(", ")}`);
|
|
46
|
+
if (gatedNames.length > 0) {
|
|
47
|
+
lines.push("", "Gated (refuse with exit 69 until their support gate ships):", ` ${gatedNames.join(", ")}`);
|
|
48
|
+
}
|
|
41
49
|
lines.push("", "Run 'dg <command> --help' for that command's flags and examples.", GLOBAL_FLAGS_FOOTER);
|
|
42
50
|
return `${lines.join("\n")}\n`;
|
|
43
51
|
}
|
|
@@ -4,7 +4,9 @@ import { writeReportAtomic } from "../util/report-writer.js";
|
|
|
4
4
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { scanProject } from "../scan/discovery.js";
|
|
6
6
|
import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
|
|
7
|
-
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
7
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
8
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
9
|
+
import { csvCell } from "../security/csv.js";
|
|
8
10
|
const LICENSE_RISKS = [
|
|
9
11
|
"network-copyleft",
|
|
10
12
|
"no-license",
|
|
@@ -40,7 +42,7 @@ export const licensesCommand = {
|
|
|
40
42
|
examples: ["dg licenses", "dg licenses --markdown -o licenses.md", "dg licenses --fail-on strong-copyleft,network-copyleft"],
|
|
41
43
|
details: [
|
|
42
44
|
"Reports project and lockfile license metadata without running package managers or package code.",
|
|
43
|
-
"License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4
|
|
45
|
+
"License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4 analysis incomplete, 64 usage error, 70 write error."
|
|
44
46
|
],
|
|
45
47
|
handler: (context) => runLicensesCommand(context.args)
|
|
46
48
|
};
|
|
@@ -92,7 +94,7 @@ async function runLicensesCommand(args) {
|
|
|
92
94
|
}
|
|
93
95
|
catch (error) {
|
|
94
96
|
return {
|
|
95
|
-
exitCode:
|
|
97
|
+
exitCode: EXIT_TOOL_ERROR,
|
|
96
98
|
stdout: "",
|
|
97
99
|
stderr: `dg licenses could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
98
100
|
};
|
|
@@ -406,10 +408,20 @@ function dedupeEntries(entries) {
|
|
|
406
408
|
const seen = new Set();
|
|
407
409
|
const deduped = [];
|
|
408
410
|
for (const entry of entries) {
|
|
409
|
-
|
|
411
|
+
// name / version / license / location come from untrusted lockfiles &
|
|
412
|
+
// manifests and are rendered to the terminal and md/csv exports — strip
|
|
413
|
+
// control bytes here, the single point every entry passes through.
|
|
414
|
+
const clean = {
|
|
415
|
+
...entry,
|
|
416
|
+
name: sanitizeLine(entry.name),
|
|
417
|
+
...(entry.version != null ? { version: sanitizeLine(entry.version) } : {}),
|
|
418
|
+
...(entry.license != null ? { license: sanitizeLine(entry.license) } : {}),
|
|
419
|
+
location: sanitizeLine(entry.location)
|
|
420
|
+
};
|
|
421
|
+
const key = `${clean.ecosystem}|${clean.name}|${clean.version ?? ""}|${clean.license ?? ""}|${clean.source}|${clean.location}`;
|
|
410
422
|
if (!seen.has(key)) {
|
|
411
423
|
seen.add(key);
|
|
412
|
-
deduped.push(
|
|
424
|
+
deduped.push(clean);
|
|
413
425
|
}
|
|
414
426
|
}
|
|
415
427
|
return deduped.sort((left, right) => `${left.ecosystem}:${left.name}`.localeCompare(`${right.ecosystem}:${right.name}`));
|
|
@@ -428,9 +440,6 @@ function displayPath(root, path) {
|
|
|
428
440
|
const display = relativePath.length === 0 ? "." : relativePath;
|
|
429
441
|
return display.split(sep).join("/");
|
|
430
442
|
}
|
|
431
|
-
function csvCell(value) {
|
|
432
|
-
return /[",\n\r]/u.test(value) ? `"${value.replace(/"/gu, "\"\"")}"` : value;
|
|
433
|
-
}
|
|
434
443
|
function markdownCell(value) {
|
|
435
444
|
return value.replace(/\|/gu, "\\|");
|
|
436
445
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
|
|
2
3
|
import { AuthError, writeAuthState } from "../auth/store.js";
|
|
3
4
|
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
5
|
+
import { envAuthToken } from "../auth/env-token.js";
|
|
4
6
|
export const loginCommand = {
|
|
5
7
|
name: "login",
|
|
6
8
|
summary: "Authenticate this machine with Dependency Guardian.",
|
|
7
|
-
usage: "dg login [--token <token>]",
|
|
9
|
+
usage: "dg login [--token <token> | --token-stdin]",
|
|
8
10
|
flags: [
|
|
9
|
-
{ flag: "--token", value: "<token>", summary: "Authenticate with an API key instead of the browser (for CI/headless)." }
|
|
11
|
+
{ flag: "--token", value: "<token>", summary: "Authenticate with an API key instead of the browser (for CI/headless)." },
|
|
12
|
+
{ flag: "--token-stdin", summary: "Read the API key from stdin, keeping it out of the process list." }
|
|
10
13
|
],
|
|
11
|
-
examples: ["dg login", "dg login --token
|
|
14
|
+
examples: ["dg login", "dg login --token-stdin", "DG_API_TOKEN=dg_live_… dg login"],
|
|
12
15
|
details: [
|
|
13
16
|
"In a terminal, 'dg login' opens your browser to sign in — no token to copy.",
|
|
14
|
-
"For CI and headless shells,
|
|
17
|
+
"For CI and headless shells, set DG_API_TOKEN (or DG_API_KEY) before running 'dg login', pipe the key to --token-stdin, or pass --token <key>.",
|
|
18
|
+
"Prefer the environment variable or --token-stdin: a key passed as --token <key> is visible to other users in the process list.",
|
|
15
19
|
"Stores dg-owned auth state under the user config directory; never executes project-local code or weakens install enforcement."
|
|
16
20
|
],
|
|
17
21
|
handler: (context) => loginHandler(context.args)
|
|
18
22
|
};
|
|
19
|
-
function loginHandler(args) {
|
|
23
|
+
export function loginHandler(args, env = process.env, readStdin = readStdinSync) {
|
|
20
24
|
const parsed = parseLoginArgs(args);
|
|
21
25
|
if ("error" in parsed) {
|
|
22
26
|
return {
|
|
@@ -25,17 +29,25 @@ function loginHandler(args) {
|
|
|
25
29
|
stderr: `dg login: ${parsed.error}. Run 'dg login --help'.\n`
|
|
26
30
|
};
|
|
27
31
|
}
|
|
32
|
+
const resolved = resolveToken(parsed, env, readStdin);
|
|
33
|
+
if ("error" in resolved) {
|
|
34
|
+
return {
|
|
35
|
+
exitCode: EXIT_USAGE,
|
|
36
|
+
stdout: "",
|
|
37
|
+
stderr: `dg login: ${resolved.error}. Run 'dg login --help'.\n`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
28
40
|
try {
|
|
29
|
-
const config = loadUserConfig();
|
|
41
|
+
const config = loadUserConfig(env);
|
|
30
42
|
const state = writeAuthState({
|
|
31
|
-
token:
|
|
43
|
+
token: resolved.token,
|
|
32
44
|
apiBaseUrl: config.api.baseUrl,
|
|
33
45
|
orgId: config.org.id
|
|
34
|
-
});
|
|
46
|
+
}, env);
|
|
35
47
|
return {
|
|
36
48
|
exitCode: 0,
|
|
37
49
|
stdout: `Logged in to ${state.apiBaseUrl}${state.orgId ? ` for org ${state.orgId}` : ""} with token ${state.tokenPreview}\n`,
|
|
38
|
-
stderr: ""
|
|
50
|
+
stderr: resolved.source === "env" ? "Using the API key from DG_API_TOKEN/DG_API_KEY in the environment.\n" : ""
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
catch (error) {
|
|
@@ -53,16 +65,34 @@ function loginHandler(args) {
|
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
}
|
|
68
|
+
function resolveToken(parsed, env, readStdin) {
|
|
69
|
+
if (parsed.fromStdin) {
|
|
70
|
+
const token = readStdin().trim();
|
|
71
|
+
if (!token) {
|
|
72
|
+
return { error: "no API key was provided on stdin" };
|
|
73
|
+
}
|
|
74
|
+
return { token, source: "stdin" };
|
|
75
|
+
}
|
|
76
|
+
if (parsed.token !== undefined) {
|
|
77
|
+
return { token: parsed.token, source: "flag" };
|
|
78
|
+
}
|
|
79
|
+
const fromEnv = envAuthToken(env);
|
|
80
|
+
if (fromEnv) {
|
|
81
|
+
return { token: fromEnv, source: "env" };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
error: "run 'dg login' in a terminal to sign in via your browser, or set DG_API_TOKEN / pipe the key to 'dg login --token-stdin' / pass 'dg login --token <key>' for CI"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
56
87
|
function parseLoginArgs(args) {
|
|
57
|
-
let token
|
|
88
|
+
let token;
|
|
89
|
+
let fromStdin = false;
|
|
58
90
|
for (let index = 0; index < args.length; index += 1) {
|
|
59
91
|
const arg = args[index];
|
|
60
92
|
if (arg === "--token") {
|
|
61
93
|
const value = args[index + 1];
|
|
62
94
|
if (!value) {
|
|
63
|
-
return {
|
|
64
|
-
error: "--token requires a value"
|
|
65
|
-
};
|
|
95
|
+
return { error: "--token requires a value" };
|
|
66
96
|
}
|
|
67
97
|
token = value;
|
|
68
98
|
index += 1;
|
|
@@ -70,10 +100,11 @@ function parseLoginArgs(args) {
|
|
|
70
100
|
else if (arg?.startsWith("--token=")) {
|
|
71
101
|
token = arg.slice("--token=".length);
|
|
72
102
|
}
|
|
103
|
+
else if (arg === "--token-stdin") {
|
|
104
|
+
fromStdin = true;
|
|
105
|
+
}
|
|
73
106
|
else if (arg?.startsWith("-")) {
|
|
74
|
-
return {
|
|
75
|
-
error: `unknown option '${arg}'`
|
|
76
|
-
};
|
|
107
|
+
return { error: `unknown option '${arg}'` };
|
|
77
108
|
}
|
|
78
109
|
else {
|
|
79
110
|
return {
|
|
@@ -81,10 +112,16 @@ function parseLoginArgs(args) {
|
|
|
81
112
|
};
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
|
-
if (
|
|
85
|
-
return {
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
if (token !== undefined && fromStdin) {
|
|
116
|
+
return { error: "--token and --token-stdin cannot be combined" };
|
|
117
|
+
}
|
|
118
|
+
return fromStdin ? { fromStdin: true } : token !== undefined ? { token } : {};
|
|
119
|
+
}
|
|
120
|
+
function readStdinSync() {
|
|
121
|
+
try {
|
|
122
|
+
return readFileSync(0, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return "";
|
|
88
126
|
}
|
|
89
|
-
return { token };
|
|
90
127
|
}
|
package/dist/commands/router.js
CHANGED
|
@@ -3,7 +3,7 @@ import { auditCommand } from "./audit.js";
|
|
|
3
3
|
import { cooldownCommand } from "./cooldown.js";
|
|
4
4
|
import { decisionsCommand } from "./decisions.js";
|
|
5
5
|
import { doctorCommand } from "./doctor.js";
|
|
6
|
-
import {
|
|
6
|
+
import { commitGuardCommand } from "./commit-guard.js";
|
|
7
7
|
import { agentsCommand } from "./agents.js";
|
|
8
8
|
import { renderCommandHelp, renderRootHelp } from "./help.js";
|
|
9
9
|
import { licensesCommand } from "./licenses.js";
|
|
@@ -11,6 +11,7 @@ import { sbomCommand } from "./sbom.js";
|
|
|
11
11
|
import { loginCommand } from "./login.js";
|
|
12
12
|
import { logoutCommand } from "./logout.js";
|
|
13
13
|
import { packageManagerCommands } from "./wrap.js";
|
|
14
|
+
import { isSupportedPackageManager, normalizeManagerName } from "../launcher/classify.js";
|
|
14
15
|
import { scanCommand } from "./scan.js";
|
|
15
16
|
import { serviceCommand } from "./service.js";
|
|
16
17
|
import { setupCommand } from "./setup.js";
|
|
@@ -26,7 +27,7 @@ export const commandCatalog = [
|
|
|
26
27
|
sbomCommand,
|
|
27
28
|
verifyCommand,
|
|
28
29
|
setupCommand,
|
|
29
|
-
|
|
30
|
+
commitGuardCommand,
|
|
30
31
|
agentsCommand,
|
|
31
32
|
decisionsCommand,
|
|
32
33
|
cooldownCommand,
|
|
@@ -79,6 +80,15 @@ export async function routeCommand(args) {
|
|
|
79
80
|
}
|
|
80
81
|
const command = commandCatalog.find((candidate) => candidate.name === commandName || candidate.aliases?.includes(commandName));
|
|
81
82
|
if (!command) {
|
|
83
|
+
// The pip3 shim execs `dg pip3`, but the wrap catalog holds only canonical
|
|
84
|
+
// names; fold as the classifier does so `dg pip3 …` reaches the pip manager.
|
|
85
|
+
const normalizedManager = normalizeManagerName(commandName);
|
|
86
|
+
if (normalizedManager !== commandName && isSupportedPackageManager(normalizedManager)) {
|
|
87
|
+
const managerCommand = commandCatalog.find((candidate) => candidate.name === normalizedManager);
|
|
88
|
+
if (managerCommand) {
|
|
89
|
+
return managerCommand.handler({ commandPath: [normalizedManager], args: rest });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
const suggestion = closestCommand(commandName, commandNames());
|
|
83
93
|
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
84
94
|
return {
|
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",
|