@westbayberry/dg 1.3.2 → 2.0.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.
Files changed (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54141
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,909 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import { BufferBudgetError, collectBounded } from "./buffer-budget.js";
4
+ import { readFileSync } from "node:fs";
5
+ import { createServer, request as httpRequest } from "node:http";
6
+ import { request as httpsRequest } from "node:https";
7
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { connect } from "node:net";
10
+ import { createSecureContext, createServer as createTlsServer, connect as tlsConnect } from "node:tls";
11
+ import { createEphemeralCertificateAuthority } from "./ca.js";
12
+ import { shouldMitmHost } from "./classify-host.js";
13
+ import { enforceProtectedInstall } from "./enforcement.js";
14
+ import { artifactDisplayName, artifactUrlHash, extractRegistryMetadataIdentities, isRegistryIndexRequest, resolveArtifactIdentity } from "./metadata-map.js";
15
+ import { authorityFor, connectViaUpstreamProxy, selectUpstreamProxy } from "./upstream-proxy.js";
16
+ import { redactSecrets } from "../launcher/output-redaction.js";
17
+ import { envAuthToken } from "../auth/env-token.js";
18
+ export async function startProductionHttpProxy(options) {
19
+ const ca = createEphemeralCertificateAuthority(options.session.files.ca);
20
+ const state = {
21
+ ready: false,
22
+ port: 0,
23
+ decisions: [],
24
+ inflight: [],
25
+ hashes: [],
26
+ identities: [],
27
+ events: []
28
+ };
29
+ const activeSockets = new Set();
30
+ const server = createServer((request, response) => {
31
+ handleProxyRequest(request, response, options, state).catch((error) => {
32
+ const decision = recordDecision(options, state, {
33
+ verdict: "block",
34
+ packageName: options.classification.manager,
35
+ cause: "proxy-setup-failure",
36
+ reason: error instanceof Error ? error.message : "proxy request failed"
37
+ });
38
+ sendBlocked(response, decision);
39
+ });
40
+ });
41
+ server.on("connection", (socket) => trackSocket(activeSockets, socket));
42
+ server.on("connect", (request, socket, head) => {
43
+ const clientSocket = socket;
44
+ handleConnectRequest(request, clientSocket, head, options, state, ca, activeSockets).catch((error) => {
45
+ const decision = recordDecision(options, state, {
46
+ verdict: "block",
47
+ packageName: request.url ?? options.classification.manager,
48
+ cause: "proxy-setup-failure",
49
+ reason: error instanceof Error ? error.message : "TLS CONNECT proxying failed"
50
+ });
51
+ clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\nDependency Guardian blocked ${redactSecrets(decision.packageName)}: ${redactSecrets(decision.reason)}\n`);
52
+ });
53
+ });
54
+ await listen(server, options.listenHost ?? "127.0.0.1", options.listenPort ?? 0);
55
+ const address = server.address();
56
+ if (typeof address !== "object" || address === null) {
57
+ throw new Error("production proxy did not bind a TCP port");
58
+ }
59
+ state.ready = true;
60
+ state.port = address.port;
61
+ writeProxyState(options.session, state);
62
+ return {
63
+ port: address.port,
64
+ close: () => closeServer(server, activeSockets)
65
+ };
66
+ }
67
+ export function readProxySessionState(session) {
68
+ try {
69
+ const parsed = JSON.parse(readFileSync(session.files.proxy, "utf8"));
70
+ return {
71
+ ready: parsed.ready === true,
72
+ port: typeof parsed.port === "number" ? parsed.port : 0,
73
+ decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
74
+ inflight: Array.isArray(parsed.inflight) ? parsed.inflight.filter((name) => typeof name === "string") : [],
75
+ hashes: Array.isArray(parsed.hashes) ? parsed.hashes : [],
76
+ identities: Array.isArray(parsed.identities) ? parsed.identities : [],
77
+ events: Array.isArray(parsed.events) ? parsed.events.filter((event) => typeof event === "string") : []
78
+ };
79
+ }
80
+ catch {
81
+ return {
82
+ ready: false,
83
+ port: 0,
84
+ decisions: [],
85
+ inflight: [],
86
+ hashes: [],
87
+ identities: [],
88
+ events: []
89
+ };
90
+ }
91
+ }
92
+ function listen(server, host, port) {
93
+ return new Promise((resolve, reject) => {
94
+ server.once("error", reject);
95
+ server.listen(port, host, () => {
96
+ server.off("error", reject);
97
+ resolve();
98
+ });
99
+ });
100
+ }
101
+ function closeServer(server, activeSockets) {
102
+ for (const socket of activeSockets) {
103
+ socket.destroy();
104
+ }
105
+ return new Promise((resolve) => {
106
+ server.close(() => resolve());
107
+ });
108
+ }
109
+ function trackSocket(activeSockets, socket) {
110
+ activeSockets.add(socket);
111
+ socket.once("close", () => {
112
+ activeSockets.delete(socket);
113
+ });
114
+ }
115
+ async function handleProxyRequest(request, response, options, state) {
116
+ const target = parseProxyTarget(request);
117
+ if (!target || (target.protocol !== "http:" && target.protocol !== "https:")) {
118
+ const decision = recordDecision(options, state, {
119
+ verdict: "block",
120
+ packageName: target?.hostname ?? "unknown-artifact",
121
+ cause: "proxy-setup-failure",
122
+ reason: "proxy request did not include a supported HTTP(S) artifact URL"
123
+ });
124
+ sendBlocked(response, decision);
125
+ return;
126
+ }
127
+ await handleArtifactRequest(request, response, target, options, state);
128
+ }
129
+ async function handleConnectRequest(request, clientSocket, head, options, state, ca, activeSockets) {
130
+ const target = parseConnectTarget(request.url);
131
+ if (!target) {
132
+ clientSocket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
133
+ return;
134
+ }
135
+ if (!shouldMitmHost(target.hostname, options.env)) {
136
+ state.events = [...state.events, `tunnel:${redactSecrets(authorityFor(target))}`];
137
+ writeProxyState(options.session, state);
138
+ await blindTunnel(clientSocket, head, target, options, activeSockets);
139
+ return;
140
+ }
141
+ state.events = [...state.events, `mitm:${redactSecrets(target.hostname)}`];
142
+ writeProxyState(options.session, state);
143
+ await mitmTunnel(clientSocket, head, target, options, state, ca, activeSockets);
144
+ }
145
+ const REDIRECT_LIMIT = 5;
146
+ function isRedirectStatus(statusCode) {
147
+ return [301, 302, 303, 307, 308].includes(statusCode);
148
+ }
149
+ function isPrivateNetworkHost(url) {
150
+ const host = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
151
+ if (host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "::" || host === "::1") {
152
+ return true;
153
+ }
154
+ if (/^(127|10|0)\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host) || /^172\.(1[6-9]|2[0-9]|3[01])\./.test(host)) {
155
+ return true;
156
+ }
157
+ if (/^::ffff:(127|10|0)\./.test(host) || /^::ffff:192\.168\./.test(host) || /^::ffff:169\.254\./.test(host) || /^::ffff:172\.(1[6-9]|2[0-9]|3[01])\./.test(host)) {
158
+ return true;
159
+ }
160
+ return /^(fe80|f[cd][0-9a-f]{2}):/.test(host);
161
+ }
162
+ function syntheticGetRequest(headers) {
163
+ const request = new EventEmitter();
164
+ request.method = "GET";
165
+ request.headers = headers;
166
+ request.complete = true;
167
+ process.nextTick(() => request.emit("end"));
168
+ return request;
169
+ }
170
+ function redirectHopHeaders(request, from, next) {
171
+ const headers = { ...request.headers };
172
+ if (from.host !== next.host) {
173
+ delete headers.authorization;
174
+ delete headers.cookie;
175
+ }
176
+ return headers;
177
+ }
178
+ async function fetchUpstreamFollowingRedirects(request, target, env) {
179
+ let current = target;
180
+ let upstream = await fetchUpstream(request, current, env);
181
+ for (let hop = 0; isRedirectStatus(upstream.statusCode); hop += 1) {
182
+ if (hop >= REDIRECT_LIMIT) {
183
+ throw new Error(`registry redirect chain exceeded ${REDIRECT_LIMIT} hops for ${current.host}`);
184
+ }
185
+ const location = headerValue(upstream.headers.location);
186
+ if (!location) {
187
+ throw new Error(`registry returned a ${upstream.statusCode} redirect with no Location header for ${current.host}`);
188
+ }
189
+ const next = new URL(location, current);
190
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
191
+ throw new Error(`registry redirected to unsupported protocol ${next.protocol}`);
192
+ }
193
+ if (isPrivateNetworkHost(next) && !isPrivateNetworkHost(target)) {
194
+ throw new Error(`registry redirected a public artifact request into a private address (${next.hostname})`);
195
+ }
196
+ upstream = await fetchUpstream(syntheticGetRequest(redirectHopHeaders(request, current, next)), next, env);
197
+ current = next;
198
+ }
199
+ return upstream;
200
+ }
201
+ const ARTIFACT_CONDITIONAL_HEADERS = ["range", "if-range", "if-none-match", "if-modified-since"];
202
+ async function handleArtifactRequest(request, response, target, options, state) {
203
+ if (!isRegistryIndexRequest(target)) {
204
+ for (const header of ARTIFACT_CONDITIONAL_HEADERS) {
205
+ delete request.headers[header];
206
+ }
207
+ }
208
+ const upstream = await fetchUpstreamFollowingRedirects(request, target, options.env).catch((error) => {
209
+ const message = error instanceof Error ? error.message : "registry request failed";
210
+ const decision = recordDecision(options, state, {
211
+ verdict: "block",
212
+ packageName: packageNameFromUrl(target),
213
+ cause: error instanceof BufferBudgetError ? "proxy-setup-failure" : "registry-timeout",
214
+ reason: message
215
+ });
216
+ sendBlocked(response, decision);
217
+ return null;
218
+ });
219
+ if (!upstream) {
220
+ return;
221
+ }
222
+ const metadataIdentities = extractRegistryMetadataIdentities(target, upstream);
223
+ if (metadataIdentities.length > 0 || isRegistryIndexRequest(target)) {
224
+ if (metadataIdentities.length > 0) {
225
+ state.identities = mergeIdentities(state.identities, metadataIdentities);
226
+ state.events = [...state.events, `metadata:${target.hostname}:${metadataIdentities.length}`];
227
+ }
228
+ else {
229
+ state.events = [...state.events, `index:${target.hostname}`];
230
+ }
231
+ writeProxyState(options.session, state);
232
+ response.writeHead(upstream.statusCode, upstream.headers);
233
+ response.end(upstream.body);
234
+ return;
235
+ }
236
+ if (upstream.statusCode === 206) {
237
+ const decision = recordDecision(options, state, {
238
+ verdict: "block",
239
+ packageName: packageNameFromUrl(target),
240
+ cause: "policy",
241
+ reason: "registry returned partial content — a partial artifact cannot be verified"
242
+ });
243
+ sendBlocked(response, decision);
244
+ return;
245
+ }
246
+ if (upstream.statusCode < 200 || upstream.statusCode >= 300) {
247
+ // A non-success status is the registry's own error (e.g. 404 for a version
248
+ // that does not exist) — not an installable artifact. Pass it through so the
249
+ // package manager handles it (404s drive version resolution); scanning an
250
+ // error body as if it were a package would mis-identify it.
251
+ state.events = [...state.events, `passthrough:${upstream.statusCode}:${target.hostname}`];
252
+ writeProxyState(options.session, state);
253
+ response.writeHead(upstream.statusCode, upstream.headers);
254
+ response.end(upstream.body);
255
+ return;
256
+ }
257
+ const sha256 = createHash("sha256").update(upstream.body).digest("hex");
258
+ const identity = resolveArtifactIdentity(target, state.identities, options.classification);
259
+ if (identity.kind === "ambiguous") {
260
+ state.hashes = [...state.hashes, {
261
+ url: redactSecrets(target.toString()),
262
+ sha256
263
+ }];
264
+ const decision = recordDecision(options, state, {
265
+ verdict: "block",
266
+ packageName: identity.packageName,
267
+ cause: "policy",
268
+ reason: identity.reason
269
+ });
270
+ sendBlocked(response, decision);
271
+ return;
272
+ }
273
+ state.hashes = [...state.hashes, {
274
+ url: redactSecrets(target.toString()),
275
+ sha256,
276
+ identity: identity.identity
277
+ }];
278
+ const inflightName = artifactDisplayName(identity.identity);
279
+ state.inflight = [...state.inflight, inflightName];
280
+ writeProxyState(options.session, state);
281
+ const verdict = await lookupVerdict(options, target, sha256, upstream, identity.identity).catch((error) => ({
282
+ verdict: "block",
283
+ packageName: artifactDisplayName(identity.identity),
284
+ cause: "api-timeout",
285
+ reason: error instanceof Error ? error.message : "Dependency Guardian API verdict lookup failed"
286
+ }));
287
+ state.inflight = removeFirst(state.inflight, inflightName);
288
+ const decision = recordDecision(options, state, verdict);
289
+ if (decision.action === "block") {
290
+ sendBlocked(response, decision);
291
+ return;
292
+ }
293
+ response.writeHead(upstream.statusCode, upstream.headers);
294
+ response.end(upstream.body);
295
+ }
296
+ async function blindTunnel(clientSocket, head, target, options, activeSockets) {
297
+ const upstreamProxy = selectUpstreamProxy(target, options.env);
298
+ const upstreamSocket = upstreamProxy
299
+ ? await connectViaUpstreamProxy(target, upstreamProxy)
300
+ : await connectDirect(target);
301
+ trackSocket(activeSockets, upstreamSocket);
302
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
303
+ if (head.length > 0) {
304
+ upstreamSocket.write(head);
305
+ }
306
+ upstreamSocket.pipe(clientSocket);
307
+ clientSocket.pipe(upstreamSocket);
308
+ }
309
+ async function mitmTunnel(clientSocket, head, target, options, state, ca, activeSockets) {
310
+ const leaf = ca.leafForHost(target.hostname);
311
+ const secureContext = createSecureContext({
312
+ cert: leaf.certPem,
313
+ key: leaf.keyPem
314
+ });
315
+ const innerHttp = createServer((request, response) => {
316
+ const artifactTarget = new URL(`${target.protocol}//${target.host}${request.url ?? "/"}`);
317
+ handleArtifactRequest(request, response, artifactTarget, options, state).catch((error) => {
318
+ const decision = recordDecision(options, state, {
319
+ verdict: "block",
320
+ packageName: artifactTarget.hostname,
321
+ cause: "proxy-setup-failure",
322
+ reason: error instanceof Error ? error.message : "MITM request failed"
323
+ });
324
+ sendBlocked(response, decision);
325
+ });
326
+ });
327
+ const tlsServer = createTlsServer({
328
+ SNICallback: (_servername, callback) => callback(null, secureContext),
329
+ cert: leaf.certPem,
330
+ key: leaf.keyPem,
331
+ ALPNProtocols: ["http/1.1"]
332
+ }, (tlsSocket) => {
333
+ trackSocket(activeSockets, tlsSocket);
334
+ innerHttp.emit("connection", tlsSocket);
335
+ });
336
+ tlsServer.once("tlsClientError", () => {
337
+ clientSocket.destroy();
338
+ });
339
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
340
+ if (head.length > 0) {
341
+ clientSocket.unshift(head);
342
+ }
343
+ tlsServer.emit("connection", clientSocket);
344
+ }
345
+ function parseProxyTarget(request) {
346
+ const rawUrl = request.url ?? "";
347
+ try {
348
+ if (/^https?:\/\//.test(rawUrl)) {
349
+ return new URL(rawUrl);
350
+ }
351
+ const host = request.headers.host;
352
+ if (!host) {
353
+ return null;
354
+ }
355
+ return new URL(`http://${host}${rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`}`);
356
+ }
357
+ catch {
358
+ return null;
359
+ }
360
+ }
361
+ function parseConnectTarget(authority) {
362
+ if (!authority) {
363
+ return null;
364
+ }
365
+ try {
366
+ const normalized = authority.startsWith("[")
367
+ ? authority
368
+ : authority.replace(/^([^:]+)$/, "$1:443");
369
+ return new URL(`https://${normalized}`);
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ }
375
+ function connectDirect(target) {
376
+ return new Promise((resolve, reject) => {
377
+ const socket = connect(Number(target.port || "443"), upstreamHostname(target));
378
+ socket.once("connect", () => resolve(socket));
379
+ socket.once("error", reject);
380
+ });
381
+ }
382
+ function upstreamHostname(target, env = process.env) {
383
+ const hostname = target.hostname.replace(/^\[(.*)\]$/, "$1");
384
+ return testUpstreamHostMap(env).get(hostname) ?? hostname;
385
+ }
386
+ function testUpstreamHostMap(env) {
387
+ const entries = (env.DG_TEST_UPSTREAM_HOST_MAP ?? "")
388
+ .split(",")
389
+ .map((entry) => entry.trim())
390
+ .filter(Boolean)
391
+ .map((entry) => {
392
+ const separator = entry.indexOf("=");
393
+ if (separator <= 0) {
394
+ return null;
395
+ }
396
+ return [entry.slice(0, separator), entry.slice(separator + 1)];
397
+ })
398
+ .filter((entry) => entry !== null);
399
+ return new Map(entries);
400
+ }
401
+ function fetchUpstream(request, target, env) {
402
+ const upstreamProxy = selectUpstreamProxy(target, env);
403
+ if (upstreamProxy && target.protocol === "http:") {
404
+ return fetchHttpViaProxy(request, target, upstreamProxy);
405
+ }
406
+ if (upstreamProxy && target.protocol === "https:") {
407
+ return fetchHttpsViaProxy(request, target, upstreamProxy, env);
408
+ }
409
+ if (target.protocol === "https:") {
410
+ return fetchHttpsDirect(request, target, env);
411
+ }
412
+ return new Promise((resolve, reject) => {
413
+ const upstream = httpRequest({
414
+ hostname: upstreamHostname(target, env),
415
+ port: target.port ? Number(target.port) : 80,
416
+ path: `${target.pathname}${target.search}`,
417
+ method: request.method,
418
+ headers: request.headers
419
+ }, (upstreamResponse) => {
420
+ collectBounded(upstreamResponse, { label: target.toString() })
421
+ .then((body) => resolve({
422
+ statusCode: upstreamResponse.statusCode ?? 502,
423
+ headers: responseHeaders(upstreamResponse),
424
+ body
425
+ }))
426
+ .catch(reject);
427
+ });
428
+ upstream.on("error", reject);
429
+ request.on("data", (chunk) => upstream.write(chunk));
430
+ request.on("end", () => upstream.end());
431
+ request.on("error", reject);
432
+ request.once("close", () => {
433
+ if (!request.complete) {
434
+ upstream.destroy();
435
+ }
436
+ });
437
+ });
438
+ }
439
+ function fetchHttpViaProxy(request, target, upstreamProxy) {
440
+ return new Promise((resolve, reject) => {
441
+ const headers = {
442
+ ...request.headers,
443
+ ...(upstreamProxy.authorizationHeader ? { "Proxy-Authorization": upstreamProxy.authorizationHeader } : {})
444
+ };
445
+ const upstream = httpRequest({
446
+ hostname: upstreamProxy.url.hostname,
447
+ port: upstreamProxy.url.port ? Number(upstreamProxy.url.port) : 80,
448
+ path: target.toString(),
449
+ method: request.method,
450
+ headers
451
+ }, (upstreamResponse) => {
452
+ collectBounded(upstreamResponse, { label: target.toString() })
453
+ .then((body) => resolve({
454
+ statusCode: upstreamResponse.statusCode ?? 502,
455
+ headers: responseHeaders(upstreamResponse),
456
+ body
457
+ }))
458
+ .catch(reject);
459
+ });
460
+ upstream.on("error", reject);
461
+ request.on("data", (chunk) => upstream.write(chunk));
462
+ request.on("end", () => upstream.end());
463
+ request.on("error", reject);
464
+ request.once("close", () => {
465
+ if (!request.complete) {
466
+ upstream.destroy();
467
+ }
468
+ });
469
+ });
470
+ }
471
+ function fetchHttpsDirect(request, target, env) {
472
+ return new Promise((resolve, reject) => {
473
+ const upstream = httpsRequest({
474
+ hostname: upstreamHostname(target, env),
475
+ port: target.port ? Number(target.port) : 443,
476
+ path: `${target.pathname}${target.search}`,
477
+ method: request.method,
478
+ headers: upstreamRequestHeaders(request, target),
479
+ ...upstreamTlsOptions(env)
480
+ }, (upstreamResponse) => {
481
+ collectBounded(upstreamResponse, { label: target.toString() })
482
+ .then((body) => resolve({
483
+ statusCode: upstreamResponse.statusCode ?? 502,
484
+ headers: responseHeaders(upstreamResponse),
485
+ body
486
+ }))
487
+ .catch(reject);
488
+ });
489
+ upstream.on("error", reject);
490
+ request.on("data", (chunk) => upstream.write(chunk));
491
+ request.on("end", () => upstream.end());
492
+ request.on("error", reject);
493
+ request.once("close", () => {
494
+ if (!request.complete) {
495
+ upstream.destroy();
496
+ }
497
+ });
498
+ });
499
+ }
500
+ async function fetchHttpsViaProxy(request, target, upstreamProxy, env) {
501
+ const tunnel = await connectViaUpstreamProxy(target, upstreamProxy);
502
+ const tlsSocket = await connectTlsOverSocket(tunnel, target, env);
503
+ const requestBody = await readRequestBody(request);
504
+ const rawResponse = await writeRawHttpRequest(tlsSocket, request, target, requestBody);
505
+ return parseRawHttpResponse(rawResponse);
506
+ }
507
+ function connectTlsOverSocket(socket, target, env) {
508
+ return new Promise((resolve, reject) => {
509
+ const tlsSocket = tlsConnect({
510
+ socket,
511
+ servername: target.hostname,
512
+ ALPNProtocols: ["http/1.1"],
513
+ ...upstreamTlsOptions(env)
514
+ }, () => resolve(tlsSocket));
515
+ tlsSocket.once("error", reject);
516
+ });
517
+ }
518
+ function writeRawHttpRequest(tlsSocket, request, target, requestBody) {
519
+ return new Promise((resolve, reject) => {
520
+ collectBounded(tlsSocket, { label: target.toString() }).then(resolve).catch(reject);
521
+ const headers = upstreamRequestHeaders(request, target);
522
+ const lines = [
523
+ `${request.method ?? "GET"} ${target.pathname}${target.search} HTTP/1.1`,
524
+ ...Object.entries(headers).flatMap(([key, value]) => headerLines(key, value)),
525
+ "Connection: close",
526
+ "",
527
+ ""
528
+ ];
529
+ tlsSocket.write(lines.join("\r\n"));
530
+ if (requestBody.length > 0) {
531
+ tlsSocket.write(requestBody);
532
+ }
533
+ tlsSocket.end();
534
+ });
535
+ }
536
+ function parseRawHttpResponse(raw) {
537
+ const headerEnd = raw.indexOf("\r\n\r\n");
538
+ if (headerEnd === -1) {
539
+ throw new Error("upstream registry returned a malformed HTTP response");
540
+ }
541
+ const head = raw.subarray(0, headerEnd).toString("latin1");
542
+ const body = raw.subarray(headerEnd + 4);
543
+ const [statusLine = "", ...headerLinesRaw] = head.split("\r\n");
544
+ const status = /^HTTP\/1\.[01] (\d{3})\b/.exec(statusLine)?.[1];
545
+ const headers = {};
546
+ for (const line of headerLinesRaw) {
547
+ const separator = line.indexOf(":");
548
+ if (separator === -1) {
549
+ continue;
550
+ }
551
+ const key = line.slice(0, separator).toLowerCase();
552
+ const value = line.slice(separator + 1).trim();
553
+ const existing = headers[key];
554
+ headers[key] = existing === undefined ? value : `${headerValue(existing)},${value}`;
555
+ }
556
+ return {
557
+ statusCode: status ? Number(status) : 502,
558
+ headers,
559
+ body: headerValue(headers["transfer-encoding"]).toLowerCase() === "chunked" ? decodeChunkedBody(body) : body
560
+ };
561
+ }
562
+ function upstreamRequestHeaders(request, target) {
563
+ const headers = {};
564
+ for (const [key, value] of Object.entries(request.headers)) {
565
+ const normalized = key.toLowerCase();
566
+ if (["proxy-authorization", "proxy-connection", "connection", "host"].includes(normalized) || value === undefined) {
567
+ continue;
568
+ }
569
+ headers[key] = Array.isArray(value) ? [...value] : value;
570
+ }
571
+ headers.Host = target.host;
572
+ return headers;
573
+ }
574
+ function headerLines(key, value) {
575
+ if (value === undefined) {
576
+ return [];
577
+ }
578
+ if (Array.isArray(value)) {
579
+ return value.map((entry) => `${key}: ${entry}`);
580
+ }
581
+ return [`${key}: ${value}`];
582
+ }
583
+ function upstreamTlsOptions(env) {
584
+ const caPath = env.DG_UPSTREAM_CA_CERT ?? env.NODE_EXTRA_CA_CERTS;
585
+ if (!caPath) {
586
+ return {};
587
+ }
588
+ try {
589
+ return {
590
+ ca: readFileSync(caPath, "utf8")
591
+ };
592
+ }
593
+ catch {
594
+ return {};
595
+ }
596
+ }
597
+ function readRequestBody(request) {
598
+ return collectBounded(request, { label: "proxied request body" });
599
+ }
600
+ function decodeChunkedBody(body) {
601
+ const chunks = [];
602
+ let offset = 0;
603
+ while (offset < body.length) {
604
+ const lineEnd = body.indexOf("\r\n", offset);
605
+ if (lineEnd === -1) {
606
+ break;
607
+ }
608
+ const size = Number.parseInt(body.subarray(offset, lineEnd).toString("latin1"), 16);
609
+ if (!Number.isFinite(size) || size < 0) {
610
+ break;
611
+ }
612
+ if (size === 0) {
613
+ return Buffer.concat(chunks);
614
+ }
615
+ const start = lineEnd + 2;
616
+ const end = start + size;
617
+ chunks.push(body.subarray(start, end));
618
+ offset = end + 2;
619
+ }
620
+ return Buffer.concat(chunks);
621
+ }
622
+ async function lookupVerdict(options, target, sha256, upstream, identity) {
623
+ if (shouldUseScanTarball(options, target, identity)) {
624
+ return lookupScanTarballVerdict(options, target, sha256, upstream, identity);
625
+ }
626
+ const controller = new AbortController();
627
+ const timeout = setTimeout(() => controller.abort(), options.verdictTimeoutMs ?? installVerdictTimeoutMs(options.env));
628
+ try {
629
+ const response = await fetch(`${options.apiBaseUrl}/v1/install-verdict`, {
630
+ method: "POST",
631
+ headers: {
632
+ "Content-Type": "application/json"
633
+ },
634
+ body: JSON.stringify({
635
+ manager: options.classification.manager,
636
+ action: options.classification.action,
637
+ url: target.toString(),
638
+ artifactUrlHash: artifactUrlHash(target),
639
+ ecosystem: identity.ecosystem,
640
+ name: identity.name,
641
+ version: identity.version,
642
+ registryHost: identity.registryHost,
643
+ sourceKind: identity.sourceKind,
644
+ sha256,
645
+ statusCode: upstream.statusCode,
646
+ contentType: headerValue(upstream.headers["content-type"])
647
+ }),
648
+ signal: controller.signal
649
+ });
650
+ if (response.status === 402 || response.status === 429) {
651
+ return {
652
+ verdict: "block",
653
+ packageName: artifactDisplayName(identity),
654
+ cause: "quota-exceeded",
655
+ reason: "You've reached your monthly scan limit. Upgrade at westbayberry.com/pricing or wait for it to reset."
656
+ };
657
+ }
658
+ if (!response.ok) {
659
+ return {
660
+ verdict: "block",
661
+ packageName: artifactDisplayName(identity),
662
+ cause: "api-unavailable",
663
+ reason: `Dependency Guardian API returned ${response.status}. Run 'dg doctor' and 'dg login' to verify API access.`
664
+ };
665
+ }
666
+ return normalizeVerdict(await response.json(), target, identity, sha256);
667
+ }
668
+ finally {
669
+ clearTimeout(timeout);
670
+ }
671
+ }
672
+ async function lookupScanTarballVerdict(options, target, sha256, upstream, identity) {
673
+ const body = upstream.body ?? Buffer.alloc(0);
674
+ const uploadPolicy = scanTarballUploadPolicy(options.env);
675
+ if (!uploadPolicy.enabled) {
676
+ return {
677
+ verdict: "block",
678
+ packageName: artifactDisplayName(identity),
679
+ cause: "private-upload-disabled",
680
+ reason: `private artifact scan upload is disabled for ${target.hostname}`
681
+ };
682
+ }
683
+ if (!uploadPolicy.token) {
684
+ return {
685
+ verdict: "block",
686
+ packageName: artifactDisplayName(identity),
687
+ cause: "private-upload-disabled",
688
+ reason: "private artifact scan upload requires DG_API_TOKEN"
689
+ };
690
+ }
691
+ if (body.length > uploadPolicy.maxBytes) {
692
+ return {
693
+ verdict: "block",
694
+ packageName: artifactDisplayName(identity),
695
+ cause: "private-upload-disabled",
696
+ reason: `private artifact is ${body.length} bytes, above the ${uploadPolicy.maxBytes} byte scan-tarball upload limit`
697
+ };
698
+ }
699
+ const controller = new AbortController();
700
+ const timeout = setTimeout(() => controller.abort(), uploadPolicy.timeoutMs);
701
+ try {
702
+ const response = await fetch(`${options.apiBaseUrl}/v1/scan-tarball`, {
703
+ method: "POST",
704
+ headers: {
705
+ "Authorization": `Bearer ${uploadPolicy.token}`,
706
+ "Content-Type": "application/octet-stream",
707
+ "X-DG-Action": options.classification.action,
708
+ "X-DG-Artifact-SHA256": sha256,
709
+ "X-DG-Artifact-URL-Hash": artifactUrlHash(target),
710
+ "X-DG-Cache-Key": `sha256:${sha256}`,
711
+ "X-DG-Ecosystem": identity.ecosystem,
712
+ "X-DG-Manager": options.classification.manager,
713
+ "X-DG-Package-Name": identity.name,
714
+ "X-DG-Package-Version": identity.version,
715
+ "X-DG-Privacy": "private-artifact",
716
+ "X-DG-Registry-Host": identity.registryHost,
717
+ "X-DG-Source-Kind": identity.sourceKind
718
+ },
719
+ body,
720
+ signal: controller.signal
721
+ });
722
+ if (!response.ok) {
723
+ throw new Error(`Dependency Guardian scan-tarball API returned ${response.status}`);
724
+ }
725
+ return normalizeScanTarballVerdict(await response.json(), target, identity, sha256);
726
+ }
727
+ finally {
728
+ clearTimeout(timeout);
729
+ }
730
+ }
731
+ function normalizeVerdict(value, target, identity, streamedSha256) {
732
+ if (!isRecord(value)) {
733
+ throw new Error("Dependency Guardian API returned a malformed verdict");
734
+ }
735
+ const verdict = value.verdict;
736
+ if (verdict !== "pass" && verdict !== "warn" && verdict !== "block") {
737
+ throw new Error("Dependency Guardian API returned a malformed verdict");
738
+ }
739
+ const scannedSha256 = typeof value.scannedSha256 === "string" ? value.scannedSha256.toLowerCase() : "";
740
+ if (scannedSha256.length > 0 && scannedSha256 !== streamedSha256) {
741
+ return {
742
+ verdict: "block",
743
+ packageName: artifactDisplayName(identity),
744
+ cause: "hash-mismatch",
745
+ reason: `server scanned SHA-256 ${scannedSha256} did not match streamed artifact SHA-256 ${streamedSha256}`
746
+ };
747
+ }
748
+ const cause = typeof value.cause === "string" ? value.cause : undefined;
749
+ return {
750
+ verdict,
751
+ packageName: typeof value.packageName === "string" ? value.packageName : artifactDisplayName(identity) || packageNameFromUrl(target),
752
+ ...(isProxyCause(cause) ? { cause } : {}),
753
+ reason: typeof value.reason === "string" ? value.reason : `API verdict ${verdict}`,
754
+ ...(typeof value.dashboardUrl === "string" ? { dashboardUrl: value.dashboardUrl } : {}),
755
+ ...(value.unauthenticated === true ? { unauthenticated: true } : {})
756
+ };
757
+ }
758
+ function normalizeScanTarballVerdict(value, target, identity, streamedSha256) {
759
+ if (!isRecord(value) || typeof value.scannedSha256 !== "string" || value.scannedSha256.length === 0) {
760
+ throw new Error("Dependency Guardian scan-tarball API did not return scannedSha256");
761
+ }
762
+ return normalizeVerdict(value, target, identity, streamedSha256);
763
+ }
764
+ function shouldUseScanTarball(options, target, identity) {
765
+ if (identity.sourceKind !== "url-fallback") {
766
+ return false;
767
+ }
768
+ return hostMatchesList(target.hostname, options.env.DG_PRIVATE_REGISTRY_HOSTS ?? "");
769
+ }
770
+ function scanTarballUploadPolicy(env) {
771
+ return {
772
+ enabled: env.DG_SCAN_TARBALL_UPLOAD === "1" || env.DG_SCAN_TARBALL_UPLOAD === "true",
773
+ maxBytes: parsePositiveInteger(env.DG_SCAN_TARBALL_MAX_BYTES, 50 * 1024 * 1024),
774
+ timeoutMs: parsePositiveInteger(env.DG_SCAN_TARBALL_TIMEOUT_MS, 5_000),
775
+ token: envAuthToken(env) ?? ""
776
+ };
777
+ }
778
+ function installVerdictTimeoutMs(env) {
779
+ return parsePositiveInteger(env.DG_INSTALL_VERDICT_TIMEOUT_MS, 30_000);
780
+ }
781
+ function parsePositiveInteger(value, fallback) {
782
+ if (!value) {
783
+ return fallback;
784
+ }
785
+ const parsed = Number.parseInt(value, 10);
786
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
787
+ }
788
+ function hostMatchesList(host, rawList) {
789
+ const normalized = normalizeHost(host);
790
+ return rawList.split(",").map((entry) => entry.trim()).filter(Boolean).some((pattern) => hostMatchesPattern(normalized, pattern));
791
+ }
792
+ function hostMatchesPattern(host, pattern) {
793
+ const normalized = normalizeHost(pattern);
794
+ if (normalized.startsWith("*.")) {
795
+ const suffix = normalized.slice(1);
796
+ return host.endsWith(suffix) && host.length > suffix.length;
797
+ }
798
+ return host === normalized;
799
+ }
800
+ function normalizeHost(host) {
801
+ return host.replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "").toLowerCase();
802
+ }
803
+ function mergeIdentities(existing, next) {
804
+ const merged = [...existing];
805
+ for (const identity of next) {
806
+ if (!merged.some((candidate) => candidate.tarballUrl === identity.tarballUrl
807
+ && candidate.ecosystem === identity.ecosystem
808
+ && candidate.name === identity.name
809
+ && candidate.version === identity.version
810
+ && candidate.registryHost === identity.registryHost)) {
811
+ merged.push(identity);
812
+ }
813
+ }
814
+ return merged;
815
+ }
816
+ function recordDecision(options, state, verdict) {
817
+ const decision = enforceProtectedInstall({
818
+ classification: options.classification,
819
+ env: options.env,
820
+ proxyVerdict: verdict,
821
+ ...(options.forceOverride ? { forceOverride: options.forceOverride } : {})
822
+ });
823
+ state.decisions = [...state.decisions, decision];
824
+ state.events = [...state.events, `${decision.action}:${decision.cause}:${redactSecrets(decision.packageName)}`];
825
+ writeProxyState(options.session, state);
826
+ return decision;
827
+ }
828
+ function sendBlocked(response, decision) {
829
+ const body = `Dependency Guardian blocked ${redactSecrets(decision.packageName)}: ${redactSecrets(decision.reason)}\n`;
830
+ response.writeHead(403, {
831
+ "Content-Type": "text/plain; charset=utf-8",
832
+ "Content-Length": Buffer.byteLength(body)
833
+ });
834
+ response.end(body);
835
+ }
836
+ function responseHeaders(response) {
837
+ const headers = {};
838
+ for (const [key, value] of Object.entries(response.headers)) {
839
+ if (value !== undefined) {
840
+ headers[key] = Array.isArray(value) ? [...value] : value;
841
+ }
842
+ }
843
+ return headers;
844
+ }
845
+ function headerValue(value) {
846
+ if (Array.isArray(value)) {
847
+ return value.join(",");
848
+ }
849
+ if (typeof value === "number") {
850
+ return String(value);
851
+ }
852
+ if (typeof value === "string") {
853
+ return value;
854
+ }
855
+ return "";
856
+ }
857
+ function packageNameFromUrl(url) {
858
+ const name = url.pathname.split("/").filter(Boolean).pop();
859
+ return name ? decodeURIComponent(name) : url.hostname;
860
+ }
861
+ function writeProxyState(session, state) {
862
+ mkdirSync(dirname(session.files.proxy), {
863
+ recursive: true,
864
+ mode: 0o700
865
+ });
866
+ const tempPath = `${session.files.proxy}.${process.pid}.${process.hrtime.bigint().toString(36)}.tmp`;
867
+ try {
868
+ writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
869
+ encoding: "utf8",
870
+ flag: "wx",
871
+ mode: 0o600
872
+ });
873
+ renameSync(tempPath, session.files.proxy);
874
+ }
875
+ catch (error) {
876
+ rmSync(tempPath, {
877
+ force: true
878
+ });
879
+ throw error;
880
+ }
881
+ }
882
+ function removeFirst(items, value) {
883
+ const index = items.indexOf(value);
884
+ if (index === -1) {
885
+ return [...items];
886
+ }
887
+ return [...items.slice(0, index), ...items.slice(index + 1)];
888
+ }
889
+ function isRecord(value) {
890
+ return typeof value === "object" && value !== null;
891
+ }
892
+ function isProxyCause(value) {
893
+ return [
894
+ "pass",
895
+ "warn",
896
+ "malware",
897
+ "policy",
898
+ "license",
899
+ "hash-mismatch",
900
+ "private-upload-disabled",
901
+ "api-unavailable",
902
+ "quota-exceeded",
903
+ "api-timeout",
904
+ "registry-timeout",
905
+ "analysis-incomplete",
906
+ "unsupported-manager",
907
+ "proxy-setup-failure"
908
+ ].includes(value ?? "");
909
+ }