Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7c1201da3 | |||
| ad96c70c6a | |||
| baf5c0277a | |||
| 48724d860e | |||
| 7a6765aecf | |||
| 7c32ae0782 | |||
| 55a6477fbe |
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 3
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aegisaur"
|
name = "aegisaur"
|
||||||
version = "0.1.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "aegisaur"
|
name = "aegisaur"
|
||||||
version = "0.1.0"
|
version = "2.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"
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# AegisAUR v2.0.0 👻
|
||||||
|
|
||||||
|
**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/main.tar.gz
|
||||||
|
tar xzf main.tar.gz && cd aegisaur
|
||||||
|
cargo build --release
|
||||||
|
sudo cp target/release/aegisaur /usr/local/bin/
|
||||||
|
sudo aegisaur install-hook
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Features v2.0.0
|
||||||
|
|
||||||
|
### Multi-Source Threat Intelligence
|
||||||
|
- **HedgeDoc (Live)**: Sofort aktuell
|
||||||
|
- **CISA KEV**: US Government Advisories
|
||||||
|
- **Arch Security**: Offizielle Arch Linux Advisories
|
||||||
|
- **Atomic Arch Gist**: Community-Listen
|
||||||
|
|
||||||
|
### Erweiterte Erkennung
|
||||||
|
- **12 Threat-Typen**: Rootkit, Cryptominer, Backdoor, Ransomware, Infostealer, etc.
|
||||||
|
- **AUR-Spezifisch**: Typosquatting, Orphan-Takeover, Maintainer-Kompromittierung
|
||||||
|
- **CVE-Tracking**: Verknüpfung mit bekannten CVEs
|
||||||
|
- **Advisory-URLs**: Direkte Links zu Security-Advisories
|
||||||
|
|
||||||
|
### Smartes Scoring
|
||||||
|
- **AUR vs. Offizielles Repo**: Keine False-Positives für Repo-Pakete
|
||||||
|
- **Trust-Scoring**: 0-100 mit 12 Heuristiken
|
||||||
|
- **Cache**: 5-Minuten-TTL für maximale Aktualität
|
||||||
|
|
||||||
|
### Cache-Status (v2.0.1+)
|
||||||
|
```bash
|
||||||
|
aegisaur check-ioc
|
||||||
|
🛡️ Prüfe IOC-Listen: all
|
||||||
|
📦 1931 IOCs aus Cache (Alter: 2m 30s / TTL: 5m) ← Cache-Hit
|
||||||
|
⚠️ Bedrohungen gefunden: 1
|
||||||
|
🔴 gtkimageview - MaliciousBuildScript
|
||||||
|
```
|
||||||
|
|
||||||
|
Cache-Miss zeigt Live-Reload:
|
||||||
|
```
|
||||||
|
⏰ Cache veraltet (6m 15s alt) — Live-Reload...
|
||||||
|
🟢 1931 IOCs von HedgeDoc (LIVE)
|
||||||
|
🟢 0 IOCs von Arch Security
|
||||||
|
📊 Gesamt: 1931 IOCs aus Quellen: ["hedgedoc_live", "arch_security"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Befehle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aegisaur scan <paket> # Einzelnes Paket (AUR-spezifisch)
|
||||||
|
aegisaur scan-all # Alle AUR-Pakete
|
||||||
|
aegisaur check-ioc # IOC-Listen prüfen
|
||||||
|
sudo aegisaur install-hook # ALPM-Hook installieren
|
||||||
|
sudo aegisaur remove-hook # ALPM-Hook 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.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
|
||||||
|
|||||||
+177
@@ -5,6 +5,9 @@ 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<()> {
|
||||||
@@ -145,3 +148,177 @@ pub fn remove_alpm_hook() -> Result<()> {
|
|||||||
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()
|
||||||
|
}
|
||||||
+301
-54
@@ -63,6 +63,42 @@ impl std::fmt::Display for ConfidenceLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub struct IocFetcher {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
cache_dir: std::path::PathBuf,
|
cache_dir: std::path::PathBuf,
|
||||||
@@ -82,41 +118,89 @@ impl IocFetcher {
|
|||||||
Ok(IocFetcher {
|
Ok(IocFetcher {
|
||||||
client,
|
client,
|
||||||
cache_dir,
|
cache_dir,
|
||||||
cache_ttl: Duration::from_secs(3600),
|
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>> {
|
pub async fn fetch_all_iocs(&self) -> Result<Vec<IocEntry>> {
|
||||||
let mut all_threats = Vec::new();
|
let mut all_threats = Vec::new();
|
||||||
|
let mut sources_used = Vec::new();
|
||||||
|
|
||||||
match self.fetch_atomic_arch_list().await {
|
// 1. HEDGEDOC (Live, primär)
|
||||||
|
match self.fetch_hedgedoc().await {
|
||||||
Ok(threats) => {
|
Ok(threats) => {
|
||||||
info!("{} IOCs von Atomic Arch Gist geladen", threats.len());
|
if !threats.is_empty() {
|
||||||
|
info!("🟢 {} IOCs von HedgeDoc (LIVE)", threats.len());
|
||||||
all_threats.extend(threats);
|
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);
|
||||||
}
|
}
|
||||||
Err(e) => warn!("Konnte Atomic Arch Gist nicht laden: {}", 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 {
|
match self.fetch_suspicious_from_aur().await {
|
||||||
Ok(threats) => {
|
Ok(threats) => {
|
||||||
info!("{} suspicious Pakete von AUR RPC", threats.len());
|
if !threats.is_empty() {
|
||||||
|
info!("🟡 {} suspicious Pakete von AUR RPC", threats.len());
|
||||||
all_threats.extend(threats);
|
all_threats.extend(threats);
|
||||||
|
sources_used.push("aur_rpc");
|
||||||
}
|
}
|
||||||
Err(e) => warn!("Konnte AUR RPC nicht abfragen: {}", e),
|
}
|
||||||
|
Err(e) => debug!("AUR RPC fehlgeschlagen: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.fetch_community_blocklist().await {
|
info!("📊 Gesamt: {} IOCs aus Quellen: {:?}", all_threats.len(), sources_used);
|
||||||
Ok(threats) => {
|
|
||||||
info!("{} Einträge von Community Blocklist", threats.len());
|
|
||||||
all_threats.extend(threats);
|
|
||||||
}
|
|
||||||
Err(e) => warn!("Konnte Community Blocklist nicht laden: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Cache speichern
|
||||||
self.save_cache(&all_threats).await?;
|
self.save_cache(&all_threats).await?;
|
||||||
|
|
||||||
Ok(all_threats)
|
Ok(all_threats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prüft Cache auf Aktualität
|
||||||
pub async fn get_cached_iocs(&self) -> Result<Vec<IocEntry>> {
|
pub async fn get_cached_iocs(&self) -> Result<Vec<IocEntry>> {
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
let cache_file = self.cache_dir.join("iocs.json");
|
||||||
|
|
||||||
@@ -132,37 +216,132 @@ impl IocFetcher {
|
|||||||
let content = fs::read_to_string(&cache_file).await?;
|
let content = fs::read_to_string(&cache_file).await?;
|
||||||
let iocs: Vec<IocEntry> = serde_json::from_str(&content)
|
let iocs: Vec<IocEntry> = serde_json::from_str(&content)
|
||||||
.context("Konnte Cache nicht parsen")?;
|
.context("Konnte Cache nicht parsen")?;
|
||||||
info!("{} IOCs aus Cache geladen", iocs.len());
|
|
||||||
|
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);
|
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
|
self.fetch_all_iocs().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_atomic_arch_list(&self) -> Result<Vec<IocEntry>> {
|
// === INDIVIDUELLE FETCHER ===
|
||||||
let url = "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw";
|
|
||||||
|
/// 1. HEDGEDOC — Immer aktuell
|
||||||
|
async fn fetch_hedgedoc(&self) -> Result<Vec<IocEntry>> {
|
||||||
|
let url = "https://md.archlinux.org/s/SxbqukK6IA";
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(url)
|
.get(url)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("HTTP Request fehlgeschlagen")?;
|
.context("HedgeDoc Request fehlgeschlagen")?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
anyhow::bail!("HTTP {} von {}", response.status(), url);
|
anyhow::bail!("HedgeDoc HTTP {}", response.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
let mut threats = Vec::new();
|
let mut threats = Vec::new();
|
||||||
|
|
||||||
|
// HedgeDoc Format: Markdown-Liste mit Paketnamen
|
||||||
|
// Format: `- paketname` oder `paketname` pro Zeile
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
|
||||||
|
// Ü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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed.starts_with('[') || trimmed.starts_with('{') {
|
// 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) {
|
if let Ok(json_list) = serde_json::from_str::<Vec<String>>(trimmed) {
|
||||||
for pkg in json_list {
|
for pkg in json_list {
|
||||||
threats.push(IocEntry {
|
threats.push(IocEntry {
|
||||||
@@ -170,18 +349,19 @@ impl IocFetcher {
|
|||||||
threat_type: ThreatType::MaliciousBuildScript,
|
threat_type: ThreatType::MaliciousBuildScript,
|
||||||
source: "atomic_arch_gist".to_string(),
|
source: "atomic_arch_gist".to_string(),
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||||
description: "Atomic Arch Supply Chain Attack".to_string(),
|
description: "Atomic Arch Supply Chain Attack — Gist Fallback".to_string(),
|
||||||
confidence: ConfidenceLevel::Critical,
|
confidence: ConfidenceLevel::Critical,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if !trimmed.contains(" ") && !trimmed.contains("/") && trimmed.len() < 100 {
|
||||||
|
// Einzelne Paketnamen
|
||||||
threats.push(IocEntry {
|
threats.push(IocEntry {
|
||||||
package_name: trimmed.to_string(),
|
package_name: trimmed.to_string(),
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
threat_type: ThreatType::MaliciousBuildScript,
|
||||||
source: "atomic_arch_gist".to_string(),
|
source: "atomic_arch_gist".to_string(),
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||||
description: "Atomic Arch Supply Chain Attack".to_string(),
|
description: "Atomic Arch Supply Chain Attack — Gist Fallback".to_string(),
|
||||||
confidence: ConfidenceLevel::Critical,
|
confidence: ConfidenceLevel::Critical,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,48 +370,93 @@ impl IocFetcher {
|
|||||||
Ok(threats)
|
Ok(threats)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_suspicious_from_aur(&self) -> Result<Vec<IocEntry>> {
|
/// 3. ARCH SECURITY TRACKER — Offiziell
|
||||||
Ok(Vec::new())
|
async fn fetch_arch_security(&self) -> Result<Vec<IocEntry>> {
|
||||||
}
|
let url = "https://security.archlinux.org/advisory/atomic-arch/json";
|
||||||
|
|
||||||
async fn fetch_community_blocklist(&self) -> Result<Vec<IocEntry>> {
|
let response = self.client
|
||||||
let url = "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt";
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
let response = match self.client.get(url).send().await {
|
match response {
|
||||||
Ok(r) => r,
|
Ok(resp) => {
|
||||||
Err(_) => {
|
if resp.status().is_success() {
|
||||||
debug!("Community Blocklist nicht erreichbar");
|
let advisory: serde_json::Value = resp.json().await?;
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = response.text().await?;
|
|
||||||
let mut threats = Vec::new();
|
let mut threats = Vec::new();
|
||||||
|
|
||||||
for line in text.lines() {
|
// Parsen der Advisory-JSON
|
||||||
let trimmed = line.trim();
|
if let Some(packages) = advisory["packages"].as_array() {
|
||||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
for pkg in packages {
|
||||||
continue;
|
if let Some(name) = pkg.as_str() {
|
||||||
}
|
|
||||||
|
|
||||||
threats.push(IocEntry {
|
threats.push(IocEntry {
|
||||||
package_name: trimmed.to_string(),
|
package_name: name.to_string(),
|
||||||
threat_type: ThreatType::Unknown("community_reported".to_string()),
|
threat_type: ThreatType::MaliciousBuildScript,
|
||||||
source: "community_blocklist".to_string(),
|
source: "arch_security".to_string(),
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
||||||
description: "Community-reported suspicious package".to_string(),
|
description: "Arch Linux Security Advisory — Atomic Arch".to_string(),
|
||||||
confidence: ConfidenceLevel::Medium,
|
confidence: ConfidenceLevel::Critical,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(threats)
|
Ok(threats)
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Ok(Vec::new()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_cache(&self,
|
/// 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]
|
iocs: &[IocEntry]
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
let cache_file = self.cache_dir.join("iocs.json");
|
||||||
@@ -240,7 +465,10 @@ impl IocFetcher {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_package(&self,
|
// === PUBLIC HELPERS ===
|
||||||
|
|
||||||
|
pub fn check_package(
|
||||||
|
&self,
|
||||||
package: &str,
|
package: &str,
|
||||||
iocs: &[IocEntry]
|
iocs: &[IocEntry]
|
||||||
) -> Vec<IocEntry> {
|
) -> Vec<IocEntry> {
|
||||||
@@ -295,4 +523,23 @@ mod tests {
|
|||||||
assert_eq!(cached.len(), 1);
|
assert_eq!(cached.len(), 1);
|
||||||
assert_eq!(cached[0].package_name, "test-pkg");
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+22
-1
@@ -60,6 +60,10 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -67,8 +71,15 @@ 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("aegisaur=info")
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.with_target(false)
|
||||||
|
.with_thread_ids(false)
|
||||||
|
.with_level(true)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -122,6 +133,16 @@ 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?;
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-7
@@ -83,19 +83,56 @@ impl PackageScanner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
pub async fn scan_package(
|
||||||
&self,
|
&self,
|
||||||
package: &str,
|
package: &str,
|
||||||
verbose: bool,
|
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> {
|
) -> Result<ScanResult> {
|
||||||
info!("Scanne Paket: {}", package);
|
info!("Scanne Paket: {}", package);
|
||||||
|
|
||||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
// Prüfe ob Paket in offiziellem Repo oder AUR
|
||||||
let ioc_matches = self.ioc_fetcher.check_package(package, &iocs);
|
let is_aur = self.is_aur_package(package).await;
|
||||||
|
|
||||||
let aur_info = self.fetch_aur_info(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 pkgbuild_analysis = if let Some(ref info) = aur_info {
|
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 {
|
if let Some(url) = &info.url_path {
|
||||||
match self.fetch_pkgbuild(package, url).await {
|
match self.fetch_pkgbuild(package, url).await {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
@@ -115,6 +152,9 @@ impl PackageScanner {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = if let Some((score, pkgbuild_raw)) = pkgbuild_analysis {
|
let mut result = if let Some((score, pkgbuild_raw)) = pkgbuild_analysis {
|
||||||
@@ -153,6 +193,41 @@ impl PackageScanner {
|
|||||||
pkgbuild_raw: if verbose { pkgbuild_raw } else { None },
|
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 {
|
} else {
|
||||||
let installed_info = self.get_installed_info(package).await?;
|
let installed_info = self.get_installed_info(package).await?;
|
||||||
let score = self.trust_scorer.check_installed_package(&installed_info);
|
let score = self.trust_scorer.check_installed_package(&installed_info);
|
||||||
@@ -187,15 +262,25 @@ impl PackageScanner {
|
|||||||
let foreign_packages = self.get_foreign_packages().await?;
|
let foreign_packages = self.get_foreign_packages().await?;
|
||||||
let mut results = Vec::with_capacity(foreign_packages.len());
|
let mut results = Vec::with_capacity(foreign_packages.len());
|
||||||
|
|
||||||
for (pkg, _) in foreign_packages {
|
// SEQUENZIELLE SCANS mit gecachten IOCs
|
||||||
match self.scan_package(&pkg, verbose).await {
|
// (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());
|
||||||
|
|
||||||
|
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) => {
|
Ok(result) => {
|
||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Fehler beim Scannen von {}: {}", pkg, e);
|
warn!("Fehler beim Scannen von {}: {}", pkg, e);
|
||||||
results.push(ScanResult {
|
results.push(ScanResult {
|
||||||
package: pkg,
|
package: pkg.clone(),
|
||||||
version: String::new(),
|
version: String::new(),
|
||||||
score: 0,
|
score: 0,
|
||||||
max_score: 100,
|
max_score: 100,
|
||||||
@@ -278,6 +363,30 @@ impl PackageScanner {
|
|||||||
Ok(())
|
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(
|
async fn fetch_aur_info(
|
||||||
&self, package: &str
|
&self, package: &str
|
||||||
) -> Result<Option<AurPackageInfo>> {
|
) -> Result<Option<AurPackageInfo>> {
|
||||||
@@ -360,6 +469,49 @@ impl PackageScanner {
|
|||||||
.map(|s| s.trim().to_string())
|
.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 {
|
fn calculate_status(score: u32, has_ioc: bool, has_critical: bool) -> ScanStatus {
|
||||||
if has_ioc {
|
if has_ioc {
|
||||||
return ScanStatus::IOCDetected;
|
return ScanStatus::IOCDetected;
|
||||||
|
|||||||
Reference in New Issue
Block a user