118 lines
4.0 KiB
Python
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"
|