@georgegiosue/pzx 0.1.5

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/index.js +1487 -0
  4. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,1487 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __require = import.meta.require;
4
+
5
+ // src/cli.ts
6
+ import { resolve } from "path";
7
+ function parseArgs(argv) {
8
+ const args = argv.slice(2);
9
+ let packageName;
10
+ let version;
11
+ let scanRoot = process.cwd();
12
+ let showHelp = false;
13
+ for (let i = 0;i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === "--help" || arg === "-h") {
16
+ showHelp = true;
17
+ continue;
18
+ }
19
+ if (arg === "--root" || arg === "-r") {
20
+ scanRoot = resolve(args[++i] || ".");
21
+ continue;
22
+ }
23
+ if (arg === "--home") {
24
+ scanRoot = process.env.HOME || process.env.USERPROFILE || "~";
25
+ continue;
26
+ }
27
+ if (!packageName) {
28
+ packageName = arg;
29
+ continue;
30
+ }
31
+ if (!version) {
32
+ version = arg;
33
+ continue;
34
+ }
35
+ }
36
+ return { packageName, version, scanRoot, showHelp };
37
+ }
38
+
39
+ // src/colors.ts
40
+ var c = {
41
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
42
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
43
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
44
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
45
+ magenta: (s) => `\x1B[35m${s}\x1B[0m`,
46
+ bold: (s) => `\x1B[1m${s}\x1B[0m`,
47
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
48
+ boldRed: (s) => `\x1B[1;31m${s}\x1B[0m`,
49
+ boldGreen: (s) => `\x1B[1;32m${s}\x1B[0m`,
50
+ boldYellow: (s) => `\x1B[1;33m${s}\x1B[0m`,
51
+ boldCyan: (s) => `\x1B[1;36m${s}\x1B[0m`,
52
+ boldMagenta: (s) => `\x1B[1;35m${s}\x1B[0m`
53
+ };
54
+
55
+ // src/semver.ts
56
+ function parseSemver(v) {
57
+ const cleaned = v.replace(/^[v^~>=<]+/, "");
58
+ const base = cleaned.split("-")[0].split("+")[0];
59
+ const parts = base.split(".");
60
+ return [
61
+ parseInt(parts[0] || "0", 10),
62
+ parseInt(parts[1] || "0", 10),
63
+ parseInt(parts[2] || "0", 10)
64
+ ];
65
+ }
66
+ function compareSemver(a, b) {
67
+ const pa = parseSemver(a);
68
+ const pb = parseSemver(b);
69
+ for (let i = 0;i < 3; i++) {
70
+ if (pa[i] < pb[i])
71
+ return -1;
72
+ if (pa[i] > pb[i])
73
+ return 1;
74
+ }
75
+ return 0;
76
+ }
77
+ function semverGte(a, b) {
78
+ return compareSemver(a, b) >= 0;
79
+ }
80
+
81
+ // src/osv.ts
82
+ var OSV_API = "https://api.osv.dev/v1/query";
83
+ async function queryOSV(packageName, version) {
84
+ const allVulns = [];
85
+ let pageToken;
86
+ do {
87
+ const body = {
88
+ package: { name: packageName, ecosystem: "npm" }
89
+ };
90
+ if (version)
91
+ body.version = version;
92
+ if (pageToken)
93
+ body.page_token = pageToken;
94
+ const res = await fetch(OSV_API, {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify(body)
98
+ });
99
+ if (!res.ok) {
100
+ throw new Error(`OSV API returned ${res.status}: ${res.statusText}`);
101
+ }
102
+ const data = await res.json();
103
+ if (data.vulns)
104
+ allVulns.push(...data.vulns);
105
+ pageToken = data.next_page_token;
106
+ } while (pageToken);
107
+ return allVulns;
108
+ }
109
+ function isVersionAffected(version, vuln) {
110
+ if (!vuln.affected)
111
+ return false;
112
+ for (const aff of vuln.affected) {
113
+ if (aff.package.ecosystem !== "npm")
114
+ continue;
115
+ if (aff.versions?.includes(version))
116
+ return true;
117
+ if (aff.ranges) {
118
+ for (const range of aff.ranges) {
119
+ if (range.type !== "ECOSYSTEM")
120
+ continue;
121
+ let vulnerable = false;
122
+ for (const event of range.events) {
123
+ if (event.introduced !== undefined) {
124
+ if (event.introduced === "0" || semverGte(version, event.introduced)) {
125
+ vulnerable = true;
126
+ }
127
+ }
128
+ if (event.fixed !== undefined) {
129
+ if (semverGte(version, event.fixed)) {
130
+ vulnerable = false;
131
+ }
132
+ }
133
+ if (event.last_affected !== undefined) {
134
+ if (compareSemver(version, event.last_affected) > 0) {
135
+ vulnerable = false;
136
+ }
137
+ }
138
+ }
139
+ if (vulnerable)
140
+ return true;
141
+ }
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ function getCVEs(vuln) {
147
+ return (vuln.aliases ?? []).filter((a) => a.startsWith("CVE-"));
148
+ }
149
+ function getAffectedRange(version, vuln) {
150
+ const ranges = [];
151
+ if (!vuln.affected)
152
+ return ranges;
153
+ for (const aff of vuln.affected) {
154
+ if (aff.package.ecosystem !== "npm")
155
+ continue;
156
+ if (!aff.ranges)
157
+ continue;
158
+ for (const range of aff.ranges) {
159
+ if (range.type !== "ECOSYSTEM")
160
+ continue;
161
+ let currentIntroduced = null;
162
+ for (const event of range.events) {
163
+ if (event.introduced !== undefined) {
164
+ if (event.introduced === "0" || semverGte(version, event.introduced)) {
165
+ currentIntroduced = event.introduced;
166
+ }
167
+ }
168
+ if (event.fixed !== undefined) {
169
+ if (currentIntroduced && !semverGte(version, event.fixed)) {
170
+ ranges.push({ introduced: currentIntroduced, fixed: event.fixed });
171
+ }
172
+ }
173
+ }
174
+ if (currentIntroduced && ranges.length === 0) {
175
+ ranges.push({ introduced: currentIntroduced, fixed: null });
176
+ }
177
+ }
178
+ }
179
+ return ranges;
180
+ }
181
+
182
+ // src/formatter.ts
183
+ function printUsage() {
184
+ console.log(`
185
+ ${c.boldCyan("pzx")} \u2014 SCA para npm: vulnerabilidades + cadena de suministro + sandbox
186
+
187
+ ${c.bold("Uso:")} pzx [paquete] [version] [opciones]
188
+
189
+ ${c.bold("Argumentos:")}
190
+ paquete Nombre del paquete npm (ej: lodash, @babel/core)
191
+ Sin argumento: escanea TODOS los paquetes instalados
192
+ version Version especifica a consultar (opcional)
193
+
194
+ ${c.bold("Opciones:")}
195
+ --root, -r Directorio raiz para escanear (default: directorio actual)
196
+ --home Escanear desde el directorio home
197
+ --help, -h Mostrar esta ayuda
198
+
199
+ ${c.bold("Ejemplos:")}
200
+ pzx Escaneo completo (todos los paquetes)
201
+ pzx lodash Buscar lodash
202
+ pzx @babel/core 7.20.0 Version especifica
203
+ pzx express --home Escanear desde home
204
+
205
+ ${c.bold("Compilar como binario:")}
206
+ bun build --compile ./index.ts --outfile pzx
207
+ `);
208
+ }
209
+
210
+ // src/scanner.ts
211
+ var {Glob } = globalThis.Bun;
212
+ import { dirname, join } from "path";
213
+ var DEP_FIELDS = [
214
+ "dependencies",
215
+ "devDependencies",
216
+ "peerDependencies",
217
+ "optionalDependencies"
218
+ ];
219
+ var BATCH_SIZE = 50;
220
+ var EMPTY_SCRIPTS = {
221
+ preinstall: null,
222
+ install: null,
223
+ postinstall: null
224
+ };
225
+ async function scanPackageJsonFiles(scanRoot) {
226
+ const files = [];
227
+ const glob = new Glob("**/package.json");
228
+ for await (const filePath of glob.scan({
229
+ cwd: scanRoot,
230
+ absolute: true,
231
+ followSymlinks: false,
232
+ onlyFiles: true
233
+ })) {
234
+ if (filePath.includes("/node_modules/") || filePath.includes("/.git/")) {
235
+ continue;
236
+ }
237
+ files.push(filePath);
238
+ }
239
+ return files;
240
+ }
241
+ async function filterProjectsWithPackage(packageJsonPaths, packageName) {
242
+ const matches = [];
243
+ for (let i = 0;i < packageJsonPaths.length; i += BATCH_SIZE) {
244
+ const batch = packageJsonPaths.slice(i, i + BATCH_SIZE);
245
+ const results = await Promise.all(batch.map(async (pkgPath) => {
246
+ try {
247
+ const pkg = await Bun.file(pkgPath).json();
248
+ for (const field of DEP_FIELDS) {
249
+ if (pkg[field]?.[packageName]) {
250
+ return {
251
+ packageJsonPath: pkgPath,
252
+ projectDir: dirname(pkgPath),
253
+ declaredVersion: pkg[field][packageName],
254
+ depType: field
255
+ };
256
+ }
257
+ }
258
+ } catch {}
259
+ return null;
260
+ }));
261
+ for (const r of results) {
262
+ if (r)
263
+ matches.push(r);
264
+ }
265
+ }
266
+ return matches;
267
+ }
268
+ function extractLifecycleScripts(scripts) {
269
+ if (!scripts)
270
+ return { ...EMPTY_SCRIPTS };
271
+ return {
272
+ preinstall: scripts.preinstall ?? null,
273
+ install: scripts.install ?? null,
274
+ postinstall: scripts.postinstall ?? null
275
+ };
276
+ }
277
+ function resolveEntryPoint(packageDir, pkg) {
278
+ if (typeof pkg.exports === "string") {
279
+ return join(packageDir, pkg.exports);
280
+ }
281
+ if (pkg.exports && typeof pkg.exports === "object") {
282
+ const dot = pkg.exports["."];
283
+ if (typeof dot === "string")
284
+ return join(packageDir, dot);
285
+ if (dot && typeof dot === "object") {
286
+ const dotObj = dot;
287
+ const resolved = dotObj.import ?? dotObj.require ?? dotObj.default;
288
+ if (typeof resolved === "string")
289
+ return join(packageDir, resolved);
290
+ }
291
+ }
292
+ if (pkg.module)
293
+ return join(packageDir, pkg.module);
294
+ if (pkg.main)
295
+ return join(packageDir, pkg.main);
296
+ return join(packageDir, "index.js");
297
+ }
298
+ async function resolveInstalledPackage(projectDir, packageName) {
299
+ let dir = projectDir;
300
+ while (true) {
301
+ const candidate = join(dir, "node_modules", packageName, "package.json");
302
+ try {
303
+ const file = Bun.file(candidate);
304
+ if (await file.exists()) {
305
+ const pkg = await file.json();
306
+ const packageDir = dirname(candidate);
307
+ const entryPoint = resolveEntryPoint(packageDir, pkg);
308
+ return {
309
+ version: pkg.version ?? null,
310
+ lifecycleScripts: extractLifecycleScripts(pkg.scripts),
311
+ entryPoint,
312
+ packageDir,
313
+ packageDeps: pkg.dependencies ?? {}
314
+ };
315
+ }
316
+ } catch {}
317
+ const parent = dirname(dir);
318
+ if (parent === dir)
319
+ break;
320
+ dir = parent;
321
+ }
322
+ return {
323
+ version: null,
324
+ lifecycleScripts: { ...EMPTY_SCRIPTS },
325
+ entryPoint: null,
326
+ packageDir: null,
327
+ packageDeps: {}
328
+ };
329
+ }
330
+
331
+ // src/registry.ts
332
+ var NPM_REGISTRY = "https://registry.npmjs.org";
333
+ async function fetchRegistryMetadata(packageName, installedVersion) {
334
+ const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
335
+ const res = await fetch(url, {
336
+ headers: { Accept: "application/json" }
337
+ });
338
+ if (!res.ok) {
339
+ throw new Error(`NPM Registry returned ${res.status}: ${res.statusText}`);
340
+ }
341
+ const data = await res.json();
342
+ const lastModified = new Date(data.time.modified);
343
+ let versionPublishedAt = null;
344
+ if (installedVersion && data.time[installedVersion]) {
345
+ versionPublishedAt = new Date(data.time[installedVersion]);
346
+ }
347
+ let deprecated = null;
348
+ if (installedVersion && data.versions?.[installedVersion]?.deprecated) {
349
+ deprecated = data.versions[installedVersion].deprecated;
350
+ } else if (data.deprecated) {
351
+ deprecated = data.deprecated;
352
+ }
353
+ const maintainerCount = data.maintainers?.length ?? 0;
354
+ const repoUrl = data.repository?.url;
355
+ const hasRepository = typeof repoUrl === "string" && repoUrl.length > 0;
356
+ return {
357
+ lastModified,
358
+ versionPublishedAt,
359
+ deprecated,
360
+ maintainerCount,
361
+ hasRepository
362
+ };
363
+ }
364
+
365
+ // src/static-analysis.ts
366
+ var IOC_SIGNATURES = [
367
+ { name: "eval()", pattern: /\beval\s*\(/ },
368
+ { name: "new Function()", pattern: /new\s+Function\s*\(/ },
369
+ { name: "Buffer.from base64", pattern: /Buffer\.from\s*\([^)]*['"]base64['"]\s*\)/ },
370
+ { name: "Hex string largo", pattern: /\\x[0-9a-fA-F]{2}(?:\\x[0-9a-fA-F]{2}){15,}/ },
371
+ { name: "child_process", pattern: /require\s*\(\s*['"]child_process['"]\s*\)/ },
372
+ { name: "child_process import", pattern: /from\s+['"]child_process['"]/ },
373
+ { name: "exec()", pattern: /\bexec\s*\(\s*['"`]/ },
374
+ { name: "execSync()", pattern: /\bexecSync\s*\(/ },
375
+ { name: "os.networkInterfaces", pattern: /os\.networkInterfaces\s*\(/ },
376
+ { name: "IP cruda en URL", pattern: /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ },
377
+ { name: "process.env exfiltration", pattern: /process\.env\b.*(?:fetch|http|request|axios|got)\b/ },
378
+ { name: "DNS exfiltration", pattern: /dns\.resolve|dns\.lookup.*process\.env/ }
379
+ ];
380
+ var MAX_FILE_SIZE = 2 * 1024 * 1024;
381
+ function scanSource(source, fileName) {
382
+ const matches = [];
383
+ const lines = source.split(`
384
+ `);
385
+ for (let i = 0;i < lines.length; i++) {
386
+ const line = lines[i];
387
+ for (const sig of IOC_SIGNATURES) {
388
+ if (sig.pattern.test(line)) {
389
+ const snippet = line.trim().length > 120 ? line.trim().substring(0, 120) + "..." : line.trim();
390
+ matches.push({
391
+ pattern: sig.name,
392
+ line: i + 1,
393
+ snippet
394
+ });
395
+ }
396
+ }
397
+ }
398
+ return matches;
399
+ }
400
+ async function analyzeEntryPoint(entryPointPath) {
401
+ try {
402
+ const file = Bun.file(entryPointPath);
403
+ if (!await file.exists())
404
+ return [];
405
+ const size = file.size;
406
+ if (size > MAX_FILE_SIZE)
407
+ return [];
408
+ const source = await file.text();
409
+ return scanSource(source, entryPointPath);
410
+ } catch {
411
+ return [];
412
+ }
413
+ }
414
+ function matchesToRisks(matches, filePath) {
415
+ if (matches.length === 0)
416
+ return [];
417
+ const uniquePatterns = [
418
+ ...new Set(matches.map((m) => m.pattern))
419
+ ];
420
+ const details = matches.slice(0, 5).map((m) => `L${m.line}: [${m.pattern}] ${m.snippet}`);
421
+ const suffix = matches.length > 5 ? `
422
+ ... y ${matches.length - 5} mas` : "";
423
+ return [
424
+ {
425
+ type: "obfuscation",
426
+ severity: "critical",
427
+ message: `Patrones sospechosos en ${filePath.split("/").pop()}: ${uniquePatterns.join(", ")}`,
428
+ detail: details.join(`
429
+ `) + suffix
430
+ }
431
+ ];
432
+ }
433
+
434
+ // src/dep-audit.ts
435
+ var RANDOM_NAME_PATTERN = /^[a-z]{8,}$/;
436
+ var URL_VERSION_PATTERN = /^(?:https?:\/\/|git\+https?:\/\/|git:\/\/|git\+ssh:\/\/)/;
437
+ var IP_IN_URL_PATTERN = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
438
+ var FILE_PROTOCOL_PATTERN = /^file:/;
439
+ var ENTROPY_THRESHOLD = 3.5;
440
+ var MIN_NAME_LENGTH_FOR_ENTROPY = 6;
441
+ function hasVowel(s) {
442
+ return /[aeiou]/i.test(s);
443
+ }
444
+ function shannonEntropy(s) {
445
+ const freq = new Map;
446
+ for (const ch of s) {
447
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
448
+ }
449
+ let entropy = 0;
450
+ for (const count of freq.values()) {
451
+ const p = count / s.length;
452
+ entropy -= p * Math.log2(p);
453
+ }
454
+ return entropy;
455
+ }
456
+ function isRandomName(name) {
457
+ const baseName = name.startsWith("@") ? name.split("/")[1] ?? name : name;
458
+ if (baseName.length < MIN_NAME_LENGTH_FOR_ENTROPY)
459
+ return false;
460
+ if (RANDOM_NAME_PATTERN.test(baseName) && !hasVowel(baseName)) {
461
+ return true;
462
+ }
463
+ if (baseName.length >= MIN_NAME_LENGTH_FOR_ENTROPY && shannonEntropy(baseName) > ENTROPY_THRESHOLD && !hasVowel(baseName)) {
464
+ return true;
465
+ }
466
+ return false;
467
+ }
468
+ function isSuspiciousVersion(version) {
469
+ if (URL_VERSION_PATTERN.test(version))
470
+ return true;
471
+ if (IP_IN_URL_PATTERN.test(version))
472
+ return true;
473
+ if (FILE_PROTOCOL_PATTERN.test(version))
474
+ return true;
475
+ return false;
476
+ }
477
+ function auditDependencies(deps) {
478
+ const suspicious = [];
479
+ for (const [name, version] of Object.entries(deps)) {
480
+ if (isRandomName(name)) {
481
+ suspicious.push({
482
+ name,
483
+ version,
484
+ reason: "Nombre con patron aleatorio"
485
+ });
486
+ }
487
+ if (isSuspiciousVersion(version)) {
488
+ suspicious.push({
489
+ name,
490
+ version,
491
+ reason: "Origen no estandar (URL/IP/file)"
492
+ });
493
+ }
494
+ }
495
+ return suspicious;
496
+ }
497
+ function auditToRisks(suspicious) {
498
+ if (suspicious.length === 0)
499
+ return [];
500
+ const details = suspicious.map((d) => `${d.name}@${d.version} (${d.reason})`);
501
+ return [
502
+ {
503
+ type: "suspicious-deps",
504
+ severity: "high",
505
+ message: `${suspicious.length} sub-dependencia(s) sospechosa(s)`,
506
+ detail: details.join(`
507
+ `)
508
+ }
509
+ ];
510
+ }
511
+
512
+ // src/ast-analysis.ts
513
+ var SENSITIVE_MODULES = new Set([
514
+ "child_process",
515
+ "node:child_process",
516
+ "crypto",
517
+ "node:crypto",
518
+ "os",
519
+ "node:os",
520
+ "vm",
521
+ "node:vm",
522
+ "net",
523
+ "node:net",
524
+ "dgram",
525
+ "node:dgram",
526
+ "dns",
527
+ "node:dns",
528
+ "tls",
529
+ "node:tls",
530
+ "cluster",
531
+ "node:cluster",
532
+ "worker_threads",
533
+ "node:worker_threads"
534
+ ]);
535
+ var BASE64_PATTERN = /["'`]([A-Za-z0-9+/=]{60,})["'`]/g;
536
+ var ENTROPY_THRESHOLD2 = 4.5;
537
+ var MAX_FILE_SIZE2 = 2 * 1024 * 1024;
538
+ function shannonEntropy2(s) {
539
+ const freq = new Map;
540
+ for (const ch of s) {
541
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
542
+ }
543
+ let entropy = 0;
544
+ for (const count of freq.values()) {
545
+ const p = count / s.length;
546
+ entropy -= p * Math.log2(p);
547
+ }
548
+ return entropy;
549
+ }
550
+ function detectHighEntropyStrings(source) {
551
+ const results = [];
552
+ let match;
553
+ while ((match = BASE64_PATTERN.exec(source)) !== null) {
554
+ const candidate = match[1];
555
+ if (shannonEntropy2(candidate) >= ENTROPY_THRESHOLD2) {
556
+ const preview = candidate.length > 40 ? candidate.substring(0, 40) + "..." : candidate;
557
+ results.push(preview);
558
+ }
559
+ }
560
+ return results;
561
+ }
562
+ async function analyzeAST(entryPointPath) {
563
+ const empty = {
564
+ sensitiveModules: [],
565
+ highEntropyStrings: []
566
+ };
567
+ try {
568
+ const file = Bun.file(entryPointPath);
569
+ if (!await file.exists())
570
+ return empty;
571
+ if (file.size > MAX_FILE_SIZE2)
572
+ return empty;
573
+ const source = await file.text();
574
+ const loader = entryPointPath.endsWith(".ts") ? "ts" : entryPointPath.endsWith(".tsx") ? "tsx" : entryPointPath.endsWith(".jsx") ? "jsx" : "js";
575
+ const transpiler = new Bun.Transpiler({ loader });
576
+ const imports = transpiler.scanImports(source);
577
+ const sensitiveModules = [];
578
+ for (const imp of imports) {
579
+ if (SENSITIVE_MODULES.has(imp.path)) {
580
+ const clean = imp.path.replace("node:", "");
581
+ if (!sensitiveModules.includes(clean)) {
582
+ sensitiveModules.push(clean);
583
+ }
584
+ }
585
+ }
586
+ const highEntropyStrings = detectHighEntropyStrings(source);
587
+ return { sensitiveModules, highEntropyStrings };
588
+ } catch {
589
+ return empty;
590
+ }
591
+ }
592
+ function astResultToRisks(result, filePath) {
593
+ const risks = [];
594
+ const fileName = filePath.split("/").pop() ?? filePath;
595
+ if (result.sensitiveModules.length > 0) {
596
+ risks.push({
597
+ type: "ast-sensitive-import",
598
+ severity: "high",
599
+ message: `Importa modulos sensibles del sistema: ${result.sensitiveModules.join(", ")}`,
600
+ detail: `Detectado en ${fileName} via AST`
601
+ });
602
+ }
603
+ if (result.highEntropyStrings.length > 0) {
604
+ risks.push({
605
+ type: "high-entropy-string",
606
+ severity: "critical",
607
+ message: `${result.highEntropyStrings.length} string(s) Base64 de alta entropia (posible payload ofuscado)`,
608
+ detail: result.highEntropyStrings.slice(0, 3).map((s) => `"${s}"`).join(`
609
+ `)
610
+ });
611
+ }
612
+ return risks;
613
+ }
614
+
615
+ // src/sandbox.ts
616
+ import { join as join2 } from "path";
617
+ import { mkdirSync } from "fs";
618
+ var SANDBOX_TIMEOUT_MS = 1500;
619
+ var VIOLATION_PATTERN = /SANDBOX_VIOLATION:(\w+)(?::(.*))?/g;
620
+ var BUN_PATH = process.execPath;
621
+ function generateMockEnv() {
622
+ return `
623
+ // \u2500\u2500\u2500 pzx sandbox: intercepta APIs peligrosas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
624
+ const _origFetch = globalThis.fetch;
625
+ globalThis.fetch = (...args) => {
626
+ const url = typeof args[0] === "string" ? args[0] : args[0]?.url ?? "unknown";
627
+ console.error("SANDBOX_VIOLATION:NETWORK:" + url);
628
+ return Promise.resolve(new Response("", { status: 200 }));
629
+ };
630
+
631
+ try {
632
+ const _fs = require("fs");
633
+ const _origRead = _fs.readFileSync;
634
+ _fs.readFileSync = (...args) => {
635
+ console.error("SANDBOX_VIOLATION:FS_READ:" + String(args[0]));
636
+ return Buffer.from("");
637
+ };
638
+ const _origWrite = _fs.writeFileSync;
639
+ _fs.writeFileSync = (...args) => {
640
+ console.error("SANDBOX_VIOLATION:FS_READ:" + String(args[0]));
641
+ };
642
+ } catch {}
643
+
644
+ try {
645
+ const _cp = require("child_process");
646
+ for (const fn of ["exec", "execSync", "spawn", "spawnSync", "execFile", "execFileSync"]) {
647
+ _cp[fn] = (...args) => {
648
+ console.error("SANDBOX_VIOLATION:EXEC:" + String(args[0]));
649
+ return { stdout: "", stderr: "", pid: 0, on: () => {}, kill: () => {} };
650
+ };
651
+ }
652
+ } catch {}
653
+
654
+ try {
655
+ const _http = require("http");
656
+ const _origRequest = _http.request;
657
+ _http.request = (...args) => {
658
+ const url = typeof args[0] === "string" ? args[0] : args[0]?.hostname ?? "unknown";
659
+ console.error("SANDBOX_VIOLATION:NETWORK:" + url);
660
+ return { on: () => {}, end: () => {}, write: () => {} };
661
+ };
662
+ } catch {}
663
+
664
+ try {
665
+ const _https = require("https");
666
+ const _origRequest = _https.request;
667
+ _https.request = (...args) => {
668
+ const url = typeof args[0] === "string" ? args[0] : args[0]?.hostname ?? "unknown";
669
+ console.error("SANDBOX_VIOLATION:NETWORK:" + url);
670
+ return { on: () => {}, end: () => {}, write: () => {} };
671
+ };
672
+ } catch {}
673
+ `.trim();
674
+ }
675
+ function generateDetonate(entryPoint) {
676
+ return `try { require(${JSON.stringify(entryPoint)}); } catch {}`;
677
+ }
678
+ async function writeSandboxFiles(entryPoint) {
679
+ const id = Math.random().toString(36).substring(2, 10);
680
+ const dir = join2("/tmp", `pzx-sandbox-${id}`);
681
+ mkdirSync(dir, { recursive: true });
682
+ const mockEnvPath = join2(dir, "mock-env.js");
683
+ const detonatePath = join2(dir, "detonate.js");
684
+ await Promise.all([
685
+ Bun.write(mockEnvPath, generateMockEnv()),
686
+ Bun.write(detonatePath, generateDetonate(entryPoint))
687
+ ]);
688
+ return { mockEnvPath, detonatePath, dir };
689
+ }
690
+ function parseSandboxOutput(output) {
691
+ const violations = [];
692
+ let match;
693
+ while ((match = VIOLATION_PATTERN.exec(output)) !== null) {
694
+ const rawType = match[1];
695
+ const detail = match[2] ?? "";
696
+ const type = rawType === "NETWORK" ? "NETWORK" : rawType === "EXEC" ? "EXEC" : "FS_READ";
697
+ violations.push({ type, detail });
698
+ }
699
+ return violations;
700
+ }
701
+ async function detonate(entryPoint) {
702
+ const { mockEnvPath, detonatePath, dir } = await writeSandboxFiles(entryPoint);
703
+ try {
704
+ const proc = Bun.spawn([BUN_PATH, "-r", mockEnvPath, detonatePath], {
705
+ env: { HOME: "/tmp" },
706
+ stdout: "pipe",
707
+ stderr: "pipe",
708
+ cwd: dir
709
+ });
710
+ const timeoutPromise = new Promise((resolve2) => {
711
+ setTimeout(() => resolve2("timeout"), SANDBOX_TIMEOUT_MS);
712
+ });
713
+ const exitPromise = proc.exited.then(() => "done");
714
+ const race = await Promise.race([
715
+ exitPromise,
716
+ timeoutPromise
717
+ ]);
718
+ const timedOut = race === "timeout";
719
+ if (timedOut) {
720
+ proc.kill();
721
+ }
722
+ const [stdoutText, stderrText] = await Promise.all([
723
+ new Response(proc.stdout).text(),
724
+ new Response(proc.stderr).text()
725
+ ]);
726
+ const combined = stdoutText + `
727
+ ` + stderrText;
728
+ const violations = parseSandboxOutput(combined);
729
+ return {
730
+ violations,
731
+ timedOut,
732
+ exitCode: timedOut ? null : proc.exitCode,
733
+ stderr: stderrText.substring(0, 2000)
734
+ };
735
+ } finally {
736
+ try {
737
+ const { rmSync } = await import("fs");
738
+ rmSync(dir, { recursive: true, force: true });
739
+ } catch {}
740
+ }
741
+ }
742
+ function sandboxResultToRisks(result) {
743
+ const risks = [];
744
+ const networkViolations = result.violations.filter((v) => v.type === "NETWORK");
745
+ if (networkViolations.length > 0) {
746
+ const targets = networkViolations.slice(0, 3).map((v) => v.detail).join(", ");
747
+ risks.push({
748
+ type: "sandbox-network",
749
+ severity: "critical",
750
+ message: "El paquete intento acceder a la red durante la inicializacion",
751
+ detail: `Destinos: ${targets}`
752
+ });
753
+ }
754
+ const execViolations = result.violations.filter((v) => v.type === "EXEC");
755
+ if (execViolations.length > 0) {
756
+ const cmds = execViolations.slice(0, 3).map((v) => v.detail).join(", ");
757
+ risks.push({
758
+ type: "sandbox-exec",
759
+ severity: "critical",
760
+ message: "Intento ejecutar comandos en la terminal",
761
+ detail: `Comandos: ${cmds}`
762
+ });
763
+ }
764
+ const fsViolations = result.violations.filter((v) => v.type === "FS_READ");
765
+ if (fsViolations.length > 0) {
766
+ const paths = fsViolations.slice(0, 3).map((v) => v.detail).join(", ");
767
+ risks.push({
768
+ type: "sandbox-fs",
769
+ severity: "critical",
770
+ message: "Intento leer/escribir el sistema de archivos",
771
+ detail: `Rutas: ${paths}`
772
+ });
773
+ }
774
+ if (result.timedOut) {
775
+ risks.push({
776
+ type: "sandbox-timeout",
777
+ severity: "high",
778
+ message: "Comportamiento anomalo: bloqueo del hilo principal o persistencia",
779
+ detail: "El proceso fue terminado tras 1500ms sin respuesta"
780
+ });
781
+ }
782
+ return risks;
783
+ }
784
+
785
+ // src/sca.ts
786
+ var DANGEROUS_SCRIPTS = [
787
+ "preinstall",
788
+ "install",
789
+ "postinstall"
790
+ ];
791
+ var ABANDONMENT_THRESHOLD_MS = 3 * 365 * 24 * 60 * 60 * 1000;
792
+ var QUARANTINE_THRESHOLD_MS = 48 * 60 * 60 * 1000;
793
+ function detectLifecycleScripts(scripts) {
794
+ const risks = [];
795
+ for (const name of DANGEROUS_SCRIPTS) {
796
+ const value = scripts[name];
797
+ if (value !== null) {
798
+ risks.push({
799
+ type: "lifecycle-scripts",
800
+ severity: "high",
801
+ message: `Script de ejecucion automatica: ${name}`,
802
+ detail: value
803
+ });
804
+ }
805
+ }
806
+ return risks;
807
+ }
808
+ function detectAbandonment(meta, now = new Date) {
809
+ const elapsed = now.getTime() - meta.lastModified.getTime();
810
+ if (elapsed > ABANDONMENT_THRESHOLD_MS) {
811
+ const years = Math.floor(elapsed / (365 * 24 * 60 * 60 * 1000));
812
+ return {
813
+ type: "abandoned",
814
+ severity: "medium",
815
+ message: `Sin actualizacion en ${years} a\xF1o(s) (riesgo de takeover)`,
816
+ detail: `Ultima actualizacion: ${meta.lastModified.toISOString().split("T")[0]}`
817
+ };
818
+ }
819
+ return null;
820
+ }
821
+ function detectDeprecation(meta) {
822
+ if (meta.deprecated) {
823
+ return {
824
+ type: "deprecated",
825
+ severity: "medium",
826
+ message: "Paquete marcado como DEPRECADO",
827
+ detail: meta.deprecated
828
+ };
829
+ }
830
+ return null;
831
+ }
832
+ function detectQuarantine(meta, now = new Date) {
833
+ if (!meta.versionPublishedAt)
834
+ return null;
835
+ const elapsed = now.getTime() - meta.versionPublishedAt.getTime();
836
+ if (elapsed < QUARANTINE_THRESHOLD_MS) {
837
+ const hours = Math.floor(elapsed / (60 * 60 * 1000));
838
+ return {
839
+ type: "quarantine",
840
+ severity: "high",
841
+ message: `Publicado hace ${hours}h (cuarentena sugerida)`,
842
+ detail: `Fecha de publicacion: ${meta.versionPublishedAt.toISOString()}`
843
+ };
844
+ }
845
+ return null;
846
+ }
847
+ function detectTyposquatting(meta) {
848
+ if (meta.maintainerCount <= 1 && !meta.hasRepository) {
849
+ return {
850
+ type: "typosquatting",
851
+ severity: "high",
852
+ message: "Posible typosquatting: un solo mantenedor y sin repositorio",
853
+ detail: `Mantenedores: ${meta.maintainerCount}, Repositorio: no`
854
+ };
855
+ }
856
+ return null;
857
+ }
858
+ function analyzeSupplyChainRisks(scripts, meta, now = new Date, staticMatches = [], entryPoint = null, packageDeps = {}, astAnalysis, sandboxResult) {
859
+ const risks = [];
860
+ risks.push(...detectLifecycleScripts(scripts));
861
+ if (meta) {
862
+ const abandonment = detectAbandonment(meta, now);
863
+ if (abandonment)
864
+ risks.push(abandonment);
865
+ const deprecation = detectDeprecation(meta);
866
+ if (deprecation)
867
+ risks.push(deprecation);
868
+ const quarantine = detectQuarantine(meta, now);
869
+ if (quarantine)
870
+ risks.push(quarantine);
871
+ const typosquatting = detectTyposquatting(meta);
872
+ if (typosquatting)
873
+ risks.push(typosquatting);
874
+ }
875
+ if (staticMatches.length > 0 && entryPoint) {
876
+ risks.push(...matchesToRisks(staticMatches, entryPoint));
877
+ }
878
+ const suspicious = auditDependencies(packageDeps);
879
+ risks.push(...auditToRisks(suspicious));
880
+ if (astAnalysis && entryPoint) {
881
+ risks.push(...astResultToRisks(astAnalysis, entryPoint));
882
+ }
883
+ if (sandboxResult) {
884
+ risks.push(...sandboxResultToRisks(sandboxResult));
885
+ }
886
+ return risks;
887
+ }
888
+
889
+ // src/reporter.ts
890
+ function printBanner(pkg, version, scanRoot) {
891
+ const versionLabel = version ? ` v${version}` : "";
892
+ console.log(`
893
+ ${c.boldCyan("pzx")} \u2014 buscando ${c.bold(`"${pkg}"`)}${versionLabel} en ${c.dim(scanRoot)}
894
+ `);
895
+ }
896
+ function printVulnSummary(pkg, version, vulns) {
897
+ if (vulns.length > 0) {
898
+ console.log(c.boldRed(`\u26A0 ${vulns.length} vulnerabilidad(es) conocida(s) para ${pkg}:`));
899
+ const preview = vulns.slice(0, 5);
900
+ for (const v of preview) {
901
+ const cves = getCVEs(v);
902
+ const cveStr = cves.length > 0 ? c.magenta(cves.join(", ")) : c.dim("sin CVE");
903
+ console.log(` ${c.red(v.id)} ${cveStr} ${c.dim(v.summary ?? "")}`);
904
+ }
905
+ if (vulns.length > 5) {
906
+ console.log(c.dim(` ... y ${vulns.length - 5} m\xE1s`));
907
+ }
908
+ console.log();
909
+ } else {
910
+ const versionLabel = version ? ` v${version}` : "";
911
+ console.log(c.boldGreen(`\u2713 Sin vulnerabilidades conocidas para ${pkg}${versionLabel}
912
+ `));
913
+ }
914
+ }
915
+ function printRegistrySummary(meta) {
916
+ if (!meta) {
917
+ console.log(c.yellow(`\u26A0 No se pudo consultar el registro NPM
918
+ `));
919
+ return;
920
+ }
921
+ const modified = meta.lastModified.toISOString().split("T")[0];
922
+ console.log(c.dim(`Ultima actualizacion en NPM: ${modified}
923
+ `));
924
+ }
925
+ function printNoProjectsFound(pkg, scanRoot) {
926
+ console.log(c.yellow(`No se encontraron proyectos que referencien "${pkg}" en ${scanRoot}`));
927
+ }
928
+ function printProjectCount(pkg, count) {
929
+ console.log(c.dim(`Encontrados ${count} proyecto(s) con "${pkg}"
930
+ `));
931
+ }
932
+ function formatOsvStatus(installedVersion, vulns) {
933
+ const affectingVulns = vulns.filter((v) => isVersionAffected(installedVersion, v));
934
+ if (affectingVulns.length === 0) {
935
+ return c.boldGreen("Seguro");
936
+ }
937
+ const labels = affectingVulns.map((v) => {
938
+ const cves = getCVEs(v);
939
+ const cveStr = cves.length > 0 ? cves.join(", ") : v.id;
940
+ return c.boldRed(`VULNERABLE ${cveStr}`);
941
+ });
942
+ return labels.join(" | ");
943
+ }
944
+ function formatNpmStatus(meta) {
945
+ if (!meta)
946
+ return c.dim("Sin datos de registro");
947
+ if (meta.deprecated) {
948
+ return c.boldYellow(`DEPRECADO: ${meta.deprecated}`);
949
+ }
950
+ const now = Date.now();
951
+ const elapsed = now - meta.lastModified.getTime();
952
+ const threeYears = 3 * 365 * 24 * 60 * 60 * 1000;
953
+ if (elapsed > threeYears) {
954
+ const years = Math.floor(elapsed / (365 * 24 * 60 * 60 * 1000));
955
+ return c.yellow(`Inactivo (${years} a\xF1os sin actualizar)`);
956
+ }
957
+ return c.green("Activo");
958
+ }
959
+ function formatRisks(risks) {
960
+ if (risks.length === 0)
961
+ return c.green("Ninguno");
962
+ const parts = risks.map((r) => {
963
+ switch (r.type) {
964
+ case "lifecycle-scripts":
965
+ return c.magenta(`[!] ${r.message}`);
966
+ case "abandoned":
967
+ return c.yellow(`[~] ${r.message}`);
968
+ case "deprecated":
969
+ return c.yellow(`[~] ${r.message}`);
970
+ case "quarantine":
971
+ return c.boldCyan(`[!!] ${r.message}`);
972
+ case "obfuscation":
973
+ return c.boldRed(`[!!!] ${r.message}`);
974
+ case "typosquatting":
975
+ return c.boldYellow(`[!!] ${r.message}`);
976
+ case "suspicious-deps":
977
+ return c.boldMagenta(`[!!] ${r.message}`);
978
+ case "ast-sensitive-import":
979
+ return c.boldYellow(`[!] ${r.message}`);
980
+ case "high-entropy-string":
981
+ return c.boldRed(`[!!] ${r.message}`);
982
+ case "sandbox-network":
983
+ return c.boldRed(`[!!!] ${r.message}`);
984
+ case "sandbox-exec":
985
+ return c.boldRed(`[!!!] ${r.message}`);
986
+ case "sandbox-fs":
987
+ return c.boldRed(`[!!!] ${r.message}`);
988
+ case "sandbox-timeout":
989
+ return c.boldYellow(`[!] ${r.message}`);
990
+ }
991
+ });
992
+ return parts.join(`
993
+ `);
994
+ }
995
+ function formatVulnDetails(installedVersion, vulns) {
996
+ const lines = [];
997
+ const affectingVulns = vulns.filter((v) => isVersionAffected(installedVersion, v));
998
+ for (const v of affectingVulns) {
999
+ const cves = getCVEs(v);
1000
+ const cveStr = cves.length > 0 ? ` (${c.magenta(cves.join(", "))})` : "";
1001
+ const ranges = getAffectedRange(installedVersion, v);
1002
+ let rangeStr = "";
1003
+ if (ranges.length > 0) {
1004
+ const r = ranges[0];
1005
+ const intro = r.introduced === "0" ? "todas" : r.introduced;
1006
+ const fix = r.fixed ?? "sin fix";
1007
+ rangeStr = c.dim(` [${intro} \u2192 ${fix}]`);
1008
+ }
1009
+ lines.push(` ${c.red(v.id)}${cveStr}${rangeStr}`);
1010
+ if (v.summary) {
1011
+ lines.push(` ${c.dim(v.summary)}`);
1012
+ }
1013
+ }
1014
+ return lines;
1015
+ }
1016
+ function formatRiskDetails(risks) {
1017
+ const lines = [];
1018
+ for (const r of risks) {
1019
+ if (r.type === "lifecycle-scripts") {
1020
+ lines.push(` ${c.dim(`\u2192 ${r.detail}`)}`);
1021
+ }
1022
+ if (r.type === "quarantine") {
1023
+ lines.push(` ${c.dim(r.detail)}`);
1024
+ }
1025
+ if (r.type === "obfuscation") {
1026
+ for (const line of r.detail.split(`
1027
+ `)) {
1028
+ lines.push(` ${c.red(line)}`);
1029
+ }
1030
+ }
1031
+ if (r.type === "typosquatting") {
1032
+ lines.push(` ${c.yellow(r.detail)}`);
1033
+ }
1034
+ if (r.type === "suspicious-deps") {
1035
+ for (const line of r.detail.split(`
1036
+ `)) {
1037
+ lines.push(` ${c.magenta(line)}`);
1038
+ }
1039
+ }
1040
+ if (r.type === "ast-sensitive-import" || r.type === "high-entropy-string") {
1041
+ lines.push(` ${c.yellow(r.detail)}`);
1042
+ }
1043
+ if (r.type === "sandbox-network" || r.type === "sandbox-exec" || r.type === "sandbox-fs") {
1044
+ lines.push(` ${c.red(`\u2192 ${r.detail}`)}`);
1045
+ }
1046
+ if (r.type === "sandbox-timeout") {
1047
+ lines.push(` ${c.yellow(r.detail)}`);
1048
+ }
1049
+ }
1050
+ return lines;
1051
+ }
1052
+ function printProjectReport(report, vulns) {
1053
+ console.log(`${c.boldCyan(`[Proyecto: ${report.projectDir}]`)}`);
1054
+ if (!report.installedVersion) {
1055
+ console.log(` \u2514\u2500\u2500 Instalado: ${c.boldYellow("NO INSTALADO")}`);
1056
+ console.log(` \u2514\u2500\u2500 Declarado: ${c.yellow(report.declaredVersion)} (${report.depType})`);
1057
+ console.log(` \u2514\u2500\u2500 ${c.yellow("Ejecuta bun install o npm install")}`);
1058
+ console.log();
1059
+ return;
1060
+ }
1061
+ const installed = report.installedVersion;
1062
+ console.log(` \u2514\u2500\u2500 Instalado: ${c.bold(installed)} ${c.dim(`(declarado: ${report.declaredVersion})`)}`);
1063
+ const osvStatus = formatOsvStatus(installed, vulns);
1064
+ console.log(` \u2514\u2500\u2500 Estado OSV: ${osvStatus}`);
1065
+ const vulnDetails = formatVulnDetails(installed, vulns);
1066
+ for (const line of vulnDetails) {
1067
+ console.log(line);
1068
+ }
1069
+ const npmStatus = formatNpmStatus(report.registryMeta);
1070
+ console.log(` \u2514\u2500\u2500 NPM Status: ${npmStatus}`);
1071
+ const risksStr = formatRisks(report.risks);
1072
+ console.log(` \u2514\u2500\u2500 Riesgos: ${risksStr}`);
1073
+ const riskDetails = formatRiskDetails(report.risks);
1074
+ for (const line of riskDetails) {
1075
+ console.log(line);
1076
+ }
1077
+ console.log();
1078
+ }
1079
+ function printReports(reports, vulns) {
1080
+ const summary = {
1081
+ vulnerable: 0,
1082
+ safe: 0,
1083
+ notInstalled: 0,
1084
+ withRisks: 0
1085
+ };
1086
+ for (const report of reports) {
1087
+ printProjectReport(report, vulns);
1088
+ if (!report.installedVersion) {
1089
+ summary.notInstalled++;
1090
+ continue;
1091
+ }
1092
+ const isVulnerable = vulns.some((v) => isVersionAffected(report.installedVersion, v));
1093
+ if (isVulnerable) {
1094
+ summary.vulnerable++;
1095
+ } else {
1096
+ summary.safe++;
1097
+ }
1098
+ if (report.risks.length > 0) {
1099
+ summary.withRisks++;
1100
+ }
1101
+ }
1102
+ return summary;
1103
+ }
1104
+ function printSummary(summary) {
1105
+ console.log(c.dim("\u2500".repeat(70)));
1106
+ console.log(`
1107
+ ${c.bold("Resumen:")} ${c.red(`${summary.vulnerable} vulnerable(s)`)} \xB7 ${c.green(`${summary.safe} seguro(s)`)} \xB7 ${c.yellow(`${summary.notInstalled} no instalado(s)`)} \xB7 ${c.magenta(`${summary.withRisks} con riesgos`)}
1108
+ `);
1109
+ }
1110
+
1111
+ // src/steps.ts
1112
+ var banner = {
1113
+ name: "banner",
1114
+ execute: async (ctx) => {
1115
+ printBanner(ctx.pkg, ctx.version, ctx.scanRoot);
1116
+ }
1117
+ };
1118
+ var fetchVulnsAndScan = {
1119
+ name: "fetch-vulns-and-scan",
1120
+ execute: async (ctx) => {
1121
+ const [vulns, packageJsonPaths] = await Promise.all([
1122
+ queryOSV(ctx.pkg, ctx.version).catch((err) => {
1123
+ console.error(c.red(`Error consultando OSV: ${err.message}`));
1124
+ return [];
1125
+ }),
1126
+ scanPackageJsonFiles(ctx.scanRoot)
1127
+ ]);
1128
+ ctx.vulns = vulns;
1129
+ ctx.packageJsonPaths = packageJsonPaths;
1130
+ printVulnSummary(ctx.pkg, ctx.version, ctx.vulns);
1131
+ }
1132
+ };
1133
+ var fetchRegistryInfo = {
1134
+ name: "fetch-registry-info",
1135
+ execute: async (ctx) => {
1136
+ try {
1137
+ ctx.registryMeta = await fetchRegistryMetadata(ctx.pkg, ctx.version ?? null);
1138
+ } catch (err) {
1139
+ const message = err instanceof Error ? err.message : String(err);
1140
+ console.error(c.yellow(`Advertencia NPM Registry: ${message}`));
1141
+ ctx.registryMeta = null;
1142
+ }
1143
+ printRegistrySummary(ctx.registryMeta);
1144
+ }
1145
+ };
1146
+ var filterProjects = {
1147
+ name: "filter-projects",
1148
+ execute: async (ctx) => {
1149
+ ctx.matchingProjects = await filterProjectsWithPackage(ctx.packageJsonPaths, ctx.pkg);
1150
+ if (ctx.matchingProjects.length === 0) {
1151
+ printNoProjectsFound(ctx.pkg, ctx.scanRoot);
1152
+ process.exit(0);
1153
+ }
1154
+ printProjectCount(ctx.pkg, ctx.matchingProjects.length);
1155
+ }
1156
+ };
1157
+ var resolveVersions = {
1158
+ name: "resolve-versions",
1159
+ execute: async (ctx) => {
1160
+ ctx.results = await Promise.all(ctx.matchingProjects.map(async (project) => {
1161
+ const resolved = await resolveInstalledPackage(project.projectDir, ctx.pkg);
1162
+ return {
1163
+ ...project,
1164
+ installedVersion: resolved.version,
1165
+ lifecycleScripts: resolved.lifecycleScripts,
1166
+ entryPoint: resolved.entryPoint,
1167
+ packageDir: resolved.packageDir,
1168
+ packageDeps: resolved.packageDeps
1169
+ };
1170
+ }));
1171
+ }
1172
+ };
1173
+ var deepAnalysis = {
1174
+ name: "deep-analysis",
1175
+ execute: async (ctx) => {
1176
+ await Promise.all(ctx.results.map(async (result) => {
1177
+ if (result.entryPoint) {
1178
+ const matches = await analyzeEntryPoint(result.entryPoint);
1179
+ result.staticMatches = matches;
1180
+ }
1181
+ }));
1182
+ }
1183
+ };
1184
+ var astAnalysisStep = {
1185
+ name: "ast-analysis",
1186
+ execute: async (ctx) => {
1187
+ await Promise.all(ctx.results.map(async (result) => {
1188
+ if (result.entryPoint) {
1189
+ const astResult = await analyzeAST(result.entryPoint);
1190
+ result.astAnalysis = astResult;
1191
+ }
1192
+ }));
1193
+ }
1194
+ };
1195
+ var sandboxAnalysis = {
1196
+ name: "sandbox-analysis",
1197
+ execute: async (ctx) => {
1198
+ for (const result of ctx.results) {
1199
+ if (!result.entryPoint)
1200
+ continue;
1201
+ const hasSuspicion = (result.staticMatches?.length ?? 0) > 0 || (result.astAnalysis?.sensitiveModules.length ?? 0) > 0 || (result.astAnalysis?.highEntropyStrings.length ?? 0) > 0;
1202
+ if (!hasSuspicion)
1203
+ continue;
1204
+ try {
1205
+ const sandboxResult = await detonate(result.entryPoint);
1206
+ result.sandboxResult = sandboxResult;
1207
+ } catch {}
1208
+ }
1209
+ }
1210
+ };
1211
+ var analyzeRisks = {
1212
+ name: "analyze-risks",
1213
+ execute: async (ctx) => {
1214
+ ctx.reports = ctx.results.map((result) => {
1215
+ const risks = analyzeSupplyChainRisks(result.lifecycleScripts, result.installedVersion ? ctx.registryMeta : null, new Date, result.staticMatches ?? [], result.entryPoint, result.packageDeps, result.astAnalysis, result.sandboxResult);
1216
+ return {
1217
+ ...result,
1218
+ risks,
1219
+ registryMeta: ctx.registryMeta
1220
+ };
1221
+ });
1222
+ }
1223
+ };
1224
+ var evaluate = {
1225
+ name: "evaluate",
1226
+ execute: async (ctx) => {
1227
+ ctx.summary = printReports(ctx.reports, ctx.vulns);
1228
+ printSummary(ctx.summary);
1229
+ if (ctx.summary.vulnerable > 0)
1230
+ process.exit(2);
1231
+ }
1232
+ };
1233
+
1234
+ // src/pipeline.ts
1235
+ function createContext(pkg, version, scanRoot) {
1236
+ return {
1237
+ pkg,
1238
+ version,
1239
+ scanRoot,
1240
+ vulns: [],
1241
+ packageJsonPaths: [],
1242
+ matchingProjects: [],
1243
+ results: [],
1244
+ registryMeta: null,
1245
+ reports: [],
1246
+ summary: { vulnerable: 0, safe: 0, notInstalled: 0, withRisks: 0 }
1247
+ };
1248
+ }
1249
+ async function executePipeline(ctx, steps) {
1250
+ for (const step of steps) {
1251
+ await step.execute(ctx);
1252
+ }
1253
+ return ctx;
1254
+ }
1255
+ var defaultSteps = [
1256
+ banner,
1257
+ fetchVulnsAndScan,
1258
+ fetchRegistryInfo,
1259
+ filterProjects,
1260
+ resolveVersions,
1261
+ deepAnalysis,
1262
+ astAnalysisStep,
1263
+ sandboxAnalysis,
1264
+ analyzeRisks,
1265
+ evaluate
1266
+ ];
1267
+ async function run(pkg, version, scanRoot, steps = defaultSteps) {
1268
+ const ctx = createContext(pkg, version, scanRoot);
1269
+ return executePipeline(ctx, steps);
1270
+ }
1271
+
1272
+ // src/scan-all.ts
1273
+ import { dirname as dirname2, join as join3 } from "path";
1274
+ import { readdirSync } from "fs";
1275
+ async function enumerateNodeModules(projectDir) {
1276
+ const packages = [];
1277
+ const nmDir = join3(projectDir, "node_modules");
1278
+ let entries;
1279
+ try {
1280
+ entries = readdirSync(nmDir);
1281
+ } catch {
1282
+ return [];
1283
+ }
1284
+ for (const entry of entries) {
1285
+ if (entry.startsWith("."))
1286
+ continue;
1287
+ if (entry.startsWith("@")) {
1288
+ let scopedEntries;
1289
+ try {
1290
+ scopedEntries = readdirSync(join3(nmDir, entry));
1291
+ } catch {
1292
+ continue;
1293
+ }
1294
+ for (const scoped of scopedEntries) {
1295
+ if (scoped.startsWith("."))
1296
+ continue;
1297
+ const name = `${entry}/${scoped}`;
1298
+ const pkg2 = await readPackageMeta(projectDir, name);
1299
+ if (pkg2)
1300
+ packages.push(pkg2);
1301
+ }
1302
+ continue;
1303
+ }
1304
+ const pkg = await readPackageMeta(projectDir, entry);
1305
+ if (pkg)
1306
+ packages.push(pkg);
1307
+ }
1308
+ return packages;
1309
+ }
1310
+ async function readPackageMeta(projectDir, packageName) {
1311
+ try {
1312
+ const resolved = await resolveInstalledPackage(projectDir, packageName);
1313
+ if (!resolved.version)
1314
+ return null;
1315
+ return {
1316
+ name: packageName,
1317
+ version: resolved.version,
1318
+ projectDir,
1319
+ packageDir: resolved.packageDir,
1320
+ entryPoint: resolved.entryPoint,
1321
+ lifecycleScripts: resolved.lifecycleScripts,
1322
+ packageDeps: resolved.packageDeps
1323
+ };
1324
+ } catch {
1325
+ return null;
1326
+ }
1327
+ }
1328
+ async function discoverAllPackages(scanRoot) {
1329
+ const packageJsonPaths = await scanPackageJsonFiles(scanRoot);
1330
+ const seen = new Set;
1331
+ const all = [];
1332
+ for (const pkgPath of packageJsonPaths) {
1333
+ const projectDir = dirname2(pkgPath);
1334
+ const packages = await enumerateNodeModules(projectDir);
1335
+ for (const pkg of packages) {
1336
+ const key = `${pkg.packageDir}`;
1337
+ if (seen.has(key))
1338
+ continue;
1339
+ seen.add(key);
1340
+ all.push(pkg);
1341
+ }
1342
+ }
1343
+ return all;
1344
+ }
1345
+ async function preFilterSuspicious(packages) {
1346
+ const suspicious = [];
1347
+ for (const pkg of packages) {
1348
+ if (isSuspiciousLocal(pkg)) {
1349
+ suspicious.push(pkg);
1350
+ continue;
1351
+ }
1352
+ if (pkg.entryPoint) {
1353
+ try {
1354
+ const file = Bun.file(pkg.entryPoint);
1355
+ if (await file.exists() && file.size < 2 * 1024 * 1024) {
1356
+ const source = await file.text();
1357
+ const regexMatches = scanSource(source, pkg.entryPoint);
1358
+ if (regexMatches.length > 0) {
1359
+ suspicious.push(pkg);
1360
+ continue;
1361
+ }
1362
+ const astResult = await analyzeAST(pkg.entryPoint);
1363
+ if (astResult.sensitiveModules.length > 0 || astResult.highEntropyStrings.length > 0) {
1364
+ suspicious.push(pkg);
1365
+ continue;
1366
+ }
1367
+ }
1368
+ } catch {}
1369
+ }
1370
+ const depAudit = auditDependencies(pkg.packageDeps);
1371
+ if (depAudit.length > 0) {
1372
+ suspicious.push(pkg);
1373
+ }
1374
+ }
1375
+ return suspicious;
1376
+ }
1377
+ function isSuspiciousLocal(pkg) {
1378
+ const scripts = pkg.lifecycleScripts;
1379
+ return scripts.preinstall !== null || scripts.install !== null || scripts.postinstall !== null;
1380
+ }
1381
+ async function analyzePackageDeep(pkg) {
1382
+ const risks = [];
1383
+ risks.push(...detectLifecycleScripts(pkg.lifecycleScripts));
1384
+ if (pkg.entryPoint) {
1385
+ try {
1386
+ const file = Bun.file(pkg.entryPoint);
1387
+ if (await file.exists() && file.size < 2 * 1024 * 1024) {
1388
+ const source = await file.text();
1389
+ const regexMatches = scanSource(source, pkg.entryPoint);
1390
+ risks.push(...matchesToRisks(regexMatches, pkg.entryPoint));
1391
+ }
1392
+ } catch {}
1393
+ }
1394
+ if (pkg.entryPoint) {
1395
+ const astResult = await analyzeAST(pkg.entryPoint);
1396
+ risks.push(...astResultToRisks(astResult, pkg.entryPoint));
1397
+ }
1398
+ const depAudit = auditDependencies(pkg.packageDeps);
1399
+ risks.push(...auditToRisks(depAudit));
1400
+ if (risks.length > 0 && pkg.entryPoint) {
1401
+ try {
1402
+ const sandboxResult = await detonate(pkg.entryPoint);
1403
+ risks.push(...sandboxResultToRisks(sandboxResult));
1404
+ } catch {}
1405
+ }
1406
+ return risks;
1407
+ }
1408
+ async function runScanAll(scanRoot) {
1409
+ console.log(`
1410
+ ${c.boldCyan("pzx")} \u2014 escaneo completo en ${c.dim(scanRoot)}
1411
+ `);
1412
+ console.log(c.dim("Descubriendo paquetes instalados..."));
1413
+ const discovered = await discoverAllPackages(scanRoot);
1414
+ console.log(c.dim(`Encontrados ${discovered.length} paquete(s)
1415
+ `));
1416
+ if (discovered.length === 0) {
1417
+ console.log(c.yellow("No se encontraron paquetes instalados."));
1418
+ return;
1419
+ }
1420
+ console.log(c.dim("Analisis estatico local (pre-filtro)..."));
1421
+ const suspicious = await preFilterSuspicious(discovered);
1422
+ console.log(c.dim(`${suspicious.length}/${discovered.length} paquete(s) marcados para analisis profundo
1423
+ `));
1424
+ if (suspicious.length === 0) {
1425
+ console.log(c.boldGreen(`\u2713 Ningun paquete sospechoso detectado.
1426
+ `));
1427
+ return;
1428
+ }
1429
+ const findings = [];
1430
+ for (const pkg of suspicious) {
1431
+ const risks = await analyzePackageDeep(pkg);
1432
+ if (risks.length > 0) {
1433
+ findings.push({ pkg, risks });
1434
+ }
1435
+ }
1436
+ if (findings.length === 0) {
1437
+ console.log(c.boldGreen(`\u2713 Analisis profundo completo \u2014 sin riesgos confirmados.
1438
+ `));
1439
+ return;
1440
+ }
1441
+ console.log(c.boldRed(`\u26A0 ${findings.length} paquete(s) con riesgos:
1442
+ `));
1443
+ for (const { pkg, risks } of findings) {
1444
+ console.log(c.boldCyan(`[${pkg.name}@${pkg.version}]`));
1445
+ console.log(c.dim(` Proyecto: ${pkg.projectDir}`));
1446
+ console.log(c.dim(` Ubicacion: ${pkg.packageDir}`));
1447
+ for (const risk of risks) {
1448
+ const icon = risk.severity === "critical" ? c.boldRed("[!!!]") : risk.severity === "high" ? c.boldYellow("[!!]") : c.yellow("[!]");
1449
+ console.log(` ${icon} ${risk.message}`);
1450
+ if (risk.detail) {
1451
+ console.log(` ${c.dim(risk.detail)}`);
1452
+ }
1453
+ }
1454
+ console.log();
1455
+ }
1456
+ const summary = {
1457
+ totalPackages: discovered.length,
1458
+ suspiciousPackages: suspicious.length,
1459
+ packagesWithVulns: 0,
1460
+ packagesWithRisks: findings.length
1461
+ };
1462
+ console.log(c.dim("\u2500".repeat(70)));
1463
+ console.log(`
1464
+ ${c.bold("Resumen:")} ${c.dim(`${summary.totalPackages} total`)} \xB7 ${c.yellow(`${summary.suspiciousPackages} analizados`)} \xB7 ${c.red(`${summary.packagesWithRisks} con riesgos`)}
1465
+ `);
1466
+ if (findings.length > 0)
1467
+ process.exit(2);
1468
+ }
1469
+
1470
+ // index.ts
1471
+ async function main() {
1472
+ const { packageName, version, scanRoot, showHelp } = parseArgs(process.argv);
1473
+ if (showHelp) {
1474
+ printUsage();
1475
+ process.exit(0);
1476
+ }
1477
+ if (!packageName) {
1478
+ await runScanAll(scanRoot);
1479
+ return;
1480
+ }
1481
+ await run(packageName, version, scanRoot);
1482
+ }
1483
+ main().catch((err) => {
1484
+ const message = err instanceof Error ? err.message : String(err);
1485
+ console.error(c.red(`Error fatal: ${message}`));
1486
+ process.exit(1);
1487
+ });