422 lines
15 KiB
Bash
422 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# AUR-Shield Client Installer
|
|
# Configures pacman to use the AUR-Shield repo
|
|
set -euo pipefail
|
|
|
|
SHIELD_HOST="${AUR_SHIELD_HOST:-10.90.9.102}"
|
|
SHIELD_PORT="${AUR_SHIELD_PORT:-8443}"
|
|
|
|
# Load config from /etc/safe-yay.conf if it exists (overrides defaults)
|
|
if [ -f /etc/safe-yay.conf ]; then
|
|
source /etc/safe-yay.conf
|
|
fi
|
|
|
|
echo "╔══════════════════════════════════════════════╗"
|
|
echo "║ AUR-Shield Client Installer ║"
|
|
echo "╚══════════════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
if ! command -v pacman &>/dev/null; then
|
|
echo "✗ This script requires Arch Linux (pacman)."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Server: http://$SHIELD_HOST:$SHIELD_PORT"
|
|
echo ""
|
|
|
|
# Quick connectivity check (3s timeout)
|
|
if ! curl -sf --max-time 3 "http://$SHIELD_HOST:$SHIELD_PORT/api/status" >/dev/null 2>&1; then
|
|
echo "⚠ WARNING: Server at $SHIELD_HOST:$SHIELD_PORT not reachable!"
|
|
echo " Continuing anyway — config will be written but safe-yay may fail."
|
|
echo ""
|
|
fi
|
|
|
|
# Write config file
|
|
echo "Writing /etc/safe-yay.conf..."
|
|
sudo tee /etc/safe-yay.conf > /dev/null << EOFCONF
|
|
# AUR-Shield client configuration
|
|
SHIELD_HOST="$SHIELD_HOST"
|
|
SHIELD_PORT="$SHIELD_PORT"
|
|
EOFCONF
|
|
echo "✓ Config written to /etc/safe-yay.conf"
|
|
|
|
# Add pacman repo (skip if scan-only server — no repo needed)
|
|
# Check if server is scan-only by querying status
|
|
SERVER_SCAN_ONLY=$(curl -sf --max-time 5 "http://$SHIELD_HOST:$SHIELD_PORT/api/status" 2>/dev/null | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('scan_only',False))" 2>/dev/null || echo False)
|
|
|
|
PACMAN_CONF="/etc/pacman.conf"
|
|
|
|
if [ "$SERVER_SCAN_ONLY" = True ]; then
|
|
echo "✓ Scan-Only Server erkannt — kein pacman repo nötig (client baut lokal)"
|
|
# Remove aur-shield repo if it exists from a previous full-mode install
|
|
if grep -q "^\[aur-shield\]" "$PACMAN_CONF"; then
|
|
echo " Entferne veralteten [aur-shield] Eintrag aus pacman.conf..."
|
|
sudo sed -i '/^\[aur-shield\]/,/^\[/{/^\[aur-shield\]/d; /^\[/!d}' "$PACMAN_CONF"
|
|
sudo sed -i '/^# AUR-Shield/d' "$PACMAN_CONF"
|
|
echo " ✓ Entfernt"
|
|
fi
|
|
else
|
|
REPO_ENTRY="[aur-shield]
|
|
Server = http://$SHIELD_HOST:$SHIELD_PORT/repo
|
|
SigLevel = Never"
|
|
|
|
if grep -q "^\[aur-shield\]" "$PACMAN_CONF"; then
|
|
echo "⚠ [aur-shield] already exists — updating server URL..."
|
|
sudo sed -i "/^\[aur-shield\]/,/^\[/ s|Server = .*|Server = http://$SHIELD_HOST:$SHIELD_PORT/repo|" "$PACMAN_CONF"
|
|
echo "✓ Updated repo server URL"
|
|
else
|
|
echo "Adding [aur-shield] to $PACMAN_CONF..."
|
|
sudo tee -a "$PACMAN_CONF" > /dev/null << EOF
|
|
|
|
# AUR-Shield — AI-scanned AUR packages
|
|
$REPO_ENTRY
|
|
EOF
|
|
echo "✓ Added repo entry"
|
|
fi
|
|
fi
|
|
|
|
# Install safe-yay wrapper
|
|
WRAPPER_PATH="/usr/local/bin/safe-yay"
|
|
echo "Installing safe-yay wrapper to $WRAPPER_PATH..."
|
|
|
|
sudo tee "$WRAPPER_PATH" > /dev/null << 'WRAPPER'
|
|
#!/usr/bin/env bash
|
|
# safe-yay — wrapper that routes AUR packages through AUR-Shield
|
|
set -euo pipefail
|
|
|
|
SHIELD_HOST="${AUR_SHIELD_HOST:-10.90.9.102}"
|
|
SHIELD_PORT="${AUR_SHIELD_PORT:-8443}"
|
|
|
|
# Load config from /etc/safe-yay.conf if it exists (overrides defaults)
|
|
if [ -f /etc/safe-yay.conf ]; then
|
|
source /etc/safe-yay.conf
|
|
fi
|
|
|
|
# Exit codes (archcanary-compatible)
|
|
# 0 = clean, 1 = warning, 2 = malicious/blocked
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "Usage: safe-yay <package> [package2 ...]"
|
|
echo " safe-yay -Syu # System-Update mit AUR-Scan"
|
|
echo " safe-yay --scan-only <pkg> # Scan without building"
|
|
echo " safe-yay --check-installed # Installierte AUR-Pakete pruefen"
|
|
echo " safe-yay --doctor # Check AUR-Shield setup"
|
|
exit 0
|
|
fi
|
|
|
|
# --doctor health check
|
|
if [ "$1" = "--doctor" ]; then
|
|
echo "AUR-Shield Doctor"
|
|
echo "================"
|
|
echo ""
|
|
STATUS=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/status" 2>/dev/null)
|
|
if [ $? -eq 0 ]; then
|
|
MODEL=$(echo "$STATUS" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('model','?'))" 2>/dev/null)
|
|
CACHE=$(echo "$STATUS" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('cached_scans',0))" 2>/dev/null)
|
|
echo " ✓ Server: http://$SHIELD_HOST:$SHIELD_PORT"
|
|
echo " Model: $MODEL"
|
|
echo " Cached scans: $CACHE"
|
|
else
|
|
echo " ✗ Server: http://$SHIELD_HOST:$SHIELD_PORT NOT REACHABLE"
|
|
exit 1
|
|
fi
|
|
OLLAMA=$(curl -sf "http://$SHIELD_HOST:11434/api/tags" 2>/dev/null)
|
|
if [ $? -eq 0 ]; then
|
|
echo " ✓ Ollama: running"
|
|
else
|
|
echo " ⚠ Ollama: not reachable on $SHIELD_HOST"
|
|
fi
|
|
if grep -q "^\[aur-shield\]" /etc/pacman.conf 2>/dev/null; then
|
|
echo " ✓ pacman repo: [aur-shield] configured"
|
|
else
|
|
echo " ⚠ pacman repo: [aur-shield] NOT in /etc/pacman.conf"
|
|
fi
|
|
echo ""
|
|
echo "All checks passed."
|
|
exit 0
|
|
fi
|
|
|
|
# --check-installed: pruefe bereits installierte AUR-Pakete gegen IOC-Listen
|
|
if [ "$1" = "--check-installed" ]; then
|
|
echo "AUR-Shield: Installierte AUR-Pakete pruefen"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
# Foreign packages = AUR + manuell installierte
|
|
FOREIGN=$(pacman -Qmq 2>/dev/null)
|
|
if [ -z "$FOREIGN" ]; then
|
|
echo "Keine AUR-Pakete installiert."
|
|
exit 0
|
|
fi
|
|
|
|
COUNT=$(echo "$FOREIGN" | wc -l)
|
|
echo "Pruefe $COUNT installierte AUR-Pakete gegen IOC-Listen..."
|
|
echo ""
|
|
|
|
MALICIOUS=0
|
|
SUSPICIOUS=0
|
|
CLEAN=0
|
|
|
|
for pkg in $FOREIGN; do
|
|
RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/scan/$pkg" 2>/dev/null)
|
|
if [ $? -ne 0 ]; then
|
|
continue
|
|
fi
|
|
VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('verdict','error'))" 2>/dev/null)
|
|
case "$VERDICT" in
|
|
malicious)
|
|
echo " ✗ MALICIOUS: $pkg"
|
|
echo "$RESPONSE" | python3 -c "
|
|
import sys,json
|
|
d = json.loads(sys.stdin.read())
|
|
for ioc in d.get('ioc_matches',[]):
|
|
print(f\" IOC: {ioc['source']} — {ioc['description']}\")
|
|
" 2>/dev/null
|
|
MALICIOUS=$((MALICIOUS + 1))
|
|
;;
|
|
suspicious)
|
|
echo " ⚠ SUSPICIOUS: $pkg"
|
|
SUSPICIOUS=$((SUSPICIOUS + 1))
|
|
;;
|
|
clean)
|
|
CLEAN=$((CLEAN + 1))
|
|
;;
|
|
esac
|
|
done
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Ergebnis:"
|
|
echo " Clean: $CLEAN"
|
|
echo " Suspicious: $SUSPICIOUS"
|
|
echo " Malicious: $MALICIOUS"
|
|
echo "=========================================="
|
|
|
|
if [ $MALICIOUS -gt 0 ]; then
|
|
exit 2
|
|
elif [ $SUSPICIOUS -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# -Syu: System-Update — scanne AUR-Updates bevor sie installiert werden
|
|
if [ "$1" = "-Syu" ] || [ "$1" = "-Sy" ]; then
|
|
echo "AUR-Shield: System-Update mit Pre-Scan"
|
|
echo "======================================="
|
|
echo ""
|
|
|
|
# Erst normales Repo-Update (ohne AUR)
|
|
echo "→ Repo-Updates..."
|
|
sudo pacman -Sy 2>&1
|
|
|
|
# AUR-Updates finden mit yay
|
|
if command -v yay &>/dev/null; then
|
|
echo ""
|
|
echo "→ Pruefe AUR-Updates..."
|
|
AUR_UPDATES=$(yay -Qua 2>/dev/null || true)
|
|
|
|
if [ -z "$AUR_UPDATES" ]; then
|
|
echo " Keine AUR-Updates."
|
|
else
|
|
echo " AUR-Updates gefunden:"
|
|
echo "$AUR_UPDATES"
|
|
echo ""
|
|
echo "→ Scanne AUR-Updates durch AUR-Shield..."
|
|
|
|
BLOCKED=0
|
|
SAFE_UPDATES=""
|
|
|
|
while IFS= read -r line; do
|
|
pkg=$(echo "$line" | awk '{print $1}')
|
|
pkg=$(echo "$pkg" | sed 's/[+]$//') # strip + suffix from yay output
|
|
|
|
RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/scan/$pkg" 2>/dev/null)
|
|
if [ $? -ne 0 ]; then
|
|
echo " ⚠ $pkg: Scan fehlgeschlagen — ueberspringe"
|
|
SAFE_UPDATES="$SAFE_UPDATES $pkg"
|
|
continue
|
|
fi
|
|
|
|
VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('verdict','error'))" 2>/dev/null)
|
|
|
|
case "$VERDICT" in
|
|
clean)
|
|
echo " ✓ $pkg: clean"
|
|
SAFE_UPDATES="$SAFE_UPDATES $pkg"
|
|
;;
|
|
suspicious)
|
|
echo " ⚠ $pkg: SUSPICIOUS"
|
|
echo "$RESPONSE" | python3 -c "
|
|
import sys,json
|
|
d = json.loads(sys.stdin.read())
|
|
for f in d.get('findings',[])[:3]:
|
|
print(f\" {f}\")
|
|
" 2>/dev/null
|
|
read -rp " Update $pkg trotzdem? [y/N] " FORCE
|
|
if [[ "${FORCE,,}" == "y" ]]; then
|
|
SAFE_UPDATES="$SAFE_UPDATES $pkg"
|
|
fi
|
|
;;
|
|
malicious)
|
|
echo " ✗ $pkg: MALICIOUS — BLOCKIERT!"
|
|
echo "$RESPONSE" | python3 -c "
|
|
import sys,json
|
|
d = json.loads(sys.stdin.read())
|
|
for ioc in d.get('ioc_matches',[]):
|
|
print(f\" IOC: {ioc['source']} — {ioc['description']}\")
|
|
" 2>/dev/null
|
|
BLOCKED=$((BLOCKED + 1))
|
|
;;
|
|
esac
|
|
done <<< "$AUR_UPDATES"
|
|
|
|
echo ""
|
|
if [ $BLOCKED -gt 0 ]; then
|
|
echo "⚠ $BLOCKED Paket(e) blockiert!"
|
|
fi
|
|
|
|
# Safe updates installieren
|
|
if [ -n "$SAFE_UPDATES" ]; then
|
|
echo "→ Installiere sichere AUR-Updates: $SAFE_UPDATES"
|
|
yay -S $SAFE_UPDATES
|
|
else
|
|
echo "→ Keine sicheren AUR-Updates."
|
|
fi
|
|
fi
|
|
else
|
|
echo " ⚠ yay nicht installiert — AUR-Updates manuell pruefen"
|
|
fi
|
|
|
|
# Normale Repo-Updates installieren
|
|
echo ""
|
|
echo "→ Repo-Updates installieren..."
|
|
sudo pacman -Su 2>&1
|
|
|
|
echo ""
|
|
echo "System-Update abgeschlossen."
|
|
exit 0
|
|
fi
|
|
|
|
# --noinstall mode (scan only, don't build/install)
|
|
NOINSTALL=false
|
|
if [ "$1" = "--noinstall" ] || [ "$1" = "--scan-only" ]; then
|
|
NOINSTALL=true
|
|
shift
|
|
fi
|
|
|
|
EXIT_CODE=0
|
|
|
|
for pkg in "$@"; do
|
|
echo "→ Processing $pkg..."
|
|
|
|
# Always scan first (server handles IOC + regex + LLM)
|
|
RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/scan/$pkg" 2>&1) || {
|
|
echo " ✗ Failed to scan $pkg"
|
|
echo " $RESPONSE"
|
|
EXIT_CODE=1
|
|
continue
|
|
}
|
|
|
|
VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('verdict','error'))" 2>/dev/null)
|
|
SERVER_SCAN_ONLY=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('scan_only',False))" 2>/dev/null)
|
|
|
|
case "$VERDICT" in
|
|
clean)
|
|
echo " ✓ Clean — verified by AUR-Shield"
|
|
if [ "$NOINSTALL" = true ]; then
|
|
echo " (scan only — not installing)"
|
|
elif [ "$SERVER_SCAN_ONLY" = True ]; then
|
|
# Scan-only server — build locally
|
|
echo " Building locally..."
|
|
if command -v yay &>/dev/null; then
|
|
yay -S "$pkg" --noconfirm 2>&1 || {
|
|
echo " ⚠ Build failed"
|
|
EXIT_CODE=1
|
|
}
|
|
elif command -v paru &>/dev/null; then
|
|
paru -S "$pkg" --noconfirm 2>&1 || {
|
|
echo " ⚠ Build failed"
|
|
EXIT_CODE=1
|
|
}
|
|
else
|
|
echo " ⚠ No AUR helper (yay/paru) — cannot build"
|
|
EXIT_CODE=1
|
|
fi
|
|
else
|
|
# Full server — install from repo
|
|
echo " Installing via pacman repo..."
|
|
sudo pacman -Sy "aur-shield/$pkg" || {
|
|
echo " ⚠ Not in repo — building locally..."
|
|
if command -v yay &>/dev/null; then
|
|
yay -S "$pkg"
|
|
else
|
|
echo " ⚠ yay not installed — cannot build"
|
|
EXIT_CODE=1
|
|
fi
|
|
}
|
|
fi
|
|
;;
|
|
suspicious)
|
|
echo " ⚠ Suspicious — review recommended"
|
|
echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg"
|
|
echo "$RESPONSE" | python3 -c "
|
|
import sys,json
|
|
d = json.loads(sys.stdin.read())
|
|
for f in d.get('findings',[])[:5]:
|
|
print(f\" {f}\")
|
|
" 2>/dev/null
|
|
EXIT_CODE=1
|
|
if [ "$NOINSTALL" = false ]; then
|
|
if [ "$SERVER_SCAN_ONLY" = True ]; then
|
|
read -rp " Build locally anyway? [y/N] " FORCE
|
|
else
|
|
read -rp " Install anyway? [y/N] " FORCE
|
|
fi
|
|
if [[ "${FORCE,,}" == "y" ]]; then
|
|
if [ "$SERVER_SCAN_ONLY" = True ]; then
|
|
yay -S "$pkg"
|
|
else
|
|
sudo pacman -Sy "aur-shield/$pkg"
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
malicious)
|
|
echo " ✗ MALICIOUS — package blocked!"
|
|
echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg"
|
|
echo "$RESPONSE" | python3 -c "
|
|
import sys,json
|
|
d = json.loads(sys.stdin.read())
|
|
for ioc in d.get('ioc_matches',[]):
|
|
print(f\" IOC: {ioc['source']} — {ioc['description']}\")
|
|
" 2>/dev/null
|
|
EXIT_CODE=2
|
|
;;
|
|
*)
|
|
echo " ? Error: $RESPONSE"
|
|
EXIT_CODE=1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
exit $EXIT_CODE
|
|
WRAPPER
|
|
|
|
sudo chmod +x "$WRAPPER_PATH"
|
|
echo "✓ Installed safe-yay"
|
|
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════════╗"
|
|
echo "║ Client setup complete! ║"
|
|
echo "╠══════════════════════════════════════════════╣"
|
|
echo "║ ║"
|
|
echo "║ Usage: ║"
|
|
echo "║ safe-yay <package> ║"
|
|
echo "║ sudo pacman -S aur-shield/<package> ║"
|
|
echo "║ ║"
|
|
echo "║ Set server via env: ║"
|
|
echo "║ export AUR_SHIELD_HOST=10.90.9.102 ║"
|
|
echo "║ export AUR_SHIELD_PORT=8443 ║"
|
|
echo "║ ║"
|
|
echo "╚══════════════════════════════════════════════╝" |