@mr-aftab-ahmad-khan/depguard-cli 0.1.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/CHANGELOG.md +10 -0
- package/LICENSE +15 -0
- package/README.md +337 -0
- package/dist/chunk-BH7GFJ3G.js +592 -0
- package/dist/chunk-BH7GFJ3G.js.map +1 -0
- package/dist/cli.cjs +679 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +88 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +639 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/scanner.ts
|
|
30
|
+
var import_node_fs2 = require("fs");
|
|
31
|
+
var import_node_path2 = require("path");
|
|
32
|
+
var import_node_os2 = require("os");
|
|
33
|
+
|
|
34
|
+
// src/scoring.ts
|
|
35
|
+
var SEVERITY_ORDER = {
|
|
36
|
+
info: 0,
|
|
37
|
+
low: 1,
|
|
38
|
+
medium: 2,
|
|
39
|
+
high: 3,
|
|
40
|
+
critical: 4
|
|
41
|
+
};
|
|
42
|
+
function maxSeverity(findings) {
|
|
43
|
+
let worst = "info";
|
|
44
|
+
for (const f of findings) {
|
|
45
|
+
if (SEVERITY_ORDER[f.severity] > SEVERITY_ORDER[worst]) worst = f.severity;
|
|
46
|
+
}
|
|
47
|
+
return worst;
|
|
48
|
+
}
|
|
49
|
+
function severityAtLeast(a, b) {
|
|
50
|
+
return SEVERITY_ORDER[a] >= SEVERITY_ORDER[b];
|
|
51
|
+
}
|
|
52
|
+
function scoreInstallScript(script) {
|
|
53
|
+
const reasons = [];
|
|
54
|
+
let score = 0;
|
|
55
|
+
const lower = script.toLowerCase();
|
|
56
|
+
if (/(curl|wget)\s+[^|]+\|\s*(sh|bash|zsh|node)/i.test(script)) {
|
|
57
|
+
score += 5;
|
|
58
|
+
reasons.push("curl/wget piped to a shell or node");
|
|
59
|
+
}
|
|
60
|
+
if (/\bbase64\b/.test(lower) && /\b(eval|exec)\b/.test(lower)) {
|
|
61
|
+
score += 5;
|
|
62
|
+
reasons.push("base64 combined with eval/exec");
|
|
63
|
+
}
|
|
64
|
+
if (/\bnew\s+Function\s*\(/.test(script)) {
|
|
65
|
+
score += 3;
|
|
66
|
+
reasons.push("dynamic `new Function()` invocation");
|
|
67
|
+
}
|
|
68
|
+
if (/process\.env(\.|\[)/.test(script)) {
|
|
69
|
+
score += 2;
|
|
70
|
+
reasons.push("reads process.env at install time");
|
|
71
|
+
}
|
|
72
|
+
if (/https?:\/\/(?!registry\.npmjs\.org|nodejs\.org|github\.com)[^\s'"`]+/i.test(script)) {
|
|
73
|
+
score += 3;
|
|
74
|
+
reasons.push("network call to a non-trusted domain");
|
|
75
|
+
}
|
|
76
|
+
if (/[A-Za-z0-9+/]{200,}={0,2}/.test(script)) {
|
|
77
|
+
score += 3;
|
|
78
|
+
reasons.push("very long opaque token (possible obfuscation)");
|
|
79
|
+
}
|
|
80
|
+
if (script.length > 300 && !script.includes("\n")) {
|
|
81
|
+
score += 2;
|
|
82
|
+
reasons.push("obfuscated one-liner > 300 chars");
|
|
83
|
+
}
|
|
84
|
+
return { score: Math.min(score, 10), reasons };
|
|
85
|
+
}
|
|
86
|
+
function levenshtein(a, b) {
|
|
87
|
+
if (a === b) return 0;
|
|
88
|
+
if (a.length === 0) return b.length;
|
|
89
|
+
if (b.length === 0) return a.length;
|
|
90
|
+
const prev = new Array(b.length + 1);
|
|
91
|
+
const curr = new Array(b.length + 1);
|
|
92
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
93
|
+
for (let i = 1; i <= a.length; i++) {
|
|
94
|
+
curr[0] = i;
|
|
95
|
+
for (let j = 1; j <= b.length; j++) {
|
|
96
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
97
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
98
|
+
}
|
|
99
|
+
for (let j = 0; j <= b.length; j++) prev[j] = curr[j];
|
|
100
|
+
}
|
|
101
|
+
return prev[b.length];
|
|
102
|
+
}
|
|
103
|
+
function findTyposquatCandidate(name, topPackages) {
|
|
104
|
+
if (topPackages.includes(name)) return void 0;
|
|
105
|
+
for (const top of topPackages) {
|
|
106
|
+
if (Math.abs(top.length - name.length) > 2) continue;
|
|
107
|
+
const dist = levenshtein(name, top);
|
|
108
|
+
if (dist > 0 && dist <= 2) return top;
|
|
109
|
+
}
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/registry.ts
|
|
114
|
+
var import_node_fs = require("fs");
|
|
115
|
+
var import_node_os = require("os");
|
|
116
|
+
var import_node_path = require("path");
|
|
117
|
+
var CACHE_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".depguard", "cache");
|
|
118
|
+
function cacheFile(name) {
|
|
119
|
+
const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
120
|
+
return (0, import_node_path.join)(CACHE_DIR, `${safe}.json`);
|
|
121
|
+
}
|
|
122
|
+
function readCache(name) {
|
|
123
|
+
const file = cacheFile(name);
|
|
124
|
+
if (!(0, import_node_fs.existsSync)(file)) return void 0;
|
|
125
|
+
try {
|
|
126
|
+
const entry = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
127
|
+
if (entry.expiresAt > Date.now()) return entry.data;
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function writeCache(name, data, ttlMs) {
|
|
134
|
+
if (!(0, import_node_fs.existsSync)(CACHE_DIR)) (0, import_node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
|
|
135
|
+
const entry = { expiresAt: Date.now() + ttlMs, data };
|
|
136
|
+
(0, import_node_fs.writeFileSync)(cacheFile(name), JSON.stringify(entry), "utf8");
|
|
137
|
+
}
|
|
138
|
+
async function fetchPackument(name, opts = {}) {
|
|
139
|
+
const ttl = opts.ttlMs ?? 60 * 60 * 1e3;
|
|
140
|
+
const cached = readCache(name);
|
|
141
|
+
if (cached) return cached;
|
|
142
|
+
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
143
|
+
if (!fetcher) return void 0;
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetcher(`https://registry.npmjs.org/${encodeURIComponent(name).replace("%40", "@")}`);
|
|
146
|
+
if (!res.ok) return void 0;
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
writeCache(name, data, ttl);
|
|
149
|
+
return data;
|
|
150
|
+
} catch {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/top-packages.ts
|
|
156
|
+
var TOP_PACKAGES = [
|
|
157
|
+
"react",
|
|
158
|
+
"react-dom",
|
|
159
|
+
"next",
|
|
160
|
+
"vue",
|
|
161
|
+
"svelte",
|
|
162
|
+
"angular",
|
|
163
|
+
"lodash",
|
|
164
|
+
"axios",
|
|
165
|
+
"express",
|
|
166
|
+
"fastify",
|
|
167
|
+
"koa",
|
|
168
|
+
"hono",
|
|
169
|
+
"moment",
|
|
170
|
+
"date-fns",
|
|
171
|
+
"dayjs",
|
|
172
|
+
"luxon",
|
|
173
|
+
"typescript",
|
|
174
|
+
"ts-node",
|
|
175
|
+
"tsx",
|
|
176
|
+
"tsup",
|
|
177
|
+
"esbuild",
|
|
178
|
+
"webpack",
|
|
179
|
+
"vite",
|
|
180
|
+
"rollup",
|
|
181
|
+
"parcel",
|
|
182
|
+
"babel",
|
|
183
|
+
"eslint",
|
|
184
|
+
"prettier",
|
|
185
|
+
"jest",
|
|
186
|
+
"vitest",
|
|
187
|
+
"mocha",
|
|
188
|
+
"chai",
|
|
189
|
+
"sinon",
|
|
190
|
+
"ava",
|
|
191
|
+
"cypress",
|
|
192
|
+
"playwright",
|
|
193
|
+
"puppeteer",
|
|
194
|
+
"redux",
|
|
195
|
+
"zustand",
|
|
196
|
+
"jotai",
|
|
197
|
+
"mobx",
|
|
198
|
+
"recoil",
|
|
199
|
+
"rxjs",
|
|
200
|
+
"graphql",
|
|
201
|
+
"apollo-client",
|
|
202
|
+
"apollo-server",
|
|
203
|
+
"prisma",
|
|
204
|
+
"drizzle-orm",
|
|
205
|
+
"kysely",
|
|
206
|
+
"knex",
|
|
207
|
+
"typeorm",
|
|
208
|
+
"sequelize",
|
|
209
|
+
"mongoose",
|
|
210
|
+
"pg",
|
|
211
|
+
"mysql2",
|
|
212
|
+
"sqlite3",
|
|
213
|
+
"better-sqlite3",
|
|
214
|
+
"redis",
|
|
215
|
+
"ioredis",
|
|
216
|
+
"bullmq",
|
|
217
|
+
"kafkajs",
|
|
218
|
+
"amqplib",
|
|
219
|
+
"socket.io",
|
|
220
|
+
"ws",
|
|
221
|
+
"uws",
|
|
222
|
+
"polka",
|
|
223
|
+
"fastify",
|
|
224
|
+
"h3",
|
|
225
|
+
"commander",
|
|
226
|
+
"yargs",
|
|
227
|
+
"minimist",
|
|
228
|
+
"inquirer",
|
|
229
|
+
"prompts",
|
|
230
|
+
"ora",
|
|
231
|
+
"chalk",
|
|
232
|
+
"picocolors",
|
|
233
|
+
"kleur",
|
|
234
|
+
"boxen",
|
|
235
|
+
"cli-table3",
|
|
236
|
+
"figlet",
|
|
237
|
+
"fs-extra",
|
|
238
|
+
"fast-glob",
|
|
239
|
+
"glob",
|
|
240
|
+
"globby",
|
|
241
|
+
"rimraf",
|
|
242
|
+
"del",
|
|
243
|
+
"execa",
|
|
244
|
+
"shelljs",
|
|
245
|
+
"cross-env",
|
|
246
|
+
"dotenv",
|
|
247
|
+
"joi",
|
|
248
|
+
"yup",
|
|
249
|
+
"zod",
|
|
250
|
+
"ajv",
|
|
251
|
+
"valibot",
|
|
252
|
+
"superstruct",
|
|
253
|
+
"io-ts",
|
|
254
|
+
"class-validator",
|
|
255
|
+
"passport",
|
|
256
|
+
"jsonwebtoken",
|
|
257
|
+
"jose",
|
|
258
|
+
"bcrypt",
|
|
259
|
+
"bcryptjs",
|
|
260
|
+
"argon2",
|
|
261
|
+
"uuid",
|
|
262
|
+
"nanoid",
|
|
263
|
+
"ulid",
|
|
264
|
+
"cuid",
|
|
265
|
+
"shortid",
|
|
266
|
+
"ms",
|
|
267
|
+
"humanize-duration",
|
|
268
|
+
"pluralize",
|
|
269
|
+
"marked",
|
|
270
|
+
"remark",
|
|
271
|
+
"rehype",
|
|
272
|
+
"showdown",
|
|
273
|
+
"turndown",
|
|
274
|
+
"cheerio",
|
|
275
|
+
"jsdom",
|
|
276
|
+
"playwright-core",
|
|
277
|
+
"node-fetch",
|
|
278
|
+
"got",
|
|
279
|
+
"ky",
|
|
280
|
+
"openai",
|
|
281
|
+
"anthropic",
|
|
282
|
+
"ai",
|
|
283
|
+
"@modelcontextprotocol/sdk",
|
|
284
|
+
"langchain",
|
|
285
|
+
"llamaindex",
|
|
286
|
+
"tiktoken",
|
|
287
|
+
"gpt-3-encoder",
|
|
288
|
+
"tailwindcss",
|
|
289
|
+
"postcss",
|
|
290
|
+
"autoprefixer",
|
|
291
|
+
"sass",
|
|
292
|
+
"stylus",
|
|
293
|
+
"less",
|
|
294
|
+
"styled-components",
|
|
295
|
+
"@emotion/react",
|
|
296
|
+
"@emotion/styled",
|
|
297
|
+
"framer-motion",
|
|
298
|
+
"react-router-dom",
|
|
299
|
+
"react-query",
|
|
300
|
+
"@tanstack/react-query",
|
|
301
|
+
"swr",
|
|
302
|
+
"trpc",
|
|
303
|
+
"@trpc/server",
|
|
304
|
+
"@trpc/client",
|
|
305
|
+
"winston",
|
|
306
|
+
"pino",
|
|
307
|
+
"bunyan",
|
|
308
|
+
"morgan",
|
|
309
|
+
"debug",
|
|
310
|
+
"sharp",
|
|
311
|
+
"jimp",
|
|
312
|
+
"canvas",
|
|
313
|
+
"puppeteer-core",
|
|
314
|
+
"playwright-chromium",
|
|
315
|
+
"exceljs",
|
|
316
|
+
"xlsx",
|
|
317
|
+
"pdfkit",
|
|
318
|
+
"pdf-lib",
|
|
319
|
+
"pdfmake",
|
|
320
|
+
"node-cron",
|
|
321
|
+
"agenda",
|
|
322
|
+
"bull",
|
|
323
|
+
"node-schedule",
|
|
324
|
+
"@sentry/node",
|
|
325
|
+
"@sentry/browser",
|
|
326
|
+
"@sentry/react",
|
|
327
|
+
"stripe",
|
|
328
|
+
"@stripe/stripe-js",
|
|
329
|
+
"twilio",
|
|
330
|
+
"nodemailer",
|
|
331
|
+
"resend",
|
|
332
|
+
"aws-sdk",
|
|
333
|
+
"@aws-sdk/client-s3",
|
|
334
|
+
"@aws-sdk/client-dynamodb",
|
|
335
|
+
"googleapis",
|
|
336
|
+
"firebase",
|
|
337
|
+
"firebase-admin",
|
|
338
|
+
"supabase",
|
|
339
|
+
"@supabase/supabase-js",
|
|
340
|
+
"mongodb",
|
|
341
|
+
"mongoose",
|
|
342
|
+
"elasticsearch",
|
|
343
|
+
"@elastic/elasticsearch",
|
|
344
|
+
"puppeteer-extra",
|
|
345
|
+
"playwright-extra",
|
|
346
|
+
"discord.js",
|
|
347
|
+
"telegraf",
|
|
348
|
+
"node-telegram-bot-api",
|
|
349
|
+
"slack-sdk",
|
|
350
|
+
"@slack/web-api",
|
|
351
|
+
"fast-xml-parser",
|
|
352
|
+
"xml2js",
|
|
353
|
+
"yaml",
|
|
354
|
+
"toml",
|
|
355
|
+
"ini",
|
|
356
|
+
"msgpack-lite",
|
|
357
|
+
"protobufjs",
|
|
358
|
+
"grpc",
|
|
359
|
+
"@grpc/grpc-js"
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
// src/scanner.ts
|
|
363
|
+
function readJsonSafe(file) {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
|
|
366
|
+
} catch {
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function findInstalled(root, depth) {
|
|
371
|
+
const nm = (0, import_node_path2.join)(root, "node_modules");
|
|
372
|
+
if (!(0, import_node_fs2.existsSync)(nm)) return [];
|
|
373
|
+
const out = [];
|
|
374
|
+
const visited = /* @__PURE__ */ new Set();
|
|
375
|
+
const walk = (dir, level) => {
|
|
376
|
+
if (visited.has(dir)) return;
|
|
377
|
+
visited.add(dir);
|
|
378
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return;
|
|
379
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
380
|
+
if (entry.startsWith(".")) continue;
|
|
381
|
+
const pkgDir = (0, import_node_path2.join)(dir, entry);
|
|
382
|
+
try {
|
|
383
|
+
const stat = (0, import_node_fs2.statSync)(pkgDir);
|
|
384
|
+
if (!stat.isDirectory()) continue;
|
|
385
|
+
} catch {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (entry.startsWith("@")) {
|
|
389
|
+
for (const sub of (0, import_node_fs2.readdirSync)(pkgDir)) {
|
|
390
|
+
handle((0, import_node_path2.join)(pkgDir, sub), `${entry}/${sub}`, level);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
handle(pkgDir, entry, level);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const handle = (pkgDir, name, level) => {
|
|
398
|
+
const pj = readJsonSafe((0, import_node_path2.join)(pkgDir, "package.json"));
|
|
399
|
+
if (!pj) return;
|
|
400
|
+
out.push({
|
|
401
|
+
name: pj.name ?? name,
|
|
402
|
+
version: pj.version ?? "0.0.0",
|
|
403
|
+
dir: pkgDir,
|
|
404
|
+
pkgJson: pj
|
|
405
|
+
});
|
|
406
|
+
if (depth === "all" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(pkgDir, "node_modules"))) {
|
|
407
|
+
walk((0, import_node_path2.join)(pkgDir, "node_modules"), level + 1);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
walk(nm, 0);
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function loadBaseline() {
|
|
414
|
+
const file = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".depguard", "baseline.json");
|
|
415
|
+
if (!(0, import_node_fs2.existsSync)(file)) return {};
|
|
416
|
+
try {
|
|
417
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
|
|
418
|
+
} catch {
|
|
419
|
+
return {};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
var INSTALL_SCRIPT_FIELDS = ["preinstall", "install", "postinstall", "preuninstall", "preprepare", "prepare"];
|
|
423
|
+
function scriptFindings(pkg) {
|
|
424
|
+
const findings = [];
|
|
425
|
+
const scripts = pkg.pkgJson.scripts ?? {};
|
|
426
|
+
for (const field of INSTALL_SCRIPT_FIELDS) {
|
|
427
|
+
const src = scripts[field];
|
|
428
|
+
if (!src) continue;
|
|
429
|
+
const { score, reasons } = scoreInstallScript(src);
|
|
430
|
+
if (score === 0) continue;
|
|
431
|
+
const severity = score >= 8 ? "critical" : score >= 5 ? "high" : score >= 3 ? "medium" : "low";
|
|
432
|
+
findings.push({
|
|
433
|
+
rule: `install-script:${field}`,
|
|
434
|
+
severity,
|
|
435
|
+
message: `${field} script is suspicious (${reasons.join("; ")})`,
|
|
436
|
+
score
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return findings;
|
|
440
|
+
}
|
|
441
|
+
function maintainerFindings(pkg, packument, baseline) {
|
|
442
|
+
if (!packument?.maintainers) return [];
|
|
443
|
+
const now = packument.maintainers.map((m) => m.name).sort();
|
|
444
|
+
const known = baseline[pkg.name]?.maintainers;
|
|
445
|
+
if (!known) return [];
|
|
446
|
+
const added = now.filter((m) => !known.includes(m));
|
|
447
|
+
if (added.length === 0) return [];
|
|
448
|
+
return [{
|
|
449
|
+
rule: "maintainer-change",
|
|
450
|
+
severity: "high",
|
|
451
|
+
message: `New maintainer(s) added since baseline: ${added.join(", ")}`,
|
|
452
|
+
score: 6
|
|
453
|
+
}];
|
|
454
|
+
}
|
|
455
|
+
function versionAnomalyFindings(pkg, packument) {
|
|
456
|
+
if (!packument?.time) return [];
|
|
457
|
+
const out = [];
|
|
458
|
+
const time = packument.time;
|
|
459
|
+
const versions = Object.keys(time).filter((k) => k !== "created" && k !== "modified");
|
|
460
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1e3;
|
|
461
|
+
const major = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const v of versions) {
|
|
463
|
+
const m = /^(\d+)\./.exec(v);
|
|
464
|
+
if (!m) continue;
|
|
465
|
+
const key = Number(m[1]);
|
|
466
|
+
const arr = major.get(key) ?? [];
|
|
467
|
+
arr.push(v);
|
|
468
|
+
major.set(key, arr);
|
|
469
|
+
}
|
|
470
|
+
const keys = [...major.keys()].sort((a, b) => a - b);
|
|
471
|
+
for (let i = 1; i < keys.length; i++) {
|
|
472
|
+
const prev = keys[i - 1];
|
|
473
|
+
const cur = keys[i];
|
|
474
|
+
const prevReleases = major.get(prev).map((v) => Date.parse(time[v] ?? "")).filter(Number.isFinite);
|
|
475
|
+
const curReleases = major.get(cur).map((v) => Date.parse(time[v] ?? "")).filter(Number.isFinite);
|
|
476
|
+
if (prevReleases.length === 0 || curReleases.length === 0) continue;
|
|
477
|
+
const lastPrev = Math.max(...prevReleases);
|
|
478
|
+
const firstCur = Math.min(...curReleases);
|
|
479
|
+
if (firstCur - lastPrev < sevenDays) {
|
|
480
|
+
out.push({
|
|
481
|
+
rule: "version-anomaly",
|
|
482
|
+
severity: "medium",
|
|
483
|
+
message: `Major bump v${prev} \u2192 v${cur} within 7 days (potential takeover)`,
|
|
484
|
+
score: 4
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const created = Date.parse(time.created ?? "");
|
|
489
|
+
if (Number.isFinite(created) && Date.now() - created < 30 * 24 * 60 * 60 * 1e3) {
|
|
490
|
+
out.push({
|
|
491
|
+
rule: "young-package",
|
|
492
|
+
severity: "low",
|
|
493
|
+
message: "Package was first published less than 30 days ago",
|
|
494
|
+
score: 2
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
function typosquatFindings(name) {
|
|
500
|
+
const sug = findTyposquatCandidate(name, TOP_PACKAGES);
|
|
501
|
+
if (!sug) return [];
|
|
502
|
+
return [{
|
|
503
|
+
rule: "typosquat",
|
|
504
|
+
severity: "critical",
|
|
505
|
+
message: `Name closely resembles "${sug}" \u2014 likely typosquat`,
|
|
506
|
+
score: 10
|
|
507
|
+
}];
|
|
508
|
+
}
|
|
509
|
+
async function scan(options = {}) {
|
|
510
|
+
const cwd = options.cwd ?? process.cwd();
|
|
511
|
+
const depth = options.scanDepth ?? "all";
|
|
512
|
+
const ignore = new Set(options.ignore ?? []);
|
|
513
|
+
const baseline = loadBaseline();
|
|
514
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
const packages = [];
|
|
516
|
+
const installed = findInstalled(cwd, depth).filter((p) => !ignore.has(p.name));
|
|
517
|
+
for (const pkg of installed) {
|
|
518
|
+
const findings = [];
|
|
519
|
+
findings.push(...scriptFindings(pkg));
|
|
520
|
+
findings.push(...typosquatFindings(pkg.name));
|
|
521
|
+
if (options.network !== false) {
|
|
522
|
+
const packument = await fetchPackument(pkg.name, { ttlMs: options.cacheTTL ?? 60 * 60 * 1e3 });
|
|
523
|
+
findings.push(...maintainerFindings(pkg, packument, baseline));
|
|
524
|
+
findings.push(...versionAnomalyFindings(pkg, packument));
|
|
525
|
+
}
|
|
526
|
+
if (findings.length === 0) continue;
|
|
527
|
+
packages.push({
|
|
528
|
+
name: pkg.name,
|
|
529
|
+
version: pkg.version,
|
|
530
|
+
worstSeverity: maxSeverity(findings),
|
|
531
|
+
totalScore: findings.reduce((s, f) => s + f.score, 0),
|
|
532
|
+
findings
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
packages.sort((a, b) => b.totalScore - a.totalScore);
|
|
536
|
+
return {
|
|
537
|
+
packages,
|
|
538
|
+
scannedAt: startedAt,
|
|
539
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
540
|
+
totalPackages: installed.length
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async function audit(name) {
|
|
544
|
+
const packument = await fetchPackument(name);
|
|
545
|
+
if (!packument) return void 0;
|
|
546
|
+
const findings = [];
|
|
547
|
+
findings.push(...typosquatFindings(name));
|
|
548
|
+
return {
|
|
549
|
+
name,
|
|
550
|
+
version: packument["dist-tags"]?.latest ?? "0.0.0",
|
|
551
|
+
worstSeverity: maxSeverity(findings),
|
|
552
|
+
totalScore: findings.reduce((s, f) => s + f.score, 0),
|
|
553
|
+
findings
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/format.ts
|
|
558
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
559
|
+
var sevColor = {
|
|
560
|
+
info: import_picocolors.default.gray,
|
|
561
|
+
low: import_picocolors.default.blue,
|
|
562
|
+
medium: import_picocolors.default.yellow,
|
|
563
|
+
high: import_picocolors.default.magenta,
|
|
564
|
+
critical: import_picocolors.default.red
|
|
565
|
+
};
|
|
566
|
+
function formatReport(result) {
|
|
567
|
+
if (result.packages.length === 0) {
|
|
568
|
+
return import_picocolors.default.green(`\u2713 No risk findings across ${result.totalPackages} packages`);
|
|
569
|
+
}
|
|
570
|
+
const lines = [];
|
|
571
|
+
lines.push(import_picocolors.default.bold(`depguard report \u2014 ${result.packages.length} package(s) with findings (of ${result.totalPackages} scanned)`));
|
|
572
|
+
for (const p of result.packages) {
|
|
573
|
+
lines.push("");
|
|
574
|
+
lines.push(` ${sevColor[p.worstSeverity](p.worstSeverity.toUpperCase())} ${import_picocolors.default.bold(p.name)}@${p.version} score=${p.totalScore}`);
|
|
575
|
+
for (const f of p.findings) {
|
|
576
|
+
lines.push(` - ${sevColor[f.severity](f.severity)} ${f.rule}: ${f.message}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return lines.join("\n");
|
|
580
|
+
}
|
|
581
|
+
function formatJson(result) {
|
|
582
|
+
return JSON.stringify(result, null, 2);
|
|
583
|
+
}
|
|
584
|
+
function formatPackage(p) {
|
|
585
|
+
const lines = [];
|
|
586
|
+
lines.push(`${sevColor[p.worstSeverity](p.worstSeverity.toUpperCase())} ${import_picocolors.default.bold(p.name)}@${p.version} score=${p.totalScore}`);
|
|
587
|
+
for (const f of p.findings) {
|
|
588
|
+
lines.push(` - ${sevColor[f.severity](f.severity)} ${f.rule}: ${f.message}`);
|
|
589
|
+
}
|
|
590
|
+
return lines.join("\n");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/config.ts
|
|
594
|
+
var import_node_fs3 = require("fs");
|
|
595
|
+
var import_node_path3 = require("path");
|
|
596
|
+
function loadConfig(cwd) {
|
|
597
|
+
const file = (0, import_node_path3.join)(cwd, ".depguardrc.json");
|
|
598
|
+
if (!(0, import_node_fs3.existsSync)(file)) return {};
|
|
599
|
+
try {
|
|
600
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
|
|
601
|
+
} catch {
|
|
602
|
+
return {};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/baseline.ts
|
|
607
|
+
var import_node_fs4 = require("fs");
|
|
608
|
+
var import_node_path4 = require("path");
|
|
609
|
+
var import_node_os3 = require("os");
|
|
610
|
+
var BASELINE_DIR = (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".depguard");
|
|
611
|
+
var BASELINE_FILE = (0, import_node_path4.join)(BASELINE_DIR, "baseline.json");
|
|
612
|
+
function saveBaseline(b) {
|
|
613
|
+
if (!(0, import_node_fs4.existsSync)(BASELINE_DIR)) (0, import_node_fs4.mkdirSync)(BASELINE_DIR, { recursive: true });
|
|
614
|
+
(0, import_node_fs4.writeFileSync)(BASELINE_FILE, JSON.stringify(b, null, 2) + "\n", "utf8");
|
|
615
|
+
}
|
|
616
|
+
function collectMaintainersFromDisk(cwd) {
|
|
617
|
+
const baseline = {};
|
|
618
|
+
const nm = (0, import_node_path4.join)(cwd, "node_modules");
|
|
619
|
+
if (!(0, import_node_fs4.existsSync)(nm)) return baseline;
|
|
620
|
+
for (const entry of (0, import_node_fs4.readdirSync)(nm)) {
|
|
621
|
+
if (entry.startsWith(".")) continue;
|
|
622
|
+
const dir = (0, import_node_path4.join)(nm, entry);
|
|
623
|
+
try {
|
|
624
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(dir, "package.json"), "utf8"));
|
|
625
|
+
if (pkg.name && pkg.version) {
|
|
626
|
+
baseline[pkg.name] = {
|
|
627
|
+
maintainers: (pkg.maintainers ?? []).map((m) => m.name).sort(),
|
|
628
|
+
version: pkg.version
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return baseline;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/cli.ts
|
|
638
|
+
var program = new import_commander.Command();
|
|
639
|
+
program.name("depguard").description("Supply-chain security scanner for npm projects").option("--cwd <path>", "Working directory", process.cwd());
|
|
640
|
+
program.command("scan", { isDefault: true }).description("Scan the current project's installed packages").option("--fail-on <severity>", "Exit 1 when any finding \u2265 severity").option("--format <fmt>", "Output format: pretty | json", "pretty").option("--no-network", "Skip registry calls").option("--depth <depth>", "direct | all", "all").action(async (opts) => {
|
|
641
|
+
const cwd = program.opts().cwd;
|
|
642
|
+
const cfg = loadConfig(cwd);
|
|
643
|
+
const failOn = opts.failOn ?? cfg.failOn;
|
|
644
|
+
const result = await scan({
|
|
645
|
+
cwd,
|
|
646
|
+
ignore: cfg.ignore ?? [],
|
|
647
|
+
network: opts.network !== false,
|
|
648
|
+
scanDepth: opts.depth ?? cfg.scanDepth ?? "all",
|
|
649
|
+
...cfg.cacheTTL !== void 0 ? { cacheTTL: cfg.cacheTTL } : {}
|
|
650
|
+
});
|
|
651
|
+
process.stdout.write(opts.format === "json" ? formatJson(result) : formatReport(result));
|
|
652
|
+
process.stdout.write("\n");
|
|
653
|
+
if (failOn) {
|
|
654
|
+
const hit = result.packages.some((p) => severityAtLeast(p.worstSeverity, failOn));
|
|
655
|
+
if (hit) process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
program.command("baseline").description("Save current installed maintainers as the trusted baseline").action(() => {
|
|
659
|
+
const cwd = program.opts().cwd;
|
|
660
|
+
const baseline = collectMaintainersFromDisk(cwd);
|
|
661
|
+
saveBaseline(baseline);
|
|
662
|
+
process.stdout.write(`depguard: baseline saved (${Object.keys(baseline).length} packages)
|
|
663
|
+
`);
|
|
664
|
+
});
|
|
665
|
+
program.command("audit <package>").description("Audit a single package by name").action(async (name) => {
|
|
666
|
+
const result = await audit(name);
|
|
667
|
+
if (!result) {
|
|
668
|
+
process.stderr.write(`depguard: could not fetch metadata for "${name}"
|
|
669
|
+
`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
process.stdout.write(formatPackage(result) + "\n");
|
|
673
|
+
});
|
|
674
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
675
|
+
process.stderr.write(`depguard: ${err instanceof Error ? err.message : String(err)}
|
|
676
|
+
`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
});
|
|
679
|
+
//# sourceMappingURL=cli.cjs.map
|