@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/proxy/worker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { startProductionHttpProxy } from "./server.js";
|
|
3
3
|
import { parseForceOverrideRequest } from "./enforcement.js";
|
|
4
4
|
import { cleanupSessionSync } from "../state/index.js";
|
|
5
|
+
import { COOLDOWN_EXEMPTIONS_ENV, loadCooldownExemptionsFile } from "./cooldown-exemptions-file.js";
|
|
5
6
|
const PARENT_POLL_MS = 500;
|
|
6
7
|
const sessionPath = process.argv[2];
|
|
7
8
|
const apiBaseUrl = process.argv[3];
|
|
@@ -13,6 +14,7 @@ if (!sessionPath || !apiBaseUrl || !classificationJson) {
|
|
|
13
14
|
const session = JSON.parse(readFileSync(sessionPath, "utf8"));
|
|
14
15
|
const classification = JSON.parse(classificationJson);
|
|
15
16
|
const forceOverride = parseForceOverrideRequest(process.env.DG_FORCE_OVERRIDE_REQUEST);
|
|
17
|
+
const cooldownExemptions = loadCooldownExemptionsFile(process.env[COOLDOWN_EXEMPTIONS_ENV]);
|
|
16
18
|
writeFileSync(session.files.pid, `${process.pid}\n`, {
|
|
17
19
|
encoding: "utf8",
|
|
18
20
|
mode: 0o600
|
|
@@ -30,6 +32,18 @@ async function close() {
|
|
|
30
32
|
function shutdown() {
|
|
31
33
|
close().finally(() => process.exit(0));
|
|
32
34
|
}
|
|
35
|
+
function exitOnFatal(error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
try {
|
|
38
|
+
process.stderr.write(`dg proxy worker: unexpected error — ${message}\n`);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// stderr is gone; nothing left to report to.
|
|
42
|
+
}
|
|
43
|
+
close().finally(() => process.exit(1));
|
|
44
|
+
}
|
|
45
|
+
process.on("uncaughtException", exitOnFatal);
|
|
46
|
+
process.on("unhandledRejection", exitOnFatal);
|
|
33
47
|
process.stdin.resume();
|
|
34
48
|
process.stdin.on("end", shutdown);
|
|
35
49
|
process.stdin.on("error", shutdown);
|
|
@@ -46,6 +60,7 @@ handle = await startProductionHttpProxy({
|
|
|
46
60
|
apiBaseUrl,
|
|
47
61
|
classification,
|
|
48
62
|
env: process.env,
|
|
49
|
-
...(forceOverride ? { forceOverride } : {})
|
|
63
|
+
...(forceOverride ? { forceOverride } : {}),
|
|
64
|
+
...(cooldownExemptions.length > 0 ? { cooldownExemptions } : {})
|
|
50
65
|
});
|
|
51
66
|
process.stdout.write(`ready ${handle.port}\n`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
2
|
-
import { isAbsolute,
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
export function buildAuditFile(root, relPath) {
|
|
4
4
|
const absolute = resolve(root, relPath);
|
|
5
5
|
let stat;
|
|
@@ -46,6 +46,3 @@ export function toDisplay(relPath) {
|
|
|
46
46
|
const normalized = relative(".", relPath) || relPath;
|
|
47
47
|
return normalized.split(sep).join("/");
|
|
48
48
|
}
|
|
49
|
-
export function joinRel(dir, name) {
|
|
50
|
-
return toDisplay(join(dir, name));
|
|
51
|
-
}
|
package/dist/publish-set/npm.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { toolInvocation } from "../util/external-tool.js";
|
|
4
5
|
import { toDisplay } from "./collect.js";
|
|
5
6
|
import { noExecPackEnv } from "./no-exec-shell.js";
|
|
6
|
-
export function npmExecutable(platform = process.platform) {
|
|
7
|
-
return platform === "win32" ? "npm.cmd" : "npm";
|
|
8
|
-
}
|
|
9
7
|
const DEFAULT_EXCLUDED_DIRS = new Set([
|
|
10
8
|
"node_modules",
|
|
11
9
|
".git",
|
|
@@ -46,15 +44,20 @@ function npmHasAllowlist(root) {
|
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
function npmPackList(root, env) {
|
|
47
|
+
const invocation = toolInvocation("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], env);
|
|
48
|
+
if (!invocation) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
49
51
|
const shell = noExecPackEnv(env);
|
|
50
52
|
let result;
|
|
51
53
|
try {
|
|
52
|
-
result = spawnSync(
|
|
54
|
+
result = spawnSync(invocation.command, [...invocation.args], {
|
|
53
55
|
cwd: root,
|
|
54
56
|
env: shell.env,
|
|
55
57
|
encoding: "utf8",
|
|
56
58
|
timeout: 60_000,
|
|
57
|
-
maxBuffer: 64 * 1024 * 1024
|
|
59
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
60
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
finally {
|
package/dist/publish-set/pack.js
CHANGED
|
@@ -3,18 +3,24 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { toolInvocation } from "../util/external-tool.js";
|
|
7
7
|
import { noExecPackEnv } from "./no-exec-shell.js";
|
|
8
8
|
export function packNpmArtifact(root, env = process.env) {
|
|
9
9
|
const dest = mkdtempSync(join(tmpdir(), "dg-audit-pack-"));
|
|
10
|
+
const invocation = toolInvocation("npm", ["pack", "--ignore-scripts", "--pack-destination", dest], env);
|
|
11
|
+
if (!invocation) {
|
|
12
|
+
rmSync(dest, { recursive: true, force: true });
|
|
13
|
+
return { error: "npm executable not found on PATH" };
|
|
14
|
+
}
|
|
10
15
|
const shell = noExecPackEnv(env);
|
|
11
16
|
try {
|
|
12
|
-
const result = spawnSync(
|
|
17
|
+
const result = spawnSync(invocation.command, [...invocation.args], {
|
|
13
18
|
cwd: root,
|
|
14
19
|
env: shell.env,
|
|
15
20
|
encoding: "utf8",
|
|
16
21
|
timeout: 120_000,
|
|
17
|
-
maxBuffer: 8 * 1024 * 1024
|
|
22
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
23
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
18
24
|
});
|
|
19
25
|
if (result.status !== 0) {
|
|
20
26
|
return { error: result.stderr ? result.stderr.trim().split("\n").pop() ?? "npm pack failed" : "npm pack failed" };
|
package/dist/runtime/cli.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
1
|
import { routeCommand } from "../commands/router.js";
|
|
3
|
-
if (process.env.NODE_ENV === "test" && process.env.DG_TEST_RUNTIME_SENTINEL) {
|
|
4
|
-
writeFileSync(process.env.DG_TEST_RUNTIME_SENTINEL, "loaded\n", "utf8");
|
|
5
|
-
}
|
|
6
2
|
export function runCli(args) {
|
|
7
3
|
return routeCommand(args);
|
|
8
4
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EXIT_TOOL_ERROR } from "../commands/types.js";
|
|
2
|
+
import { leaveTui, tuiIsActive } from "../scan-ui/alt-screen.js";
|
|
3
|
+
const defaultIo = {
|
|
4
|
+
writeStderr: (text) => {
|
|
5
|
+
process.stderr.write(text);
|
|
6
|
+
},
|
|
7
|
+
exit: (code) => {
|
|
8
|
+
process.exit(code);
|
|
9
|
+
},
|
|
10
|
+
tuiIsActive,
|
|
11
|
+
leaveTui
|
|
12
|
+
};
|
|
13
|
+
// Anything written while the alt screen is active is discarded when the exit handler restores the main screen.
|
|
14
|
+
export function exitOnFatal(error, io = defaultIo) {
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
try {
|
|
17
|
+
if (io.tuiIsActive()) {
|
|
18
|
+
io.leaveTui();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// screen restore must never mask the fatal report
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
io.writeStderr(`dg: unexpected error — ${message}\nRun 'dg doctor' to check your installation; remove ~/.dg/config.json if it is corrupted.\n`);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// stderr is gone; nothing left to report to.
|
|
29
|
+
}
|
|
30
|
+
io.exit(EXIT_TOOL_ERROR);
|
|
31
|
+
}
|
|
@@ -13,14 +13,13 @@ const SKIP_COMMANDS = new Set([
|
|
|
13
13
|
"version",
|
|
14
14
|
"--version",
|
|
15
15
|
"-v",
|
|
16
|
-
"completion",
|
|
17
16
|
"login",
|
|
18
17
|
"logout",
|
|
19
18
|
"update",
|
|
20
19
|
"upgrade",
|
|
21
20
|
"uninstall"
|
|
22
21
|
]);
|
|
23
|
-
const MACHINE_OUTPUT_FLAGS = new Set([
|
|
22
|
+
export const MACHINE_OUTPUT_FLAGS = new Set([
|
|
24
23
|
"--json",
|
|
25
24
|
"--sarif",
|
|
26
25
|
"--csv",
|
|
@@ -51,6 +50,16 @@ export function sweepLegacyHooksOnVersionChange(env = process.env, version = dgV
|
|
|
51
50
|
return false;
|
|
52
51
|
}
|
|
53
52
|
}
|
|
53
|
+
export function markFirstRunShown(env = process.env, now = new Date()) {
|
|
54
|
+
try {
|
|
55
|
+
const marker = firstRunMarkerPath(env);
|
|
56
|
+
mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
|
|
57
|
+
writeFileSync(marker, `${now.toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
export function maybeShowFirstRun(args, options = {}) {
|
|
55
64
|
const env = options.env ?? process.env;
|
|
56
65
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -75,17 +84,9 @@ export function maybeShowFirstRun(args, options = {}) {
|
|
|
75
84
|
` ${theme.paint("muted", "dg npm install / dg pip install scan packages before they run.")}`,
|
|
76
85
|
` ${theme.paint("muted", "Run")} ${command_("dg setup")} ${theme.paint("muted", "once to protect bare npm/pip installs too.")}`,
|
|
77
86
|
"",
|
|
78
|
-
` Next: ${command_("dg login")} to connect your account.`,
|
|
79
|
-
"",
|
|
80
87
|
""
|
|
81
88
|
];
|
|
82
89
|
stderr.write(lines.join("\n"));
|
|
83
|
-
|
|
84
|
-
mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
|
|
85
|
-
writeFileSync(marker, `${new Date().toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
+
markFirstRunShown(env);
|
|
90
91
|
return true;
|
|
91
92
|
}
|
|
@@ -46,13 +46,50 @@ export function currentNodeVersion() {
|
|
|
46
46
|
}
|
|
47
47
|
return process.version;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// The shims exec dg unconditionally; a hard exit on old Node would brick every shimmed manager.
|
|
50
|
+
export async function resolveShimFallback(env = process.env, argv = process.argv) {
|
|
51
|
+
if (!env.DG_SHIM_ACTIVE) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const manager = argv[2];
|
|
55
|
+
if (!manager) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
50
58
|
try {
|
|
51
|
-
|
|
59
|
+
const { resolveRealBinary } = await import("../launcher/resolve-real-binary.js");
|
|
60
|
+
const binary = resolveRealBinary({ name: manager, env }).path;
|
|
61
|
+
if (!binary) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
binary,
|
|
66
|
+
args: argv.slice(3),
|
|
67
|
+
warning: `dg: protection inactive — dg requires Node.js >=22.14.0 but found ${currentNodeVersion()}; running ${manager} unprotected\n`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function assertCurrentNode() {
|
|
75
|
+
const version = currentNodeVersion();
|
|
76
|
+
if (isSupportedNode(version)) {
|
|
77
|
+
return;
|
|
52
78
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
process.stderr.write(
|
|
56
|
-
|
|
79
|
+
const fallback = await resolveShimFallback();
|
|
80
|
+
if (fallback) {
|
|
81
|
+
process.stderr.write(fallback.warning);
|
|
82
|
+
const [{ spawnSync }, { resolveSpawnInvocation }] = await Promise.all([
|
|
83
|
+
import("node:child_process"),
|
|
84
|
+
import("../launcher/spawn-invocation.js")
|
|
85
|
+
]);
|
|
86
|
+
const invocation = resolveSpawnInvocation(fallback.binary, fallback.args);
|
|
87
|
+
const result = spawnSync(invocation.command, [...invocation.args], {
|
|
88
|
+
stdio: "inherit",
|
|
89
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
90
|
+
});
|
|
91
|
+
process.exit(result.status ?? 1);
|
|
57
92
|
}
|
|
93
|
+
process.stderr.write(`dg requires Node.js >=22.14.0. Current runtime is ${version}. Upgrade Node before running dg.\n`);
|
|
94
|
+
process.exit(1);
|
|
58
95
|
}
|
package/dist/runtime/nudges.js
CHANGED
|
@@ -10,6 +10,7 @@ const UPDATE_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
|
10
10
|
const LOGIN_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
11
|
const LATEST_LOOKUP_TIMEOUT_MS = 1500;
|
|
12
12
|
const SKIP_COMMANDS = new Set([
|
|
13
|
+
"",
|
|
13
14
|
"help",
|
|
14
15
|
"--help",
|
|
15
16
|
"-h",
|
|
@@ -17,14 +18,15 @@ const SKIP_COMMANDS = new Set([
|
|
|
17
18
|
"version",
|
|
18
19
|
"--version",
|
|
19
20
|
"-v",
|
|
20
|
-
"completion",
|
|
21
21
|
"login",
|
|
22
22
|
"logout",
|
|
23
|
+
"setup",
|
|
23
24
|
"update",
|
|
24
25
|
"upgrade",
|
|
25
26
|
"uninstall",
|
|
26
27
|
"scan",
|
|
27
|
-
"licenses"
|
|
28
|
+
"licenses",
|
|
29
|
+
"sbom"
|
|
28
30
|
]);
|
|
29
31
|
export function nudgeStatePath(env = process.env) {
|
|
30
32
|
return join(resolveDgPaths(env).stateDir, "nudges.json");
|
|
@@ -68,6 +70,37 @@ export function maybeShowNudges(args, options = {}) {
|
|
|
68
70
|
writeState(statePath, state);
|
|
69
71
|
}
|
|
70
72
|
}
|
|
73
|
+
export function maybeSetupNudge(manager, options = {}) {
|
|
74
|
+
const env = options.env ?? process.env;
|
|
75
|
+
if (!(options.stderrIsTTY ?? process.stderr.isTTY)) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
if (isCiEnv(env)) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
if (env.DG_SHIM_ACTIVE) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
const paths = resolveDgPaths(env);
|
|
85
|
+
if (existsSync(join(paths.homeDir, ".dg", "shims", manager))) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
const statePath = nudgeStatePath(env);
|
|
89
|
+
const state = readState(statePath);
|
|
90
|
+
if (state.setupNudgedAt) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
state.setupNudgedAt = (options.now ?? new Date()).toISOString();
|
|
94
|
+
writeState(statePath, state);
|
|
95
|
+
const theme = createTheme(true);
|
|
96
|
+
return `\n ${theme.paint("muted", "Make this automatic —")} ${theme.paint("accent", "dg setup")} ${theme.paint("muted", "protects every install, no prefix.")}\n`;
|
|
97
|
+
}
|
|
98
|
+
export function recordLoginNudge(env = process.env, now = new Date()) {
|
|
99
|
+
const statePath = nudgeStatePath(env);
|
|
100
|
+
const state = readState(statePath);
|
|
101
|
+
state.loginNudgedAt = now.toISOString();
|
|
102
|
+
writeState(statePath, state);
|
|
103
|
+
}
|
|
71
104
|
export function pendingUpdate(env = process.env) {
|
|
72
105
|
const latest = readState(nudgeStatePath(env)).updateLatest;
|
|
73
106
|
if (!latest || compareVersions(latest, dgVersion()) <= 0) {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { parseLockfilePackages } from "../verify/preflight.js";
|
|
4
|
+
import { normalizePypiName } from "../policy/cooldown.js";
|
|
5
|
+
export function collectSbomComponents(projects) {
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const droppedSeen = new Set();
|
|
8
|
+
const components = [];
|
|
9
|
+
const dropped = [];
|
|
10
|
+
for (const project of projects) {
|
|
11
|
+
const parsed = parseLockfilePackages(join(project.path, project.depFile));
|
|
12
|
+
for (const identity of parsed.packages) {
|
|
13
|
+
if (identity.ecosystem === "unknown" || !identity.version) {
|
|
14
|
+
const label = `${identity.ecosystem}:${identity.name}`;
|
|
15
|
+
if (identity.name.length > 0 && !droppedSeen.has(label)) {
|
|
16
|
+
droppedSeen.add(label);
|
|
17
|
+
dropped.push(label);
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const component = { ...identity, version: identity.version };
|
|
22
|
+
const key = canonicalKey(component);
|
|
23
|
+
if (seen.has(key)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seen.add(key);
|
|
27
|
+
components.push(component);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { components, dropped };
|
|
31
|
+
}
|
|
32
|
+
export function readRootComponent(root) {
|
|
33
|
+
const manifest = join(root, "package.json");
|
|
34
|
+
if (!existsSync(manifest)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
39
|
+
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
return { component: { name: parsed.name, ...(typeof parsed.version === "string" ? { version: parsed.version } : {}) } };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { malformed: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function buildCycloneDxSbom(components, opts) {
|
|
49
|
+
const byRef = new Map();
|
|
50
|
+
for (const component of components) {
|
|
51
|
+
const mapped = toCycloneDxComponent(component);
|
|
52
|
+
if (!byRef.has(mapped["bom-ref"])) {
|
|
53
|
+
byRef.set(mapped["bom-ref"], mapped);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const mapped = [...byRef.values()].sort((a, b) => compareCodePoints(a["bom-ref"], b["bom-ref"]));
|
|
57
|
+
return {
|
|
58
|
+
bomFormat: "CycloneDX",
|
|
59
|
+
specVersion: "1.5",
|
|
60
|
+
serialNumber: opts.serialNumber,
|
|
61
|
+
version: 1,
|
|
62
|
+
metadata: {
|
|
63
|
+
...(opts.timestamp ? { timestamp: opts.timestamp } : {}),
|
|
64
|
+
tools: [{ vendor: "WestBayBerry", name: "dg", version: opts.toolVersion }],
|
|
65
|
+
...(opts.rootComponent
|
|
66
|
+
? {
|
|
67
|
+
component: {
|
|
68
|
+
type: "application",
|
|
69
|
+
"bom-ref": "root",
|
|
70
|
+
name: opts.rootComponent.name,
|
|
71
|
+
...(opts.rootComponent.version ? { version: opts.rootComponent.version } : {})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
: {})
|
|
75
|
+
},
|
|
76
|
+
components: mapped
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function compareCodePoints(a, b) {
|
|
80
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
export function bomRefOf(component) {
|
|
83
|
+
return purlFor(component) ?? `${component.ecosystem}:${component.name}@${component.version}`;
|
|
84
|
+
}
|
|
85
|
+
function canonicalKey(component) {
|
|
86
|
+
const name = canonicalPurlName(component.ecosystem, component.name);
|
|
87
|
+
return `${component.ecosystem}:${name}@${component.version}`;
|
|
88
|
+
}
|
|
89
|
+
function canonicalPurlName(ecosystem, name) {
|
|
90
|
+
if (ecosystem === "pypi") {
|
|
91
|
+
return normalizePypiName(name);
|
|
92
|
+
}
|
|
93
|
+
if (ecosystem === "npm") {
|
|
94
|
+
return name.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
return name;
|
|
97
|
+
}
|
|
98
|
+
// The declared license string is emitted verbatim as a CycloneDX `license.name`,
|
|
99
|
+
// which is always schema-valid for any free text. We deliberately do NOT classify
|
|
100
|
+
// it into the SPDX `expression` form: correctly distinguishing a valid SPDX
|
|
101
|
+
// expression from free text requires validating operands against the SPDX license
|
|
102
|
+
// registry, and any heuristic shortcut emits schema-invalid expressions for some
|
|
103
|
+
// inputs (unbalanced parens, operator-only strings, unregistered ids).
|
|
104
|
+
function toCycloneDxComponent(component) {
|
|
105
|
+
const purl = purlFor(component);
|
|
106
|
+
const hashes = hashesFor(component.integrity);
|
|
107
|
+
const license = component.license?.trim();
|
|
108
|
+
const licenses = license ? [{ license: { name: license } }] : undefined;
|
|
109
|
+
return {
|
|
110
|
+
type: "library",
|
|
111
|
+
"bom-ref": bomRefOf(component),
|
|
112
|
+
name: component.name,
|
|
113
|
+
version: component.version,
|
|
114
|
+
...(purl ? { purl } : {}),
|
|
115
|
+
...(licenses ? { licenses } : {}),
|
|
116
|
+
...(hashes.length > 0 ? { hashes } : {})
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function purlFor(component) {
|
|
120
|
+
const version = encodeURIComponent(component.version);
|
|
121
|
+
if (component.ecosystem === "npm") {
|
|
122
|
+
const lower = component.name.toLowerCase();
|
|
123
|
+
if (lower.startsWith("@") && lower.includes("/")) {
|
|
124
|
+
const slash = lower.indexOf("/");
|
|
125
|
+
const scope = lower.slice(1, slash);
|
|
126
|
+
const bare = lower.slice(slash + 1);
|
|
127
|
+
return `pkg:npm/%40${encodeURIComponent(scope)}/${encodeURIComponent(bare)}@${version}`;
|
|
128
|
+
}
|
|
129
|
+
return `pkg:npm/${encodeURIComponent(lower)}@${version}`;
|
|
130
|
+
}
|
|
131
|
+
if (component.ecosystem === "pypi") {
|
|
132
|
+
return `pkg:pypi/${encodeURIComponent(normalizePypiName(component.name))}@${version}`;
|
|
133
|
+
}
|
|
134
|
+
if (component.ecosystem === "cargo") {
|
|
135
|
+
const base = `pkg:cargo/${encodeURIComponent(component.name.toLowerCase())}@${version}`;
|
|
136
|
+
const repository = cargoAlternateRegistry(component.resolvedUrl);
|
|
137
|
+
return repository ? `${base}?repository_url=${encodeURIComponent(repository)}` : base;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function cargoAlternateRegistry(source) {
|
|
142
|
+
if (!source || (!source.startsWith("registry+") && !source.startsWith("sparse+"))) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const url = source.replace(/^registry\+/u, "").replace(/^sparse\+/u, "");
|
|
146
|
+
if (/(?:^|\/\/)(?:github\.com\/rust-lang\/crates\.io-index|index\.crates\.io)/u.test(url)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
const SRI_ALG_BYTES = {
|
|
152
|
+
sha1: { alg: "SHA-1", bytes: 20 },
|
|
153
|
+
sha256: { alg: "SHA-256", bytes: 32 },
|
|
154
|
+
sha384: { alg: "SHA-384", bytes: 48 },
|
|
155
|
+
sha512: { alg: "SHA-512", bytes: 64 }
|
|
156
|
+
};
|
|
157
|
+
export function hashesFor(integrity) {
|
|
158
|
+
if (!integrity) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const hashes = [];
|
|
162
|
+
for (const token of integrity.trim().split(/\s+/)) {
|
|
163
|
+
const sri = /^(sha1|sha256|sha384|sha512)-([^?]+)/u.exec(token);
|
|
164
|
+
if (sri && sri[1] && sri[2]) {
|
|
165
|
+
const spec = SRI_ALG_BYTES[sri[1]];
|
|
166
|
+
const hex = base64ToHex(sri[2]);
|
|
167
|
+
if (spec && hex && hex.length === spec.bytes * 2) {
|
|
168
|
+
hashes.push({ alg: spec.alg, content: hex });
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const hex = /^(sha1|sha256|sha384|sha512|md5):([0-9a-fA-F]+)$/u.exec(token);
|
|
173
|
+
if (hex && hex[1] && hex[2]) {
|
|
174
|
+
const spec = SRI_ALG_BYTES[hex[1]];
|
|
175
|
+
if (spec && hex[2].length === spec.bytes * 2) {
|
|
176
|
+
hashes.push({ alg: spec.alg, content: hex[2].toLowerCase() });
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const berry = /^[0-9a-z]+\/([0-9a-f]{40,128})$/iu.exec(token);
|
|
181
|
+
if (berry && berry[1]) {
|
|
182
|
+
const content = berry[1].toLowerCase();
|
|
183
|
+
const alg = SHA_HEX_LENGTHS[content.length];
|
|
184
|
+
if (alg) {
|
|
185
|
+
hashes.push({ alg, content });
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const bareHex = /^[0-9a-f]{40,128}$/iu.exec(token);
|
|
190
|
+
if (bareHex) {
|
|
191
|
+
const content = token.toLowerCase();
|
|
192
|
+
const alg = SHA_HEX_LENGTHS[content.length];
|
|
193
|
+
if (alg) {
|
|
194
|
+
hashes.push({ alg, content });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return hashes;
|
|
199
|
+
}
|
|
200
|
+
const SHA_HEX_LENGTHS = { 40: "SHA-1", 64: "SHA-256", 96: "SHA-384", 128: "SHA-512" };
|
|
201
|
+
function base64ToHex(value) {
|
|
202
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return Buffer.from(value, "base64").toString("hex");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|