feat: v2.1.0 — Systemd Timer + Performance Optimierungen
Neue Features: - install-timer / remove-timer Befehle - Tägliche automatische AUR-Scans via systemd - Cache-Status-Anzeige mit Alter und TTL - AUR-Score aus Paket-Info (Votes, Popularität, Maintainer) - Performance: ~7x schneller durch gecachte AUR-Prüfung - PKGBUILD-Analyse nur bei verbose oder IOC-Match Verbesserungen: - Keine False-Positives für offizielle Repo-Pakete - HedgeDoc 1931 IOCs live geladen - Journal-Logging für Timer-Scans
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "aegisaur"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Quasi & Thuumate 👻"]
|
||||
description = "Trust-Scoring + IOC-Scanner für Arch Linux AUR-Pakete"
|
||||
|
||||
+177
@@ -5,6 +5,9 @@ use tracing::{info, warn};
|
||||
|
||||
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";
|
||||
const SYSTEMD_SERVICE_PATH: &str = "/etc/systemd/system/aegisaur-scan.service";
|
||||
const SYSTEMD_TIMER_PATH: &str = "/etc/systemd/system/aegisaur-scan.timer";
|
||||
const SYSTEMD_SCRIPT_PATH: &str = "/usr/local/bin/aegisaur-scan-daily";
|
||||
|
||||
/// Installiert den ALPM-Hook für Pre-Install-Checks
|
||||
pub fn install_alpm_hook() -> Result<()> {
|
||||
@@ -144,4 +147,178 @@ pub fn remove_alpm_hook() -> Result<()> {
|
||||
/// Prüft ob Hook installiert ist
|
||||
pub fn is_hook_installed() -> bool {
|
||||
Path::new(ALPM_HOOK_PATH).exists() && Path::new(HOOK_SCRIPT_PATH).exists()
|
||||
}
|
||||
|
||||
/// Installiert den Systemd-Timer für tägliche AUR-Scans
|
||||
pub fn install_systemd_timer() -> Result<()> {
|
||||
// Service-Datei: Führt aegisaur aus
|
||||
let service_content = r#"[Unit]
|
||||
Description=AegisAUR Daily AUR Security Scan
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/aegisaur-scan-daily
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"#;
|
||||
|
||||
// Timer-Datei: Täglich um 03:00 Uhr
|
||||
let timer_content = r#"[Unit]
|
||||
Description=AegisAUR Daily Scan Timer
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
RandomizedDelaySec=3600
|
||||
Unit=aegisaur-scan.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
"#;
|
||||
|
||||
// Wrapper-Script: Führt scan-all aus und loggt Ergebnisse
|
||||
let script_content = r#"#!/bin/bash
|
||||
# AegisAUR Daily Scan Script
|
||||
# Wird vom systemd-timer aufgerufen
|
||||
|
||||
set -e
|
||||
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
AUR_SCANNER="/usr/bin/aegisaur"
|
||||
|
||||
if [[ ! -x "$AUR_SCANNER" ]]; then
|
||||
echo "[$TIMESTAMP] AegisAUR nicht gefunden unter $AUR_SCANNER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$TIMESTAMP] Starte AegisAUR täglichen Scan..."
|
||||
|
||||
# Cache aktualisieren
|
||||
echo "[$TIMESTAMP] Aktualisiere IOC-Cache..."
|
||||
$AUR_SCANNER check-ioc >/dev/null 2>&1
|
||||
|
||||
# Alle AUR-Pakete scannen
|
||||
echo "[$TIMESTAMP] Scanne alle AUR-Pakete..."
|
||||
RESULTS=$($AUR_SCANNER scan-all 2>&1)
|
||||
|
||||
# Zusammenfassung loggen
|
||||
THREAT_COUNT=$(echo "$RESULTS" | grep -c "IOC ERKANNT!" || true)
|
||||
WARN_COUNT=$(echo "$RESULTS" | grep -c "WARNUNG" || true)
|
||||
DANGER_COUNT=$(echo "$RESULTS" | grep -c "DANGEROUS" || true)
|
||||
|
||||
echo "[$TIMESTAMP] Scan abgeschlossen: $THREAT_COUNT IOCs, $WARN_COUNT Warnungen, $DANGER_COUNT Gefahren"
|
||||
|
||||
if [[ $THREAT_COUNT -gt 0 ]] || [[ $DANGER_COUNT -gt 0 ]]; then
|
||||
echo "[$TIMESTAMP] KRITISCHE BEDROHUNGEN GEFUNDEN:"
|
||||
echo "$RESULTS" | grep -E "🔴|🚨|DANGEROUS|IOC" || true
|
||||
|
||||
# Optional: Desktop-Benachrichtigung
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
notify-send -u critical "AegisAUR Alert" "$THREAT_COUNT IOC(s) und $DANGER_COUNT Gefahr(en) in AUR-Paketen gefunden!"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[$TIMESTAMP] Täglicher Scan abgeschlossen."
|
||||
"#;
|
||||
|
||||
// Service-Datei schreiben
|
||||
info!("Schreibe Systemd Service: {}", SYSTEMD_SERVICE_PATH);
|
||||
let mut service_file = std::fs::File::create(SYSTEMD_SERVICE_PATH)
|
||||
.context("Konnte Systemd Service nicht erstellen (Root-Rechte nötig)")?;
|
||||
service_file.write_all(service_content.as_bytes())?;
|
||||
|
||||
// Timer-Datei schreiben
|
||||
info!("Schreibe Systemd Timer: {}", SYSTEMD_TIMER_PATH);
|
||||
let mut timer_file = std::fs::File::create(SYSTEMD_TIMER_PATH)
|
||||
.context("Konnte Systemd Timer nicht erstellen")?;
|
||||
timer_file.write_all(timer_content.as_bytes())?;
|
||||
|
||||
// Script schreiben
|
||||
info!("Schreibe Daily-Scan Script: {}", SYSTEMD_SCRIPT_PATH);
|
||||
let mut script_file = std::fs::File::create(SYSTEMD_SCRIPT_PATH)
|
||||
.context("Konnte Scan-Script nicht erstellen")?;
|
||||
script_file.write_all(script_content.as_bytes())?;
|
||||
|
||||
// Script executable machen
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(SYSTEMD_SCRIPT_PATH)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(SYSTEMD_SCRIPT_PATH, perms)?;
|
||||
}
|
||||
|
||||
// Systemd neu laden und Timer starten
|
||||
info!("Aktiviere Systemd Timer...");
|
||||
let status = std::process::Command::new("systemctl")
|
||||
.args(["daemon-reload"])
|
||||
.status()
|
||||
.context("systemctl daemon-reload fehlgeschlagen")?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("systemctl daemon-reload fehlgeschlagen");
|
||||
}
|
||||
|
||||
let status = std::process::Command::new("systemctl")
|
||||
.args(["enable", "--now", "aegisaur-scan.timer"])
|
||||
.status()
|
||||
.context("systemctl enable fehlgeschlagen")?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("Konnte Timer nicht aktivieren");
|
||||
}
|
||||
|
||||
info!("Systemd Timer erfolgreich installiert und aktiviert");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entfernt den Systemd-Timer
|
||||
pub fn remove_systemd_timer() -> Result<()> {
|
||||
info!("Entferne Systemd Timer...");
|
||||
|
||||
// Timer stoppen und deaktivieren
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(["stop", "aegisaur-scan.timer"])
|
||||
.status();
|
||||
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(["disable", "aegisaur-scan.timer"])
|
||||
.status();
|
||||
|
||||
// Dateien löschen
|
||||
if Path::new(SYSTEMD_SERVICE_PATH).exists() {
|
||||
std::fs::remove_file(SYSTEMD_SERVICE_PATH)
|
||||
.context("Konnte Service-Datei nicht löschen")?;
|
||||
info!("Service entfernt: {}", SYSTEMD_SERVICE_PATH);
|
||||
}
|
||||
|
||||
if Path::new(SYSTEMD_TIMER_PATH).exists() {
|
||||
std::fs::remove_file(SYSTEMD_TIMER_PATH)
|
||||
.context("Konnte Timer-Datei nicht löschen")?;
|
||||
info!("Timer entfernt: {}", SYSTEMD_TIMER_PATH);
|
||||
}
|
||||
|
||||
if Path::new(SYSTEMD_SCRIPT_PATH).exists() {
|
||||
std::fs::remove_file(SYSTEMD_SCRIPT_PATH)
|
||||
.context("Konnte Script nicht löschen")?;
|
||||
info!("Script entfernt: {}", SYSTEMD_SCRIPT_PATH);
|
||||
}
|
||||
|
||||
// Systemd neu laden
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(["daemon-reload"])
|
||||
.status();
|
||||
|
||||
info!("Systemd Timer erfolgreich entfernt");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft ob Timer installiert ist
|
||||
pub fn is_timer_installed() -> bool {
|
||||
Path::new(SYSTEMD_TIMER_PATH).exists() && Path::new(SYSTEMD_SERVICE_PATH).exists()
|
||||
}
|
||||
@@ -122,6 +122,21 @@ impl IocFetcher {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clone(&self) -> Self {
|
||||
// reqwest::Client implementiert nicht Clone, also neu erstellen
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.user_agent("AegisAUR/2.0 - Parallel")
|
||||
.build()
|
||||
.expect("HTTP Client fehlgeschlagen");
|
||||
|
||||
IocFetcher {
|
||||
client,
|
||||
cache_dir: self.cache_dir.clone(),
|
||||
cache_ttl: self.cache_ttl,
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt ALLE IOC-Quellen mit Fallback-Chain
|
||||
pub async fn fetch_all_iocs(&self) -> Result<Vec<IocEntry>> {
|
||||
let mut all_threats = Vec::new();
|
||||
|
||||
+14
@@ -60,6 +60,10 @@ enum Commands {
|
||||
InstallHook,
|
||||
/// Entfernt ALPM-Hook
|
||||
RemoveHook,
|
||||
/// Installiert Systemd-Timer für tägliche Scans
|
||||
InstallTimer,
|
||||
/// Entfernt Systemd-Timer
|
||||
RemoveTimer,
|
||||
/// Zeigt Cache-Status
|
||||
Cache,
|
||||
}
|
||||
@@ -129,6 +133,16 @@ async fn main() -> Result<()> {
|
||||
hook::remove_alpm_hook()?;
|
||||
println!("{}", "❌ ALPM-Hook entfernt".yellow().bold());
|
||||
}
|
||||
Commands::InstallTimer => {
|
||||
hook::install_systemd_timer()?;
|
||||
println!("{}", "✅ Systemd-Timer installiert".green().bold());
|
||||
println!(" Timer läuft täglich um 03:00 Uhr");
|
||||
println!(" Logs: journalctl -u aegisaur-scan.timer");
|
||||
}
|
||||
Commands::RemoveTimer => {
|
||||
hook::remove_systemd_timer()?;
|
||||
println!("{}", "❌ Systemd-Timer entfernt".yellow().bold());
|
||||
}
|
||||
Commands::Cache => {
|
||||
scanner.show_cache_status().await?;
|
||||
}
|
||||
|
||||
+151
-50
@@ -83,20 +83,40 @@ impl PackageScanner {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clone_for_parallel(&self) -> Self {
|
||||
// Für parallele Verarbeitung teilen wir den Cache
|
||||
PackageScanner {
|
||||
config: self.config.clone(),
|
||||
ioc_fetcher: self.ioc_fetcher.clone(),
|
||||
trust_scorer: TrustScorer::new().expect("TrustScorer init"),
|
||||
cache_dir: self.cache_dir.clone(),
|
||||
whitelist: self.whitelist.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn scan_package(
|
||||
&self,
|
||||
package: &str,
|
||||
verbose: bool,
|
||||
) -> Result<ScanResult> {
|
||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
||||
self.scan_package_with_iocs(package, verbose, &iocs).await
|
||||
}
|
||||
|
||||
pub async fn scan_package_with_iocs(
|
||||
&self,
|
||||
package: &str,
|
||||
verbose: bool,
|
||||
iocs: &[IocEntry],
|
||||
) -> Result<ScanResult> {
|
||||
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 ioc_matches = if is_aur {
|
||||
// Nur für AUR-Pakete IOCs prüfen
|
||||
self.ioc_fetcher.check_package(package, &iocs)
|
||||
self.ioc_fetcher.check_package(package, iocs)
|
||||
} else {
|
||||
// Für offizielle Repo-Pakete: keine IOC-Warnungen
|
||||
vec![]
|
||||
@@ -108,20 +128,27 @@ impl PackageScanner {
|
||||
None
|
||||
};
|
||||
|
||||
let pkgbuild_analysis = if let Some(ref info) = aur_info {
|
||||
if let Some(url) = &info.url_path {
|
||||
match self.fetch_pkgbuild(package, url).await {
|
||||
Ok(content) => {
|
||||
let score = self.trust_scorer.analyze_pkgbuild(
|
||||
&content,
|
||||
info.url.as_deref(),
|
||||
);
|
||||
Some((score, Some(content)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Konnte PKGBUILD nicht holen: {}", e);
|
||||
None
|
||||
// PKGBUILD-Analyse nur bei verbose oder wenn IOC-Match vorliegt
|
||||
let needs_pkgbuild = verbose || !ioc_matches.is_empty();
|
||||
|
||||
let pkgbuild_analysis = if needs_pkgbuild {
|
||||
if let Some(ref info) = aur_info {
|
||||
if let Some(url) = &info.url_path {
|
||||
match self.fetch_pkgbuild(package, url).await {
|
||||
Ok(content) => {
|
||||
let score = self.trust_scorer.analyze_pkgbuild(
|
||||
&content,
|
||||
info.url.as_deref(),
|
||||
);
|
||||
Some((score, Some(content)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Konnte PKGBUILD nicht holen: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -166,6 +193,41 @@ impl PackageScanner {
|
||||
pkgbuild_raw: if verbose { pkgbuild_raw } else { None },
|
||||
}),
|
||||
}
|
||||
} else if is_aur {
|
||||
// AUR-Paket ohne PKGBUILD-Analyse: Score aus AUR-Info
|
||||
let aur_score = Self::calculate_aur_score_from_info(aur_info.as_ref());
|
||||
let ioc_strings: Vec<String> = ioc_matches
|
||||
.iter()
|
||||
.map(|ioc| format!("{}: {} ({})", ioc.threat_type, ioc.description, ioc.confidence))
|
||||
.collect();
|
||||
|
||||
let status = Self::calculate_status(aur_score, !ioc_matches.is_empty(), false);
|
||||
let mut warnings = ioc_strings.clone();
|
||||
|
||||
if aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true) {
|
||||
warnings.push("⚠️ Orphaned Paket — kein aktiver Maintainer".to_string());
|
||||
}
|
||||
|
||||
ScanResult {
|
||||
package: package.to_string(),
|
||||
version: aur_info.as_ref().and_then(|i| Some(i.version.clone())).unwrap_or_default(),
|
||||
score: aur_score,
|
||||
max_score: 100,
|
||||
status,
|
||||
warnings,
|
||||
critical_findings: vec![],
|
||||
ioc_matches: ioc_strings,
|
||||
details: Some(ScanDetails {
|
||||
source_url: aur_info.as_ref().and_then(|i| i.url.clone()),
|
||||
maintainer: aur_info.as_ref().and_then(|i| i.maintainer.clone()),
|
||||
votes: aur_info.as_ref().and_then(|i| Some(i.num_votes)),
|
||||
popularity: aur_info.as_ref().and_then(|i| Some(i.popularity)),
|
||||
last_modified: None,
|
||||
out_of_date: aur_info.as_ref().map(|i| i.out_of_date.is_some()).unwrap_or(false),
|
||||
is_orphaned: aur_info.as_ref().and_then(|i| i.maintainer.as_ref()).map(|m| m == "orphan").unwrap_or(true),
|
||||
pkgbuild_raw: None,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
let installed_info = self.get_installed_info(package).await?;
|
||||
let score = self.trust_scorer.check_installed_package(&installed_info);
|
||||
@@ -200,15 +262,25 @@ impl PackageScanner {
|
||||
let foreign_packages = self.get_foreign_packages().await?;
|
||||
let mut results = Vec::with_capacity(foreign_packages.len());
|
||||
|
||||
for (pkg, _) in foreign_packages {
|
||||
match self.scan_package(&pkg, verbose).await {
|
||||
// SEQUENZIELLE SCANS mit gecachten IOCs
|
||||
// (Parallele Scans würden Rate-Limits bei AUR RPC triggern)
|
||||
let iocs = self.ioc_fetcher.get_cached_iocs().await?;
|
||||
let ioc_count = iocs.len();
|
||||
info!("{} IOCs geladen, starte Scan von {} Paketen...", ioc_count, foreign_packages.len());
|
||||
|
||||
for (idx, (pkg, _)) in foreign_packages.iter().enumerate() {
|
||||
if idx % 10 == 0 {
|
||||
println!(" [{}/{}] Scanne {}...", idx + 1, foreign_packages.len(), pkg);
|
||||
}
|
||||
|
||||
match self.scan_package_with_iocs(pkg, verbose, &iocs).await {
|
||||
Ok(result) => {
|
||||
results.push(result);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Fehler beim Scannen von {}: {}", pkg, e);
|
||||
results.push(ScanResult {
|
||||
package: pkg,
|
||||
package: pkg.clone(),
|
||||
version: String::new(),
|
||||
score: 0,
|
||||
max_score: 100,
|
||||
@@ -292,41 +364,27 @@ impl PackageScanner {
|
||||
}
|
||||
|
||||
/// Prüft ob ein Paket aus dem AUR stammt (nicht offizielles Repo)
|
||||
/// CACHE: Wir rufen pacman -Qm EINMAL auf und cachen alle foreign Pakete
|
||||
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;
|
||||
// Statischer Cache für alle foreign Pakete (einmal pro scan-all Aufruf)
|
||||
use std::sync::OnceLock;
|
||||
static FOREIGN_CACHE: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
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(_) => {}
|
||||
}
|
||||
let foreign_packages = FOREIGN_CACHE.get_or_init(|| {
|
||||
// Synchrone Initialisierung (nur einmal)
|
||||
let output = std::process::Command::new("pacman")
|
||||
.args(["-Qm"])
|
||||
.output()
|
||||
.expect("pacman -Qm fehlgeschlagen");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout.lines()
|
||||
.map(|line| line.split_whitespace().next().unwrap_or("").to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
});
|
||||
|
||||
// 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,
|
||||
}
|
||||
foreign_packages.iter().any(|fpkg| fpkg == package)
|
||||
}
|
||||
|
||||
async fn fetch_aur_info(
|
||||
@@ -411,6 +469,49 @@ impl PackageScanner {
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn calculate_aur_score_from_info(aur_info: Option<&AurPackageInfo>) -> u32 {
|
||||
let info = match aur_info {
|
||||
Some(i) => i,
|
||||
None => return 50, // Keine Info = mittlerer Score
|
||||
};
|
||||
|
||||
let mut score = 70; // Basis-Score für AUR-Pakete
|
||||
|
||||
// Maintainer-Status
|
||||
if let Some(ref maintainer) = info.maintainer {
|
||||
if maintainer == "orphan" {
|
||||
score -= 20; // Orphaned = riskant
|
||||
} else {
|
||||
score += 10; // Aktiver Maintainer = gut
|
||||
}
|
||||
} else {
|
||||
score -= 15; // Kein Maintainer = unbekannt
|
||||
}
|
||||
|
||||
// Popularität
|
||||
if info.popularity > 1.0 {
|
||||
score += 10; // Sehr populär
|
||||
} else if info.popularity > 0.1 {
|
||||
score += 5; // Moderat populär
|
||||
} else {
|
||||
score -= 5; // Unbekannt
|
||||
}
|
||||
|
||||
// Votes
|
||||
if info.num_votes > 50 {
|
||||
score += 10;
|
||||
} else if info.num_votes > 10 {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Out of Date
|
||||
if info.out_of_date.is_some() {
|
||||
score -= 15; // Veraltet
|
||||
}
|
||||
|
||||
score.clamp(0, 100)
|
||||
}
|
||||
|
||||
fn calculate_status(score: u32, has_ioc: bool, has_critical: bool) -> ScanStatus {
|
||||
if has_ioc {
|
||||
return ScanStatus::IOCDetected;
|
||||
|
||||
Reference in New Issue
Block a user