Compare commits
12 Commits
043f0a2577
..
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a6765aecf | |||
| 7c32ae0782 | |||
| 55a6477fbe | |||
| faba3737f2 | |||
| 974ede8f5b | |||
| 6001eef3d6 | |||
| d560d2f5d3 | |||
| 577e2aba5c | |||
| ec9e0ec7d6 | |||
| 7fc2db44ad | |||
| 45a4282943 | |||
| df7f46a8a2 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "aegisaur"
|
name = "aegisaur"
|
||||||
version = "0.1.0"
|
version = "2.0.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"
|
||||||
|
|||||||
+41
-41
@@ -1,53 +1,41 @@
|
|||||||
# 📦 Installation Guide
|
# 📦 Installation Guide
|
||||||
|
|
||||||
## Schnellstart
|
## ⚡ Schnellstart (empfohlen)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Als AUR-Paket installieren (empfohlen)
|
# 1. Source Tarball herunterladen (enthält ALLE Dateien)
|
||||||
makepkg -si PKGBUILD
|
cd /tmp
|
||||||
|
wget https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/archive/master.tar.gz
|
||||||
|
|
||||||
# Oder systemweit nach /usr/local/bin
|
# 2. Entpacken
|
||||||
sudo cp target/release/aegisaur /usr/local/bin/
|
tar xzf master.tar.gz
|
||||||
sudo chmod +x /usr/local/bin/aegisaur
|
|
||||||
|
|
||||||
# Oder symbolischer Link
|
|
||||||
sudo ln -s $(pwd)/target/release/aegisaur /usr/local/bin/aegisaur
|
|
||||||
```
|
|
||||||
|
|
||||||
## Eigenes AUR-Repository
|
|
||||||
|
|
||||||
### Pfad auf Gitea
|
|
||||||
```
|
|
||||||
https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
|
||||||
```
|
|
||||||
|
|
||||||
### Installation (empfohlen)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/arch_agent_system/.openclaw/workspace/aegisaur
|
|
||||||
makepkg -si
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative: Git-Clone + Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://gitea.die-heimatlosen.eu/arch_agent/aegisaur.git
|
|
||||||
cd aegisaur
|
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
|
cargo build --release
|
||||||
sudo cp target/release/aegisaur /usr/local/bin/
|
sudo cp target/release/aegisaur /usr/local/bin/
|
||||||
sudo aegisaur install-hook
|
sudo aegisaur install-hook
|
||||||
```
|
```
|
||||||
|
|
||||||
### ⚠️ Pacman-Repo Hinweis
|
## ⚠️ WICHTIG: Git-Clone NICHT verwenden!
|
||||||
|
|
||||||
> Ein pacman-Remote (`[aegisaur]` in pacman.conf) braucht eine `.db` Datei, die Gitea nicht automatisch bereitstellt. Nutze stattdessen `makepkg` oder den Release-Download.
|
|
||||||
|
|
||||||
### Release-Download (Fallback)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/releases/download/v0.1.0/aegisaur-0.1.0-x86_64.tar.gz
|
# ❌ NICHT SO - Fehlende Dateien!
|
||||||
tar xzf aegisaur-0.1.0-x86_64.tar.gz
|
git clone https://gitea.die-heimatlosen.eu/arch_agent/aegisaur.git
|
||||||
sudo install -Dm755 aegisaur /usr/bin/aegisaur
|
|
||||||
|
# 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)
|
## ALPM-Hook (systemweit)
|
||||||
@@ -65,16 +53,14 @@ sudo aegisaur remove-hook
|
|||||||
```bash
|
```bash
|
||||||
# Erstellt ~/.config/aegisaur/config.toml
|
# Erstellt ~/.config/aegisaur/config.toml
|
||||||
aegisaur config
|
aegisaur config
|
||||||
|
|
||||||
# Beispiel-Config kopieren
|
|
||||||
cp /usr/share/aegisaur/config.example.toml ~/.config/aegisaur/config.toml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pfad-Übersicht
|
## Pfad-Übersicht
|
||||||
|
|
||||||
| Komponente | Pfad |
|
| Komponente | Pfad |
|
||||||
|------------|------|
|
|------------|------|
|
||||||
| Binary | `/usr/bin/aegisaur` |
|
| Binary (makepkg) | `/usr/bin/aegisaur` |
|
||||||
|
| Binary (manuell) | `/usr/local/bin/aegisaur` |
|
||||||
| ALPM-Hook | `/usr/share/libalpm/hooks/99-aegisaur.hook` |
|
| ALPM-Hook | `/usr/share/libalpm/hooks/99-aegisaur.hook` |
|
||||||
| Hook-Script | `/usr/share/libalpm/hooks/aegisaur-check.sh` |
|
| Hook-Script | `/usr/share/libalpm/hooks/aegisaur-check.sh` |
|
||||||
| Dokumentation | `/usr/share/doc/aegisaur/` |
|
| Dokumentation | `/usr/share/doc/aegisaur/` |
|
||||||
@@ -82,3 +68,17 @@ cp /usr/share/aegisaur/config.example.toml ~/.config/aegisaur/config.toml
|
|||||||
| Cache | `~/.cache/aegisaur/` |
|
| Cache | `~/.cache/aegisaur/` |
|
||||||
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` |
|
| Quellcode | `/home/arch_agent_system/.openclaw/workspace/aegisaur/` |
|
||||||
| Gitea-Repo | `https://gitea.die-heimatlosen.eu/arch_agent/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,82 +1,43 @@
|
|||||||
# AegisAUR 👻
|
# AegisAUR 👻
|
||||||
|
|
||||||
Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete.
|
**Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete**
|
||||||
|
|
||||||
Automatisierter Schutz gegen Supply-Chain-Angriffe wie **Atomic Arch**.
|
Automatisierter Schutz gegen Supply-Chain-Angriffe wie **Atomic Arch**.
|
||||||
|
|
||||||
|
## ⚡ Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tarball herunterladen (Git-Clone hat fehlende Dateien!)
|
||||||
|
wget https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/archive/master.tar.gz
|
||||||
|
tar xzf master.tar.gz && cd aegisaur
|
||||||
|
cargo build --release
|
||||||
|
sudo cp target/release/aegisaur /usr/local/bin/
|
||||||
|
sudo aegisaur install-hook
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🔍 **Live IOC-Abfrage** - Holt aktuelle Threat-Intelligence von Community-Quellen
|
- 🔍 **Live IOC-Abfrage** — Holt aktuelle Threat-Intelligence
|
||||||
- 🛡️ **Trust-Scoring** - Analysiert PKGBUILDs auf verdächtige Muster
|
- 🛡️ **Trust-Scoring** — Analysiert PKGBUILDs auf verdächtige Muster
|
||||||
- ⚡ **ALPM-Hook** - Automatischer Pre-Install-Scan
|
- ⚡ **ALPM-Hook** — Automatischer Pre-Install-Scan
|
||||||
- 📊 **Detallierte Reports** - JSON-Output für Automatisierung
|
- 📊 **JSON-Output** — Für Automatisierung
|
||||||
- 🔴 **Kritische Alerts** - Sofortige Warnung bei IOC-Matches
|
- 🔴 **Kritische Alerts** — Sofortige Warnung bei IOC-Matches
|
||||||
|
|
||||||
## Installation
|
## Befehle
|
||||||
|
|
||||||
### Aus AUR
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yay -S aegisaur
|
aegisaur scan <paket> # Einzelnes Paket scannen
|
||||||
# oder
|
aegisaur scan-all # Alle AUR-Pakete scannen
|
||||||
paru -S aegisaur
|
aegisaur check-ioc # IOC-Listen prüfen
|
||||||
|
sudo aegisaur install-hook # ALPM-Hook installieren
|
||||||
|
sudo aegisaur remove-hook # ALPM-Hook entfernen
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manuel
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install aegisaur
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
### Einzelnes Paket scannen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aegisaur scan paketname
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alle installierten AUR-Pakete scannen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aegisaur scan-all
|
|
||||||
```
|
|
||||||
|
|
||||||
### IOC-Check (wie `aurvulntest`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aegisaur check-ioc
|
|
||||||
```
|
|
||||||
|
|
||||||
### ALPM-Hook installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo aegisaur install-hook
|
|
||||||
```
|
|
||||||
|
|
||||||
## IOC-Quellen
|
|
||||||
|
|
||||||
Alle Quellen sind **ohne Authentifizierung** erreichbar:
|
|
||||||
|
|
||||||
- [Atomic Arch Gist](https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14)
|
|
||||||
- [AUR Community Blocklist](https://github.com/Kidev/AUR-Blocklist)
|
|
||||||
- [Arch Security Advisories](https://security.archlinux.org)
|
|
||||||
|
|
||||||
## Trust-Scoring Kategorien
|
|
||||||
|
|
||||||
| Kategorie | Gewichtung | Beschreibung |
|
|
||||||
|-----------|-----------|--------------|
|
|
||||||
| Shell-Script | 40% | Analyse von PKGBUILD als Shell-Script |
|
|
||||||
| Source-URL | 20% | Verifizierung der Herkunft |
|
|
||||||
| Checksums | 20% | Qualität der Prüfsummen |
|
|
||||||
| Maintainer | 20% | Heuristiken zum Maintainer |
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
MIT - © 2026 Quasi & Thuumate 👻
|
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Gitea: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
- **Gitea:** https://gitea.die-heimatlosen.eu/arch_agent/aegisaur
|
||||||
- Issues: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur/issues
|
- **Docs:** Siehe INSTALL.md, USAGE.md, TODO.md
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT — © 2026 Quasi & Thuumate 👻
|
||||||
|
|||||||
+1
-1
@@ -34,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" >/devdev/null 2>&1; then
|
if pacman -Qi "$package" >/dev/null 2>&1; then
|
||||||
# Paket ist bereits installiert (Upgrade)
|
# Paket ist bereits installiert (Upgrade)
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
+286
-61
@@ -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,74 @@ 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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
all_threats.extend(threats);
|
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);
|
||||||
}
|
}
|
||||||
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() {
|
||||||
all_threats.extend(threats);
|
info!("🟡 {} suspicious Pakete von AUR RPC", threats.len());
|
||||||
|
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,56 +201,70 @@ 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());
|
debug!("{} IOCs aus Cache geladen (Alter: {}s)", iocs.len(), age.num_seconds());
|
||||||
return Ok(iocs);
|
return Ok(iocs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
if let Ok(json_list) = serde_json::from_str::<Vec<String>>(trimmed) {
|
let pkg = if trimmed.starts_with("- ") {
|
||||||
for pkg in json_list {
|
trimmed[2..].trim()
|
||||||
threats.push(IocEntry {
|
} else if trimmed.starts_with("* ") {
|
||||||
package_name: pkg,
|
trimmed[2..].trim()
|
||||||
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".to_string(),
|
|
||||||
confidence: ConfidenceLevel::Critical,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} 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 {
|
threats.push(IocEntry {
|
||||||
package_name: trimmed.to_string(),
|
package_name: pkg.to_string(),
|
||||||
threat_type: ThreatType::MaliciousBuildScript,
|
threat_type: ThreatType::MaliciousBuildScript,
|
||||||
source: "atomic_arch_gist".to_string(),
|
source: "hedgedoc_live".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 — Live HedgeDoc".to_string(),
|
||||||
confidence: ConfidenceLevel::Critical,
|
confidence: ConfidenceLevel::Critical,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,48 +273,168 @@ impl IocFetcher {
|
|||||||
Ok(threats)
|
Ok(threats)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_suspicious_from_aur(&self) -> Result<Vec<IocEntry>> {
|
/// 2. ATOMIC ARCH GIST — Versionierter Fallback
|
||||||
Ok(Vec::new())
|
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";
|
||||||
async fn fetch_community_blocklist(&self) -> Result<Vec<IocEntry>> {
|
let latest_version = match self.client.get(history_url).send().await {
|
||||||
let url = "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt";
|
Ok(resp) => {
|
||||||
|
if resp.status().is_success() {
|
||||||
let response = match self.client.get(url).send().await {
|
let gist: serde_json::Value = resp.json().await?;
|
||||||
Ok(r) => r,
|
gist["history"][0]["version"].as_str().map(|s| s.to_string())
|
||||||
Err(_) => {
|
} else {
|
||||||
debug!("Community Blocklist nicht erreichbar");
|
None
|
||||||
return Ok(Vec::new());
|
}
|
||||||
}
|
}
|
||||||
|
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() {
|
if !response.status().is_success() {
|
||||||
return Ok(Vec::new());
|
anyhow::bail!("Gist HTTP {}", response.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
let mut threats = Vec::new();
|
let mut threats = Vec::new();
|
||||||
|
|
||||||
|
// Gist ist ein Script — wir parsen die Paketliste
|
||||||
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('#') {
|
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("echo") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
threats.push(IocEntry {
|
// Extrahiere Paketnamen (ähnlich wie HedgeDoc)
|
||||||
package_name: trimmed.to_string(),
|
if trimmed.starts_with("[") || trimmed.starts_with("{") {
|
||||||
threat_type: ThreatType::Unknown("community_reported".to_string()),
|
if let Ok(json_list) = serde_json::from_str::<Vec<String>>(trimmed) {
|
||||||
source: "community_blocklist".to_string(),
|
for pkg in json_list {
|
||||||
discovered_date: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
threats.push(IocEntry {
|
||||||
description: "Community-reported suspicious package".to_string(),
|
package_name: pkg,
|
||||||
confidence: ConfidenceLevel::Medium,
|
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)
|
Ok(threats)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_cache(&self,
|
/// 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]
|
iocs: &[IocEntry]
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let cache_file = self.cache_dir.join("iocs.json");
|
let cache_file = self.cache_dir.join("iocs.json");
|
||||||
@@ -240,7 +443,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 +501,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+53
-2
@@ -90,10 +90,23 @@ impl PackageScanner {
|
|||||||
) -> Result<ScanResult> {
|
) -> Result<ScanResult> {
|
||||||
info!("Scanne Paket: {}", package);
|
info!("Scanne Paket: {}", package);
|
||||||
|
|
||||||
|
// Prüfe ob Paket in offiziellem Repo oder AUR
|
||||||
|
let is_aur = self.is_aur_package(package).await;
|
||||||
|
|
||||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
||||||
let ioc_matches = self.ioc_fetcher.check_package(package, &iocs);
|
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 = self.fetch_aur_info(package).await?;
|
let aur_info = if is_aur {
|
||||||
|
self.fetch_aur_info(package).await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let pkgbuild_analysis = if let Some(ref info) = aur_info {
|
let pkgbuild_analysis = if let Some(ref info) = aur_info {
|
||||||
if let Some(url) = &info.url_path {
|
if let Some(url) = &info.url_path {
|
||||||
@@ -278,6 +291,44 @@ impl PackageScanner {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prüft ob ein Paket aus dem AUR stammt (nicht offizielles Repo)
|
||||||
|
async fn is_aur_package(&self, package: &str) -> bool {
|
||||||
|
// Versuche offizielles Repo-Info zu holen
|
||||||
|
let official = Command::new("pacman")
|
||||||
|
.args(["-Si", package])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match official {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
// Paket in offiziellem Repo gefunden
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if stdout.contains("Repository : aur") || stdout.contains("Repository : AUR") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Alle anderen Repos (core, extra, community, multilib, etc.)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Prüfe ob es ein "foreign" Paket ist (AUR)
|
||||||
|
let foreign = Command::new("pacman")
|
||||||
|
.args(["-Qm"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match foreign {
|
||||||
|
Ok(output) => {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
stdout.lines().any(|line| line.starts_with(package))
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_aur_info(
|
async fn fetch_aur_info(
|
||||||
&self, package: &str
|
&self, package: &str
|
||||||
) -> Result<Option<AurPackageInfo>> {
|
) -> Result<Option<AurPackageInfo>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user