Skip to content

About

Threat-intelligence aggregator corroborating AbuseIPDB, AlienVault OTX and VirusTotal into one verdict.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

🍵 Threat Tea

Steep three noisy threat feeds into one calm cup.

python license tests interface metrics transmits

Three feeds, three verdicts, three different answers. Now what?


threat-tea takes an IP address, asks three reputation providers about it in parallel-ish, boils the answers down to a single verdict + score, and caches the result so you never pay for the same lookup twice. It ships a Rich terminal UI, a Flask dashboard, a JSON API, and Prometheus metrics for Grafana.

It is a defensive tool: it only reads public reputation data about indicators you supply. It never scans, attacks, or touches a target host.


1. What it does and why it exists

Threat intel providers answer the same question — "is this IP bad?" — in three different dialects:

Provider Signal it gives How threat-tea scores it
AbuseIPDB abuse-confidence % + report count uses the confidence score directly (0–100)
AlienVault OTX number of pulses referencing the IP min(100, pulses × 20)
VirusTotal last-analysis malicious/suspicious counts min(100, malicious×15 + suspicious×5)

An analyst enriching an alert shouldn't have to open three browser tabs, learn three JSON shapes, and eyeball three scoring scales. threat-tea normalizes all of it into one Finding:

indicator · type · max_score (0–100) · verdict · [per-source rows with raw evidence]

The headline max_score is the worst thing any single source said (an analyst wants the loudest alarm), but every source row is kept verbatim — including the raw provider JSON — so you can always audit why. The UI also shows corroboration (hits = how many sources independently flagged it), which is what separates "one twitchy feed" from "everybody agrees."

Verdict thresholds (stable — there's a test pinning these):

score verdict colour
≥ 75 high red
40–74 medium yellow
1–39 low cyan
0 informational green (clean)

2. How it works (data flow)

        check-ip 45.83.64.1
                │
                ▼
   ┌─────────────────────────┐   validate.parse_ip()
   │  1. VALIDATE + NORMALIZE │   • must be a real IP (ipaddress)  → blocks URL injection
   │                         │   • must be public (is_global)     → won't leak internal hosts
   └─────────────────────────┘   • canonicalized (dedupe key)
                │
                ▼
   ┌─────────────────────────┐   aggregate.aggregate_ip()
   │  2. FAN OUT TO SOURCES   │   AbuseIPDB / OTX / VirusTotal
   │                         │   • unconfigured key → "skipped", not fatal
   │                         │   • one source erroring → recorded, others continue
   └─────────────────────────┘
                │
                ▼
   ┌─────────────────────────┐   Finding(indicator, max_score, verdict, sources…)
   │  3. NORMALIZE → FINDING  │   max_score = worst source; hits = # that flagged it
   └─────────────────────────┘
                │
                ▼
   ┌─────────────────────────┐   store.save_finding()  (SQLite, upsert by indicator)
   │  4. CACHE                │   findings(indicator PK, type, max_score, verdict,
   └─────────────────────────┘             sources_json, observed_at)
                │
                ▼
   ┌─────────────────────────┐   Rich panel/table (human) · pure JSON (--json) ·
   │  5. RENDER / SERVE       │   Flask dashboard (/) · JSON API · /metrics (Grafana)
   └─────────────────────────┘

The network fan-out is lazy about failure: a missing API key or a provider outage becomes a "skipped" row with a short reason, never a crash. With no keys configured at all, check-ip does zero network I/O and returns an informational finding — which is exactly why the whole test suite runs offline.


3. CLI usage

Global flags (before the subcommand):

flag effect
--db PATH SQLite cache location (env THREAT_TEA_DB, default threat_tea.sqlite3)
--plain / --no-color disable colour/decoration (also honours NO_COLOR)
--version print version

Output rules, applied everywhere:

  • --json → pure JSON to stdout, always, with zero decoration.
  • piped / --plain / NO_COLOR / not-a-TTY → clean plain text (the same lines older scripts already parse).
  • interactive terminal → the Rich banner, panels, tables, and spinners.

check-ip — look up one IP

threat-tea check-ip 45.83.64.1 --yes
threat-tea check-ip 8.8.8.8 --yes --json           # machine-readable
threat-tea check-ip 10.0.0.5 --yes --allow-private # opt in to private ranges
flag meaning
-y, --yes acknowledge you're authorized to query these indicators (or set THREAT_TEA_ASSUME_YES=1)
--allow-private permit private/reserved/loopback IPs (blocked by default)
--json emit the finding as pure JSON

Sample interactive output:

╭─ 🍵 threat-tea ─────────────────────────────╮
│  steep three noisy feeds into one calm cup  │
│  authorized threat-intel lookups only       │
╰─────────────────────────────────────────────╯
╭─ finding ───────────────────────────────────────────────────────╮
│                                                                  │
│  45.83.64.1   high (92)   2 source(s) flagged it                 │
│                                                                  │
│   source      score  summary                                     │
│   ───────────────────────────────────────────────────────────   │
│   abuseipdb      92   Abuse confidence 92; reports 431           │
│   otx           60   OTX pulse references 3                      │
│   virustotal     0   VT malicious=0, suspicious=0                │
│                                                                  │
╰──────────────────────────────────────────────────────────────────╯

Plain/piped output stays as the original one line:

45.83.64.1 high score=92

collect — batch a file of IPs

threat-tea collect --input iocs.txt --yes
threat-tea collect --input iocs.txt --yes --delay 1.5   # throttle for rate limits
threat-tea collect --input iocs.txt --yes --json > out.json

Blank lines and # comments are ignored, duplicates are de-duped (first-seen order), and invalid or private lines are skipped and reported on stderr — the run continues. The file is size-capped (5 MB / 100k lines) so a runaway file can't exhaust memory.

flag meaning
--input PATH newline-delimited list of IPs (required)
--delay SECONDS pause between lookups to respect provider rate limits (default 0)
-y, --yes / --allow-private / --json as above

findings — show what's cached

threat-tea findings                       # table in a terminal, JSON when piped
threat-tea findings --verdict high        # filter
threat-tea findings --limit 20
threat-tea findings --json | jq '.[].indicator'

findings prints JSON whenever it isn't drawing to an interactive terminal, so threat-tea findings > report.json keeps working exactly as before. In a real terminal you get a sorted table (worst verdict first) with score, corroboration hits, source count, and freshness.

serve — dashboard, API, metrics

threat-tea serve                              # binds 127.0.0.1:5064 by default
threat-tea serve --host 127.0.0.1 --port 8080
route purpose
GET / HTML dashboard
GET /api/findings cached findings as JSON
GET /metrics threat_tea_risk_score, threat_tea_indicators_total for Prometheus

Every response carries hardening headers (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, a strict Content-Security-Policy, Referrer-Policy).


4. Safety & authorization notes

This is a defensive, read-only tool, but it still sends every indicator you give it to third parties. threat-tea makes that explicit:

  • Consent gate. Network commands (check-ip, collect) refuse to run until you acknowledge authorization — pass --yes, set THREAT_TEA_ASSUME_YES=1, or answer the prompt on an interactive terminal. Piped with no acknowledgement, it refuses (exit 2) rather than silently phoning home about someone's IPs.
  • No internal-topology leaks. Private/reserved/loopback addresses are blocked by default — the providers have no data on them and submitting them can leak your internal layout. Override consciously with --allow-private.
  • Injection-safe. Every indicator is parsed with the stdlib ipaddress module before it can be spliced into a provider URL, so 1.2.3.4/../../x never leaves the process. SQL uses bound parameters throughout.
  • No secrets in output. API keys live in request headers, never URLs or logs; source errors are truncated to one short line.
  • Safe serve defaults. Binds to 127.0.0.1; warns on any non-loopback bind; and refuses --debug on a non-loopback bind because Werkzeug's interactive debugger is a remote-code-execution surface.
  • Bounded input. The collect file is size- and line-capped.

Only look up indicators you are authorized to investigate.


5. Setup, running, and tests

Install

cd threat-tea
python -m venv .venv && source .venv/bin/activate   # optional
pip install -r requirements.txt
cp .env.example .env      # then add whatever keys you have

Configure whichever keys you have (any subset works; missing ones are skipped):

ABUSEIPDB_API_KEY=...
OTX_API_KEY=...
VIRUSTOTAL_API_KEY=...

Run without installing (PYTHONPATH)

PYTHONPATH=src python -m threat_tea.cli check-ip 8.8.8.8 --yes
PYTHONPATH=src python -m threat_tea.cli findings
PYTHONPATH=src python -m threat_tea.cli serve

Tests

Fast, offline, no network and no sleeps:

PYTHONPATH=src python -m pytest -q

Grafana stack (optional)

docker compose up --build

Prometheus scrapes /metrics; Grafana is provisioned at http://127.0.0.1:3000 (admin / threat-tea).


Prerequisites

Python 3.11+, plus flask, requests, prometheus-client, and rich (all in requirements.txt). Docker only for the Grafana stack.

References


🏷️ Why "Threat Tea"?

Steep the feeds, pour one cup. AbuseIPDB, AlienVault OTX and VirusTotal rarely agree perfectly, and whichever one you happened to query first shouldn't get to decide your verdict. Corroboration across all three beats trusting any single source — so the output tells you not just how bad an IP looks, but how many independent sources actually back that up.


🔬 How this tool was built

One of ten defensive tools rebuilt to a single standard — understand, reimagine, build the CLI, harden, document, verify. The goal was never to change what these tools are, but to take each one as far as it could honestly go on correctness, safety and the experience of the person actually running it at 2am.

Purpose. A calm IP-reputation aggregator: give it an indicator and it steeps AbuseIPDB, OTX, and VirusTotal into one color-coded verdict with corroboration and raw evidence — safe by default, pure JSON for scripts.

How it works. Each IP is validated and canonically normalized, then queried against each configured source (keys stay in request headers). Results are cached in SQLite as findings carrying a max score, a verdict, and a hits count of how many sources independently flagged the IP. collect batches a file of indicators; findings filters and renders the store.

Standout CLI. A check-ip finding panel with a verdict chip, corroboration line, and per-source score/summary table; a sorted findings table (worst first) with score/hits/source-count/freshness; a collect progress bar and summary panel; a consistent severity ramp (green clean → cyan whisper → yellow look → red act-now).

Key improvements. Functional: strict IP validation with IPv6 supported end-to-end (correct OTX IPv4/IPv6 endpoints, dedupes compressed vs. expanded forms); the corroboration hits signal; collect de-dupes, skips+reports invalid/private lines while continuing, caps file size (5 MB / 100k lines), and supports --delay; parametrized SQL filters. Security: the consent gate on egress (--yes / THREAT_TEA_ASSUME_YES=1 / interactive; piped-without-ack is refused, exit 2); private/reserved/loopback IPs blocked by default (--allow-private to override) so you don't leak internal topology to third parties; every indicator parsed with stdlib ipaddress before it can enter a provider URL; API keys never in URLs or logs, error summaries trimmed to 160 chars.

Example.

PYTHONPATH=src python -m threat_tea.cli check-ip 45.83.64.1 --yes


⚖️ Authorized use & safety charter

These are defensive tools for systems, files, networks, and people you own or are explicitly authorized to assess. Read this before you run anything:

  • Authorization is not optional. Phishing simulations, network scans, IP reputation lookups, and web-app probing all touch other people's systems or data. Get written authorization first. Several tools refuse to act until you assert it (--yes, --authorized-training, --i-have-authorization, --i-am-authorized, and friends).
  • Safe by default. Every web UI binds to 127.0.0.1 (loopback). Egress, live sends, and active scans are gated behind explicit flags — dry-run, passive, or refuse-and-warn is always the default.
  • No accidental foot-guns. Flask --debug (the Werkzeug interactive debugger is remote code execution on any traceback) is warned about loudly and refused entirely off loopback. Untrusted input is size-capped, validated, and rendered inertly so a hostile file or log line can't crash — or take over — your terminal.
  • No secrets in output. API keys stay in request headers, passwords come from the environment, and nothing sensitive is logged or printed.

These are not offensive tooling. They contain no exploits, no credential harvesters, and no payloads. If a tool could be misused, it is built to resist that misuse by construction.


🧪 Development

python -m venv .venv && source .venv/bin/activate
pip install -e .
pytest -q                 # 24 tests, no network, no sleeps
threat-tea --help

📄 License

Released under the MIT License. See LICENSE.

Defensive tooling. No exploits, no payloads, no credential harvesters.

About

Threat-intelligence aggregator corroborating AbuseIPDB, AlienVault OTX and VirusTotal into one verdict.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages