#!/usr/bin/env python3 import os import json import subprocess # Indicadores de Compromiso (IoCs): Archivos dropper usados por el gusano DROPPER_FILES = ["setup.mjs", "math_init.js", "Math_Symbol.js"] # Paquetes a auditar y sus versiones estrictamente maliciosas confirmadas KNOWN_VULNERABLE_PACKAGES = { "keyv": ["6.0.0"], "cacheable": ["2.5.1"], "@cacheable/memory": ["2.2.1"], "@cacheable/net": ["2.1.1"], "@cacheable/node-cache": ["3.1.2"], "@cacheable/utils": ["2.5.1"], "cache-manager": ["7.2.10"], "cacheable-request": ["13.0.20"] } TARGET_PACKAGES = [ "keyv", "cacheable", "cacheable-request", "cache-manager", "flat-cache", "file-entry-cache" ] def check_dropper_files(package_dir): """Inspecciona el directorio de un paquete en busca de archivos dropper.""" found_droppers = [] for dropper in DROPPER_FILES: path = os.path.join(package_dir, dropper) if os.path.exists(path): found_droppers.append(path) return found_droppers def run_npm_list_audit(project_dir): """Ejecuta una inspección de dependencias equivalente a 'npm list'.""" print(f"\n[+] Ejecutando auditoría del árbol de dependencias en: {project_dir}") pkgs_str = " ".join(TARGET_PACKAGES) try: result = subprocess.run( f"npm list {pkgs_str}", shell=True, cwd=project_dir, capture_output=True, text=True ) output = result.stdout.strip() or result.stderr.strip() print("--- Árbol de dependencias resuelto ---") print(output) print("--------------------------------------") except Exception as e: print(f"[!] No se pudo ejecutar npm list: {e}") def scan_directory(root_dir): print(f"[*] Iniciando escaneo avanzado contra 'Shai-Hulud' desde: {os.path.abspath(root_dir)}\n") warnings_found = 0 projects_with_npm = set() for dirpath, dirnames, filenames in os.walk(root_dir): if ".git" in dirnames: dirnames.remove(".git") # 1. Comprobación directa de archivos dropper en la ruta actual for file in filenames: if file in DROPPER_FILES: file_path = os.path.join(dirpath, file) print(f"[CRÍTICO] Archivo dropper malicioso detectado directamente en: {file_path}") warnings_found += 1 # 2. Análisis de package.json if "package.json" in filenames: pkg_path = os.path.join(dirpath, "package.json") # Registrar si es la raíz de un proyecto de Node.js if "node_modules" in dirnames or os.path.exists(os.path.join(dirpath, "package-lock.json")): projects_with_npm.add(dirpath) try: with open(pkg_path, "r", encoding="utf-8") as f: data = json.load(f) # Verificar scripts preinstall sospechosos scripts = data.get("scripts", {}) preinstall = scripts.get("preinstall", "") if any(dropper in preinstall for dropper in DROPPER_FILES): print(f"[CRÍTICO] Hook preinstall infectado en: {pkg_path}") print(f" Comando: \"preinstall\": \"{preinstall}\"") warnings_found += 1 # Inspección de instalación local si este package.json pertenece a un nodo instalado pkg_name = data.get("name", "") if pkg_name in TARGET_PACKAGES or any(pkg_name.startswith(p) for p in ["@cacheable/"]): # Verificar si este paquete en node_modules contiene un dropper físicamente droppers = check_dropper_files(dirpath) if droppers: print(f"[CRÍTICO] ¡ALERTA CONFIRMADA! Dropper hallado dentro del paquete '{pkg_name}':") for d in droppers: print(f" -> {d}") warnings_found += 1 else: version = data.get("version", "desconocida") # Verificar versión contra la lista de infecciones confirmadas if pkg_name in KNOWN_VULNERABLE_PACKAGES and version in KNOWN_VULNERABLE_PACKAGES[pkg_name]: print(f"[ALERTA] Versión infectada conocida hallada: {pkg_name}@{version} en {dirpath}") warnings_found += 1 else: print(f"[OK] Paquete de riesgo presente pero LIMPIO (Sin droppers, versión segura): {pkg_name}@{version}") except (json.JSONDecodeError, PermissionError): continue # 3. Ejecutar la comprobación automática del árbol en los proyectos encontrados for proj in projects_with_npm: run_npm_list_audit(proj) print("\n" + "="*60) if warnings_found == 0: print("[OK] Escaneo finalizado: No se detectaron archivos dropper ni versiones maliciosas.") else: print(f"[RESULTADO] Se encontraron {warnings_found} amenazas reales/críticas. Revisa los detalles arriba.") print("="*60) if __name__ == "__main__": scan_directory(os.getcwd())