secmanifest 1.0.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.
@@ -0,0 +1,182 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
3
+
4
+ interface ParsedLockfile {
5
+ packages: Map<string, string>;
6
+ }
7
+
8
+ async function parsePnpmLock(
9
+ lockfilePath: string,
10
+ ): Promise<ParsedLockfile> {
11
+ const content = await readFile(lockfilePath, "utf-8");
12
+ const packages = new Map<string, string>();
13
+
14
+ const packageRegex =
15
+ /^ \/([^@]+)@([^:\s]+)/gm;
16
+ let match: RegExpExecArray | null;
17
+
18
+ while ((match = packageRegex.exec(content)) !== null) {
19
+ const name = match[1];
20
+ const version = match[2];
21
+ if (name && version) {
22
+ packages.set(name, version);
23
+ }
24
+ }
25
+
26
+ const snapshotRegex = /^ ['"]?([^'":\s]+)@([^'":\s]+)/gm;
27
+ while ((match = snapshotRegex.exec(content)) !== null) {
28
+ const name = match[1];
29
+ const version = match[2];
30
+ if (name && version && !packages.has(name)) {
31
+ packages.set(name, version);
32
+ }
33
+ }
34
+
35
+ return { packages };
36
+ }
37
+
38
+ async function parseYarnLock(
39
+ lockfilePath: string,
40
+ ): Promise<ParsedLockfile> {
41
+ const content = await readFile(lockfilePath, "utf-8");
42
+ const packages = new Map<string, string>();
43
+
44
+ const regex = /^"?([^@\s"]+)@[^"]*"?:\s*$/gm;
45
+ const versionRegex = /^\s+version\s+"?([^"\s]+)"?$/m;
46
+
47
+ let match: RegExpExecArray | null;
48
+ const lines = content.split("\n");
49
+
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+ if (!line) continue;
53
+ const nameMatch = line.match(/^"?([^@\s"]+)@[^"]*"?:\s*$/);
54
+ if (nameMatch && nameMatch[1]) {
55
+ const name = nameMatch[1];
56
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
57
+ const versionMatch = lines[j]?.match(
58
+ /^\s+version\s+"?([^"\s]+)"?$/,
59
+ );
60
+ if (versionMatch && versionMatch[1]) {
61
+ packages.set(name, versionMatch[1]);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ return { packages };
69
+ }
70
+
71
+ async function parseBunLock(
72
+ lockfilePath: string,
73
+ ): Promise<ParsedLockfile> {
74
+ const content = await readFile(lockfilePath, "utf-8");
75
+ const packages = new Map<string, string>();
76
+
77
+ try {
78
+ const lockfile = JSON.parse(content);
79
+ if (lockfile.packages) {
80
+ for (const [key, value] of Object.entries(
81
+ lockfile.packages as Record<string, { version?: string }>,
82
+ )) {
83
+ const name = key.replace(/^.*?node_modules\//, "").replace(/\/.*$/, "");
84
+ if (value?.version) {
85
+ packages.set(name, value.version);
86
+ }
87
+ }
88
+ }
89
+ } catch {
90
+ const regex = /"([^"]+)":\s*\{\s*"version":\s*"([^"]+)"/g;
91
+ let match: RegExpExecArray | null;
92
+ while ((match = regex.exec(content)) !== null) {
93
+ if (match[1] && match[2]) {
94
+ packages.set(match[1], match[2]);
95
+ }
96
+ }
97
+ }
98
+
99
+ return { packages };
100
+ }
101
+
102
+ export async function scanLockfileDrift(
103
+ projectInfo: ProjectInfo,
104
+ ): Promise<ScanResult> {
105
+ const startTime = Date.now();
106
+ const findings: Finding[] = [];
107
+ const errors: Error[] = [];
108
+
109
+ if (!projectInfo.lockfilePath) {
110
+ return {
111
+ findings,
112
+ errors: [
113
+ new Error("No se encontro lockfile para analizar."),
114
+ ],
115
+ duration: Date.now() - startTime,
116
+ };
117
+ }
118
+
119
+ let parsed: ParsedLockfile;
120
+
121
+ try {
122
+ switch (projectInfo.lockfileType) {
123
+ case "pnpm":
124
+ parsed = await parsePnpmLock(projectInfo.lockfilePath);
125
+ break;
126
+ case "yarn":
127
+ parsed = await parseYarnLock(projectInfo.lockfilePath);
128
+ break;
129
+ case "bun":
130
+ parsed = await parseBunLock(projectInfo.lockfilePath);
131
+ break;
132
+ default:
133
+ errors.push(
134
+ new Error(
135
+ `Tipo de lockfile no soportado: ${projectInfo.lockfileType}`,
136
+ ),
137
+ );
138
+ return {
139
+ findings,
140
+ errors,
141
+ duration: Date.now() - startTime,
142
+ };
143
+ }
144
+ } catch (error) {
145
+ errors.push(
146
+ new Error(
147
+ `Error al parsear lockfile: ${error instanceof Error ? error.message : String(error)}`,
148
+ ),
149
+ );
150
+ return {
151
+ findings,
152
+ errors,
153
+ duration: Date.now() - startTime,
154
+ };
155
+ }
156
+
157
+ const allDeclaredDeps = {
158
+ ...projectInfo.dependencies,
159
+ ...projectInfo.devDependencies,
160
+ };
161
+
162
+ for (const [lockName, lockVersion] of parsed.packages) {
163
+ if (!(lockName in allDeclaredDeps)) {
164
+ findings.push({
165
+ id: `drift-phantom-${lockName}`,
166
+ severity: "medium",
167
+ category: "drift",
168
+ title: `Paquete fantasma: ${lockName}`,
169
+ description: `${lockName} esta en el lockfile pero no declarado en package.json. Puede ser una dependencia no intencionada.`,
170
+ package: lockName,
171
+ version: lockVersion,
172
+ recommendation: `Ejecutar un fresh install o eliminar ${lockName} si no es necesario.`,
173
+ });
174
+ }
175
+ }
176
+
177
+ return {
178
+ findings,
179
+ errors,
180
+ duration: Date.now() - startTime,
181
+ };
182
+ }
@@ -0,0 +1,148 @@
1
+ import type { Finding, ScanResult } from "../utils/types.js";
2
+
3
+ const KNOWN_MALICIOUS_PACKAGES = new Set([
4
+ "event-stream",
5
+ "flatmap-stream",
6
+ "crossenv",
7
+ "cross-env.js",
8
+ "crossenv.js",
9
+ "getcookies",
10
+ "mongose",
11
+ "lodashs",
12
+ "colors.js",
13
+ "faker.js",
14
+ "babelcli",
15
+ "babelcli.js",
16
+ "chromium-poly",
17
+ "chromium-polyfill",
18
+ "coa",
19
+ "coa.js",
20
+ "rc",
21
+ "rc.js",
22
+ "ua-parser-js",
23
+ "ua-parser",
24
+ "npm-script",
25
+ "npm-script.js",
26
+ "opener",
27
+ "opener.js",
28
+ "eslint-scope",
29
+ "eslint-scope.js",
30
+ "prettier.js",
31
+ "prettierx",
32
+ "pirs",
33
+ "pirs.js",
34
+ ]);
35
+
36
+ const TYPOSQUAT_PATTERNS: Array<{
37
+ target: string;
38
+ typos: string[];
39
+ }> = [
40
+ { target: "express", typos: ["exress", "expresss", "exprss", "expres"] },
41
+ { target: "lodash", typos: ["lodahs", "lodashh", "lodah", "lodsh"] },
42
+ {
43
+ target: "react",
44
+ typos: ["reacr", "reactt", "reac", "reaact"],
45
+ },
46
+ { target: "webpack", typos: ["weback", "webpak", "webpacck"] },
47
+ { target: "typescript", typos: ["typescrip", "typescrpt", "tpyescript"] },
48
+ {
49
+ target: "eslint",
50
+ typos: ["eslit", "eslnt", "elint", "eslintt"],
51
+ },
52
+ {
53
+ target: "prettier",
54
+ typos: ["prettir", "prettieer"],
55
+ },
56
+ { target: "chalk", typos: ["chakl", "chal", "challk"] },
57
+ { target: "axios", typos: ["axois", "axio", "axiios"] },
58
+ { target: "moment", typos: ["momnet", "momment", "momet"] },
59
+ {
60
+ target: "commander",
61
+ typos: ["commandr", "comander"],
62
+ },
63
+ {
64
+ target: "inquirer",
65
+ typos: ["inquiere", "inquirerr"],
66
+ },
67
+ { target: "jest", typos: ["jset", "jestt", "jst"] },
68
+ { target: "mocha", typos: ["moch", "mochaa", "moccha"] },
69
+ { target: "next", typos: ["nexxt", "neext", "nextt"] },
70
+ { target: "nuxt", typos: ["nuxxt", "nuuxt", "nuxst"] },
71
+ { target: "vue", typos: ["vvue", "vuee", "vued"] },
72
+ { target: "angular", typos: ["angularr", "angularjs", "anngular"] },
73
+ ];
74
+
75
+ const KNOWN_MALICIOUS_AUTHORS = new Set([
76
+ "admin@admin.com",
77
+ "test@test.com",
78
+ "unknown",
79
+ ]);
80
+
81
+ function levenshteinDistance(a: string, b: string): number {
82
+ const m = a.length;
83
+ const n = b.length;
84
+ const dp: number[][] = Array.from({ length: m + 1 }, () =>
85
+ new Array<number>(n + 1).fill(0),
86
+ );
87
+
88
+ for (let i = 0; i <= m; i++) dp[i]![0] = i;
89
+ for (let j = 0; j <= n; j++) dp[0]![j] = j;
90
+
91
+ for (let i = 1; i <= m; i++) {
92
+ for (let j = 1; j <= n; j++) {
93
+ dp[i]![j] =
94
+ a[i - 1] === b[j - 1]
95
+ ? dp[i - 1]![j - 1]!
96
+ : 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!);
97
+ }
98
+ }
99
+
100
+ return dp[m]![n]!;
101
+ }
102
+
103
+ export function scanMalware(
104
+ packages: Array<{ name: string; version: string }>,
105
+ ): ScanResult {
106
+ const startTime = Date.now();
107
+ const findings: Finding[] = [];
108
+ const errors: Error[] = [];
109
+
110
+ for (const pkg of packages) {
111
+ if (KNOWN_MALICIOUS_PACKAGES.has(pkg.name)) {
112
+ findings.push({
113
+ id: `malware-known-${pkg.name}`,
114
+ severity: "critical",
115
+ category: "malware",
116
+ title: `Paquete malicioso conocido: ${pkg.name}`,
117
+ description: `${pkg.name} esta en la lista de paquetes maliciosos conocidos. Este paquete ha sido comprometido anteriormente.`,
118
+ package: pkg.name,
119
+ version: pkg.version,
120
+ recommendation: `Eliminar ${pkg.name} inmediatamente y buscar alternativas seguras.`,
121
+ });
122
+ continue;
123
+ }
124
+
125
+ for (const pattern of TYPOSQUAT_PATTERNS) {
126
+ const distance = levenshteinDistance(pkg.name, pattern.target);
127
+ if (distance > 0 && distance <= 2 && pkg.name.length > 2) {
128
+ findings.push({
129
+ id: `malware-typosquat-${pkg.name}`,
130
+ severity: "high",
131
+ category: "malware",
132
+ title: `Posible typosquatting: ${pkg.name} (similar a ${pattern.target})`,
133
+ description: `El paquete ${pkg.name} es sospechosamente similar a ${pattern.target}. Diferencia de编辑距离: ${distance}.`,
134
+ package: pkg.name,
135
+ version: pkg.version,
136
+ recommendation: `Verificar que ${pkg.name} es el paquete intencionado y no una suplantación.`,
137
+ });
138
+ break;
139
+ }
140
+ }
141
+ }
142
+
143
+ return {
144
+ findings,
145
+ errors,
146
+ duration: Date.now() - startTime,
147
+ };
148
+ }
@@ -0,0 +1,148 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
4
+
5
+ const SUSPICIOUS_METADATA_PATTERNS = [
6
+ {
7
+ field: "description",
8
+ check: (val: string, pkgName: string) => !val || val.length < 5,
9
+ title: "Paquete sin descripcion",
10
+ description: "El paquete no tiene descripcion o es muy corta. Los paquetes maliciosos suelen omitir metadatos.",
11
+ severity: "info" as const,
12
+ skipIfScoped: true,
13
+ skipIfTypes: true,
14
+ },
15
+ {
16
+ field: "repository",
17
+ check: (val: unknown) => !val,
18
+ title: "Paquete sin repositorio",
19
+ description: "El paquete no tiene repositorio asociado. Los paquetes legitimos usualmente tienen un repositorio publico.",
20
+ severity: "low" as const,
21
+ skipIfScoped: true,
22
+ skipIfTypes: true,
23
+ },
24
+ {
25
+ field: "license",
26
+ check: (val: unknown) => !val,
27
+ title: "Paquete sin licencia",
28
+ description: "El paquete no tiene licencia declarada. Esto puede indicar un paquete abandonado o malicioso.",
29
+ severity: "low" as const,
30
+ skipIfScoped: false,
31
+ skipIfTypes: true,
32
+ },
33
+ ];
34
+
35
+ const BINARY_FIELDS = [
36
+ "bin",
37
+ "binary",
38
+ "native",
39
+ "gypfile",
40
+ "binding.gyp",
41
+ ];
42
+
43
+ const NETWORK_ACCESS_FIELDS = [
44
+ "postinstall",
45
+ "preinstall",
46
+ "install",
47
+ "prepare",
48
+ ];
49
+
50
+ export async function scanMetadata(
51
+ projectInfo: ProjectInfo,
52
+ projectPath: string,
53
+ ): Promise<ScanResult> {
54
+ const startTime = Date.now();
55
+ const findings: Finding[] = [];
56
+ const errors: Error[] = [];
57
+
58
+ const allDeps = {
59
+ ...projectInfo.dependencies,
60
+ ...projectInfo.devDependencies,
61
+ };
62
+
63
+ for (const [name, version] of Object.entries(allDeps)) {
64
+ const pkgJsonPath = join(
65
+ projectPath,
66
+ "node_modules",
67
+ name,
68
+ "package.json",
69
+ );
70
+
71
+ let pkgJson: Record<string, unknown>;
72
+ try {
73
+ const content = await readFile(pkgJsonPath, "utf-8");
74
+ pkgJson = JSON.parse(content);
75
+ } catch {
76
+ continue;
77
+ }
78
+
79
+ for (const rule of SUSPICIOUS_METADATA_PATTERNS) {
80
+ const value = pkgJson[rule.field];
81
+
82
+ if ("skipIfScoped" in rule && rule.skipIfScoped && name.startsWith("@")) {
83
+ continue;
84
+ }
85
+ if ("skipIfTypes" in rule && rule.skipIfTypes && name.startsWith("@types/")) {
86
+ continue;
87
+ }
88
+
89
+ if (rule.check(value as string, name)) {
90
+ findings.push({
91
+ id: `metadata-${rule.field}-${name}`,
92
+ severity: rule.severity,
93
+ category: "malware",
94
+ title: `${rule.title}: ${name}`,
95
+ description: `${name}: ${rule.description}`,
96
+ package: name,
97
+ version,
98
+ recommendation: "Verificar que el paquete es legitimo y esta activamente mantenido.",
99
+ });
100
+ }
101
+ }
102
+
103
+ if (pkgJson.gypfile === true) {
104
+ findings.push({
105
+ id: `metadata-native-${name}`,
106
+ severity: "medium",
107
+ category: "malware",
108
+ title: `Paquete con codigo nativo: ${name}`,
109
+ description: `${name} contiene codigo nativo (gypfile). Los binarios pueden ejecutar codigo arbitrario durante la instalacion.`,
110
+ package: name,
111
+ version,
112
+ recommendation: "Verificar que el codigo nativo es necesario y proviene de una fuente confiable.",
113
+ });
114
+ }
115
+
116
+ const scripts = pkgJson.scripts as Record<string, string> | undefined;
117
+ if (scripts) {
118
+ for (const hook of NETWORK_ACCESS_FIELDS) {
119
+ if (scripts[hook]) {
120
+ const scriptVal = scripts[hook];
121
+ if (
122
+ scriptVal.includes("curl") ||
123
+ scriptVal.includes("wget") ||
124
+ scriptVal.includes("http") ||
125
+ scriptVal.includes("download")
126
+ ) {
127
+ findings.push({
128
+ id: `metadata-network-${name}-${hook}`,
129
+ severity: "high",
130
+ category: "backdoor",
131
+ title: `${name} accede a red en "${hook}"`,
132
+ description: `${name} ejecuta acceso a red en el hook "${hook}": ${scriptVal.slice(0, 100)}`,
133
+ package: name,
134
+ version,
135
+ recommendation: "Este paquete descarga contenido durante la instalacion. Verificar que es seguro.",
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ return {
144
+ findings,
145
+ errors,
146
+ duration: Date.now() - startTime,
147
+ };
148
+ }
@@ -0,0 +1,71 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
4
+
5
+ const EOL_NODE_VERSIONS = [
6
+ { version: "0.10", eolDate: "2016-10-31", severity: "critical" as const },
7
+ { version: "0.12", eolDate: "2016-12-31", severity: "critical" as const },
8
+ { version: "4", eolDate: "2018-04-30", severity: "critical" as const },
9
+ { version: "5", eolDate: "2016-06-30", severity: "critical" as const },
10
+ { version: "6", eolDate: "2019-04-30", severity: "critical" as const },
11
+ { version: "7", eolDate: "2017-06-30", severity: "critical" as const },
12
+ { version: "8", eolDate: "2019-12-31", severity: "high" as const },
13
+ { version: "9", eolDate: "2018-06-30", severity: "high" as const },
14
+ { version: "10", eolDate: "2021-04-30", severity: "high" as const },
15
+ { version: "11", eolDate: "2019-06-30", severity: "high" as const },
16
+ { version: "12", eolDate: "2022-04-30", severity: "medium" as const },
17
+ { version: "13", eolDate: "2020-06-01", severity: "high" as const },
18
+ { version: "14", eolDate: "2023-04-30", severity: "medium" as const },
19
+ { version: "15", eolDate: "2021-06-01", severity: "medium" as const },
20
+ { version: "16", eolDate: "2023-09-11", severity: "low" as const },
21
+ { version: "17", eolDate: "2022-06-01", severity: "medium" as const },
22
+ { version: "18", eolDate: "2025-04-30", severity: "low" as const },
23
+ { version: "19", eolDate: "2023-06-01", severity: "low" as const },
24
+ ];
25
+
26
+ const ENGINES_FIELD_PATTERN = /(\d+)(?:\.x)?/;
27
+
28
+ export async function scanNodeVersion(
29
+ projectInfo: ProjectInfo,
30
+ projectPath: string,
31
+ ): Promise<ScanResult> {
32
+ const startTime = Date.now();
33
+ const findings: Finding[] = [];
34
+ const errors: Error[] = [];
35
+
36
+ const pkgJsonPath = join(projectPath, "package.json");
37
+ try {
38
+ const content = await readFile(pkgJsonPath, "utf-8");
39
+ const pkgJson = JSON.parse(content);
40
+
41
+ const engines = pkgJson.engines as { node?: string } | undefined;
42
+ if (engines?.node) {
43
+ const match = engines.node.match(ENGINES_FIELD_PATTERN);
44
+ if (match && match[1]) {
45
+ const majorVersion = parseInt(match[1], 10);
46
+ const eolVersion = EOL_NODE_VERSIONS.find(
47
+ (v) => parseInt(v.version, 10) === majorVersion,
48
+ );
49
+
50
+ if (eolVersion) {
51
+ findings.push({
52
+ id: `node-eol-${majorVersion}`,
53
+ severity: eolVersion.severity,
54
+ category: "vulnerability",
55
+ title: `Node.js ${majorVersion} esta en End-of-Life`,
56
+ description: `El proyecto requiere Node.js ${majorVersion} que reaching EOL el ${eolVersion.eolDate}. Sin actualizaciones de seguridad.`,
57
+ recommendation: `Actualizar a una version soportada de Node.js (18+ recomendado).`,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ } catch {
63
+ // Could not read package.json
64
+ }
65
+
66
+ return {
67
+ findings,
68
+ errors,
69
+ duration: Date.now() - startTime,
70
+ };
71
+ }
@@ -0,0 +1,151 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
4
+
5
+ const OBFUSCATION_PATTERNS = [
6
+ {
7
+ name: "Base64 encoded execution",
8
+ regex: /(?:eval|exec|Function)\s*\(\s*(?:atob|Buffer\.from)\s*\(/g,
9
+ severity: "critical" as const,
10
+ description: "Ejecucion de codigo codificado en Base64. Patron comun en malware.",
11
+ },
12
+ {
13
+ name: "Base64 string literal",
14
+ regex: /['"][A-Za-z0-9+/]{40,}={0,2}['"]/g,
15
+ severity: "medium" as const,
16
+ description: "String Base64 largo detectado. Puede contener codigo ofuscado.",
17
+ },
18
+ {
19
+ name: "eval with concatenation",
20
+ regex: /eval\s*\(\s*['"].*['"]\s*\+/g,
21
+ severity: "high" as const,
22
+ description: "eval() con concatenacion de strings. Patron de inyeccion de codigo.",
23
+ },
24
+ {
25
+ name: "String.fromCharCode",
26
+ regex: /String\.fromCharCode\s*\(/g,
27
+ severity: "medium" as const,
28
+ description: "Uso de fromCharCode para construir strings. Comun en ofuscacion.",
29
+ },
30
+ {
31
+ name: "Hex escape sequences",
32
+ regex: /(?:\\x[0-9a-f]{2}){4,}/gi,
33
+ severity: "medium" as const,
34
+ description: "Secuencias de escape hexadecimales. Puede ocultar codigo malicioso.",
35
+ },
36
+ {
37
+ name: "Unicode escape sequences",
38
+ regex: /(?:\\u[0-9a-f]{4}){4,}/gi,
39
+ severity: "medium" as const,
40
+ description: "Secuencias de escape Unicode. Comun en ofuscacion de codigo.",
41
+ },
42
+ {
43
+ name: "Obfuscated require",
44
+ regex: /require\s*\(\s*['"][^'"]*\\x[^'"]*['"]\s*\)/g,
45
+ severity: "critical" as const,
46
+ description: "require() con path ofuscado. Intento de ocultar dependencias.",
47
+ },
48
+ {
49
+ name: "Dynamic import with eval",
50
+ regex: /import\s*\(\s*eval\s*\(/g,
51
+ severity: "critical" as const,
52
+ description: "import() dinamico con eval. Carga de modulos arbitrarios.",
53
+ },
54
+ {
55
+ name: "Process.env access pattern",
56
+ regex: /process\.env\s*\[\s*['"][^'"]*['"]\s*\]/g,
57
+ severity: "low" as const,
58
+ description: "Acceso a process.env con bracket notation. Puede ocultar acceso a secrets.",
59
+ },
60
+ {
61
+ name: "Child process spawn",
62
+ regex: /(?:child_process|spawn|execSync|execFile)\s*\.\s*(?:spawn|exec|execSync)\s*\(/g,
63
+ severity: "high" as const,
64
+ description: "Ejecucion de procesos hijos. Comun en malware para ejecutar comandos del sistema.",
65
+ },
66
+ ];
67
+
68
+ const COMPRESSION_PATTERNS = [
69
+ {
70
+ name: "zlib inflate",
71
+ regex: /zlib\s*\.\s*inflate(?:Sync)?\s*\(/g,
72
+ severity: "medium" as const,
73
+ description: "Descompresion zlib. Puede usarse para desempaquetar codigo malicioso.",
74
+ },
75
+ {
76
+ name: "gzip decompress",
77
+ regex: /(?:gunzip|ungzip|createGunzip)\s*\(/g,
78
+ severity: "medium" as const,
79
+ description: "Descompresion gzip en tiempo de ejecucion.",
80
+ },
81
+ ];
82
+
83
+ async function scanFileForObfuscation(
84
+ filePath: string,
85
+ findings: Finding[],
86
+ ): Promise<void> {
87
+ let content: string;
88
+ try {
89
+ content = await readFile(filePath, "utf-8");
90
+ } catch {
91
+ return;
92
+ }
93
+
94
+ const allPatterns = [...OBFUSCATION_PATTERNS, ...COMPRESSION_PATTERNS];
95
+
96
+ for (const pattern of allPatterns) {
97
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
98
+ const matches = content.match(regex);
99
+
100
+ if (matches && matches.length > 0) {
101
+ findings.push({
102
+ id: `obfuscation-${pattern.name.toLowerCase().replace(/\s+/g, "-")}-${filePath}`,
103
+ severity: pattern.severity,
104
+ category: "malware",
105
+ title: `${pattern.name} en ${filePath}`,
106
+ description: `${pattern.description}. Ocurrencias: ${matches.length}`,
107
+ recommendation: `Revisar manualmente el archivo ${filePath} para confirmar que no es malicioso.`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ export async function scanObfuscation(
114
+ projectInfo: ProjectInfo,
115
+ projectPath: string,
116
+ ): Promise<ScanResult> {
117
+ const startTime = Date.now();
118
+ const findings: Finding[] = [];
119
+ const errors: Error[] = [];
120
+
121
+ const allDeps = {
122
+ ...projectInfo.dependencies,
123
+ ...projectInfo.devDependencies,
124
+ };
125
+
126
+ const sourceExtensions = [".js", ".ts", ".mjs", ".cjs"];
127
+
128
+ for (const [name] of Object.entries(allDeps)) {
129
+ const pkgDir = join(projectPath, "node_modules", name);
130
+
131
+ try {
132
+ const { readdir } = await import("node:fs/promises");
133
+ const entries = await readdir(pkgDir, { withFileTypes: true });
134
+
135
+ for (const entry of entries) {
136
+ if (entry.isFile() && sourceExtensions.some((ext) => entry.name.endsWith(ext))) {
137
+ const filePath = join(pkgDir, entry.name);
138
+ await scanFileForObfuscation(filePath, findings);
139
+ }
140
+ }
141
+ } catch {
142
+ // Package directory not accessible
143
+ }
144
+ }
145
+
146
+ return {
147
+ findings,
148
+ errors,
149
+ duration: Date.now() - startTime,
150
+ };
151
+ }