Files
calvana/ayn-antivirus/ayn_antivirus/signatures/feeds/threatfox.py

118 lines
4.0 KiB
Python

"""ThreatFox feed for AYN Antivirus.
Fetches IOCs (IPs, domains, URLs, hashes) from the abuse.ch ThreatFox
CSV export (free, no API key required).
CSV export: https://threatfox.abuse.ch/export/
"""
from __future__ import annotations
import csv
import io
import logging
from typing import Any, Dict, List
import requests
from ayn_antivirus.signatures.feeds.base_feed import BaseFeed
logger = logging.getLogger(__name__)
_CSV_RECENT_URL = "https://threatfox.abuse.ch/export/csv/recent/"
_CSV_FULL_URL = "https://threatfox.abuse.ch/export/csv/full/"
_TIMEOUT = 60
class ThreatFoxFeed(BaseFeed):
"""Fetch IOCs from ThreatFox CSV export."""
def get_name(self) -> str:
return "threatfox"
def fetch(self) -> List[Dict[str, Any]]:
return self.fetch_recent()
def fetch_recent(self, days: int = 7) -> List[Dict[str, Any]]:
"""Fetch recent IOCs from CSV export."""
self._rate_limit_wait()
self._log("Fetching IOCs from CSV export")
try:
resp = requests.get(_CSV_RECENT_URL, timeout=_TIMEOUT)
resp.raise_for_status()
except requests.RequestException as exc:
self._error("CSV download failed: %s", exc)
return []
results: List[Dict[str, Any]] = []
lines = [l for l in resp.text.splitlines() if l.strip() and not l.startswith("#")]
reader = csv.reader(io.StringIO("\n".join(lines)))
for row in reader:
if len(row) < 6:
continue
# CSV: 0:first_seen, 1:ioc_id, 2:ioc_value, 3:ioc_type,
# 4:threat_type, 5:malware, 6:malware_alias,
# 7:malware_printable, 8:last_seen, 9:confidence,
# 10:reference, 11:tags, 12:reporter
ioc_value = row[2].strip().strip('"')
ioc_type_raw = row[3].strip().strip('"').lower()
threat_type = row[4].strip().strip('"') if len(row) > 4 else ""
malware = row[5].strip().strip('"') if len(row) > 5 else ""
malware_printable = row[7].strip().strip('"') if len(row) > 7 else ""
confidence = row[9].strip().strip('"') if len(row) > 9 else "0"
if not ioc_value:
continue
# Classify IOC type
ioc_type = _classify_ioc(ioc_type_raw, ioc_value)
threat_name = malware_printable or malware or "Unknown"
# Hash IOCs go into hash DB
if ioc_type == "hash":
results.append({
"hash": ioc_value.lower(),
"threat_name": threat_name,
"threat_type": "MALWARE",
"severity": "HIGH",
"source": "threatfox",
"details": f"threat={threat_type}, confidence={confidence}",
})
else:
clean_value = ioc_value
if ioc_type == "ip" and ":" in ioc_value:
clean_value = ioc_value.rsplit(":", 1)[0]
results.append({
"ioc_type": ioc_type,
"value": clean_value,
"threat_name": threat_name,
"type": threat_type or "C2",
"source": "threatfox",
"confidence": int(confidence) if confidence.isdigit() else 0,
})
self._log("Fetched %d IOC(s)", len(results))
self._mark_updated()
return results
def _classify_ioc(raw_type: str, value: str) -> str:
if "ip" in raw_type:
return "ip"
if "domain" in raw_type:
return "domain"
if "url" in raw_type:
return "url"
if "hash" in raw_type or "sha256" in raw_type or "md5" in raw_type:
return "hash"
if value.startswith("http://") or value.startswith("https://"):
return "url"
if len(value) == 64 and all(c in "0123456789abcdef" for c in value.lower()):
return "hash"
if ":" in value and value.replace(".", "").replace(":", "").isdigit():
return "ip"
return "domain"