feat: IOC pre-check via public threat intel (AegisAUR integration)

- ioc_fetcher.py: fetches from HedgeDoc, Atomic Arch Gist, Arch Security
  Tracker, AUR RPC orphan detection (concurrent)
- scanner.py: IOC pre-check before LLM scan — known malicious packages
  get instant MALICIOUS verdict without LLM cost
- typosquatting check with Levenshtein distance
- server.py: API returns ioc_matches + typosquat_matches
- README: threat intel sources documented

Sources ported from AegisAUR (Rust) to Python.
This commit is contained in:
arch_agent
2026-08-04 09:39:53 +02:00
parent adee5dfc78
commit 7f46bc8f9a
4 changed files with 383 additions and 8 deletions
+89 -3
View File
@@ -12,6 +12,10 @@ import httpx
from .config import OllamaConfig, SecurityConfig
from .aur_client import extract_sources, extract_install_hooks
from .ioc_fetcher import (
fetch_all_iocs, check_package_against_iocs,
check_typosquatting, IOCEntry, IOCResult, Confidence,
)
class ScanVerdict(str, Enum):
@@ -30,6 +34,61 @@ class ScanResult:
reasoning: str = ""
sources_checked: list[str] = field(default_factory=list)
raw_response: str = ""
ioc_matches: list[dict] = field(default_factory=list)
typosquat_matches: list[dict] = field(default_factory=list)
async def pre_check_iocs(package: str) -> tuple[list[str], list[dict], list[dict]]:
"""Check package against public IOC lists before LLM scan.
Returns (findings, ioc_matches, typosquat_matches).
If ioc_matches contains critical entries, the package is known malicious.
"""
findings = []
ioc_dicts = []
typo_dicts = []
try:
iocs = await fetch_all_iocs()
# Exact match
result = check_package_against_iocs(package, iocs)
if result.matches:
for m in result.matches:
ioc_dicts.append({
"package": m.package_name,
"threat": m.threat_type.value,
"source": m.source,
"confidence": m.confidence.value,
"description": m.description,
})
if m.confidence == Confidence.CRITICAL:
findings.append(
f"KNOWN MALICIOUS: '{package}' found in {m.source} "
f"({m.threat_type.value}: {m.description})"
)
else:
findings.append(
f"Suspicious: '{package}' flagged in {m.source} "
f"({m.threat_type.value})"
)
# Typosquatting
typo_matches = check_typosquatting(package, iocs)
for m in typo_matches:
typo_dicts.append({
"package": m.package_name,
"threat": m.threat_type.value,
"source": m.source,
"confidence": m.confidence.value,
"description": m.description,
})
findings.append(f"Typosquatting: {m.description}")
except Exception as e:
findings.append(f"IOC check error (non-blocking): {e}")
return findings, ioc_dicts, typo_dicts
SYSTEM_PROMPT = """\
@@ -76,7 +135,27 @@ async def scan_pkgbuild(
ollama_cfg: OllamaConfig,
sec_cfg: SecurityConfig,
) -> ScanResult:
"""Scan a PKGBUILD with regex pre-check + LLM analysis."""
"""Scan a PKGBUILD with IOC pre-check + regex pre-scan + LLM analysis."""
# 0. IOC pre-check — public threat intelligence
ioc_findings, ioc_matches, typo_matches = await pre_check_iocs(package_name)
# If package is known malicious from public lists, skip LLM scan
has_critical_ioc = any(
m.get("confidence") == "Critical" for m in ioc_matches
)
if has_critical_ioc:
return ScanResult(
package=package_name,
verdict=ScanVerdict.MALICIOUS,
confidence=1.0,
findings=ioc_findings,
reasoning="Package found in public IOC lists (known malicious). "
"LLM scan skipped — package is confirmed malicious by "
"Arch Linux security advisories.",
ioc_matches=ioc_matches,
typosquat_matches=typo_matches,
)
# 1. Fast regex pre-scan
regex_findings = _pre_scan(pkgbuild, sec_cfg)
@@ -124,8 +203,8 @@ async def scan_pkgbuild(
raw = data.get("message", {}).get("content", "")
result = _parse_llm_response(raw)
# Combine with regex findings
all_findings = result.get("findings", []) + regex_findings
# Combine with regex + IOC findings
all_findings = result.get("findings", []) + regex_findings + ioc_findings
verdict = result.get("verdict", "suspicious")
confidence = result.get("confidence", 0.5)
@@ -134,6 +213,11 @@ async def scan_pkgbuild(
verdict = "suspicious"
confidence = max(confidence, 0.7)
# If IOC found non-critical matches, escalate
if ioc_matches and verdict == "clean":
verdict = "suspicious"
confidence = max(confidence, 0.8)
return ScanResult(
package=package_name,
verdict=ScanVerdict(verdict),
@@ -142,6 +226,8 @@ async def scan_pkgbuild(
reasoning=result.get("reasoning", ""),
sources_checked=sources,
raw_response=raw,
ioc_matches=ioc_matches,
typosquat_matches=typo_matches,
)
except Exception as e: