@westbayberry/dg 2.0.7 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +158 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/pip-report.js +18 -2
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
package/dist/commands/explain.js
CHANGED
|
@@ -60,6 +60,134 @@ export const EXPLANATIONS = {
|
|
|
60
60
|
"proxy-setup-failure": {
|
|
61
61
|
what: "dg's per-install enforcement proxy could not start, so protected installs fail closed rather than run unverified.",
|
|
62
62
|
next: "Run 'dg doctor' for the failing check, then retry."
|
|
63
|
+
},
|
|
64
|
+
"needs-login": {
|
|
65
|
+
what: "This install needed a verdict that requires an authenticated account, so it was blocked rather than installed unverified.",
|
|
66
|
+
next: "Run 'dg login', then retry the install."
|
|
67
|
+
},
|
|
68
|
+
lifecycle: {
|
|
69
|
+
what: "The scanner flagged an install lifecycle script (preinstall/install/postinstall) that runs arbitrary code during install.",
|
|
70
|
+
next: "Review the script in the package source, or install with --ignore-scripts."
|
|
71
|
+
},
|
|
72
|
+
"install-script": {
|
|
73
|
+
what: "The scanner found an install-time script in this package that executes code on your machine during install.",
|
|
74
|
+
next: "Review the script before installing; prefer a version without install scripts."
|
|
75
|
+
},
|
|
76
|
+
"archive-path-traversal": {
|
|
77
|
+
what: "The package archive contains an entry whose path escapes the extraction directory, a classic overwrite attack.",
|
|
78
|
+
next: "Do not install it; report the package and pin a known-safe version."
|
|
79
|
+
},
|
|
80
|
+
"archive-path-too-long": {
|
|
81
|
+
what: "The package archive contains an entry path long enough to break extraction or smuggle content past tooling.",
|
|
82
|
+
next: "Treat the artifact as untrusted and verify it out of band."
|
|
83
|
+
},
|
|
84
|
+
"archive-size-limit": {
|
|
85
|
+
what: "The package archive expands past dg's size limit, so its contents were not fully verified (possible zip bomb).",
|
|
86
|
+
next: "Verify the artifact out of band before trusting it."
|
|
87
|
+
},
|
|
88
|
+
"encrypted-archive-entry": {
|
|
89
|
+
what: "The package archive contains an encrypted entry dg cannot inspect, so part of the package is unverified.",
|
|
90
|
+
next: "Treat it as unverified; ask the publisher why the archive is encrypted."
|
|
91
|
+
},
|
|
92
|
+
"zip-data-descriptor": {
|
|
93
|
+
what: "The package archive uses zip data descriptors that can make different tools see different contents.",
|
|
94
|
+
next: "Verify the artifact out of band before trusting it."
|
|
95
|
+
},
|
|
96
|
+
"license-policy-denied": {
|
|
97
|
+
what: "The dependency's license is denied by your license policy.",
|
|
98
|
+
next: "Replace the dependency, or update the policy if the obligation is acceptable."
|
|
99
|
+
},
|
|
100
|
+
"lockfile-url-fallback": {
|
|
101
|
+
what: "A lockfile entry resolves to a raw URL instead of a registry version, so it has no registry identity to verify.",
|
|
102
|
+
next: "Re-pin the dependency to a registry version and regenerate the lockfile."
|
|
103
|
+
},
|
|
104
|
+
"unverified-lockfile-url": {
|
|
105
|
+
what: "A lockfile entry downloads from a URL the scanner could not verify against a registry artifact.",
|
|
106
|
+
next: "Pin a registry version, or run 'dg verify <url>' before installing."
|
|
107
|
+
},
|
|
108
|
+
"unverified-network-spec": {
|
|
109
|
+
what: "A dependency spec points at a raw http/git source instead of the registry, so its contents are unverified.",
|
|
110
|
+
next: "Pin the dependency to a registry version, or run 'dg verify <url>' first."
|
|
111
|
+
},
|
|
112
|
+
"unpinned-package-spec": {
|
|
113
|
+
what: "A dependency spec has no pinned version, so installs can silently pull a different artifact than was reviewed.",
|
|
114
|
+
next: "Pin an exact version (or use a lockfile) and re-run the scan."
|
|
115
|
+
},
|
|
116
|
+
"unsupported-package-spec": {
|
|
117
|
+
what: "A dependency spec uses a form dg cannot resolve to a registry artifact, so it was not verified.",
|
|
118
|
+
next: "Rewrite the spec as a registry name@version, or verify the artifact out of band."
|
|
119
|
+
},
|
|
120
|
+
"malformed-lockfile": {
|
|
121
|
+
what: "A lockfile could not be parsed, so the packages it pins were not verified.",
|
|
122
|
+
next: "Regenerate the lockfile with your package manager and re-run the scan."
|
|
123
|
+
},
|
|
124
|
+
"malformed-package-manifest": {
|
|
125
|
+
what: "A package manifest could not be parsed, so its dependencies were not verified.",
|
|
126
|
+
next: "Fix the manifest syntax and re-run the scan."
|
|
127
|
+
},
|
|
128
|
+
"missing-artifact-integrity": {
|
|
129
|
+
what: "A lockfile entry has no integrity hash, so the downloaded artifact cannot be checked against what was reviewed.",
|
|
130
|
+
next: "Regenerate the lockfile so every entry carries an integrity hash."
|
|
131
|
+
},
|
|
132
|
+
"credential-file": {
|
|
133
|
+
what: "Your publish set includes a credentials file (for example .npmrc, .netrc, or cloud keys) that would ship to the registry.",
|
|
134
|
+
next: "Remove the file from the publish set and rotate any leaked credential."
|
|
135
|
+
},
|
|
136
|
+
"private-key": {
|
|
137
|
+
what: "Your publish set includes a private key file that would become public on publish.",
|
|
138
|
+
next: "Remove it from the publish set and rotate the key."
|
|
139
|
+
},
|
|
140
|
+
"bundled-secret": {
|
|
141
|
+
what: "A file in your publish set contains what looks like a live secret (API key, token, or password).",
|
|
142
|
+
next: "Remove or redact the secret, rotate it, and re-run 'dg audit'."
|
|
143
|
+
},
|
|
144
|
+
"scm-leakage": {
|
|
145
|
+
what: "Your publish set includes version-control internals (for example a .git directory) that leak history and remotes.",
|
|
146
|
+
next: "Exclude the directory via the files allowlist or an ignore file."
|
|
147
|
+
},
|
|
148
|
+
"ci-config": {
|
|
149
|
+
what: "Your publish set includes CI configuration that often references internal infrastructure and secrets.",
|
|
150
|
+
next: "Exclude it from the publish set unless consumers need it."
|
|
151
|
+
},
|
|
152
|
+
"iac-secret": {
|
|
153
|
+
what: "Your publish set includes infrastructure-as-code state or vaults (for example tfstate) that commonly embed secrets.",
|
|
154
|
+
next: "Remove the file and rotate anything it contains."
|
|
155
|
+
},
|
|
156
|
+
"source-map": {
|
|
157
|
+
what: "Your publish set includes source maps that expose your original source to every consumer.",
|
|
158
|
+
next: "Exclude .map files from the publish set unless that is intended."
|
|
159
|
+
},
|
|
160
|
+
"build-artifact": {
|
|
161
|
+
what: "Your publish set includes build or coverage output that bloats the package and can leak internal paths.",
|
|
162
|
+
next: "Exclude the directory via the files allowlist."
|
|
163
|
+
},
|
|
164
|
+
"editor-os-junk": {
|
|
165
|
+
what: "Your publish set includes editor or OS junk files (for example .DS_Store or editor swap files).",
|
|
166
|
+
next: "Exclude them; they add noise and can leak local paths."
|
|
167
|
+
},
|
|
168
|
+
"data-dump": {
|
|
169
|
+
what: "Your publish set includes database dumps, logs, or captures that frequently contain real user data.",
|
|
170
|
+
next: "Remove the file and check whether the data requires disclosure."
|
|
171
|
+
},
|
|
172
|
+
"internal-leak": {
|
|
173
|
+
what: "Your publish set includes files that look internal-only (for example memory dumps or HAR captures).",
|
|
174
|
+
next: "Remove them from the publish set before publishing."
|
|
175
|
+
},
|
|
176
|
+
"lifecycle-risk": {
|
|
177
|
+
what: "Your package declares install lifecycle scripts that will run arbitrary code on every consumer's machine.",
|
|
178
|
+
next: "Drop the script if possible; consumers increasingly refuse install scripts."
|
|
179
|
+
},
|
|
180
|
+
structural: {
|
|
181
|
+
what: "The publish set has a structural problem (for example no files allowlist or a symlink escaping the package root).",
|
|
182
|
+
next: "Fix the structure so the publish set is exactly what you reviewed."
|
|
183
|
+
},
|
|
184
|
+
"pem-private-key": {
|
|
185
|
+
what: "A PEM-encoded private key is inside your publish set and would become public on publish.",
|
|
186
|
+
next: "Remove it from the publish set and rotate the key."
|
|
187
|
+
},
|
|
188
|
+
"no-files-allowlist": {
|
|
189
|
+
what: "package.json has no files allowlist, so npm publishes nearly everything in the directory by default.",
|
|
190
|
+
next: "Add a files allowlist so the publish set is explicit."
|
|
63
191
|
}
|
|
64
192
|
};
|
|
65
193
|
export const explainCommand = {
|
|
@@ -82,14 +210,18 @@ function runExplain(args) {
|
|
|
82
210
|
stderr: "dg explain: expected exactly one finding or cause id. Run 'dg explain --help'.\n"
|
|
83
211
|
};
|
|
84
212
|
}
|
|
85
|
-
const
|
|
213
|
+
const normalized = id.toLowerCase().replaceAll("_", "-");
|
|
214
|
+
const explanation = EXPLANATIONS[id] ?? EXPLANATIONS[normalized];
|
|
86
215
|
if (!explanation) {
|
|
87
|
-
const suggestion = closestCommand(
|
|
88
|
-
const hint = suggestion ? `
|
|
216
|
+
const suggestion = closestCommand(normalized, Object.keys(EXPLANATIONS));
|
|
217
|
+
const hint = suggestion ? `Did you mean '${suggestion}'? ` : "";
|
|
89
218
|
return {
|
|
90
|
-
exitCode:
|
|
91
|
-
stdout:
|
|
92
|
-
|
|
219
|
+
exitCode: 0,
|
|
220
|
+
stdout: `${id}\n\n` +
|
|
221
|
+
`dg has no local entry for '${id}'. ${hint}` +
|
|
222
|
+
"Scanner finding ids evolve faster than this glossary, so the id may still be real.\n\n" +
|
|
223
|
+
`Next: open the affected package's page on westbayberry.com — its findings list shows the evidence and remediation for '${id}'.\n`,
|
|
224
|
+
stderr: ""
|
|
93
225
|
};
|
|
94
226
|
}
|
|
95
227
|
return {
|
|
@@ -3,7 +3,7 @@ import { launchScanTui, shouldLaunchScanTui } from "../scan-ui/launch.js";
|
|
|
3
3
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { scanProject } from "../scan/discovery.js";
|
|
5
5
|
import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
|
|
6
|
-
import {
|
|
6
|
+
import { EXIT_USAGE_VERDICT } from "./types.js";
|
|
7
7
|
const LICENSE_RISKS = [
|
|
8
8
|
"network-copyleft",
|
|
9
9
|
"no-license",
|
|
@@ -72,8 +72,9 @@ async function runLicensesCommand(args) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
let report;
|
|
75
|
+
let skippedDirectories;
|
|
75
76
|
try {
|
|
76
|
-
report = buildLicenseReport(parsed);
|
|
77
|
+
({ report, skippedDirectories } = buildLicenseReport(parsed));
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
80
|
return {
|
|
@@ -82,6 +83,7 @@ async function runLicensesCommand(args) {
|
|
|
82
83
|
stderr: `dg licenses failed: ${error instanceof Error ? error.message : "unknown license error"}\n`
|
|
83
84
|
};
|
|
84
85
|
}
|
|
86
|
+
const notices = skippedDirectories.map((directory) => `dg licenses: skipped unreadable directory ${directory}\n`).join("");
|
|
85
87
|
const rendered = renderLicenseReport(report, parsed.format);
|
|
86
88
|
if (parsed.outputPath) {
|
|
87
89
|
try {
|
|
@@ -97,13 +99,13 @@ async function runLicensesCommand(args) {
|
|
|
97
99
|
return {
|
|
98
100
|
exitCode: exitCodeForReport(report),
|
|
99
101
|
stdout: `Wrote ${parsed.format} license report to ${parsed.outputPath}\n`,
|
|
100
|
-
stderr:
|
|
102
|
+
stderr: notices
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
return {
|
|
104
106
|
exitCode: exitCodeForReport(report),
|
|
105
107
|
stdout: rendered,
|
|
106
|
-
stderr:
|
|
108
|
+
stderr: notices
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
111
|
function parseLicensesArgs(args) {
|
|
@@ -196,9 +198,10 @@ function buildLicenseReport(parsed) {
|
|
|
196
198
|
const targetPath = resolve(parsed.target);
|
|
197
199
|
const targetInfo = statSync(targetPath);
|
|
198
200
|
const root = targetInfo.isFile() ? dirname(targetPath) : targetPath;
|
|
201
|
+
const unreadable = [];
|
|
199
202
|
const lockfiles = targetInfo.isFile() && isSupportedLockfilePath(targetPath)
|
|
200
203
|
? [targetPath]
|
|
201
|
-
: discoverLockfiles(root);
|
|
204
|
+
: discoverLockfiles(root, unreadable);
|
|
202
205
|
const entries = dedupeEntries([
|
|
203
206
|
...manifestLicenseEntries(parsed.target),
|
|
204
207
|
...lockfiles.flatMap((lockfile) => lockfileLicenseEntries(root, lockfile))
|
|
@@ -214,18 +217,21 @@ function buildLicenseReport(parsed) {
|
|
|
214
217
|
byRisk[entry.risk] += 1;
|
|
215
218
|
}
|
|
216
219
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
report: {
|
|
221
|
+
target: displayPath(process.cwd(), targetPath),
|
|
222
|
+
status: blocked.length > 0 ? "block" : "pass",
|
|
223
|
+
entries,
|
|
224
|
+
policy: {
|
|
225
|
+
deniedLicenses: [...denied].sort(),
|
|
226
|
+
failOn: [...failOn].sort()
|
|
227
|
+
},
|
|
228
|
+
summary: {
|
|
229
|
+
packageCount: entries.length,
|
|
230
|
+
blockedCount: blocked.length,
|
|
231
|
+
byRisk
|
|
232
|
+
}
|
|
223
233
|
},
|
|
224
|
-
|
|
225
|
-
packageCount: entries.length,
|
|
226
|
-
blockedCount: blocked.length,
|
|
227
|
-
byRisk
|
|
228
|
-
}
|
|
234
|
+
skippedDirectories: unreadable.map((directory) => displayPath(root, directory)).sort()
|
|
229
235
|
};
|
|
230
236
|
}
|
|
231
237
|
function manifestLicenseEntries(target) {
|
|
@@ -267,23 +273,30 @@ function licenseEntryFromIdentity(root, lockfile, identity) {
|
|
|
267
273
|
version: identity.version
|
|
268
274
|
};
|
|
269
275
|
}
|
|
270
|
-
function discoverLockfiles(root) {
|
|
276
|
+
function discoverLockfiles(root, unreadable) {
|
|
271
277
|
const lockfiles = [];
|
|
272
|
-
walk(root, 0, lockfiles);
|
|
278
|
+
walk(root, 0, lockfiles, unreadable);
|
|
273
279
|
return lockfiles.sort((left, right) => displayPath(root, left).localeCompare(displayPath(root, right)));
|
|
274
280
|
}
|
|
275
|
-
function walk(directory, depth, lockfiles) {
|
|
281
|
+
function walk(directory, depth, lockfiles, unreadable) {
|
|
276
282
|
if (depth > MAX_DISCOVERY_DEPTH) {
|
|
277
283
|
return;
|
|
278
284
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
let entries;
|
|
286
|
+
try {
|
|
287
|
+
entries = readdirSync(directory, {
|
|
288
|
+
withFileTypes: true
|
|
289
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
unreadable.push(directory);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
282
295
|
for (const entry of entries) {
|
|
283
296
|
const absolutePath = resolve(directory, entry.name);
|
|
284
297
|
if (entry.isDirectory()) {
|
|
285
298
|
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
286
|
-
walk(absolutePath, depth + 1, lockfiles);
|
|
299
|
+
walk(absolutePath, depth + 1, lockfiles, unreadable);
|
|
287
300
|
}
|
|
288
301
|
continue;
|
|
289
302
|
}
|
|
@@ -383,7 +396,7 @@ function exitCodeForReport(report) {
|
|
|
383
396
|
}
|
|
384
397
|
function usageError(message) {
|
|
385
398
|
return {
|
|
386
|
-
exitCode:
|
|
399
|
+
exitCode: EXIT_USAGE_VERDICT,
|
|
387
400
|
stdout: "",
|
|
388
401
|
stderr: `dg licenses: ${message}. Usage: dg licenses [path] [--json|--csv|--markdown] [--output <path>] [--fail-on <risk[,risk...]>] [--deny-license <id>]\n`
|
|
389
402
|
};
|
package/dist/commands/login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXIT_USAGE } from "./types.js";
|
|
1
|
+
import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
|
|
2
2
|
import { AuthError, writeAuthState } from "../auth/store.js";
|
|
3
3
|
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
4
4
|
export const loginCommand = {
|
|
@@ -46,7 +46,11 @@ function loginHandler(args) {
|
|
|
46
46
|
stderr: `dg login: ${error.message}\n`
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
return {
|
|
50
|
+
exitCode: EXIT_TOOL_ERROR,
|
|
51
|
+
stdout: "",
|
|
52
|
+
stderr: `dg login: could not save auth state: ${error instanceof Error ? error.message : "unknown error"}\n`
|
|
53
|
+
};
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
function parseLoginArgs(args) {
|
|
@@ -66,9 +70,14 @@ function parseLoginArgs(args) {
|
|
|
66
70
|
else if (arg?.startsWith("--token=")) {
|
|
67
71
|
token = arg.slice("--token=".length);
|
|
68
72
|
}
|
|
73
|
+
else if (arg?.startsWith("-")) {
|
|
74
|
+
return {
|
|
75
|
+
error: `unknown option '${arg}'`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
69
78
|
else {
|
|
70
79
|
return {
|
|
71
|
-
error: `
|
|
80
|
+
error: `unexpected argument '${arg ?? ""}'. dg login takes no arguments: run 'dg login' for browser sign-in, or 'dg login --token <key>' for CI`
|
|
72
81
|
};
|
|
73
82
|
}
|
|
74
83
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXIT_USAGE } from "./types.js";
|
|
1
|
+
import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
|
|
2
2
|
import { clearAuthState } from "../auth/store.js";
|
|
3
3
|
export const logoutCommand = {
|
|
4
4
|
name: "logout",
|
|
@@ -24,10 +24,21 @@ function logoutHandler(args, env = process.env) {
|
|
|
24
24
|
stderr: "dg logout requires --yes to confirm.\n"
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
let removed;
|
|
28
|
+
try {
|
|
29
|
+
removed = clearAuthState();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
exitCode: EXIT_TOOL_ERROR,
|
|
34
|
+
stdout: "",
|
|
35
|
+
stderr: `dg logout: could not remove the local auth token: ${error instanceof Error ? error.message : "unknown error"}\n`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
28
38
|
const lines = [removed ? "Removed local dg auth token." : "No local dg auth token was present."];
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
const activeEnvVars = ["DG_API_KEY", "DG_API_TOKEN"].filter((name) => env[name]);
|
|
40
|
+
if (activeEnvVars.length > 0) {
|
|
41
|
+
lines.push(`${activeEnvVars.join(" and ")} ${activeEnvVars.length === 1 ? "is" : "are"} still set, so env-var auth remains active (file token removed only).`);
|
|
31
42
|
}
|
|
32
43
|
return {
|
|
33
44
|
exitCode: 0,
|
package/dist/commands/scan.js
CHANGED
|
@@ -12,7 +12,7 @@ export const scanCommand = {
|
|
|
12
12
|
],
|
|
13
13
|
examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --staged"],
|
|
14
14
|
details: [
|
|
15
|
-
"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."
|
|
15
|
+
"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, 64 usage error."
|
|
16
16
|
],
|
|
17
17
|
handler: runScanCommand
|
|
18
18
|
};
|
package/dist/commands/service.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { EXIT_USAGE } from "./types.js";
|
|
1
|
+
import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
|
|
2
2
|
import { renderCommandHelp } from "./help.js";
|
|
3
|
-
import { buildServiceUninstallPlan, buildTrustInstallPlan, buildTrustUninstallPlan, installServiceTrust, readServiceState, renderServicePlan, restartService, ServiceNotConfiguredError, ServiceProxyError, ServiceTrustStoreError, startService, stopService, uninstallService, uninstallServiceTrust } from "../service/state.js";
|
|
3
|
+
import { buildServiceUninstallPlan, buildTrustInstallPlan, buildTrustUninstallPlan, installServiceTrust, readServiceState, renderServicePlan, resolveServicePaths, restartService, ServiceNotConfiguredError, ServiceProxyError, ServiceTrustStoreError, ServiceTrustToolMissingError, startService, stopService, uninstallService, uninstallServiceTrust } from "../service/state.js";
|
|
4
4
|
import { LockBusyError } from "../state/locks.js";
|
|
5
5
|
const trustCommand = {
|
|
6
6
|
name: "trust",
|
|
@@ -26,27 +26,9 @@ const trustCommand = {
|
|
|
26
26
|
handler: routeService
|
|
27
27
|
};
|
|
28
28
|
const serviceSubcommands = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
usage: "dg service start",
|
|
33
|
-
details: ["Service mode is explicit, reversible, documented, and separate from default setup."],
|
|
34
|
-
handler: () => serviceMutation("start", () => startService())
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "stop",
|
|
38
|
-
summary: "Stop explicit service mode.",
|
|
39
|
-
usage: "dg service stop",
|
|
40
|
-
details: ["Service mode is explicit, reversible, documented, and separate from default setup."],
|
|
41
|
-
handler: () => serviceMutation("stop", () => stopService())
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "restart",
|
|
45
|
-
summary: "Restart explicit service mode.",
|
|
46
|
-
usage: "dg service restart",
|
|
47
|
-
details: ["Service mode is explicit, reversible, documented, and separate from default setup."],
|
|
48
|
-
handler: () => serviceMutation("restart", () => restartService())
|
|
49
|
-
},
|
|
29
|
+
mutationSpec("start", "Start explicit service mode.", () => startService()),
|
|
30
|
+
mutationSpec("stop", "Stop explicit service mode.", () => stopService()),
|
|
31
|
+
mutationSpec("restart", "Restart explicit service mode.", () => restartService()),
|
|
50
32
|
{
|
|
51
33
|
name: "status",
|
|
52
34
|
summary: "Show explicit service mode status.",
|
|
@@ -76,7 +58,7 @@ export const serviceCommand = {
|
|
|
76
58
|
examples: ["dg service start", "dg service status --json", "dg service trust install --print"],
|
|
77
59
|
details: [
|
|
78
60
|
"Service mode is never silently enabled by package install or default setup.",
|
|
79
|
-
"status and doctor accept --json; uninstall and trust accept --print / --yes."
|
|
61
|
+
"status and doctor accept --json; uninstall and trust accept --print / --yes; start, stop, and restart accept --print."
|
|
80
62
|
],
|
|
81
63
|
subcommands: [...serviceSubcommands, trustCommand],
|
|
82
64
|
handler: routeService
|
|
@@ -165,6 +147,64 @@ function serviceDoctorHandler(args) {
|
|
|
165
147
|
stderr: ""
|
|
166
148
|
};
|
|
167
149
|
}
|
|
150
|
+
function mutationSpec(action, summary, run) {
|
|
151
|
+
const spec = {
|
|
152
|
+
name: action,
|
|
153
|
+
summary,
|
|
154
|
+
usage: `dg service ${action} [--print]`,
|
|
155
|
+
flags: [{ flag: "--print", summary: "Show what this would change without acting." }],
|
|
156
|
+
details: ["Service mode is explicit, reversible, documented, and separate from default setup."],
|
|
157
|
+
handler: (context) => mutationHandler(spec, action, run, context.args)
|
|
158
|
+
};
|
|
159
|
+
return spec;
|
|
160
|
+
}
|
|
161
|
+
function mutationHandler(spec, action, run, args) {
|
|
162
|
+
let printOnly = false;
|
|
163
|
+
for (const arg of args) {
|
|
164
|
+
if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
165
|
+
return {
|
|
166
|
+
exitCode: 0,
|
|
167
|
+
stdout: renderCommandHelp(spec, ["service", action]),
|
|
168
|
+
stderr: ""
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (arg === "--print") {
|
|
172
|
+
printOnly = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
exitCode: EXIT_USAGE,
|
|
177
|
+
stdout: "",
|
|
178
|
+
stderr: `dg service ${action}: unknown option '${arg}'. Run 'dg service ${action} --help'.\n`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (printOnly) {
|
|
182
|
+
return {
|
|
183
|
+
exitCode: 0,
|
|
184
|
+
stdout: renderServicePlan(`Dependency Guardian service ${action} plan`, { writes: mutationPlanWrites(action) }),
|
|
185
|
+
stderr: ""
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return serviceMutation(action, run);
|
|
189
|
+
}
|
|
190
|
+
function mutationPlanWrites(action) {
|
|
191
|
+
const paths = resolveServicePaths();
|
|
192
|
+
const startWrites = [
|
|
193
|
+
{ action: "start the dg service proxy worker and record its runtime state", path: paths.runtimePath },
|
|
194
|
+
{ action: "mark explicit service mode running", path: paths.statePath }
|
|
195
|
+
];
|
|
196
|
+
const stopWrites = [
|
|
197
|
+
{ action: "stop the dg service proxy worker and remove its runtime state", path: paths.runtimePath },
|
|
198
|
+
{ action: "mark explicit service mode stopped", path: paths.statePath }
|
|
199
|
+
];
|
|
200
|
+
if (action === "start") {
|
|
201
|
+
return startWrites;
|
|
202
|
+
}
|
|
203
|
+
if (action === "stop") {
|
|
204
|
+
return stopWrites;
|
|
205
|
+
}
|
|
206
|
+
return [...stopWrites.slice(0, 1), ...startWrites];
|
|
207
|
+
}
|
|
168
208
|
function serviceMutation(action, run) {
|
|
169
209
|
try {
|
|
170
210
|
const result = run();
|
|
@@ -221,6 +261,9 @@ function trustInstallHandler(args) {
|
|
|
221
261
|
if (error instanceof ServiceNotConfiguredError) {
|
|
222
262
|
return serviceNotConfiguredResult();
|
|
223
263
|
}
|
|
264
|
+
if (error instanceof ServiceTrustToolMissingError) {
|
|
265
|
+
return trustToolMissingResult(planText, error.tool);
|
|
266
|
+
}
|
|
224
267
|
if (error instanceof ServiceTrustStoreError) {
|
|
225
268
|
return trustStoreFailureResult(planText, "install", error.message);
|
|
226
269
|
}
|
|
@@ -473,3 +516,12 @@ function trustStoreFailureResult(planText, action, message) {
|
|
|
473
516
|
stderr: `dg service trust ${action} failed before recording successful trust state: ${message}\n`
|
|
474
517
|
};
|
|
475
518
|
}
|
|
519
|
+
function trustToolMissingResult(planText, tool) {
|
|
520
|
+
return {
|
|
521
|
+
exitCode: EXIT_UNAVAILABLE,
|
|
522
|
+
stdout: planText,
|
|
523
|
+
stderr: `dg service trust install: '${tool}' is not available on this system, so no trust state was changed.\n` +
|
|
524
|
+
"Native trust install is supported on macOS (security) and Debian/Ubuntu Linux (update-ca-certificates).\n" +
|
|
525
|
+
"On other systems, set DG_SERVICE_TRUST_STORE_BACKEND=file with DG_SERVICE_TRUST_STORE_DIR=<dir> and point your tooling at the exported CA file.\n"
|
|
526
|
+
};
|
|
527
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { authStatus, displayTier } from "../auth/store.js";
|
|
1
|
+
import { authStatus, displayTier, readAuthState } from "../auth/store.js";
|
|
2
|
+
import { envAuthToken } from "../auth/env-token.js";
|
|
3
|
+
import { fetchAccountStatus } from "../auth/device-login.js";
|
|
4
|
+
import { formatUsage } from "../scan-ui/format-helpers.js";
|
|
2
5
|
import { loadUserConfig } from "../config/settings.js";
|
|
3
6
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
4
7
|
import { createTheme } from "../presentation/theme.js";
|
|
@@ -17,7 +20,8 @@ export const statusCommand = {
|
|
|
17
20
|
],
|
|
18
21
|
handler: (context) => runStatusCommand(context.args)
|
|
19
22
|
};
|
|
20
|
-
|
|
23
|
+
export const STATUS_USAGE_TIMEOUT_MS = 2_500;
|
|
24
|
+
export async function runStatusCommand(args, io = {}) {
|
|
21
25
|
const json = args.includes("--json");
|
|
22
26
|
const unknown = args.find((arg) => arg !== "--json");
|
|
23
27
|
if (unknown) {
|
|
@@ -27,7 +31,7 @@ function runStatusCommand(args) {
|
|
|
27
31
|
stderr: `dg status: unknown option '${unknown}'. Run 'dg status --help'.\n`
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
|
-
const report = buildStatusReport();
|
|
34
|
+
const report = await buildStatusReport(io);
|
|
31
35
|
if (json) {
|
|
32
36
|
return {
|
|
33
37
|
exitCode: 0,
|
|
@@ -41,19 +45,39 @@ function runStatusCommand(args) {
|
|
|
41
45
|
stderr: ""
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
|
-
function buildStatusReport() {
|
|
48
|
+
async function buildStatusReport(io) {
|
|
45
49
|
const checks = doctorReport().checks;
|
|
46
50
|
const checkPassed = (name) => checks.find((check) => check.name === name)?.status === "pass";
|
|
47
51
|
const auth = safeAuthStatus();
|
|
48
52
|
const policy = loadUserConfig().policy;
|
|
49
53
|
return {
|
|
50
54
|
account: auth,
|
|
55
|
+
usage: await fetchStatusUsage(auth.connected, io),
|
|
51
56
|
protection: { shims: checkPassed("shims"), path: checkPassed("path"), configured: checkPassed("shell-rc") },
|
|
52
57
|
reloadHint: currentShellActivation(),
|
|
53
58
|
commitGuard: safeCommitGuard(),
|
|
54
59
|
policy: { mode: policy.mode, trustProjectAllowlists: policy.trustProjectAllowlists }
|
|
55
60
|
};
|
|
56
61
|
}
|
|
62
|
+
async function fetchStatusUsage(connected, io) {
|
|
63
|
+
const token = connected ? resolveStatusToken() : undefined;
|
|
64
|
+
if (!token) {
|
|
65
|
+
return { state: "anonymous" };
|
|
66
|
+
}
|
|
67
|
+
const account = await fetchAccountStatus(token, process.env, io.fetchImpl ?? fetch, io.usageTimeoutMs ?? STATUS_USAGE_TIMEOUT_MS);
|
|
68
|
+
if (!account || account.scansUsed === null) {
|
|
69
|
+
return { state: "unavailable" };
|
|
70
|
+
}
|
|
71
|
+
return { state: "ok", used: account.scansUsed, limit: account.scansLimit };
|
|
72
|
+
}
|
|
73
|
+
function resolveStatusToken() {
|
|
74
|
+
try {
|
|
75
|
+
return envAuthToken(process.env) ?? readAuthState()?.token;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
57
81
|
function safeCommitGuard() {
|
|
58
82
|
try {
|
|
59
83
|
return gitHookStatusState();
|
|
@@ -91,6 +115,7 @@ function renderStatus(report, theme) {
|
|
|
91
115
|
"Dependency Guardian status",
|
|
92
116
|
"",
|
|
93
117
|
` Account ${account}`,
|
|
118
|
+
` Usage ${usageLine(report, theme)}`,
|
|
94
119
|
` Installs ${bare}`
|
|
95
120
|
];
|
|
96
121
|
const guard = commitGuardLine(report.commitGuard, theme);
|
|
@@ -102,6 +127,15 @@ function renderStatus(report, theme) {
|
|
|
102
127
|
lines.push(`Full diagnostics: ${theme.paint("accent", "dg doctor")}`);
|
|
103
128
|
return `${lines.join("\n")}\n`;
|
|
104
129
|
}
|
|
130
|
+
function usageLine(report, theme) {
|
|
131
|
+
if (report.usage.state === "anonymous") {
|
|
132
|
+
return theme.paint("muted", "sign in to see usage");
|
|
133
|
+
}
|
|
134
|
+
if (report.usage.state === "unavailable") {
|
|
135
|
+
return theme.paint("muted", "unavailable offline");
|
|
136
|
+
}
|
|
137
|
+
return formatUsage({ used: report.usage.used, limit: report.usage.limit, tier: report.account.tier ?? "" }).text;
|
|
138
|
+
}
|
|
105
139
|
function commitGuardLine(state, theme) {
|
|
106
140
|
if (state === "not-a-repo") {
|
|
107
141
|
return null;
|