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.
This commit is contained in:
@@ -108,7 +108,7 @@ cache:
|
|||||||
|
|
||||||
## Threat Intelligence Sources
|
## 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 |
|
| 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/...` |
|
| Atomic Arch Gist | GitHub Gist | Versioned | `gist.githubusercontent.com/Kidev/...` |
|
||||||
| Arch Security | Official advisory | Slow but authoritative | `security.archlinux.org` |
|
| Arch Security | Official advisory | Slow but authoritative | `security.archlinux.org` |
|
||||||
| AUR RPC | API | Real-time | `aur.archlinux.org/rpc/v5` |
|
| 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
|
## API
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""LLM scanner — sends PKGBUILD to Ollama for security analysis."""
|
"""LLM scanner — sends PKGBUILD to Ollama for security analysis."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -16,6 +17,7 @@ from .ioc_fetcher import (
|
|||||||
fetch_all_iocs, check_package_against_iocs,
|
fetch_all_iocs, check_package_against_iocs,
|
||||||
check_typosquatting, IOCEntry, IOCResult, Confidence,
|
check_typosquatting, IOCEntry, IOCResult, Confidence,
|
||||||
)
|
)
|
||||||
|
from .extended_iocs import fetch_extended_iocs
|
||||||
|
|
||||||
|
|
||||||
class ScanVerdict(str, Enum):
|
class ScanVerdict(str, Enum):
|
||||||
@@ -49,7 +51,12 @@ async def pre_check_iocs(package: str) -> tuple[list[str], list[dict], list[dict
|
|||||||
typo_dicts = []
|
typo_dicts = []
|
||||||
|
|
||||||
try:
|
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
|
# Exact match
|
||||||
result = check_package_against_iocs(package, iocs)
|
result = check_package_against_iocs(package, iocs)
|
||||||
|
|||||||
+91
-13
@@ -49,52 +49,130 @@ set -euo pipefail
|
|||||||
SHIELD_HOST="${AUR_SHIELD_HOST:-localhost}"
|
SHIELD_HOST="${AUR_SHIELD_HOST:-localhost}"
|
||||||
SHIELD_PORT="${AUR_SHIELD_PORT:-8443}"
|
SHIELD_PORT="${AUR_SHIELD_PORT:-8443}"
|
||||||
|
|
||||||
|
# Exit codes (archcanary-compatible)
|
||||||
|
# 0 = clean, 1 = warning, 2 = malicious/blocked
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
echo "Usage: safe-yay <package> [package2 ...]"
|
echo "Usage: safe-yay <package> [package2 ...]"
|
||||||
echo "Scans and builds AUR packages through AUR-Shield"
|
echo "Scans and builds AUR packages through AUR-Shield"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --scan-only <pkg> Scan without building"
|
||||||
|
echo " --doctor Check AUR-Shield setup"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
for pkg in "$@"; do
|
||||||
echo "→ Processing $pkg..."
|
echo "→ Processing $pkg..."
|
||||||
|
|
||||||
# Call AUR-Shield to scan + build
|
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) || {
|
RESPONSE=$(curl -sf "http://$SHIELD_HOST:$SHIELD_PORT/api/build/$pkg" 2>&1) || {
|
||||||
echo " ✗ Failed to process $pkg"
|
echo " ✗ Failed to process $pkg"
|
||||||
echo " $RESPONSE"
|
echo " $RESPONSE"
|
||||||
|
EXIT_CODE=1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# Extract verdict
|
|
||||||
VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('verdict','error'))" 2>/dev/null)
|
VERDICT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('verdict','error'))" 2>/dev/null)
|
||||||
|
|
||||||
case "$VERDICT" in
|
case "$VERDICT" in
|
||||||
clean)
|
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)
|
suspicious)
|
||||||
echo " ⚠ Suspicious — package built but review recommended"
|
echo " ⚠ Suspicious — review recommended"
|
||||||
echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg"
|
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)
|
malicious)
|
||||||
echo " ✗ MALICIOUS — package blocked!"
|
echo " ✗ MALICIOUS — package blocked!"
|
||||||
echo " Report: http://$SHIELD_HOST:$SHIELD_PORT/api/report/$pkg"
|
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"
|
echo " ? Error: $RESPONSE"
|
||||||
continue
|
EXIT_CODE=1
|
||||||
;;
|
;;
|
||||||
esac
|
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
|
done
|
||||||
|
|
||||||
echo "Done."
|
exit $EXIT_CODE
|
||||||
WRAPPER
|
WRAPPER
|
||||||
|
|
||||||
sudo chmod +x "$WRAPPER_PATH"
|
sudo chmod +x "$WRAPPER_PATH"
|
||||||
|
|||||||
Reference in New Issue
Block a user