Skip to content

Commit ddf0494

Browse files
committed
Add multi-ecosystem support
1 parent f75403d commit ddf0494

File tree

13 files changed

+1065
-48
lines changed

13 files changed

+1065
-48
lines changed

README.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ A security tool that detects malicious packages from external vulnerability feed
1313
## Table of Contents
1414

1515
- [Features](#️-features)
16+
- [Ecosystem Support](#️-ecosystem-support)
17+
- [Blocking Pattern Examples](#blocking-pattern-examples)
18+
- [Multi-Ecosystem Usage](#multi-ecosystem-usage)
1619
- [Package Blocking & Security](#-package-blocking--security)
1720
- [How Exclusion Patterns Work](#how-exclusion-patterns-work)
1821
- [Blocking Commands](#blocking-commands)
@@ -44,6 +47,7 @@ A security tool that detects malicious packages from external vulnerability feed
4447

4548
## 🛡️ Features
4649

50+
- **Multi-Ecosystem Support**: Supports scanning and blocking across 10 major package ecosystems
4751
- **OSV Feed Integration**: Fetches malicious package data from Google Cloud Storage OSV vulnerability database
4852
- **JFrog Artifactory Search**: Searches for packages in your Artifactory repositories using AQL (Artifactory Query Language)
4953
- **Security Cross-Reference**: Compares OSV malicious packages against your JFrog repositories to identify potential threats
@@ -54,6 +58,63 @@ A security tool that detects malicious packages from external vulnerability feed
5458
- **Rich CLI Interface**: Interactive command-line interface with progress bars and formatted output
5559
- **Comprehensive Health Checks**: Validates connectivity to OSV and JFrog services
5660

61+
## 📦 Ecosystem Support
62+
63+
Malifiscan supports 10 major package ecosystems with varying levels of OSV scanning, JFrog searching, and blocking capabilities:
64+
65+
| Ecosystem | OSV Scanning | JFrog Scanning | JFrog Blocking | Notes |
66+
|-------------|--------------|----------------|----------------|-------|
67+
| **npm** | ✅ Full | ✅ Full | ✅ Full | Complete support with scoped packages |
68+
| **PyPI** | ✅ Full | ✅ Full | ✅ Full | Complete support with normalized names |
69+
| **Maven** | ✅ Full | ✅ Full | ✅ Full | Complete GAV (GroupId:ArtifactId:Version) support |
70+
| **Go** | ✅ Full | ✅ Full | ✅ Full | Complete module path and version support |
71+
| **NuGet** | ✅ Full | ✅ Full | ✅ Full | Complete package ID and version support |
72+
| **RubyGems** | ✅ Full | ✅ Full | ✅ Basic | Standard gem file patterns |
73+
| **crates.io** | ✅ Full | ✅ Full | ✅ Basic | Standard crate file patterns |
74+
| **Packagist** | ✅ Full | ✅ Full | ✅ Basic | Composer vendor/package patterns |
75+
| **Pub** | ✅ Full | ✅ Full | ⚠️ Limited | Generic patterns only (Dart/Flutter) |
76+
| **Hex** | ✅ Full | ✅ Full | ⚠️ Limited | Generic patterns only (Elixir) |
77+
78+
**Legend:**
79+
- **OSV Scanning**: Fetches malicious package data from OSV vulnerability database
80+
- **JFrog Scanning**: Searches for packages in JFrog Artifactory repositories using AQL
81+
- **JFrog Blocking**: Creates exclusion patterns to block package downloads
82+
- **✅ Full**: Complete blocking support with ecosystem-specific patterns
83+
- **✅ Basic**: Good support with standard file patterns
84+
- **⚠️ Limited**: Basic patterns with limited blocking effectiveness
85+
86+
### Blocking Pattern Examples
87+
88+
Different ecosystem support levels create different types of exclusion patterns when blocking packages:
89+
90+
**✅ Full Support (Precise Patterns)**
91+
- **npm**: `axios/-/axios-1.12.2.tgz` (targets exact tarball file)
92+
- **PyPI**: `simple/requests/requests-2.28.1*` (follows PyPI simple API structure)
93+
- **Maven**: `com/example/evil-lib/1.0.0/**` (follows GAV structure: GroupId/ArtifactId/Version)
94+
- **Go**: `github.com/user/module/@v/v1.2.3*` (follows Go module proxy structure)
95+
- **NuGet**: `packagename/1.0.0/**` (follows NuGet repository layout)
96+
97+
**✅ Basic Support (Standard Patterns)**
98+
- **RubyGems**: `gems/package-name-1.0.0.gem` (standard gem file format)
99+
- **crates.io**: `crates/package-name/package-name-1.0.0.crate` (standard crate format)
100+
- **Packagist**: `vendor/package/1.0.0/**` (Composer vendor/package structure)
101+
102+
**⚠️ Limited Support (Generic Patterns)**
103+
- **Pub/Hex**: `**/package-name-1.0.0*` or `**/package-name/**` (broad wildcards)
104+
105+
The blocking effectiveness decreases from **Full** (surgical precision) to **Limited** (broad patterns that might block more than intended or miss some variations).
106+
107+
### Multi-Ecosystem Usage
108+
109+
```bash
110+
# Scan all available ecosystems (default behavior)
111+
uv run python cli.py scan crossref
112+
113+
# Scan specific ecosystem only
114+
uv run python cli.py scan crossref --ecosystem npm
115+
uv run python cli.py scan crossref --ecosystem PyPI
116+
```
117+
57118
## 🚫 Package Blocking & Security
58119

59120
Malifiscan can automatically block malicious packages in your JFrog Artifactory repositories using **exclusion patterns**. This prevents developers from downloading compromised packages while preserving existing patterns.
@@ -218,9 +279,6 @@ uv run python cli.py registry list-blocked npm
218279
uv run python cli.py scan crossref
219280
uv run python cli.py scan crossref --hours 24
220281

221-
# Test security scan
222-
uv run python cli.py scan test
223-
224282
# Interactive mode
225283
uv run python cli.py interactive
226284
```
@@ -262,12 +320,6 @@ python cli.py scan crossref --hours 6 --ecosystem npm --limit 100
262320
```
263321
Fetches malicious packages from OSV (last 6 hours by default) and searches for them in your JFrog repositories.
264322

265-
**Test Security Scan**
266-
```bash
267-
python cli.py scan test
268-
```
269-
Runs a test scan with known packages to validate the system.
270-
271323
#### Using Docker
272324

273325
Replace `python cli.py` with `docker run --env-file .env rotemreiss/malifiscan python cli.py` for any command. Add `-v $(pwd)/data:/app/data` for persistent storage.
@@ -508,4 +560,4 @@ This project is licensed under the MIT License - see the LICENSE file for detail
508560

509561
---
510562

511-
**⚠️ Note**: This tool is for security assessment purposes. Always validate results before taking action on package repositories.
563+
**⚠️ Note**: This tool is provided as-is for security assessment purposes - users are responsible for testing and validating all results before taking any action, and the author assumes no responsibility for issues arising from its use. 🤷

cli.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,17 @@ async def registry_search(self, package_name: str, ecosystem: str = "npm") -> bo
271271
await registry.close()
272272
return False
273273

274-
async def security_crossref(self, hours: int = 6, ecosystem: str = "npm", limit: Optional[int] = None, no_report: bool = False, block: bool = False, no_notifications: bool = False) -> bool:
274+
async def security_crossref(self, hours: int = 6, ecosystem: Optional[str] = None, limit: Optional[int] = None, no_report: bool = False, block: bool = False, no_notifications: bool = False) -> bool:
275275
"""Cross-reference malicious packages from feed with package registry."""
276276
try:
277277
self.console.print(f"🔍 Security Cross-Reference Analysis", style="bold cyan")
278278
self.console.print(f"📅 Looking for malicious packages from the last {hours} hours")
279-
self.console.print(f"🏗️ Ecosystem: {ecosystem}")
279+
280+
if ecosystem:
281+
self.console.print(f"🏗️ Ecosystem: {ecosystem}")
282+
else:
283+
self.console.print(f"🏗️ Ecosystems: All available (registry-first optimization)")
284+
280285
if block:
281286
self.console.print(f"🚫 Block mode: Will proactively block malicious packages", style="bold red")
282287
self.console.print()
@@ -310,10 +315,20 @@ async def security_crossref(self, hours: int = 6, ecosystem: str = "npm", limit:
310315
malicious_packages = fetch_result["packages"]
311316

312317
if not malicious_packages:
313-
self.console.print(f"✅ No malicious {ecosystem} packages found in the last {hours} hours", style="green")
318+
ecosystem_desc = ecosystem if ecosystem else "packages across all ecosystems"
319+
self.console.print(f"✅ No malicious {ecosystem_desc} found in the last {hours} hours", style="green")
314320
return True
315321

316-
self.console.print(f"📦 Found {len(malicious_packages)} malicious {ecosystem} packages from feed", style="green")
322+
# Create ecosystem description for display
323+
ecosystem_desc = ecosystem if ecosystem else ""
324+
self.console.print(f"📦 Found {len(malicious_packages)} malicious packages from feed{' (' + ecosystem + ')' if ecosystem else ''}", style="green")
325+
326+
# Show ecosystem breakdown if available and not filtering by single ecosystem
327+
if not ecosystem and fetch_result.get("ecosystems"):
328+
ecosystems = fetch_result["ecosystems"]
329+
for eco, count in ecosystems.items():
330+
self.console.print(f" • {eco}: {count} packages", style="blue")
331+
self.console.print() # Add blank line for better readability
317332

318333
# Step 2: Block packages (if selected)
319334
if block:
@@ -392,6 +407,14 @@ async def security_crossref(self, hours: int = 6, ecosystem: str = "npm", limit:
392407
self.console.print("🛡️ SECURITY ANALYSIS RESULTS", style="bold cyan")
393408
self.console.print("="*80, style="bold")
394409

410+
# Display ecosystem information
411+
ecosystems_scanned = analysis_result.get("ecosystems_scanned", [])
412+
if ecosystems_scanned:
413+
self.console.print(f"🏗️ Ecosystems scanned: {', '.join(ecosystems_scanned)}", style="blue")
414+
if len(ecosystems_scanned) > 1:
415+
self.console.print(f"📊 Multi-ecosystem analysis with registry-first optimization", style="blue")
416+
self.console.print()
417+
395418
# Get registry name and dynamic field names
396419
registry_name = await self._get_registry_name()
397420
field_names = await self._get_dynamic_field_names()
@@ -1190,7 +1213,7 @@ async def main():
11901213

11911214
crossref_parser = scan_subparsers.add_parser("crossref", help="Cross-reference malicious packages from feed with package registry")
11921215
crossref_parser.add_argument("--hours", type=int, default=6, help="Hours ago to look for recent malicious packages (default: 6)")
1193-
crossref_parser.add_argument("--ecosystem", default="npm", help="Package ecosystem (default: npm)")
1216+
crossref_parser.add_argument("--ecosystem", help="Package ecosystem (default: all available ecosystems)")
11941217
crossref_parser.add_argument("--limit", type=int, help="Maximum number of malicious packages to check (default: no limit)")
11951218
crossref_parser.add_argument("--no-report", action="store_true", help="Skip saving scan report to storage")
11961219
crossref_parser.add_argument("--block", action="store_true", help="Block malicious packages from OSV feed before searching (default: false)")

src/core/entities/scan_result.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ScanResult:
2828
malicious_packages_list: List[MaliciousPackage] # Previously packages_already_present
2929
errors: List[str]
3030
execution_duration_seconds: float
31+
ecosystems_scanned: Optional[List[str]] = None # Multi-ecosystem support
3132

3233
@property
3334
def has_new_threats(self) -> bool:

src/core/interfaces/packages_feed.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ class PackagesFeed(ABC):
1010
"""Abstract interface for malicious packages feed providers."""
1111

1212
@abstractmethod
13-
async def fetch_malicious_packages(self, max_packages: Optional[int] = None, hours: Optional[int] = None) -> List[MaliciousPackage]:
13+
async def fetch_malicious_packages(self, max_packages: Optional[int] = None, hours: Optional[int] = None, ecosystems: Optional[List[str]] = None) -> List[MaliciousPackage]:
1414
"""
1515
Fetch list of malicious packages from the feed.
1616
1717
Args:
1818
max_packages: Maximum number of packages to fetch (None for all)
1919
hours: Fetch packages modified within the last N hours (None for all time)
20+
ecosystems: List of ecosystems to fetch (None for all available ecosystems)
2021
2122
Returns:
2223
List of MaliciousPackage entities
@@ -26,6 +27,19 @@ async def fetch_malicious_packages(self, max_packages: Optional[int] = None, hou
2627
"""
2728
pass
2829

30+
@abstractmethod
31+
async def get_available_ecosystems(self) -> List[str]:
32+
"""
33+
Get list of available ecosystems in the feed.
34+
35+
Returns:
36+
List of ecosystem names available in the feed
37+
38+
Raises:
39+
FeedError: If the feed cannot be accessed
40+
"""
41+
pass
42+
2943
@abstractmethod
3044
async def health_check(self) -> bool:
3145
"""

0 commit comments

Comments
 (0)