@lateos/npm-scan 0.15.6 → 0.16.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.
Files changed (31) hide show
  1. package/README.de.md +5 -1
  2. package/README.fr.md +5 -1
  3. package/README.ja.md +5 -1
  4. package/README.md +18 -2
  5. package/README.zh.md +5 -1
  6. package/backend/detectors/index.js +4 -0
  7. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +24 -0
  8. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +19 -0
  9. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +40 -0
  10. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +31 -0
  11. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +73 -0
  12. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +37 -0
  13. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +49 -0
  14. package/backend/detectors/node-ipc-compromise/d6-bootstrap-resolver.js +40 -0
  15. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +42 -0
  16. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +27 -0
  17. package/backend/detectors/node-ipc-compromise/d9-temp-artifact.js +20 -0
  18. package/backend/detectors/node-ipc-compromise/index.js +93 -0
  19. package/backend/detectors/node-ipc-compromise/iocs.json +59 -0
  20. package/backend/detectors/trapdoor/d1-campaign-marker.js +20 -0
  21. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +22 -0
  22. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +10 -0
  23. package/backend/detectors/trapdoor/d4-gists-exfil.js +34 -0
  24. package/backend/detectors/trapdoor/d5-ai-poisoning.js +35 -0
  25. package/backend/detectors/trapdoor/d6-lure-name.js +42 -0
  26. package/backend/detectors/trapdoor/d7-crypto-primitives.js +22 -0
  27. package/backend/detectors/trapdoor/d8-xor-key.js +15 -0
  28. package/backend/detectors/trapdoor/d9-cred-validation.js +32 -0
  29. package/backend/detectors/trapdoor/index.js +77 -0
  30. package/backend/detectors/trapdoor/iocs.json +51 -0
  31. package/package.json +1 -1
package/README.de.md CHANGED
@@ -228,6 +228,8 @@ npm-scan report --pdf # alle Scans (Premium)
228
228
  | **ATK-010** | Sandbox-Evasion / Anti-Analyse | Verhaltensbasiert | 🟠 mittel | SR-10.3 |
229
229
  | **ATK-011** | Transitive Verbreitung (wurmartige laterale Ausbreitung) | Verhaltensbasiert | 🔴 hoch | SR-11.4 |
230
230
  | **CVE-2026-48710** | BadHost — Starlette Authentifizierungs-Bypass via Host-Header-Injection (CVE-2026-48710, CVSS 7.0). Python-Abhängigkeitsversionserkennung (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py/cfg), transitive Heuristik (15 bekannte Downstream-Pakete: fastapi, vllm, litellm, MCP-Server, etc.), statische Code-Pattern-Analyse für gefährliche `request.url.path`-Nutzung in Auth/Middleware-Kontexten mit `request.scope["path"]`-Unterdrückung | Statisch + Registry | 🔴 hoch / 🟠 mittel / ℹ️ info | SR-3.1, SR-5.3 |
231
+ | **TRAPDOOR** | TrapDoor plattformübergreifende Angriffskampagne — Kampagnenmarker P-2024-001, trap-core.js-Payload-Fingerprint, Publisher-Blocklist asdxzxc, Gist-basierter Credential-Exfil, KI-Kontextvergiftung (Zero-Width-Unicode), Crypto/DeFi-Locknamen, Fernet+ECDH-Verschlüsselung, XOR-Key cargo-build-helper-2026, STS/GitHub-API-Validierung | Statisch + Registry | 🟠 mittel / 🔴 hoch / ⚫ kritisch | SR-3.1, SR-5.3, SR-7.5 |
232
+ | **NODE_IPC_COMPROMISE** | node-ipc Supply-Chain-Kompromittierung (14. Mai 2026) — Versions-Blocklist (9.1.6/9.2.3/12.0.1) mit sicheren Pins, Tarball-SHA-256-Verifikation, CJS-Payload-IIFE-Injektion, DNS-over-nicht-Standard-Port-C2, Bootstrap-Resolver sh.azurestaticprovider.net, DNS-TXT-Exfiltrationszone bt.node.js, setImmediate()-Laufzeitauslöser, ~/nt-*/ Staging-Artefakte, unbefugter Publisher atiertant, Lockfile-Blastradius | Statisch + Registry | ⚫ kritisch | SR-3.1, SR-5.3, SR-7.5 |
231
233
 
232
234
  > **Wie ausweichende Angriffe erkannt werden:** ATK-009 erkennt Pakete, die `process.env.CI` prüfen, Hostnamen sondieren oder zeitbasierte Aktivierung verwenden. ATK-010 markiert `debugger`-Anweisungen, `os.hostname()`-Sonden und Umgebungs-Fingerprinting. ATK-011 verfolgt Peer-Abhängigkeitsgraphen, um wurmartige Verbreitungsmuster zu erkennen.
233
235
  > Vollständige Dokumentation der Ausweichfläche und PoC-Beispiele finden Sie in [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md).
@@ -549,7 +551,7 @@ Siehe den obigen [Docker-Schnellstart-Abschnitt](#-lateosnpm-scan-überall-mit-d
549
551
 
550
552
  ### Kostenlose Stufe (ausgeliefert)
551
553
 
552
- - Alle 11 ATK-Detektoren (statisch + verhaltensbasiert) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
554
+ - Alle 11 ATK-Detektoren (statisch + verhaltensbasiert) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)** + **TRAPDOOR** (9 Regeln) + **NODE_IPC_COMPROMISE** (11 Regeln)
553
555
  - SBOM-Ausgabe (CycloneDX + SPDX)
554
556
  - HTML-, Text- und Compliance-Berichte (NIST + EU CRA)
555
557
  - Policy-as-Code-Engine (YAML)
@@ -616,6 +618,8 @@ node --test test/detectors-corpus.test.js
616
618
  - `test/cve-2026-48710-badhost/transitive.test.js` — 7 transitive Abhängigkeitstests (Tier 1/2, fastapi-Version-Gating, Pin-Unterdrückung)
617
619
  - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 statische Code-Pattern-Tests (Auth-Kontext, INFO-Durchgriff, Scope-Unterdrückung)
618
620
  - `test/cve-2026-48710-badhost/integration.test.js` — 4 Integrationstests (End-to-End-Composite-Findings, sauberes Projekt, keine Python-Dateien)
621
+ - `test/trapdoor.test.js` — 40 TrapDoor-Kampagnenerkennungstests (D1–D9: Kampagnenmarker, Payload-Fingerprint, Publisher-Blocklist, Gist-Exfil, KI-Vergiftung, Lockname, Krypto-Primitive, XOR-Key, Credential-Validierung)
622
+ - `test/node-ipc.test.js` — 37 node-ipc-Kompromittierungstests (D1–D11: Versions-Blocklist, Tarball-Hash, CJS-Injektion, Payload-Hash, DNS-C2-Muster, Bootstrap-Resolver, DNS-TXT-Exfil, Laufzeitauslöser, Temp-Artefakte, unbefugter Publisher, Blastradius)
619
623
  - `test/cli.test.js` — Commander-Integrationstests (Hilfe, Version, Scan, Bericht, Fehlerbehandlung)
620
624
 
621
625
  ### Hilfe benötigt?
package/README.fr.md CHANGED
@@ -228,6 +228,8 @@ npm-scan report --pdf # tous les scans (premium)
228
228
  | **ATK-010** | Contournement de sandbox / anti-analyse | Comportementale | 🟠 moyenne | SR-10.3 |
229
229
  | **ATK-011** | Propagation transitive (dissémination latérale de type ver) | Comportementale | 🔴 élevée | SR-11.4 |
230
230
  | **CVE-2026-48710** | BadHost — contournement d'authentification Starlette par injection d'en-tête Host (CVE-2026-48710, CVSS 7.0). Détection de version de dépendance Python (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py/cfg), heuristique transitive (15 paquets aval connus : fastapi, vllm, litellm, serveurs MCP, etc.), analyse statique de code pour `request.url.path` dangereux en contexte auth/middleware avec suppression par `request.scope["path"]` | Statique + Registre | 🔴 élevée / 🟠 moyenne / ℹ️ info | SR-3.1, SR-5.3 |
231
+ | **TRAPDOOR** | Campagne d'attaque multi-écosystème TrapDoor — marqueur de campagne P-2024-001, empreinte de charge utile trap-core.js, liste noire d'éditeur asdxzxc, exfiltration d'identifiants via Gist, empoisonnement de contexte IA (Unicode largeur nulle), noms leurres crypto/DeFi, chiffrement Fernet+ECDH, clé XOR cargo-build-helper-2026, validation d'identifiants STS/API GitHub | Statique + Registre | 🟠 moyenne / 🔴 élevée / ⚫ critique | SR-3.1, SR-5.3, SR-7.5 |
232
+ | **NODE_IPC_COMPROMISE** | Compromission de la chaîne d'approvisionnement node-ipc (14 mai 2026) — liste noire de versions (9.1.6/9.2.3/12.0.1) avec épingle de sécurité, vérification SHA-256 du tarball, injection IIFE de charge utile CJS, DNS sur port non standard, résolveur d'amorçage sh.azurestaticprovider.net, zone d'exfiltration DNS TXT bt.node.js, déclencheur d'exécution setImmediate(), artefacts de staging ~/nt-*/, éditeur non autorisé atiertant, détection du rayon d'impact dans les lockfiles avec recommandations d'épingle | Statique + Registre | ⚫ critique | SR-3.1, SR-5.3, SR-7.5 |
231
233
 
232
234
  > **Comment les attaques furtives sont détectées :** ATK-009 détecte les paquets qui vérifient `process.env.CI`, sondent les noms d'hôte ou utilisent une activation temporelle. ATK-009 signale les instructions `debugger`, les sondes `os.hostname()` et l'empreinte environnementale. ATK-011 trace les graphes de dépendances peer pour détecter les schémas de propagation de type ver.
233
235
  > Voir [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md) pour la documentation complète de la surface d'évasion et des exemples de PoC.
@@ -549,7 +551,7 @@ Voir la [section Démarrage rapide Docker](#-exécutez-lateosnpm-scan-partout-av
549
551
 
550
552
  ### Niveau gratuit (livré)
551
553
 
552
- - Les 11 détecteurs ATK (statique + comportemental) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
554
+ - Les 11 détecteurs ATK (statique + comportemental) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)** + **TRAPDOOR** (9 règles) + **NODE_IPC_COMPROMISE** (11 règles)
553
555
  - Sortie SBOM (CycloneDX + SPDX)
554
556
  - Rapports HTML, texte et conformité (NIST + EU CRA)
555
557
  - Moteur de politique en tant que code (YAML)
@@ -616,6 +618,8 @@ node --test test/detectors-corpus.test.js
616
618
  - `test/cve-2026-48710-badhost/transitive.test.js` — 7 tests de dépendances transitives (Tier 1/2, contrôle de version fastapi, suppression par épinglage)
617
619
  - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 tests de motifs de code statiques (contexte auth, passage INFO, suppression scope)
618
620
  - `test/cve-2026-48710-badhost/integration.test.js` — 4 tests d'intégration (résultats composites de bout en bout, projet propre, pas de fichiers Python)
621
+ - `test/trapdoor.test.js` — 40 tests de détection de la campagne TrapDoor (D1–D9 : marqueur de campagne, empreinte de charge utile, liste noire d'éditeur, exfiltration Gist, empoisonnement IA, nom leurre, primitives cryptographiques, clé XOR, validation d'identifiants)
622
+ - `test/node-ipc.test.js` — 37 tests de détection de compromission node-ipc (D1–D11 : liste noire de versions, hachage tarball, injection CJS, hachage de charge utile, motif DNS C2, résolveur d'amorçage, exfiltration DNS TXT, déclencheur d'exécution, artefacts temporaires, éditeur non autorisé, rayon d'impact)
619
623
  - `test/cli.test.js` — tests d'intégration commander (aide, version, scan, rapport, gestion d'erreurs)
620
624
 
621
625
  ### Besoin d'aide ?
package/README.ja.md CHANGED
@@ -224,6 +224,8 @@ npm-scan report --pdf # すべてのスキャン(プレミアム
224
224
  | **ATK-010** | サンドボックス回避/アンチ解析 | 行動 | 🟠 中 | SR-10.3 |
225
225
  | **ATK-011** | 推移的伝播(ワーム型横方向拡散) | 行動 | 🔴 高 | SR-11.4 |
226
226
  | **CVE-2026-48710** | BadHost — Starlette Host ヘッダーインジェクション認証バイパス (CVE-2026-48710, CVSS 7.0)。Python 依存関係バージョン検出 (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py/cfg)、推移的ヒューリスティック (15 の既知ダウンストリームパッケージ:fastapi, vllm, litellm, MCP サーバー等)、auth/middleware コンテキストでの危険な `request.url.path` 使用の静的コードパターンスキャン、`request.scope["path"]` による抑制対応 | 静的 + レジストリ | 🔴 高 / 🟠 中 / ℹ️ 情報 | SR-3.1, SR-5.3 |
227
+ | **TRAPDOOR** | TrapDoor クロスエコシステム攻撃キャンペーン — キャンペーンマーカー P-2024-001、trap-core.js ペイロードフィンガープリント、パブリッシャーブロックリスト asdxzxc、Gist ベースの認証情報流出、AI コンテキストポイズニング(ゼロ幅 Unicode)、暗号資産/DeFi ルアー名、Fernet+ECDH 暗号化、XOR キー cargo-build-helper-2026、STS/GitHub API 認証情報検証 | 静的 + レジストリ | 🟠 中 / 🔴 高 / ⚫ クリティカル | SR-3.1, SR-5.3, SR-7.5 |
228
+ | **NODE_IPC_COMPROMISE** | node-ipc サプライチェーン侵害(2026年5月14日)— バージョンブロックリスト (9.1.6/9.2.3/12.0.1) と安全な固定、tarball SHA-256 検証、CJS ペイロード IIFE インジェクション、非標準ポート DNS C2 パターン、ブートストラップリゾルバー sh.azurestaticprovider.net、DNS TXT 流出ゾーン bt.node.js、setImmediate() ランタイムトリガー、~/nt-*/ ステージングアーティファクト、未承認パブリッシャー atiertant、ロックファイル影響範囲検出と安全な固定推奨 | 静的 + レジストリ | ⚫ クリティカル | SR-3.1, SR-5.3, SR-7.5 |
227
229
 
228
230
  > **回避型攻撃の捕捉方法:** ATK-009は`process.env.CI`をチェックする、ホスト名をプローブする、または時間ベースのアクティベーションを使用するパッケージを検出します。ATK-010は`debugger`文、`os.hostname()`プローブ、環境フィンガープリンティングをフラグ付けします。ATK-011はピア依存関係グラフをトレースしてワーム型伝播パターンを検出します。
229
231
  > 完全な回避面のドキュメントとPoC例については、[`docs/attack-taxonomy.md`](docs/attack-taxonomy.md)を参照してください。
@@ -545,7 +547,7 @@ npm-scan report --html > report.html
545
547
 
546
548
  ### 無料版(出荷済み)
547
549
 
548
- - 全11ATK検出器(静的+行動)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
550
+ - 全11ATK検出器(静的+行動)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)** + **TRAPDOOR**(9ルール)+ **NODE_IPC_COMPROMISE**(11ルール)
549
551
  - SBOM出力(CycloneDX + SPDX)
550
552
  - HTML、テキスト、コンプライアンスレポート(NIST + EU CRA)
551
553
  - ポリシー・アズ・コードエンジン(YAML)
@@ -612,6 +614,8 @@ node --test test/detectors-corpus.test.js
612
614
  - `test/cve-2026-48710-badhost/transitive.test.js` — 7の推移的依存関係テスト(Tier 1/2, fastapiバージョンゲーティング, 固定抑制)
613
615
  - `test/cve-2026-48710-badhost/codePattern.test.js` — 6の静的コードパターンテスト(authコンテキスト, INFOフォールスルー, scope抑制)
614
616
  - `test/cve-2026-48710-badhost/integration.test.js` — 4の統合テスト(エンドツーエンド複合発見項目, クリーンプロジェクト, Pythonファイルなし)
617
+ - `test/trapdoor.test.js` — 40のTrapDoorキャンペーン検出テスト(D1–D9:キャンペーンマーカー、ペイロードフィンガープリント、パブリッシャーブロックリスト、Gist流出、AIポイズニング、ルアー名、暗号プリミティブ、XORキー、認証情報検証)
618
+ - `test/node-ipc.test.js` — 37のnode-ipc侵害検出テスト(D1–D11:バージョンブロックリスト、tarballハッシュ、CJSインジェクション、ペイロードハッシュ、DNS C2パターン、ブートストラップリゾルバー、DNS TXT流出、ランタイムトリガー、一時アーティファクト、未承認パブリッシャー、影響範囲)
615
619
  - `test/cli.test.js` — commander統合テスト(ヘルプ、バージョン、スキャン、レポート、エラーハンドリング)
616
620
 
617
621
  ### ヘルプが必要ですか?
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@lateos/npm-scan?style=flat-square)](https://www.npmjs.com/package/@lateos/npm-scan)
4
4
  [![License](https://img.shields.io/badge/license-Apache%202.0%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSING.md)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
6
- [![Tests](https://img.shields.io/badge/tests-459%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
6
+ [![Tests](https://img.shields.io/badge/tests-536%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
7
7
  [![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
8
8
  [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
9
9
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
@@ -28,6 +28,10 @@ A growing attack vector is **HuggingFace org impersonation** — packages that m
28
28
 
29
29
  The **Megalodon campaign** (2026) alone compromised 5,500+ repositories via fake GitHub PRs, malicious workflow injection, and cloud credential exfiltration — all coordinated through a single actor automating the entire kill chain. **@lateos/npm-scan** now detects artifacts of this campaign out of the box.
30
30
 
31
+ The **TrapDoor campaign** (May 2026) spans npm, PyPI, and Crates.io — 34 malicious packages, 384+ versions attributed to a single publisher, targeting crypto, DeFi, Solana, and AI developers with Fernet + ECDH encrypted payloads, AI context poisoning via zero-width Unicode injection in `.cursorrules`/`CLAUDE.md`, and credential live-validation against AWS STS and GitHub API before exfiltration. **@lateos/npm-scan** now detects all 9 TrapDoor signals.
32
+
33
+ The **node-ipc compromise** (May 14, 2026) weaponized an expired maintainer email domain to hijack one of npm's most depended-upon packages (822K weekly downloads). Three malicious versions (9.1.6, 9.2.3, 12.0.1) delivered an 80KB credential stealer via DNS TXT tunneling — no HTTP, no postinstall hook, invisible to HTTP-layer firewalls. **@lateos/npm-scan** now detects all 11 node-ipc compromise signals.
34
+
31
35
  Critical infrastructure vulnerabilities in the Python ecosystem are also in scope. The **BadHost (CVE-2026-48710)** vulnerability in Starlette < 1.0.1 enables authentication bypass via unvalidated HTTP Host header injection, affecting FastAPI, vLLM, LiteLLM, MCP servers, and any project using Starlette transitively — now detected across Python manifests, transitive dependency chains, and source code patterns in a single scan.
32
36
 
33
37
  **npm audit** checks known CVEs. **Snyk** scans for vulnerabilities. **Socket** looks at package behavior. None of them were designed for the generation of attacks that emerged in 2025 — attacks that look benign until they reach production.
@@ -53,6 +57,8 @@ Critical infrastructure vulnerabilities in the Python ecosystem are also in scop
53
57
  | HF model repo impersonation + README clone | ❌ | ❌ | ❌ | ✅ |
54
58
  | VS Code extension supply chain scan (--vsix) | ❌ | ❌ | ❌ | ✅ |
55
59
  | Python vulnerability detection (CVE-2026-48710 BadHost) | ❌ | ❌ | ❌ | ✅ |
60
+ | Cross-ecosystem attack detection (TrapDoor) | ❌ | ❌ | ❌ | ✅ |
61
+ | Expired-domain hijack detection (node-ipc) | ❌ | ❌ | ❌ | ✅ |
56
62
  | Attack taxonomy (ATK series) | ❌ | ❌ | ❌ | ✅ |
57
63
  | SBOM output (CycloneDX + SPDX) | ❌ | ✅ | ❌ | ✅ |
58
64
  | SARIF v2.1 (GitHub Code Scanning) | ❌ | ❌ | ❌ | ✅ |
@@ -76,6 +82,8 @@ Critical infrastructure vulnerabilities in the Python ecosystem are also in scop
76
82
  | 🪱 | **Worm campaign detection** | Mini Shai-Hulud — 6 sub-checks detecting burst publish, sibling compromise, SLSA attestation mismatch, publisher drift, IOC match, and token exfil across 3 waves (TanStack, AntV/atool, Nx Console) |
77
83
  | 🧩 | **VSIX extension scanning** | `npm-scan scan --vsix nrwl.angular-console` — detects VS Code Marketplace supply chain attacks: burst publish, publisher anomaly, activation event risk, orphan commit fetch, known IOC, and exfil patterns (Nx Console 18.95.0 CVE-2026-48027) |
78
84
  | 🐍 | **Python vulnerability detection** | CVE-2026-48710 (BadHost) — Starlette Host header injection across 6 Python manifest formats, 15 transitive downstream packages (fastapi, vllm, litellm, MCP), and static `request.url.path` code pattern analysis with `scope["path"]` suppression |
85
+ | 🪤 | **Cross-ecosystem attack detection** | TrapDoor — 9 sub-checks: campaign marker P-2024-001, trap-core.js payload fingerprint, publisher blocklist asdxzxc, Gist-based credential exfil, AI config zero-width Unicode poisoning, crypto/DeFi lure name heuristic, Fernet+ECDH encryption, XOR key cargo-build-helper-2026, STS/GitHub API credential validation |
86
+ | 📡 | **Expired-domain hijack detection** | node-ipc compromise — version blocklist (9.1.6/9.2.3/12.0.1), tarball SHA-256 verification, CJS vs ESM size anomaly with IIFE injection, DNS-over-non-standard-port C2, bootstrap resolver sh.azurestaticprovider.net, DNS TXT exfil zone bt.node.js, setImmediate() runtime trigger, ~/nt-*/ staging artifacts, unauthorized publisher atiertant, lockfile blast-radius with safe pin recommendations |
79
87
  | 📦 | **SBOM generation** | CycloneDX 1.5 and SPDX 2.3 with findings embedded as vulnerabilities |
80
88
  | 🔍 | **SARIF output** | GitHub Advanced Security / CodeQL compatible SARIF v2.1 — shows findings directly in Security tab |
81
89
  | 🧾 | **Compliance reporting** | NIST SP 800-161 traceability matrix + EU Cyber Resilience Act mapping (free tier) |
@@ -286,6 +294,8 @@ npm-scan report --pdf # all scans (premium)
286
294
  | **MINI_SHAI_HULUD** | Mini Shai-Hulud worm campaign — burst publish velocity (≥3 versions/30 min), co-temporal sibling compromise, SLSA attestation mismatch (sub-60s gap, first-ever, builder mismatch), publisher drift (<10 min account change), IOC match (scope/sha512/publisher from seed file), token exfil (NPM_TOKEN/.npmrc/atob patterns), Nx Console downstream detection | Static + Registry | 🔴 high / ⚫ critical | SR-3.1, SR-7.5 |
287
295
  | **VSIX_SCAN** | VS Code extension supply chain scan — burst publish (≥2 versions/30 min, hot-pull <20 min), publisher anomaly (account substitution, new-account on high-install ext, 15-min add+publish), activation event risk (onStartupFinished→HIGH, *→CRITICAL, escalation on shell keywords), orphan commit fetch (GitHub API SHA refs, npx git URL, MCP-disguised exfil, Bun install), known IOC (extensionId/publisherAccount/commit hash from seed), exfil patterns (cred paths, DNS tunneling, AES+RSA, anti-analysis, Bun APIs) | Static + Registry | 🟠 medium / 🔴 high / ⚫ critical | SR-3.1, SR-5.3 |
288
296
  | **CVE-2026-48710** | BadHost — Starlette authentication bypass via Host header injection (CVE-2026-48710, CVSS 7.0). Python dependency version detection (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py/cfg), transitive heuristic (15 known downstream packages: fastapi, vllm, litellm, MCP servers, etc.), static code pattern scan for dangerous `request.url.path` usage in auth/middleware context with `request.scope["path"]` suppression | Static + Registry | 🔴 high / 🟠 medium / ℹ️ info | SR-3.1, SR-5.3 |
297
+ | **TRAPDOOR** | TrapDoor cross-ecosystem attack — campaign marker P-2024-001 in files, shared payload trap-core.js filename/48,485-byte fingerprint, publisher blocklist asdxzxc, Gist-based credential exfil (ddjidd564.github.io + credential paths), AI context poisoning via zero-width Unicode in .cursorrules/CLAUDE.md, crypto/DeFi lure name heuristic (<30 days), Fernet+ECDH crypto primitives in postinstall, XOR key cargo-build-helper-2026 in lockfiles, STS/GitHub API credential validation in postinstall | Static + Registry | 🟠 medium / 🔴 high / ⚫ critical | SR-3.1, SR-5.3, SR-7.5 |
298
+ | **NODE_IPC_COMPROMISE** | node-ipc supply chain compromise (May 14, 2026) — version blocklist (9.1.6/9.2.3/12.0.1) with safe pins, tarball SHA-256 verification, CJS payload IIFE injection detection (CJS>ESM size differential), injected payload hash match, DNS-over-non-standard-port C2 (setServers + custom resolver), bootstrap resolver sh.azurestaticprovider.net + C2 IP 37.16.75.69, DNS TXT exfiltration zone bt.node.js, setImmediate() runtime trigger, ~/nt-*/ staging artifact detection, unauthorized publisher atiertant, lockfile blast-radius detection with pin recommendations | Static + Registry | ⚫ critical | SR-3.1, SR-5.3, SR-7.5 |
289
299
 
290
300
  > **How evasive attacks are caught:** ATK-009 detects packages that check `process.env.CI`, probe hostnames, or use time-based activation. ATK-010 flags `debugger` statements, `os.hostname()` probes, and env fingerprinting. ATK-011 traces peer dependency graphs to detect worm-like propagation patterns.
291
301
  > **MEGALODON** campaign detection analyzes bundled `.github/workflows/` files for C2 co-occurrence and base64 decode chains, scans tarball files for credential + outbound network patterns, detects version publish velocity spikes via npm registry metadata, and identifies publisher account drift — all without any network calls beyond the initial package fetch.
@@ -293,6 +303,8 @@ npm-scan report --pdf # all scans (premium)
293
303
  > **MINI_SHAI_HULUD** worm campaign detection uses a lazy two-stage evaluation: Stage 1 runs burst velocity, publisher drift, IOC, and token exfil checks (in-memory, no network). If burst triggers, Stage 2 queries npm attestation endpoints for SLSA anomalies and fetches sibling package registry metadata for co-temporal burst detection. Composite finding includes wave attribution (wave1-tanstack, wave2-antv, wave3-nx-console) and critical severity when SLSA or IOC match. NX_CONSOLE_DOWNSTREAM (D7) flags npm packages with `@nx/*` dependencies and checks for `nrwl.angular-console` in `.vscode/extensions.json`.
294
304
  > **VSIX_SCAN** extension scanning wraps both VS Code Marketplace and Open VSX registries with rate-limited (10 req/min), cached (5 min TTL) API clients. All 6 detectors run asynchronously and aggregate into a single composite `VSIX_SCAN` finding. Zero extension code is executed — all analysis is static regex/text-pattern matching. No Bun installation required for Bun pattern detection.
295
305
  > **CVE-2026-48710 (BadHost)** detection uses three independent layers: Layer 1 parses 6 Python manifest formats (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py, setup.cfg) with PEP 440 semver-aware version comparison. Layer 2 scans for 15 known Starlette-downstream packages with Tier 1 (HIGH confidence) and Tier 2 (MEDIUM confidence) transitive heuristics, suppressed by explicit `starlette >= 1.0.1` pin. Layer 3 performs function-boundary static analysis on `.py` files for `request.url.path` usage, escalating to MEDIUM severity in auth/middleware contexts and suppressing when `request.scope["path"]` is used in the same function.
306
+ > **TRAPDOOR** campaign detection runs 9 sub-detectors across all package files (README.md, package.json, .md, shell scripts, .cursorrules, CLAUDE.md) for the hardcoded marker P-2024-001, scans for trap-core.js by filename or exact 48,485-byte size, unconditionally blocks publisher asdxzxc, detects outbound references to ddjidd564.github.io or gist.github.com combined with credential-path patterns, scans .cursorrules and CLAUDE.md for zero-width Unicode characters (U+200B, U+200C, U+200D, U+FEFF), flags crypto/DeFi-themed packages <30 days old with no prior version history, detects simultaneous Fernet and ECDH/createECDH usage in postinstall JS, and identifies sts.amazonaws.com and api.github.com/user calls in postinstall hooks.
307
+ > **NODE_IPC_COMPROMISE** detection intercepts the expired-domain takeover attack across 11 dimensions: version blocklist with safe-pin recommendations (9.1.5 for 9.x, 12.0.0 for 12.x), tarball SHA-256 hash verification against known malicious hashes, CJS vs ESM size comparison with IIFE suffix pattern detection for injected payload identification, DNS resolver pattern analysis (dns.promises.Resolver + setServers with non-public IP + resolveTxt), bootstrap domain and C2 IP detection, resolveTxt() with bt.node.js zone references, setImmediate() runtime trigger detection, ~/nt-*/ staging artifact path identification, publisher account verification against unauthorized account atiertant, and lockfile scanning for compromised version resolution with safe-pin suggestions.
296
308
  > See [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md) for full evasion surface documentation and PoC examples.
297
309
 
298
310
  ---
@@ -377,6 +389,8 @@ Campaign detectors use seed IOC files for known-malicious fingerprints:
377
389
  | IOC File | Detector | Types |
378
390
  |----------|----------|-------|
379
391
  | `backend/detectors/mini-shai-hulud/iocs.json` | Mini Shai-Hulud (Waves 1–3) | `packageScope`, `publisherAccount`, `sha512`, `extensionId` |
392
+ | `backend/detectors/trapdoor/iocs.json` | TrapDoor | `publisherAccount`, `campaignMarker`, `payloadFilename`, `payloadSize`, `xorKey`, `c2Domain`, `gistDomain` |
393
+ | `backend/detectors/node-ipc-compromise/iocs.json` | node-ipc compromise | `publisherAccount`, `c2Domain`, `c2IP`, `exfilZone`, `payloadHash` |
380
394
  | `backend/vsix-scan/vsix-iocs.json` | VSIX extension scan | `extensionId`, `publisherAccount`, `orphanCommitHash` |
381
395
 
382
396
  IOC files follow a unified schema (`iocs: [{ type, value, ... }]`) and are loaded at module init. Update them from your threat intel feed to extend detection coverage without code changes.
@@ -651,7 +665,7 @@ See the [Docker quick-start section](#-run-lateosnpm-scan-anywhere-with-docker--
651
665
 
652
666
  ### Free tier (shipped)
653
667
 
654
- - All 11 ATK detectors + **MEGALODON** CI/CD campaign detection (D1–D6) + **HF_IMPERSONATION** detector + **MINI_SHAI_HULUD** worm campaign (D1–D7, 3 waves) + **VSIX_SCAN** extension supply chain scan (6 detectors) + **CVE-2026-48710 (BadHost)** Python vulnerability detection (3 layers)
668
+ - All 11 ATK detectors + **MEGALODON** CI/CD campaign detection (D1–D6) + **HF_IMPERSONATION** detector + **MINI_SHAI_HULUD** worm campaign (D1–D7, 3 waves) + **VSIX_SCAN** extension supply chain scan (6 detectors) + **CVE-2026-48710 (BadHost)** Python vulnerability detection (3 layers) + **TRAPDOOR** cross-ecosystem attack detection (9 rules) + **NODE_IPC_COMPROMISE** expired-domain hijack detection (11 rules)
655
669
  - SBOM output (CycloneDX + SPDX)
656
670
  - HTML, text, and compliance reports (NIST + EU CRA)
657
671
  - Policy-as-code engine (YAML)
@@ -734,6 +748,8 @@ node --test test/detectors-corpus.test.js
734
748
  - `test/cve-2026-48710-badhost/transitive.test.js` — 7 transitive dependency tests (Tier 1/2, fastapi version gating, pin suppression)
735
749
  - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 static code pattern tests (auth context, INFO fallthrough, scope suppression)
736
750
  - `test/cve-2026-48710-badhost/integration.test.js` — 4 integration tests (end-to-end composite findings, clean project, no Python files)
751
+ - `test/trapdoor.test.js` — 40 TrapDoor campaign detection tests (D1–D9: campaign marker, payload fingerprint, publisher blocklist, Gist exfil, AI poisoning, lure name, crypto primitives, XOR key, credential validation)
752
+ - `test/node-ipc.test.js` — 37 node-ipc compromise detection tests (D1–D11: version blocklist, tarball hash, CJS injection, payload hash, DNS C2 pattern, bootstrap resolver, DNS TXT exfil, runtime trigger, temp artifact, unauthorized publisher, blast radius)
737
753
  - `test/cli.test.js` — commander integration tests (help, version, scan, report, error handling)
738
754
  - `test/cli-lockfile.test.js` — scan-lockfile CLI options, yarn/pnpm/monorepo/watch tests
739
755
 
package/README.zh.md CHANGED
@@ -228,6 +228,8 @@ npm-scan report --pdf # 所有扫描(高级版)
228
228
  | **ATK-010** | 沙箱逃逸 / 反分析 | 行为 | 🟠 中 | SR-10.3 |
229
229
  | **ATK-011** | 传递性传播(蠕虫式横向扩散) | 行为 | 🔴 高 | SR-11.4 |
230
230
  | **CVE-2026-48710** | BadHost — Starlette Host 头注入认证绕过 (CVE-2026-48710, CVSS 7.0)。Python 依赖版本检测 (requirements.txt, pyproject.toml, poetry.lock, Pipfile, setup.py/cfg),传递性启发式检测 (15 个已知下游包:fastapi, vllm, litellm, MCP 服务器等),auth/middleware 上下文中危险 `request.url.path` 使用的静态代码模式扫描,支持 `request.scope["path"]` 抑制 | 静态 + 注册表 | 🔴 高 / 🟠 中 / ℹ️ 信息 | SR-3.1, SR-5.3 |
231
+ | **TRAPDOOR** | TrapDoor 跨生态系统攻击活动 — 活动标记 P-2024-001,trap-core.js 载荷指纹,发布者黑名单 asdxzxc,基于 Gist 的凭证窃取,AI 上下文注入(零宽 Unicode),加密/DeFi 诱饵名称,Fernet+ECDH 加密,XOR 密钥 cargo-build-helper-2026,STS/GitHub API 凭证验证 | 静态 + 注册表 | 🟠 中 / 🔴 高 / ⚫ 严重 | SR-3.1, SR-5.3, SR-7.5 |
232
+ | **NODE_IPC_COMPROMISE** | node-ipc 供应链入侵(2026年5月14日)— 版本黑名单 (9.1.6/9.2.3/12.0.1) 及安全锁定,tarball SHA-256 验证,CJS 载荷 IIFE 注入检测,DNS 非标准端口 C2 模式,引导解析器 sh.azurestaticprovider.net,DNS TXT 外泄区域 bt.node.js,setImmediate() 运行时触发,~/nt-*/ 临时制品检测,未授权发布者 atiertant,锁定文件影响范围检测并推荐安全固定版本 | 静态 + 注册表 | ⚫ 严重 | SR-3.1, SR-5.3, SR-7.5 |
231
233
 
232
234
  > **如何捕获逃避式攻击:** ATK-009 检测检查 `process.env.CI`、探测主机名或使用时间激活的包。ATK-010 标记 `debugger` 语句、`os.hostname()` 探测和环境指纹采集。ATK-011 追踪同级依赖图以检测蠕虫式传播模式。
233
235
  > 完整逃避面文档和 PoC 示例请参阅 [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md)。
@@ -549,7 +551,7 @@ npm-scan report --html > report.html
549
551
 
550
552
  ### 免费版(已发布)
551
553
 
552
- - 全部 11 个 ATK 检测器(静态 + 行为)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
554
+ - 全部 11 个 ATK 检测器(静态 + 行为)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)** + **TRAPDOOR**(9 条规则)+ **NODE_IPC_COMPROMISE**(11 条规则)
553
555
  - SBOM 输出(CycloneDX + SPDX)
554
556
  - HTML、文本和合规报告(NIST + EU CRA)
555
557
  - 策略即代码引擎(YAML)
@@ -616,6 +618,8 @@ node --test test/detectors-corpus.test.js
616
618
  - `test/cve-2026-48710-badhost/transitive.test.js` — 7 个传递性依赖测试(Tier 1/2, fastapi 版本门控, 固定版本抑制)
617
619
  - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 个静态代码模式测试(auth 上下文, INFO 穿透, scope 抑制)
618
620
  - `test/cve-2026-48710-badhost/integration.test.js` — 4 个集成测试(端到端复合发现项, 清洁项目, 无 Python 文件)
621
+ - `test/trapdoor.test.js` — 40 个 TrapDoor 活动检测测试(D1–D9:活动标记、载荷指纹、发布者黑名单、Gist 外泄、AI 注入、诱饵名称、加密原语、XOR 密钥、凭证验证)
622
+ - `test/node-ipc.test.js` — 37 个 node-ipc 入侵检测测试(D1–D11:版本黑名单、tarball 哈希、CJS 注入、载荷哈希、DNS C2 模式、引导解析器、DNS TXT 外泄、运行时触发、临时制品、未授权发布者、影响范围)
619
623
  - `test/cli.test.js` — commander 集成测试(帮助、版本、扫描、报告、错误处理)
620
624
 
621
625
  ### 需要帮助?
@@ -13,6 +13,8 @@ import { scanAll as megalodonScan } from './megalodon/index.js';
13
13
  import { scan as hfScan } from './hf-impersonation/index.js';
14
14
  import { scan as miniShaiHuludScan } from './mini-shai-hulud/index.js';
15
15
  import { scan as badhostScan } from './cve-2026-48710-badhost/index.js';
16
+ import { scan as trapdoorScan } from './trapdoor/index.js';
17
+ import { scan as nodeIpcScan } from './node-ipc-compromise/index.js';
16
18
 
17
19
  export async function runAll(pkgJson, files = [], registryMeta = null, allFiles = null) {
18
20
  const findings = [];
@@ -31,5 +33,7 @@ export async function runAll(pkgJson, files = [], registryMeta = null, allFiles
31
33
  findings.push(...await hfScan(pkgJson, files, registryMeta, allFiles || files));
32
34
  findings.push(...await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files));
33
35
  findings.push(...await badhostScan(pkgJson, files, registryMeta, allFiles || files));
36
+ findings.push(...await trapdoorScan(pkgJson, files, registryMeta, allFiles || files));
37
+ findings.push(...await nodeIpcScan(pkgJson, files, registryMeta, allFiles || files));
34
38
  return findings.sort((a, b) => b.severity.localeCompare(a.severity));
35
39
  }
@@ -0,0 +1,24 @@
1
+ const BLOCKED_VERSIONS = new Set(['9.1.6', '9.2.3', '12.0.1']);
2
+
3
+ const SAFE_PINS = {
4
+ '9.1.6': '9.1.5',
5
+ '9.2.3': '9.1.5',
6
+ '12.0.1': '12.0.0',
7
+ };
8
+
9
+ export function scanVersionBlocklist(pkgJson, registryMeta) {
10
+ const pkgName = pkgJson?.name || '';
11
+ if (pkgName !== 'node-ipc') return { triggered: false };
12
+
13
+ const version = pkgJson?.version || '';
14
+ if (BLOCKED_VERSIONS.has(version)) {
15
+ return {
16
+ triggered: true,
17
+ version,
18
+ safePin: SAFE_PINS[version],
19
+ maliciousVersions: ['9.1.6', '9.2.3', '12.0.1'],
20
+ };
21
+ }
22
+
23
+ return { triggered: false, version };
24
+ }
@@ -0,0 +1,19 @@
1
+ export function scanUnauthorizedPublisher(pkgJson, registryMeta) {
2
+ const pkgName = pkgJson?.name || '';
3
+ if (pkgName !== 'node-ipc') return { triggered: false };
4
+
5
+ const publisherAccount = registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name
6
+ || registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name
7
+ || null;
8
+
9
+ if (publisherAccount === 'atiertant') {
10
+ return {
11
+ triggered: true,
12
+ publisher: publisherAccount,
13
+ package: pkgName,
14
+ detail: 'Account atiertant has no prior release history on node-ipc — account recovery via expired email domain takeover',
15
+ };
16
+ }
17
+
18
+ return { triggered: false, publisher: publisherAccount };
19
+ }
@@ -0,0 +1,40 @@
1
+ const COMPROMISED_VERSIONS = {
2
+ '9.1.6': { safePin: '9.1.5', ranges: ['~9.1.x', '^9.1', '^9'] },
3
+ '9.2.3': { safePin: '9.1.5', ranges: ['~9.2.x', '^9.2'] },
4
+ '12.0.1': { safePin: '12.0.0', ranges: ['~12.0.x', '^12'] },
5
+ };
6
+
7
+ const LOCKFILE_PATTERNS = [
8
+ /package-lock\.json$/i,
9
+ /yarn\.lock$/i,
10
+ /pnpm-lock\.yaml$/i,
11
+ /pnpm-lock\.yml$/i,
12
+ ];
13
+
14
+ export function scanBlastRadius(allFiles) {
15
+ const matches = [];
16
+
17
+ for (const file of allFiles) {
18
+ const path = file.path?.replace(/\\/g, '/') || '';
19
+ const isLockfile = LOCKFILE_PATTERNS.some(p => p.test(path));
20
+ if (!isLockfile) continue;
21
+
22
+ const content = file.content || '';
23
+ const hasNodeIpc = /\bnode-ipc\b/i.test(content);
24
+ if (!hasNodeIpc) continue;
25
+
26
+ for (const [badVersion, info] of Object.entries(COMPROMISED_VERSIONS)) {
27
+ const versionInQuotes = `"${badVersion}"`;
28
+ if (content.includes(versionInQuotes)) {
29
+ matches.push({
30
+ file: path,
31
+ compromisedVersion: badVersion,
32
+ safePin: info.safePin,
33
+ detail: `node-ipc resolved to compromised version ${badVersion} in lockfile. Pin to ${info.safePin}.`,
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return { triggered: matches.length > 0, matches };
40
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ const MALICIOUS_HASHES = new Set([
4
+ '449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e',
5
+ 'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea',
6
+ '78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981',
7
+ ]);
8
+
9
+ export function scanTarballHash(allFiles) {
10
+ const matches = [];
11
+
12
+ for (const file of allFiles) {
13
+ const path = file.path || '';
14
+ if (!path.endsWith('.tgz') && !path.endsWith('.tar.gz')) continue;
15
+
16
+ const content = file.content || '';
17
+ const hash = createHash('sha256').update(content, 'utf8').digest('hex');
18
+
19
+ if (MALICIOUS_HASHES.has(hash)) {
20
+ matches.push({
21
+ file: path,
22
+ sha256: hash,
23
+ version: hash === '449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e'
24
+ ? '9.1.6' : hash === 'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea'
25
+ ? '9.2.3' : '12.0.1',
26
+ });
27
+ }
28
+ }
29
+
30
+ return { triggered: matches.length > 0, matches };
31
+ }
@@ -0,0 +1,73 @@
1
+ const IIFE_END_PATTERN = /}\)\(\);\s*$/;
2
+ const SIZE_DIFFERENTIAL_THRESHOLD = 50 * 1024;
3
+
4
+ export function scanCjsPayloadInjection(allFiles) {
5
+ const matches = [];
6
+
7
+ let cjsContent = null;
8
+ let mjsContent = null;
9
+ let cjsPath = null;
10
+ let mjsPath = null;
11
+
12
+ for (const file of allFiles) {
13
+ const path = file.path?.replace(/\\/g, '/') || '';
14
+ if (path.endsWith('node-ipc.cjs')) {
15
+ cjsContent = file.content || '';
16
+ cjsPath = path;
17
+ }
18
+ if (path.endsWith('node-ipc.mjs')) {
19
+ mjsContent = file.content || '';
20
+ mjsPath = path;
21
+ }
22
+ }
23
+
24
+ if (cjsContent && !mjsContent) {
25
+ matches.push({
26
+ file: cjsPath,
27
+ finding: 'cjs-present-no-esm',
28
+ detail: 'node-ipc.cjs present but node-ipc.mjs not found — unable to cross-reference size',
29
+ });
30
+ }
31
+
32
+ if (cjsContent && mjsContent) {
33
+ const cjsSize = Buffer.byteLength(cjsContent, 'utf8');
34
+ const mjsSize = Buffer.byteLength(mjsContent, 'utf8');
35
+ const sizeDiff = cjsSize - mjsSize;
36
+
37
+ if (sizeDiff > SIZE_DIFFERENTIAL_THRESHOLD) {
38
+ matches.push({
39
+ file: cjsPath,
40
+ finding: 'size-anomaly',
41
+ cjsSize,
42
+ mjsSize,
43
+ sizeDiff,
44
+ detail: `CJS (${cjsSize} bytes) exceeds ESM (${mjsSize} bytes) by ${sizeDiff} bytes — potential injected payload`,
45
+ });
46
+ }
47
+
48
+ if (IIFE_END_PATTERN.test(cjsContent.trim())) {
49
+ const trimmed = cjsContent.trim();
50
+ const iifeMatch = trimmed.match(IIFE_END_PATTERN);
51
+ if (iifeMatch) {
52
+ matches.push({
53
+ file: cjsPath,
54
+ finding: 'iife-suffix',
55
+ detail: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
56
+ });
57
+ }
58
+ }
59
+ }
60
+
61
+ if (cjsContent && IIFE_END_PATTERN.test(cjsContent.trim())) {
62
+ const alreadyReported = matches.some(m => m.finding === 'iife-suffix');
63
+ if (!alreadyReported) {
64
+ matches.push({
65
+ file: cjsPath,
66
+ finding: 'iife-suffix',
67
+ detail: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
68
+ });
69
+ }
70
+ }
71
+
72
+ return { triggered: matches.length > 0, matches };
73
+ }
@@ -0,0 +1,37 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ const INJECTED_PAYLOAD_HASH = '3427a90c8cb9af764445448648176e120ebc6af0a538158340cf6220de4d01b7';
4
+
5
+ const IIFE_BOUNDARY = /}\)\(\);\s*$/;
6
+
7
+ export function scanInjectedPayloadHash(allFiles) {
8
+ const matches = [];
9
+
10
+ for (const file of allFiles) {
11
+ const path = file.path?.replace(/\\/g, '/') || '';
12
+ if (!path.endsWith('node-ipc.cjs')) continue;
13
+
14
+ const content = file.content || '';
15
+
16
+ if (content.includes(INJECTED_PAYLOAD_HASH)) {
17
+ matches.push({
18
+ file: path,
19
+ finding: 'hash-string-present',
20
+ sha256: INJECTED_PAYLOAD_HASH,
21
+ detail: 'Known injected payload SHA-256 found within node-ipc.cjs content',
22
+ });
23
+ }
24
+
25
+ const fileHash = createHash('sha256').update(content, 'utf8').digest('hex');
26
+ if (fileHash === INJECTED_PAYLOAD_HASH) {
27
+ matches.push({
28
+ file: path,
29
+ finding: 'file-hash-match',
30
+ sha256: fileHash,
31
+ detail: 'node-ipc.cjs SHA-256 matches known injected payload hash',
32
+ });
33
+ }
34
+ }
35
+
36
+ return { triggered: matches.length > 0, matches };
37
+ }
@@ -0,0 +1,49 @@
1
+ const PUBLIC_RESOLVERS = new Set([
2
+ '1.1.1.1',
3
+ '8.8.8.8',
4
+ '8.8.4.4',
5
+ '9.9.9.9',
6
+ ]);
7
+
8
+ const IP_PATTERN = /setServers\(\s*\[?\s*['"`](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})['"`]/;
9
+
10
+ export function scanDnsC2Pattern(allFiles, pkgJson) {
11
+ const matches = [];
12
+
13
+ const sources = [];
14
+
15
+ const scripts = pkgJson?.scripts || {};
16
+ for (const [hook, content] of Object.entries(scripts)) {
17
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
18
+ sources.push({ file: `script:${hook}`, content });
19
+ }
20
+ }
21
+
22
+ for (const file of allFiles) {
23
+ const path = file.path || '';
24
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
25
+ sources.push({ file: path, content: file.content || '' });
26
+ }
27
+
28
+ for (const { file, content } of sources) {
29
+ const hasDnsResolver = /\bdns\.promises\s*\.\s*Resolver\b/.test(content);
30
+ if (!hasDnsResolver) continue;
31
+
32
+ const ipMatch = content.match(IP_PATTERN);
33
+ if (!ipMatch) continue;
34
+
35
+ const customIP = ipMatch[1];
36
+ if (PUBLIC_RESOLVERS.has(customIP)) continue;
37
+
38
+ const hasResolveTxt = /\bresolveTxt\b/.test(content);
39
+
40
+ matches.push({
41
+ file,
42
+ customResolverIP: customIP,
43
+ hasResolveTxt,
44
+ detail: `Custom DNS resolver at ${customIP}${hasResolveTxt ? ' with resolveTxt() — TXT tunneling fingerprint' : ''}`,
45
+ });
46
+ }
47
+
48
+ return { triggered: matches.length > 0, matches };
49
+ }
@@ -0,0 +1,40 @@
1
+ const BOOTSTRAP_DOMAIN = /sh\.azurestaticprovider\.net/i;
2
+ const C2_IP = /37\.16\.75\.69/;
3
+
4
+ export function scanBootstrapResolver(allFiles, pkgJson) {
5
+ const matches = [];
6
+
7
+ const sources = [];
8
+
9
+ const scripts = pkgJson?.scripts || {};
10
+ for (const [hook, content] of Object.entries(scripts)) {
11
+ sources.push({ file: `script:${hook}`, content });
12
+ }
13
+
14
+ for (const file of allFiles) {
15
+ const path = file.path || '';
16
+ sources.push({ file: path, content: file.content || '' });
17
+ }
18
+
19
+ for (const { file, content } of sources) {
20
+ if (BOOTSTRAP_DOMAIN.test(content)) {
21
+ matches.push({
22
+ file,
23
+ finding: 'c2-domain',
24
+ value: 'sh.azurestaticprovider.net',
25
+ detail: 'Bootstrap resolver domain — lookalike, not a Microsoft domain',
26
+ });
27
+ }
28
+
29
+ if (C2_IP.test(content)) {
30
+ matches.push({
31
+ file,
32
+ finding: 'c2-ip',
33
+ value: '37.16.75.69',
34
+ detail: 'Known C2 IP address for DNS TXT tunneling',
35
+ });
36
+ }
37
+ }
38
+
39
+ return { triggered: matches.length > 0, matches };
40
+ }
@@ -0,0 +1,42 @@
1
+ const EXFIL_ZONE = /bt\.node\.js/i;
2
+ const RESOLVE_TXT_DYNAMIC = /\bresolveTxt\s*\(/;
3
+
4
+ export function scanDnsTxtExfil(allFiles, pkgJson) {
5
+ const matches = [];
6
+
7
+ const sources = [];
8
+
9
+ const scripts = pkgJson?.scripts || {};
10
+ for (const [hook, content] of Object.entries(scripts)) {
11
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
12
+ sources.push({ file: `script:${hook}`, content });
13
+ }
14
+ }
15
+
16
+ for (const file of allFiles) {
17
+ const path = file.path || '';
18
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
19
+ sources.push({ file: path, content: file.content || '' });
20
+ }
21
+
22
+ for (const { file, content } of sources) {
23
+ if (EXFIL_ZONE.test(content)) {
24
+ matches.push({
25
+ file,
26
+ finding: 'exfil-zone',
27
+ value: 'bt.node.js',
28
+ detail: 'DNS TXT exfiltration zone reference found',
29
+ });
30
+ }
31
+
32
+ if (RESOLVE_TXT_DYNAMIC.test(content)) {
33
+ matches.push({
34
+ file,
35
+ finding: 'resolve-txt',
36
+ detail: 'resolveTxt() call detected — potential DNS TXT tunneling',
37
+ });
38
+ }
39
+ }
40
+
41
+ return { triggered: matches.length > 0, matches };
42
+ }
@@ -0,0 +1,27 @@
1
+ export function scanRuntimeTrigger(allFiles, pkgJson) {
2
+ const matches = [];
3
+
4
+ const sources = [];
5
+
6
+ const scripts = pkgJson?.scripts || {};
7
+ for (const [hook, content] of Object.entries(scripts)) {
8
+ sources.push({ file: `script:${hook}`, content });
9
+ }
10
+
11
+ for (const file of allFiles) {
12
+ const path = file.path || '';
13
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
14
+ sources.push({ file: path, content: file.content || '' });
15
+ }
16
+
17
+ for (const { file, content } of sources) {
18
+ if (/\bsetImmediate\s*\(/.test(content)) {
19
+ matches.push({
20
+ file,
21
+ detail: 'setImmediate() call found — node-ipc malware fires at require() time, not via postinstall',
22
+ });
23
+ }
24
+ }
25
+
26
+ return { triggered: matches.length > 0, matches };
27
+ }
@@ -0,0 +1,20 @@
1
+ const NT_DIR_PATTERN = /~\/(nt-(?:[\w-]+))\/.*\.tar\.gz/;
2
+
3
+ export function scanTempArtifact(allFiles) {
4
+ const matches = [];
5
+
6
+ for (const file of allFiles) {
7
+ const path = file.path || '';
8
+
9
+ const artifactMatch = path.match(NT_DIR_PATTERN);
10
+ if (artifactMatch) {
11
+ matches.push({
12
+ file: path,
13
+ dirName: artifactMatch[1],
14
+ detail: `Staging artifact found in ~/${artifactMatch[1]}/ — exfil may have been interrupted`,
15
+ });
16
+ }
17
+ }
18
+
19
+ return { triggered: matches.length > 0, matches };
20
+ }
@@ -0,0 +1,93 @@
1
+ import { scanVersionBlocklist } from './d1-version-blocklist.js';
2
+ import { scanTarballHash } from './d2-tarball-hash.js';
3
+ import { scanCjsPayloadInjection } from './d3-cjs-payload-injection.js';
4
+ import { scanInjectedPayloadHash } from './d4-injected-payload-hash.js';
5
+ import { scanDnsC2Pattern } from './d5-dns-c2-pattern.js';
6
+ import { scanBootstrapResolver } from './d6-bootstrap-resolver.js';
7
+ import { scanDnsTxtExfil } from './d7-dns-txt-exfil.js';
8
+ import { scanRuntimeTrigger } from './d8-runtime-trigger.js';
9
+ import { scanTempArtifact } from './d9-temp-artifact.js';
10
+ import { scanUnauthorizedPublisher } from './d10-unauthorized-publisher.js';
11
+ import { scanBlastRadius } from './d11-blast-radius.js';
12
+
13
+ const RULE_SEVERITY = {
14
+ D1: 'critical',
15
+ D2: 'critical',
16
+ D3: 'critical',
17
+ D4: 'critical',
18
+ D5: 'critical',
19
+ D6: 'critical',
20
+ D7: 'critical',
21
+ D8: 'info',
22
+ D9: 'critical',
23
+ D10: 'critical',
24
+ D11: 'critical',
25
+ };
26
+
27
+ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
28
+
29
+ function highestSeverity(severities) {
30
+ for (const s of SEVERITY_ORDER) {
31
+ if (severities.includes(s)) return s;
32
+ }
33
+ return 'none';
34
+ }
35
+
36
+ function buildRemediation(triggered) {
37
+ const lines = [];
38
+ if (triggered.includes('D1') || triggered.includes('D11')) {
39
+ lines.push('Pin node-ipc to safe version: 9.1.5 or 12.0.0');
40
+ }
41
+ if (triggered.includes('D9')) {
42
+ lines.push('PRESERVE ~/nt-*/ artifacts for incident response');
43
+ }
44
+ if (triggered.includes('D5') || triggered.includes('D6') || triggered.includes('D7')) {
45
+ lines.push('Review DNS egress logs for sh.azurestaticprovider.net and 37.16.75.69 post May 14, 2026');
46
+ }
47
+ lines.push('Rotate all CI/CD secrets and OIDC tokens');
48
+ lines.push('Audit maintainer email domain expiry for all critical dependencies');
49
+ return lines.join('. ');
50
+ }
51
+
52
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
53
+ const fileList = allFiles || files || [];
54
+
55
+ const results = {
56
+ D1: scanVersionBlocklist(pkgJson, registryMeta),
57
+ D2: scanTarballHash(fileList),
58
+ D3: scanCjsPayloadInjection(fileList),
59
+ D4: scanInjectedPayloadHash(fileList),
60
+ D5: scanDnsC2Pattern(fileList, pkgJson),
61
+ D6: scanBootstrapResolver(fileList, pkgJson),
62
+ D7: scanDnsTxtExfil(fileList, pkgJson),
63
+ D8: scanRuntimeTrigger(fileList, pkgJson),
64
+ D9: scanTempArtifact(fileList),
65
+ D10: scanUnauthorizedPublisher(pkgJson, registryMeta),
66
+ D11: scanBlastRadius(fileList),
67
+ };
68
+
69
+ const triggered = Object.entries(results)
70
+ .filter(([_, r]) => r.triggered)
71
+ .map(([id]) => id);
72
+
73
+ if (triggered.length === 0) return [];
74
+
75
+ const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
76
+
77
+ const evidence = {
78
+ campaign: 'NODE_IPC_COMPROMISE',
79
+ triggeredRules: triggered,
80
+ details: Object.fromEntries(
81
+ Object.entries(results).filter(([_, r]) => r.triggered)
82
+ ),
83
+ };
84
+
85
+ return [{
86
+ id: 'NODE_IPC_COMPROMISE',
87
+ severity,
88
+ title: 'node-ipc supply chain compromise (May 14, 2026)',
89
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
90
+ evidence: JSON.stringify(evidence),
91
+ mitigation: buildRemediation(triggered),
92
+ }];
93
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "lastUpdated": "2026-05-28T00:00:00.000Z",
3
+ "campaign": {
4
+ "id": "node-ipc-compromise",
5
+ "description": "node-ipc supply chain compromise (May 14, 2026) — 3 malicious versions (9.1.6, 9.2.3, 12.0.1) published via expired maintainer email domain takeover. 80KB credential stealer delivered via DNS TXT tunneling.",
6
+ "firstObserved": "2026-05-14T00:00:00.000Z",
7
+ "attribution": {
8
+ "npmPublisher": "atiertant",
9
+ "c2Domain": "sh.azurestaticprovider.net",
10
+ "c2IP": "37.16.75.69",
11
+ "exfilZone": "bt.node.js"
12
+ }
13
+ },
14
+ "versions": {
15
+ "9.1.6": {
16
+ "tarballSha256": "449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e",
17
+ "safePin": "9.1.5",
18
+ "semverRanges": ["~9.1.x", "^9.1", "^9"]
19
+ },
20
+ "9.2.3": {
21
+ "tarballSha256": "c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea",
22
+ "safePin": "9.1.5",
23
+ "semverRanges": ["~9.2.x", "^9.2"]
24
+ },
25
+ "12.0.1": {
26
+ "tarballSha256": "78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981",
27
+ "safePin": "12.0.0",
28
+ "semverRanges": ["~12.0.x", "^12"]
29
+ }
30
+ },
31
+ "iocs": [
32
+ {
33
+ "type": "publisherAccount",
34
+ "value": "atiertant",
35
+ "ecosystem": "npm",
36
+ "notes": "Unauthorized publisher — no prior history on node-ipc, exclusively used for malicious versions."
37
+ },
38
+ {
39
+ "type": "c2Domain",
40
+ "value": "sh.azurestaticprovider.net",
41
+ "notes": "Bootstrap resolver domain (lookalike, not Microsoft). DNS TXT tunneling C2."
42
+ },
43
+ {
44
+ "type": "c2IP",
45
+ "value": "37.16.75.69",
46
+ "notes": "C2 IP address for DNS TXT tunneling."
47
+ },
48
+ {
49
+ "type": "exfilZone",
50
+ "value": "bt.node.js",
51
+ "notes": "DNS TXT exfiltration zone. Queries encode stolen data into subdomain labels."
52
+ },
53
+ {
54
+ "type": "payloadHash",
55
+ "value": "3427a90c8cb9af764445448648176e120ebc6af0a538158340cf6220de4d01b7",
56
+ "notes": "SHA-256 of injected payload blob within node-ipc.cjs."
57
+ }
58
+ ]
59
+ }
@@ -0,0 +1,20 @@
1
+ const TARGET_EXTENSIONS = ['.md', '.sh', '.json'];
2
+ const TARGET_FILENAMES = new Set(['.cursorrules', 'CLAUDE.md', 'README.md', 'package.json']);
3
+
4
+ export function scanCampaignMarker(allFiles) {
5
+ const matches = [];
6
+ for (const file of allFiles) {
7
+ const path = file.path || '';
8
+ const content = file.content || '';
9
+ const basename = path.split(/[\\/]/).pop();
10
+ const ext = path.includes('.') ? '.' + path.split('.').pop() : '';
11
+
12
+ const isTarget = TARGET_FILENAMES.has(basename) || TARGET_EXTENSIONS.includes(ext);
13
+ if (!isTarget) continue;
14
+
15
+ if (content.includes('P-2024-001')) {
16
+ matches.push({ file: path });
17
+ }
18
+ }
19
+ return { triggered: matches.length > 0, matches };
20
+ }
@@ -0,0 +1,22 @@
1
+ export function scanPayloadFingerprint(allFiles) {
2
+ const matches = [];
3
+ for (const file of allFiles) {
4
+ const path = file.path || '';
5
+ const content = file.content || '';
6
+ const basename = path.split(/[\\/]/).pop();
7
+
8
+ const byteSize = Buffer.byteLength(content, 'utf8');
9
+
10
+ if (basename === 'trap-core.js') {
11
+ matches.push({ file: path, matchType: 'filename', byteSize });
12
+ }
13
+
14
+ if (byteSize === 48485) {
15
+ const alreadyMatched = matches.some(m => m.file === path);
16
+ if (!alreadyMatched) {
17
+ matches.push({ file: path, matchType: 'byteSize', byteSize });
18
+ }
19
+ }
20
+ }
21
+ return { triggered: matches.length > 0, matches };
22
+ }
@@ -0,0 +1,10 @@
1
+ export function scanPublisherBlocklist(pkgJson, registryMeta) {
2
+ const publisherAccount = registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name
3
+ || registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name
4
+ || null;
5
+
6
+ if (publisherAccount === 'asdxzxc') {
7
+ return { triggered: true, publisher: publisherAccount };
8
+ }
9
+ return { triggered: false, publisher: publisherAccount };
10
+ }
@@ -0,0 +1,34 @@
1
+ const CRED_PATH_PATTERNS = /\.aws\/|id_rsa|\.env|keystore|credentials|\.npmrc|\.ssh\//i;
2
+ const C2_PATTERNS = [/ddjidd564\.github\.io/i, /gist\.github\.com/i];
3
+
4
+ function scanContent(content, filePath) {
5
+ const matches = [];
6
+ const hasC2 = C2_PATTERNS.some(p => p.test(content));
7
+ if (!hasC2) return matches;
8
+
9
+ const hasCredPath = CRED_PATH_PATTERNS.test(content);
10
+ if (hasCredPath) {
11
+ matches.push({ file: filePath });
12
+ }
13
+ return matches;
14
+ }
15
+
16
+ export function scanGistsExfil(allFiles, pkgJson) {
17
+ const matches = [];
18
+
19
+ const scripts = pkgJson?.scripts || {};
20
+ for (const [hook, scriptContent] of Object.entries(scripts)) {
21
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
22
+ matches.push(...scanContent(scriptContent, `script:${hook}`));
23
+ }
24
+ }
25
+
26
+ for (const file of allFiles) {
27
+ const path = file.path || '';
28
+ if (path.endsWith('.js') || path.endsWith('.mjs') || path.endsWith('.cjs')) {
29
+ matches.push(...scanContent(file.content || '', path));
30
+ }
31
+ }
32
+
33
+ return { triggered: matches.length > 0, matches };
34
+ }
@@ -0,0 +1,35 @@
1
+ const ZERO_WIDTH_RANGES = [
2
+ [0x200B, 0x200D],
3
+ [0xFEFF, 0xFEFF],
4
+ ];
5
+
6
+ function isZeroWidthChar(code) {
7
+ return ZERO_WIDTH_RANGES.some(([lo, hi]) => code >= lo && code <= hi);
8
+ }
9
+
10
+ const TARGET_FILES = /(^|[\\/])(\.cursorrules|CLAUDE\.md)$/i;
11
+
12
+ export function scanAIPoisoning(allFiles) {
13
+ const matches = [];
14
+
15
+ for (const file of allFiles) {
16
+ const path = file.path?.replace(/\\/g, '/') || '';
17
+ if (!TARGET_FILES.test(path)) continue;
18
+
19
+ const content = file.content || '';
20
+ const found = [];
21
+
22
+ for (let i = 0; i < content.length; i++) {
23
+ const code = content.charCodeAt(i);
24
+ if (isZeroWidthChar(code)) {
25
+ found.push({ char: `U+${code.toString(16).toUpperCase()}`, position: i });
26
+ }
27
+ }
28
+
29
+ if (found.length > 0) {
30
+ matches.push({ file: path, zeroWidthChars: found, count: found.length });
31
+ }
32
+ }
33
+
34
+ return { triggered: matches.length > 0, matches };
35
+ }
@@ -0,0 +1,42 @@
1
+ const LURE_PATTERNS = [
2
+ /solidity/i,
3
+ /defi/i,
4
+ /solana/i,
5
+ /sui\b/i,
6
+ /move-lang/i,
7
+ /^eth-/i,
8
+ /prompt-engineering/i,
9
+ /token-usage/i,
10
+ /dev-env-bootstrap/i,
11
+ ];
12
+
13
+ export function scanLureName(pkgJson, registryMeta) {
14
+ const pkgName = pkgJson?.name || '';
15
+ const matchedPattern = LURE_PATTERNS.find(p => p.test(pkgName));
16
+ if (!matchedPattern) return { triggered: false };
17
+
18
+ const timeMap = registryMeta?.time || {};
19
+ const versions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
20
+ const firstVersion = versions.length > 0
21
+ ? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
22
+ : null;
23
+
24
+ if (!firstVersion) return { triggered: false };
25
+
26
+ const firstPubDate = new Date(timeMap[firstVersion]);
27
+ const now = new Date();
28
+ const daysSinceFirstPub = (now - firstPubDate) / (1000 * 60 * 60 * 24);
29
+
30
+ if (daysSinceFirstPub < 30 && versions.length <= 2) {
31
+ return {
32
+ triggered: true,
33
+ packageName: pkgName,
34
+ matchedPattern: matchedPattern.source,
35
+ firstPublished: timeMap[firstVersion],
36
+ ageDays: Math.round(daysSinceFirstPub),
37
+ versionCount: versions.length,
38
+ };
39
+ }
40
+
41
+ return { triggered: false };
42
+ }
@@ -0,0 +1,22 @@
1
+ export function scanCryptoPrimitives(allFiles, pkgJson) {
2
+ const matches = [];
3
+
4
+ const scripts = pkgJson?.scripts || {};
5
+ const scriptEntries = Object.entries(scripts)
6
+ .filter(([hook]) => /preinstall|install|postinstall|prepare/.test(hook))
7
+ .map(([hook, content]) => ({ file: `script:${hook}`, content }));
8
+
9
+ const jsFiles = allFiles
10
+ .filter(f => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
11
+ .map(f => ({ file: f.path, content: f.content || '' }));
12
+
13
+ for (const { file, content } of [...scriptEntries, ...jsFiles]) {
14
+ const hasFernet = /Fernet/i.test(content);
15
+ const hasECDH = /\bECDH\b|\bcreateECDH\b/i.test(content);
16
+ if (hasFernet && hasECDH) {
17
+ matches.push({ file });
18
+ }
19
+ }
20
+
21
+ return { triggered: matches.length > 0, matches };
22
+ }
@@ -0,0 +1,15 @@
1
+ export function scanXorKey(allFiles) {
2
+ const matches = [];
3
+ for (const file of allFiles) {
4
+ const path = file.path?.replace(/\\/g, '/') || '';
5
+ const isLockFile = /(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(path);
6
+ const isBundled = /\.node$|vendor|native/i.test(path);
7
+ if (!isLockFile && !isBundled) continue;
8
+
9
+ const content = file.content || '';
10
+ if (content.includes('cargo-build-helper-2026')) {
11
+ matches.push({ file: path });
12
+ }
13
+ }
14
+ return { triggered: matches.length > 0, matches };
15
+ }
@@ -0,0 +1,32 @@
1
+ const CRED_VALIDATION_PATTERNS = [
2
+ /sts\.amazonaws\.com/i,
3
+ /api\.github\.com\/user/i,
4
+ ];
5
+
6
+ export function scanCredValidation(allFiles, pkgJson) {
7
+ const matches = [];
8
+
9
+ const scripts = pkgJson?.scripts || {};
10
+ for (const [hook, content] of Object.entries(scripts)) {
11
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
12
+ for (const pattern of CRED_VALIDATION_PATTERNS) {
13
+ if (pattern.test(content)) {
14
+ matches.push({ file: `script:${hook}`, pattern: pattern.source });
15
+ }
16
+ }
17
+ }
18
+ }
19
+
20
+ for (const file of allFiles) {
21
+ const path = file.path || '';
22
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
23
+ const content = file.content || '';
24
+ for (const pattern of CRED_VALIDATION_PATTERNS) {
25
+ if (pattern.test(content)) {
26
+ matches.push({ file: path, pattern: pattern.source });
27
+ }
28
+ }
29
+ }
30
+
31
+ return { triggered: matches.length > 0, matches };
32
+ }
@@ -0,0 +1,77 @@
1
+ import { scanCampaignMarker } from './d1-campaign-marker.js';
2
+ import { scanPayloadFingerprint } from './d2-payload-fingerprint.js';
3
+ import { scanPublisherBlocklist } from './d3-publisher-blocklist.js';
4
+ import { scanGistsExfil } from './d4-gists-exfil.js';
5
+ import { scanAIPoisoning } from './d5-ai-poisoning.js';
6
+ import { scanLureName } from './d6-lure-name.js';
7
+ import { scanCryptoPrimitives } from './d7-crypto-primitives.js';
8
+ import { scanXorKey } from './d8-xor-key.js';
9
+ import { scanCredValidation } from './d9-cred-validation.js';
10
+
11
+ const RULE_SEVERITY = {
12
+ D1: 'critical',
13
+ D2: 'critical',
14
+ D3: 'critical',
15
+ D4: 'critical',
16
+ D5: 'high',
17
+ D6: 'medium',
18
+ D7: 'high',
19
+ D8: 'high',
20
+ D9: 'critical',
21
+ };
22
+
23
+ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
24
+
25
+ function highestSeverity(severities) {
26
+ for (const s of SEVERITY_ORDER) {
27
+ if (severities.includes(s)) return s;
28
+ }
29
+ return 'none';
30
+ }
31
+
32
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
33
+ const fileList = allFiles || files || [];
34
+
35
+ const results = {
36
+ D1: scanCampaignMarker(fileList),
37
+ D2: scanPayloadFingerprint(fileList),
38
+ D3: scanPublisherBlocklist(pkgJson, registryMeta),
39
+ D4: scanGistsExfil(fileList, pkgJson),
40
+ D5: scanAIPoisoning(fileList),
41
+ D6: scanLureName(pkgJson, registryMeta),
42
+ D7: scanCryptoPrimitives(fileList, pkgJson),
43
+ D8: scanXorKey(fileList),
44
+ D9: scanCredValidation(fileList, pkgJson),
45
+ };
46
+
47
+ const triggered = Object.entries(results)
48
+ .filter(([_, r]) => r.triggered)
49
+ .map(([id]) => id);
50
+
51
+ if (triggered.length === 0) return [];
52
+
53
+ const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
54
+
55
+ const evidence = {
56
+ campaign: 'TRAPDOOR',
57
+ triggeredRules: triggered,
58
+ details: Object.fromEntries(
59
+ Object.entries(results).filter(([_, r]) => r.triggered)
60
+ ),
61
+ iocSummary: {
62
+ publisher: 'asdxzxc',
63
+ c2Domain: 'ddjidd564.github.io',
64
+ campaignMarker: 'P-2024-001',
65
+ payloadFile: 'trap-core.js',
66
+ },
67
+ };
68
+
69
+ return [{
70
+ id: 'TRAPDOOR',
71
+ severity,
72
+ title: 'TrapDoor cross-ecosystem supply chain attack campaign',
73
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
74
+ evidence: JSON.stringify(evidence),
75
+ mitigation: 'Block install immediately. Revoke any npm tokens associated with this package. Rotate CI/CD secrets. Audit for postinstall scripts accessing credentials. Check for AI config poisoning (.cursorrules/CLAUDE.md). Verify all package versions from publisher asdxzxc. If confirmed compromise, follow incident response procedures per SECURITY.md.',
76
+ }];
77
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "lastUpdated": "2026-05-28T00:00:00.000Z",
3
+ "campaign": {
4
+ "id": "trapdoor",
5
+ "description": "TrapDoor cross-ecosystem supply chain attack (npm, PyPI, Crates.io) targeting crypto, DeFi, Solana, and AI developer communities.",
6
+ "firstObserved": "2026-05-22T00:00:00.000Z",
7
+ "attribution": {
8
+ "npmPublisher": "asdxzxc",
9
+ "githubAccount": "ddjidd564",
10
+ "githubPages": "ddjidd564.github.io"
11
+ }
12
+ },
13
+ "iocs": [
14
+ {
15
+ "type": "publisherAccount",
16
+ "value": "asdxzxc",
17
+ "ecosystem": "npm",
18
+ "notes": "TrapDoor campaign — known malicious npm publisher account."
19
+ },
20
+ {
21
+ "type": "campaignMarker",
22
+ "value": "P-2024-001",
23
+ "notes": "Hardcoded campaign marker found in package files (README.md, .cursorrules, CLAUDE.md)."
24
+ },
25
+ {
26
+ "type": "payloadFilename",
27
+ "value": "trap-core.js",
28
+ "notes": "Shared payload filename in TrapDoor packages."
29
+ },
30
+ {
31
+ "type": "payloadSize",
32
+ "value": 48485,
33
+ "notes": "Exact byte size of trap-core.js payload."
34
+ },
35
+ {
36
+ "type": "xorKey",
37
+ "value": "cargo-build-helper-2026",
38
+ "notes": "XOR key string found in Crates.io / cross-ecosystem packages."
39
+ },
40
+ {
41
+ "type": "c2Domain",
42
+ "value": "ddjidd564.github.io",
43
+ "notes": "GitHub Pages C2 infrastructure."
44
+ },
45
+ {
46
+ "type": "gistDomain",
47
+ "value": "gist.github.com",
48
+ "notes": "GitHub Gist used for exfiltration."
49
+ }
50
+ ]
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lateos/npm-scan",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "description": "Modern npm supply chain security scanner — detects obfuscated payloads, credential stealers, conditional triggers, sandbox evasion, and worm-like propagation. 11 attack types, SBOM, NIST/EU CRA compliance reporting.",
5
5
  "main": "backend/index.js",
6
6
  "bin": {