@westbayberry/dg 2.0.8 → 2.0.11
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/launcher/install-preflight.js +81 -12
- package/dist/launcher/output-redaction.js +5 -3
- 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 +45 -32
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +75 -7
- 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 +21 -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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Dependency Guardian's command-line scanner. It checks the packages you are
|
|
4
4
|
about to install or publish and tells you whether Dependency Guardian
|
|
5
|
-
verified them, flagged them, or blocked them
|
|
5
|
+
verified them, flagged them, or blocked them before they reach your machine.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -29,7 +29,7 @@ the project, 2 means DG blocked it.
|
|
|
29
29
|
## Core concepts
|
|
30
30
|
|
|
31
31
|
**Verdicts.** Every result is one of three states: **PASS** (DG verified),
|
|
32
|
-
**WARN** (DG flagged
|
|
32
|
+
**WARN** (DG flagged, review advised), or **BLOCK** (DG blocked, do not
|
|
33
33
|
install). The exit code follows the verdict.
|
|
34
34
|
|
|
35
35
|
**The server is the verdict source.** For authenticated registry checks and the
|
|
@@ -91,7 +91,7 @@ dg guard-commit install
|
|
|
91
91
|
### dg scan [path]
|
|
92
92
|
|
|
93
93
|
Scans package manifests and supported lockfiles for the project at `path` (the
|
|
94
|
-
current directory by default). Reads manifests only
|
|
94
|
+
current directory by default). Reads manifests only; it never runs package
|
|
95
95
|
scripts, installs dependencies, loads project-local config, or changes setup
|
|
96
96
|
state.
|
|
97
97
|
|
|
@@ -118,8 +118,8 @@ Two paths, one exit-code contract.
|
|
|
118
118
|
|
|
119
119
|
### dg audit [path]
|
|
120
120
|
|
|
121
|
-
Inspects exactly the resolved publish set of one package
|
|
122
|
-
repo
|
|
121
|
+
Inspects exactly the resolved publish set of one package, never the whole
|
|
122
|
+
repo, for leaked secrets, private keys, source-control and build-artifact
|
|
123
123
|
leakage, and suspicious lifecycle-script strings. Basic checks run 100% locally
|
|
124
124
|
and upload nothing. On a paid plan, with org policy allowing it and only after
|
|
125
125
|
explicit consent, the deep audit uploads a packed copy of an npm package to the
|
|
@@ -184,7 +184,7 @@ Installs a per-repo, reversible git pre-commit hook that scans staged changes.
|
|
|
184
184
|
Explicit, reversible service mode: `dg service start | stop | restart | status
|
|
185
185
|
| doctor | uninstall`, plus consent-gated `dg service trust install |
|
|
186
186
|
uninstall`. Trust records store only public certificate fingerprints, provider,
|
|
187
|
-
target, and timestamps
|
|
187
|
+
target, and timestamps, never private keys. `dg service status`/`doctor`
|
|
188
188
|
detect stale runtime state and `dg service restart` clears it.
|
|
189
189
|
|
|
190
190
|
### dg login / dg logout
|
|
@@ -233,18 +233,23 @@ set`.
|
|
|
233
233
|
Project-local allowlists never suppress an install firewall block unless
|
|
234
234
|
user-global or org policy explicitly trusts them.
|
|
235
235
|
|
|
236
|
+
Telemetry is disabled by default. When enabled, the CLI appends command and
|
|
237
|
+
install-decision events to a local `telemetry.jsonl` file in the dg state
|
|
238
|
+
directory, limited to a fixed attribute allowlist. Nothing is transmitted
|
|
239
|
+
anywhere; the file never leaves your machine.
|
|
240
|
+
|
|
236
241
|
### Environment variables
|
|
237
242
|
|
|
238
|
-
- `DG_API_TOKEN
|
|
239
|
-
- `DG_PRIVATE_REGISTRY_HOSTS
|
|
243
|
+
- `DG_API_TOKEN`: auth token; overrides the token stored in the config file.
|
|
244
|
+
- `DG_PRIVATE_REGISTRY_HOSTS`: comma-separated hosts whose URL-fallback
|
|
240
245
|
artifacts are eligible for private-registry scan upload.
|
|
241
|
-
- `DG_SCAN_TARBALL_UPLOAD
|
|
246
|
+
- `DG_SCAN_TARBALL_UPLOAD`: set to `1` to enable private-registry artifact
|
|
242
247
|
upload to `/v1/scan-tarball` (requires `DG_API_TOKEN`).
|
|
243
|
-
- `DG_SERVICE_TRUST_STORE_BACKEND` / `DG_SERVICE_TRUST_STORE_DIR
|
|
248
|
+
- `DG_SERVICE_TRUST_STORE_BACKEND` / `DG_SERVICE_TRUST_STORE_DIR`: for
|
|
244
249
|
CI/Docker, set the backend to `file` and a directory to copy the active
|
|
245
250
|
public CA certificate into an image-local trust directory without touching
|
|
246
251
|
the host OS trust store.
|
|
247
|
-
- `NO_COLOR` / `FORCE_COLOR
|
|
252
|
+
- `NO_COLOR` / `FORCE_COLOR`: disable or force ANSI color output.
|
|
248
253
|
|
|
249
254
|
## Exit codes
|
|
250
255
|
|
|
@@ -272,7 +277,7 @@ Per-command specifics:
|
|
|
272
277
|
|
|
273
278
|
## Troubleshooting
|
|
274
279
|
|
|
275
|
-
Run `dg doctor` first
|
|
280
|
+
Run `dg doctor` first; it reports Node version, package health, config
|
|
276
281
|
readability, shim/PATH state, and which optional gates are unavailable.
|
|
277
282
|
|
|
278
283
|
- "Scanner unavailable" / `scannerUnavailable` in JSON: the API was
|
package/dist/api/analyze.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { readAuthStateOrWarn } from "../auth/store.js";
|
|
5
5
|
import { envAuthToken } from "../auth/env-token.js";
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
7
|
import { sanitize, sanitizeResponse } from "../security/sanitize.js";
|
|
@@ -10,12 +10,81 @@ import { dgVersion } from "../commands/version.js";
|
|
|
10
10
|
export class AnalyzeError extends Error {
|
|
11
11
|
statusCode;
|
|
12
12
|
body;
|
|
13
|
-
|
|
13
|
+
code;
|
|
14
|
+
scansUsed;
|
|
15
|
+
scansLimit;
|
|
16
|
+
constructor(message, statusCode, body, code) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.statusCode = statusCode;
|
|
16
19
|
this.body = body;
|
|
17
20
|
this.name = "AnalyzeError";
|
|
21
|
+
const quota = quotaBodyFields(body);
|
|
22
|
+
if (quota?.scansUsed !== undefined) {
|
|
23
|
+
this.scansUsed = quota.scansUsed;
|
|
24
|
+
}
|
|
25
|
+
if (quota?.scansLimit !== undefined) {
|
|
26
|
+
this.scansLimit = quota.scansLimit;
|
|
27
|
+
}
|
|
28
|
+
this.code = code ?? classifyAnalyzeError(statusCode, body, quota !== undefined);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function quotaBodyFields(body) {
|
|
32
|
+
if (!body || typeof body !== "object") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const candidate = body;
|
|
36
|
+
const quotaShaped = candidate.code === "quota_exceeded"
|
|
37
|
+
|| candidate.reason === "monthly_limit"
|
|
38
|
+
|| candidate.reason === "prefix_cap"
|
|
39
|
+
|| typeof candidate.scansUsed === "number"
|
|
40
|
+
|| typeof candidate.scansLimit === "number"
|
|
41
|
+
|| typeof candidate.maxScans === "number";
|
|
42
|
+
if (!quotaShaped) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const limit = typeof candidate.scansLimit === "number"
|
|
46
|
+
? candidate.scansLimit
|
|
47
|
+
: typeof candidate.maxScans === "number"
|
|
48
|
+
? candidate.maxScans
|
|
49
|
+
: undefined;
|
|
50
|
+
return {
|
|
51
|
+
...(typeof candidate.scansUsed === "number" ? { scansUsed: candidate.scansUsed } : {}),
|
|
52
|
+
...(limit !== undefined ? { scansLimit: limit } : {})
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function classifyAnalyzeError(statusCode, body, quotaShaped) {
|
|
56
|
+
const bodyCode = body && typeof body === "object" ? body.code : undefined;
|
|
57
|
+
if (bodyCode === "rate_limited") {
|
|
58
|
+
return "rate_limited";
|
|
59
|
+
}
|
|
60
|
+
if (statusCode === 402 || (quotaShaped && (statusCode === 403 || statusCode === 429))) {
|
|
61
|
+
return "quota_exceeded";
|
|
62
|
+
}
|
|
63
|
+
if (statusCode === 429) {
|
|
64
|
+
return "rate_limited";
|
|
65
|
+
}
|
|
66
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
67
|
+
return "auth";
|
|
68
|
+
}
|
|
69
|
+
if (statusCode === 0) {
|
|
70
|
+
return "network";
|
|
18
71
|
}
|
|
72
|
+
return "server";
|
|
73
|
+
}
|
|
74
|
+
export function scannerErrorFromUnknown(error) {
|
|
75
|
+
if (error instanceof AnalyzeError) {
|
|
76
|
+
return {
|
|
77
|
+
kind: error.code,
|
|
78
|
+
message: error.message,
|
|
79
|
+
statusCode: error.statusCode,
|
|
80
|
+
...(error.scansUsed !== undefined ? { scansUsed: error.scansUsed } : {}),
|
|
81
|
+
...(error.scansLimit !== undefined ? { scansLimit: error.scansLimit } : {})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
kind: "worker",
|
|
86
|
+
message: error instanceof Error ? error.message : String(error)
|
|
87
|
+
};
|
|
19
88
|
}
|
|
20
89
|
const ANALYZE_PATHS = {
|
|
21
90
|
npm: "/v1/analyze",
|
|
@@ -26,12 +95,16 @@ const DEFAULT_TIMEOUT_MS = 180_000;
|
|
|
26
95
|
const BATCH_CONCURRENCY = Math.max(1, Number(process.env.DG_ANALYZE_CONCURRENCY) || 4);
|
|
27
96
|
export async function analyzePackages(packages, options) {
|
|
28
97
|
const env = options.env ?? process.env;
|
|
29
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
30
98
|
const baseUrl = resolveApiBaseUrl(env);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
99
|
+
const context = {
|
|
100
|
+
url: `${baseUrl}${ANALYZE_PATHS[options.ecosystem]}`,
|
|
101
|
+
token: resolveToken(env),
|
|
102
|
+
deviceId: getOrCreateDeviceId(env),
|
|
103
|
+
scanId: options.scanId ?? randomUUID(),
|
|
104
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
105
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
106
|
+
...(options.signal ? { signal: options.signal } : {})
|
|
107
|
+
};
|
|
35
108
|
const total = packages.length;
|
|
36
109
|
const batches = [];
|
|
37
110
|
for (let index = 0; index < packages.length; index += BATCH_SIZE) {
|
|
@@ -52,7 +125,7 @@ export async function analyzePackages(packages, options) {
|
|
|
52
125
|
const batch = batches[i];
|
|
53
126
|
if (!batch)
|
|
54
127
|
continue;
|
|
55
|
-
const response = await analyzeBatchWithRetry(
|
|
128
|
+
const response = await analyzeBatchWithRetry(context, batch, (batchDone) => {
|
|
56
129
|
perBatchDone[i] = Math.min(batchDone, batch.length);
|
|
57
130
|
reportProgress();
|
|
58
131
|
});
|
|
@@ -67,21 +140,21 @@ export async function analyzePackages(packages, options) {
|
|
|
67
140
|
return mergeAnalyzeResponses(responses);
|
|
68
141
|
}
|
|
69
142
|
const MAX_BATCH_ATTEMPTS = 3;
|
|
70
|
-
async function analyzeBatchWithRetry(
|
|
143
|
+
async function analyzeBatchWithRetry(context, batch, onBatchProgress) {
|
|
71
144
|
let lastError;
|
|
72
145
|
for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt += 1) {
|
|
73
146
|
try {
|
|
74
|
-
return await analyzeBatch(
|
|
147
|
+
return await analyzeBatch(context, batch, onBatchProgress);
|
|
75
148
|
}
|
|
76
149
|
catch (error) {
|
|
77
150
|
lastError = error;
|
|
78
|
-
if (attempt === MAX_BATCH_ATTEMPTS || !isRetryableAnalyzeError(error)) {
|
|
151
|
+
if (context.signal?.aborted || attempt === MAX_BATCH_ATTEMPTS || !isRetryableAnalyzeError(error)) {
|
|
79
152
|
break;
|
|
80
153
|
}
|
|
81
154
|
await delay(300 * attempt);
|
|
82
155
|
}
|
|
83
156
|
}
|
|
84
|
-
if (lastError instanceof AnalyzeError) {
|
|
157
|
+
if (context.signal?.aborted || lastError instanceof AnalyzeError) {
|
|
85
158
|
throw lastError;
|
|
86
159
|
}
|
|
87
160
|
if (isNetworkFailure(lastError)) {
|
|
@@ -104,9 +177,24 @@ function isRetryableAnalyzeError(error) {
|
|
|
104
177
|
function delay(ms) {
|
|
105
178
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
179
|
}
|
|
107
|
-
async function analyzeBatch(
|
|
180
|
+
async function analyzeBatch(context, batch, onBatchProgress) {
|
|
181
|
+
const { url, token, deviceId, scanId, fetchImpl, timeoutMs, signal } = context;
|
|
108
182
|
const controller = new AbortController();
|
|
109
|
-
|
|
183
|
+
let timedOut = false;
|
|
184
|
+
let silenceTimer;
|
|
185
|
+
const armSilenceTimer = () => {
|
|
186
|
+
clearTimeout(silenceTimer);
|
|
187
|
+
silenceTimer = setTimeout(() => {
|
|
188
|
+
timedOut = true;
|
|
189
|
+
controller.abort();
|
|
190
|
+
}, timeoutMs);
|
|
191
|
+
};
|
|
192
|
+
const forwardAbort = () => controller.abort();
|
|
193
|
+
signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
194
|
+
if (signal?.aborted) {
|
|
195
|
+
controller.abort();
|
|
196
|
+
}
|
|
197
|
+
armSilenceTimer();
|
|
110
198
|
try {
|
|
111
199
|
const response = await fetchImpl(url, {
|
|
112
200
|
method: "POST",
|
|
@@ -114,6 +202,7 @@ async function analyzeBatch(url, batch, token, deviceId, fetchImpl, timeoutMs, o
|
|
|
114
202
|
"Content-Type": "application/json",
|
|
115
203
|
"Accept": "application/x-ndjson",
|
|
116
204
|
"X-Device-Id": deviceId,
|
|
205
|
+
"X-Scan-Id": scanId,
|
|
117
206
|
"X-Dg-Version": dgVersion(),
|
|
118
207
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
119
208
|
},
|
|
@@ -122,31 +211,49 @@ async function analyzeBatch(url, batch, token, deviceId, fetchImpl, timeoutMs, o
|
|
|
122
211
|
}),
|
|
123
212
|
signal: controller.signal
|
|
124
213
|
});
|
|
214
|
+
armSilenceTimer();
|
|
125
215
|
if (!response.ok) {
|
|
126
216
|
const body = await response.json().catch(() => undefined);
|
|
127
|
-
|
|
128
|
-
? sanitize(body.error)
|
|
129
|
-
: `scanner returned ${response.status}`;
|
|
130
|
-
throw new AnalyzeError(serverMessage, response.status, body);
|
|
217
|
+
throw analyzeErrorFromResponse(response.status, body);
|
|
131
218
|
}
|
|
132
219
|
const headers = response.headers;
|
|
133
220
|
const contentType = headers?.get("content-type") ?? "";
|
|
134
221
|
if (contentType.includes("application/x-ndjson") && response.body) {
|
|
135
|
-
return await consumeAnalyzeStream(response.body, onBatchProgress);
|
|
222
|
+
return await consumeAnalyzeStream(response.body, onBatchProgress, armSilenceTimer);
|
|
136
223
|
}
|
|
137
|
-
|
|
224
|
+
const payload = await response.json().catch(() => {
|
|
225
|
+
throw new AnalyzeError("scanner returned an unreadable response", response.status, undefined, "invalid_response");
|
|
226
|
+
});
|
|
227
|
+
return normalizeAnalyzeResponse(payload);
|
|
138
228
|
}
|
|
139
229
|
catch (error) {
|
|
140
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
141
|
-
throw new AnalyzeError(`scanner
|
|
230
|
+
if (timedOut && error instanceof Error && error.name === "AbortError") {
|
|
231
|
+
throw new AnalyzeError(`scanner sent no data for ${timeoutMs}ms — scan timed out`, 0, undefined, "timeout");
|
|
142
232
|
}
|
|
143
233
|
throw error;
|
|
144
234
|
}
|
|
145
235
|
finally {
|
|
146
|
-
clearTimeout(
|
|
236
|
+
clearTimeout(silenceTimer);
|
|
237
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
147
238
|
}
|
|
148
239
|
}
|
|
149
|
-
|
|
240
|
+
function analyzeErrorFromResponse(status, body) {
|
|
241
|
+
const serverMessage = body && typeof body === "object" && "error" in body && typeof body.error === "string"
|
|
242
|
+
? sanitize(body.error)
|
|
243
|
+
: undefined;
|
|
244
|
+
const probe = new AnalyzeError(serverMessage ?? `scanner returned ${status}`, status, body);
|
|
245
|
+
if (serverMessage) {
|
|
246
|
+
return probe;
|
|
247
|
+
}
|
|
248
|
+
if (probe.code === "quota_exceeded") {
|
|
249
|
+
return new AnalyzeError("scan limit reached", status, body);
|
|
250
|
+
}
|
|
251
|
+
if (probe.code === "rate_limited") {
|
|
252
|
+
return new AnalyzeError("scanner rate limit reached — wait a moment and retry", status, body);
|
|
253
|
+
}
|
|
254
|
+
return probe;
|
|
255
|
+
}
|
|
256
|
+
async function consumeAnalyzeStream(body, onBatchProgress, onActivity) {
|
|
150
257
|
const reader = body.getReader();
|
|
151
258
|
const decoder = new TextDecoder();
|
|
152
259
|
let buffer = "";
|
|
@@ -177,6 +284,7 @@ async function consumeAnalyzeStream(body, onBatchProgress) {
|
|
|
177
284
|
const { done, value } = await reader.read();
|
|
178
285
|
if (done)
|
|
179
286
|
break;
|
|
287
|
+
onActivity();
|
|
180
288
|
buffer += decoder.decode(value, { stream: true });
|
|
181
289
|
let newline;
|
|
182
290
|
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
@@ -255,14 +363,14 @@ export function mergeAnalyzeResponses(responses) {
|
|
|
255
363
|
}));
|
|
256
364
|
}
|
|
257
365
|
function resolveApiBaseUrl(env) {
|
|
258
|
-
const auth =
|
|
366
|
+
const auth = readAuthStateOrWarn(env);
|
|
259
367
|
if (auth?.apiBaseUrl) {
|
|
260
368
|
return auth.apiBaseUrl;
|
|
261
369
|
}
|
|
262
370
|
return loadUserConfig(env).api.baseUrl;
|
|
263
371
|
}
|
|
264
372
|
function resolveToken(env) {
|
|
265
|
-
return envAuthToken(env) ??
|
|
373
|
+
return envAuthToken(env) ?? readAuthStateOrWarn(env)?.token;
|
|
266
374
|
}
|
|
267
375
|
export function identityHeaders(env) {
|
|
268
376
|
const headers = { "X-Device-Id": getOrCreateDeviceId(env) };
|
|
@@ -272,14 +380,6 @@ export function identityHeaders(env) {
|
|
|
272
380
|
}
|
|
273
381
|
return headers;
|
|
274
382
|
}
|
|
275
|
-
function readAuthStateSafe(env) {
|
|
276
|
-
try {
|
|
277
|
-
return readAuthState(env);
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
383
|
function getOrCreateDeviceId(env) {
|
|
284
384
|
const path = join(resolveDgPaths(env).stateDir, "device-id");
|
|
285
385
|
try {
|
package/dist/audit-ui/export.js
CHANGED
|
@@ -33,14 +33,13 @@ function buildMd(input) {
|
|
|
33
33
|
`**Verdict:** ${input.action.toUpperCase()} · ${input.ecosystem}`,
|
|
34
34
|
`**Files:** ${input.fileCount} · ${countSummaryLine(input.findings)}`,
|
|
35
35
|
input.publishSetSource === "fallback" ? "**Publish set approximated**" : "",
|
|
36
|
-
`**Deep behavioral scan:** ${deepSummary(input.deep)}
|
|
37
|
-
""
|
|
36
|
+
`**Deep behavioral scan:** ${deepSummary(input.deep)}`
|
|
38
37
|
].filter((line, index) => line !== "" || index === 1);
|
|
39
38
|
if (input.findings.length === 0) {
|
|
40
|
-
lines.push("> No findings — the publish set is clean.", "");
|
|
39
|
+
lines.push("", "> No findings — the publish set is clean.", "");
|
|
41
40
|
return `${lines.join("\n")}\n`;
|
|
42
41
|
}
|
|
43
|
-
lines.push("| Severity | Location | Title | Evidence | Recommendation |", "|---|---|---|---|---|");
|
|
42
|
+
lines.push("", "| Severity | Location | Title | Evidence | Recommendation |", "|---|---|---|---|---|");
|
|
44
43
|
for (const finding of input.findings) {
|
|
45
44
|
const kind = severityKind(finding.severity);
|
|
46
45
|
const sev = kind === "block" ? "BLOCK" : kind === "warn" ? "WARN" : "NOTE";
|
|
@@ -12,7 +12,8 @@ export function resolveWebBase(env) {
|
|
|
12
12
|
if (override) {
|
|
13
13
|
try {
|
|
14
14
|
const url = new URL(override);
|
|
15
|
-
|
|
15
|
+
const localhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
16
|
+
if (url.protocol === "https:" || (url.protocol === "http:" && localhost)) {
|
|
16
17
|
return override.replace(/\/$/, "");
|
|
17
18
|
}
|
|
18
19
|
}
|
|
@@ -169,7 +170,7 @@ function waitForEnter() {
|
|
|
169
170
|
closeSync(tty);
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
export async function
|
|
173
|
+
export async function fetchAccountStatus(token, env, fetchImpl, timeoutMs = 5_000) {
|
|
173
174
|
let apiBase;
|
|
174
175
|
try {
|
|
175
176
|
apiBase = loadUserConfig(env).api.baseUrl.replace(/\/$/, "");
|
|
@@ -178,7 +179,7 @@ export async function fetchAccountTier(token, env, fetchImpl) {
|
|
|
178
179
|
return null;
|
|
179
180
|
}
|
|
180
181
|
const controller = new AbortController();
|
|
181
|
-
const timer = setTimeout(() => controller.abort(),
|
|
182
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
182
183
|
try {
|
|
183
184
|
const response = await fetchImpl(`${apiBase}/v1/auth/status`, {
|
|
184
185
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -188,10 +189,11 @@ export async function fetchAccountTier(token, env, fetchImpl) {
|
|
|
188
189
|
return null;
|
|
189
190
|
}
|
|
190
191
|
const body = (await response.json());
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
return {
|
|
193
|
+
tier: typeof body.tier === "string" && body.tier.length > 0 ? body.tier.toLowerCase() : null,
|
|
194
|
+
scansUsed: typeof body.scansUsed === "number" && Number.isFinite(body.scansUsed) ? body.scansUsed : null,
|
|
195
|
+
scansLimit: typeof body.scansLimit === "number" && Number.isFinite(body.scansLimit) ? body.scansLimit : null
|
|
196
|
+
};
|
|
195
197
|
}
|
|
196
198
|
catch {
|
|
197
199
|
return null;
|
|
@@ -200,6 +202,9 @@ export async function fetchAccountTier(token, env, fetchImpl) {
|
|
|
200
202
|
clearTimeout(timer);
|
|
201
203
|
}
|
|
202
204
|
}
|
|
205
|
+
export async function fetchAccountTier(token, env, fetchImpl) {
|
|
206
|
+
return (await fetchAccountStatus(token, env, fetchImpl))?.tier ?? null;
|
|
207
|
+
}
|
|
203
208
|
export async function runDeviceLogin(io = {}) {
|
|
204
209
|
const env = io.env ?? process.env;
|
|
205
210
|
const fetchImpl = io.fetchImpl ?? fetch;
|
|
@@ -254,8 +259,7 @@ export async function maybeDeviceLogin(args) {
|
|
|
254
259
|
if (args[0] !== "login") {
|
|
255
260
|
return { handled: false, result: noop };
|
|
256
261
|
}
|
|
257
|
-
|
|
258
|
-
if (rest.some((arg) => arg === "--token" || arg.startsWith("--token=") || arg === "--help" || arg === "-h")) {
|
|
262
|
+
if (args.length > 1) {
|
|
259
263
|
return { handled: false, result: noop };
|
|
260
264
|
}
|
|
261
265
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
package/dist/auth/store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { loadUserConfig, saveUserConfig } from "../config/settings.js";
|
|
4
|
+
import { loadUserConfig, saveUserConfig, withUserConfigLock } from "../config/settings.js";
|
|
5
5
|
import { resolveDgPaths } from "../state/index.js";
|
|
6
6
|
import { envAuthToken } from "./env-token.js";
|
|
7
7
|
export class AuthError extends Error {
|
|
@@ -41,38 +41,55 @@ export function readAuthState(env = process.env) {
|
|
|
41
41
|
throw new AuthError(`Malformed dg auth state at ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
const warnedAuthPaths = new Set();
|
|
45
|
+
export function readAuthStateOrWarn(env = process.env, options = {}) {
|
|
46
|
+
try {
|
|
47
|
+
return readAuthState(env);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
const path = authPath(resolveDgPaths(env));
|
|
51
|
+
if (!warnedAuthPaths.has(path)) {
|
|
52
|
+
warnedAuthPaths.add(path);
|
|
53
|
+
const stderr = options.stderr ?? process.stderr;
|
|
54
|
+
stderr.write(`dg: auth state at ${path} is unreadable; continuing without your account. Run 'dg login' to repair it.\n`);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
44
59
|
export function writeAuthState(options, env = process.env) {
|
|
45
60
|
const token = options.token.trim();
|
|
46
61
|
if (token.length < 8) {
|
|
47
62
|
throw new AuthError("token must be at least 8 characters");
|
|
48
63
|
}
|
|
49
|
-
const config = loadUserConfig(env);
|
|
50
|
-
const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
|
|
51
|
-
const orgId = options.orgId ?? config.org.id;
|
|
52
64
|
const email = typeof options.email === "string" && options.email.length > 0 ? options.email : undefined;
|
|
53
65
|
const tier = typeof options.tier === "string" && options.tier.length > 0 ? options.tier : undefined;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
return withUserConfigLock(env, () => {
|
|
67
|
+
const config = loadUserConfig(env);
|
|
68
|
+
const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
|
|
69
|
+
const orgId = options.orgId ?? config.org.id;
|
|
70
|
+
const state = {
|
|
71
|
+
version: 1,
|
|
72
|
+
token,
|
|
73
|
+
tokenPreview: redactToken(token),
|
|
74
|
+
apiBaseUrl,
|
|
75
|
+
orgId,
|
|
76
|
+
loggedInAt: (options.now ?? new Date()).toISOString(),
|
|
77
|
+
...(email ? { email } : {}),
|
|
78
|
+
...(tier ? { tier } : {})
|
|
79
|
+
};
|
|
80
|
+
const paths = resolveDgPaths(env);
|
|
81
|
+
writeJsonAtomic(authPath(paths), state);
|
|
82
|
+
saveUserConfig({
|
|
83
|
+
...config,
|
|
84
|
+
api: {
|
|
85
|
+
baseUrl: apiBaseUrl
|
|
86
|
+
},
|
|
87
|
+
org: {
|
|
88
|
+
id: orgId
|
|
89
|
+
}
|
|
90
|
+
}, env);
|
|
91
|
+
return state;
|
|
92
|
+
});
|
|
76
93
|
}
|
|
77
94
|
export function clearAuthState(env = process.env) {
|
|
78
95
|
const path = authPath(resolveDgPaths(env));
|
package/dist/bin/dg.js
CHANGED
|
@@ -31,6 +31,11 @@ const { maybeShowFirstRun } = await import("../runtime/first-run.js");
|
|
|
31
31
|
const { maybePreflightInstallPrompt } = await import("../launcher/preflight-prompt.js");
|
|
32
32
|
const args = process.argv.slice(2);
|
|
33
33
|
maybeShowFirstRun(args);
|
|
34
|
+
import("../state/index.js")
|
|
35
|
+
.then(({ pruneDeadSessionsSync, resolveDgPaths }) => {
|
|
36
|
+
pruneDeadSessionsSync(resolveDgPaths());
|
|
37
|
+
})
|
|
38
|
+
.catch(() => { });
|
|
34
39
|
const { maybeDeviceLogin } = await import("../auth/device-login.js");
|
|
35
40
|
const { maybeVerifyPackage } = await import("../verify/package-check.js");
|
|
36
41
|
const { maybeAudit } = await import("../commands/audit.js");
|
package/dist/commands/audit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, resolve } from "node:path";
|
|
3
|
-
import { EXIT_TOOL_ERROR,
|
|
3
|
+
import { EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
4
4
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
5
5
|
import { createTheme } from "../presentation/theme.js";
|
|
6
6
|
import { actionForFindings, detectFindings, findingLocation } from "../audit/detectors.js";
|
|
@@ -30,7 +30,7 @@ export const auditCommand = {
|
|
|
30
30
|
examples: ["dg audit", "dg audit ./packages/api", "dg audit --json", "dg audit --local"],
|
|
31
31
|
details: [
|
|
32
32
|
"Audits exactly what you're about to publish — the resolved publish set of one package, never the whole repo.",
|
|
33
|
-
"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."
|
|
33
|
+
"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."
|
|
34
34
|
],
|
|
35
35
|
handler: (context) => runAuditCommand(context.args)
|
|
36
36
|
};
|
|
@@ -82,7 +82,7 @@ function finalize(gathered, deep) {
|
|
|
82
82
|
return { exitCode: exitCodeFor(report, parsed), stdout: rendered, stderr: "" };
|
|
83
83
|
}
|
|
84
84
|
function gatherError(gathered) {
|
|
85
|
-
return gathered.usage ? usageError(gathered.error) : { exitCode:
|
|
85
|
+
return gathered.usage ? usageError(gathered.error) : { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg audit: ${gathered.error}.\n` };
|
|
86
86
|
}
|
|
87
87
|
function runAuditCommand(args) {
|
|
88
88
|
const gathered = gather(args);
|
|
@@ -327,6 +327,13 @@ function parseAuditArgs(args) {
|
|
|
327
327
|
failOn = next;
|
|
328
328
|
index += 1;
|
|
329
329
|
}
|
|
330
|
+
else if (arg.startsWith("--fail-on=")) {
|
|
331
|
+
const value = arg.slice("--fail-on=".length);
|
|
332
|
+
if (value !== "warn" && value !== "block") {
|
|
333
|
+
return { error: "--fail-on requires 'warn' or 'block'" };
|
|
334
|
+
}
|
|
335
|
+
failOn = value;
|
|
336
|
+
}
|
|
330
337
|
else if (arg.startsWith("-")) {
|
|
331
338
|
return { error: `unknown option '${arg}'` };
|
|
332
339
|
}
|
|
@@ -338,6 +345,9 @@ function parseAuditArgs(args) {
|
|
|
338
345
|
sawTarget = true;
|
|
339
346
|
}
|
|
340
347
|
}
|
|
348
|
+
if (local && requireDeep) {
|
|
349
|
+
return { error: "--local skips the deep audit, so it cannot be combined with --require-deep" };
|
|
350
|
+
}
|
|
341
351
|
return { target, format, outputPath, local, requireDeep, failOn };
|
|
342
352
|
}
|
|
343
353
|
export function displayTarget(root) {
|
|
@@ -355,7 +365,7 @@ function safeReadJson(path) {
|
|
|
355
365
|
}
|
|
356
366
|
function usageError(message) {
|
|
357
367
|
return {
|
|
358
|
-
exitCode:
|
|
368
|
+
exitCode: EXIT_USAGE_VERDICT,
|
|
359
369
|
stdout: "",
|
|
360
370
|
stderr: `dg audit: ${message}. Usage: ${auditCommand.usage}\n`
|
|
361
371
|
};
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EXIT_USAGE } from "./types.js";
|
|
2
|
-
import { ConfigError, getConfigValue, isConfigKey, listConfig, loadUserConfig,
|
|
2
|
+
import { ConfigError, getConfigValue, isConfigKey, listConfig, loadUserConfig, setConfigValue, unsetConfigValue, updateUserConfig } from "../config/settings.js";
|
|
3
3
|
export const configCommand = {
|
|
4
4
|
name: "config",
|
|
5
5
|
summary: "Inspect or edit trusted dg configuration.",
|
|
@@ -54,8 +54,7 @@ function configHandler(args) {
|
|
|
54
54
|
if (value === undefined) {
|
|
55
55
|
return usageError("set requires a value");
|
|
56
56
|
}
|
|
57
|
-
const next = setConfigValue(
|
|
58
|
-
saveUserConfig(next);
|
|
57
|
+
const next = updateUserConfig((current) => setConfigValue(current, key, value));
|
|
59
58
|
return {
|
|
60
59
|
exitCode: 0,
|
|
61
60
|
stdout: `${key}=${getConfigValue(next, key)}\n`,
|
|
@@ -66,8 +65,7 @@ function configHandler(args) {
|
|
|
66
65
|
if (value) {
|
|
67
66
|
return usageError("unset accepts only a key");
|
|
68
67
|
}
|
|
69
|
-
const next = unsetConfigValue(
|
|
70
|
-
saveUserConfig(next);
|
|
68
|
+
const next = updateUserConfig((current) => unsetConfigValue(current, key));
|
|
71
69
|
return {
|
|
72
70
|
exitCode: 0,
|
|
73
71
|
stdout: `${key}=${getConfigValue(next, key)}\n`,
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EXIT_USAGE } from "./types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { doctorReportWithRemote, renderDoctorReport } from "../setup/plan.js";
|
|
3
3
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
4
4
|
import { createTheme } from "../presentation/theme.js";
|
|
5
5
|
export const doctorCommand = {
|
|
@@ -17,7 +17,7 @@ export const doctorCommand = {
|
|
|
17
17
|
],
|
|
18
18
|
handler: (context) => doctorHandler(context.args)
|
|
19
19
|
};
|
|
20
|
-
function doctorHandler(args) {
|
|
20
|
+
async function doctorHandler(args) {
|
|
21
21
|
const json = args.includes("--json");
|
|
22
22
|
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
23
23
|
const unknown = args.find((arg) => arg !== "--json" && arg !== "--verbose" && arg !== "-v");
|
|
@@ -28,7 +28,7 @@ function doctorHandler(args) {
|
|
|
28
28
|
stderr: `dg doctor: unknown option '${unknown}'. Run 'dg doctor --help'.\n`
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
const report =
|
|
31
|
+
const report = await doctorReportWithRemote();
|
|
32
32
|
const hasFailure = report.checks.some((check) => check.status === "fail");
|
|
33
33
|
const theme = createTheme(resolvePresentation().color);
|
|
34
34
|
return {
|