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
This commit is contained in:
arch_agent
2026-08-04 10:34:34 +02:00
parent 88072de4fe
commit f015092b1f
4 changed files with 75 additions and 28 deletions
+1
View File
@@ -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
+14 -1
View File
@@ -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,
+1
View File
@@ -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:
+59 -27
View File
@@ -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
;;