@devshub198211/devguard 2.0.1

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.
@@ -0,0 +1,654 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var crypto = require('crypto');
5
+ var path = require('path');
6
+ var os = require('os');
7
+ var https = require('https');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
+ var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
29
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
30
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
31
+ var https__namespace = /*#__PURE__*/_interopNamespace(https);
32
+
33
+ // src/security/lockfile-guardian.ts
34
+ var LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"];
35
+ var MAX_LOCKFILE_SIZE = 100 * 1024 * 1024;
36
+ function safeResolve(root, ...segments) {
37
+ const resolvedRoot = path__namespace.resolve(root);
38
+ const resolved = path__namespace.resolve(resolvedRoot, ...segments);
39
+ if (!resolved.startsWith(resolvedRoot + path__namespace.sep) && resolved !== resolvedRoot) {
40
+ throw new Error(`Path traversal detected: ${segments.join("/")} escapes root ${resolvedRoot}`);
41
+ }
42
+ return resolved;
43
+ }
44
+ function hashFile(filePath) {
45
+ const stat = fs__namespace.statSync(filePath);
46
+ if (stat.size > MAX_LOCKFILE_SIZE) {
47
+ throw new Error(`File too large to hash (${stat.size} bytes, max ${MAX_LOCKFILE_SIZE}): ${filePath}`);
48
+ }
49
+ const buf = fs__namespace.readFileSync(filePath);
50
+ return "sha512-" + crypto__namespace.createHash("sha512").update(buf).digest("base64");
51
+ }
52
+ function verifyLockfile(root = process.cwd(), snapshotPath) {
53
+ const resolvedRoot = path__namespace.resolve(root);
54
+ const snap = snapshotPath ? path__namespace.resolve(snapshotPath) : path__namespace.join(resolvedRoot, ".lockguard-snapshot.json");
55
+ let snapshot = null;
56
+ if (fs__namespace.existsSync(snap)) {
57
+ try {
58
+ const raw = JSON.parse(fs__namespace.readFileSync(snap, "utf-8"));
59
+ if (raw && typeof raw === "object" && raw.version === 2 && typeof raw.entries === "object" && raw.entries !== null) {
60
+ snapshot = raw;
61
+ }
62
+ } catch {
63
+ }
64
+ }
65
+ const tampered = [];
66
+ const missing = [];
67
+ const added = [];
68
+ const entries = [];
69
+ const known = new Set(Object.keys(snapshot?.entries ?? {}));
70
+ for (const lf of LOCKFILES) {
71
+ const full = safeResolve(resolvedRoot, lf);
72
+ if (!fs__namespace.existsSync(full)) {
73
+ if (known.has(lf)) missing.push(lf);
74
+ continue;
75
+ }
76
+ try {
77
+ const stat = fs__namespace.statSync(full);
78
+ if (!stat.isFile()) continue;
79
+ const hash = hashFile(full);
80
+ entries.push({ file: lf, hash, size: stat.size, mtime: stat.mtimeMs });
81
+ if (snapshot) {
82
+ const prev = snapshot.entries[lf];
83
+ if (!prev) {
84
+ added.push(lf);
85
+ } else if (prev.hash !== hash) {
86
+ tampered.push(lf);
87
+ }
88
+ }
89
+ } catch {
90
+ }
91
+ }
92
+ return {
93
+ valid: tampered.length === 0 && missing.length === 0,
94
+ tampered,
95
+ missing,
96
+ added,
97
+ entries,
98
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString()
99
+ };
100
+ }
101
+ function createSnapshot(root = process.cwd(), snapshotPath) {
102
+ const resolvedRoot = path__namespace.resolve(root);
103
+ const snap = snapshotPath ? path__namespace.resolve(snapshotPath) : path__namespace.join(resolvedRoot, ".lockguard-snapshot.json");
104
+ const entries = {};
105
+ for (const lf of LOCKFILES) {
106
+ const full = safeResolve(resolvedRoot, lf);
107
+ if (!fs__namespace.existsSync(full)) continue;
108
+ try {
109
+ const stat = fs__namespace.statSync(full);
110
+ if (!stat.isFile()) continue;
111
+ entries[lf] = { hash: hashFile(full), size: stat.size };
112
+ } catch {
113
+ }
114
+ }
115
+ const snapshot = {
116
+ version: 2,
117
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
118
+ host: os__namespace.hostname(),
119
+ entries
120
+ };
121
+ const tmpPath = snap + ".tmp." + crypto__namespace.randomBytes(4).toString("hex");
122
+ try {
123
+ fs__namespace.writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2));
124
+ fs__namespace.renameSync(tmpPath, snap);
125
+ } catch (e) {
126
+ try {
127
+ fs__namespace.unlinkSync(tmpPath);
128
+ } catch {
129
+ }
130
+ throw e;
131
+ }
132
+ return snapshot;
133
+ }
134
+ function extractTransitiveDeps(root = process.cwd()) {
135
+ const resolvedRoot = path__namespace.resolve(root);
136
+ const pkgLock = path__namespace.join(resolvedRoot, "package-lock.json");
137
+ if (!fs__namespace.existsSync(pkgLock)) return {};
138
+ try {
139
+ const lock = JSON.parse(fs__namespace.readFileSync(pkgLock, "utf-8"));
140
+ const deps = {};
141
+ const packages = lock.packages;
142
+ if (typeof packages !== "object" || packages === null) return {};
143
+ for (const [k, v] of Object.entries(packages)) {
144
+ if (k && typeof v === "object" && v !== null && typeof v.version === "string") {
145
+ const name = k.replace(/^node_modules\//, "");
146
+ if (name === "__proto__" || name === "constructor" || name === "prototype") continue;
147
+ deps[name] = v.version;
148
+ }
149
+ }
150
+ return deps;
151
+ } catch {
152
+ return {};
153
+ }
154
+ }
155
+ var MAX_PKG_JSON_SIZE = 2 * 1024 * 1024;
156
+ var MAX_SCRIPT_LENGTH = 1e4;
157
+ var MAX_SCAN_DEPTH = 5;
158
+ var RULES = [
159
+ { id: "R001", label: "Curl-to-shell pipe", severity: "critical", test: (s) => /curl\s+.+\|.*(ba)?sh/i.test(s) },
160
+ { id: "R002", label: "Wget-to-shell pipe", severity: "critical", test: (s) => /wget\s+.+\|.*(ba)?sh/i.test(s) },
161
+ { id: "R003", label: "Netcat reverse shell", severity: "critical", test: (s) => /\bnc(at)?\b.+-e\b|\bncat\b.+\bsh\b/i.test(s) },
162
+ { id: "R004", label: "System credential access", severity: "critical", test: (s) => /\/etc\/(passwd|shadow|sudoers)/i.test(s) },
163
+ { id: "R005", label: "SSH key exfiltration", severity: "critical", test: (s) => /\.ssh\/(id_rsa|authorized_keys|config)/i.test(s) },
164
+ { id: "R006", label: "Crypto miner binary", severity: "critical", test: (s) => /\b(xmrig|minerd|cpuminer|ethminer|claymore)\b/i.test(s) },
165
+ { id: "R007", label: "Stratum mining pool URL", severity: "critical", test: (s) => /stratum\+tcp:\/\//i.test(s) },
166
+ { id: "R008", label: "eval() usage", severity: "high", test: (s) => /\beval\s*\(/.test(s) },
167
+ { id: "R009", label: "Base64 decode + exec", severity: "high", test: (s) => /base64.*(decode|--decode|-d).*exec|exec.*base64.*(decode|--decode|-d)/i.test(s) },
168
+ { id: "R010", label: "Dynamic require from URL", severity: "high", test: (s) => /require\s*\(\s*['"]https?:/i.test(s) },
169
+ { id: "R011", label: "child_process exec in hook", severity: "high", test: (s) => /child_process.*\.(exec|spawn|execSync|spawnSync)/i.test(s) },
170
+ { id: "R012", label: "Function constructor exec", severity: "high", test: (s) => /new\s+Function\s*\(/.test(s) },
171
+ { id: "R013", label: "setTimeout with string arg", severity: "high", test: (s) => /setTimeout\s*\(\s*['"`]/.test(s) },
172
+ { id: "R014", label: "Unauthorized npm publish", severity: "high", test: (s) => /npm\s+(publish|adduser|login)/.test(s) },
173
+ { id: "R015", label: "HOME/USERPROFILE exfiltration", severity: "medium", test: (s) => /process\.env\.(HOME|USERPROFILE|APPDATA)/i.test(s) },
174
+ { id: "R016", label: "PATH env manipulation", severity: "medium", test: (s) => /process\.env\.PATH\s*=/.test(s) },
175
+ { id: "R017", label: "Outbound HTTP in hook", severity: "medium", test: (s) => /https?\.get|axios\.|fetch\s*\(|request\s*\(/i.test(s) },
176
+ { id: "R018", label: "Filesystem write in hook", severity: "medium", test: (s) => /fs\.(writeFile|writeFileSync|appendFile|unlink|rmdir|rm\b)/i.test(s) },
177
+ { id: "R019", label: "Registry credential file read", severity: "medium", test: (s) => /\.npmrc|\.yarnrc|\.gradle|\.m2\/settings/i.test(s) },
178
+ { id: "R020", label: "Hex string obfuscation", severity: "high", test: (s) => /(\\x[0-9a-f]{2}){6,}/i.test(s) },
179
+ { id: "R021", label: "Unicode escape obfuscation", severity: "high", test: (s) => /(\\u[0-9a-f]{4}){4,}/i.test(s) },
180
+ { id: "R022", label: "String.fromCharCode array", severity: "high", test: (s) => /String\.fromCharCode\s*\(.{20,}\)/i.test(s) },
181
+ { id: "R023", label: "Excessive base64 payload", severity: "high", test: (_s, d) => d !== void 0 && d.length > 200 }
182
+ ];
183
+ var HOOK_NAMES = ["preinstall", "install", "postinstall", "preuninstall", "postuninstall", "prepare", "prepublish", "prepack", "postpack"];
184
+ function tryDecodeBase64(script) {
185
+ const match = script.match(/[A-Za-z0-9+/]{60,}={0,2}/);
186
+ if (!match) return void 0;
187
+ try {
188
+ return Buffer.from(match[0], "base64").toString("utf-8");
189
+ } catch {
190
+ return void 0;
191
+ }
192
+ }
193
+ function safeTruncate(script) {
194
+ return script.length > MAX_SCRIPT_LENGTH ? script.slice(0, MAX_SCRIPT_LENGTH) : script;
195
+ }
196
+ function scanPackage(pkgJsonPath) {
197
+ if (!fs__namespace.existsSync(pkgJsonPath)) return [];
198
+ try {
199
+ const stat = fs__namespace.statSync(pkgJsonPath);
200
+ if (!stat.isFile()) return [];
201
+ if (stat.size > MAX_PKG_JSON_SIZE) return [];
202
+ } catch {
203
+ return [];
204
+ }
205
+ let pkg;
206
+ try {
207
+ pkg = JSON.parse(fs__namespace.readFileSync(pkgJsonPath, "utf-8"));
208
+ } catch {
209
+ return [];
210
+ }
211
+ if (typeof pkg !== "object" || pkg === null) return [];
212
+ const scripts = pkg.scripts ?? {};
213
+ if (typeof scripts !== "object" || scripts === null) return [];
214
+ const findings = [];
215
+ const pkgName = typeof pkg.name === "string" ? pkg.name : path__namespace.dirname(pkgJsonPath);
216
+ const version = typeof pkg.version === "string" ? pkg.version : "unknown";
217
+ for (const hook of HOOK_NAMES) {
218
+ const script = scripts[hook];
219
+ if (!script || typeof script !== "string") continue;
220
+ const truncated = safeTruncate(script);
221
+ const decoded = tryDecodeBase64(truncated);
222
+ for (const rule of RULES) {
223
+ if (rule.test(truncated, decoded) || decoded && rule.test(decoded, void 0)) {
224
+ findings.push({
225
+ package: pkgName,
226
+ version,
227
+ script: hook,
228
+ ruleId: rule.id,
229
+ pattern: rule.label,
230
+ severity: rule.severity,
231
+ raw: script.slice(0, 300),
232
+ deobfuscated: decoded?.slice(0, 300)
233
+ });
234
+ }
235
+ }
236
+ }
237
+ return findings;
238
+ }
239
+ function scanNodeModules(root = process.cwd()) {
240
+ const resolvedRoot = path__namespace.resolve(root);
241
+ const nmPath = path__namespace.join(resolvedRoot, "node_modules");
242
+ if (!fs__namespace.existsSync(nmPath)) return [];
243
+ if (!nmPath.startsWith(resolvedRoot + path__namespace.sep) && nmPath !== resolvedRoot) return [];
244
+ const all = [];
245
+ function scanDir(dir, depth) {
246
+ if (depth > MAX_SCAN_DEPTH) return;
247
+ let entries;
248
+ try {
249
+ entries = fs__namespace.readdirSync(dir, { withFileTypes: true });
250
+ } catch {
251
+ return;
252
+ }
253
+ for (const e of entries) {
254
+ if (e.isSymbolicLink()) continue;
255
+ if (!e.isDirectory()) continue;
256
+ const full = path__namespace.join(dir, e.name);
257
+ const resolvedFull = path__namespace.resolve(full);
258
+ if (!resolvedFull.startsWith(nmPath)) continue;
259
+ if (e.name.startsWith("@")) {
260
+ scanDir(full, depth + 1);
261
+ continue;
262
+ }
263
+ const pkgJson = path__namespace.join(full, "package.json");
264
+ if (fs__namespace.existsSync(pkgJson)) all.push(...scanPackage(pkgJson));
265
+ }
266
+ }
267
+ scanDir(nmPath, 0);
268
+ return all;
269
+ }
270
+ function scanProject(root = process.cwd()) {
271
+ const resolvedRoot = path__namespace.resolve(root);
272
+ return [...scanPackage(path__namespace.join(resolvedRoot, "package.json")), ...scanNodeModules(resolvedRoot)];
273
+ }
274
+ var MAX_RESPONSE_SIZE = 1 * 1024 * 1024;
275
+ var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["api.github.com", "registry.npmjs.org"]);
276
+ function validateHost(url) {
277
+ const parsed = new URL(url);
278
+ if (parsed.protocol !== "https:") {
279
+ throw new Error("Only HTTPS URLs are allowed");
280
+ }
281
+ if (!ALLOWED_HOSTS.has(parsed.hostname)) {
282
+ throw new Error(`Host not allowed: ${parsed.hostname}`);
283
+ }
284
+ return parsed;
285
+ }
286
+ function httpsGet(url, headers) {
287
+ return new Promise((resolve4, reject) => {
288
+ const u = validateHost(url);
289
+ const req = https__namespace.request({ hostname: u.hostname, path: u.pathname + u.search, headers, method: "GET" }, (res) => {
290
+ let body = "";
291
+ let size = 0;
292
+ res.on("data", (d) => {
293
+ size += typeof d === "string" ? d.length : d.length;
294
+ if (size > MAX_RESPONSE_SIZE) {
295
+ req.destroy();
296
+ reject(new Error("Response too large"));
297
+ return;
298
+ }
299
+ body += d;
300
+ });
301
+ res.on("end", () => resolve4({ status: res.statusCode ?? 0, body }));
302
+ });
303
+ req.on("error", (e) => reject(new Error(`Network error: ${e.message}`)));
304
+ req.setTimeout(8e3, () => {
305
+ req.destroy();
306
+ reject(new Error("Request timeout"));
307
+ });
308
+ req.end();
309
+ });
310
+ }
311
+ function httpsPost(url, headers, payload) {
312
+ return new Promise((resolve4, reject) => {
313
+ const u = validateHost(url);
314
+ const body = Buffer.from(payload);
315
+ if (body.length > MAX_RESPONSE_SIZE) {
316
+ reject(new Error("Payload too large"));
317
+ return;
318
+ }
319
+ const req = https__namespace.request({
320
+ hostname: u.hostname,
321
+ path: u.pathname + u.search,
322
+ method: "POST",
323
+ headers: { ...headers, "Content-Length": String(body.length) }
324
+ }, (res) => {
325
+ let b = "";
326
+ let size = 0;
327
+ res.on("data", (d) => {
328
+ size += typeof d === "string" ? d.length : d.length;
329
+ if (size > MAX_RESPONSE_SIZE) {
330
+ req.destroy();
331
+ reject(new Error("Response too large"));
332
+ return;
333
+ }
334
+ b += d;
335
+ });
336
+ res.on("end", () => resolve4({ status: res.statusCode ?? 0, body: b }));
337
+ });
338
+ req.on("error", (e) => reject(new Error(`Network error: ${e.message}`)));
339
+ req.setTimeout(1e4, () => {
340
+ req.destroy();
341
+ reject(new Error("Request timeout"));
342
+ });
343
+ req.write(body);
344
+ req.end();
345
+ });
346
+ }
347
+ async function inspectGitHubToken(token, name = "GITHUB_TOKEN") {
348
+ if (!token || typeof token !== "string" || token.length < 4) {
349
+ return { name, provider: "github", status: "invalid", ageDays: null, maxAgeDays: 90, message: "Token is empty or too short" };
350
+ }
351
+ try {
352
+ const res = await httpsGet("https://api.github.com/user", {
353
+ "Authorization": `Bearer ${token}`,
354
+ "User-Agent": "devguard/2.0",
355
+ "Accept": "application/vnd.github+json",
356
+ "X-GitHub-Api-Version": "2022-11-28"
357
+ });
358
+ if (res.status === 401) return { name, provider: "github", status: "invalid", ageDays: null, maxAgeDays: 90, message: "Token is invalid or revoked" };
359
+ if (res.status !== 200) return { name, provider: "github", status: "unknown", ageDays: null, maxAgeDays: 90, message: `API returned ${res.status}` };
360
+ return { name, provider: "github", status: "ok", ageDays: null, maxAgeDays: 90, message: "Token valid \u2014 use fine-grained PATs with expiry for age tracking" };
361
+ } catch (e) {
362
+ const msg = e instanceof Error ? e.message : "Unknown error";
363
+ return { name, provider: "github", status: "unknown", ageDays: null, maxAgeDays: 90, message: `Check failed: ${msg}` };
364
+ }
365
+ }
366
+ async function inspectNpmToken(token, name = "NPM_TOKEN") {
367
+ if (!token || typeof token !== "string" || token.length < 4) {
368
+ return { name, provider: "npm", status: "invalid", ageDays: null, maxAgeDays: 90, message: "Token is empty or too short" };
369
+ }
370
+ try {
371
+ const res = await httpsGet("https://registry.npmjs.org/-/npm/v1/tokens", { "Authorization": `Bearer ${token}`, "Accept": "application/json" });
372
+ if (res.status === 401) return { name, provider: "npm", status: "invalid", ageDays: null, maxAgeDays: 90, message: "Token invalid or revoked" };
373
+ if (res.status === 200) {
374
+ let data;
375
+ try {
376
+ data = JSON.parse(res.body);
377
+ } catch {
378
+ return { name, provider: "npm", status: "unknown", ageDays: null, maxAgeDays: 90, message: "Invalid JSON response from registry" };
379
+ }
380
+ const tokens = Array.isArray(data.objects) ? data.objects : [];
381
+ const prefix = token.slice(0, 6);
382
+ const found = tokens.find((t) => t.key?.startsWith(prefix) || t.token?.startsWith(prefix));
383
+ const extra = [found?.readonly ? "read-only" : "", found?.cidr_whitelist?.length ? "CIDR-restricted" : ""].filter(Boolean).join(", ");
384
+ return { name, provider: "npm", status: "ok", ageDays: null, maxAgeDays: 90, message: `Valid npm token${extra ? " (" + extra + ")" : ""}` };
385
+ }
386
+ return { name, provider: "npm", status: "unknown", ageDays: null, maxAgeDays: 90, message: `Registry returned ${res.status}` };
387
+ } catch (e) {
388
+ const msg = e instanceof Error ? e.message : "Unknown error";
389
+ return { name, provider: "npm", status: "unknown", ageDays: null, maxAgeDays: 90, message: `Check failed: ${msg}` };
390
+ }
391
+ }
392
+ async function createNpmToken(existingToken, opts) {
393
+ if (!existingToken || typeof existingToken !== "string") {
394
+ return { success: false, error: "Token is required" };
395
+ }
396
+ try {
397
+ const payload = JSON.stringify({ password: existingToken, readonly: opts?.readonly ?? false, cidr_whitelist: opts?.cidr ?? [] });
398
+ const res = await httpsPost("https://registry.npmjs.org/-/npm/v1/tokens", { "Authorization": `Bearer ${existingToken}`, "Content-Type": "application/json" }, payload);
399
+ if (res.status === 200 || res.status === 201) {
400
+ let parsed;
401
+ try {
402
+ parsed = JSON.parse(res.body);
403
+ } catch {
404
+ return { success: false, error: "Invalid JSON in npm response" };
405
+ }
406
+ if (typeof parsed.token !== "string") return { success: false, error: "No token in response" };
407
+ return { success: true, newToken: parsed.token };
408
+ }
409
+ return { success: false, error: `npm registry returned ${res.status}` };
410
+ } catch (e) {
411
+ const msg = e instanceof Error ? e.message : "Unknown error";
412
+ return { success: false, error: msg };
413
+ }
414
+ }
415
+ function checkTokenAge(configs) {
416
+ return configs.map((cfg) => {
417
+ const maxAgeDays = cfg.maxAgeDays ?? 90;
418
+ if (!cfg.createdAt) return { name: cfg.name, provider: cfg.provider, status: "unknown", ageDays: null, maxAgeDays, message: "Creation date unknown \u2014 set createdAt to enable age tracking" };
419
+ if (typeof cfg.createdAt !== "number" || cfg.createdAt < 0) return { name: cfg.name, provider: cfg.provider, status: "unknown", ageDays: null, maxAgeDays, message: "Invalid createdAt timestamp" };
420
+ const ageDays = Math.floor((Date.now() - cfg.createdAt) / 864e5);
421
+ if (ageDays < 0) return { name: cfg.name, provider: cfg.provider, status: "unknown", ageDays: null, maxAgeDays, message: "Token creation date is in the future" };
422
+ const status = ageDays >= maxAgeDays ? "stale" : ageDays >= maxAgeDays * 0.8 ? "expiring_soon" : "ok";
423
+ return { name: cfg.name, provider: cfg.provider, status, ageDays, maxAgeDays, message: `${ageDays}/${maxAgeDays} days old` };
424
+ });
425
+ }
426
+ function maskToken(token) {
427
+ if (!token || typeof token !== "string") return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
428
+ if (token.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
429
+ return token.slice(0, 4) + "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + token.slice(-4);
430
+ }
431
+ function loadTokensFromEnv(names, maxAgeDays = 90) {
432
+ if (!Array.isArray(names)) return [];
433
+ return names.filter((n) => typeof n === "string" && n.length > 0 && process.env[n]).map((n) => {
434
+ const value = process.env[n];
435
+ const provider = /github|gh_/i.test(n) ? "github" : /npm/i.test(n) ? "npm" : "generic";
436
+ return { name: n, value, provider, maxAgeDays };
437
+ });
438
+ }
439
+ var RANGE_CHARS = /^[\^~*]|^latest$|^next$|^>=|^>/;
440
+ var DEP_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
441
+ var MAX_PKG_JSON_SIZE2 = 5 * 1024 * 1024;
442
+ var MAX_RESPONSE_SIZE2 = 2 * 1024 * 1024;
443
+ var MAX_REDIRECTS = 3;
444
+ function enforceExactPins(pkgJsonPath) {
445
+ const resolved = path__namespace.resolve(pkgJsonPath);
446
+ if (!fs__namespace.existsSync(resolved)) return [];
447
+ try {
448
+ const stat = fs__namespace.statSync(resolved);
449
+ if (!stat.isFile() || stat.size > MAX_PKG_JSON_SIZE2) return [];
450
+ } catch {
451
+ return [];
452
+ }
453
+ let pkg;
454
+ try {
455
+ pkg = JSON.parse(fs__namespace.readFileSync(resolved, "utf-8"));
456
+ } catch {
457
+ return [];
458
+ }
459
+ if (typeof pkg !== "object" || pkg === null) return [];
460
+ const violations = [];
461
+ for (const field of DEP_FIELDS) {
462
+ const deps = pkg[field];
463
+ if (!deps || typeof deps !== "object") continue;
464
+ for (const [name, ver] of Object.entries(deps)) {
465
+ if (typeof ver !== "string") continue;
466
+ if (RANGE_CHARS.test(ver.trim())) {
467
+ const suggestion = resolveExactVersion(path__namespace.dirname(resolved), name) ?? ver.replace(/^[\^~]/, "");
468
+ violations.push({ name, specifier: ver, field, suggestion });
469
+ }
470
+ }
471
+ }
472
+ return violations;
473
+ }
474
+ function resolveExactVersion(root, pkgName) {
475
+ const lockPath = path__namespace.join(root, "package-lock.json");
476
+ if (!fs__namespace.existsSync(lockPath)) return null;
477
+ try {
478
+ const stat = fs__namespace.statSync(lockPath);
479
+ if (stat.size > MAX_PKG_JSON_SIZE2 * 10) return null;
480
+ const lock = JSON.parse(fs__namespace.readFileSync(lockPath, "utf-8"));
481
+ const packages = lock.packages;
482
+ if (typeof packages !== "object" || packages === null) return null;
483
+ const entry = packages["node_modules/" + pkgName];
484
+ return typeof entry?.version === "string" ? entry.version : null;
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+ function extractLockfileSRI(root = process.cwd()) {
490
+ const resolvedRoot = path__namespace.resolve(root);
491
+ const lockPath = path__namespace.join(resolvedRoot, "package-lock.json");
492
+ if (!fs__namespace.existsSync(lockPath)) return {};
493
+ try {
494
+ const lock = JSON.parse(fs__namespace.readFileSync(lockPath, "utf-8"));
495
+ const packages = lock.packages;
496
+ if (typeof packages !== "object" || packages === null) return {};
497
+ const result = {};
498
+ for (const [key, val] of Object.entries(packages)) {
499
+ if (key && typeof val === "object" && val !== null && typeof val.version === "string" && typeof val.integrity === "string") {
500
+ const name = key.replace(/^node_modules\//, "");
501
+ if (name === "__proto__" || name === "constructor" || name === "prototype") continue;
502
+ result[name] = { version: val.version, integrity: val.integrity };
503
+ }
504
+ }
505
+ return result;
506
+ } catch {
507
+ return {};
508
+ }
509
+ }
510
+ function httpsGet2(url, redirectCount = 0) {
511
+ return new Promise((resolve4, reject) => {
512
+ if (redirectCount > MAX_REDIRECTS) {
513
+ reject(new Error("Too many redirects"));
514
+ return;
515
+ }
516
+ let parsed;
517
+ try {
518
+ parsed = new URL(url);
519
+ } catch {
520
+ reject(new Error("Invalid URL"));
521
+ return;
522
+ }
523
+ if (parsed.protocol !== "https:") {
524
+ reject(new Error("Only HTTPS URLs are allowed"));
525
+ return;
526
+ }
527
+ if (parsed.hostname !== "registry.npmjs.org") {
528
+ reject(new Error(`Host not allowed: ${parsed.hostname}`));
529
+ return;
530
+ }
531
+ const req = https__namespace.get(url, { headers: { "Accept": "application/json", "User-Agent": "devguard/2.0" } }, (res) => {
532
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
533
+ try {
534
+ const redirectUrl = new URL(res.headers.location);
535
+ if (redirectUrl.protocol !== "https:" || redirectUrl.hostname !== "registry.npmjs.org") {
536
+ reject(new Error("Redirect to disallowed host"));
537
+ return;
538
+ }
539
+ } catch {
540
+ reject(new Error("Invalid redirect URL"));
541
+ return;
542
+ }
543
+ httpsGet2(res.headers.location, redirectCount + 1).then(resolve4).catch(reject);
544
+ return;
545
+ }
546
+ let body = "";
547
+ let size = 0;
548
+ res.on("data", (d) => {
549
+ size += typeof d === "string" ? d.length : d.length;
550
+ if (size > MAX_RESPONSE_SIZE2) {
551
+ req.destroy();
552
+ reject(new Error("Response too large"));
553
+ return;
554
+ }
555
+ body += d;
556
+ });
557
+ res.on("end", () => resolve4(body));
558
+ });
559
+ req.on("error", (e) => reject(new Error(`Network error: ${e.message}`)));
560
+ req.setTimeout(8e3, () => {
561
+ req.destroy();
562
+ reject(new Error("Request timeout"));
563
+ });
564
+ });
565
+ }
566
+ async function fetchRegistrySRI(name, version) {
567
+ if (!name || !version || typeof name !== "string" || typeof version !== "string") return null;
568
+ try {
569
+ const encoded = encodeURIComponent(name).replace(/%40/g, "@").replace(/%2F/gi, "/");
570
+ const encodedVersion = encodeURIComponent(version);
571
+ const body = await httpsGet2(`https://registry.npmjs.org/${encoded}/${encodedVersion}`);
572
+ const parsed = JSON.parse(body);
573
+ const integrity = parsed?.dist?.integrity;
574
+ return typeof integrity === "string" ? integrity : null;
575
+ } catch {
576
+ return null;
577
+ }
578
+ }
579
+ async function verifySRI(root = process.cwd(), sampleSize = 20) {
580
+ const lockEntries = extractLockfileSRI(root);
581
+ const names = Object.keys(lockEntries).sort();
582
+ const toCheck = sampleSize < names.length ? names.slice(0, sampleSize) : names;
583
+ return Promise.all(toCheck.map(async (name) => {
584
+ const { version, integrity } = lockEntries[name];
585
+ const registryHash = await fetchRegistrySRI(name, version);
586
+ return { name, version, lockfileHash: integrity, registryHash, match: registryHash === null ? null : registryHash === integrity };
587
+ }));
588
+ }
589
+ function autoPin(pkgJsonPath, dryRun = false) {
590
+ const resolved = path__namespace.resolve(pkgJsonPath);
591
+ let rawContent;
592
+ try {
593
+ rawContent = fs__namespace.readFileSync(resolved, "utf-8");
594
+ } catch {
595
+ return { fixed: 0 };
596
+ }
597
+ let pkg;
598
+ try {
599
+ pkg = JSON.parse(rawContent);
600
+ } catch {
601
+ return { fixed: 0 };
602
+ }
603
+ if (typeof pkg !== "object" || pkg === null) return { fixed: 0 };
604
+ const root = path__namespace.dirname(resolved);
605
+ let fixed = 0;
606
+ for (const field of DEP_FIELDS) {
607
+ const deps = pkg[field];
608
+ if (!deps || typeof deps !== "object") continue;
609
+ for (const [name, ver] of Object.entries(deps)) {
610
+ if (typeof ver !== "string") continue;
611
+ if (RANGE_CHARS.test(ver.trim())) {
612
+ const exact = resolveExactVersion(root, name);
613
+ if (exact) {
614
+ deps[name] = exact;
615
+ fixed++;
616
+ }
617
+ }
618
+ }
619
+ }
620
+ const content = JSON.stringify(pkg, null, 2) + "\n";
621
+ if (!dryRun && fixed > 0) {
622
+ const tmpPath = resolved + ".tmp." + crypto__namespace.randomBytes(4).toString("hex");
623
+ try {
624
+ fs__namespace.writeFileSync(tmpPath, content);
625
+ fs__namespace.renameSync(tmpPath, resolved);
626
+ } catch (e) {
627
+ try {
628
+ fs__namespace.unlinkSync(tmpPath);
629
+ } catch {
630
+ }
631
+ throw e;
632
+ }
633
+ }
634
+ return { fixed, content };
635
+ }
636
+
637
+ exports.autoPin = autoPin;
638
+ exports.checkTokenAge = checkTokenAge;
639
+ exports.createNpmToken = createNpmToken;
640
+ exports.createSnapshot = createSnapshot;
641
+ exports.enforceExactPins = enforceExactPins;
642
+ exports.extractLockfileSRI = extractLockfileSRI;
643
+ exports.extractTransitiveDeps = extractTransitiveDeps;
644
+ exports.fetchRegistrySRI = fetchRegistrySRI;
645
+ exports.hashFile = hashFile;
646
+ exports.inspectGitHubToken = inspectGitHubToken;
647
+ exports.inspectNpmToken = inspectNpmToken;
648
+ exports.loadTokensFromEnv = loadTokensFromEnv;
649
+ exports.maskToken = maskToken;
650
+ exports.scanNodeModules = scanNodeModules;
651
+ exports.scanPackage = scanPackage;
652
+ exports.scanProject = scanProject;
653
+ exports.verifyLockfile = verifyLockfile;
654
+ exports.verifySRI = verifySRI;