@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/launcher/run.js
CHANGED
|
@@ -4,40 +4,26 @@ import { constants as osConstants } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { EXIT_UNAVAILABLE } from "../commands/types.js";
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
|
+
import { findProjectRoot, loadDgFile } from "../project/dgfile.js";
|
|
8
|
+
import { writeCooldownExemptionsFile } from "../proxy/cooldown-exemptions-file.js";
|
|
9
|
+
import { writePreverifiedFile } from "../proxy/preverified.js";
|
|
7
10
|
import { describeBlockedInstall, describeFlaggedWarn, renderInstallDecision } from "../install-ui/block-render.js";
|
|
8
11
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
9
12
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
13
|
+
import { maybeSetupNudge } from "../runtime/nudges.js";
|
|
10
14
|
import { createTheme } from "../presentation/theme.js";
|
|
11
15
|
import { readProxySessionState } from "../proxy/server.js";
|
|
12
16
|
import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
|
|
13
17
|
import { classifyPackageManagerInvocation } from "./classify.js";
|
|
14
18
|
import { buildProxyChildEnv } from "./env.js";
|
|
19
|
+
import { prepareCargoHome, userCargoHome } from "./cargo-cache.js";
|
|
15
20
|
import { cachedPipResolution } from "./install-preflight.js";
|
|
16
|
-
import { parsePipReportInstallCount } from "./pip-report.js";
|
|
17
21
|
import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
|
|
22
|
+
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
18
23
|
import { resolveRealBinary } from "./resolve-real-binary.js";
|
|
24
|
+
import { runScriptGateAfterInstall } from "../scripts/gate.js";
|
|
19
25
|
export const EXIT_INSTALL_BLOCKED = 2;
|
|
20
|
-
|
|
21
|
-
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
22
|
-
export function resolveSpawnInvocation(binary, args, platform = process.platform) {
|
|
23
|
-
if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
|
|
24
|
-
return { command: binary, args, windowsVerbatimArguments: false };
|
|
25
|
-
}
|
|
26
|
-
const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
|
|
27
|
-
return {
|
|
28
|
-
command: process.env.comspec ?? "cmd.exe",
|
|
29
|
-
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
30
|
-
windowsVerbatimArguments: true
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function escapeCmdCommand(command) {
|
|
34
|
-
return command.replace(CMD_META_CHARS, "^$1");
|
|
35
|
-
}
|
|
36
|
-
// cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
|
|
37
|
-
function escapeCmdArgument(argument) {
|
|
38
|
-
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
39
|
-
return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
|
|
40
|
-
}
|
|
26
|
+
export { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
41
27
|
export function shimDepth(env) {
|
|
42
28
|
const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
|
|
43
29
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
@@ -129,7 +115,7 @@ export async function runPackageManager(manager, args, options = {}) {
|
|
|
129
115
|
return {
|
|
130
116
|
exitCode: child.exitCode,
|
|
131
117
|
stdout: streamedOut(child.stdout, options),
|
|
132
|
-
stderr: `${rendered}${streamedErr(child.stderr, options)}`
|
|
118
|
+
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
|
|
133
119
|
};
|
|
134
120
|
}
|
|
135
121
|
const child = await spawnPackageManager(plan, args, options);
|
|
@@ -151,6 +137,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
151
137
|
}
|
|
152
138
|
const restoreSignalHandlers = installProxySignalHandlers(proxy);
|
|
153
139
|
try {
|
|
140
|
+
const cacheDir = prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env);
|
|
154
141
|
const proxiedPlan = {
|
|
155
142
|
...plan,
|
|
156
143
|
childEnv: buildProxyChildEnv({
|
|
@@ -158,7 +145,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
158
145
|
baseEnv: plan.childEnv,
|
|
159
146
|
proxyUrl: proxy.proxyUrl,
|
|
160
147
|
caBundlePath: proxy.session.files.ca,
|
|
161
|
-
cacheDir
|
|
148
|
+
cacheDir
|
|
162
149
|
})
|
|
163
150
|
};
|
|
164
151
|
const child = await spawnPackageManager(proxiedPlan, args, options);
|
|
@@ -195,7 +182,7 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
195
182
|
return {
|
|
196
183
|
exitCode: child.exitCode,
|
|
197
184
|
stdout: streamedOut(child.stdout, options),
|
|
198
|
-
stderr: `${rendered}${streamedErr(child.stderr, options)}`
|
|
185
|
+
stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
|
|
199
186
|
};
|
|
200
187
|
}
|
|
201
188
|
finally {
|
|
@@ -203,6 +190,22 @@ async function runWithProductionProxy(plan, args, options) {
|
|
|
203
190
|
stopProxyWorker(proxy);
|
|
204
191
|
}
|
|
205
192
|
}
|
|
193
|
+
function scriptGateLine(plan, exitCode, options) {
|
|
194
|
+
if (exitCode !== 0) {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
return runScriptGateAfterInstall({
|
|
198
|
+
classification: plan.classification,
|
|
199
|
+
env: options.env ?? process.env,
|
|
200
|
+
...(options.projectDir ? { projectDir: options.projectDir } : {})
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function setupNudgeAfterInstall(plan, exitCode, options) {
|
|
204
|
+
if (exitCode !== 0) {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
return maybeSetupNudge(plan.classification.manager, { env: options.env ?? process.env });
|
|
208
|
+
}
|
|
206
209
|
export function deriveLiveView(state, phase, resolvedTotal) {
|
|
207
210
|
const verified = state.decisions.filter((decision) => decision.action === "pass").length;
|
|
208
211
|
const warnDecisions = state.decisions.filter((decision) => decision.action === "warn");
|
|
@@ -221,57 +224,43 @@ export function deriveLiveView(state, phase, resolvedTotal) {
|
|
|
221
224
|
...(blocked ? { blocked: describeBlockedInstall(blocked) } : {})
|
|
222
225
|
};
|
|
223
226
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const finish = (value) => {
|
|
232
|
-
if (settled)
|
|
227
|
+
export function prepareProxyWorker(classification, env) {
|
|
228
|
+
const worker = startProxyWorker(classification, env, undefined);
|
|
229
|
+
let discarded = false;
|
|
230
|
+
return {
|
|
231
|
+
worker,
|
|
232
|
+
discard: () => {
|
|
233
|
+
if (discarded) {
|
|
233
234
|
return;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
242
|
-
child = spawn(invocation.command, [...invocation.args], {
|
|
243
|
-
env,
|
|
244
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
245
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
235
|
+
}
|
|
236
|
+
discarded = true;
|
|
237
|
+
void worker.then((proxy) => {
|
|
238
|
+
if (!("decision" in proxy)) {
|
|
239
|
+
stopProxyWorker(proxy);
|
|
240
|
+
}
|
|
246
241
|
});
|
|
247
242
|
}
|
|
248
|
-
|
|
249
|
-
finish(undefined);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
register(child);
|
|
253
|
-
timer = setTimeout(() => {
|
|
254
|
-
try {
|
|
255
|
-
child.kill();
|
|
256
|
-
}
|
|
257
|
-
catch { /* already exited */ }
|
|
258
|
-
finish(undefined);
|
|
259
|
-
}, 30_000);
|
|
260
|
-
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
261
|
-
child.on("error", () => finish(undefined));
|
|
262
|
-
child.on("close", (code) => finish(code === 0 ? parsePipReportInstallCount(stdout) : undefined));
|
|
263
|
-
});
|
|
243
|
+
};
|
|
264
244
|
}
|
|
265
|
-
export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
245
|
+
export async function runWithProductionProxyLive(plan, args, options, onView, prepared) {
|
|
266
246
|
if (options.onStdout || options.onStderr) {
|
|
267
247
|
throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
|
|
268
248
|
}
|
|
269
249
|
const env = options.env ?? process.env;
|
|
270
250
|
maybeWarnRootWithoutState(env);
|
|
271
|
-
|
|
251
|
+
if (prepared && options.forceOverride) {
|
|
252
|
+
prepared.discard();
|
|
253
|
+
prepared = undefined;
|
|
254
|
+
}
|
|
255
|
+
const proxy = prepared
|
|
256
|
+
? await prepared.worker
|
|
257
|
+
: await startProxyWorker(plan.classification, env, options.forceOverride);
|
|
272
258
|
if ("decision" in proxy) {
|
|
273
259
|
return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
|
|
274
260
|
}
|
|
261
|
+
if (options.preverified && options.preverified.length > 0) {
|
|
262
|
+
writePreverifiedFile(proxy.session.dir, options.preverified);
|
|
263
|
+
}
|
|
275
264
|
const restoreSignalHandlers = installProxySignalHandlers(proxy);
|
|
276
265
|
try {
|
|
277
266
|
const childEnv = buildProxyChildEnv({
|
|
@@ -279,22 +268,10 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
279
268
|
baseEnv: plan.childEnv,
|
|
280
269
|
proxyUrl: proxy.proxyUrl,
|
|
281
270
|
caBundlePath: proxy.session.files.ca,
|
|
282
|
-
cacheDir:
|
|
271
|
+
cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
|
|
283
272
|
});
|
|
284
273
|
const spawner = options.spawner ?? defaultSpawner;
|
|
285
|
-
|
|
286
|
-
let dryRunChild;
|
|
287
|
-
if (plan.classification.manager === "pip") {
|
|
288
|
-
const cached = cachedPipResolution(plan.realBinary.path ?? "", args);
|
|
289
|
-
if (cached) {
|
|
290
|
-
resolvedTotal = cached.count;
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
void resolvePipInstallTotal(plan.realBinary.path ?? "", args, childEnv, (child) => { dryRunChild = child; })
|
|
294
|
-
.then((count) => { resolvedTotal = count; })
|
|
295
|
-
.catch(() => undefined);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
274
|
+
const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
|
|
298
275
|
const poll = setInterval(() => {
|
|
299
276
|
onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
|
|
300
277
|
}, 90);
|
|
@@ -308,12 +285,6 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
308
285
|
}
|
|
309
286
|
finally {
|
|
310
287
|
clearInterval(poll);
|
|
311
|
-
if (dryRunChild) {
|
|
312
|
-
try {
|
|
313
|
-
dryRunChild.kill();
|
|
314
|
-
}
|
|
315
|
-
catch { /* already exited */ }
|
|
316
|
-
}
|
|
317
288
|
}
|
|
318
289
|
const proxyState = readProxySessionState(proxy.session);
|
|
319
290
|
onView(deriveLiveView(proxyState, "done", resolvedTotal));
|
|
@@ -331,13 +302,20 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
|
|
|
331
302
|
const outcome = installOutcome(finished.stdout);
|
|
332
303
|
return { exitCode: EXIT_INSTALL_BLOCKED, stdout: outcome, stderr: cacheOnlyNotice(outcome.length > 0) };
|
|
333
304
|
}
|
|
334
|
-
return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr:
|
|
305
|
+
return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: `${scriptGateLine(plan, finished.exitCode, options)}${setupNudgeAfterInstall(plan, finished.exitCode, options)}` };
|
|
335
306
|
}
|
|
336
307
|
finally {
|
|
337
308
|
restoreSignalHandlers();
|
|
338
309
|
stopProxyWorker(proxy);
|
|
339
310
|
}
|
|
340
311
|
}
|
|
312
|
+
function prepareProxyCacheDir(manager, sessionDir, env) {
|
|
313
|
+
const cacheDir = `${sessionDir}/pm-cache`;
|
|
314
|
+
if (manager === "cargo") {
|
|
315
|
+
prepareCargoHome(cacheDir, userCargoHome(env));
|
|
316
|
+
}
|
|
317
|
+
return cacheDir;
|
|
318
|
+
}
|
|
341
319
|
function cacheOnlyNotice(installedFresh) {
|
|
342
320
|
const theme = createTheme(resolvePresentation().color);
|
|
343
321
|
const reason = installedFresh ? "used a saved copy" : "already installed";
|
|
@@ -425,6 +403,38 @@ function renderDecisions(decisions, env) {
|
|
|
425
403
|
const visible = isCiEnv(env) ? decisions.filter((decision) => decision.action !== "pass") : decisions;
|
|
426
404
|
return visible.map((decision) => redactSecrets(renderInstallDecision(decision))).join("");
|
|
427
405
|
}
|
|
406
|
+
export function loadProjectCooldownExemptions(env, cwd = process.cwd()) {
|
|
407
|
+
try {
|
|
408
|
+
const root = findProjectRoot(cwd, env);
|
|
409
|
+
if (!root) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const file = loadDgFile(root);
|
|
413
|
+
return file.readable ? file.cooldownExemptions : [];
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
export function dgFileExemptionsNotice(exemptions) {
|
|
420
|
+
if (exemptions.length === 0) {
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
const plural = exemptions.length === 1 ? "exemption" : "exemptions";
|
|
424
|
+
return `dg: applying ${exemptions.length} cooldown ${plural} from this project's dg.json\n`;
|
|
425
|
+
}
|
|
426
|
+
let dgFileExemptionNoticeWritten = false;
|
|
427
|
+
function maybeNoticeDgFileExemptions(exemptions) {
|
|
428
|
+
if (dgFileExemptionNoticeWritten) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const notice = dgFileExemptionsNotice(exemptions);
|
|
432
|
+
if (!notice) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
dgFileExemptionNoticeWritten = true;
|
|
436
|
+
process.stderr.write(notice);
|
|
437
|
+
}
|
|
428
438
|
async function startProxyWorker(classification, env, forceOverride) {
|
|
429
439
|
const paths = resolveDgPaths(env);
|
|
430
440
|
const session = createSessionSync(paths);
|
|
@@ -439,11 +449,15 @@ async function startProxyWorker(classification, env, forceOverride) {
|
|
|
439
449
|
throw new Error("production proxy worker is not built");
|
|
440
450
|
}
|
|
441
451
|
const config = loadUserConfig(env);
|
|
452
|
+
const exemptions = loadProjectCooldownExemptions(env);
|
|
453
|
+
maybeNoticeDgFileExemptions(exemptions);
|
|
454
|
+
const exemptionsEnv = writeCooldownExemptionsFile(session.dir, exemptions);
|
|
442
455
|
const child = spawn(process.execPath, [workerPath, sessionBootstrapPath, config.api.baseUrl], {
|
|
443
456
|
env: {
|
|
444
457
|
...env,
|
|
445
458
|
DG_PROXY_CLASSIFICATION: JSON.stringify(classification),
|
|
446
|
-
...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {})
|
|
459
|
+
...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {}),
|
|
460
|
+
...exemptionsEnv
|
|
447
461
|
},
|
|
448
462
|
stdio: ["pipe", "pipe", "pipe"]
|
|
449
463
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
|
|
2
|
+
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
3
|
+
export function resolveSpawnInvocation(binary, args, platform = process.platform) {
|
|
4
|
+
if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
|
|
5
|
+
return { command: binary, args, windowsVerbatimArguments: false };
|
|
6
|
+
}
|
|
7
|
+
const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
|
|
8
|
+
return {
|
|
9
|
+
command: process.env.comspec ?? "cmd.exe",
|
|
10
|
+
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
11
|
+
windowsVerbatimArguments: true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function escapeCmdCommand(command) {
|
|
15
|
+
return command.replace(CMD_META_CHARS, "^$1");
|
|
16
|
+
}
|
|
17
|
+
// cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
|
|
18
|
+
function escapeCmdArgument(argument) {
|
|
19
|
+
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
20
|
+
return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
|
|
21
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { parseCooldownAge } from "../config/settings.js";
|
|
2
|
+
import { cooldownExemptionActive } from "../project/dgfile.js";
|
|
3
|
+
import { canonicalCooldownName, normalizePypiName } from "./pypi-name.js";
|
|
4
|
+
export { normalizePypiName };
|
|
5
|
+
export function durationToDays(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (trimmed === "" || trimmed === "0" || trimmed === "off") {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
const match = /^([1-9]\d{0,3})(h|d)$/.exec(trimmed);
|
|
11
|
+
if (!match || !match[1] || !match[2]) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const amount = Number.parseInt(match[1], 10);
|
|
15
|
+
return match[2] === "h" ? amount / 24 : amount;
|
|
16
|
+
}
|
|
17
|
+
export function effectiveCooldownAge(config, env, ecosystem) {
|
|
18
|
+
const fromEnv = env.DG_COOLDOWN_AGE;
|
|
19
|
+
if (fromEnv !== undefined) {
|
|
20
|
+
try {
|
|
21
|
+
return parseCooldownAge(fromEnv, "DG_COOLDOWN_AGE", false);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// fall through to config: a malformed env override must not change policy
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const perEcosystem = ecosystem === "npm"
|
|
28
|
+
? config.cooldown.npmAge
|
|
29
|
+
: ecosystem === "pypi"
|
|
30
|
+
? config.cooldown.pypiAge
|
|
31
|
+
: config.cooldown.cargoAge;
|
|
32
|
+
return perEcosystem !== "" ? perEcosystem : config.cooldown.age;
|
|
33
|
+
}
|
|
34
|
+
export function isCooldownExempt(packageName, exempt, ecosystem) {
|
|
35
|
+
const name = ecosystem ? canonicalCooldownName(ecosystem, packageName) : packageName;
|
|
36
|
+
return exempt
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((entry) => entry.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.some((pattern) => {
|
|
41
|
+
const candidate = ecosystem ? canonicalCooldownName(ecosystem, pattern) : pattern;
|
|
42
|
+
if (candidate.endsWith("*")) {
|
|
43
|
+
return name.startsWith(candidate.slice(0, -1));
|
|
44
|
+
}
|
|
45
|
+
return name === candidate;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function isCooldownExemptByDgFile(packageName, ecosystem, exemptions, now = new Date()) {
|
|
49
|
+
if (ecosystem !== "npm" && ecosystem !== "pypi" && ecosystem !== "cargo") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const target = canonicalCooldownName(ecosystem, packageName);
|
|
53
|
+
return exemptions.some((e) => {
|
|
54
|
+
if (e.ecosystem !== ecosystem) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const candidate = canonicalCooldownName(ecosystem, e.name);
|
|
58
|
+
return candidate === target && cooldownExemptionActive(e, now);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function cooldownRequestParam(config, env, ecosystem, packageName) {
|
|
62
|
+
const minAgeDays = durationToDays(effectiveCooldownAge(config, env, ecosystem));
|
|
63
|
+
if (minAgeDays <= 0) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (packageName && isCooldownExempt(packageName, config.cooldown.exempt, ecosystem)) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return { minAgeDays, onUnknown: config.cooldown.onUnknown };
|
|
70
|
+
}
|
|
71
|
+
export function formatCooldownDuration(days) {
|
|
72
|
+
const hours = days * 24;
|
|
73
|
+
if (hours <= 0) {
|
|
74
|
+
return "0h";
|
|
75
|
+
}
|
|
76
|
+
if (hours < 48) {
|
|
77
|
+
return `${Math.round(hours)}h`;
|
|
78
|
+
}
|
|
79
|
+
return `${Math.floor(days)}d`;
|
|
80
|
+
}
|
|
81
|
+
export function formatPackageAge(ageDays) {
|
|
82
|
+
if (ageDays < 0) {
|
|
83
|
+
return "in the future";
|
|
84
|
+
}
|
|
85
|
+
const hours = ageDays * 24;
|
|
86
|
+
if (hours < 1) {
|
|
87
|
+
return "<1h ago";
|
|
88
|
+
}
|
|
89
|
+
if (hours < 48) {
|
|
90
|
+
return `${Math.floor(hours)}h ago`;
|
|
91
|
+
}
|
|
92
|
+
return `${Math.floor(ageDays)}d ago`;
|
|
93
|
+
}
|
|
94
|
+
export function describeCooldownSettings(config, env) {
|
|
95
|
+
if (env.DG_COOLDOWN_AGE !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
const envDays = durationToDays(parseCooldownAge(env.DG_COOLDOWN_AGE, "DG_COOLDOWN_AGE", false));
|
|
98
|
+
return `${envDays > 0 ? formatCooldownDuration(envDays) : "off"} (DG_COOLDOWN_AGE)`;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// malformed env override is ignored everywhere; describe the config instead
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const baseDays = durationToDays(config.cooldown.age);
|
|
105
|
+
const base = baseDays > 0 ? formatCooldownDuration(baseDays) : "off";
|
|
106
|
+
const overrides = ["npm", "pypi", "cargo"]
|
|
107
|
+
.map((ecosystem) => {
|
|
108
|
+
const value = ecosystem === "npm" ? config.cooldown.npmAge : ecosystem === "pypi" ? config.cooldown.pypiAge : config.cooldown.cargoAge;
|
|
109
|
+
if (value === "") {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const days = durationToDays(value);
|
|
113
|
+
return `${ecosystem} ${days > 0 ? formatCooldownDuration(days) : "off"}`;
|
|
114
|
+
})
|
|
115
|
+
.filter((entry) => entry !== undefined);
|
|
116
|
+
return overrides.length > 0 ? `${base} (${overrides.join(", ")})` : base;
|
|
117
|
+
}
|
package/dist/policy/evaluate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { recordAuditEvent } from "../audit/events.js";
|
|
2
2
|
import { DEFAULT_CONFIG } from "../config/settings.js";
|
|
3
3
|
export function resolveEffectivePolicy(options) {
|
|
4
4
|
if (options.orgPolicy) {
|
|
@@ -69,21 +69,21 @@ export function applyForceOverride(options, env = process.env) {
|
|
|
69
69
|
return {
|
|
70
70
|
allowed: false,
|
|
71
71
|
reason: "force override was not requested",
|
|
72
|
-
|
|
72
|
+
auditRecorded: false
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
if (options.currentAction !== "block") {
|
|
76
76
|
return {
|
|
77
77
|
allowed: false,
|
|
78
78
|
reason: "force override is only valid for block decisions",
|
|
79
|
-
|
|
79
|
+
auditRecorded: false
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
if (!options.policy.allowForceOverride) {
|
|
83
83
|
return {
|
|
84
84
|
allowed: false,
|
|
85
85
|
reason: "force override is disabled by policy",
|
|
86
|
-
|
|
86
|
+
auditRecorded: false
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
const event = {
|
|
@@ -93,25 +93,12 @@ export function applyForceOverride(options, env = process.env) {
|
|
|
93
93
|
policyMode: options.policy.mode,
|
|
94
94
|
createdAt: (options.now ?? new Date()).toISOString()
|
|
95
95
|
};
|
|
96
|
-
recordAuditEvent(event, env);
|
|
97
96
|
return {
|
|
98
97
|
allowed: true,
|
|
99
98
|
reason: event.reason,
|
|
100
|
-
|
|
99
|
+
auditRecorded: recordAuditEvent(event, env)
|
|
101
100
|
};
|
|
102
101
|
}
|
|
103
|
-
export function scriptPolicyArgs(options) {
|
|
104
|
-
if (!options.policy.scriptHardening && options.policy.mode !== "strict") {
|
|
105
|
-
return options.existingArgs;
|
|
106
|
-
}
|
|
107
|
-
if (hasScriptDisableFlag(options.existingArgs)) {
|
|
108
|
-
return options.existingArgs;
|
|
109
|
-
}
|
|
110
|
-
if (options.manager === "yarn") {
|
|
111
|
-
return [...options.existingArgs, "--ignore-scripts"];
|
|
112
|
-
}
|
|
113
|
-
return [...options.existingArgs, "--ignore-scripts"];
|
|
114
|
-
}
|
|
115
102
|
function findTrustedAllowlist(packageName, policy, allowlists) {
|
|
116
103
|
return allowlists.find((entry) => {
|
|
117
104
|
if (entry.packageName !== packageName) {
|
|
@@ -123,6 +110,3 @@ function findTrustedAllowlist(packageName, policy, allowlists) {
|
|
|
123
110
|
return true;
|
|
124
111
|
});
|
|
125
112
|
}
|
|
126
|
-
function hasScriptDisableFlag(args) {
|
|
127
|
-
return args.some((arg) => arg === "--ignore-scripts" || arg === "--ignore-scripts=true");
|
|
128
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizePypiName(name) {
|
|
2
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
3
|
+
}
|
|
4
|
+
// crates.io is case-insensitive and treats '-' and '_' as equivalent, so fold
|
|
5
|
+
// both to a single canonical form for exemption matching.
|
|
6
|
+
export function normalizeCargoName(name) {
|
|
7
|
+
return name.toLowerCase().replace(/_/g, "-");
|
|
8
|
+
}
|
|
9
|
+
export function canonicalCooldownName(ecosystem, name) {
|
|
10
|
+
if (ecosystem === "pypi") {
|
|
11
|
+
return normalizePypiName(name);
|
|
12
|
+
}
|
|
13
|
+
if (ecosystem === "cargo") {
|
|
14
|
+
return normalizeCargoName(name);
|
|
15
|
+
}
|
|
16
|
+
return name;
|
|
17
|
+
}
|
|
@@ -19,8 +19,9 @@ export function colorEnabled(input) {
|
|
|
19
19
|
if (input.forceColorFlag) {
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const forceColor = input.env.FORCE_COLOR;
|
|
23
|
+
if (forceColor !== undefined) {
|
|
24
|
+
return forceColor !== "0" && forceColor !== "false";
|
|
24
25
|
}
|
|
25
26
|
if (input.noColorFlag) {
|
|
26
27
|
return false;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const SITE = "https://westbayberry.com";
|
|
2
|
+
// Only npm and pypi have public, indexable per-package pages today; cargo and
|
|
3
|
+
// other ecosystems have none, so return null rather than link to a 404.
|
|
4
|
+
export function packagePageUrl(ecosystem, name) {
|
|
5
|
+
if (ecosystem !== "npm" && ecosystem !== "pypi") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return `${SITE}/${ecosystem}/${name}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function provenanceLabel(provenance) {
|
|
2
|
+
if (provenance.status === "attested") {
|
|
3
|
+
const tag = slsaTag(provenance.predicateType);
|
|
4
|
+
return tag ? `attested (${tag})` : "attested";
|
|
5
|
+
}
|
|
6
|
+
if (provenance.status === "none") {
|
|
7
|
+
return "none";
|
|
8
|
+
}
|
|
9
|
+
return "unknown (not yet checked)";
|
|
10
|
+
}
|
|
11
|
+
export function provenanceDowngradeLine(version, provenance) {
|
|
12
|
+
if (!provenance.downgrade) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return `provenance downgraded — ${provenance.downgrade.fromVersion} was attested, ${version} is not`;
|
|
16
|
+
}
|
|
17
|
+
function slsaTag(predicateType) {
|
|
18
|
+
if (!predicateType) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const match = /slsa\.dev\/provenance\/(v[0-9][0-9.]*)/.exec(predicateType);
|
|
22
|
+
return match ? `slsa ${match[1]}` : null;
|
|
23
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const ANSI = {
|
|
2
|
-
block: "
|
|
3
|
-
warn: "
|
|
4
|
-
pass: "
|
|
5
|
-
unknown: "
|
|
6
|
-
muted: "
|
|
7
|
-
accent: "
|
|
2
|
+
block: "\u001b[31m",
|
|
3
|
+
warn: "\u001b[33m",
|
|
4
|
+
pass: "\u001b[32m",
|
|
5
|
+
unknown: "\u001b[36m",
|
|
6
|
+
muted: "\u001b[2m",
|
|
7
|
+
accent: "\u001b[1m"
|
|
8
8
|
};
|
|
9
|
-
const RESET = "
|
|
9
|
+
const RESET = "\u001b[0m";
|
|
10
10
|
const BADGES = {
|
|
11
11
|
block: { word: "BLOCK", glyph: "✘", role: "block" },
|
|
12
12
|
warn: { word: "WARN", glyph: "⚠", role: "warn" },
|