@lateos/npm-scan 0.15.5 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.de.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@lateos/npm-scan?style=flat-square)](https://www.npmjs.com/package/@lateos/npm-scan)
10
10
  [![License](https://img.shields.io/badge/license-Apache%202.0%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSING.md)
11
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
- [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
12
+ [![Tests](https://img.shields.io/badge/tests-459%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
14
  [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![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)
@@ -227,6 +227,7 @@ npm-scan report --pdf # alle Scans (Premium)
227
227
  | **ATK-009** | Bedingte/schlafende Auslöser (CI-Erkennung, zeitbasiert) | Verhaltensbasiert | 🔴 hoch | SR-9.2 |
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
+ | **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 |
230
231
 
231
232
  > **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.
232
233
  > Vollständige Dokumentation der Ausweichfläche und PoC-Beispiele finden Sie in [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md).
@@ -548,7 +549,7 @@ Siehe den obigen [Docker-Schnellstart-Abschnitt](#-lateosnpm-scan-überall-mit-d
548
549
 
549
550
  ### Kostenlose Stufe (ausgeliefert)
550
551
 
551
- - Alle 11 ATK-Detektoren (statisch + verhaltensbasiert)
552
+ - Alle 11 ATK-Detektoren (statisch + verhaltensbasiert) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
552
553
  - SBOM-Ausgabe (CycloneDX + SPDX)
553
554
  - HTML-, Text- und Compliance-Berichte (NIST + EU CRA)
554
555
  - Policy-as-Code-Engine (YAML)
@@ -611,6 +612,10 @@ node --test test/detectors-corpus.test.js
611
612
  - `test/fetch.test.js` — Tarball-Extraktion, Bereinigung temporärer Verzeichnisse
612
613
  - `test/policy-edge-cases.test.js` — Grenzfälle bei Unterdrückung, Überschreibung, Ladevalidierung
613
614
  - `test/report-snapshots.test.js` — HTML/Text/CRA/PDF-Format-Assertions
615
+ - `test/cve-2026-48710-badhost/manifest.test.js` — 13 Python-Manifest-Parsing-Tests (requirements.txt, pyproject.toml, poetry.lock, Version-Grenzfälle)
616
+ - `test/cve-2026-48710-badhost/transitive.test.js` — 7 transitive Abhängigkeitstests (Tier 1/2, fastapi-Version-Gating, Pin-Unterdrückung)
617
+ - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 statische Code-Pattern-Tests (Auth-Kontext, INFO-Durchgriff, Scope-Unterdrückung)
618
+ - `test/cve-2026-48710-badhost/integration.test.js` — 4 Integrationstests (End-to-End-Composite-Findings, sauberes Projekt, keine Python-Dateien)
614
619
  - `test/cli.test.js` — Commander-Integrationstests (Hilfe, Version, Scan, Bericht, Fehlerbehandlung)
615
620
 
616
621
  ### Hilfe benötigt?
package/README.fr.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@lateos/npm-scan?style=flat-square)](https://www.npmjs.com/package/@lateos/npm-scan)
10
10
  [![License](https://img.shields.io/badge/license-Apache%202.0%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSING.md)
11
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
- [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
12
+ [![Tests](https://img.shields.io/badge/tests-459%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
14
  [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![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)
@@ -227,6 +227,7 @@ npm-scan report --pdf # tous les scans (premium)
227
227
  | **ATK-009** | Déclencheurs conditionnels/dormants (détection CI, temporel) | Comportementale | 🔴 élevée | SR-9.2 |
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
+ | **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 |
230
231
 
231
232
  > **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.
232
233
  > Voir [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md) pour la documentation complète de la surface d'évasion et des exemples de PoC.
@@ -548,7 +549,7 @@ Voir la [section Démarrage rapide Docker](#-exécutez-lateosnpm-scan-partout-av
548
549
 
549
550
  ### Niveau gratuit (livré)
550
551
 
551
- - Les 11 détecteurs ATK (statique + comportemental)
552
+ - Les 11 détecteurs ATK (statique + comportemental) + **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
552
553
  - Sortie SBOM (CycloneDX + SPDX)
553
554
  - Rapports HTML, texte et conformité (NIST + EU CRA)
554
555
  - Moteur de politique en tant que code (YAML)
@@ -611,6 +612,10 @@ node --test test/detectors-corpus.test.js
611
612
  - `test/fetch.test.js` — extraction de tarball, nettoyage de répertoire temporaire
612
613
  - `test/policy-edge-cases.test.js` — cas limites dans la suppression, la surcharge, la validation de chargement
613
614
  - `test/report-snapshots.test.js` — assertions de format HTML/texte/CRA/PDF
615
+ - `test/cve-2026-48710-badhost/manifest.test.js` — 13 tests d'analyse de manifeste Python (requirements.txt, pyproject.toml, poetry.lock, cas limites de version)
616
+ - `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
+ - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 tests de motifs de code statiques (contexte auth, passage INFO, suppression scope)
618
+ - `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)
614
619
  - `test/cli.test.js` — tests d'intégration commander (aide, version, scan, rapport, gestion d'erreurs)
615
620
 
616
621
  ### Besoin d'aide ?
package/README.ja.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@lateos/npm-scan?style=flat-square)](https://www.npmjs.com/package/@lateos/npm-scan)
10
10
  [![License](https://img.shields.io/badge/license-Apache%202.0%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSING.md)
11
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
- [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
12
+ [![Tests](https://img.shields.io/badge/tests-459%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
14
  [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![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)
@@ -223,6 +223,7 @@ npm-scan report --pdf # すべてのスキャン(プレミアム
223
223
  | **ATK-009** | 条件付き/潜伏トリガー(CI検出、時間ベース) | 行動 | 🔴 高 | SR-9.2 |
224
224
  | **ATK-010** | サンドボックス回避/アンチ解析 | 行動 | 🟠 中 | SR-10.3 |
225
225
  | **ATK-011** | 推移的伝播(ワーム型横方向拡散) | 行動 | 🔴 高 | SR-11.4 |
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 |
226
227
 
227
228
  > **回避型攻撃の捕捉方法:** ATK-009は`process.env.CI`をチェックする、ホスト名をプローブする、または時間ベースのアクティベーションを使用するパッケージを検出します。ATK-010は`debugger`文、`os.hostname()`プローブ、環境フィンガープリンティングをフラグ付けします。ATK-011はピア依存関係グラフをトレースしてワーム型伝播パターンを検出します。
228
229
  > 完全な回避面のドキュメントとPoC例については、[`docs/attack-taxonomy.md`](docs/attack-taxonomy.md)を参照してください。
@@ -544,7 +545,7 @@ npm-scan report --html > report.html
544
545
 
545
546
  ### 無料版(出荷済み)
546
547
 
547
- - 全11ATK検出器(静的+行動)
548
+ - 全11ATK検出器(静的+行動)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
548
549
  - SBOM出力(CycloneDX + SPDX)
549
550
  - HTML、テキスト、コンプライアンスレポート(NIST + EU CRA)
550
551
  - ポリシー・アズ・コードエンジン(YAML)
@@ -607,6 +608,10 @@ node --test test/detectors-corpus.test.js
607
608
  - `test/fetch.test.js` — tarball抽出、一時ディレクトリクリーンアップ
608
609
  - `test/policy-edge-cases.test.js` — 抑制、上書き、ロード検証のエッジケース
609
610
  - `test/report-snapshots.test.js` — HTML/テキスト/CRA/PDF形式のアサーション
611
+ - `test/cve-2026-48710-badhost/manifest.test.js` — 13のPythonマニフェスト解析テスト(requirements.txt, pyproject.toml, poetry.lock, バージョンエッジケース)
612
+ - `test/cve-2026-48710-badhost/transitive.test.js` — 7の推移的依存関係テスト(Tier 1/2, fastapiバージョンゲーティング, 固定抑制)
613
+ - `test/cve-2026-48710-badhost/codePattern.test.js` — 6の静的コードパターンテスト(authコンテキスト, INFOフォールスルー, scope抑制)
614
+ - `test/cve-2026-48710-badhost/integration.test.js` — 4の統合テスト(エンドツーエンド複合発見項目, クリーンプロジェクト, Pythonファイルなし)
610
615
  - `test/cli.test.js` — commander統合テスト(ヘルプ、バージョン、スキャン、レポート、エラーハンドリング)
611
616
 
612
617
  ### ヘルプが必要ですか?
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-428%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
6
+ [![Tests](https://img.shields.io/badge/tests-459%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,8 @@ 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
+ 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
+
31
33
  **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.
32
34
 
33
35
  **@lateos/npm-scan** was built for this moment.
@@ -50,6 +52,7 @@ The **Megalodon campaign** (2026) alone compromised 5,500+ repositories via fake
50
52
  | Worm campaign detection (Mini Shai-Hulud Wave 1–3) | ❌ | ❌ | ❌ | ✅ |
51
53
  | HF model repo impersonation + README clone | ❌ | ❌ | ❌ | ✅ |
52
54
  | VS Code extension supply chain scan (--vsix) | ❌ | ❌ | ❌ | ✅ |
55
+ | Python vulnerability detection (CVE-2026-48710 BadHost) | ❌ | ❌ | ❌ | ✅ |
53
56
  | Attack taxonomy (ATK series) | ❌ | ❌ | ❌ | ✅ |
54
57
  | SBOM output (CycloneDX + SPDX) | ❌ | ✅ | ❌ | ✅ |
55
58
  | SARIF v2.1 (GitHub Code Scanning) | ❌ | ❌ | ❌ | ✅ |
@@ -72,6 +75,7 @@ The **Megalodon campaign** (2026) alone compromised 5,500+ repositories via fake
72
75
  | 🧬 | **ATK attack taxonomy** | 11 classified attack types with NIST 800-161 mappings — versioned, documented, and PR-able |
73
76
  | 🪱 | **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) |
74
77
  | 🧩 | **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
+ | 🐍 | **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 |
75
79
  | 📦 | **SBOM generation** | CycloneDX 1.5 and SPDX 2.3 with findings embedded as vulnerabilities |
76
80
  | 🔍 | **SARIF output** | GitHub Advanced Security / CodeQL compatible SARIF v2.1 — shows findings directly in Security tab |
77
81
  | 🧾 | **Compliance reporting** | NIST SP 800-161 traceability matrix + EU Cyber Resilience Act mapping (free tier) |
@@ -281,12 +285,14 @@ npm-scan report --pdf # all scans (premium)
281
285
  | **HF_IMPERSONATION** | HuggingFace org spoof detection — Jaro-Winkler similarity against 15 known-good orgs, SimHash README clone detection, artifact mismatch (`.exe`/`.dll` in model repos), postinstall escalation, new-org amplifier | Static + Network (Stage 2) | 🔴 high / ⚫ critical | SR-2.1 |
282
286
  | **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 |
283
287
  | **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
+ | **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 |
284
289
 
285
290
  > **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.
286
291
  > **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.
287
292
  > **HF_IMPERSONATION** detection uses a lazy two-stage evaluation: Stage 1 scans `package.json` scripts and JS/TS sources for HuggingFace references (URLs, `from_pretrained()`, `hub.download()`) and runs Jaro-Winkler similarity against 15 known-good HF orgs — zero network. If spoofs are found, Stage 2 fetches the HF model API, computes SimHash of both READMEs for clone detection, validates artifact type consistency (e.g., `transformers` library with `.exe` files is flagged as critical), applies a new-org amplifier (<30 days), and escalates when the reference appears in a lifecycle script.
288
293
  > **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`.
289
294
  > **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
+ > **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.
290
296
  > See [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md) for full evasion surface documentation and PoC examples.
291
297
 
292
298
  ---
@@ -645,7 +651,7 @@ See the [Docker quick-start section](#-run-lateosnpm-scan-anywhere-with-docker--
645
651
 
646
652
  ### Free tier (shipped)
647
653
 
648
- - 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)
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)
649
655
  - SBOM output (CycloneDX + SPDX)
650
656
  - HTML, text, and compliance reports (NIST + EU CRA)
651
657
  - Policy-as-code engine (YAML)
@@ -724,6 +730,10 @@ node --test test/detectors-corpus.test.js
724
730
  - `test/vsix-scan/known-ioc.test.js` — 4 known IOC tests (extensionId, publisher window, outside window)
725
731
  - `test/vsix-scan/exfil-pattern.test.js` — 5 exfil pattern tests (creds, DNS tunnel, AES+RSA, anti-analysis, silent)
726
732
  - `test/vsix-scan/integration.test.js` — 4 integration tests (Nx Console CRITICAL, safe version clean, orphan commit, skipNetwork)
733
+ - `test/cve-2026-48710-badhost/manifest.test.js` — 13 Python manifest parsing tests (requirements.txt, pyproject.toml, poetry.lock, version edge cases)
734
+ - `test/cve-2026-48710-badhost/transitive.test.js` — 7 transitive dependency tests (Tier 1/2, fastapi version gating, pin suppression)
735
+ - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 static code pattern tests (auth context, INFO fallthrough, scope suppression)
736
+ - `test/cve-2026-48710-badhost/integration.test.js` — 4 integration tests (end-to-end composite findings, clean project, no Python files)
727
737
  - `test/cli.test.js` — commander integration tests (help, version, scan, report, error handling)
728
738
  - `test/cli-lockfile.test.js` — scan-lockfile CLI options, yarn/pnpm/monorepo/watch tests
729
739
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm version](https://img.shields.io/npm/v/@lateos/npm-scan?style=flat-square)](https://www.npmjs.com/package/@lateos/npm-scan)
10
10
  [![License](https://img.shields.io/badge/license-Apache%202.0%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSING.md)
11
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
- [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
12
+ [![Tests](https://img.shields.io/badge/tests-459%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
14
  [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![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)
@@ -227,6 +227,7 @@ npm-scan report --pdf # 所有扫描(高级版)
227
227
  | **ATK-009** | 条件/潜伏触发器(CI 检测、基于时间) | 行为 | 🔴 高 | SR-9.2 |
228
228
  | **ATK-010** | 沙箱逃逸 / 反分析 | 行为 | 🟠 中 | SR-10.3 |
229
229
  | **ATK-011** | 传递性传播(蠕虫式横向扩散) | 行为 | 🔴 高 | SR-11.4 |
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 |
230
231
 
231
232
  > **如何捕获逃避式攻击:** ATK-009 检测检查 `process.env.CI`、探测主机名或使用时间激活的包。ATK-010 标记 `debugger` 语句、`os.hostname()` 探测和环境指纹采集。ATK-011 追踪同级依赖图以检测蠕虫式传播模式。
232
233
  > 完整逃避面文档和 PoC 示例请参阅 [`docs/attack-taxonomy.md`](docs/attack-taxonomy.md)。
@@ -548,7 +549,7 @@ npm-scan report --html > report.html
548
549
 
549
550
  ### 免费版(已发布)
550
551
 
551
- - 全部 11 个 ATK 检测器(静态 + 行为)
552
+ - 全部 11 个 ATK 检测器(静态 + 行为)+ **MEGALODON** + **HF_IMPERSONATION** + **MINI_SHAI_HULUD** + **VSIX_SCAN** + **CVE-2026-48710 (BadHost)**
552
553
  - SBOM 输出(CycloneDX + SPDX)
553
554
  - HTML、文本和合规报告(NIST + EU CRA)
554
555
  - 策略即代码引擎(YAML)
@@ -611,6 +612,10 @@ node --test test/detectors-corpus.test.js
611
612
  - `test/fetch.test.js` — tarball 提取、临时目录清理
612
613
  - `test/policy-edge-cases.test.js` — 抑制、覆盖、加载验证的边缘情况
613
614
  - `test/report-snapshots.test.js` — HTML/文本/CRA/PDF 格式断言
615
+ - `test/cve-2026-48710-badhost/manifest.test.js` — 13 个 Python 清单解析测试(requirements.txt, pyproject.toml, poetry.lock, 版本边界情况)
616
+ - `test/cve-2026-48710-badhost/transitive.test.js` — 7 个传递性依赖测试(Tier 1/2, fastapi 版本门控, 固定版本抑制)
617
+ - `test/cve-2026-48710-badhost/codePattern.test.js` — 6 个静态代码模式测试(auth 上下文, INFO 穿透, scope 抑制)
618
+ - `test/cve-2026-48710-badhost/integration.test.js` — 4 个集成测试(端到端复合发现项, 清洁项目, 无 Python 文件)
614
619
  - `test/cli.test.js` — commander 集成测试(帮助、版本、扫描、报告、错误处理)
615
620
 
616
621
  ### 需要帮助?
@@ -0,0 +1,99 @@
1
+ import { codePatternAuthFinding, codePatternInfoFinding } from './findings.js';
2
+
3
+ const AUTH_CONTEXT_PATHS = [
4
+ 'middleware',
5
+ 'auth',
6
+ 'security',
7
+ 'router',
8
+ 'depends',
9
+ 'guard',
10
+ 'permission',
11
+ ];
12
+
13
+ const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
14
+ const SCOPE_PATH_PATTERN = /request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
15
+
16
+ function hasAuthContext(filePath) {
17
+ const lower = filePath.toLowerCase();
18
+ return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
19
+ }
20
+
21
+ function findFunctionBoundaries(lines) {
22
+ const functions = [];
23
+ let currentFn = null;
24
+ let fnBodyStart = -1;
25
+ let indent = 0;
26
+
27
+ for (let i = 0; i < lines.length; i++) {
28
+ const line = lines[i];
29
+ const defMatch = line.match(/^(def\s+\w+|async\s+def\s+\w+)/);
30
+ if (defMatch) {
31
+ if (currentFn) {
32
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
33
+ }
34
+ currentFn = defMatch[1];
35
+ fnBodyStart = i;
36
+ indent = line.length - line.trimStart().length;
37
+ } else if (currentFn && line.trim() && line.length - line.trimStart().length <= indent && !line.trim().startsWith('#') && !line.trim().startsWith('@')) {
38
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
39
+ currentFn = null;
40
+ }
41
+ }
42
+ if (currentFn) {
43
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: lines.length - 1 });
44
+ }
45
+
46
+ return functions;
47
+ }
48
+
49
+ function hasScopePathInFunction(lines, fnStart, fnEnd) {
50
+ for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
51
+ if (SCOPE_PATH_PATTERN.test(lines[i])) return true;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export function scanCodePatterns(allFiles) {
57
+ const findings = [];
58
+
59
+ for (const file of (allFiles || [])) {
60
+ const content = typeof file.content === 'string' ? file.content : '';
61
+ if (!content) continue;
62
+ const path = file.path || '';
63
+ if (!path.endsWith('.py')) continue;
64
+
65
+ const lines = content.split('\n');
66
+ const isAuthContext = hasAuthContext(path);
67
+ const functions = findFunctionBoundaries(lines);
68
+ const suppressedLines = new Set();
69
+
70
+ for (const fn of functions) {
71
+ if (hasScopePathInFunction(lines, fn.startLine, fn.endLine)) {
72
+ for (let i = fn.startLine; i <= fn.endLine && i < lines.length; i++) {
73
+ if (URL_PATH_PATTERN.test(lines[i])) {
74
+ suppressedLines.add(i + 1);
75
+ }
76
+ URL_PATH_PATTERN.lastIndex = 0;
77
+ }
78
+ }
79
+ }
80
+
81
+ if (isAuthContext) {
82
+ let m;
83
+ while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
84
+ const lineNumber = content.slice(0, m.index).split('\n').length;
85
+ if (suppressedLines.has(lineNumber)) continue;
86
+ findings.push(codePatternAuthFinding(path, lineNumber));
87
+ }
88
+ } else {
89
+ let m;
90
+ while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
91
+ const lineNumber = content.slice(0, m.index).split('\n').length;
92
+ if (suppressedLines.has(lineNumber)) continue;
93
+ findings.push(codePatternInfoFinding(path, lineNumber));
94
+ }
95
+ }
96
+ }
97
+
98
+ return findings;
99
+ }
@@ -0,0 +1,105 @@
1
+ const CVE = 'CVE-2026-48710';
2
+ const NICKNAME = 'BadHost';
3
+ const CVSS = 7.0;
4
+ const REFERENCES = [
5
+ 'https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/',
6
+ 'https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr',
7
+ 'https://badhost.org/',
8
+ 'https://osv.dev/vulnerability/PYSEC-2026-161',
9
+ ];
10
+
11
+ const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
12
+
13
+ const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
14
+
15
+ const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
16
+
17
+ function makeFinding(overrides = {}) {
18
+ return {
19
+ id: CVE,
20
+ severity: 'high',
21
+ title: `${NICKNAME} — ${overrides.source || 'unknown'}`,
22
+ description: '',
23
+ remediation: '',
24
+ evidence: JSON.stringify({
25
+ cve: CVE,
26
+ nickname: NICKNAME,
27
+ cvss: CVSS,
28
+ references: REFERENCES,
29
+ ...overrides,
30
+ }),
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ export function directDependencyFinding(version, specifier) {
36
+ return makeFinding({
37
+ severity: 'high',
38
+ confidence: 'HIGH',
39
+ source: 'direct-dependency',
40
+ title: `${NICKNAME}: Starlette ${version} vulnerable`,
41
+ description: `Starlette ${version} (${specifier}) is vulnerable to CVE-2026-48710 (BadHost) — authentication bypass via Host header injection. Upgrade to starlette >= 1.0.1.`,
42
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
43
+ file: null,
44
+ line: null,
45
+ via: null,
46
+ });
47
+ }
48
+
49
+ export function directDependencyUnpinnedFinding() {
50
+ return makeFinding({
51
+ severity: 'high',
52
+ confidence: 'HIGH',
53
+ source: 'direct-dependency-unpinned',
54
+ title: `${NICKNAME}: Starlette unpinned`,
55
+ description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
56
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
57
+ file: null,
58
+ line: null,
59
+ via: null,
60
+ });
61
+ }
62
+
63
+ export function transitiveDependencyFinding(packageName, tier) {
64
+ const confidence = tier === 1 ? 'HIGH' : 'MEDIUM';
65
+ const tierLabel = tier === 1 ? 'Tier 1' : 'Tier 2';
66
+ return makeFinding({
67
+ severity: 'high',
68
+ confidence,
69
+ source: 'transitive-dependency',
70
+ title: `${NICKNAME}: Transitive via ${packageName}`,
71
+ description: `Starlette not directly pinned; inherited through ${packageName} (${tierLabel}). ${packageName} depends on Starlette — if its version constraint allows Starlette < 1.0.1, your deployment is vulnerable.`,
72
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
73
+ file: null,
74
+ line: null,
75
+ via: packageName,
76
+ });
77
+ }
78
+
79
+ export function codePatternAuthFinding(filePath, lineNumber) {
80
+ return makeFinding({
81
+ severity: 'medium',
82
+ confidence: 'MEDIUM',
83
+ source: 'code-pattern',
84
+ title: `${NICKNAME}: Dangerous path extraction in auth/middleware`,
85
+ description: `request.url.path used in auth/middleware context at ${filePath}:${lineNumber} — use request.scope['path'] instead. The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.`,
86
+ remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
87
+ file: filePath,
88
+ line: lineNumber,
89
+ via: null,
90
+ });
91
+ }
92
+
93
+ export function codePatternInfoFinding(filePath, lineNumber) {
94
+ return makeFinding({
95
+ severity: 'info',
96
+ confidence: 'LOW',
97
+ source: 'code-pattern',
98
+ title: `${NICKNAME}: request.url.path usage detected`,
99
+ description: `request.url.path used at ${filePath}:${lineNumber} — may be influenced by unvalidated Host header in Starlette < 1.0.1. Verify request.scope['path'] is used for security decisions.`,
100
+ remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
101
+ file: filePath,
102
+ line: lineNumber,
103
+ via: null,
104
+ });
105
+ }
@@ -0,0 +1,15 @@
1
+ import { scanFiles } from './manifest.js';
2
+ import { scanTransitive } from './transitive.js';
3
+ import { scanCodePatterns } from './codePattern.js';
4
+
5
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
6
+ const targetFiles = allFiles || files;
7
+
8
+ const manifestFindings = scanFiles(targetFiles);
9
+ const transitiveFindings = scanTransitive(targetFiles);
10
+ const codeFindings = scanCodePatterns(targetFiles);
11
+
12
+ const allFindings = [...manifestFindings, ...transitiveFindings, ...codeFindings];
13
+
14
+ return allFindings;
15
+ }
@@ -0,0 +1,305 @@
1
+ import { directDependencyFinding, directDependencyUnpinnedFinding } from './findings.js';
2
+
3
+ function parseReqTxtLine(line) {
4
+ const trimmed = line.trim();
5
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) return null;
6
+ const idx = trimmed.indexOf('#');
7
+ const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
8
+ if (!spec || !spec.startsWith('starlette')) return null;
9
+
10
+ const eqIdx = spec.indexOf('==');
11
+ const geIdx = spec.indexOf('>=');
12
+ const tildeIdx = spec.indexOf('~=');
13
+ const ltIdx = spec.indexOf('<');
14
+
15
+ if (eqIdx >= 0) {
16
+ const ver = spec.slice(eqIdx + 2).trim();
17
+ return { name: 'starlette', version: ver, specifier: `==${ver}` };
18
+ }
19
+ if (geIdx >= 0) {
20
+ const rest = spec.slice(geIdx + 2).trim();
21
+ const parts = rest.split(',');
22
+ const lower = parts[0]?.trim();
23
+ const upper = parts[1]?.trim();
24
+ let specStr = `>=${lower}`;
25
+ if (upper && upper.startsWith('<')) specStr += `,${upper}`;
26
+ return { name: 'starlette', version: lower, specifier: specStr };
27
+ }
28
+ if (tildeIdx >= 0) {
29
+ const ver = spec.slice(tildeIdx + 2).trim();
30
+ return { name: 'starlette', version: ver, specifier: `~=${ver}` };
31
+ }
32
+ if (ltIdx >= 0) {
33
+ const ver = spec.slice(ltIdx + 1).trim();
34
+ const name = 'starlette';
35
+ return { name, version: ver, specifier: `<${ver}` };
36
+ }
37
+
38
+ const rest = spec.slice('starlette'.length).trim();
39
+ if (!rest) return { name: 'starlette', version: null, specifier: null };
40
+
41
+ if (!rest.includes('=') && !rest.includes('<') && !rest.includes('>') && !rest.includes('~') && !rest.includes('!')) {
42
+ return { name: 'starlette', version: rest, specifier: rest };
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ export function parseRequirementsTxt(content) {
49
+ const lines = content.split('\n');
50
+ for (const line of lines) {
51
+ const result = parseReqTxtLine(line);
52
+ if (result) return result;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function parsePEP440(versionStr) {
58
+ if (!versionStr) return null;
59
+ const clean = versionStr.trim().replace(/^v/, '');
60
+ const parts = clean.split('.');
61
+ return {
62
+ major: parseInt(parts[0], 10) || 0,
63
+ minor: parseInt(parts[1], 10) || 0,
64
+ patch: parseInt(parts[2], 10) || 0,
65
+ };
66
+ }
67
+
68
+ function compareVersions(a, b) {
69
+ if (!a) return 1;
70
+ if (!b) return -1;
71
+ if (a.major !== b.major) return a.major - b.major;
72
+ if (a.minor !== b.minor) return a.minor - b.minor;
73
+ return a.patch - b.patch;
74
+ }
75
+
76
+ const STARLETTE_SAFE = parsePEP440('1.0.1');
77
+
78
+ function isVulnerable(version) {
79
+ if (!version) return true;
80
+ const parsed = parsePEP440(version);
81
+ if (!parsed) return true;
82
+ return compareVersions(parsed, STARLETTE_SAFE) < 0;
83
+ }
84
+
85
+ function findStarletteInTOML(obj) {
86
+ if (!obj || typeof obj !== 'object') return null;
87
+
88
+ const sectionPaths = ['dependencies', 'project.dependencies', 'tool.poetry.dependencies'];
89
+ for (const path of sectionPaths) {
90
+ const parts = path.split('.');
91
+ let ptr = obj;
92
+ let found = true;
93
+ for (const p of parts) {
94
+ if (!ptr || typeof ptr !== 'object') { found = false; break; }
95
+ ptr = ptr[p];
96
+ }
97
+ if (!found || !ptr || typeof ptr !== 'object') continue;
98
+ for (const [key, val] of Object.entries(ptr)) {
99
+ if (key === 'starlette' || key === '"starlette"') {
100
+ const version = typeof val === 'string' ? val : (val?.version || null);
101
+ const specifier = typeof val === 'string' ? val : null;
102
+ return { name: 'starlette', version, specifier };
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function parseTomlSimple(content) {
110
+ const result = {};
111
+ let currentSection = result;
112
+
113
+ for (const line of content.split('\n')) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed || trimmed.startsWith('#')) continue;
116
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
117
+ if (sectionMatch) {
118
+ const parts = sectionMatch[1].split('.');
119
+ let ptr = result;
120
+ for (const p of parts) {
121
+ const key = p.replace(/^"(.*)"$/, '$1').trim();
122
+ if (!ptr[key]) ptr[key] = {};
123
+ ptr = ptr[key];
124
+ }
125
+ currentSection = ptr;
126
+ continue;
127
+ }
128
+ const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
129
+ if (!kvMatch) continue;
130
+ const key = kvMatch[1].trim().replace(/^"(.*)"$/, '$1');
131
+ let val = kvMatch[2].trim();
132
+ if (val.startsWith('"') && val.endsWith('"')) {
133
+ val = val.slice(1, -1);
134
+ } else if (val.startsWith("'") && val.endsWith("'")) {
135
+ val = val.slice(1, -1);
136
+ } else if (val.startsWith('^') || val.startsWith('~') || val.startsWith('>') || val.startsWith('<') || val.startsWith('=') || val.startsWith('!')) {
137
+ val = val.replace(/"/g, '');
138
+ }
139
+ currentSection[key] = val;
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ export function parsePyprojectToml(content) {
146
+ let obj;
147
+ try {
148
+ obj = JSON.parse(content);
149
+ } catch {
150
+ obj = parseTomlSimple(content);
151
+ }
152
+ return findStarletteInTOML(obj);
153
+ }
154
+
155
+ function parsePoetryLockEntry(content) {
156
+ const lines = content.split('\n');
157
+ let inStarlette = false;
158
+ let version = null;
159
+ for (const line of lines) {
160
+ const trimmed = line.trim();
161
+ if (trimmed.startsWith('[[package]]')) {
162
+ inStarlette = false;
163
+ }
164
+ if (trimmed.startsWith('name = "starlette"') || trimmed.startsWith("name = 'starlette'")) {
165
+ inStarlette = true;
166
+ }
167
+ if (inStarlette && trimmed.startsWith('version = ')) {
168
+ const match = trimmed.match(/version\s*=\s*["'](.+?)["']/);
169
+ if (match) version = match[1];
170
+ }
171
+ if (inStarlette && trimmed.startsWith('[[package]]') && trimmed !== '[[package]]') {
172
+ break;
173
+ }
174
+ }
175
+ if (version) {
176
+ return { name: 'starlette', version, specifier: `==${version}` };
177
+ }
178
+ return null;
179
+ }
180
+
181
+ export function parsePoetryLock(content) {
182
+ return parsePoetryLockEntry(content);
183
+ }
184
+
185
+ function parsePipfileEntry(content) {
186
+ try {
187
+ const obj = JSON.parse(content);
188
+ const packages = obj?.packages || {};
189
+ for (const [key, val] of Object.entries(packages)) {
190
+ if (key === 'starlette' || key === '"starlette"') {
191
+ const version = typeof val === 'string' ? val : null;
192
+ return { name: 'starlette', version, specifier: version || null };
193
+ }
194
+ }
195
+ return null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ export function parsePipfile(content) {
202
+ return parsePipfileEntry(content);
203
+ }
204
+
205
+ function parseSetupPyContent(content) {
206
+ const match = content.match(/install_requires\s*=\s*\[([^\]]+)\]/s);
207
+ if (!match) return null;
208
+ const block = match[1];
209
+ const lines = block.split(',').map(l => l.trim().replace(/["']/g, ''));
210
+ for (const line of lines) {
211
+ const clean = line.trim();
212
+ if (!clean) continue;
213
+ if (clean.startsWith('starlette')) {
214
+ const eqIdx = clean.indexOf('==');
215
+ const geIdx = clean.indexOf('>=');
216
+ const tildeIdx = clean.indexOf('~=');
217
+ const ltIdx = clean.indexOf('<');
218
+ let version = null;
219
+ let specifier = null;
220
+ if (eqIdx >= 0) { version = clean.slice(eqIdx + 2).trim(); specifier = `==${version}`; }
221
+ else if (geIdx >= 0) { version = clean.slice(geIdx + 2).split(',')[0].trim(); specifier = `>=${version}`; }
222
+ else if (tildeIdx >= 0) { version = clean.slice(tildeIdx + 2).trim(); specifier = `~=${version}`; }
223
+ else if (ltIdx >= 0) { version = clean.slice(ltIdx + 1).trim(); specifier = `<${version}`; }
224
+ else if (clean === 'starlette') { return { name: 'starlette', version: null, specifier: null }; }
225
+ return { name: 'starlette', version, specifier };
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+
231
+ export function parseSetupPy(content) {
232
+ return parseSetupPyContent(content);
233
+ }
234
+
235
+ function parseSetupCfgContent(content) {
236
+ const lines = content.split('\n');
237
+ let inInstallRequires = false;
238
+ for (const line of lines) {
239
+ const trimmed = line.trim();
240
+ if (trimmed.startsWith('install_requires')) {
241
+ inInstallRequires = true;
242
+ continue;
243
+ }
244
+ if (inInstallRequires) {
245
+ if (trimmed.startsWith('[')) break;
246
+ if (trimmed.startsWith('starlette')) {
247
+ const eqIdx = trimmed.indexOf('==');
248
+ const geIdx = trimmed.indexOf('>=');
249
+ const tildeIdx = trimmed.indexOf('~=');
250
+ const ltIdx = trimmed.indexOf('<');
251
+ let version = null;
252
+ let specifier = null;
253
+ if (eqIdx >= 0) { version = trimmed.slice(eqIdx + 2).trim(); specifier = `==${version}`; }
254
+ else if (geIdx >= 0) { version = trimmed.slice(geIdx + 2).split(',')[0].trim(); specifier = `>=${version}`; }
255
+ else if (tildeIdx >= 0) { version = trimmed.slice(tildeIdx + 2).trim(); specifier = `~=${version}`; }
256
+ else if (ltIdx >= 0) { version = trimmed.slice(ltIdx + 1).trim(); specifier = `<${version}`; }
257
+ else if (trimmed === 'starlette') { return { name: 'starlette', version: null, specifier: null }; }
258
+ if (trimmed.startsWith('starlette')) {
259
+ return { name: 'starlette', version, specifier };
260
+ }
261
+ }
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ export function parseSetupCfg(content) {
268
+ return parseSetupCfgContent(content);
269
+ }
270
+
271
+ export function scanFiles(allFiles) {
272
+ const findings = [];
273
+
274
+ for (const file of (allFiles || [])) {
275
+ const content = typeof file.content === 'string' ? file.content : '';
276
+ if (!content) continue;
277
+ const path = file.path || '';
278
+
279
+ let result = null;
280
+
281
+ if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
282
+ result = parseRequirementsTxt(content);
283
+ } else if (path === 'pyproject.toml') {
284
+ result = parsePyprojectToml(content);
285
+ } else if (path === 'poetry.lock') {
286
+ result = parsePoetryLock(content);
287
+ } else if (path === 'Pipfile' || path === 'Pipfile.lock') {
288
+ result = parsePipfile(content);
289
+ } else if (path === 'setup.py') {
290
+ result = parseSetupPy(content);
291
+ } else if (path === 'setup.cfg') {
292
+ result = parseSetupCfg(content);
293
+ }
294
+
295
+ if (!result) continue;
296
+
297
+ if (result.version === null && result.specifier === null) {
298
+ findings.push(directDependencyUnpinnedFinding());
299
+ } else if (isVulnerable(result.version)) {
300
+ findings.push(directDependencyFinding(result.version, result.specifier || 'unknown'));
301
+ }
302
+ }
303
+
304
+ return findings;
305
+ }
@@ -0,0 +1,189 @@
1
+ import { transitiveDependencyFinding } from './findings.js';
2
+ import { parseRequirementsTxt, parsePyprojectToml, parsePoetryLock, parsePipfile, parseSetupPy, parseSetupCfg } from './manifest.js';
3
+
4
+ const TIER_1_PACKAGES = [
5
+ 'fastapi',
6
+ 'vllm',
7
+ 'litellm',
8
+ 'bentoml',
9
+ 'text-generation-inference',
10
+ 'ray-serve',
11
+ 'ray[serve]',
12
+ ];
13
+
14
+ const TIER_2_PACKAGES = [
15
+ 'langserve',
16
+ 'fastapi-mcp',
17
+ 'mcp',
18
+ 'starlette-admin',
19
+ 'piccolo-api',
20
+ 'fastapi-users',
21
+ 'broadcaster',
22
+ ];
23
+
24
+ function normalizePkgName(name) {
25
+ return name.replace(/["'\[\]]/g, '').trim().toLowerCase();
26
+ }
27
+
28
+ function findPackagesInManifests(allFiles) {
29
+ const packages = new Set();
30
+
31
+ for (const file of (allFiles || [])) {
32
+ const content = typeof file.content === 'string' ? file.content : '';
33
+ if (!content) continue;
34
+ const path = file.path || '';
35
+
36
+ let deps = [];
37
+
38
+ if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
39
+ const lines = content.split('\n');
40
+ for (const line of lines) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
43
+ const idx = trimmed.indexOf('#');
44
+ const spec = idx >= 0 ? trimmed.slice(0, idx).trim() : trimmed;
45
+ const eqIdx = spec.indexOf('==');
46
+ const geIdx = spec.indexOf('>=');
47
+ const name = eqIdx >= 0 ? spec.slice(0, eqIdx).trim() : (geIdx >= 0 ? spec.slice(0, geIdx).trim() : spec);
48
+ if (name && !name.includes('=') && !name.includes('<') && !name.includes('>')) {
49
+ deps.push(normalizePkgName(name));
50
+ }
51
+ }
52
+ } else if (path === 'pyproject.toml') {
53
+ try {
54
+ const obj = JSON.parse(content);
55
+ const allDeps = { ...(obj?.tool?.poetry?.dependencies || {}), ...(obj?.dependencies || {}), ...(obj?.['dev-dependencies'] || {}) };
56
+ for (const key of Object.keys(allDeps)) {
57
+ deps.push(normalizePkgName(key));
58
+ }
59
+ } catch {}
60
+ } else if (path === 'poetry.lock') {
61
+ const pattern = /name\s*=\s*["']([^"']+)["']/g;
62
+ let m;
63
+ while ((m = pattern.exec(content)) !== null) {
64
+ deps.push(normalizePkgName(m[1]));
65
+ }
66
+ } else if (path === 'Pipfile' || path === 'Pipfile.lock') {
67
+ try {
68
+ const obj = JSON.parse(content);
69
+ for (const key of Object.keys(obj?.packages || {})) {
70
+ deps.push(normalizePkgName(key));
71
+ }
72
+ } catch {}
73
+ }
74
+ for (const dep of deps) packages.add(dep);
75
+ }
76
+
77
+ return packages;
78
+ }
79
+
80
+ function hasStarlettePin(allFiles) {
81
+ for (const file of (allFiles || [])) {
82
+ const content = typeof file.content === 'string' ? file.content : '';
83
+ if (!content) continue;
84
+ const path = file.path || '';
85
+
86
+ let result = null;
87
+ if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
88
+ result = parseRequirementsTxt(content);
89
+ } else if (path === 'pyproject.toml') {
90
+ result = parsePyprojectToml(content);
91
+ } else if (path === 'poetry.lock') {
92
+ result = parsePoetryLock(content);
93
+ } else if (path === 'Pipfile' || path === 'Pipfile.lock') {
94
+ result = parsePipfile(content);
95
+ } else if (path === 'setup.py') {
96
+ result = parseSetupPy(content);
97
+ } else if (path === 'setup.cfg') {
98
+ result = parseSetupCfg(content);
99
+ }
100
+
101
+ if (result) {
102
+ if (result.version === null && result.specifier === null) return false;
103
+ const parsed = parsePEP440(result.version);
104
+ const safe = parsePEP440('1.0.1');
105
+ if (parsed && compareVersions(parsed, safe) >= 0) return true;
106
+ }
107
+ }
108
+
109
+ return false;
110
+ }
111
+
112
+ function parsePEP440(versionStr) {
113
+ if (!versionStr) return null;
114
+ const clean = versionStr.trim().replace(/^v/, '');
115
+ const parts = clean.split('.');
116
+ return {
117
+ major: parseInt(parts[0], 10) || 0,
118
+ minor: parseInt(parts[1], 10) || 0,
119
+ patch: parseInt(parts[2], 10) || 0,
120
+ };
121
+ }
122
+
123
+ function compareVersions(a, b) {
124
+ if (!a) return 1;
125
+ if (!b) return -1;
126
+ if (a.major !== b.major) return a.major - b.major;
127
+ if (a.minor !== b.minor) return a.minor - b.minor;
128
+ return a.patch - b.patch;
129
+ }
130
+
131
+ export function scanTransitive(allFiles) {
132
+ const findings = [];
133
+
134
+ if (!allFiles || allFiles.length === 0) return findings;
135
+
136
+ const packages = findPackagesInManifests(allFiles);
137
+
138
+ if (hasStarlettePin(allFiles)) return findings;
139
+
140
+ const handled = new Set();
141
+
142
+ for (const pkg of packages) {
143
+ if (TIER_1_PACKAGES.includes(pkg)) {
144
+ handled.add(pkg);
145
+ if (pkg === 'fastapi') {
146
+ const version = findFastapiVersion(allFiles);
147
+ if (version) {
148
+ const parsed = parsePEP440(version);
149
+ const safeFastapi = parsePEP440('0.116.0');
150
+ if (parsed && compareVersions(parsed, safeFastapi) >= 0) continue;
151
+ }
152
+ }
153
+ findings.push(transitiveDependencyFinding(pkg, 1));
154
+ break;
155
+ }
156
+ }
157
+
158
+ if (findings.length === 0) {
159
+ for (const pkg of packages) {
160
+ if (handled.has(pkg)) continue;
161
+ if (TIER_2_PACKAGES.includes(pkg)) {
162
+ findings.push(transitiveDependencyFinding(pkg, 2));
163
+ break;
164
+ }
165
+ }
166
+ }
167
+
168
+ return findings;
169
+ }
170
+
171
+ function findFastapiVersion(allFiles) {
172
+ for (const file of (allFiles || [])) {
173
+ const content = typeof file.content === 'string' ? file.content : '';
174
+ if (!content) continue;
175
+ const path = file.path || '';
176
+ if (path === 'requirements.txt' || path.match(/^requirements\/.*\.txt$/)) {
177
+ const lines = content.split('\n');
178
+ for (const line of lines) {
179
+ const trimmed = line.trim();
180
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
181
+ if (trimmed.startsWith('fastapi')) {
182
+ const eqIdx = trimmed.indexOf('==');
183
+ if (eqIdx >= 0) return trimmed.slice(eqIdx + 2).trim();
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return null;
189
+ }
@@ -12,6 +12,7 @@ import * as atk011 from './atk-011-transitive-prop.js';
12
12
  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
+ import { scan as badhostScan } from './cve-2026-48710-badhost/index.js';
15
16
 
16
17
  export async function runAll(pkgJson, files = [], registryMeta = null, allFiles = null) {
17
18
  const findings = [];
@@ -29,5 +30,6 @@ export async function runAll(pkgJson, files = [], registryMeta = null, allFiles
29
30
  findings.push(...await megalodonScan(pkgJson, allFiles || files, registryMeta));
30
31
  findings.push(...await hfScan(pkgJson, files, registryMeta, allFiles || files));
31
32
  findings.push(...await miniShaiHuludScan(pkgJson, files, registryMeta, allFiles || files));
33
+ findings.push(...await badhostScan(pkgJson, files, registryMeta, allFiles || files));
32
34
  return findings.sort((a, b) => b.severity.localeCompare(a.severity));
33
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lateos/npm-scan",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
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": {