Skip to content

Threat feeds

During enrichment, each attacker IP is checked against external reputation services ("threat feeds"). A feed that flags the IP as malicious adds +20 to the attacker's threat score — awarded once per (IP, feed), so repeat events don't re-score — and the verdict is cached as a ThreatFeedEntry row (default 7-day TTL, purged hourly by Celery beat).

Two feeds ship built in: AbuseIPDB and VirusTotal. Both are optional and self-disable when no API key is configured.

AbuseIPDB

AbuseIPDB aggregates community abuse reports and returns a 0–100 abuse confidence score per IP. HoneyDjango treats confidence above 50 as malicious.

Get a free API key:

  1. Create a free account at abuseipdb.com.
  2. Go to your account's API tab and create a key. The free tier allows 1,000 checks per day — plenty for a single honeypot, especially since verdicts are cached for THREAT_FEED_TTL_DAYS.

Configure:

ABUSEIPDB_API_KEY = "your-key-here"   # usually from an env var
Setting Default Purpose
ABUSEIPDB_API_KEY "" (disabled) API key
ABUSEIPDB_MAX_AGE_DAYS 90 Only consider reports newer than this
ABUSEIPDB_TIMEOUT 5.0 HTTP timeout in seconds

VirusTotal

VirusTotal runs each IP past dozens of security engines. HoneyDjango folds the engine verdicts into a single confidence value — the percentage of engines flagging the IP as malicious — and treats above 50 as malicious. The full engine breakdown is kept in the raw response.

Get a free API key:

  1. Create a free account at virustotal.com.
  2. Your API key is under your profile menu → API key. The free tier is rate-limited (4 requests/minute, 500/day) — fine for a honeypot, since the per-IP cache means each attacker is looked up once per TTL, and a 429 is handled gracefully (logged, skipped, never fatal).

Configure:

VIRUSTOTAL_API_KEY = "your-key-here"
Setting Default Purpose
VIRUSTOTAL_API_KEY "" (disabled) API key
VIRUSTOTAL_TIMEOUT 5.0 HTTP timeout in seconds

With no API keys configured

Nothing breaks. Each adapter reports itself unconfigured and is skipped by the feed registry — enrichment still runs GeoIP, TTP classification, and JA3 fingerprinting; attacker profiles simply accrue no feed-based score and no ThreatFeedEntry rows are written. The same graceful degradation applies to network failures, rate limits (429), and malformed responses at runtime: the adapter logs a warning and returns nothing, and the event is enriched without that feed's input.

Caching and expiry

Setting Default Purpose
THREAT_FEED_TTL_DAYS 7 How long a feed verdict is kept

A malicious verdict is stored as a ThreatFeedEntry (IP, source, confidence, category, expiry). The hourly refresh_threat_feeds beat task purges expired rows, so a since-cleaned IP can be re-scored on its next visit.

Writing a custom adapter

Any reputation source can be plugged in with one class. The enrichment pipeline never changes — it always goes through the registry.

1. Write the adapter — subclass ThreatFeedAdapter and implement lookup. The contract: return a ThreatFeedResult, or None when unconfigured or on any failure. Never raise — a flaky feed must not break enrichment.

# honeydj/feeds/adapters/greynoise.py
import logging

import requests
from django.conf import settings

from honeydj.feeds.base import ThreatFeedAdapter, ThreatFeedResult

logger = logging.getLogger(__name__)


class GreyNoiseAdapter(ThreatFeedAdapter):
    name = "greynoise"          # must match a ThreatFeedEntry.Source value
    requires_api_key = True

    @property
    def api_key(self) -> str | None:
        return settings.GREYNOISE_API_KEY or None

    def lookup(self, ip: str) -> ThreatFeedResult | None:
        if not self.api_key:
            return None
        try:
            response = requests.get(
                f"https://api.greynoise.io/v3/community/{ip}",
                headers={"key": self.api_key},
                timeout=5.0,
            )
            response.raise_for_status()
            data = response.json()
        except (requests.RequestException, ValueError, KeyError) as exc:
            logger.warning("GreyNoise lookup failed for %s: %s", ip, exc)
            return None

        malicious = data.get("classification") == "malicious"
        return ThreatFeedResult(
            ip=ip,
            source=self.name,
            confidence=100 if malicious else 0,
            category=data.get("classification"),
            is_malicious=malicious,
            raw=data,
        )

2. Register the sourceThreatFeedResult.source is stored verbatim on ThreatFeedEntry.source, so add a matching choice to ThreatFeedEntry.Source in honeydj/feeds/models.py (and generate a migration).

3. Add it to the registry in honeydj/feeds/registry.py:

FEED_REGISTRY: list[ThreatFeedAdapter] = [
    AbuseIPDBAdapter(),
    VirusTotalAdapter(),
    GreyNoiseAdapter(),
]

That's it. run_all_feeds will skip it while unconfigured and start querying it as soon as its key appears in settings. A key-less feed (a local blocklist file, say) sets requires_api_key = False and needs no api_key override.