@westbayberry/dg 2.0.11 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +6 -4
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +26 -10
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +169 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +8 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +3 -2
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/status.js +5 -2
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +154 -65
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +92 -0
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/install-ui/prompt.js +14 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +167 -17
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +43 -3
- package/dist/launcher/run.js +100 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +117 -0
- package/dist/policy/evaluate.js +5 -21
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/provenance.js +23 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +446 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +57 -17
- package/dist/proxy/metadata-map.js +66 -4
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +473 -45
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +51 -17
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +63 -8
- package/dist/scan/scanner-report.js +42 -9
- package/dist/scan/staged.js +71 -11
- package/dist/scan-ui/LegacyApp.js +12 -16
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +46 -4
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +14 -4
- package/dist/scripts/detect.js +158 -0
- package/dist/scripts/gate.js +170 -0
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +98 -29
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/json-file.js +24 -0
- package/dist/util/report-writer.js +57 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +86 -18
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +14 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -116
- package/dist/commands/explain.js +0 -232
- package/dist/commands/unavailable.js +0 -11
- package/dist/telemetry/events.js +0 -40
package/dist/scan/command.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { writeReportAtomic } from "../util/report-writer.js";
|
|
4
|
+
import { findProjectRoot, loadDgFile, warnUnreadableDgFile } from "../project/dgfile.js";
|
|
3
5
|
import { scanProject } from "./discovery.js";
|
|
4
6
|
import { renderJsonReport, renderSarifReport, renderTextReport } from "./render.js";
|
|
5
7
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
@@ -9,7 +11,7 @@ import { runScannerScan } from "./scanner-report.js";
|
|
|
9
11
|
import { runStagedScan, stagedScanReport } from "./staged.js";
|
|
10
12
|
import { scanExitCode } from "../scan-ui/shims.js";
|
|
11
13
|
import { loadUserConfig } from "../config/settings.js";
|
|
12
|
-
import { EXIT_USAGE_VERDICT } from "../commands/types.js";
|
|
14
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_NOTHING_TO_SCAN, EXIT_USAGE_VERDICT } from "../commands/types.js";
|
|
13
15
|
export function runScanCommand(context) {
|
|
14
16
|
const parsed = parseScanArgs(context.args);
|
|
15
17
|
if ("error" in parsed) {
|
|
@@ -18,12 +20,12 @@ export function runScanCommand(context) {
|
|
|
18
20
|
const stagedTarget = parsed.sawTarget ? parsed.targetPath : null;
|
|
19
21
|
const machineOutput = parsed.format !== "text" || parsed.outputPath !== null;
|
|
20
22
|
if (parsed.staged && !machineOutput) {
|
|
21
|
-
return runStagedScan({ hook: parsed.hook, targetPath: stagedTarget });
|
|
23
|
+
return runStagedScan({ hook: parsed.hook, targetPath: stagedTarget, useDecisions: !parsed.noDecisions });
|
|
22
24
|
}
|
|
23
25
|
let report;
|
|
24
26
|
let outcome;
|
|
25
27
|
if (parsed.staged) {
|
|
26
|
-
const staged = stagedScanReport({ targetPath: stagedTarget });
|
|
28
|
+
const staged = stagedScanReport({ targetPath: stagedTarget, useDecisions: !parsed.noDecisions });
|
|
27
29
|
if ("result" in staged) {
|
|
28
30
|
return staged.result;
|
|
29
31
|
}
|
|
@@ -53,12 +55,12 @@ export function runScanCommand(context) {
|
|
|
53
55
|
}
|
|
54
56
|
catch (error) {
|
|
55
57
|
return {
|
|
56
|
-
exitCode:
|
|
58
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
57
59
|
stdout: "",
|
|
58
60
|
stderr: `dg scan failed: ${error instanceof Error ? error.message : "unknown scan error"}\n`
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
|
-
outcome = runScannerScan(parsed.targetPath, report);
|
|
63
|
+
outcome = runScannerScan(parsed.targetPath, report, process.env, parsed.noDecisions ? null : loadScanDecisions(parsed.targetPath));
|
|
62
64
|
}
|
|
63
65
|
if (outcome.kind === "report") {
|
|
64
66
|
report = outcome.report;
|
|
@@ -68,26 +70,32 @@ export function runScanCommand(context) {
|
|
|
68
70
|
}
|
|
69
71
|
const skipNotice = skipNoticeFor(outcome, report);
|
|
70
72
|
const scannerUnavailable = !report.scanner && report.summary.projectCount > 0;
|
|
71
|
-
const
|
|
73
|
+
const nothingToScan = !parsed.staged &&
|
|
74
|
+
!report.scanner &&
|
|
75
|
+
!report.scannerError &&
|
|
76
|
+
report.summary.projectCount === 0 &&
|
|
77
|
+
report.summary.errorCount === 0;
|
|
78
|
+
const exitCode = nothingToScan ? EXIT_NOTHING_TO_SCAN : exitCodeForReport(report);
|
|
79
|
+
const rendered = renderReport(report, parsed.format, scannerUnavailable, skipNotice, nothingToScan);
|
|
72
80
|
if (parsed.outputPath) {
|
|
73
81
|
try {
|
|
74
|
-
|
|
82
|
+
writeReportAtomic(resolve(parsed.outputPath), rendered);
|
|
75
83
|
}
|
|
76
84
|
catch (error) {
|
|
77
85
|
return {
|
|
78
|
-
exitCode:
|
|
86
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
79
87
|
stdout: "",
|
|
80
88
|
stderr: `dg scan could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
81
89
|
};
|
|
82
90
|
}
|
|
83
91
|
return {
|
|
84
|
-
exitCode
|
|
92
|
+
exitCode,
|
|
85
93
|
stdout: `Wrote ${parsed.format} scan report to ${parsed.outputPath}\n`,
|
|
86
94
|
stderr: ""
|
|
87
95
|
};
|
|
88
96
|
}
|
|
89
97
|
return {
|
|
90
|
-
exitCode
|
|
98
|
+
exitCode,
|
|
91
99
|
stdout: rendered,
|
|
92
100
|
stderr: ""
|
|
93
101
|
};
|
|
@@ -99,6 +107,7 @@ function parseScanArgs(args) {
|
|
|
99
107
|
let sawTarget = false;
|
|
100
108
|
let staged = false;
|
|
101
109
|
let hook = false;
|
|
110
|
+
let noDecisions = false;
|
|
102
111
|
for (let index = 0; index < args.length; index += 1) {
|
|
103
112
|
const arg = args[index];
|
|
104
113
|
if (!arg) {
|
|
@@ -112,6 +121,10 @@ function parseScanArgs(args) {
|
|
|
112
121
|
hook = true;
|
|
113
122
|
continue;
|
|
114
123
|
}
|
|
124
|
+
if (arg === "--no-decisions") {
|
|
125
|
+
noDecisions = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
115
128
|
if (arg === "--json") {
|
|
116
129
|
if (format !== "text") {
|
|
117
130
|
return { error: "choose only one output format" };
|
|
@@ -150,16 +163,35 @@ function parseScanArgs(args) {
|
|
|
150
163
|
targetPath,
|
|
151
164
|
sawTarget,
|
|
152
165
|
staged,
|
|
153
|
-
hook
|
|
166
|
+
hook,
|
|
167
|
+
noDecisions
|
|
154
168
|
};
|
|
155
169
|
}
|
|
156
170
|
function usageError(message) {
|
|
157
171
|
return {
|
|
158
172
|
exitCode: EXIT_USAGE_VERDICT,
|
|
159
173
|
stdout: "",
|
|
160
|
-
stderr: `dg scan: ${message}. Usage: dg scan [path] [--json|--sarif] [--output <path>]\n`
|
|
174
|
+
stderr: `dg scan: ${message}. Usage: dg scan [path] [--json|--sarif] [--output <path>] [--no-decisions]\n`
|
|
161
175
|
};
|
|
162
176
|
}
|
|
177
|
+
function loadScanDecisions(targetPath, env = process.env) {
|
|
178
|
+
let dir = resolve(targetPath);
|
|
179
|
+
try {
|
|
180
|
+
if (!statSync(dir).isDirectory()) {
|
|
181
|
+
dir = dirname(dir);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const root = findProjectRoot(dir, env);
|
|
188
|
+
if (!root) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const file = loadDgFile(root);
|
|
192
|
+
warnUnreadableDgFile(file);
|
|
193
|
+
return file.readable ? file : null;
|
|
194
|
+
}
|
|
163
195
|
function degradeReport(report, error) {
|
|
164
196
|
const status = report.status === "block" || report.status === "warn" ? report.status : "unknown";
|
|
165
197
|
return { ...report, status, scannerError: error };
|
|
@@ -173,9 +205,9 @@ function skipNoticeFor(outcome, report) {
|
|
|
173
205
|
}
|
|
174
206
|
return "empty_lockfile";
|
|
175
207
|
}
|
|
176
|
-
function renderReport(report, format, scannerUnavailable, skipNotice) {
|
|
208
|
+
function renderReport(report, format, scannerUnavailable, skipNotice, nothingToScan) {
|
|
177
209
|
if (format === "json") {
|
|
178
|
-
return renderJsonReport(report, scannerUnavailable);
|
|
210
|
+
return renderJsonReport(report, scannerUnavailable, nothingToScan);
|
|
179
211
|
}
|
|
180
212
|
if (format === "sarif") {
|
|
181
213
|
return renderSarifReport(report);
|
|
@@ -184,7 +216,9 @@ function renderReport(report, format, scannerUnavailable, skipNotice) {
|
|
|
184
216
|
}
|
|
185
217
|
function exitCodeForReport(report) {
|
|
186
218
|
if (report.scanner) {
|
|
187
|
-
|
|
219
|
+
const mode = loadUserConfig().policy.mode;
|
|
220
|
+
const action = mode === "strict" ? report.scanner.action : report.decisions?.effectiveAction ?? report.scanner.action;
|
|
221
|
+
return scanExitCode(action, mode);
|
|
188
222
|
}
|
|
189
223
|
if (report.status === "block") {
|
|
190
224
|
return 2;
|
package/dist/scan/discovery.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { resolveToolPath } from "../util/external-tool.js";
|
|
4
5
|
const IGNORED_DIRECTORIES = new Set([
|
|
5
6
|
".git",
|
|
6
7
|
".hg",
|
|
@@ -113,8 +114,12 @@ export function gitIgnoredDirectories(root) {
|
|
|
113
114
|
return ignored;
|
|
114
115
|
}
|
|
115
116
|
function insideGitWorkTree(root) {
|
|
117
|
+
const git = resolveToolPath("git");
|
|
118
|
+
if (!git) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
116
121
|
try {
|
|
117
|
-
const out = execFileSync(
|
|
122
|
+
const out = execFileSync(git, ["-C", root, "rev-parse", "--is-inside-work-tree"], {
|
|
118
123
|
encoding: "utf8",
|
|
119
124
|
timeout: 3000,
|
|
120
125
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -126,8 +131,12 @@ function insideGitWorkTree(root) {
|
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
function checkIgnoreBatch(root, directories) {
|
|
134
|
+
const git = resolveToolPath("git");
|
|
135
|
+
if (!git) {
|
|
136
|
+
return new Set();
|
|
137
|
+
}
|
|
129
138
|
try {
|
|
130
|
-
const out = execFileSync(
|
|
139
|
+
const out = execFileSync(git, ["-C", root, "check-ignore", "--stdin", "-z"], {
|
|
131
140
|
input: directories.join("\0"),
|
|
132
141
|
encoding: "utf8",
|
|
133
142
|
timeout: 5000,
|
package/dist/scan/render.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createTheme } from "../presentation/theme.js";
|
|
2
|
+
import { packagePageUrl } from "../presentation/package-page.js";
|
|
3
|
+
import { sanitizeDeep } from "../security/sanitize.js";
|
|
2
4
|
const DETAIL_PROJECT_LIMIT = 8;
|
|
3
5
|
const CLEAN_PROJECT_PREVIEW_LIMIT = 5;
|
|
4
6
|
const FINDING_GROUP_PREVIEW_LIMIT = 3;
|
|
@@ -16,22 +18,29 @@ const SCANNER_SKIP_MESSAGES = {
|
|
|
16
18
|
no_lockfile: "no lockfile found — server verification skipped (local heuristics only)",
|
|
17
19
|
empty_lockfile: "lockfile contains no scannable packages — server verification skipped (local heuristics only)"
|
|
18
20
|
};
|
|
19
|
-
export function renderTextReport(
|
|
21
|
+
export function renderTextReport(rawReport, terminalWidth = terminalWidthFromEnv(), theme = createTheme(false), scannerNotice) {
|
|
22
|
+
const report = sanitizeDeep(rawReport);
|
|
20
23
|
const width = Math.max(48, Math.min(terminalWidth, 140));
|
|
21
24
|
const cleanProjects = report.projects.filter((project) => project.findings.length === 0);
|
|
22
25
|
const findingProjects = report.projects.filter((project) => project.findings.length > 0);
|
|
23
26
|
const shouldCollapseProjects = report.projects.length > DETAIL_PROJECT_LIMIT;
|
|
27
|
+
const acknowledgedCount = report.decisions?.acknowledgedCount ?? 0;
|
|
28
|
+
const activeFindings = report.findings.filter((finding) => !finding.acknowledged);
|
|
24
29
|
const lines = [
|
|
25
30
|
"Dependency Guardian scan",
|
|
26
31
|
`Target: ${report.target}`,
|
|
27
32
|
`Scanning: checked ${report.summary.projectCount} project manifest${report.summary.projectCount === 1 ? "" : "s"}.`,
|
|
28
|
-
`Status: ${
|
|
33
|
+
`Status: ${paintEffectiveStatus(report, theme)}`,
|
|
29
34
|
`Projects: ${report.summary.projectCount}`,
|
|
30
35
|
`Dependencies: ${report.summary.dependencyCount}`,
|
|
31
|
-
`Findings: ${report.summary.findingCount} (${report.summary.warnCount} warn, ${report.summary.blockCount} block)`,
|
|
36
|
+
`Findings: ${report.summary.findingCount} (${report.summary.warnCount} warn, ${report.summary.blockCount} block)${acknowledgedCount > 0 ? ` · ${acknowledgedCount} acknowledged` : ""}`,
|
|
37
|
+
...(acknowledgedCount > 0
|
|
38
|
+
? [`Acknowledged: ${acknowledgedCount} warn verdict${acknowledgedCount === 1 ? "" : "s"} accepted in dg.json — run 'dg decisions' to review`]
|
|
39
|
+
: []),
|
|
32
40
|
...(report.scanner
|
|
33
41
|
? [`Scanner: score ${report.scanner.score}, ${report.scanner.packages.length} packages verified`]
|
|
34
42
|
: []),
|
|
43
|
+
...(report.scanner ? provenanceDowngradeLines(report.scanner.packages, theme) : []),
|
|
35
44
|
""
|
|
36
45
|
];
|
|
37
46
|
if (report.scannerError) {
|
|
@@ -52,7 +61,7 @@ export function renderTextReport(report, terminalWidth = terminalWidthFromEnv(),
|
|
|
52
61
|
}
|
|
53
62
|
if (findingProjects.length > 0) {
|
|
54
63
|
lines.push("Finding groups:");
|
|
55
|
-
for (const group of groupFindings(
|
|
64
|
+
for (const group of groupFindings(activeFindings)) {
|
|
56
65
|
const severityLabel = theme.paint(group.severity === "block" ? "block" : "warn", group.severity.toUpperCase());
|
|
57
66
|
lines.push(` ${severityLabel} ${group.id}: ${group.count} finding${group.count === 1 ? "" : "s"} across ${group.projectCount} project${group.projectCount === 1 ? "" : "s"}`);
|
|
58
67
|
for (const finding of group.examples) {
|
|
@@ -89,11 +98,46 @@ export function renderTextReport(report, terminalWidth = terminalWidthFromEnv(),
|
|
|
89
98
|
lines.push("");
|
|
90
99
|
}
|
|
91
100
|
}
|
|
101
|
+
lines.push(...packagePageLines(report, theme));
|
|
92
102
|
for (const error of report.errors) {
|
|
93
103
|
lines.push(...wrapLine(`ERROR ${error.location}: `, error.message, width));
|
|
94
104
|
}
|
|
95
105
|
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
96
106
|
}
|
|
107
|
+
function packagePageLines(report, theme) {
|
|
108
|
+
const urls = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const pkg of report.scanner?.packages ?? []) {
|
|
111
|
+
const action = pkg.action ?? "pass";
|
|
112
|
+
if (action !== "block" && action !== "warn") {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const url = packagePageUrl(pkg.ecosystem ?? "", pkg.name);
|
|
116
|
+
if (url && !seen.has(url)) {
|
|
117
|
+
seen.add(url);
|
|
118
|
+
urls.push(url);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (urls.length === 0) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
return ["Full report for each flagged package:", ...urls.map((url) => ` ${theme.paint("muted", url)}`), ""];
|
|
125
|
+
}
|
|
126
|
+
function provenanceDowngradeLines(packages, theme) {
|
|
127
|
+
return packages
|
|
128
|
+
.filter((pkg) => pkg.provenance?.downgrade)
|
|
129
|
+
.map((pkg) => theme.paint("warn", `Provenance downgrades: ${pkg.name}@${pkg.version} (was attested at ${pkg.provenance.downgrade.fromVersion})`));
|
|
130
|
+
}
|
|
131
|
+
function paintEffectiveStatus(report, theme) {
|
|
132
|
+
if (report.scanner && report.decisions && !report.scannerError) {
|
|
133
|
+
const action = report.decisions.effectiveAction;
|
|
134
|
+
if (action === "analysis_incomplete") {
|
|
135
|
+
return theme.paint("unknown", "analysis_incomplete");
|
|
136
|
+
}
|
|
137
|
+
return theme.paint(SCAN_STATUS_ROLE[action], action);
|
|
138
|
+
}
|
|
139
|
+
return theme.paint(SCAN_STATUS_ROLE[report.status], displayScanStatus(report));
|
|
140
|
+
}
|
|
97
141
|
function formatProject(project, width) {
|
|
98
142
|
const lines = [];
|
|
99
143
|
const version = project.version ? `@${project.version}` : "";
|
|
@@ -111,8 +155,8 @@ function formatProject(project, width) {
|
|
|
111
155
|
}
|
|
112
156
|
return lines;
|
|
113
157
|
}
|
|
114
|
-
export function renderJsonReport(report, scannerUnavailable = false) {
|
|
115
|
-
return `${JSON.stringify({ ...report, status: displayScanStatus(report), scannerUnavailable }, null, 2)}\n`;
|
|
158
|
+
export function renderJsonReport(report, scannerUnavailable = false, nothingToScan = false) {
|
|
159
|
+
return `${JSON.stringify({ schemaVersion: 1, ...report, status: nothingToScan ? "nothing_to_scan" : displayScanStatus(report), scannerUnavailable }, null, 2)}\n`;
|
|
116
160
|
}
|
|
117
161
|
export function renderSarifReport(report) {
|
|
118
162
|
const rules = uniqueFindings(report.findings).map((finding) => ({
|
|
@@ -154,7 +198,17 @@ export function renderSarifReport(report) {
|
|
|
154
198
|
}
|
|
155
199
|
}
|
|
156
200
|
}
|
|
157
|
-
]
|
|
201
|
+
],
|
|
202
|
+
...(finding.acknowledged
|
|
203
|
+
? {
|
|
204
|
+
suppressions: [
|
|
205
|
+
{
|
|
206
|
+
kind: "external",
|
|
207
|
+
justification: finding.acknowledged.reason || `accepted by ${finding.acknowledged.by}`
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
: {})
|
|
158
212
|
}))
|
|
159
213
|
}
|
|
160
214
|
]
|
|
@@ -250,7 +304,8 @@ function wrapLine(prefix, text, width) {
|
|
|
250
304
|
return lines.map((line, index) => `${index === 0 ? prefix : continuation}${line}`);
|
|
251
305
|
}
|
|
252
306
|
function terminalWidthFromEnv() {
|
|
253
|
-
const
|
|
307
|
+
const override = process.env.NODE_ENV === "test" ? process.env.DG_TEST_TERMINAL_WIDTH : undefined;
|
|
308
|
+
const value = override ?? String(process.stdout.columns ?? 100);
|
|
254
309
|
const parsed = Number.parseInt(value, 10);
|
|
255
310
|
return Number.isFinite(parsed) ? parsed : 100;
|
|
256
311
|
}
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { applyDecisions, packageKey } from "../decisions/apply.js";
|
|
6
7
|
import { sanitize } from "../security/sanitize.js";
|
|
7
8
|
import { collectScanPackages, discoverScanProjects } from "./collect.js";
|
|
8
9
|
const WORKER_TIMEOUT_BASE_MS = 180_000;
|
|
@@ -12,7 +13,7 @@ const WORKER_MAX_BUFFER = 64 * 1024 * 1024;
|
|
|
12
13
|
export function scanWorkerTimeoutMs(packageCount) {
|
|
13
14
|
return WORKER_TIMEOUT_BASE_MS + packageCount * Math.ceil(SERVER_PER_PACKAGE_WORST_CASE_MS / SERVER_SCAN_CONCURRENCY);
|
|
14
15
|
}
|
|
15
|
-
export function runScannerScan(targetPath, localReport, env = process.env) {
|
|
16
|
+
export function runScannerScan(targetPath, localReport, env = process.env, decisionsFile = null) {
|
|
16
17
|
const projects = discoverScanProjects(resolve(targetPath));
|
|
17
18
|
if (projects.length === 0) {
|
|
18
19
|
return { kind: "skipped", reason: "no_lockfiles" };
|
|
@@ -58,12 +59,38 @@ export function runScannerScan(targetPath, localReport, env = process.env) {
|
|
|
58
59
|
error: { kind: "invalid_response", message: "scanner worker returned unreadable output" }
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
const ecosystems = new Map();
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
for (const pkg of group.packages) {
|
|
65
|
+
ecosystems.set(packageKey(pkg.name, pkg.version), group.ecosystem);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const report = buildScannerReport(localReport, response, total, ecosystems);
|
|
69
|
+
if (!decisionsFile?.readable) {
|
|
70
|
+
return { kind: "report", report };
|
|
71
|
+
}
|
|
72
|
+
return { kind: "report", report: annotateReportWithDecisions(report, decisionsFile, ecosystems) };
|
|
62
73
|
}
|
|
63
|
-
export function tryScannerScan(targetPath, localReport, env = process.env) {
|
|
64
|
-
const outcome = runScannerScan(targetPath, localReport, env);
|
|
74
|
+
export function tryScannerScan(targetPath, localReport, env = process.env, decisionsFile = null) {
|
|
75
|
+
const outcome = runScannerScan(targetPath, localReport, env, decisionsFile);
|
|
65
76
|
return outcome.kind === "report" ? outcome.report : null;
|
|
66
77
|
}
|
|
78
|
+
export function annotateReportWithDecisions(report, decisionsFile, ecosystems) {
|
|
79
|
+
if (!report.scanner || !decisionsFile.readable) {
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
const applied = applyDecisions(report.scanner.packages, (pkg) => ecosystems.get(packageKey(pkg.name, pkg.version)), decisionsFile, report.scanner.action);
|
|
83
|
+
const findings = report.findings.map((finding) => {
|
|
84
|
+
const acknowledged = applied.packages[finding.location]?.acknowledged;
|
|
85
|
+
return acknowledged && finding.severity === "warn" ? { ...finding, acknowledged } : finding;
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
...report,
|
|
89
|
+
findings,
|
|
90
|
+
summary: { ...report.summary, acknowledgedCount: applied.acknowledgedCount },
|
|
91
|
+
decisions: applied
|
|
92
|
+
};
|
|
93
|
+
}
|
|
67
94
|
export function workerFailure(worker, timeoutMs, packageCount) {
|
|
68
95
|
if (worker.error?.code === "ETIMEDOUT") {
|
|
69
96
|
return {
|
|
@@ -103,9 +130,9 @@ function parseWorkerError(stdout) {
|
|
|
103
130
|
return null;
|
|
104
131
|
}
|
|
105
132
|
function lockfileParseError(projects, collected) {
|
|
106
|
-
const parseErrors = collected.parseErrors
|
|
133
|
+
const parseErrors = collected.parseErrors;
|
|
107
134
|
const detail = parseErrors
|
|
108
|
-
.map((entry) => [entry.
|
|
135
|
+
.map((entry) => [entry.file, entry.reason].filter(Boolean).join(": "))
|
|
109
136
|
.filter((line) => line.length > 0)
|
|
110
137
|
.join("; ");
|
|
111
138
|
const lockfiles = [...new Set(projects.map((project) => project.depFile))].join(", ");
|
|
@@ -116,8 +143,14 @@ function lockfileParseError(projects, collected) {
|
|
|
116
143
|
: `found ${projects.length} lockfile${projects.length === 1 ? "" : "s"} (${lockfiles}) but no packages could be parsed`
|
|
117
144
|
};
|
|
118
145
|
}
|
|
119
|
-
export function buildScannerReport(localReport, response, analyzedCount) {
|
|
120
|
-
const
|
|
146
|
+
export function buildScannerReport(localReport, response, analyzedCount, ecosystems) {
|
|
147
|
+
const packages = ecosystems
|
|
148
|
+
? response.packages.map((pkg) => {
|
|
149
|
+
const ecosystem = ecosystems.get(packageKey(pkg.name, pkg.version));
|
|
150
|
+
return ecosystem ? { ...pkg, ecosystem } : pkg;
|
|
151
|
+
})
|
|
152
|
+
: response.packages;
|
|
153
|
+
const findings = packages
|
|
121
154
|
.filter((pkg) => (pkg.action ?? "pass") !== "pass")
|
|
122
155
|
.map((pkg) => scannerFinding(pkg));
|
|
123
156
|
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
@@ -136,7 +169,7 @@ export function buildScannerReport(localReport, response, analyzedCount) {
|
|
|
136
169
|
blockCount,
|
|
137
170
|
errorCount: localReport.summary.errorCount
|
|
138
171
|
},
|
|
139
|
-
scanner: response
|
|
172
|
+
scanner: { ...response, packages }
|
|
140
173
|
};
|
|
141
174
|
}
|
|
142
175
|
function scannerFinding(pkg) {
|
package/dist/scan/staged.js
CHANGED
|
@@ -4,6 +4,9 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
|
|
|
4
4
|
import { createTheme } from "../presentation/theme.js";
|
|
5
5
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
|
+
import { offerRememberSync } from "../decisions/remember-prompt.js";
|
|
8
|
+
import { packageKey } from "../decisions/apply.js";
|
|
9
|
+
import { loadDgFile, resolveAcceptedBy, warnUnreadableDgFile } from "../project/dgfile.js";
|
|
7
10
|
import { gitSync, gitTrimmed } from "../util/git.js";
|
|
8
11
|
import { promptYesNo } from "../util/tty-prompt.js";
|
|
9
12
|
import { GUARD_SELFTEST_ENV } from "../setup/git-hook.js";
|
|
@@ -67,7 +70,8 @@ export function runStagedScan(options) {
|
|
|
67
70
|
const env = options.env ?? process.env;
|
|
68
71
|
const cwd = options.cwd ?? process.cwd();
|
|
69
72
|
const theme = createTheme(resolvePresentation().color);
|
|
70
|
-
|
|
73
|
+
// git exports GIT_INDEX_FILE to real pre-commit runs; its absence separates dg's hook self-test from a user commit that inherited the env var.
|
|
74
|
+
if (options.hook && env[GUARD_SELFTEST_ENV] === "1" && env.GIT_INDEX_FILE === undefined) {
|
|
71
75
|
return { exitCode: 2, stdout: "", stderr: "dg guard-commit self-test: synthetic block (exit 2)\n" };
|
|
72
76
|
}
|
|
73
77
|
const root = gitTrimmed(["rev-parse", "--show-toplevel"], { cwd, env });
|
|
@@ -85,16 +89,21 @@ export function runStagedScan(options) {
|
|
|
85
89
|
if (scoped.length === 0) {
|
|
86
90
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
87
91
|
}
|
|
92
|
+
const dgFile = options.useDecisions === false ? null : loadDgFile(root);
|
|
93
|
+
if (dgFile) {
|
|
94
|
+
warnUnreadableDgFile(dgFile);
|
|
95
|
+
}
|
|
96
|
+
const usableDgFile = dgFile?.readable ? dgFile : null;
|
|
88
97
|
const { dir, count } = materializeStaged(scoped, cwd, env);
|
|
89
98
|
try {
|
|
90
99
|
if (count === 0) {
|
|
91
100
|
return failOpen(theme, "could not read the staged lockfile contents");
|
|
92
101
|
}
|
|
93
|
-
const report = tryScannerScan(dir, emptyLocalReport(dir), env);
|
|
102
|
+
const report = tryScannerScan(dir, emptyLocalReport(dir), env, usableDgFile);
|
|
94
103
|
if (!report || !report.scanner) {
|
|
95
104
|
return failOpen(theme, "could not reach the scanner");
|
|
96
105
|
}
|
|
97
|
-
return decideStagedVerdict(report, env, options.hook);
|
|
106
|
+
return decideStagedVerdict(report, env, options.hook, usableDgFile ? { root, file: usableDgFile } : undefined);
|
|
98
107
|
}
|
|
99
108
|
finally {
|
|
100
109
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -119,12 +128,16 @@ export function stagedScanReport(options) {
|
|
|
119
128
|
if (scoped.length === 0) {
|
|
120
129
|
return { report: base, outcome: { kind: "skipped", reason: "no_lockfiles" } };
|
|
121
130
|
}
|
|
131
|
+
const dgFile = options.useDecisions === false ? null : loadDgFile(root);
|
|
132
|
+
if (dgFile) {
|
|
133
|
+
warnUnreadableDgFile(dgFile);
|
|
134
|
+
}
|
|
122
135
|
const { dir, count } = materializeStaged(scoped, cwd, env);
|
|
123
136
|
try {
|
|
124
137
|
if (count === 0) {
|
|
125
138
|
return { report: base, outcome: { kind: "failed", error: { kind: "worker", message: "could not read the staged lockfile contents" } } };
|
|
126
139
|
}
|
|
127
|
-
return { report: base, outcome: runScannerScan(dir, base, env) };
|
|
140
|
+
return { report: base, outcome: runScannerScan(dir, base, env, dgFile?.readable ? dgFile : null) };
|
|
128
141
|
}
|
|
129
142
|
finally {
|
|
130
143
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -144,11 +157,13 @@ function notARepoResult() {
|
|
|
144
157
|
function outsideRepoResult(targetPath) {
|
|
145
158
|
return { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg scan --staged: ${targetPath} is outside this repository.\n` };
|
|
146
159
|
}
|
|
147
|
-
export function decideStagedVerdict(report, env = process.env, hook = false) {
|
|
160
|
+
export function decideStagedVerdict(report, env = process.env, hook = false, decisionContext) {
|
|
148
161
|
const theme = createTheme(resolvePresentation().color);
|
|
149
162
|
const action = report.scanner?.action ?? report.status;
|
|
150
163
|
const config = loadUserConfig(env);
|
|
151
164
|
const count = report.summary.dependencyCount;
|
|
165
|
+
const effective = config.policy.mode === "strict" ? action : report.decisions?.effectiveAction ?? action;
|
|
166
|
+
const acknowledgedCount = report.decisions?.acknowledgedCount ?? 0;
|
|
152
167
|
if (action === "block") {
|
|
153
168
|
return { exitCode: 2, stdout: "", stderr: renderBlock(report.findings, theme) };
|
|
154
169
|
}
|
|
@@ -156,17 +171,61 @@ export function decideStagedVerdict(report, env = process.env, hook = false) {
|
|
|
156
171
|
return scannerUnavailable(theme);
|
|
157
172
|
}
|
|
158
173
|
if (action === "warn") {
|
|
159
|
-
|
|
174
|
+
if (acknowledgedCount > 0 && effective === "pass") {
|
|
175
|
+
return {
|
|
176
|
+
exitCode: 0,
|
|
177
|
+
stdout: "",
|
|
178
|
+
stderr: ` ${theme.paint("pass", "✓")} ${theme.paint("muted", `DG verified ${count} staged package${count === 1 ? "" : "s"} — ${acknowledgedCount} warning${acknowledgedCount === 1 ? "" : "s"} previously accepted (dg.json)`)}\n`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (acknowledgedCount > 0 && effective === "analysis_incomplete") {
|
|
182
|
+
return decideIncomplete(config.gitHook.onIncomplete, theme);
|
|
183
|
+
}
|
|
184
|
+
const activeFindings = report.findings.filter((finding) => !finding.acknowledged);
|
|
185
|
+
return decideWarn(activeFindings, theme, config.gitHook.onWarn, config.policy.mode, hook, stagedRememberOffer(report, decisionContext, env));
|
|
160
186
|
}
|
|
161
187
|
if (action === "analysis_incomplete") {
|
|
162
|
-
|
|
163
|
-
return { exitCode: 1, stdout: "", stderr: incompleteNotice(theme, true) };
|
|
164
|
-
}
|
|
165
|
-
return { exitCode: 0, stdout: "", stderr: incompleteNotice(theme, false) };
|
|
188
|
+
return decideIncomplete(config.gitHook.onIncomplete, theme);
|
|
166
189
|
}
|
|
167
190
|
return { exitCode: 0, stdout: "", stderr: ` ${theme.paint("pass", "✓")} ${theme.paint("muted", `DG verified ${count} staged package${count === 1 ? "" : "s"} — clean`)}\n` };
|
|
168
191
|
}
|
|
169
|
-
function
|
|
192
|
+
function decideIncomplete(onIncomplete, theme) {
|
|
193
|
+
if (onIncomplete === "block") {
|
|
194
|
+
return { exitCode: 1, stdout: "", stderr: incompleteNotice(theme, true) };
|
|
195
|
+
}
|
|
196
|
+
return { exitCode: 0, stdout: "", stderr: incompleteNotice(theme, false) };
|
|
197
|
+
}
|
|
198
|
+
export function stagedRememberOffer(report, decisionContext, env) {
|
|
199
|
+
if (!decisionContext || !report.scanner || !report.decisions) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const annotations = report.decisions.packages;
|
|
203
|
+
const packages = [];
|
|
204
|
+
for (const pkg of report.scanner.packages) {
|
|
205
|
+
if ((pkg.action ?? "pass") !== "warn") {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const annotation = annotations[packageKey(pkg.name, pkg.version)];
|
|
209
|
+
if (!annotation || annotation.acknowledged) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
packages.push({ ecosystem: annotation.ecosystem, name: pkg.name, version: pkg.version, findings: pkg.findings });
|
|
213
|
+
}
|
|
214
|
+
if (packages.length === 0) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
return () => {
|
|
218
|
+
offerRememberSync({
|
|
219
|
+
file: decisionContext.file,
|
|
220
|
+
packages,
|
|
221
|
+
acceptedBy: resolveAcceptedBy(decisionContext.root, env),
|
|
222
|
+
surface: "guard-commit",
|
|
223
|
+
env,
|
|
224
|
+
...(decisionContext.prompts ? { prompts: decisionContext.prompts } : {})
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function decideWarn(findings, theme, onWarn, policyMode, hook, onAccepted) {
|
|
170
229
|
const summary = warnSummary(findings, theme);
|
|
171
230
|
if (onWarn === "allow") {
|
|
172
231
|
return { exitCode: 0, stdout: "", stderr: `${summary} ${theme.paint("muted", "proceeding (gitHook.onWarn=allow)")}\n` };
|
|
@@ -183,6 +242,7 @@ function decideWarn(findings, theme, onWarn, policyMode, hook) {
|
|
|
183
242
|
return { exitCode: 1, stdout: "", stderr: `${summary} ${theme.paint("muted", `commit blocked (no terminal; policy ${policyMode}). Use`)} ${theme.paint("accent", "git commit --no-verify")} ${theme.paint("muted", "to override.")}\n` };
|
|
184
243
|
}
|
|
185
244
|
if (answer) {
|
|
245
|
+
onAccepted?.();
|
|
186
246
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
187
247
|
}
|
|
188
248
|
return { exitCode: 1, stdout: "", stderr: ` ${theme.paint("muted", "Nothing was committed.")}\n` };
|