From f6b5ec4031234d25af0747f33ce3d50996ae931f Mon Sep 17 00:00:00 2001 From: arch_agent Date: Tue, 4 Aug 2026 09:42:02 +0200 Subject: [PATCH] feat: extended IOC sources + archcanary features Extended IOC sources (from archcanary): - aur-audit.wtako.net black/red API (3rd-party continuous AUR scanner) - Community reports list (community-curated malicious packages) - CHAOS RAT campaign list (backdoor payload) - Russian spam campaign list (.bashrc injection) Client features (archcanary-inspired): - Exit codes: 0=clean, 1=warning, 2=malicious (scriptable) - --doctor health check (server, ollama, repo status) - --scan-only mode (scan without building) - IOC match display in malicious blocks - Suspicious packages: interactive install prompt All IOC fetches run concurrently for speed. --- README.md | 15 ++- aur_shield/extended_iocs.py | 195 ++++++++++++++++++++++++++++++++++++ aur_shield/scanner.py | 9 +- install-client.sh | 114 +++++++++++++++++---- 4 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 aur_shield/extended_iocs.py diff --git a/README.md b/README.md index 9cbe64b..fd26ef2 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ cache: ## Threat Intelligence Sources -Based on [AegisAUR](https://gitea.die-heimatlosen.eu/arch_agent/aegisaur) IOC fetcher: +Based on [AegisAUR](https://gitea.die-heimatlosen.eu/arch_agent/aegisaur) IOC fetcher + [archcanary](https://github.com/musqz/archcanary) extended sources: | Source | Type | Freshness | URL | |--------|------|-----------|-----| @@ -116,6 +116,19 @@ Based on [AegisAUR](https://gitea.die-heimatlosen.eu/arch_agent/aegisaur) IOC fe | Atomic Arch Gist | GitHub Gist | Versioned | `gist.githubusercontent.com/Kidev/...` | | Arch Security | Official advisory | Slow but authoritative | `security.archlinux.org` | | AUR RPC | API | Real-time | `aur.archlinux.org/rpc/v5` | +| aur-audit (black) | 3rd-party API | Continuous | `aur-audit.wtako.net/api/black` | +| aur-audit (red) | 3rd-party API | Continuous | `aur-audit.wtako.net/api/red` | +| Community Reports | Curated list | Manual | `github.com/musqz/archcanary` | +| CHAOS RAT | Campaign list | Manual | `github.com/musqz/archcanary` | +| Russian Spam | Campaign list | Manual | `github.com/musqz/archcanary` | + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Clean — no indicators found | +| 1 | Warning — suspicious, review recommended | +| 2 | Malicious — package blocked | ## API diff --git a/aur_shield/extended_iocs.py b/aur_shield/extended_iocs.py new file mode 100644 index 0000000..947ce3d --- /dev/null +++ b/aur_shield/extended_iocs.py @@ -0,0 +1,195 @@ +"""Extended IOC sources — additional threat intel from archcanary-inspired feeds. + +Sources: +1. aur-audit.wtako.net — third-party AUR audit API (black/red lists) +2. Community reports — community-curated malicious package list +3. CHAOS RAT list — additional threat campaign +4. Russian spam campaign — separate list +""" +from __future__ import annotations + +import asyncio +import json +from typing import Any + +import httpx + +from .ioc_fetcher import IOCEntry, ThreatType, Confidence + +# aur-audit.wtako.net API +AUR_AUDIT_BLACK_URL = "https://aur-audit.wtako.net/api/black" +AUR_AUDIT_RED_URL = "https://aur-audit.wtako.net/api/red" + +# Community reports (archcanary repo) +COMMUNITY_REPORTS_URL = ( + "https://raw.githubusercontent.com/musqz/archcanary/master/lists/" + "community_reports.txt" +) + +# CHAOS RAT campaign +CHAOS_RAT_URL = ( + "https://raw.githubusercontent.com/musqz/archcanary/master/lists/" + "chaos_rat_packages.txt" +) + +# Russian spam campaign +RUSSIAN_SPAM_URL = ( + "https://raw.githubusercontent.com/musqz/archcanary/master/lists/" + "malicious_russian_spam_packages.txt" +) + + +def _valid_pkg(name: str) -> bool: + if not name or len(name) > 100: + return False + return all( + c.isascii() and (c.islower() or c.isdigit() or c in "-_.") + for c in name + ) + + +def _parse_line_list(text: str, source: str, threat: ThreatType, + confidence: Confidence, description: str) -> list[IOCEntry]: + """Parse a plaintext one-package-per-line list.""" + threats = [] + for line in text.splitlines(): + pkg = line.strip() + if pkg.startswith(("- ", "* ", "#")): + pkg = pkg[2:].strip() if not pkg.startswith("#") else "" + if _valid_pkg(pkg): + threats.append(IOCEntry( + package_name=pkg, + threat_type=threat, + source=source, + description=description, + confidence=confidence, + )) + return threats + + +async def _fetch_aur_audit_black(client: httpx.AsyncClient) -> list[IOCEntry]: + """Fetch confirmed-malicious packages from aur-audit.wtako.net.""" + try: + resp = await client.get(AUR_AUDIT_BLACK_URL) + if resp.status_code != 200: + return [] + data = resp.json() + except Exception: + return [] + + threats = [] + packages = data if isinstance(data, list) else data.get("packages", []) + for pkg in packages: + name = pkg if isinstance(pkg, str) else pkg.get("name", "") + if _valid_pkg(name): + threats.append(IOCEntry( + package_name=name, + threat_type=ThreatType.MALICIOUS_BUILD, + source="aur-audit:black", + description="Confirmed malicious by aur-audit.wtako.net", + confidence=Confidence.CRITICAL, + )) + return threats + + +async def _fetch_aur_audit_red(client: httpx.AsyncClient) -> list[IOCEntry]: + """Fetch high-risk packages from aur-audit.wtako.net.""" + try: + resp = await client.get(AUR_AUDIT_RED_URL) + if resp.status_code != 200: + return [] + data = resp.json() + except Exception: + return [] + + threats = [] + packages = data if isinstance(data, list) else data.get("packages", []) + for pkg in packages: + name = pkg if isinstance(pkg, str) else pkg.get("name", "") + if _valid_pkg(name): + threats.append(IOCEntry( + package_name=name, + threat_type=ThreatType.MALICIOUS_BUILD, + source="aur-audit:red", + description="High-risk by aur-audit.wtako.net", + confidence=Confidence.HIGH, + )) + return threats + + +async def _fetch_community_reports(client: httpx.AsyncClient) -> list[IOCEntry]: + """Fetch community-reported malicious packages.""" + try: + resp = await client.get(COMMUNITY_REPORTS_URL) + if resp.status_code != 200: + return [] + return _parse_line_list( + resp.text, + source="community_reports", + threat=ThreatType.UNKNOWN("CommunityReport"), + confidence=Confidence.MEDIUM, + description="Reported by community (unverified)", + ) + except Exception: + return [] + + +async def _fetch_chaos_rat(client: httpx.AsyncClient) -> list[IOCEntry]: + """Fetch CHAOS RAT campaign packages.""" + try: + resp = await client.get(CHAOS_RAT_URL) + if resp.status_code != 200: + return [] + return _parse_line_list( + resp.text, + source="chaos_rat", + threat=ThreatType.BACKDOOR, + confidence=Confidence.CRITICAL, + description="CHAOS RAT campaign — backdoor payload", + ) + except Exception: + return [] + + +async def _fetch_russian_spam(client: httpx.AsyncClient) -> list[IOCEntry]: + """Fetch Russian spam campaign packages.""" + try: + resp = await client.get(RUSSIAN_SPAM_URL) + if resp.status_code != 200: + return [] + return _parse_line_list( + resp.text, + source="russian_spam", + threat=ThreatType.UNKNOWN("SpamInjection"), + confidence=Confidence.MEDIUM, + description="Russian spam campaign — .bashrc injection", + ) + except Exception: + return [] + + +async def fetch_extended_iocs() -> list[IOCEntry]: + """Fetch IOCs from extended sources (archcanary-inspired). + + Run all fetches concurrently with individual error handling. + """ + async with httpx.AsyncClient(timeout=30) as client: + results = await asyncio.gather( + _fetch_aur_audit_black(client), + _fetch_aur_audit_red(client), + _fetch_community_reports(client), + _fetch_chaos_rat(client), + _fetch_russian_spam(client), + return_exceptions=True, + ) + + all_threats = [] + source_names = [ + "aur-audit:black", "aur-audit:red", "community_reports", + "chaos_rat", "russian_spam", + ] + for name, r in zip(source_names, results): + if isinstance(r, list) and r: + all_threats.extend(r) + + return all_threats \ No newline at end of file diff --git a/aur_shield/scanner.py b/aur_shield/scanner.py index 5c85c8e..81c2f74 100644 --- a/aur_shield/scanner.py +++ b/aur_shield/scanner.py @@ -1,6 +1,7 @@ """LLM scanner — sends PKGBUILD to Ollama for security analysis.""" from __future__ import annotations +import asyncio import json import re import subprocess @@ -16,6 +17,7 @@ from .ioc_fetcher import ( fetch_all_iocs, check_package_against_iocs, check_typosquatting, IOCEntry, IOCResult, Confidence, ) +from .extended_iocs import fetch_extended_iocs class ScanVerdict(str, Enum): @@ -49,7 +51,12 @@ async def pre_check_iocs(package: str) -> tuple[list[str], list[dict], list[dict typo_dicts = [] try: - iocs = await fetch_all_iocs() + # Fetch from both base + extended IOC sources concurrently + base_iocs, ext_iocs = await asyncio.gather( + fetch_all_iocs(), + fetch_extended_iocs(), + ) + iocs = base_iocs + ext_iocs # Exact match result = check_package_against_iocs(package, iocs) diff --git a/install-client.sh b/install-client.sh index 6ea8b82..174343d 100644 --- a/install-client.sh +++ b/install-client.sh @@ -49,52 +49,130 @@ set -euo pipefail SHIELD_HOST="${AUR_SHIELD_HOST:-localhost}" SHIELD_PORT="${AUR_SHIELD_PORT:-8443}" +# Exit codes (archcanary-compatible) +# 0 = clean, 1 = warning, 2 = malicious/blocked + if [ $# -eq 0 ]; then echo "Usage: safe-yay [package2 ...]" echo "Scans and builds AUR packages through AUR-Shield" + echo "" + echo "Options:" + echo " --scan-only Scan without building" + echo " --doctor Check AUR-Shield setup" exit 0 fi +# --doctor health check +if [ "$1" = "--doctor" ]; then + echo "AUR-Shield Doctor" + echo "================" + echo "" + + # Check server + 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 + + # Check ollama + 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 + + # Check repo + 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 + +# --scan-only mode +SCAN_ONLY=false +if [ "$1" = "--scan-only" ]; then + SCAN_ONLY=true + shift +fi + +EXIT_CODE=0 + for pkg in "$@"; do echo "→ Processing $pkg..." - # Call AUR-Shield to scan + build - RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/build/$pkg" 2>&1) || { - echo "✗ Failed to process $pkg" - echo " $RESPONSE" - continue - } + 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 - # Extract verdict 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 " ✓ Clean — package built and added to repo" + 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" + EXIT_CODE=1 + } + fi ;; suspicious) - echo " ⚠ Suspicious — package built but review recommended" + echo " ⚠ Suspicious — review recommended" echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg" + EXIT_CODE=1 + if [ "$SCAN_ONLY" = false ]; then + read -rp " Install anyway? [y/N] " FORCE + if [[ "${FORCE,,}" == "y" ]]; then + sudo pacman -Sy "aur-shield/$pkg" + fi + fi ;; malicious) echo " ✗ MALICIOUS — package blocked!" echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg" - continue + # Show IOC matches if available + 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" - continue + EXIT_CODE=1 ;; esac - - # Now install via pacman - echo " Installing via pacman..." - sudo pacman -Sy "aur-shield/$pkg" || { - echo " ⚠ Package not in repo yet — may need to wait for build" - } done -echo "Done." +exit $EXIT_CODE WRAPPER sudo chmod +x "$WRAPPER_PATH"