@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/server.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { BufferBudgetError, collectBounded } from "./buffer-budget.js";
|
|
3
|
+
import { BufferBudgetError, collectBounded, maxArtifactBytes } from "./buffer-budget.js";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
|
-
import { createServer, request as httpRequest } from "node:http";
|
|
6
|
-
import { request as httpsRequest } from "node:https";
|
|
5
|
+
import { Agent as HttpAgent, createServer, request as httpRequest } from "node:http";
|
|
6
|
+
import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
|
|
7
7
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
9
|
import { connect } from "node:net";
|
|
10
|
-
import { createSecureContext, createServer as createTlsServer, connect as tlsConnect } from "node:tls";
|
|
10
|
+
import { createSecureContext, createServer as createTlsServer, connect as tlsConnect, rootCertificates } from "node:tls";
|
|
11
11
|
import { createEphemeralCertificateAuthority } from "./ca.js";
|
|
12
12
|
import { shouldMitmHost } from "./classify-host.js";
|
|
13
|
-
import {
|
|
13
|
+
import { generateProxyAuthToken, proxyAuthorizationValue, verifyProxyAuthorization, writeProxyAuthToken } from "./auth.js";
|
|
14
|
+
import { enforceProtectedInstall, noteEnforcementSideEffectFailure } from "./enforcement.js";
|
|
15
|
+
import { cooldownRequestParam, isCooldownExemptByDgFile } from "../policy/cooldown.js";
|
|
16
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
14
17
|
import { artifactDisplayName, artifactUrlHash, extractRegistryMetadataIdentities, isRegistryIndexRequest, resolveArtifactIdentity } from "./metadata-map.js";
|
|
18
|
+
import { loadPreverifiedMap, preverifiedKey } from "./preverified.js";
|
|
15
19
|
import { authorityFor, connectViaUpstreamProxy, selectUpstreamProxy } from "./upstream-proxy.js";
|
|
16
20
|
import { redactSecrets } from "../launcher/output-redaction.js";
|
|
21
|
+
import { sanitize } from "../security/sanitize.js";
|
|
17
22
|
import { envAuthToken } from "../auth/env-token.js";
|
|
18
23
|
import { identityHeaders } from "../api/analyze.js";
|
|
19
24
|
import { dgVersion } from "../commands/version.js";
|
|
25
|
+
import { recordHeldPackage } from "../state/index.js";
|
|
20
26
|
export async function startProductionHttpProxy(options) {
|
|
21
|
-
const
|
|
27
|
+
const onCaRotate = options.onCaRotate;
|
|
28
|
+
const ca = createEphemeralCertificateAuthority(options.session.files.ca, onCaRotate ? { onRotate: (caCertPem) => safeCaRotateCallback(onCaRotate, caCertPem) } : {});
|
|
29
|
+
const authToken = generateProxyAuthToken();
|
|
30
|
+
writeProxyAuthToken(options.session.dir, authToken);
|
|
22
31
|
const state = {
|
|
23
32
|
ready: false,
|
|
24
33
|
port: 0,
|
|
@@ -30,6 +39,10 @@ export async function startProductionHttpProxy(options) {
|
|
|
30
39
|
};
|
|
31
40
|
const activeSockets = new Set();
|
|
32
41
|
const server = createServer((request, response) => {
|
|
42
|
+
if (!verifyProxyAuthorization(headerValue(request.headers["proxy-authorization"]), authToken)) {
|
|
43
|
+
sendProxyAuthRequired(response);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
33
46
|
handleProxyRequest(request, response, options, state).catch((error) => {
|
|
34
47
|
const decision = recordDecision(options, state, {
|
|
35
48
|
verdict: "block",
|
|
@@ -43,6 +56,10 @@ export async function startProductionHttpProxy(options) {
|
|
|
43
56
|
server.on("connection", (socket) => trackSocket(activeSockets, socket));
|
|
44
57
|
server.on("connect", (request, socket, head) => {
|
|
45
58
|
const clientSocket = socket;
|
|
59
|
+
if (!verifyProxyAuthorization(headerValue(request.headers["proxy-authorization"]), authToken)) {
|
|
60
|
+
clientSocket.end("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"dg\"\r\nConnection: close\r\n\r\n");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
46
63
|
handleConnectRequest(request, clientSocket, head, options, state, ca, activeSockets).catch((error) => {
|
|
47
64
|
const decision = recordDecision(options, state, {
|
|
48
65
|
verdict: "block",
|
|
@@ -63,9 +80,27 @@ export async function startProductionHttpProxy(options) {
|
|
|
63
80
|
writeProxyState(options.session, state);
|
|
64
81
|
return {
|
|
65
82
|
port: address.port,
|
|
83
|
+
proxyAuthorization: proxyAuthorizationValue(authToken),
|
|
66
84
|
close: () => closeServer(server, activeSockets)
|
|
67
85
|
};
|
|
68
86
|
}
|
|
87
|
+
function safeCaRotateCallback(onCaRotate, caCertPem) {
|
|
88
|
+
try {
|
|
89
|
+
onCaRotate(caCertPem);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
noteEnforcementSideEffectFailure(error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function sendProxyAuthRequired(response) {
|
|
96
|
+
const body = "Dependency Guardian proxy requires the per-session credential issued to the wrapped package manager.\n";
|
|
97
|
+
response.writeHead(407, {
|
|
98
|
+
"Proxy-Authenticate": "Basic realm=\"dg\"",
|
|
99
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
100
|
+
"Content-Length": Buffer.byteLength(body)
|
|
101
|
+
});
|
|
102
|
+
response.end(body);
|
|
103
|
+
}
|
|
69
104
|
export function readProxySessionState(session) {
|
|
70
105
|
try {
|
|
71
106
|
const parsed = JSON.parse(readFileSync(session.files.proxy, "utf8"));
|
|
@@ -134,6 +169,16 @@ async function handleConnectRequest(request, clientSocket, head, options, state,
|
|
|
134
169
|
clientSocket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
135
170
|
return;
|
|
136
171
|
}
|
|
172
|
+
if (isLinkLocalHost(target)) {
|
|
173
|
+
recordDecision(options, state, {
|
|
174
|
+
verdict: "block",
|
|
175
|
+
packageName: target.hostname,
|
|
176
|
+
cause: "policy",
|
|
177
|
+
reason: `refusing CONNECT to link-local/metadata address ${target.hostname}`
|
|
178
|
+
});
|
|
179
|
+
clientSocket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
137
182
|
if (!shouldMitmHost(target.hostname, options.env)) {
|
|
138
183
|
state.events = [...state.events, `tunnel:${redactSecrets(authorityFor(target))}`];
|
|
139
184
|
writeProxyState(options.session, state);
|
|
@@ -145,22 +190,248 @@ async function handleConnectRequest(request, clientSocket, head, options, state,
|
|
|
145
190
|
await mitmTunnel(clientSocket, head, target, options, state, ca, activeSockets);
|
|
146
191
|
}
|
|
147
192
|
const REDIRECT_LIMIT = 5;
|
|
193
|
+
// Streaming tail-hold: an OPT-IN fast path for artifacts so large their raw
|
|
194
|
+
// download alone would outlast the client read timeout. It streams bytes while
|
|
195
|
+
// hashing and withholds only the final TAIL bytes until the verdict arrives —
|
|
196
|
+
// so on a block the client receives most of the (unverified) artifact and is
|
|
197
|
+
// relied on to reject the truncated body via its own size/integrity checks.
|
|
198
|
+
// That is a weaker guarantee than the default buffered path, which delivers
|
|
199
|
+
// ZERO bytes until a pass verdict. It is therefore DISABLED by default; the
|
|
200
|
+
// preferred fix for a slow verify is a generous client read timeout (set in
|
|
201
|
+
// buildProxyChildEnv), which keeps the strong buffered guarantee. Enable only
|
|
202
|
+
// for genuinely huge artifacts via DG_STREAM_THRESHOLD_BYTES=<bytes>.
|
|
203
|
+
const STREAM_TAIL_BYTES = 64 * 1024;
|
|
204
|
+
const STREAMING_DISABLED = Number.POSITIVE_INFINITY;
|
|
205
|
+
// A package install fetches dozens-to-hundreds of artifacts from the same few
|
|
206
|
+
// registry hosts. Without connection reuse each is a fresh TCP+TLS handshake
|
|
207
|
+
// (~100-250ms to registry.npmjs.org); keep-alive agents pool sockets per host
|
|
208
|
+
// so an N-tarball install pays one handshake, not N. Reuse is keyed by the full
|
|
209
|
+
// connection option-set (host, port, TLS params), so this never crosses hosts
|
|
210
|
+
// or mixes TLS trust. Purely transport-level — no effect on what gets verified.
|
|
211
|
+
const upstreamHttpAgent = new HttpAgent({ keepAlive: true, maxSockets: 64, scheduling: "fifo" });
|
|
212
|
+
const upstreamHttpsAgent = new HttpsAgent({ keepAlive: true, maxSockets: 64, scheduling: "fifo" });
|
|
213
|
+
function streamThresholdBytes(env) {
|
|
214
|
+
const raw = env.DG_STREAM_THRESHOLD_BYTES;
|
|
215
|
+
if (raw === undefined || raw === "") {
|
|
216
|
+
return STREAMING_DISABLED;
|
|
217
|
+
}
|
|
218
|
+
const parsed = Number(raw);
|
|
219
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : STREAMING_DISABLED;
|
|
220
|
+
}
|
|
221
|
+
function canStreamTarget(target, options) {
|
|
222
|
+
if (selectUpstreamProxy(target, options.env)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (isRegistryIndexRequest(target)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return !hostMatchesList(target.hostname, options.env.DG_PRIVATE_REGISTRY_HOSTS ?? "");
|
|
229
|
+
}
|
|
230
|
+
function shouldStreamArtifactResponse(live, env) {
|
|
231
|
+
if (live.statusCode < 200 || live.statusCode >= 300 || live.statusCode === 206) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const contentType = headerValue(live.headers["content-type"]) ?? "";
|
|
235
|
+
if (/\bjson\b/i.test(contentType)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const contentLength = Number(headerValue(live.headers["content-length"]) ?? "");
|
|
239
|
+
return Number.isFinite(contentLength) && contentLength > streamThresholdBytes(env);
|
|
240
|
+
}
|
|
241
|
+
async function fetchUpstreamHeadersFollowingRedirects(request, target, env) {
|
|
242
|
+
let current = target;
|
|
243
|
+
let live = await fetchUpstreamHeaders(request, current, env);
|
|
244
|
+
for (let hop = 0; isRedirectStatus(live.statusCode); hop += 1) {
|
|
245
|
+
live.stream.resume();
|
|
246
|
+
if (hop >= REDIRECT_LIMIT) {
|
|
247
|
+
throw new Error(`registry redirect chain exceeded ${REDIRECT_LIMIT} hops for ${current.host}`);
|
|
248
|
+
}
|
|
249
|
+
const location = headerValue(live.headers.location);
|
|
250
|
+
if (!location) {
|
|
251
|
+
throw new Error(`registry returned a ${live.statusCode} redirect with no Location header for ${current.host}`);
|
|
252
|
+
}
|
|
253
|
+
const next = new URL(location, current);
|
|
254
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
255
|
+
throw new Error(`registry redirected to unsupported protocol ${next.protocol}`);
|
|
256
|
+
}
|
|
257
|
+
if (isPrivateNetworkHost(next) && !isPrivateNetworkHost(target)) {
|
|
258
|
+
throw new Error(`registry redirected a public artifact request into a private address (${next.hostname})`);
|
|
259
|
+
}
|
|
260
|
+
live = await fetchUpstreamHeaders(syntheticGetRequest(redirectHopHeaders(request, current, next)), next, env);
|
|
261
|
+
current = next;
|
|
262
|
+
}
|
|
263
|
+
return live;
|
|
264
|
+
}
|
|
265
|
+
function fetchUpstreamHeaders(request, target, env) {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const isHttps = target.protocol === "https:";
|
|
268
|
+
const requester = isHttps ? httpsRequest : httpRequest;
|
|
269
|
+
let liveStream = null;
|
|
270
|
+
const upstream = requester({
|
|
271
|
+
hostname: upstreamHostname(target, env),
|
|
272
|
+
port: target.port ? Number(target.port) : isHttps ? 443 : 80,
|
|
273
|
+
path: `${target.pathname}${target.search}`,
|
|
274
|
+
method: request.method,
|
|
275
|
+
agent: isHttps ? upstreamHttpsAgent : upstreamHttpAgent,
|
|
276
|
+
headers: isHttps ? upstreamRequestHeaders(request, target) : stripProxyHopHeaders(request.headers),
|
|
277
|
+
...(isHttps ? upstreamTlsOptions(env) : {})
|
|
278
|
+
}, (upstreamResponse) => {
|
|
279
|
+
liveStream = upstreamResponse;
|
|
280
|
+
resolve({
|
|
281
|
+
statusCode: upstreamResponse.statusCode ?? 502,
|
|
282
|
+
headers: responseHeaders(upstreamResponse),
|
|
283
|
+
stream: upstreamResponse
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
applyUpstreamTimeBudget(upstream, target, env, (error) => {
|
|
287
|
+
liveStream?.destroy(error);
|
|
288
|
+
reject(error);
|
|
289
|
+
});
|
|
290
|
+
upstream.on("error", reject);
|
|
291
|
+
request.on("data", (chunk) => upstream.write(chunk));
|
|
292
|
+
request.on("end", () => upstream.end());
|
|
293
|
+
request.on("error", reject);
|
|
294
|
+
request.once("close", () => {
|
|
295
|
+
if (!request.complete) {
|
|
296
|
+
upstream.destroy();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function streamArtifactWithTailHold(response, target, options, state, live) {
|
|
302
|
+
const identityResolution = resolveArtifactIdentity(target, state.identities, options.classification);
|
|
303
|
+
if (identityResolution.kind === "ambiguous") {
|
|
304
|
+
live.stream.destroy();
|
|
305
|
+
const decision = recordDecision(options, state, {
|
|
306
|
+
verdict: "block",
|
|
307
|
+
packageName: identityResolution.packageName,
|
|
308
|
+
cause: "policy",
|
|
309
|
+
reason: identityResolution.reason
|
|
310
|
+
});
|
|
311
|
+
sendBlocked(response, decision);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const identity = identityResolution.identity;
|
|
315
|
+
const inflightName = artifactDisplayName(identity);
|
|
316
|
+
state.inflight = [...state.inflight, inflightName];
|
|
317
|
+
writeProxyState(options.session, state);
|
|
318
|
+
const limit = maxArtifactBytes(options.env);
|
|
319
|
+
const hash = createHash("sha256");
|
|
320
|
+
const tail = [];
|
|
321
|
+
let tailSize = 0;
|
|
322
|
+
let total = 0;
|
|
323
|
+
response.writeHead(live.statusCode, live.headers);
|
|
324
|
+
response.once("close", () => {
|
|
325
|
+
if (!response.writableEnded) {
|
|
326
|
+
live.stream.destroy();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
const finishInflight = () => {
|
|
330
|
+
state.inflight = removeFirst(state.inflight, inflightName);
|
|
331
|
+
};
|
|
332
|
+
try {
|
|
333
|
+
await new Promise((resolve, reject) => {
|
|
334
|
+
live.stream.on("data", (chunk) => {
|
|
335
|
+
total += chunk.length;
|
|
336
|
+
if (total > limit) {
|
|
337
|
+
reject(new BufferBudgetError(`artifact exceeded the ${limit}-byte streaming budget for ${target.host}`));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
hash.update(chunk);
|
|
341
|
+
tail.push(chunk);
|
|
342
|
+
tailSize += chunk.length;
|
|
343
|
+
while (tail.length > 1 && tailSize - (tail[0]?.length ?? 0) >= STREAM_TAIL_BYTES) {
|
|
344
|
+
const released = tail.shift();
|
|
345
|
+
if (released) {
|
|
346
|
+
tailSize -= released.length;
|
|
347
|
+
response.write(released);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
live.stream.once("end", () => resolve());
|
|
352
|
+
live.stream.once("error", reject);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
finishInflight();
|
|
357
|
+
live.stream.destroy();
|
|
358
|
+
recordDecision(options, state, {
|
|
359
|
+
verdict: "block",
|
|
360
|
+
packageName: artifactDisplayName(identity),
|
|
361
|
+
cause: error instanceof BufferBudgetError ? "proxy-setup-failure" : "registry-timeout",
|
|
362
|
+
reason: error instanceof Error ? error.message : "registry stream failed"
|
|
363
|
+
});
|
|
364
|
+
response.destroy();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const sha256 = hash.digest("hex");
|
|
368
|
+
state.hashes = [...state.hashes, {
|
|
369
|
+
url: redactSecrets(target.toString()),
|
|
370
|
+
sha256,
|
|
371
|
+
identity
|
|
372
|
+
}];
|
|
373
|
+
writeProxyState(options.session, state);
|
|
374
|
+
const verdict = await lookupVerdict(options, target, sha256, { statusCode: live.statusCode, headers: live.headers }, identity).catch((error) => ({
|
|
375
|
+
verdict: "block",
|
|
376
|
+
packageName: artifactDisplayName(identity),
|
|
377
|
+
cause: "api-timeout",
|
|
378
|
+
reason: error instanceof Error ? error.message : "Dependency Guardian API verdict lookup failed"
|
|
379
|
+
}));
|
|
380
|
+
finishInflight();
|
|
381
|
+
const decision = recordDecision(options, state, verdict);
|
|
382
|
+
if (decision.action === "block") {
|
|
383
|
+
response.destroy();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
for (const chunk of tail) {
|
|
387
|
+
response.write(chunk);
|
|
388
|
+
}
|
|
389
|
+
response.end();
|
|
390
|
+
}
|
|
148
391
|
function isRedirectStatus(statusCode) {
|
|
149
392
|
return [301, 302, 303, 307, 308].includes(statusCode);
|
|
150
393
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
394
|
+
// Node's WHATWG URL parser canonicalizes an IPv4-mapped IPv6 literal to the hex
|
|
395
|
+
// hextet form ([::ffff:169.254.169.254] -> [::ffff:a9fe:a9fe]), so a guard that
|
|
396
|
+
// only string-matches the dotted-decimal form silently lets the cloud-metadata
|
|
397
|
+
// IP through as "public". Recover the embedded IPv4 (both forms) before
|
|
398
|
+
// classifying so the IPv4 ranges below cover the mapped address too.
|
|
399
|
+
function recoverIpv4MappedHost(host) {
|
|
400
|
+
const dotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i.exec(host);
|
|
401
|
+
if (dotted?.[1]) {
|
|
402
|
+
return dotted[1];
|
|
403
|
+
}
|
|
404
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(host);
|
|
405
|
+
if (hex?.[1] && hex?.[2]) {
|
|
406
|
+
const hi = parseInt(hex[1], 16);
|
|
407
|
+
const lo = parseInt(hex[2], 16);
|
|
408
|
+
return `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
155
409
|
}
|
|
156
|
-
|
|
410
|
+
return host;
|
|
411
|
+
}
|
|
412
|
+
function isPrivateIpv4(host) {
|
|
413
|
+
return (/^(127|10|0)\./.test(host) ||
|
|
414
|
+
/^192\.168\./.test(host) ||
|
|
415
|
+
/^169\.254\./.test(host) ||
|
|
416
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./.test(host) ||
|
|
417
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./.test(host));
|
|
418
|
+
}
|
|
419
|
+
export function isPrivateNetworkHost(url) {
|
|
420
|
+
const raw = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
|
|
421
|
+
const host = recoverIpv4MappedHost(raw);
|
|
422
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "::" || host === "::1") {
|
|
157
423
|
return true;
|
|
158
424
|
}
|
|
159
|
-
if (
|
|
425
|
+
if (isPrivateIpv4(host)) {
|
|
160
426
|
return true;
|
|
161
427
|
}
|
|
162
428
|
return /^(fe80|f[cd][0-9a-f]{2}):/.test(host);
|
|
163
429
|
}
|
|
430
|
+
export function isLinkLocalHost(url) {
|
|
431
|
+
const raw = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
|
|
432
|
+
const host = recoverIpv4MappedHost(raw);
|
|
433
|
+
return /^169\.254\./.test(host) || /^fe80:/.test(host);
|
|
434
|
+
}
|
|
164
435
|
function syntheticGetRequest(headers) {
|
|
165
436
|
const request = new EventEmitter();
|
|
166
437
|
request.method = "GET";
|
|
@@ -202,12 +473,40 @@ async function fetchUpstreamFollowingRedirects(request, target, env) {
|
|
|
202
473
|
}
|
|
203
474
|
const ARTIFACT_CONDITIONAL_HEADERS = ["range", "if-range", "if-none-match", "if-modified-since"];
|
|
204
475
|
async function handleArtifactRequest(request, response, target, options, state) {
|
|
476
|
+
if (isLinkLocalHost(target)) {
|
|
477
|
+
const decision = recordDecision(options, state, {
|
|
478
|
+
verdict: "block",
|
|
479
|
+
packageName: packageNameFromUrl(target),
|
|
480
|
+
cause: "policy",
|
|
481
|
+
reason: `refusing to fetch from link-local/metadata address ${target.hostname}`
|
|
482
|
+
});
|
|
483
|
+
sendBlocked(response, decision);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
205
486
|
if (!isRegistryIndexRequest(target)) {
|
|
206
487
|
for (const header of ARTIFACT_CONDITIONAL_HEADERS) {
|
|
207
488
|
delete request.headers[header];
|
|
208
489
|
}
|
|
209
490
|
}
|
|
210
|
-
|
|
491
|
+
let upstream;
|
|
492
|
+
try {
|
|
493
|
+
if (canStreamTarget(target, options)) {
|
|
494
|
+
const live = await fetchUpstreamHeadersFollowingRedirects(request, target, options.env);
|
|
495
|
+
if (shouldStreamArtifactResponse(live, options.env)) {
|
|
496
|
+
await streamArtifactWithTailHold(response, target, options, state, live);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
upstream = {
|
|
500
|
+
statusCode: live.statusCode,
|
|
501
|
+
headers: live.headers,
|
|
502
|
+
body: await collectBounded(live.stream, { label: target.toString() })
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
upstream = await fetchUpstreamFollowingRedirects(request, target, options.env);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
211
510
|
const message = error instanceof Error ? error.message : "registry request failed";
|
|
212
511
|
const decision = recordDecision(options, state, {
|
|
213
512
|
verdict: "block",
|
|
@@ -216,8 +515,8 @@ async function handleArtifactRequest(request, response, target, options, state)
|
|
|
216
515
|
reason: message
|
|
217
516
|
});
|
|
218
517
|
sendBlocked(response, decision);
|
|
219
|
-
return
|
|
220
|
-
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
221
520
|
if (!upstream) {
|
|
222
521
|
return;
|
|
223
522
|
}
|
|
@@ -287,7 +586,7 @@ async function handleArtifactRequest(request, response, target, options, state)
|
|
|
287
586
|
reason: error instanceof Error ? error.message : "Dependency Guardian API verdict lookup failed"
|
|
288
587
|
}));
|
|
289
588
|
state.inflight = removeFirst(state.inflight, inflightName);
|
|
290
|
-
const decision = recordDecision(options, state, verdict);
|
|
589
|
+
const decision = recordDecision(options, state, verdict, identity.identity);
|
|
291
590
|
if (decision.action === "block") {
|
|
292
591
|
sendBlocked(response, decision);
|
|
293
592
|
return;
|
|
@@ -386,6 +685,9 @@ function upstreamHostname(target, env = process.env) {
|
|
|
386
685
|
return testUpstreamHostMap(env).get(hostname) ?? hostname;
|
|
387
686
|
}
|
|
388
687
|
function testUpstreamHostMap(env) {
|
|
688
|
+
if (env.NODE_ENV !== "test") {
|
|
689
|
+
return new Map();
|
|
690
|
+
}
|
|
389
691
|
const entries = (env.DG_TEST_UPSTREAM_HOST_MAP ?? "")
|
|
390
692
|
.split(",")
|
|
391
693
|
.map((entry) => entry.trim())
|
|
@@ -403,7 +705,7 @@ function testUpstreamHostMap(env) {
|
|
|
403
705
|
function fetchUpstream(request, target, env) {
|
|
404
706
|
const upstreamProxy = selectUpstreamProxy(target, env);
|
|
405
707
|
if (upstreamProxy && target.protocol === "http:") {
|
|
406
|
-
return fetchHttpViaProxy(request, target, upstreamProxy);
|
|
708
|
+
return fetchHttpViaProxy(request, target, upstreamProxy, env);
|
|
407
709
|
}
|
|
408
710
|
if (upstreamProxy && target.protocol === "https:") {
|
|
409
711
|
return fetchHttpsViaProxy(request, target, upstreamProxy, env);
|
|
@@ -417,7 +719,8 @@ function fetchUpstream(request, target, env) {
|
|
|
417
719
|
port: target.port ? Number(target.port) : 80,
|
|
418
720
|
path: `${target.pathname}${target.search}`,
|
|
419
721
|
method: request.method,
|
|
420
|
-
|
|
722
|
+
agent: upstreamHttpAgent,
|
|
723
|
+
headers: stripProxyHopHeaders(request.headers)
|
|
421
724
|
}, (upstreamResponse) => {
|
|
422
725
|
collectBounded(upstreamResponse, { label: target.toString() })
|
|
423
726
|
.then((body) => resolve({
|
|
@@ -427,6 +730,7 @@ function fetchUpstream(request, target, env) {
|
|
|
427
730
|
}))
|
|
428
731
|
.catch(reject);
|
|
429
732
|
});
|
|
733
|
+
applyUpstreamTimeBudget(upstream, target, env, reject);
|
|
430
734
|
upstream.on("error", reject);
|
|
431
735
|
request.on("data", (chunk) => upstream.write(chunk));
|
|
432
736
|
request.on("end", () => upstream.end());
|
|
@@ -438,10 +742,10 @@ function fetchUpstream(request, target, env) {
|
|
|
438
742
|
});
|
|
439
743
|
});
|
|
440
744
|
}
|
|
441
|
-
function fetchHttpViaProxy(request, target, upstreamProxy) {
|
|
745
|
+
function fetchHttpViaProxy(request, target, upstreamProxy, env) {
|
|
442
746
|
return new Promise((resolve, reject) => {
|
|
443
747
|
const headers = {
|
|
444
|
-
...request.headers,
|
|
748
|
+
...stripProxyHopHeaders(request.headers),
|
|
445
749
|
...(upstreamProxy.authorizationHeader ? { "Proxy-Authorization": upstreamProxy.authorizationHeader } : {})
|
|
446
750
|
};
|
|
447
751
|
const upstream = httpRequest({
|
|
@@ -459,6 +763,7 @@ function fetchHttpViaProxy(request, target, upstreamProxy) {
|
|
|
459
763
|
}))
|
|
460
764
|
.catch(reject);
|
|
461
765
|
});
|
|
766
|
+
applyUpstreamTimeBudget(upstream, target, env, reject);
|
|
462
767
|
upstream.on("error", reject);
|
|
463
768
|
request.on("data", (chunk) => upstream.write(chunk));
|
|
464
769
|
request.on("end", () => upstream.end());
|
|
@@ -477,6 +782,7 @@ function fetchHttpsDirect(request, target, env) {
|
|
|
477
782
|
port: target.port ? Number(target.port) : 443,
|
|
478
783
|
path: `${target.pathname}${target.search}`,
|
|
479
784
|
method: request.method,
|
|
785
|
+
agent: upstreamHttpsAgent,
|
|
480
786
|
headers: upstreamRequestHeaders(request, target),
|
|
481
787
|
...upstreamTlsOptions(env)
|
|
482
788
|
}, (upstreamResponse) => {
|
|
@@ -488,6 +794,7 @@ function fetchHttpsDirect(request, target, env) {
|
|
|
488
794
|
}))
|
|
489
795
|
.catch(reject);
|
|
490
796
|
});
|
|
797
|
+
applyUpstreamTimeBudget(upstream, target, env, reject);
|
|
491
798
|
upstream.on("error", reject);
|
|
492
799
|
request.on("data", (chunk) => upstream.write(chunk));
|
|
493
800
|
request.on("end", () => upstream.end());
|
|
@@ -514,9 +821,35 @@ function connectTlsOverSocket(socket, target, env) {
|
|
|
514
821
|
ALPNProtocols: ["http/1.1"],
|
|
515
822
|
...upstreamTlsOptions(env)
|
|
516
823
|
}, () => resolve(tlsSocket));
|
|
824
|
+
applyUpstreamTimeBudget(tlsSocket, target, env);
|
|
517
825
|
tlsSocket.once("error", reject);
|
|
518
826
|
});
|
|
519
827
|
}
|
|
828
|
+
function upstreamIdleTimeoutMs(env) {
|
|
829
|
+
return parsePositiveInteger(env.DG_UPSTREAM_IDLE_TIMEOUT_MS, 30_000);
|
|
830
|
+
}
|
|
831
|
+
function upstreamTotalTimeoutMs(env) {
|
|
832
|
+
return parsePositiveInteger(env.DG_UPSTREAM_TOTAL_TIMEOUT_MS, 600_000);
|
|
833
|
+
}
|
|
834
|
+
function applyUpstreamTimeBudget(upstream, target, env, onTimeout) {
|
|
835
|
+
const idleMs = upstreamIdleTimeoutMs(env);
|
|
836
|
+
const totalMs = upstreamTotalTimeoutMs(env);
|
|
837
|
+
const expire = (detail) => {
|
|
838
|
+
const error = new Error(`upstream registry request for ${target.host} timed out: ${detail}`);
|
|
839
|
+
onTimeout?.(error);
|
|
840
|
+
upstream.destroy(error);
|
|
841
|
+
};
|
|
842
|
+
upstream.setTimeout(idleMs, () => expire(`no data for ${idleMs}ms`));
|
|
843
|
+
const totalTimer = setTimeout(() => expire(`exceeded the ${totalMs}ms total budget`), totalMs);
|
|
844
|
+
totalTimer.unref();
|
|
845
|
+
upstream.once("close", () => clearTimeout(totalTimer));
|
|
846
|
+
}
|
|
847
|
+
function stripProxyHopHeaders(headers) {
|
|
848
|
+
const stripped = { ...headers };
|
|
849
|
+
delete stripped["proxy-authorization"];
|
|
850
|
+
delete stripped["proxy-connection"];
|
|
851
|
+
return stripped;
|
|
852
|
+
}
|
|
520
853
|
function writeRawHttpRequest(tlsSocket, request, target, requestBody) {
|
|
521
854
|
return new Promise((resolve, reject) => {
|
|
522
855
|
collectBounded(tlsSocket, { label: target.toString() }).then(resolve).catch(reject);
|
|
@@ -589,7 +922,7 @@ function upstreamTlsOptions(env) {
|
|
|
589
922
|
}
|
|
590
923
|
try {
|
|
591
924
|
return {
|
|
592
|
-
ca: readFileSync(caPath, "utf8")
|
|
925
|
+
ca: [...rootCertificates, readFileSync(caPath, "utf8")]
|
|
593
926
|
};
|
|
594
927
|
}
|
|
595
928
|
catch {
|
|
@@ -625,6 +958,11 @@ async function lookupVerdict(options, target, sha256, upstream, identity) {
|
|
|
625
958
|
if (shouldUseScanTarball(options, target, identity)) {
|
|
626
959
|
return lookupScanTarballVerdict(options, target, sha256, upstream, identity);
|
|
627
960
|
}
|
|
961
|
+
const preverified = preverifiedVerdict(options, target, sha256, identity);
|
|
962
|
+
if (preverified) {
|
|
963
|
+
return preverified;
|
|
964
|
+
}
|
|
965
|
+
const cooldown = resolveCooldownRequest(options, identity);
|
|
628
966
|
const controller = new AbortController();
|
|
629
967
|
const timeout = setTimeout(() => controller.abort(), options.verdictTimeoutMs ?? installVerdictTimeoutMs(options.env));
|
|
630
968
|
try {
|
|
@@ -638,7 +976,7 @@ async function lookupVerdict(options, target, sha256, upstream, identity) {
|
|
|
638
976
|
body: JSON.stringify({
|
|
639
977
|
manager: options.classification.manager,
|
|
640
978
|
action: options.classification.action,
|
|
641
|
-
url: target.toString(),
|
|
979
|
+
url: redactSecrets(target.toString()),
|
|
642
980
|
artifactUrlHash: artifactUrlHash(target),
|
|
643
981
|
ecosystem: identity.ecosystem,
|
|
644
982
|
name: identity.name,
|
|
@@ -647,7 +985,8 @@ async function lookupVerdict(options, target, sha256, upstream, identity) {
|
|
|
647
985
|
sourceKind: identity.sourceKind,
|
|
648
986
|
sha256,
|
|
649
987
|
statusCode: upstream.statusCode,
|
|
650
|
-
contentType: headerValue(upstream.headers["content-type"])
|
|
988
|
+
contentType: headerValue(upstream.headers["content-type"]),
|
|
989
|
+
...(cooldown ? { cooldown } : {})
|
|
651
990
|
}),
|
|
652
991
|
signal: controller.signal
|
|
653
992
|
});
|
|
@@ -658,7 +997,7 @@ async function lookupVerdict(options, target, sha256, upstream, identity) {
|
|
|
658
997
|
packageName: artifactDisplayName(identity),
|
|
659
998
|
cause: "quota-exceeded",
|
|
660
999
|
reason: "You've reached your monthly scan limit. Upgrade at westbayberry.com/pricing or wait for it to reset.",
|
|
661
|
-
...(typeof body.resetsAt === "string" ? { resetsAt: body.resetsAt } : {}),
|
|
1000
|
+
...(typeof body.resetsAt === "string" ? { resetsAt: sanitize(body.resetsAt) } : {}),
|
|
662
1001
|
quotaBehavior: body.quotaBehavior === "pass" ? "pass" : "block"
|
|
663
1002
|
};
|
|
664
1003
|
}
|
|
@@ -744,6 +1083,31 @@ async function lookupScanTarballVerdict(options, target, sha256, upstream, ident
|
|
|
744
1083
|
clearTimeout(timeout);
|
|
745
1084
|
}
|
|
746
1085
|
}
|
|
1086
|
+
// A preflight batch-analyze already verdicted this exact name@version (and the
|
|
1087
|
+
// user accepted any warns), so a per-artifact API round-trip would re-answer
|
|
1088
|
+
// the same question. Trust it only when the identity came from registry
|
|
1089
|
+
// metadata and the preflight applied the same cooldown gate the proxy would;
|
|
1090
|
+
// the synthesized verdict goes through normalizeVerdict so the scanned-SHA
|
|
1091
|
+
// cross-check blocks tampered bytes exactly like a server response would.
|
|
1092
|
+
function preverifiedVerdict(options, target, streamedSha256, identity) {
|
|
1093
|
+
if (identity.sourceKind !== "registry-metadata") {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const entry = loadPreverifiedMap(options.session.dir).get(preverifiedKey(identity.ecosystem, identity.name, identity.version));
|
|
1097
|
+
if (!entry) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
if (!entry.cooldownEvaluated && resolveCooldownRequest(options, identity) !== undefined) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
return normalizeVerdict({
|
|
1104
|
+
verdict: entry.action,
|
|
1105
|
+
cause: entry.action,
|
|
1106
|
+
packageName: `${identity.name}@${identity.version}`,
|
|
1107
|
+
reason: entry.reason ?? (entry.action === "pass" ? "verified before install" : "flagged for review"),
|
|
1108
|
+
...(entry.scannedSha256 ? { scannedSha256: entry.scannedSha256 } : {})
|
|
1109
|
+
}, target, identity, streamedSha256);
|
|
1110
|
+
}
|
|
747
1111
|
function normalizeVerdict(value, target, identity, streamedSha256) {
|
|
748
1112
|
if (!isRecord(value)) {
|
|
749
1113
|
throw new Error("Dependency Guardian API returned a malformed verdict");
|
|
@@ -762,13 +1126,53 @@ function normalizeVerdict(value, target, identity, streamedSha256) {
|
|
|
762
1126
|
};
|
|
763
1127
|
}
|
|
764
1128
|
const cause = typeof value.cause === "string" ? value.cause : undefined;
|
|
1129
|
+
const cooldown = parseCooldownInfo(value.cooldown);
|
|
1130
|
+
const dashboardUrl = safeDashboardUrl(value.dashboardUrl);
|
|
765
1131
|
return {
|
|
766
1132
|
verdict,
|
|
767
|
-
packageName: typeof value.packageName === "string" ? value.packageName : artifactDisplayName(identity) || packageNameFromUrl(target),
|
|
1133
|
+
packageName: typeof value.packageName === "string" ? sanitize(value.packageName) : artifactDisplayName(identity) || packageNameFromUrl(target),
|
|
768
1134
|
...(isProxyCause(cause) ? { cause } : {}),
|
|
769
|
-
reason: typeof value.reason === "string" ? value.reason : `API verdict ${verdict}`,
|
|
770
|
-
...(
|
|
771
|
-
...(value.unauthenticated === true ? { unauthenticated: true } : {})
|
|
1135
|
+
reason: typeof value.reason === "string" ? sanitize(value.reason) : `API verdict ${verdict}`,
|
|
1136
|
+
...(dashboardUrl ? { dashboardUrl } : {}),
|
|
1137
|
+
...(value.unauthenticated === true ? { unauthenticated: true } : {}),
|
|
1138
|
+
...(cooldown ? { cooldown } : {})
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function safeDashboardUrl(value) {
|
|
1142
|
+
if (typeof value !== "string") {
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
const url = new URL(sanitize(value));
|
|
1147
|
+
return url.protocol === "https:" ? url.toString() : undefined;
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
return undefined;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function resolveCooldownRequest(options, identity) {
|
|
1154
|
+
if (identity.ecosystem === "unknown" || identity.version === "unknown") {
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
1157
|
+
if (isCooldownExemptByDgFile(identity.name, identity.ecosystem, options.cooldownExemptions ?? [])) {
|
|
1158
|
+
return undefined;
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
return cooldownRequestParam(loadUserConfig(options.env), options.env, identity.ecosystem, identity.name);
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
return undefined;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function parseCooldownInfo(value) {
|
|
1168
|
+
if (!isRecord(value) || typeof value.requiredDays !== "number" || !Number.isFinite(value.requiredDays)) {
|
|
1169
|
+
return undefined;
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
requiredDays: value.requiredDays,
|
|
1173
|
+
...(typeof value.ageDays === "number" && Number.isFinite(value.ageDays) ? { ageDays: value.ageDays } : {}),
|
|
1174
|
+
...(typeof value.publishedAt === "string" ? { publishedAt: sanitize(value.publishedAt) } : {}),
|
|
1175
|
+
...(typeof value.eligibleAt === "string" ? { eligibleAt: sanitize(value.eligibleAt) } : {})
|
|
772
1176
|
};
|
|
773
1177
|
}
|
|
774
1178
|
function normalizeScanTarballVerdict(value, target, identity, streamedSha256) {
|
|
@@ -829,7 +1233,7 @@ function mergeIdentities(existing, next) {
|
|
|
829
1233
|
}
|
|
830
1234
|
return merged;
|
|
831
1235
|
}
|
|
832
|
-
function recordDecision(options, state, verdict) {
|
|
1236
|
+
function recordDecision(options, state, verdict, identity) {
|
|
833
1237
|
const decision = enforceProtectedInstall({
|
|
834
1238
|
classification: options.classification,
|
|
835
1239
|
env: options.env,
|
|
@@ -839,10 +1243,28 @@ function recordDecision(options, state, verdict) {
|
|
|
839
1243
|
state.decisions = [...state.decisions, decision];
|
|
840
1244
|
state.events = [...state.events, `${decision.action}:${decision.cause}:${redactSecrets(decision.packageName)}`];
|
|
841
1245
|
writeProxyState(options.session, state);
|
|
1246
|
+
if (identity && decision.action === "block" && decision.cause === "cooldown" && decision.cooldown) {
|
|
1247
|
+
try {
|
|
1248
|
+
recordHeldPackage({
|
|
1249
|
+
ecosystem: identity.ecosystem,
|
|
1250
|
+
name: identity.name,
|
|
1251
|
+
version: identity.version,
|
|
1252
|
+
requiredDays: decision.cooldown.requiredDays,
|
|
1253
|
+
...(decision.cooldown.ageDays !== undefined ? { ageDays: decision.cooldown.ageDays } : {}),
|
|
1254
|
+
...(decision.cooldown.publishedAt ? { publishedAt: decision.cooldown.publishedAt } : {}),
|
|
1255
|
+
...(decision.cooldown.eligibleAt ? { eligibleAt: decision.cooldown.eligibleAt } : {}),
|
|
1256
|
+
manager: options.classification.manager
|
|
1257
|
+
}, options.env);
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
noteEnforcementSideEffectFailure(error);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
842
1263
|
return decision;
|
|
843
1264
|
}
|
|
844
1265
|
function sendBlocked(response, decision) {
|
|
845
|
-
const
|
|
1266
|
+
const breadcrumb = decision.cause === "cooldown" ? " · holds: dg cooldown" : "";
|
|
1267
|
+
const body = `Dependency Guardian blocked ${redactSecrets(decision.packageName)}: ${redactSecrets(decision.reason)}${breadcrumb}\n`;
|
|
846
1268
|
response.writeHead(403, {
|
|
847
1269
|
"Content-Type": "text/plain; charset=utf-8",
|
|
848
1270
|
"Content-Length": Buffer.byteLength(body)
|
|
@@ -875,24 +1297,29 @@ function packageNameFromUrl(url) {
|
|
|
875
1297
|
return name ? decodeURIComponent(name) : url.hostname;
|
|
876
1298
|
}
|
|
877
1299
|
function writeProxyState(session, state) {
|
|
878
|
-
mkdirSync(dirname(session.files.proxy), {
|
|
879
|
-
recursive: true,
|
|
880
|
-
mode: 0o700
|
|
881
|
-
});
|
|
882
|
-
const tempPath = `${session.files.proxy}.${process.pid}.${process.hrtime.bigint().toString(36)}.tmp`;
|
|
883
1300
|
try {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
mode: 0o600
|
|
1301
|
+
mkdirSync(dirname(session.files.proxy), {
|
|
1302
|
+
recursive: true,
|
|
1303
|
+
mode: 0o700
|
|
888
1304
|
});
|
|
889
|
-
|
|
1305
|
+
const tempPath = `${session.files.proxy}.${process.pid}.${process.hrtime.bigint().toString(36)}.tmp`;
|
|
1306
|
+
try {
|
|
1307
|
+
writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
1308
|
+
encoding: "utf8",
|
|
1309
|
+
flag: "wx",
|
|
1310
|
+
mode: 0o600
|
|
1311
|
+
});
|
|
1312
|
+
renameSync(tempPath, session.files.proxy);
|
|
1313
|
+
}
|
|
1314
|
+
catch (error) {
|
|
1315
|
+
rmSync(tempPath, {
|
|
1316
|
+
force: true
|
|
1317
|
+
});
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
890
1320
|
}
|
|
891
1321
|
catch (error) {
|
|
892
|
-
|
|
893
|
-
force: true
|
|
894
|
-
});
|
|
895
|
-
throw error;
|
|
1322
|
+
noteEnforcementSideEffectFailure(error);
|
|
896
1323
|
}
|
|
897
1324
|
}
|
|
898
1325
|
function removeFirst(items, value) {
|
|
@@ -905,7 +1332,7 @@ function removeFirst(items, value) {
|
|
|
905
1332
|
function isRecord(value) {
|
|
906
1333
|
return typeof value === "object" && value !== null;
|
|
907
1334
|
}
|
|
908
|
-
function isProxyCause(value) {
|
|
1335
|
+
export function isProxyCause(value) {
|
|
909
1336
|
return [
|
|
910
1337
|
"pass",
|
|
911
1338
|
"warn",
|
|
@@ -920,6 +1347,7 @@ function isProxyCause(value) {
|
|
|
920
1347
|
"api-timeout",
|
|
921
1348
|
"registry-timeout",
|
|
922
1349
|
"analysis-incomplete",
|
|
1350
|
+
"cooldown",
|
|
923
1351
|
"unsupported-manager",
|
|
924
1352
|
"proxy-setup-failure"
|
|
925
1353
|
].includes(value ?? "");
|