@westbayberry/dg 2.1.0 → 2.3.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 +123 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +118 -0
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +351 -0
- package/dist/agents/registry.js +128 -0
- package/dist/agents/routing.js +118 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit/rules.js +6 -2
- 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 +17 -5
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +275 -0
- package/dist/commands/audit.js +28 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- 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 +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +196 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +49 -48
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +5 -4
- package/dist/install-ui/block-render.js +18 -14
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +896 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +39 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +44 -7
- package/dist/launcher/env.js +105 -26
- package/dist/launcher/install-preflight.js +64 -18
- package/dist/launcher/live-install.js +29 -8
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +10 -4
- package/dist/launcher/preflight-prompt.js +18 -4
- package/dist/launcher/run.js +178 -103
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- 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/theme.js +7 -7
- package/dist/project/dgfile.js +146 -6
- package/dist/project/override-trust.js +0 -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 +55 -16
- package/dist/proxy/metadata-map.js +66 -12
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +568 -53
- 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 +14 -12
- 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 +132 -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 +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +33 -9
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- 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 +19 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +28 -9
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/trust-store.js +5 -9
- 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 +227 -49
- package/dist/setup/uninstall-standalone.js +25 -0
- 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 +233 -0
- package/dist/standalone/uninstall.mjs +2123 -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/report-writer.js +57 -0
- package/dist/verify/local.js +242 -44
- package/dist/verify/package-check.js +87 -29
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +7 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
package/dist/project/dgfile.js
CHANGED
|
@@ -4,12 +4,50 @@ import { userInfo } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { gitTrimmed } from "../util/git.js";
|
|
6
6
|
import { writeJsonAtomic } from "../util/json-file.js";
|
|
7
|
+
import { canonicalCooldownName } from "../policy/pypi-name.js";
|
|
8
|
+
import { acquireLockSyncWithRetry, resolveDgPaths } from "../state/index.js";
|
|
9
|
+
import { stampAuthoredEntries } from "./override-trust.js";
|
|
10
|
+
const DG_FILE_LOCK_STALE_MS = 10_000;
|
|
11
|
+
const DG_FILE_LOCK_TIMEOUT_MS = 5_000;
|
|
12
|
+
export function withDgFileLock(root, env, action) {
|
|
13
|
+
const name = `dgfile-${createHash("sha1").update(root).digest("hex").slice(0, 16)}`;
|
|
14
|
+
const lock = acquireLockSyncWithRetry(resolveDgPaths(env), name, {
|
|
15
|
+
staleMs: DG_FILE_LOCK_STALE_MS,
|
|
16
|
+
timeoutMs: DG_FILE_LOCK_TIMEOUT_MS
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return action();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
lock.release();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Serialized read-modify-write: re-reads dg.json under the lock so a concurrent
|
|
26
|
+
// cooldown/decisions writer can never clobber the other's section.
|
|
27
|
+
export function mutateDgFile(root, env, mutate) {
|
|
28
|
+
return withDgFileLock(root, env, () => {
|
|
29
|
+
const file = loadDgFile(root);
|
|
30
|
+
if (!file.readable) {
|
|
31
|
+
throw new Error(`refusing to rewrite ${file.path}: ${file.failure ?? "unreadable"}`);
|
|
32
|
+
}
|
|
33
|
+
const next = stampAuthoredEntries(mutate(file), file, root, env);
|
|
34
|
+
saveDgFile(next);
|
|
35
|
+
return next;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function canonicalExemptionName(ecosystem, name) {
|
|
39
|
+
return canonicalCooldownName(ecosystem, name);
|
|
40
|
+
}
|
|
41
|
+
export class CooldownExemptionCapError extends Error {
|
|
42
|
+
}
|
|
7
43
|
export const DG_FILE_NAME = "dg.json";
|
|
8
44
|
export const DECISION_ENTRY_CAP = 500;
|
|
45
|
+
export const COOLDOWN_EXEMPTION_CAP = 500;
|
|
9
46
|
export const DECISION_REASON_MAX = 500;
|
|
10
|
-
const KNOWN_TOP_LEVEL_KEYS = new Set(["version", "scriptApprovals", "decisions"]);
|
|
47
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set(["version", "scriptApprovals", "decisions", "cooldownExemptions"]);
|
|
11
48
|
const KNOWN_SCRIPT_APPROVAL_KEYS = new Set(["npm", "observed"]);
|
|
12
49
|
const SCRIPT_HOOKS = ["preinstall", "install", "postinstall", "gyp"];
|
|
50
|
+
const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
13
51
|
export function dgFilePath(root) {
|
|
14
52
|
return join(root, DG_FILE_NAME);
|
|
15
53
|
}
|
|
@@ -22,7 +60,7 @@ export function findProjectRoot(cwd, env = process.env) {
|
|
|
22
60
|
}
|
|
23
61
|
}
|
|
24
62
|
export function emptyDgFile(path) {
|
|
25
|
-
return { path, exists: false, readable: true, raw: {}, decisions: [], scriptApprovals: emptyScriptApprovals() };
|
|
63
|
+
return { path, exists: false, readable: true, raw: {}, decisions: [], cooldownExemptions: [], scriptApprovals: emptyScriptApprovals() };
|
|
26
64
|
}
|
|
27
65
|
export function loadDgFile(root) {
|
|
28
66
|
const path = dgFilePath(root);
|
|
@@ -61,6 +99,20 @@ export function loadDgFile(root) {
|
|
|
61
99
|
decisions.push(parsed);
|
|
62
100
|
}
|
|
63
101
|
}
|
|
102
|
+
const exemptionsRaw = raw.cooldownExemptions;
|
|
103
|
+
if (exemptionsRaw !== undefined && !Array.isArray(exemptionsRaw)) {
|
|
104
|
+
return failOpen(path, "cooldownExemptions must be an array");
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(exemptionsRaw) && exemptionsRaw.length > COOLDOWN_EXEMPTION_CAP) {
|
|
107
|
+
return failOpen(path, `more than ${COOLDOWN_EXEMPTION_CAP} cooldownExemptions`);
|
|
108
|
+
}
|
|
109
|
+
const cooldownExemptions = [];
|
|
110
|
+
for (const entry of Array.isArray(exemptionsRaw) ? exemptionsRaw : []) {
|
|
111
|
+
const parsed = parseCooldownExemption(entry);
|
|
112
|
+
if (parsed) {
|
|
113
|
+
cooldownExemptions.push(parsed);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
64
116
|
const approvals = approvalsRaw ?? {};
|
|
65
117
|
return {
|
|
66
118
|
path,
|
|
@@ -68,6 +120,7 @@ export function loadDgFile(root) {
|
|
|
68
120
|
readable: true,
|
|
69
121
|
raw,
|
|
70
122
|
decisions,
|
|
123
|
+
cooldownExemptions,
|
|
71
124
|
scriptApprovals: {
|
|
72
125
|
npm: parseEntryMap(approvals.npm, parseApprovalEntry),
|
|
73
126
|
observed: parseEntryMap(approvals.observed, parseObservedEntry),
|
|
@@ -92,6 +145,85 @@ export function appendDecisions(file, additions, now = new Date()) {
|
|
|
92
145
|
export function removeDecisions(file, ids) {
|
|
93
146
|
return { ...file, decisions: file.decisions.filter((entry) => !ids.has(entry.id)) };
|
|
94
147
|
}
|
|
148
|
+
export function appendCooldownExemptions(file, additions, now = new Date()) {
|
|
149
|
+
const canonicalAll = additions.map((a) => ({ ...a, name: canonicalExemptionName(a.ecosystem, a.name) }));
|
|
150
|
+
const canonical = [...new Map(canonicalAll.map((a) => [`${a.ecosystem}:${a.name}`, a])).values()];
|
|
151
|
+
const priorExtra = new Map(file.cooldownExemptions.map((e) => [`${e.ecosystem}:${e.name}`, e.extra]));
|
|
152
|
+
const keep = file.cooldownExemptions.filter((e) => !canonical.some((a) => a.ecosystem === e.ecosystem && a.name === e.name));
|
|
153
|
+
const added = canonical.map((a) => {
|
|
154
|
+
const carried = priorExtra.get(`${a.ecosystem}:${a.name}`);
|
|
155
|
+
return {
|
|
156
|
+
...a,
|
|
157
|
+
reason: a.reason.slice(0, DECISION_REASON_MAX),
|
|
158
|
+
acceptedAt: a.acceptedAt ?? now.toISOString(),
|
|
159
|
+
...(carried !== undefined ? { extra: carried } : {})
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
const next = [...keep, ...added];
|
|
163
|
+
if (next.length > COOLDOWN_EXEMPTION_CAP) {
|
|
164
|
+
const live = next.filter((e) => cooldownExemptionActive(e, now));
|
|
165
|
+
if (live.length > COOLDOWN_EXEMPTION_CAP) {
|
|
166
|
+
throw new CooldownExemptionCapError(`cooldown exemptions would exceed the ${COOLDOWN_EXEMPTION_CAP} cap; run 'dg cooldown prune' or remove some with 'dg cooldown rm'`);
|
|
167
|
+
}
|
|
168
|
+
return { ...file, cooldownExemptions: live };
|
|
169
|
+
}
|
|
170
|
+
return { ...file, cooldownExemptions: next };
|
|
171
|
+
}
|
|
172
|
+
export function removeCooldownExemptions(file, predicate) {
|
|
173
|
+
return { ...file, cooldownExemptions: file.cooldownExemptions.filter((e) => !predicate(e)) };
|
|
174
|
+
}
|
|
175
|
+
// Active when there is no expiry, or the expiry parses to a future instant. A
|
|
176
|
+
// present-but-unparseable expiry fails CLOSED (treated as expired) so a typo'd
|
|
177
|
+
// date can never become a permanent cooldown bypass.
|
|
178
|
+
export function cooldownExemptionActive(e, now = new Date()) {
|
|
179
|
+
if (!e.expiresAt) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
const expiry = Date.parse(e.expiresAt);
|
|
183
|
+
return Number.isFinite(expiry) && expiry > now.getTime();
|
|
184
|
+
}
|
|
185
|
+
function serializeCooldownExemption(e) {
|
|
186
|
+
return {
|
|
187
|
+
...e.extra,
|
|
188
|
+
ecosystem: e.ecosystem,
|
|
189
|
+
name: e.name,
|
|
190
|
+
reason: e.reason,
|
|
191
|
+
acceptedBy: e.acceptedBy,
|
|
192
|
+
acceptedAt: e.acceptedAt,
|
|
193
|
+
...(e.expiresAt ? { expiresAt: e.expiresAt } : {})
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const EXEMPTION_FIELDS = new Set(["ecosystem", "name", "reason", "acceptedBy", "acceptedAt", "expiresAt"]);
|
|
197
|
+
export function parseCooldownExemption(value) {
|
|
198
|
+
if (!isPlainObject(value)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const ecosystem = value.ecosystem;
|
|
202
|
+
if (ecosystem !== "npm" && ecosystem !== "pypi" && ecosystem !== "cargo") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const rawName = value.name;
|
|
206
|
+
if (typeof rawName !== "string" || rawName.length === 0 || /[\u0000-\u001f\u007f\s*]/u.test(rawName)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const name = canonicalExemptionName(ecosystem, rawName);
|
|
210
|
+
const extra = {};
|
|
211
|
+
for (const [key, field] of Object.entries(value)) {
|
|
212
|
+
if (!EXEMPTION_FIELDS.has(key) && !UNSAFE_OBJECT_KEYS.has(key)) {
|
|
213
|
+
extra[key] = field;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const expiresAt = value.expiresAt;
|
|
217
|
+
return {
|
|
218
|
+
ecosystem,
|
|
219
|
+
name,
|
|
220
|
+
reason: typeof value.reason === "string" ? value.reason.slice(0, DECISION_REASON_MAX) : "",
|
|
221
|
+
acceptedBy: typeof value.acceptedBy === "string" && value.acceptedBy.length > 0 ? value.acceptedBy : "unknown",
|
|
222
|
+
acceptedAt: typeof value.acceptedAt === "string" ? value.acceptedAt : "",
|
|
223
|
+
...(typeof expiresAt === "string" ? { expiresAt } : {}),
|
|
224
|
+
...(Object.keys(extra).length > 0 ? { extra } : {})
|
|
225
|
+
};
|
|
226
|
+
}
|
|
95
227
|
export function saveDgFile(file) {
|
|
96
228
|
if (!file.readable) {
|
|
97
229
|
throw new Error(`refusing to rewrite ${file.path}: ${file.failure ?? "unreadable"}`);
|
|
@@ -100,6 +232,9 @@ export function saveDgFile(file) {
|
|
|
100
232
|
if (file.decisions.length > 0) {
|
|
101
233
|
top.decisions = file.decisions.map(serializeDecisionEntry);
|
|
102
234
|
}
|
|
235
|
+
if (file.cooldownExemptions.length > 0) {
|
|
236
|
+
top.cooldownExemptions = file.cooldownExemptions.map(serializeCooldownExemption);
|
|
237
|
+
}
|
|
103
238
|
const approvals = { ...file.scriptApprovals.unknownKeys };
|
|
104
239
|
if (Object.keys(file.scriptApprovals.npm).length > 0) {
|
|
105
240
|
approvals.npm = sortedRecord(file.scriptApprovals.npm);
|
|
@@ -174,7 +309,7 @@ function parseDecisionEntry(value) {
|
|
|
174
309
|
}
|
|
175
310
|
const extra = {};
|
|
176
311
|
for (const [key, field] of Object.entries(value)) {
|
|
177
|
-
if (!ENTRY_FIELDS.has(key)) {
|
|
312
|
+
if (!ENTRY_FIELDS.has(key) && !UNSAFE_OBJECT_KEYS.has(key)) {
|
|
178
313
|
extra[key] = field;
|
|
179
314
|
}
|
|
180
315
|
}
|
|
@@ -215,7 +350,9 @@ function parseFindings(value) {
|
|
|
215
350
|
if (typeof severity !== "number" || !Number.isInteger(severity) || severity < 1 || severity > 5) {
|
|
216
351
|
return null;
|
|
217
352
|
}
|
|
218
|
-
|
|
353
|
+
if (!UNSAFE_OBJECT_KEYS.has(category)) {
|
|
354
|
+
findings[category] = severity;
|
|
355
|
+
}
|
|
219
356
|
}
|
|
220
357
|
return findings;
|
|
221
358
|
}
|
|
@@ -228,6 +365,9 @@ function parseEntryMap(raw, parseEntry) {
|
|
|
228
365
|
}
|
|
229
366
|
const entries = {};
|
|
230
367
|
for (const [name, value] of Object.entries(raw)) {
|
|
368
|
+
if (UNSAFE_OBJECT_KEYS.has(name)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
231
371
|
const parsed = parseEntry(value);
|
|
232
372
|
if (parsed !== null) {
|
|
233
373
|
entries[name] = parsed;
|
|
@@ -290,7 +430,7 @@ function sortedRecord(record) {
|
|
|
290
430
|
function unknownKeysOf(raw, known) {
|
|
291
431
|
const unknown = {};
|
|
292
432
|
for (const [key, value] of Object.entries(raw)) {
|
|
293
|
-
if (!known.has(key)) {
|
|
433
|
+
if (!known.has(key) && !UNSAFE_OBJECT_KEYS.has(key)) {
|
|
294
434
|
unknown[key] = value;
|
|
295
435
|
}
|
|
296
436
|
}
|
|
@@ -303,5 +443,5 @@ function emptyScriptApprovals() {
|
|
|
303
443
|
return { npm: {}, observed: {}, unknownKeys: {} };
|
|
304
444
|
}
|
|
305
445
|
function failOpen(path, failure) {
|
|
306
|
-
return { path, exists: true, readable: false, failure, raw: {}, decisions: [], scriptApprovals: emptyScriptApprovals() };
|
|
446
|
+
return { path, exists: true, readable: false, failure, raw: {}, decisions: [], cooldownExemptions: [], scriptApprovals: emptyScriptApprovals() };
|
|
307
447
|
}
|
|
Binary file
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const PROXY_AUTH_TOKEN_FILENAME = "proxy-auth-token";
|
|
5
|
+
export const PROXY_AUTH_USER = "dg";
|
|
6
|
+
export function generateProxyAuthToken() {
|
|
7
|
+
return randomBytes(32).toString("hex");
|
|
8
|
+
}
|
|
9
|
+
export function proxyAuthTokenPath(sessionDir) {
|
|
10
|
+
return join(sessionDir, PROXY_AUTH_TOKEN_FILENAME);
|
|
11
|
+
}
|
|
12
|
+
export function writeProxyAuthToken(sessionDir, token) {
|
|
13
|
+
writeFileSync(proxyAuthTokenPath(sessionDir), `${token}\n`, {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
mode: 0o600
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function readProxyAuthToken(sessionDir) {
|
|
19
|
+
try {
|
|
20
|
+
const token = readFileSync(proxyAuthTokenPath(sessionDir), "utf8").trim();
|
|
21
|
+
return token.length > 0 ? token : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function proxyUrlWithAuth(proxyUrl, token) {
|
|
28
|
+
const url = new URL(proxyUrl);
|
|
29
|
+
return `${url.protocol}//${PROXY_AUTH_USER}:${token}@${url.host}`;
|
|
30
|
+
}
|
|
31
|
+
export function proxyAuthorizationValue(token) {
|
|
32
|
+
return `Basic ${Buffer.from(`${PROXY_AUTH_USER}:${token}`, "utf8").toString("base64")}`;
|
|
33
|
+
}
|
|
34
|
+
export function verifyProxyAuthorization(header, token) {
|
|
35
|
+
if (typeof header !== "string" || header.length === 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return timingSafeEqual(digest(header), digest(proxyAuthorizationValue(token)));
|
|
39
|
+
}
|
|
40
|
+
function digest(value) {
|
|
41
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
42
|
+
}
|
package/dist/proxy/ca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { isIP } from "node:net";
|
|
@@ -17,6 +17,12 @@ export function createEphemeralCertificateAuthority(caPath, options = {}) {
|
|
|
17
17
|
}
|
|
18
18
|
active = issueAuthority(caPath, lifetimeMs, now(), active.certPem);
|
|
19
19
|
leafs.clear();
|
|
20
|
+
try {
|
|
21
|
+
options.onRotate?.(active.certPem);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
20
26
|
};
|
|
21
27
|
setInterval(rotateIfDue, ROTATION_CHECK_INTERVAL_MS).unref();
|
|
22
28
|
return {
|
|
@@ -37,11 +43,24 @@ export function createEphemeralCertificateAuthority(caPath, options = {}) {
|
|
|
37
43
|
}
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// node:crypto keygen piped into forge via PEM so keygen never falls back to forge's pure-JS path.
|
|
47
|
+
function generateRsaKeyPair() {
|
|
48
|
+
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
|
49
|
+
modulusLength: 2048
|
|
44
50
|
});
|
|
51
|
+
return {
|
|
52
|
+
privateKey: forge.pki.privateKeyFromPem(privateKey.export({
|
|
53
|
+
type: "pkcs8",
|
|
54
|
+
format: "pem"
|
|
55
|
+
}).toString()),
|
|
56
|
+
publicKey: forge.pki.publicKeyFromPem(publicKey.export({
|
|
57
|
+
type: "spki",
|
|
58
|
+
format: "pem"
|
|
59
|
+
}).toString())
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function issueAuthority(caPath, lifetimeMs, nowMs, previousCertPem) {
|
|
63
|
+
const keys = generateRsaKeyPair();
|
|
45
64
|
const cert = forge.pki.createCertificate();
|
|
46
65
|
cert.publicKey = keys.publicKey;
|
|
47
66
|
cert.serialNumber = serialNumber();
|
|
@@ -65,6 +84,10 @@ function issueAuthority(caPath, lifetimeMs, nowMs, previousCertPem) {
|
|
|
65
84
|
cRLSign: true,
|
|
66
85
|
critical: true
|
|
67
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "extKeyUsage",
|
|
89
|
+
serverAuth: true
|
|
90
|
+
},
|
|
68
91
|
{
|
|
69
92
|
name: "subjectKeyIdentifier"
|
|
70
93
|
}
|
|
@@ -102,10 +125,7 @@ function writeCaBundleAtomic(caPath, bundle) {
|
|
|
102
125
|
}
|
|
103
126
|
}
|
|
104
127
|
function createLeafCertificate(host, issuer, lifetimeMs, nowMs) {
|
|
105
|
-
const keys =
|
|
106
|
-
bits: 2048,
|
|
107
|
-
workers: -1
|
|
108
|
-
});
|
|
128
|
+
const keys = generateRsaKeyPair();
|
|
109
129
|
const cert = forge.pki.createCertificate();
|
|
110
130
|
cert.publicKey = keys.publicKey;
|
|
111
131
|
cert.serialNumber = serialNumber();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseCooldownExemption } from "../project/dgfile.js";
|
|
4
|
+
export const COOLDOWN_EXEMPTIONS_FILE = "cooldown-exemptions.json";
|
|
5
|
+
export const COOLDOWN_EXEMPTIONS_ENV = "DG_PROXY_COOLDOWN_EXEMPTIONS_FILE";
|
|
6
|
+
export function writeCooldownExemptionsFile(sessionDir, exemptions) {
|
|
7
|
+
if (exemptions.length === 0) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const path = join(sessionDir, COOLDOWN_EXEMPTIONS_FILE);
|
|
12
|
+
writeFileSync(path, JSON.stringify(exemptions), { encoding: "utf8", mode: 0o600 });
|
|
13
|
+
return { [COOLDOWN_EXEMPTIONS_ENV]: path };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function loadCooldownExemptionsFile(path) {
|
|
20
|
+
if (!path) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
if (!Array.isArray(parsed)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return parsed.map((entry) => parseCooldownExemption(entry)).filter((entry) => entry !== null);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { recordAuditEvent } from "../audit/events.js";
|
|
2
|
-
import { loadUserConfig } from "../config/settings.js";
|
|
2
|
+
import { DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
|
|
3
3
|
import { applyForceOverride, evaluatePackagePolicy, resolveEffectivePolicy } from "../policy/evaluate.js";
|
|
4
4
|
export function parseForceOverrideRequest(raw) {
|
|
5
5
|
if (!raw) {
|
|
@@ -16,8 +16,31 @@ export function parseForceOverrideRequest(raw) {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
let sideEffectFailureNoticed = false;
|
|
20
|
+
export function noteEnforcementSideEffectFailure(error) {
|
|
21
|
+
if (sideEffectFailureNoticed) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
sideEffectFailureNoticed = true;
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
try {
|
|
27
|
+
process.stderr.write(`dg: local state read/write failed (${message}); the install decision is still enforced.\n`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadUserConfigOrDefault(env) {
|
|
34
|
+
try {
|
|
35
|
+
return loadUserConfig(env);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
noteEnforcementSideEffectFailure(error);
|
|
39
|
+
return DEFAULT_CONFIG;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
export function enforceProtectedInstall(request) {
|
|
20
|
-
const userConfig = request.userConfig ??
|
|
43
|
+
const userConfig = request.userConfig ?? loadUserConfigOrDefault(request.env);
|
|
21
44
|
const policy = resolveEffectivePolicy({ userConfig });
|
|
22
45
|
const proxyVerdict = request.proxyVerdict ?? failClosedVerdict(request.classification);
|
|
23
46
|
const packageName = proxyVerdict.packageName ?? derivePackageName(request.classification);
|
|
@@ -39,13 +62,24 @@ export function enforceProtectedInstall(request) {
|
|
|
39
62
|
if (baseDecision.action !== "block") {
|
|
40
63
|
return baseDecision;
|
|
41
64
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
let force;
|
|
66
|
+
try {
|
|
67
|
+
force = applyForceOverride({
|
|
68
|
+
packageName,
|
|
69
|
+
currentAction: "block",
|
|
70
|
+
force: request.forceOverride?.force ?? false,
|
|
71
|
+
policy,
|
|
72
|
+
...(request.now ? { now: request.now } : {})
|
|
73
|
+
}, request.env);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
noteEnforcementSideEffectFailure(error);
|
|
77
|
+
force = {
|
|
78
|
+
allowed: false,
|
|
79
|
+
reason: "force override is unavailable because dg local state could not be read or written",
|
|
80
|
+
auditRecorded: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
49
83
|
if (force.allowed) {
|
|
50
84
|
return {
|
|
51
85
|
...baseDecision,
|
|
@@ -57,13 +91,18 @@ export function enforceProtectedInstall(request) {
|
|
|
57
91
|
}
|
|
58
92
|
};
|
|
59
93
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
try {
|
|
95
|
+
recordAuditEvent({
|
|
96
|
+
type: "install.blocked",
|
|
97
|
+
packageName,
|
|
98
|
+
reason: baseDecision.reason,
|
|
99
|
+
policyMode: policy.mode,
|
|
100
|
+
createdAt: (request.now ?? new Date()).toISOString()
|
|
101
|
+
}, request.env);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
noteEnforcementSideEffectFailure(error);
|
|
105
|
+
}
|
|
67
106
|
if (!request.forceOverride?.force) {
|
|
68
107
|
return baseDecision;
|
|
69
108
|
}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { brotliDecompressSync, gunzipSync, inflateRawSync, inflateSync } from "node:zlib";
|
|
3
|
+
// decodeURIComponent throws on malformed percent-encoding (e.g. a stray % in a
|
|
4
|
+
// registry href); fall back to the raw value so one bad entry cannot throw out of
|
|
5
|
+
// the whole metadata-extraction pass and drop every identity.
|
|
6
|
+
function safeDecodeUriComponent(value) {
|
|
7
|
+
try {
|
|
8
|
+
return decodeURIComponent(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
2
14
|
export function extractRegistryMetadataIdentities(metadataUrl, response) {
|
|
15
|
+
const decoded = { headers: response.headers, body: decodeContentEncoding(response.headers, response.body) };
|
|
3
16
|
// PyPI Simple index (PEP 503 HTML or PEP 691 JSON) — the pip/uv/pipx flow.
|
|
4
17
|
if (isPypiSimpleIndexUrl(metadataUrl)) {
|
|
5
|
-
return extractPypiSimpleIdentities(metadataUrl,
|
|
18
|
+
return extractPypiSimpleIdentities(metadataUrl, decoded);
|
|
6
19
|
}
|
|
7
|
-
if (!looksLikeJson(
|
|
20
|
+
if (!looksLikeJson(decoded.headers["content-type"])) {
|
|
8
21
|
return [];
|
|
9
22
|
}
|
|
10
|
-
const parsed = parseJson(
|
|
23
|
+
const parsed = parseJson(decoded.body);
|
|
11
24
|
if (!isRecord(parsed)) {
|
|
12
25
|
return [];
|
|
13
26
|
}
|
|
@@ -16,6 +29,46 @@ export function extractRegistryMetadataIdentities(metadataUrl, response) {
|
|
|
16
29
|
...extractPypiIdentities(metadataUrl, parsed)
|
|
17
30
|
];
|
|
18
31
|
}
|
|
32
|
+
// A MITM'd registry can return a tiny gzip/brotli body that expands to gigabytes;
|
|
33
|
+
// cap the decoded size so a decompression bomb cannot exhaust proxy memory.
|
|
34
|
+
const MAX_METADATA_DECODED_BYTES = 32 * 1024 * 1024;
|
|
35
|
+
// Content-Encoding lists encodings in the order they were applied; decode in
|
|
36
|
+
// reverse. Any unknown token or decode failure returns the body untouched so
|
|
37
|
+
// the response falls back to artifact handling instead of crashing the proxy.
|
|
38
|
+
function decodeContentEncoding(headers, body) {
|
|
39
|
+
const raw = headers["content-encoding"];
|
|
40
|
+
const encodings = (Array.isArray(raw) ? raw.join(",") : String(raw ?? ""))
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((token) => token.trim().toLowerCase())
|
|
43
|
+
.filter((token) => token.length > 0 && token !== "identity");
|
|
44
|
+
const cap = { maxOutputLength: MAX_METADATA_DECODED_BYTES };
|
|
45
|
+
let decoded = body;
|
|
46
|
+
for (const encoding of encodings.reverse()) {
|
|
47
|
+
try {
|
|
48
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
49
|
+
decoded = gunzipSync(decoded, cap);
|
|
50
|
+
}
|
|
51
|
+
else if (encoding === "br") {
|
|
52
|
+
decoded = brotliDecompressSync(decoded, cap);
|
|
53
|
+
}
|
|
54
|
+
else if (encoding === "deflate") {
|
|
55
|
+
try {
|
|
56
|
+
decoded = inflateSync(decoded, cap);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
decoded = inflateRawSync(decoded, cap);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return decoded;
|
|
71
|
+
}
|
|
19
72
|
// A registry INDEX/metadata request (not an artifact download). These must be
|
|
20
73
|
// passed through untouched — never verified as a package — so e.g. pip's
|
|
21
74
|
// `pypi.org/simple/<pkg>/` index is not mistaken for a package named "simple".
|
|
@@ -27,7 +80,8 @@ export function extractRegistryMetadataIdentities(metadataUrl, response) {
|
|
|
27
80
|
export function isRegistryIndexRequest(url) {
|
|
28
81
|
return (isPypiSimpleIndexUrl(url) ||
|
|
29
82
|
/\/pypi\/[^/]+\/json\/?$/i.test(url.pathname) ||
|
|
30
|
-
|
|
83
|
+
// PEP 658 sidecar: only `<wheel|sdist>.metadata`, not any path ending .metadata
|
|
84
|
+
/\.(whl|tar\.gz|tgz|zip)\.metadata$/i.test(url.pathname) ||
|
|
31
85
|
/^\/-\//.test(url.pathname));
|
|
32
86
|
}
|
|
33
87
|
function isPypiSimpleIndexUrl(url) {
|
|
@@ -65,7 +119,7 @@ function extractPypiSimpleIdentities(metadataUrl, response) {
|
|
|
65
119
|
catch {
|
|
66
120
|
continue;
|
|
67
121
|
}
|
|
68
|
-
const file = entry.filename ??
|
|
122
|
+
const file = entry.filename ?? safeDecodeUriComponent(absolute.pathname.split("/").filter(Boolean).at(-1) ?? "");
|
|
69
123
|
const parsed = parsePypiArtifactFilename(file);
|
|
70
124
|
if (!parsed) {
|
|
71
125
|
continue;
|
|
@@ -201,7 +255,7 @@ function extractPypiIdentities(metadataUrl, parsed) {
|
|
|
201
255
|
function fallbackIdentity(artifactUrl, classification) {
|
|
202
256
|
const ecosystem = ecosystemForManager(classification.manager);
|
|
203
257
|
const parsed = ecosystem === "pypi"
|
|
204
|
-
? parsePypiArtifactFilename(
|
|
258
|
+
? parsePypiArtifactFilename(safeDecodeUriComponent(artifactUrl.pathname.split("/").filter(Boolean).at(-1) ?? "")) ?? parsePackageVersionFromUrl(artifactUrl)
|
|
205
259
|
: ecosystem === "cargo"
|
|
206
260
|
? parseCargoArtifactUrl(artifactUrl) ?? parsePackageVersionFromUrl(artifactUrl)
|
|
207
261
|
: parsePackageVersionFromUrl(artifactUrl);
|
|
@@ -223,11 +277,11 @@ function parseCargoArtifactUrl(artifactUrl) {
|
|
|
223
277
|
const downloadMatch = /^\/(?:api\/v1\/)?crates\/([^/]+)\/([^/]+)\/download\/?$/.exec(artifactUrl.pathname);
|
|
224
278
|
if (downloadMatch && downloadMatch[1] && downloadMatch[2]) {
|
|
225
279
|
return {
|
|
226
|
-
name:
|
|
227
|
-
version:
|
|
280
|
+
name: safeDecodeUriComponent(downloadMatch[1]),
|
|
281
|
+
version: safeDecodeUriComponent(downloadMatch[2])
|
|
228
282
|
};
|
|
229
283
|
}
|
|
230
|
-
const file =
|
|
284
|
+
const file = safeDecodeUriComponent(artifactUrl.pathname.split("/").filter(Boolean).at(-1) ?? "");
|
|
231
285
|
if (/\.crate$/i.test(file)) {
|
|
232
286
|
const stem = file.slice(0, -".crate".length);
|
|
233
287
|
const match = /^(.+)-(\d[0-9A-Za-z.+-]*)$/.exec(stem);
|
|
@@ -238,7 +292,7 @@ function parseCargoArtifactUrl(artifactUrl) {
|
|
|
238
292
|
return null;
|
|
239
293
|
}
|
|
240
294
|
function parsePackageVersionFromUrl(artifactUrl) {
|
|
241
|
-
const parts = artifactUrl.pathname.split("/").filter(Boolean).map((part) =>
|
|
295
|
+
const parts = artifactUrl.pathname.split("/").filter(Boolean).map((part) => safeDecodeUriComponent(part));
|
|
242
296
|
const file = parts.at(-1) ?? artifactUrl.hostname;
|
|
243
297
|
const npmPackage = npmPackageFromTarballPath(parts);
|
|
244
298
|
const packageName = npmPackage ?? parts.at(-2) ?? file.replace(/\.(?:tgz|tar\.gz|zip|whl)$/i, "");
|
|
@@ -300,14 +354,14 @@ function parseNpmPackageSpec(spec) {
|
|
|
300
354
|
return { name, version };
|
|
301
355
|
}
|
|
302
356
|
function packageNameFromNpmMetadataPath(metadataUrl) {
|
|
303
|
-
const parts = metadataUrl.pathname.split("/").filter(Boolean).map((part) =>
|
|
357
|
+
const parts = metadataUrl.pathname.split("/").filter(Boolean).map((part) => safeDecodeUriComponent(part));
|
|
304
358
|
if (parts[0]?.startsWith("@") && parts[1]) {
|
|
305
359
|
return `${parts[0]}/${parts[1]}`;
|
|
306
360
|
}
|
|
307
361
|
return parts[0] ?? metadataUrl.hostname;
|
|
308
362
|
}
|
|
309
363
|
function packageNameFromPypiMetadataPath(metadataUrl) {
|
|
310
|
-
const parts = metadataUrl.pathname.split("/").filter(Boolean).map((part) =>
|
|
364
|
+
const parts = metadataUrl.pathname.split("/").filter(Boolean).map((part) => safeDecodeUriComponent(part));
|
|
311
365
|
const projectIndex = parts.findIndex((part) => part.toLowerCase() === "pypi");
|
|
312
366
|
return parts[projectIndex + 1] ?? parts[0] ?? metadataUrl.hostname;
|
|
313
367
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const PREVERIFIED_FILE = "preverified.json";
|
|
4
|
+
export function preverifiedKey(ecosystem, name, version) {
|
|
5
|
+
const canonicalName = ecosystem === "pypi" ? name.toLowerCase().replace(/[-_.]+/g, "-") : name;
|
|
6
|
+
return `${ecosystem}:${canonicalName}@${version}`;
|
|
7
|
+
}
|
|
8
|
+
export function writePreverifiedFile(sessionDir, entries) {
|
|
9
|
+
writeFileSync(join(sessionDir, PREVERIFIED_FILE), `${JSON.stringify(entries)}\n`, {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
mode: 0o600
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
let loaded = null;
|
|
15
|
+
export function loadPreverifiedMap(sessionDir) {
|
|
16
|
+
if (loaded?.dir === sessionDir) {
|
|
17
|
+
return loaded.map;
|
|
18
|
+
}
|
|
19
|
+
const path = join(sessionDir, PREVERIFIED_FILE);
|
|
20
|
+
if (!existsSync(path)) {
|
|
21
|
+
return new Map();
|
|
22
|
+
}
|
|
23
|
+
const map = new Map();
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
if (Array.isArray(parsed)) {
|
|
27
|
+
for (const entry of parsed) {
|
|
28
|
+
if (isPreverifiedEntry(entry)) {
|
|
29
|
+
map.set(preverifiedKey(entry.ecosystem, entry.name, entry.version), entry);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return new Map();
|
|
36
|
+
}
|
|
37
|
+
loaded = { dir: sessionDir, map };
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
export function resetPreverifiedCache() {
|
|
41
|
+
loaded = null;
|
|
42
|
+
}
|
|
43
|
+
function isPreverifiedEntry(value) {
|
|
44
|
+
if (typeof value !== "object" || value === null) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const entry = value;
|
|
48
|
+
return ((entry.ecosystem === "npm" || entry.ecosystem === "pypi") &&
|
|
49
|
+
typeof entry.name === "string" && entry.name.length > 0 &&
|
|
50
|
+
typeof entry.version === "string" && entry.version.length > 0 &&
|
|
51
|
+
(entry.action === "pass" || entry.action === "warn") &&
|
|
52
|
+
(entry.reason === undefined || typeof entry.reason === "string") &&
|
|
53
|
+
(entry.scannedSha256 === undefined || typeof entry.scannedSha256 === "string") &&
|
|
54
|
+
typeof entry.cooldownEvaluated === "boolean");
|
|
55
|
+
}
|