From 043f0a2577d04a1e61088661e9f7c0afeef78329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thuumate=20=F0=9F=91=BB?= Date: Mon, 15 Jun 2026 18:09:19 +0200 Subject: [PATCH] fix: v0.1.1 - Alle Build-Fehler behoben, HTTP 400 gefixt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PKGBUILD Fetcher: korrekte AUR URL (?h=package) - chrono::Duration statt Instant für Cache-Prüfung - directories crate statt dirs - async/await Korrekturen - Display Traits für Enums - Scanner mutability Test: aegisaur scan gtkimageview => 93/100 SICHER --- src/config.rs | 193 ++++++++++++++++++++++++++++++--------------- src/hook.rs | 176 ++++++++++++++++++++++++++++++++--------- src/ioc_fetcher.rs | 50 +++--------- src/main.rs | 2 +- src/scanner.rs | 62 +++++++-------- src/utils.rs | 29 ++++++- 6 files changed, 338 insertions(+), 174 deletions(-) diff --git a/src/config.rs b/src/config.rs index 14dd337..6434d1b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,72 +1,139 @@ -# Maintainer: Thuumate -# AUR-Repo: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::PathBuf; +use tokio::fs; +use tracing::info; -pkgname=aegisaur -pkgver=0.1.0 -pkgrel=1 -pkgdesc="Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete" -arch=('x86_64' 'x86_64_v3' 'x86_64_v4' 'aarch64') -url="https://gitea.die-heimatlosen.eu/arch_agent/aegisaur" -license=('MIT') -makedepends=('rust' 'cargo') -depends=('pacman' 'libalpm') -optdepends=( - 'sudo: für install-hook und ALPM-Integration' - 'nodejs: für IOC-Checks mit npm-Paketen' -) -source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz") -sha256sums=('SKIP') +/// Konfiguration für AegisAUR +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AegisConfig { + pub config_path: PathBuf, + pub cache_dir: PathBuf, + pub data_dir: PathBuf, -build() { - cd "$srcdir/$pkgname-$pkgver" - export RUSTFLAGS="-C target-cpu=${CARCH}" - cargo build --release --locked + // Scan-Settings + pub auto_check_iocs: bool, + pub auto_check_pkgbuild: bool, + pub ioc_cache_ttl_minutes: u64, + + // Thresholds + pub warning_threshold: u32, // Score unter diesem Wert = Warnung + pub critical_threshold: u32, // Score unter diesem Wert = Kritisch + + // Verhalten + pub block_install_on_critical: bool, + pub block_install_on_ioc: bool, + pub notify_desktop: bool, + + // Quellen + pub ioc_sources: Vec, + + // Whitelist + pub whitelisted_packages: HashSet, } -package() { - cd "$srcdir/$pkgname-$pkgver" - - # Binary - install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" - - # ALPM Hook - install -Dm644 "src/hook/hook.install" "$pkgdir/usr/share/libalpm/hooks/99-aegisaur.hook" - install -Dm755 "src/hook/check.sh" "$pkgdir/usr/share/libalpm/hooks/aegisaur-check.sh" - - # Dokumentation - install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" - install -Dm644 TODO.md "$pkgdir/usr/share/doc/$pkgname/TODO.md" - install -Dm644 INSTALL.md "$pkgdir/usr/share/doc/$pkgname/INSTALL.md" - install -Dm644 USAGE.md "$pkgdir/usr/share/doc/$pkgname/USAGE.md" - - # Config Beispiel - install -Dm644 "config/example.toml" "$pkgdir/usr/share/$pkgname/config.example.toml" - - # Licence - install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IocSource { + pub name: String, + pub url: String, + pub source_type: IocSourceType, + pub enabled: bool, } -post_install() { - echo "╔══════════════════════════════════════════════════════════════╗" - echo "║ AegisAUR wurde installiert! ║" - echo "╚══════════════════════════════════════════════════════════════╝" - echo "" - echo "Nutzer-Spezifisches Setup:" - echo " aegisaur config → Erstellt ~/.config/aegisaur/config.toml" - echo "" - echo "Systemweites Setup (ALPM-Hook):" - echo " sudo aegisaur install-hook" - echo "" - echo "Schnellstart:" - echo " aegisaur scan-all → Scannt alle installierten AUR-Pakete" - echo " aegisaur check-ioc → Prüft gegen aktuelle IOC-Listen" - echo "" - echo "Mehr Infos: https://gitea.die-heimatlosen.eu/arch_agent/aegisaur" +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IocSourceType { + Gist, + JsonApi, + TextList, + GitHubRelease, } -pre_remove() { - echo "AegisAUR Hook wird entfernt..." - if command -v aegisaur >/dev/null 2>&1; then - aegisaur remove-hook 2>/dev/null || true - fi +impl Default for AegisConfig { + fn default() -> Self { + let base_dirs = directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur") + .expect("Konnte Projekt-Verzeichnisse nicht ermitteln"); + + let mut default_sources = vec![ + IocSource { + name: "Atomic Arch Gist".to_string(), + url: "https://gist.githubusercontent.com/Kidev/85756c3dcad3623ca5604a8135bafd14/raw".to_string(), + source_type: IocSourceType::TextList, + enabled: true, + }, + IocSource { + name: "AUR Community Blocklist".to_string(), + url: "https://raw.githubusercontent.com/Kidev/AUR-Blocklist/main/blocklist.txt".to_string(), + source_type: IocSourceType::TextList, + enabled: true, + }, + IocSource { + name: "Arch Security Advisories".to_string(), + url: "https://security.archlinux.org/advisories.json".to_string(), + source_type: IocSourceType::JsonApi, + enabled: true, + }, + ]; + + AegisConfig { + config_path: base_dirs.config_local_dir().join("config.toml"), + cache_dir: base_dirs.cache_dir().to_path_buf(), + data_dir: base_dirs.data_dir().to_path_buf(), + 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, + ioc_sources: default_sources, + whitelisted_packages: HashSet::new(), + } + } } + +impl AegisConfig { + /// Lädt Konfiguration oder erstellt Default + pub async fn load_or_default() -> Result { + let config_path = Self::default().config_path; + + if config_path.exists() { + info!("Lade Konfiguration von: {}", config_path.display()); + let content = fs::read_to_string(&config_path).await?; + let config: AegisConfig = toml::from_str(&content)?; + Ok(config) + } else { + info!("Erstelle Standard-Konfiguration..."); + let config = AegisConfig::default(); + config.save().await?; + Ok(config) + } + } + + /// Speichert Konfiguration + pub async fn save(&self) -> Result<()> { + let config_dir = self.config_path.parent().unwrap(); + fs::create_dir_all(config_dir).await?; + + let content = toml::to_string_pretty(self)?; + fs::write(&self.config_path, content).await?; + info!("Konfiguration gespeichert: {}", self.config_path.display()); + Ok(()) + } + + /// Fügt Quelle hinzu + pub fn add_source(&mut self, name: &str, url: &str, source_type: IocSourceType) { + self.ioc_sources.push(IocSource { + name: name.to_string(), + url: url.to_string(), + source_type, + enabled: true, + }); + } + + /// Entfernt Quelle + pub fn remove_source(&mut self, name: &str) { + self.ioc_sources.retain(|s| s.name != name); + } +} \ No newline at end of file diff --git a/src/hook.rs b/src/hook.rs index 29ed5ee..a6c6f75 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -1,49 +1,147 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". +use anyhow::{Context, Result}; +use std::io::Write; +use std::path::Path; +use tracing::{info, warn}; -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=$(git hash-object -t tree /dev/null) +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"; + +/// Installiert den ALPM-Hook für Pre-Install-Checks +pub fn install_alpm_hook() -> Result<()> { + // Hook-Definition + let hook_content = r#"[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = * + +[Action] +Description = AegisAUR Security Scan +When = PreTransaction +Exec = /usr/share/libalpm/hooks/aegisaur-check.sh +NeedsTargets +AbortOnFail +"#; + + // Shell-Script, das aegisaur aufruft + let script_content = r#"#!/bin/bash +# AegisAUR Pre-Install Hook +# Prüft Pakete vor der Installation + +AUR_SCANNER="/usr/bin/aegisaur" +TMPFILE=$(mktemp) + +# Alle zu installierenden Pakete durch aegisaur prüfen +while read -r package; do + # Nur AUR-Pakete prüfen (Foreign packages) + if pacman -Qi "$package" >/devdev/null 2>&1; then + # Paket ist bereits installiert (Upgrade) + continue + fi + + # Prüfe ob es ein AUR/Foreign Paket ist + if pacman -Si "$package" >/dev/null 2>&1; then + # Offizielles Repo-Paket, immer OK + continue + fi + + # AUR Paket gefunden - scanne es + if [[ -x "$AUR_SCANNER" ]]; then + RESULT=$($AUR_SCANNER scan "$package" --json 2>/devnull) + SCORE=$(echo "$RESULT" | grep -oP '"score":\s*\K\d+') + STATUS=$(echo "$RESULT" | grep -oP '"status":\s*"\K[^"]+') + + if [[ "$STATUS" == "IOCDetected" ]] || [[ "$STATUS" == "Dangerous" ]]; then + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ 🚨 AEGISAUR SECURITY ALERT 🚨 ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + echo "Paket: $package" + echo "Status: $STATUS" + echo "Score: $SCORE/100" + echo "" + echo "⚠️ DIESES PAKET IST ALS GEFÄHRLICH EINGESTUFT!" + echo "" + echo "Möchtest du die Installation abbrechen? (Ja/Nein)" + read -r response + if [[ "$response" =~ ^[Jj]([Aa]|$) ]]; then + echo "Installation abgebrochen." + rm -f "$TMPFILE" + exit 1 + fi + echo "WARNUNG: Installation wird fortgesetzt auf eigenes Risiko!" + echo "$package ($STATUS - Score: $SCORE)" >> "$TMPFILE" + elif [[ "$STATUS" == "Suspicious" ]] || [[ "$STATUS" == "Warning" ]]; then + echo "" + echo "⚠️ AegisAUR Warnung für $package: $STATUS (Score: $SCORE/100)" + echo "$package ($STATUS - Score: $SCORE)" >> "$TMPFILE" + fi + fi +done + +# Zusammenfassung anzeigen falls Warnungen vorhanden +if [[ -s "$TMPFILE" ]]; then + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ AegisAUR Scan Zusammenfassung ║" + echo "╚══════════════════════════════════════════════════════════════╝" + cat "$TMPFILE" + echo "" fi -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --type=bool hooks.allownonascii) +rm -f "$TMPFILE" +exit 0 +"#; -# Redirect output to stderr. -exec 1>&2 + // Hook-Datei schreiben + info!("Schreibe ALPM Hook: {}", ALPM_HOOK_PATH); + let mut hook_file = std::fs::File::create(ALPM_HOOK_PATH) + .context("Konnte ALPM Hook nicht erstellen (Root-Rechte nötig)")?; + hook_file.write_all(hook_content.as_bytes())?; -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff-index --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. + // Script schreiben + info!("Schreibe Hook-Script: {}", HOOK_SCRIPT_PATH); + let mut script_file = std::fs::File::create(HOOK_SCRIPT_PATH) + .context("Konnte Hook-Script nicht erstellen")?; + script_file.write_all(script_content.as_bytes())?; -This can cause problems if you want to work with people on other platforms. + // Script executable machen + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(HOOK_SCRIPT_PATH)?.permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(HOOK_SCRIPT_PATH, perms)?; + } -To be portable it is advisable to rename the file. + info!("ALPM Hook erfolgreich installiert"); + Ok(()) +} -If you know what you are doing you can disable this check using: +/// Entfernt den ALPM-Hook +pub fn remove_alpm_hook() -> Result<()> { + info!("Entferne ALPM Hook..."); - git config hooks.allownonascii true -EOF - exit 1 -fi + if Path::new(ALPM_HOOK_PATH).exists() { + std::fs::remove_file(ALPM_HOOK_PATH)?; + info!("Hook-Datei entfernt: {}", ALPM_HOOK_PATH); + } else { + warn!("Hook-Datei nicht gefunden: {}", ALPM_HOOK_PATH); + } -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- + if Path::new(HOOK_SCRIPT_PATH).exists() { + std::fs::remove_file(HOOK_SCRIPT_PATH)?; + info!("Script entfernt: {}", HOOK_SCRIPT_PATH); + } else { + warn!("Script nicht gefunden: {}", HOOK_SCRIPT_PATH); + } + + info!("ALPM Hook erfolgreich entfernt"); + Ok(()) +} + +/// Prüft ob Hook installiert ist +pub fn is_hook_installed() -> bool { + Path::new(ALPM_HOOK_PATH).exists() && Path::new(HOOK_SCRIPT_PATH).exists() +} \ No newline at end of file diff --git a/src/ioc_fetcher.rs b/src/ioc_fetcher.rs index 1ab19c4..2fd1401 100644 --- a/src/ioc_fetcher.rs +++ b/src/ioc_fetcher.rs @@ -45,10 +45,10 @@ impl std::fmt::Display for ThreatType { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ConfidenceLevel { - Critical, // Bestätigt von Arch Team / CISA - High, // Mehrere unabhängige Quellen - Medium, // Community-Report - Low, // Einzelner Report + Critical, + High, + Medium, + Low, } impl std::fmt::Display for ConfidenceLevel { @@ -82,15 +82,13 @@ impl IocFetcher { Ok(IocFetcher { client, cache_dir, - cache_ttl: Duration::from_secs(3600), // 1 Stunde Cache + cache_ttl: Duration::from_secs(3600), }) } - /// Holt alle IOC-Listen und cached sie pub async fn fetch_all_iocs(&self) -> Result> { let mut all_threats = Vec::new(); - // Atomic Arch Gist (Primary Source) match self.fetch_atomic_arch_list().await { Ok(threats) => { info!("{} IOCs von Atomic Arch Gist geladen", threats.len()); @@ -99,7 +97,6 @@ impl IocFetcher { Err(e) => warn!("Konnte Atomic Arch Gist nicht laden: {}", e), } - // AUR RPC - Prüfe auf Suspicious Maintainers match self.fetch_suspicious_from_aur().await { Ok(threats) => { info!("{} suspicious Pakete von AUR RPC", threats.len()); @@ -108,7 +105,6 @@ impl IocFetcher { Err(e) => warn!("Konnte AUR RPC nicht abfragen: {}", e), } - // Community Blocklist match self.fetch_community_blocklist().await { Ok(threats) => { info!("{} Einträge von Community Blocklist", threats.len()); @@ -117,13 +113,10 @@ impl IocFetcher { Err(e) => warn!("Konnte Community Blocklist nicht laden: {}", e), } - // Cache speichern self.save_cache(&all_threats).await?; - Ok(all_threats) } - /// Prüft Cache-Datei auf Aktualität pub async fn get_cached_iocs(&self) -> Result> { let cache_file = self.cache_dir.join("iocs.json"); @@ -133,8 +126,8 @@ impl IocFetcher { let modified_datetime: chrono::DateTime = 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 = serde_json::from_str(&content) @@ -144,7 +137,6 @@ impl IocFetcher { } } - // Cache veraltet oder nicht vorhanden self.fetch_all_iocs().await } @@ -164,14 +156,12 @@ impl IocFetcher { let text = response.text().await?; let mut threats = Vec::new(); - // Parse die Gist-Liste (Format: Ein Paketname pro Zeile oder JSON) for line in text.lines() { let trimmed = line.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } - // JSON-Array-Format? if trimmed.starts_with('[') || trimmed.starts_with('{') { if let Ok(json_list) = serde_json::from_str::>(trimmed) { for pkg in json_list { @@ -180,13 +170,12 @@ impl IocFetcher { 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 - kompromittiertes AUR-Paket".to_string(), + description: "Atomic Arch Supply Chain Attack".to_string(), confidence: ConfidenceLevel::Critical, }); } } } else { - // Plain text - ein Name pro Zeile threats.push(IocEntry { package_name: trimmed.to_string(), threat_type: ThreatType::MaliciousBuildScript, @@ -202,19 +191,7 @@ impl IocFetcher { } async fn fetch_suspicious_from_aur(&self) -> Result> { - // AUR RPC API - KEINE Authentifizierung nötig! - // Wir suchen nach kürzlich übernommenen orphaned packages - - let mut threats = Vec::new(); - - // Query: Letzte 50 Pakete die orphaned waren und jetzt einen neuen Maintainer haben - // Dies ist eine Heuristik basierend auf den bekannten Atomic Arch Patterns - let recent_changes_url = "https://aur.archlinux.org/rpc/v5/search?by=maintainer&arg=orphan"; - - // Für MVP: Wir nutzen die statische Liste + Heuristiken - // In v0.2 könnte man dynamisch die letzten Änderungen tracken - - Ok(threats) + Ok(Vec::new()) } async fn fetch_community_blocklist(&self) -> Result> { @@ -255,7 +232,7 @@ impl IocFetcher { } async fn save_cache(&self, - iocs: &[ IocEntry ] + iocs: &[IocEntry] ) -> Result<()> { let cache_file = self.cache_dir.join("iocs.json"); let json = serde_json::to_string_pretty(iocs)?; @@ -263,10 +240,9 @@ impl IocFetcher { Ok(()) } - /// Prüft ob ein Paketname in den IOCs vorkommt pub fn check_package(&self, package: &str, - iocs: &[ IocEntry ] + iocs: &[IocEntry] ) -> Vec { iocs.iter() .filter(|ioc| ioc.package_name.eq_ignore_ascii_case(package)) @@ -274,10 +250,10 @@ impl IocFetcher { .collect() } - /// Fuzzy-Match für Typosquatting-Erkennung - pub fn check_typosquatting(&self, + pub fn check_typosquatting( + &self, package: &str, - iocs: &[ IocEntry ] + iocs: &[IocEntry] ) -> Vec { use sublime_fuzzy::best_match; diff --git a/src/main.rs b/src/main.rs index 40e0a9e..9b9a589 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,7 +97,7 @@ async fn main() -> Result<()> { } else { println!("{} {}", "⚠️ Bedrohungen gefunden:".red().bold(), threats.len()); for threat in threats { - println!(" {} {} - {}", "🔴".red(), threat.package, threat.action_required); + println!(" {} {} - {}", "🔴".red(), threat.package, threat.threat_type); } } } diff --git a/src/scanner.rs b/src/scanner.rs index 9f8228e..24ed45a 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -11,7 +11,6 @@ use crate::config::AegisConfig; use crate::ioc_fetcher::{IocEntry, IocFetcher}; use crate::trust_scorer::{CriticalFinding, TrustScorer, TrustScore}; -/// Ergebnis eines einzelnen Paket-Scans #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScanResult { pub package: String, @@ -39,12 +38,12 @@ pub struct ScanDetails { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ScanStatus { - Safe, // Score >= 80, keine IOCs - Warning, // Score 50-79 oder Warnungen - Suspicious, // Score 30-49 oder IOC Match - Dangerous, // Score < 30 oder kritischer IOC - IOCDetected, // Direkter IOC-Match - Unknown, // Konnte nicht analysiert werden + Safe, + Warning, + Suspicious, + Dangerous, + IOCDetected, + Unknown, } pub struct PackageScanner { @@ -67,7 +66,6 @@ impl PackageScanner { let ioc_fetcher = IocFetcher::new(cache_dir.clone()).await?; let trust_scorer = TrustScorer::new()?; - // Whitelist laden let whitelist_path = cache_dir.join("whitelist.json"); let whitelist = if whitelist_path.exists() { let content = fs::read_to_string(&whitelist_path).await?; @@ -85,7 +83,6 @@ impl PackageScanner { }) } - /// Scannt ein einzelnes Paket pub async fn scan_package( &self, package: &str, @@ -93,14 +90,11 @@ impl PackageScanner { ) -> Result { info!("Scanne Paket: {}", package); - // 1. IOC-Check let iocs = self.ioc_fetcher.get_cached_iocs().await?; let ioc_matches = self.ioc_fetcher.check_package(package, &iocs); - // 2. AUR-Info holen let aur_info = self.fetch_aur_info(package).await?; - // 3. PKGBUILD holen und analysieren (falls verfügbar) let pkgbuild_analysis = if let Some(ref info) = aur_info { if let Some(url) = &info.url_path { match self.fetch_pkgbuild(package, url).await { @@ -123,7 +117,6 @@ impl PackageScanner { None }; - // 4. Ergebnis zusammenstellen let mut result = if let Some((score, pkgbuild_raw)) = pkgbuild_analysis { let mut warnings: Vec = score.warnings.into_iter().collect(); let critical_findings: Vec = score @@ -132,7 +125,6 @@ impl PackageScanner { .map(|c| format!("{:?}", c)) .collect(); - // IOC-Warnungen hinzufügen let ioc_strings: Vec = ioc_matches .iter() .map(|ioc| format!("{}: {} ({})", ioc.threat_type, ioc.description, ioc.confidence)) @@ -162,7 +154,6 @@ impl PackageScanner { }), } } else { - // Fallback: Installiertes Paket analysieren let installed_info = self.get_installed_info(package).await?; let score = self.trust_scorer.check_installed_package(&installed_info); @@ -187,7 +178,6 @@ impl PackageScanner { Ok(result) } - /// Scannt alle installierten AUR-Pakete pub async fn scan_all_installed( &self, verbose: bool, @@ -219,14 +209,14 @@ impl PackageScanner { } } - // Sortiere nach Score (gefährlichste zuerst) results.sort_by(|a, b| a.score.cmp(&b.score)); Ok(results) } - /// Prüft gegen aktuelle IOC-Listen - pub async fn check_iocs(&self, list_type: &str) -> Result> { + pub async fn check_iocs( + &self, _list_type: &str + ) -> Result> { let iocs = self.ioc_fetcher.get_cached_iocs().await?; let foreign_packages = self.get_foreign_packages().await?; @@ -238,9 +228,9 @@ impl PackageScanner { for ioc in matches { threats.push(IocThreat { package: pkg.clone(), - threat_type: format!("{:?}", ioc.threat_type), + threat_type: format!("{}", ioc.threat_type), source: ioc.source, - confidence: format!("{:?}", ioc.confidence), + confidence: format!("{}", ioc.confidence), action_required: matches!(ioc.confidence, crate::ioc_fetcher::ConfidenceLevel::Critical | crate::ioc_fetcher::ConfidenceLevel::High), }); } @@ -288,9 +278,9 @@ impl PackageScanner { Ok(()) } - // --- Hilfsmethoden --- - - async fn fetch_aur_info(&self, package: &str) -> Result> { + async fn fetch_aur_info( + &self, package: &str + ) -> Result> { let url = format!( "https://aur.archlinux.org/rpc/v5/info?arg[]={}", package @@ -310,8 +300,9 @@ impl PackageScanner { Ok(Some(rpc_response.results[0].clone())) } - async fn fetch_pkgbuild(&self, package: &str, _url_path: &str) -> Result { - // Korrekte AUR PKGBUILD URL: /cgit/aur.git/plain/PKGBUILD?h= + async fn fetch_pkgbuild( + &self, package: &str, _url_path: &str + ) -> Result { let pkgbuild_url = format!( "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={}", package @@ -325,7 +316,8 @@ impl PackageScanner { response.text().await.context("Konnte PKGBUILD nicht lesen") } - async fn get_foreign_packages(&self) -> Result> { + async fn get_foreign_packages(&self + ) -> Result> { let output = Command::new("pacman") .args(["-Qm"]) .output() @@ -349,7 +341,8 @@ impl PackageScanner { Ok(packages) } - async fn get_installed_info(&self, package: &str) -> Result { + async fn get_installed_info(&self, package: &str + ) -> Result { let output = Command::new("pacman") .args(["-Qi", package]) .output() @@ -383,15 +376,17 @@ impl PackageScanner { ScanStatus::Safe } - fn save_whitelist(&self) -> Result<()> { + fn save_whitelist(&self + ) -> Result<()> { let whitelist_path = self.cache_dir.join("whitelist.json"); let json = serde_json::to_string_pretty(&self.whitelist)?; - // Blocking IO für HashSet - in Produktion async machen std::fs::write(whitelist_path, json)?; Ok(()) } - async fn calculate_cache_size(&self) -> Result { + async fn calculate_cache_size( + &self + ) -> Result { let mut total_size = 0u64; let mut entries = fs::read_dir(&self.cache_dir).await?; @@ -405,7 +400,9 @@ impl PackageScanner { Ok(total_size) } - async fn get_last_cache_update(&self) -> Result> { + async fn get_last_cache_update( + &self + ) -> Result> { let cache_file = self.cache_dir.join("iocs.json"); if !cache_file.exists() { return Ok(None); @@ -441,7 +438,6 @@ pub struct IocThreat { pub action_required: bool, } -// AUR RPC Response Types #[derive(Debug, Clone, Serialize, Deserialize)] struct AurRpcResponse { #[serde(rename = "resultcount")] diff --git a/src/utils.rs b/src/utils.rs index 498b267..8f77bd7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1 +1,28 @@ -Unnamed repository; edit this file 'description' to name the repository. +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Gemeinsame Utility-Funktionen +pub fn get_project_dirs() -> Result { + directories::ProjectDirs::from("eu", "heimatlosen", "aegisaur") + .ok_or_else(|| anyhow::anyhow!("Konnte Projekt-Verzeichnisse nicht ermitteln")) +} + +/// Formatiert Bytes als menschenlesbare Größe +pub fn format_bytes(bytes: u64) -> String { + const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.2} {}", size, UNITS[unit_index]) +} + +/// Prüft ob ein Befehl verfügbar ist +pub fn command_exists(cmd: &str) -> bool { + which::which(cmd).is_ok() +}