From f015092b1f6115e3ace9e0a8edee94698f1979e1 Mon Sep 17 00:00:00 2001 From: arch_agent Date: Tue, 4 Aug 2026 10:34:34 +0200 Subject: [PATCH] feat: scan-only server mode + local build on client Server: - config: server.scan_only flag (default: false) - /api/build returns scan_only=true without building when enabled - /api/status reports scan_only mode Client (safe-yay): - Detects server scan_only from API response - Scan-only server: builds locally with yay/paru after clean scan - Full server: installs from pacman repo as before - --noinstall flag for scan-only without build (was --scan-only) - Suspicious: prompts for local build or repo install depending on server mode --- aur_shield/config.py | 1 + aur_shield/server.py | 15 +++++++- config.example.yaml | 1 + install-client.sh | 86 ++++++++++++++++++++++++++++++-------------- 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/aur_shield/config.py b/aur_shield/config.py index e008874..fa38f4f 100644 --- a/aur_shield/config.py +++ b/aur_shield/config.py @@ -23,6 +23,7 @@ class ServerConfig: repo_dir: str = "/var/cache/aur-shield/repo" work_dir: str = "/var/cache/aur-shield/build" build_user: str = "nobody" + scan_only: bool = False @dataclass diff --git a/aur_shield/server.py b/aur_shield/server.py index 86e3452..8ee6162 100644 --- a/aur_shield/server.py +++ b/aur_shield/server.py @@ -53,6 +53,7 @@ async def status() -> dict[str, Any]: "ollama_url": cfg.ollama.url, "repo_dir": cfg.server.repo_dir, "cached_scans": len(cache.list_all()), + "scan_only": cfg.server.scan_only, } @@ -148,7 +149,19 @@ async def build_endpoint(package: str) -> dict[str, Any]: # Allow suspicious but warn pass - # 5. Build + # 5. Build (skip if scan_only mode) + if cfg.server.scan_only: + return { + "package": package, + "verdict": verdict, + "confidence": confidence, + "findings": findings, + "scan_only": True, + "build_success": False, + "message": "Scan-only mode — build locally on client", + } + + # 6. Build build_result = build_package( source.pkgbuild, package, cfg.server, cfg.build, diff --git a/config.example.yaml b/config.example.yaml index 698a0ed..d466115 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -9,6 +9,7 @@ server: repo_dir: /var/cache/aur-shield/repo work_dir: /var/cache/aur-shield/build build_user: nobody # never build as root + scan_only: true # if true, /api/build returns scan result without building security: block_patterns: diff --git a/install-client.sh b/install-client.sh index 6c12f60..e99a229 100644 --- a/install-client.sh +++ b/install-client.sh @@ -276,10 +276,10 @@ for ioc in d.get('ioc_matches',[]): exit 0 fi -# --scan-only mode -SCAN_ONLY=false -if [ "$1" = "--scan-only" ]; then - SCAN_ONLY=true +# --noinstall mode (scan only, don't build/install) +NOINSTALL=false +if [ "$1" = "--noinstall" ] || [ "$1" = "--scan-only" ]; then + NOINSTALL=true shift fi @@ -288,43 +288,75 @@ EXIT_CODE=0 for pkg in "$@"; do echo "→ Processing $pkg..." - if [ "$SCAN_ONLY" = true ]; then - RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/scan/$pkg" 2>&1) || { - echo " ✗ Failed to scan $pkg" - echo " $RESPONSE" - EXIT_CODE=1 - continue - } - else - RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/build/$pkg" 2>&1) || { - echo " ✗ Failed to process $pkg" - echo " $RESPONSE" - EXIT_CODE=1 - continue - } - fi + # 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 — package verified" - if [ "$SCAN_ONLY" = false ]; then - echo " Installing via pacman..." - sudo pacman -Sy "aur-shield/$pkg" || { - echo " ⚠ Package not in repo yet — may need to wait for build" + 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 [ "$SCAN_ONLY" = false ]; then - read -rp " Install anyway? [y/N] " FORCE + 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 - sudo pacman -Sy "aur-shield/$pkg" + if [ "$SERVER_SCAN_ONLY" = True ]; then + yay -S "$pkg" + else + sudo pacman -Sy "aur-shield/$pkg" + fi fi fi ;;