@westbayberry/dg 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +95 -208
  2. package/dist/agents/copilot-cli.js +7 -2
  3. package/dist/agents/persistence.js +83 -8
  4. package/dist/agents/registry.js +5 -2
  5. package/dist/agents/routing.js +31 -14
  6. package/dist/api/analyze.js +138 -41
  7. package/dist/audit/deep.js +2 -1
  8. package/dist/audit/detectors.js +23 -2
  9. package/dist/audit/rules.js +4 -1
  10. package/dist/audit-ui/export.js +5 -4
  11. package/dist/auth/device-login.js +33 -11
  12. package/dist/auth/login-app.js +17 -12
  13. package/dist/auth/store.js +65 -9
  14. package/dist/bin/dg.js +1 -1
  15. package/dist/commands/audit.js +12 -20
  16. package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
  17. package/dist/commands/cooldown.js +2 -1
  18. package/dist/commands/decisions.js +8 -7
  19. package/dist/commands/help.js +11 -3
  20. package/dist/commands/licenses.js +17 -8
  21. package/dist/commands/login.js +58 -21
  22. package/dist/commands/router.js +12 -2
  23. package/dist/commands/sbom.js +20 -5
  24. package/dist/commands/scan.js +5 -4
  25. package/dist/commands/service.js +1 -1
  26. package/dist/commands/setup.js +12 -12
  27. package/dist/commands/status.js +4 -4
  28. package/dist/commands/uninstall.js +1 -1
  29. package/dist/commands/update.js +20 -0
  30. package/dist/commands/verify.js +4 -4
  31. package/dist/config/settings.js +81 -12
  32. package/dist/export-ui/ExportDialog.js +1 -1
  33. package/dist/gate/cooldown-request.js +22 -0
  34. package/dist/gate/egress-guard.js +185 -0
  35. package/dist/gate/host-match.js +48 -0
  36. package/dist/gate/verdict-core.js +101 -0
  37. package/dist/gate/verdict-fetch.js +214 -0
  38. package/dist/launcher/agent-check.js +1096 -42
  39. package/dist/launcher/agent-hook-exec.js +8 -1
  40. package/dist/launcher/agent-hook-io.js +5 -3
  41. package/dist/launcher/classify.js +110 -26
  42. package/dist/launcher/env.js +84 -18
  43. package/dist/launcher/install-preflight.js +26 -3
  44. package/dist/launcher/live-install.js +4 -2
  45. package/dist/launcher/manifest-screen.js +88 -7
  46. package/dist/launcher/output-redaction.js +153 -15
  47. package/dist/launcher/preflight-prompt.js +1 -1
  48. package/dist/launcher/resolve-real-binary.js +8 -3
  49. package/dist/launcher/run.js +237 -40
  50. package/dist/origin/artifact-cache.js +55 -0
  51. package/dist/origin/cargo-config.js +12 -0
  52. package/dist/origin/gate-mode.js +7 -0
  53. package/dist/origin/platform-paths.js +14 -0
  54. package/dist/origin/precedence.js +345 -0
  55. package/dist/origin/server.js +611 -0
  56. package/dist/origin/spike-worker.js +35 -0
  57. package/dist/origin/worker.js +116 -0
  58. package/dist/presentation/package-page.js +21 -1
  59. package/dist/presentation/safe-version.js +11 -0
  60. package/dist/presentation/sarif.js +109 -0
  61. package/dist/project/dgfile.js +54 -17
  62. package/dist/project/override-trust.js +0 -0
  63. package/dist/proxy/enforcement.js +27 -7
  64. package/dist/proxy/metadata-map.js +33 -7
  65. package/dist/proxy/preverified.js +0 -3
  66. package/dist/proxy/server.js +104 -446
  67. package/dist/proxy/upstream-proxy.js +23 -4
  68. package/dist/proxy/worker.js +3 -2
  69. package/dist/publish-set/collect.js +6 -0
  70. package/dist/runtime/fatal.js +2 -1
  71. package/dist/runtime/first-run.js +5 -1
  72. package/dist/runtime/node-version.js +51 -0
  73. package/dist/runtime/nudges.js +27 -1
  74. package/dist/sbom/cyclonedx.js +120 -33
  75. package/dist/sbom/graph.js +236 -0
  76. package/dist/sbom/spdx.js +53 -0
  77. package/dist/sbom-ui/SbomApp.js +5 -3
  78. package/dist/sbom-ui/inventory.js +8 -10
  79. package/dist/scan/analyze-worker.js +3 -1
  80. package/dist/scan/collect.js +38 -8
  81. package/dist/scan/command.js +77 -27
  82. package/dist/scan/discovery.js +18 -7
  83. package/dist/scan/installed.js +202 -0
  84. package/dist/scan/manifest-coverage.js +128 -0
  85. package/dist/scan/render.js +125 -40
  86. package/dist/scan/scanner-report.js +92 -22
  87. package/dist/scan/staged.js +68 -15
  88. package/dist/scan/types.js +7 -1
  89. package/dist/scan-ui/LegacyApp.js +3 -3
  90. package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
  91. package/dist/scan-ui/hooks/useScan.js +37 -11
  92. package/dist/scan-ui/launch.js +2 -2
  93. package/dist/scan-ui/logo.js +6 -2
  94. package/dist/scan-ui/shims.js +6 -6
  95. package/dist/scripts/gate.js +53 -35
  96. package/dist/security/csv.js +9 -0
  97. package/dist/security/sanitize.js +11 -2
  98. package/dist/service/state.js +120 -37
  99. package/dist/service/trust-refresh.js +12 -1
  100. package/dist/service/trust-store.js +36 -5
  101. package/dist/service/worker.js +5 -7
  102. package/dist/setup/git-hook.js +2 -2
  103. package/dist/setup/install-method.js +36 -0
  104. package/dist/setup/plan.js +119 -57
  105. package/dist/setup/uninstall-standalone.js +7 -2
  106. package/dist/setup-ui/gate.js +1 -8
  107. package/dist/setup-ui/offer.js +14 -7
  108. package/dist/setup-ui/selector.js +1 -1
  109. package/dist/setup-ui/wizard.js +73 -78
  110. package/dist/standalone/uninstall.mjs +1114 -386
  111. package/dist/state/cleanup-registry.js +14 -3
  112. package/dist/state/cooldown-held.js +36 -13
  113. package/dist/state/locks.js +239 -34
  114. package/dist/state/sessions.js +2 -2
  115. package/dist/util/sh-escape.js +6 -0
  116. package/dist/util/tty-prompt.js +13 -4
  117. package/dist/verify/local.js +60 -10
  118. package/dist/verify/package-check.js +35 -49
  119. package/dist/verify/preflight.js +59 -33
  120. package/dist/verify/render.js +11 -12
  121. package/npm-shrinkwrap.json +714 -213
  122. package/package.json +4 -2
@@ -0,0 +1,101 @@
1
+ import { sanitize } from "../security/sanitize.js";
2
+ import { artifactDisplayName, safeDecodeUriComponent } from "../proxy/metadata-map.js";
3
+ export function normalizeVerdict(value, target, identity, streamedSha256) {
4
+ if (!isRecord(value)) {
5
+ throw new Error("Dependency Guardian API returned a malformed verdict");
6
+ }
7
+ const verdict = value.verdict;
8
+ if (verdict !== "pass" && verdict !== "warn" && verdict !== "block") {
9
+ throw new Error("Dependency Guardian API returned a malformed verdict");
10
+ }
11
+ const scannedSha256 = typeof value.scannedSha256 === "string" ? value.scannedSha256.toLowerCase() : "";
12
+ if (scannedSha256.length > 0 && scannedSha256 !== streamedSha256) {
13
+ return {
14
+ verdict: "block",
15
+ packageName: artifactDisplayName(identity),
16
+ cause: "hash-mismatch",
17
+ reason: `server scanned SHA-256 ${scannedSha256} did not match streamed artifact SHA-256 ${streamedSha256}`
18
+ };
19
+ }
20
+ const cause = typeof value.cause === "string" ? value.cause : undefined;
21
+ const cooldown = parseCooldownInfo(value.cooldown);
22
+ const provenance = parseProvenance(value.provenance);
23
+ const dashboardUrl = safeDashboardUrl(value.dashboardUrl);
24
+ return {
25
+ verdict,
26
+ packageName: typeof value.packageName === "string" ? sanitize(value.packageName) : artifactDisplayName(identity) || packageNameFromUrl(target),
27
+ ...(isProxyCause(cause) ? { cause } : {}),
28
+ reason: typeof value.reason === "string" ? sanitize(value.reason) : `API verdict ${verdict}`,
29
+ ...(dashboardUrl ? { dashboardUrl } : {}),
30
+ ...(value.unauthenticated === true ? { unauthenticated: true } : {}),
31
+ ...(cooldown ? { cooldown } : {}),
32
+ ...(provenance ? { provenance } : {})
33
+ };
34
+ }
35
+ export function safeDashboardUrl(value) {
36
+ if (typeof value !== "string") {
37
+ return undefined;
38
+ }
39
+ try {
40
+ const url = new URL(sanitize(value));
41
+ return url.protocol === "https:" ? url.toString() : undefined;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ export function parseCooldownInfo(value) {
48
+ if (!isRecord(value) || typeof value.requiredDays !== "number" || !Number.isFinite(value.requiredDays)) {
49
+ return undefined;
50
+ }
51
+ return {
52
+ requiredDays: value.requiredDays,
53
+ ...(typeof value.ageDays === "number" && Number.isFinite(value.ageDays) ? { ageDays: value.ageDays } : {}),
54
+ ...(typeof value.publishedAt === "string" ? { publishedAt: sanitize(value.publishedAt) } : {}),
55
+ ...(typeof value.eligibleAt === "string" ? { eligibleAt: sanitize(value.eligibleAt) } : {})
56
+ };
57
+ }
58
+ function parseProvenance(value) {
59
+ if (!isRecord(value)) {
60
+ return undefined;
61
+ }
62
+ const status = value.status;
63
+ if (status !== "attested" && status !== "none" && status !== "unknown") {
64
+ return undefined;
65
+ }
66
+ const downgrade = isRecord(value.downgrade) && typeof value.downgrade.fromVersion === "string"
67
+ ? { fromVersion: sanitize(value.downgrade.fromVersion) }
68
+ : undefined;
69
+ return {
70
+ status,
71
+ ...(typeof value.predicateType === "string" ? { predicateType: sanitize(value.predicateType) } : {}),
72
+ ...(downgrade ? { downgrade } : {})
73
+ };
74
+ }
75
+ export function packageNameFromUrl(url) {
76
+ const name = url.pathname.split("/").filter(Boolean).pop();
77
+ return name ? safeDecodeUriComponent(name) : url.hostname;
78
+ }
79
+ export function isRecord(value) {
80
+ return typeof value === "object" && value !== null;
81
+ }
82
+ export function isProxyCause(value) {
83
+ return [
84
+ "pass",
85
+ "warn",
86
+ "malware",
87
+ "policy",
88
+ "license",
89
+ "hash-mismatch",
90
+ "private-upload-disabled",
91
+ "needs-login",
92
+ "api-unavailable",
93
+ "quota-exceeded",
94
+ "api-timeout",
95
+ "registry-timeout",
96
+ "analysis-incomplete",
97
+ "cooldown",
98
+ "unsupported-manager",
99
+ "proxy-setup-failure"
100
+ ].includes(value ?? "");
101
+ }
@@ -0,0 +1,214 @@
1
+ import { isRecord, normalizeVerdict } from "./verdict-core.js";
2
+ import { resolveCooldownRequest } from "./cooldown-request.js";
3
+ import { hostMatchesList } from "./host-match.js";
4
+ import { artifactDisplayName, artifactUrlHash } from "../proxy/metadata-map.js";
5
+ import { loadPreverifiedMap, preverifiedKey } from "../proxy/preverified.js";
6
+ import { sanitize } from "../security/sanitize.js";
7
+ import { identityHeaders } from "../api/analyze.js";
8
+ import { dgVersion } from "../commands/version.js";
9
+ import { redactSecrets } from "../launcher/output-redaction.js";
10
+ import { envAuthToken } from "../auth/env-token.js";
11
+ export async function lookupVerdict(options, target, sha256, upstream, identity) {
12
+ if (shouldUseScanTarball(options, target, identity)) {
13
+ return lookupScanTarballVerdict(options, target, sha256, upstream, identity);
14
+ }
15
+ const preverified = preverifiedVerdict(options, target, sha256, identity);
16
+ if (preverified) {
17
+ return preverified;
18
+ }
19
+ const cooldown = resolveCooldownRequest(options, identity);
20
+ const controller = new AbortController();
21
+ const timeout = setTimeout(() => controller.abort(), options.verdictTimeoutMs ?? installVerdictTimeoutMs(options.env));
22
+ try {
23
+ const response = await fetch(`${options.apiBaseUrl}/v1/install-verdict`, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ "X-Dg-Version": dgVersion(),
28
+ ...identityHeaders(options.env)
29
+ },
30
+ body: JSON.stringify({
31
+ manager: options.classification.manager,
32
+ action: options.classification.action,
33
+ url: redactSecrets(target.toString()),
34
+ artifactUrlHash: artifactUrlHash(target),
35
+ ecosystem: identity.ecosystem,
36
+ name: identity.name,
37
+ version: identity.version,
38
+ registryHost: identity.registryHost,
39
+ sourceKind: identity.sourceKind,
40
+ sha256,
41
+ statusCode: upstream.statusCode,
42
+ contentType: headerValue(upstream.headers["content-type"]),
43
+ ...(cooldown ? { cooldown } : {})
44
+ }),
45
+ signal: controller.signal
46
+ });
47
+ if (response.status === 402 || response.status === 429) {
48
+ const body = (await response.json().catch(() => ({})));
49
+ return {
50
+ verdict: "block",
51
+ packageName: artifactDisplayName(identity),
52
+ cause: "quota-exceeded",
53
+ reason: "You've reached your monthly scan limit. Upgrade at westbayberry.com/pricing or wait for it to reset.",
54
+ ...(typeof body.resetsAt === "string" ? { resetsAt: sanitize(body.resetsAt) } : {}),
55
+ quotaBehavior: body.quotaBehavior === "pass" ? "pass" : "block"
56
+ };
57
+ }
58
+ if (response.status === 401) {
59
+ return {
60
+ verdict: "block",
61
+ packageName: artifactDisplayName(identity),
62
+ cause: "needs-login",
63
+ unauthenticated: true,
64
+ reason: "Checking a package from the registry before it installs requires sign-in."
65
+ };
66
+ }
67
+ if (!response.ok) {
68
+ return {
69
+ verdict: "block",
70
+ packageName: artifactDisplayName(identity),
71
+ cause: "api-unavailable",
72
+ reason: `Dependency Guardian API returned ${response.status}. Run 'dg doctor' and 'dg login' to verify API access.`
73
+ };
74
+ }
75
+ return normalizeVerdict(await response.json(), target, identity, sha256);
76
+ }
77
+ finally {
78
+ clearTimeout(timeout);
79
+ }
80
+ }
81
+ async function lookupScanTarballVerdict(options, target, sha256, upstream, identity) {
82
+ const body = upstream.body ?? Buffer.alloc(0);
83
+ const uploadPolicy = scanTarballUploadPolicy(options.env);
84
+ if (!uploadPolicy.enabled) {
85
+ return {
86
+ verdict: "block",
87
+ packageName: artifactDisplayName(identity),
88
+ cause: "private-upload-disabled",
89
+ reason: `private artifact scan upload is disabled for ${target.hostname}`
90
+ };
91
+ }
92
+ if (!uploadPolicy.token) {
93
+ return {
94
+ verdict: "block",
95
+ packageName: artifactDisplayName(identity),
96
+ cause: "private-upload-disabled",
97
+ reason: "private artifact scan upload requires DG_API_TOKEN"
98
+ };
99
+ }
100
+ if (body.length > uploadPolicy.maxBytes) {
101
+ return {
102
+ verdict: "block",
103
+ packageName: artifactDisplayName(identity),
104
+ cause: "private-upload-disabled",
105
+ reason: `private artifact is ${body.length} bytes, above the ${uploadPolicy.maxBytes} byte scan-tarball upload limit`
106
+ };
107
+ }
108
+ const controller = new AbortController();
109
+ const timeout = setTimeout(() => controller.abort(), uploadPolicy.timeoutMs);
110
+ try {
111
+ const response = await fetch(`${options.apiBaseUrl}/v1/scan-tarball`, {
112
+ method: "POST",
113
+ headers: {
114
+ "Authorization": `Bearer ${uploadPolicy.token}`,
115
+ "Content-Type": "application/octet-stream",
116
+ "X-DG-Action": options.classification.action,
117
+ "X-DG-Artifact-SHA256": sha256,
118
+ "X-DG-Artifact-URL-Hash": artifactUrlHash(target),
119
+ "X-DG-Cache-Key": `sha256:${sha256}`,
120
+ "X-DG-Ecosystem": identity.ecosystem,
121
+ "X-DG-Manager": options.classification.manager,
122
+ "X-DG-Package-Name": identity.name,
123
+ "X-DG-Package-Version": identity.version,
124
+ "X-DG-Privacy": "private-artifact",
125
+ "X-DG-Registry-Host": identity.registryHost,
126
+ "X-DG-Source-Kind": identity.sourceKind
127
+ },
128
+ body,
129
+ signal: controller.signal
130
+ });
131
+ if (!response.ok) {
132
+ throw new Error(`Dependency Guardian scan-tarball API returned ${response.status}`);
133
+ }
134
+ return normalizeScanTarballVerdict(await response.json(), target, identity, sha256);
135
+ }
136
+ finally {
137
+ clearTimeout(timeout);
138
+ }
139
+ }
140
+ // A preflight batch-analyze already verdicted this exact name@version (and the
141
+ // user accepted any warns), so a per-artifact API round-trip would re-answer
142
+ // the same question. Trust it only when the identity came from registry
143
+ // metadata and the preflight applied the same cooldown gate the proxy would;
144
+ // the synthesized verdict goes through normalizeVerdict so the scanned-SHA
145
+ // cross-check blocks tampered bytes exactly like a server response would.
146
+ function preverifiedVerdict(options, target, streamedSha256, identity) {
147
+ if (identity.sourceKind !== "registry-metadata") {
148
+ return null;
149
+ }
150
+ const entry = loadPreverifiedMap(options.session.dir).get(preverifiedKey(identity.ecosystem, identity.name, identity.version));
151
+ if (!entry) {
152
+ return null;
153
+ }
154
+ if (!entry.cooldownEvaluated && resolveCooldownRequest(options, identity) !== undefined) {
155
+ return null;
156
+ }
157
+ if (!entry.scannedSha256) {
158
+ // No byte-level fingerprint to cross-check against the streamed artifact, so
159
+ // a preverified pass/warn can't prove the downloaded bytes match what was
160
+ // screened (a metadata-only preflight, or a registry swap between preflight
161
+ // and fetch). Fall through to a real scan of the streamed bytes — TOCTOU
162
+ // defense — rather than honoring the verdict for whatever now arrives.
163
+ return null;
164
+ }
165
+ return normalizeVerdict({
166
+ verdict: entry.action,
167
+ cause: entry.action,
168
+ packageName: `${identity.name}@${identity.version}`,
169
+ reason: entry.reason ?? (entry.action === "pass" ? "verified before install" : "flagged for review"),
170
+ ...(entry.scannedSha256 ? { scannedSha256: entry.scannedSha256 } : {})
171
+ }, target, identity, streamedSha256);
172
+ }
173
+ function normalizeScanTarballVerdict(value, target, identity, streamedSha256) {
174
+ if (!isRecord(value) || typeof value.scannedSha256 !== "string" || value.scannedSha256.length === 0) {
175
+ throw new Error("Dependency Guardian scan-tarball API did not return scannedSha256");
176
+ }
177
+ return normalizeVerdict(value, target, identity, streamedSha256);
178
+ }
179
+ function shouldUseScanTarball(options, target, identity) {
180
+ if (identity.sourceKind !== "url-fallback") {
181
+ return false;
182
+ }
183
+ return hostMatchesList(target.hostname, options.env.DG_PRIVATE_REGISTRY_HOSTS ?? "");
184
+ }
185
+ function scanTarballUploadPolicy(env) {
186
+ return {
187
+ enabled: env.DG_SCAN_TARBALL_UPLOAD === "1" || env.DG_SCAN_TARBALL_UPLOAD === "true",
188
+ maxBytes: parsePositiveInteger(env.DG_SCAN_TARBALL_MAX_BYTES, 50 * 1024 * 1024),
189
+ timeoutMs: parsePositiveInteger(env.DG_SCAN_TARBALL_TIMEOUT_MS, 5_000),
190
+ token: envAuthToken(env) ?? ""
191
+ };
192
+ }
193
+ function installVerdictTimeoutMs(env) {
194
+ return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS, 240_000);
195
+ }
196
+ function parsePositiveInteger(value, fallback) {
197
+ if (!value) {
198
+ return fallback;
199
+ }
200
+ const parsed = Number.parseInt(value, 10);
201
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
202
+ }
203
+ function headerValue(value) {
204
+ if (Array.isArray(value)) {
205
+ return value.join(",");
206
+ }
207
+ if (typeof value === "number") {
208
+ return String(value);
209
+ }
210
+ if (typeof value === "string") {
211
+ return value;
212
+ }
213
+ return "";
214
+ }