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:
+89
-3
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user