threattrove 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. threattrove-2.0.1/LICENSE +21 -0
  2. threattrove-2.0.1/PKG-INFO +347 -0
  3. threattrove-2.0.1/README.md +320 -0
  4. threattrove-2.0.1/pyproject.toml +53 -0
  5. threattrove-2.0.1/setup.cfg +4 -0
  6. threattrove-2.0.1/threattrove/__init__.py +23 -0
  7. threattrove-2.0.1/threattrove/__main__.py +11 -0
  8. threattrove-2.0.1/threattrove/bulk.py +401 -0
  9. threattrove-2.0.1/threattrove/cli.py +700 -0
  10. threattrove-2.0.1/threattrove/core.py +729 -0
  11. threattrove-2.0.1/threattrove/sources.py +972 -0
  12. threattrove-2.0.1/threattrove/static/app.js +75 -0
  13. threattrove-2.0.1/threattrove/static/favicon.svg +7 -0
  14. threattrove-2.0.1/threattrove/static/style.css +303 -0
  15. threattrove-2.0.1/threattrove/templates/api.html +88 -0
  16. threattrove-2.0.1/threattrove/templates/base.html +66 -0
  17. threattrove-2.0.1/threattrove/templates/bulk.html +71 -0
  18. threattrove-2.0.1/threattrove/templates/bulk_result.html +106 -0
  19. threattrove-2.0.1/threattrove/templates/error.html +14 -0
  20. threattrove-2.0.1/threattrove/templates/index.html +86 -0
  21. threattrove-2.0.1/threattrove/templates/local.html +68 -0
  22. threattrove-2.0.1/threattrove/templates/recent.html +30 -0
  23. threattrove-2.0.1/threattrove/templates/result.html +125 -0
  24. threattrove-2.0.1/threattrove/templates/sources.html +63 -0
  25. threattrove-2.0.1/threattrove/web.py +578 -0
  26. threattrove-2.0.1/threattrove.egg-info/PKG-INFO +347 -0
  27. threattrove-2.0.1/threattrove.egg-info/SOURCES.txt +29 -0
  28. threattrove-2.0.1/threattrove.egg-info/dependency_links.txt +1 -0
  29. threattrove-2.0.1/threattrove.egg-info/entry_points.txt +3 -0
  30. threattrove-2.0.1/threattrove.egg-info/requires.txt +7 -0
  31. threattrove-2.0.1/threattrove.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 threattrove contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.4
2
+ Name: threattrove
3
+ Version: 2.0.1
4
+ Summary: Self-hosted reputation lookup for file hashes, URLs, domains and IPs
5
+ Author: threattrove
6
+ License: MIT
7
+ Project-URL: Homepage, https://threattrove.com
8
+ Project-URL: Documentation, https://threattrove.com/api
9
+ Project-URL: Source, https://threattrove.com
10
+ Keywords: threat-intelligence,malware,hash,virustotal,ioc,osint
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Flask
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests<3,>=2.31
21
+ Requires-Dist: flask<4,>=3.0
22
+ Requires-Dist: gunicorn<24,>=21.2
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: ruff>=0.5; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # ThreatTrove
29
+
30
+ **Self-hosted reputation lookup for file hashes, URLs, domains and IPs — built to run on [threattrove.com](https://threattrove.com) or on your own box.**
31
+
32
+ ThreatTrove does the three things most people open VirusTotal for: paste an
33
+ indicator, get a verdict, and see *which* sources said what. It runs keyless out
34
+ of the box (8 sources, no account anywhere) and gets sharper as you add API keys.
35
+
36
+ ```
37
+ $ threattrove 44d88612fea8a8f36de82e1278abb02f
38
+
39
+ ━━ 44d88612fea8a8f36de82e1278abb02f
40
+
41
+ File hash
42
+ MALICIOUS 100% confidence Flagged as malicious by 2 security source(s)
43
+
44
+ SECURITY SOURCES
45
+ ──────────────────────────────────────────────────────────────────────────
46
+ ● circl MALICIOUS listed by malshare.com
47
+ ● otx MALICIOUS 50 pulse(s)
48
+
49
+ elapsed 1.14s · 2 security sources queried
50
+ ```
51
+
52
+ That's the EICAR test file, found by two keyless sources with no API key set.
53
+
54
+ ---
55
+
56
+ ## Why another one
57
+
58
+ * **Keyless by default.** CIRCL hashlookup, AlienVault OTX, urlscan.io, crt.sh,
59
+ DNS and RDAP answer without registration, so a fresh clone is useful in ~30s.
60
+ * **One verdict, many voices.** Each source gets its own row with status, note
61
+ and (where it exists) an engine ratio; the verdict is a transparent score, not
62
+ a black box.
63
+ * **Built for bulk.** CSV/feed ingestion in the CLI and in the web UI, with
64
+ SQLite-backed jobs you can poll and export.
65
+ * **Own your data.** Everything — cache, bulk jobs, your own labels — lives in a
66
+ single SQLite file you control. Nothing phones home.
67
+ * **Small.** Flask + requests + gunicorn. No queue, no Redis, no JS build step.
68
+
69
+ ---
70
+
71
+ ## Quickstart
72
+
73
+ ```bash
74
+ pip install threattrove
75
+
76
+ threattrove 44d88612fea8a8f36de82e1278abb02f # CLI
77
+ threattrove-web --port 8080 # UI + API on http://127.0.0.1:8080
78
+ ```
79
+
80
+ Or from source:
81
+
82
+ ```bash
83
+ git clone https://github.com/threattrove/threattrove && cd threattrove
84
+ python3 -m venv .venv && . .venv/bin/activate
85
+ pip install -e .
86
+ ```
87
+
88
+ Python 3.9+. Runtime dependencies: `requests`, `flask`, `gunicorn`.
89
+
90
+ ---
91
+
92
+ ## CLI
93
+
94
+ ```bash
95
+ # one indicator, text report
96
+ threattrove 275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
97
+ threattrove https://example.com/login
98
+ threattrove example.com --json
99
+
100
+ # hash a local file and look it up
101
+ threattrove --hash-file ./suspicious.exe
102
+
103
+ # bulk: CSV, newline list or JSON/JSONL feed (format auto-detected)
104
+ threattrove --csv iocs.csv --out results.csv
105
+ threattrove --feed hashes.txt --out results.jsonl
106
+ threattrove --feed feed.json --fail-on-malicious # exit 3 if anything is bad
107
+
108
+ # sources and engine tuning
109
+ threattrove --list-sources
110
+ threattrove ioc.example --sources keyless # only keyless sources
111
+ threattrove ioc.example --sources reputation,intel # by category
112
+ threattrove ioc.example --no-cache --timeout 60 --workers 8
113
+
114
+ # local corpus: your own known-bad / known-good labels
115
+ threattrove --add-local 275a021b... --label malware --note "EICAR test file"
116
+ threattrove --list-local
117
+ threattrove --stats
118
+ ```
119
+
120
+ Exit codes: `0` success, `1` usage error, `3` `--fail-on-malicious` tripped.
121
+
122
+ ---
123
+
124
+ ## Web UI
125
+
126
+ | Page | What it does |
127
+ | --- | --- |
128
+ | `/` | Paste a hash, URL or domain; recent lookups |
129
+ | `/i/hash?q=…`, `/i/url?q=…`, `/i/domain?q=…` | Result page: verdict, score, per-source table |
130
+ | `/bulk` | Upload a CSV/feed or paste a list; runs as a background job |
131
+ | `/bulk/<job_id>` | Job progress, per-row verdicts, CSV/JSON export |
132
+ | `/recent` | Recent lookups on this instance |
133
+ | `/sources` | Source catalogue with live state (`active` / `needs key` / `opt-in`) |
134
+ | `/local` | Manage the local corpus |
135
+ | `/api` | JSON API documentation |
136
+
137
+ ---
138
+
139
+ ## JSON API
140
+
141
+ All lookups accept `?fresh=1` to bypass the cache. Responses are
142
+ VirusTotal-shaped (`data.attributes`-ish flat object with `verdict`, `score`,
143
+ `confidence`, `results[]`).
144
+
145
+ ```bash
146
+ curl -s http://127.0.0.1:8080/api/v1/hash/44d88612fea8a8f36de82e1278abb02f
147
+ curl -s "http://127.0.0.1:8080/api/v1/url?url=https://example.com/login"
148
+ curl -s "http://127.0.0.1:8080/api/v1/domain?domain=example.com"
149
+ curl -s "http://127.0.0.1:8080/api/v1/lookup?q=$(echo -n evil | sha256sum | cut -d' ' -f1)"
150
+ curl -s http://127.0.0.1:8080/api/v1/sources
151
+ curl -s http://127.0.0.1:8080/api/v1/stats
152
+ ```
153
+
154
+ Bulk over the API — submit, then poll:
155
+
156
+ ```bash
157
+ curl -s -X POST http://127.0.0.1:8080/api/v1/bulk \
158
+ -F file=@iocs.csv -F label=incident-42
159
+ # {"job_id":"j_ab12…","total":120,"skipped":0,"status_url":"…/api/v1/bulk/j_ab12…"}
160
+
161
+ curl -s http://127.0.0.1:8080/api/v1/bulk/j_ab12…
162
+ curl -s http://127.0.0.1:8080/api/v1/bulk/j_ab12…/items
163
+ ```
164
+
165
+ Raw bodies work too (`text/plain`, one indicator per line). Limits come from
166
+ `TT_RATE_LIMIT` (default 60/min per IP) and `TT_MAX_UPLOAD` (2 MiB).
167
+
168
+ ---
169
+
170
+ ## How a verdict is produced
171
+
172
+ Each source returns `verdict`, `detections`/`total` (engines), `note` and a raw
173
+ `detail`. Weights are additive:
174
+
175
+ | Condition | Points |
176
+ | --- | --- |
177
+ | Source says malicious, ≥5 engines involved | +100 |
178
+ | Source says malicious (list membership, few engines) | +40…+100 |
179
+ | Source says suspicious | +40 |
180
+ | Source says known-good (NSRL, whitelist) | −100 |
181
+ | Source says clean | −20 |
182
+
183
+ ```
184
+ score ≥ 100 → MALICIOUS
185
+ score ≥ 40 → SUSPICIOUS
186
+ score ≤ −100 → KNOWN GOOD
187
+ score ≤ −20 → CLEAN
188
+ otherwise → UNKNOWN (no data, or sources disagree)
189
+ ```
190
+
191
+ `confidence = min(100, abs(score))`. Sources in the `intel` category (DNS, RDAP,
192
+ crt.sh, reverse IP) enrich the report but never change the verdict. Engine
193
+ ratios are only computed from sources that actually report engines — a urlscan
194
+ "6987 scans" is a scan count, not a detection count, and is shown as `–`.
195
+
196
+ Deliberate anti-noise choices: OTX pulses that carry `Whitelisted domain` or
197
+ `Known False Positive` validation are downgraded to UNKNOWN; EICAR-style
198
+ keyless hits are attributed to the *listing* source rather than multiplied.
199
+
200
+ ---
201
+
202
+ ## Sources
203
+
204
+ 21 sources are compiled in; 8 work with no key. `httpx`-free HTTP via `requests`.
205
+
206
+ | Source | Kind | Category | Key | Env var |
207
+ | --- | --- | --- | --- | --- |
208
+ | CIRCL hashlookup | hash | reputation | keyless | — |
209
+ | AlienVault OTX (file) | hash | reputation | keyless | — |
210
+ | AlienVault OTX (URL) | url | reputation | keyless | — |
211
+ | AlienVault OTX (domain/IP) | domain/ip | reputation | keyless | — |
212
+ | urlscan.io | url/domain/ip | reputation | keyless | — |
213
+ | DNS (Cloudflare DoH) | domain/ip | intel | keyless | — |
214
+ | RDAP | domain/ip | intel | keyless | — |
215
+ | crt.sh | domain | intel | keyless | — |
216
+ | reverse IP (HackerTarget) | domain/ip | intel | keyless, **opt-in** | `TT_ENABLE_REVERSE_IP=1` |
217
+ | VirusTotal (file/url/domain) | hash/url/domain/ip | reputation | keyed | `VT_API_KEY` |
218
+ | MalwareBazaar | hash | reputation | keyed | `ABUSECH_AUTH_KEY` |
219
+ | ThreatFox | hash/url/domain/ip | reputation | keyed | `ABUSECH_AUTH_KEY` |
220
+ | URLhaus (hash + URL) | hash/url | reputation | keyed | `ABUSECH_AUTH_KEY` |
221
+ | Hybrid Analysis | hash | reputation | keyed | `HA_API_KEY` |
222
+ | MalShare | hash | reputation | keyed | `MALSHARE_API_KEY` |
223
+ | Triage | hash | reputation | keyed | `TRIAGE_API_KEY` |
224
+ | Google Safe Browsing | url/domain | reputation | keyed | `GSB_API_KEY` |
225
+ | PhishTank | url | reputation | keyed | `PHISHTANK_APPKEY` |
226
+ | urlscan (keyed) | url/domain/ip | reputation | keyed | `URLSCAN_API_KEY` |
227
+
228
+ Notes:
229
+
230
+ * abuse.ch (MalwareBazaar, ThreatFox, URLhaus) requires a free Auth-Key from
231
+ <https://auth.abuse.ch/> since API 3.0.0. A bad key answers **403**, which
232
+ ThreatTrove reports as `unauthorized` — the run continues.
233
+ * `TT_ENABLE_REVERSE_IP` is off by default because that lookup sends the queried
234
+ IP/hostname to a third-party service. That's the one source gated on consent.
235
+ * Sources without a key are simply skipped and listed as `needs key` in
236
+ `/sources`; they never fail a lookup.
237
+
238
+ ---
239
+
240
+ ## Configuration
241
+
242
+ Copy `deploy/threattrove.env.example` and edit. The important ones:
243
+
244
+ | Variable | Default | Meaning |
245
+ | --- | --- | --- |
246
+ | `TT_DB` | `~/.threattrove/threattrove.db` | cache + jobs + local corpus |
247
+ | `TT_CACHE_TTL` | `604800` | cache lifetime, seconds |
248
+ | `TT_TIMEOUT` | `20` | per-source HTTP timeout |
249
+ | `TT_DEADLINE` | `45` | whole-lookup budget, seconds |
250
+ | `TT_BULK_MAX` | `5000` | indicators per bulk job / CLI run |
251
+ | `TT_MAX_JOBS` | `2` | concurrent bulk jobs per worker |
252
+ | `TT_MAX_UPLOAD` | `2097152` | upload cap, bytes |
253
+ | `TT_RATE_LIMIT` | `60` | API requests/min per IP |
254
+ | `TT_PAGE_RATE_LIMIT` | `240` | page requests/min per IP |
255
+ | `TT_SITE_URL` | `https://threattrove.com` | canonical base URL |
256
+ | `TT_SITE_NAME` | `ThreatTrove` | brand shown in nav, titles, OG tags |
257
+ | `TT_ENABLE_REVERSE_IP` | *(empty)* | `1` enables reverse-IP |
258
+
259
+ ---
260
+
261
+ ## Deployment
262
+
263
+ ### Docker
264
+
265
+ ```bash
266
+ cp deploy/threattrove.env.example .env # add keys if you have them
267
+ docker compose up -d --build
268
+ curl -s http://127.0.0.1:8080/health
269
+ ```
270
+
271
+ Data persists in the `threattrove-data` volume (`/data/threattrove.db`). The image
272
+ runs as an unprivileged user with a read-only root filesystem.
273
+
274
+ ### systemd + nginx on threattrove.com
275
+
276
+ ```bash
277
+ useradd --system --home /opt/threattrove --shell /usr/sbin/nologin threattrove
278
+ git clone <repo> /opt/threattrove && cd /opt/threattrove
279
+ python3 -m venv .venv && .venv/bin/pip install -r requirements.txt && .venv/bin/pip install --no-deps .
280
+ install -d -o threattrove -g threattrove /var/lib/threattrove
281
+ install -Dm640 deploy/threattrove.env.example /etc/threattrove/threattrove.env
282
+ install -Dm644 deploy/threattrove.service /etc/systemd/system/threattrove.service
283
+ systemctl daemon-reload && systemctl enable --now threattrove
284
+
285
+ # TLS + reverse proxy
286
+ apt install nginx certbot python3-certbot-nginx
287
+ install -Dm644 deploy/nginx-threattrove.conf /etc/nginx/sites-available/threattrove.com
288
+ ln -s /etc/nginx/sites-available/threattrove.com /etc/nginx/sites-enabled/
289
+ rm -f /etc/nginx/sites-enabled/default
290
+ nginx -t && systemctl reload nginx
291
+ certbot --nginx -d threattrove.com -d www.threattrove.com
292
+ ```
293
+
294
+ `deploy/gunicorn.conf.py` binds `127.0.0.1:8080`, 2 gthread workers × 8 threads,
295
+ 120s timeout (bulk lookups are slow by nature). Nginx adds TLS/HSTS, gzip,
296
+ static caching and per-zone rate limits.
297
+
298
+ **Public-instance checklist**
299
+
300
+ 1. Set `TT_RATE_LIMIT` low (30–60) — every lookup fans out to 8+ upstreams, and
301
+ they rate-limit *you* first.
302
+ 2. Keep `TT_ENABLE_REVERSE_IP` off unless your privacy notice says otherwise.
303
+ 3. Add `X-Forwarded-For` handling (already configured) so the limiter sees real
304
+ clients.
305
+ 4. Cache is shared, so identical indicators are only queried upstream once per
306
+ `TT_CACHE_TTL`.
307
+ 5. `/health` is the liveness endpoint used by the Docker healthcheck.
308
+
309
+ ---
310
+
311
+ ## Project layout
312
+
313
+ ```
314
+ threattrove/
315
+ core.py parse indicators, Engine (threaded, paced, deadline), Store, Report
316
+ sources.py 21 source implementations + verdict helpers
317
+ bulk.py CSV/JSON/newline parsing, job runner, CSV export
318
+ cli.py `threattrove` entry point
319
+ web.py Flask app factory + `threattrove-web`
320
+ templates/ Jinja2 views
321
+ static/ app.js, style.css, favicon.svg
322
+ scripts/smoke.py offline parser/verdict checks + live lookups
323
+ deploy/ gunicorn, systemd, nginx, env example
324
+ legacy/threattrove-v1-cli.py original single-file CLI (superseded, kept for reference)
325
+ Dockerfile, docker-compose.yml
326
+ ```
327
+
328
+ ## Testing
329
+
330
+ ```bash
331
+ .venv/bin/python scripts/smoke.py --offline # 13 parsing + 6 verdict checks
332
+ .venv/bin/python scripts/smoke.py # + live hash/URL/domain lookups
333
+ ```
334
+
335
+ ## Limitations
336
+
337
+ * ThreatTrove never uploads files — it hashes locally and only sends hashes.
338
+ * Keyless sources are free, so they are shallow: absence of a hit means "no data",
339
+ never "safe". Treat `UNKNOWN` as unknown.
340
+ * No user accounts, no per-user quota: the rate limiter is per IP, meant for a
341
+ single node behind a proxy.
342
+ * Bulk jobs run inside the web worker process. Two workers means two independent
343
+ job pools (fine for this scale; swap in a real queue if you outgrow it).
344
+
345
+ ## License
346
+
347
+ MIT.
@@ -0,0 +1,320 @@
1
+ # ThreatTrove
2
+
3
+ **Self-hosted reputation lookup for file hashes, URLs, domains and IPs — built to run on [threattrove.com](https://threattrove.com) or on your own box.**
4
+
5
+ ThreatTrove does the three things most people open VirusTotal for: paste an
6
+ indicator, get a verdict, and see *which* sources said what. It runs keyless out
7
+ of the box (8 sources, no account anywhere) and gets sharper as you add API keys.
8
+
9
+ ```
10
+ $ threattrove 44d88612fea8a8f36de82e1278abb02f
11
+
12
+ ━━ 44d88612fea8a8f36de82e1278abb02f
13
+
14
+ File hash
15
+ MALICIOUS 100% confidence Flagged as malicious by 2 security source(s)
16
+
17
+ SECURITY SOURCES
18
+ ──────────────────────────────────────────────────────────────────────────
19
+ ● circl MALICIOUS listed by malshare.com
20
+ ● otx MALICIOUS 50 pulse(s)
21
+
22
+ elapsed 1.14s · 2 security sources queried
23
+ ```
24
+
25
+ That's the EICAR test file, found by two keyless sources with no API key set.
26
+
27
+ ---
28
+
29
+ ## Why another one
30
+
31
+ * **Keyless by default.** CIRCL hashlookup, AlienVault OTX, urlscan.io, crt.sh,
32
+ DNS and RDAP answer without registration, so a fresh clone is useful in ~30s.
33
+ * **One verdict, many voices.** Each source gets its own row with status, note
34
+ and (where it exists) an engine ratio; the verdict is a transparent score, not
35
+ a black box.
36
+ * **Built for bulk.** CSV/feed ingestion in the CLI and in the web UI, with
37
+ SQLite-backed jobs you can poll and export.
38
+ * **Own your data.** Everything — cache, bulk jobs, your own labels — lives in a
39
+ single SQLite file you control. Nothing phones home.
40
+ * **Small.** Flask + requests + gunicorn. No queue, no Redis, no JS build step.
41
+
42
+ ---
43
+
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ pip install threattrove
48
+
49
+ threattrove 44d88612fea8a8f36de82e1278abb02f # CLI
50
+ threattrove-web --port 8080 # UI + API on http://127.0.0.1:8080
51
+ ```
52
+
53
+ Or from source:
54
+
55
+ ```bash
56
+ git clone https://github.com/threattrove/threattrove && cd threattrove
57
+ python3 -m venv .venv && . .venv/bin/activate
58
+ pip install -e .
59
+ ```
60
+
61
+ Python 3.9+. Runtime dependencies: `requests`, `flask`, `gunicorn`.
62
+
63
+ ---
64
+
65
+ ## CLI
66
+
67
+ ```bash
68
+ # one indicator, text report
69
+ threattrove 275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
70
+ threattrove https://example.com/login
71
+ threattrove example.com --json
72
+
73
+ # hash a local file and look it up
74
+ threattrove --hash-file ./suspicious.exe
75
+
76
+ # bulk: CSV, newline list or JSON/JSONL feed (format auto-detected)
77
+ threattrove --csv iocs.csv --out results.csv
78
+ threattrove --feed hashes.txt --out results.jsonl
79
+ threattrove --feed feed.json --fail-on-malicious # exit 3 if anything is bad
80
+
81
+ # sources and engine tuning
82
+ threattrove --list-sources
83
+ threattrove ioc.example --sources keyless # only keyless sources
84
+ threattrove ioc.example --sources reputation,intel # by category
85
+ threattrove ioc.example --no-cache --timeout 60 --workers 8
86
+
87
+ # local corpus: your own known-bad / known-good labels
88
+ threattrove --add-local 275a021b... --label malware --note "EICAR test file"
89
+ threattrove --list-local
90
+ threattrove --stats
91
+ ```
92
+
93
+ Exit codes: `0` success, `1` usage error, `3` `--fail-on-malicious` tripped.
94
+
95
+ ---
96
+
97
+ ## Web UI
98
+
99
+ | Page | What it does |
100
+ | --- | --- |
101
+ | `/` | Paste a hash, URL or domain; recent lookups |
102
+ | `/i/hash?q=…`, `/i/url?q=…`, `/i/domain?q=…` | Result page: verdict, score, per-source table |
103
+ | `/bulk` | Upload a CSV/feed or paste a list; runs as a background job |
104
+ | `/bulk/<job_id>` | Job progress, per-row verdicts, CSV/JSON export |
105
+ | `/recent` | Recent lookups on this instance |
106
+ | `/sources` | Source catalogue with live state (`active` / `needs key` / `opt-in`) |
107
+ | `/local` | Manage the local corpus |
108
+ | `/api` | JSON API documentation |
109
+
110
+ ---
111
+
112
+ ## JSON API
113
+
114
+ All lookups accept `?fresh=1` to bypass the cache. Responses are
115
+ VirusTotal-shaped (`data.attributes`-ish flat object with `verdict`, `score`,
116
+ `confidence`, `results[]`).
117
+
118
+ ```bash
119
+ curl -s http://127.0.0.1:8080/api/v1/hash/44d88612fea8a8f36de82e1278abb02f
120
+ curl -s "http://127.0.0.1:8080/api/v1/url?url=https://example.com/login"
121
+ curl -s "http://127.0.0.1:8080/api/v1/domain?domain=example.com"
122
+ curl -s "http://127.0.0.1:8080/api/v1/lookup?q=$(echo -n evil | sha256sum | cut -d' ' -f1)"
123
+ curl -s http://127.0.0.1:8080/api/v1/sources
124
+ curl -s http://127.0.0.1:8080/api/v1/stats
125
+ ```
126
+
127
+ Bulk over the API — submit, then poll:
128
+
129
+ ```bash
130
+ curl -s -X POST http://127.0.0.1:8080/api/v1/bulk \
131
+ -F file=@iocs.csv -F label=incident-42
132
+ # {"job_id":"j_ab12…","total":120,"skipped":0,"status_url":"…/api/v1/bulk/j_ab12…"}
133
+
134
+ curl -s http://127.0.0.1:8080/api/v1/bulk/j_ab12…
135
+ curl -s http://127.0.0.1:8080/api/v1/bulk/j_ab12…/items
136
+ ```
137
+
138
+ Raw bodies work too (`text/plain`, one indicator per line). Limits come from
139
+ `TT_RATE_LIMIT` (default 60/min per IP) and `TT_MAX_UPLOAD` (2 MiB).
140
+
141
+ ---
142
+
143
+ ## How a verdict is produced
144
+
145
+ Each source returns `verdict`, `detections`/`total` (engines), `note` and a raw
146
+ `detail`. Weights are additive:
147
+
148
+ | Condition | Points |
149
+ | --- | --- |
150
+ | Source says malicious, ≥5 engines involved | +100 |
151
+ | Source says malicious (list membership, few engines) | +40…+100 |
152
+ | Source says suspicious | +40 |
153
+ | Source says known-good (NSRL, whitelist) | −100 |
154
+ | Source says clean | −20 |
155
+
156
+ ```
157
+ score ≥ 100 → MALICIOUS
158
+ score ≥ 40 → SUSPICIOUS
159
+ score ≤ −100 → KNOWN GOOD
160
+ score ≤ −20 → CLEAN
161
+ otherwise → UNKNOWN (no data, or sources disagree)
162
+ ```
163
+
164
+ `confidence = min(100, abs(score))`. Sources in the `intel` category (DNS, RDAP,
165
+ crt.sh, reverse IP) enrich the report but never change the verdict. Engine
166
+ ratios are only computed from sources that actually report engines — a urlscan
167
+ "6987 scans" is a scan count, not a detection count, and is shown as `–`.
168
+
169
+ Deliberate anti-noise choices: OTX pulses that carry `Whitelisted domain` or
170
+ `Known False Positive` validation are downgraded to UNKNOWN; EICAR-style
171
+ keyless hits are attributed to the *listing* source rather than multiplied.
172
+
173
+ ---
174
+
175
+ ## Sources
176
+
177
+ 21 sources are compiled in; 8 work with no key. `httpx`-free HTTP via `requests`.
178
+
179
+ | Source | Kind | Category | Key | Env var |
180
+ | --- | --- | --- | --- | --- |
181
+ | CIRCL hashlookup | hash | reputation | keyless | — |
182
+ | AlienVault OTX (file) | hash | reputation | keyless | — |
183
+ | AlienVault OTX (URL) | url | reputation | keyless | — |
184
+ | AlienVault OTX (domain/IP) | domain/ip | reputation | keyless | — |
185
+ | urlscan.io | url/domain/ip | reputation | keyless | — |
186
+ | DNS (Cloudflare DoH) | domain/ip | intel | keyless | — |
187
+ | RDAP | domain/ip | intel | keyless | — |
188
+ | crt.sh | domain | intel | keyless | — |
189
+ | reverse IP (HackerTarget) | domain/ip | intel | keyless, **opt-in** | `TT_ENABLE_REVERSE_IP=1` |
190
+ | VirusTotal (file/url/domain) | hash/url/domain/ip | reputation | keyed | `VT_API_KEY` |
191
+ | MalwareBazaar | hash | reputation | keyed | `ABUSECH_AUTH_KEY` |
192
+ | ThreatFox | hash/url/domain/ip | reputation | keyed | `ABUSECH_AUTH_KEY` |
193
+ | URLhaus (hash + URL) | hash/url | reputation | keyed | `ABUSECH_AUTH_KEY` |
194
+ | Hybrid Analysis | hash | reputation | keyed | `HA_API_KEY` |
195
+ | MalShare | hash | reputation | keyed | `MALSHARE_API_KEY` |
196
+ | Triage | hash | reputation | keyed | `TRIAGE_API_KEY` |
197
+ | Google Safe Browsing | url/domain | reputation | keyed | `GSB_API_KEY` |
198
+ | PhishTank | url | reputation | keyed | `PHISHTANK_APPKEY` |
199
+ | urlscan (keyed) | url/domain/ip | reputation | keyed | `URLSCAN_API_KEY` |
200
+
201
+ Notes:
202
+
203
+ * abuse.ch (MalwareBazaar, ThreatFox, URLhaus) requires a free Auth-Key from
204
+ <https://auth.abuse.ch/> since API 3.0.0. A bad key answers **403**, which
205
+ ThreatTrove reports as `unauthorized` — the run continues.
206
+ * `TT_ENABLE_REVERSE_IP` is off by default because that lookup sends the queried
207
+ IP/hostname to a third-party service. That's the one source gated on consent.
208
+ * Sources without a key are simply skipped and listed as `needs key` in
209
+ `/sources`; they never fail a lookup.
210
+
211
+ ---
212
+
213
+ ## Configuration
214
+
215
+ Copy `deploy/threattrove.env.example` and edit. The important ones:
216
+
217
+ | Variable | Default | Meaning |
218
+ | --- | --- | --- |
219
+ | `TT_DB` | `~/.threattrove/threattrove.db` | cache + jobs + local corpus |
220
+ | `TT_CACHE_TTL` | `604800` | cache lifetime, seconds |
221
+ | `TT_TIMEOUT` | `20` | per-source HTTP timeout |
222
+ | `TT_DEADLINE` | `45` | whole-lookup budget, seconds |
223
+ | `TT_BULK_MAX` | `5000` | indicators per bulk job / CLI run |
224
+ | `TT_MAX_JOBS` | `2` | concurrent bulk jobs per worker |
225
+ | `TT_MAX_UPLOAD` | `2097152` | upload cap, bytes |
226
+ | `TT_RATE_LIMIT` | `60` | API requests/min per IP |
227
+ | `TT_PAGE_RATE_LIMIT` | `240` | page requests/min per IP |
228
+ | `TT_SITE_URL` | `https://threattrove.com` | canonical base URL |
229
+ | `TT_SITE_NAME` | `ThreatTrove` | brand shown in nav, titles, OG tags |
230
+ | `TT_ENABLE_REVERSE_IP` | *(empty)* | `1` enables reverse-IP |
231
+
232
+ ---
233
+
234
+ ## Deployment
235
+
236
+ ### Docker
237
+
238
+ ```bash
239
+ cp deploy/threattrove.env.example .env # add keys if you have them
240
+ docker compose up -d --build
241
+ curl -s http://127.0.0.1:8080/health
242
+ ```
243
+
244
+ Data persists in the `threattrove-data` volume (`/data/threattrove.db`). The image
245
+ runs as an unprivileged user with a read-only root filesystem.
246
+
247
+ ### systemd + nginx on threattrove.com
248
+
249
+ ```bash
250
+ useradd --system --home /opt/threattrove --shell /usr/sbin/nologin threattrove
251
+ git clone <repo> /opt/threattrove && cd /opt/threattrove
252
+ python3 -m venv .venv && .venv/bin/pip install -r requirements.txt && .venv/bin/pip install --no-deps .
253
+ install -d -o threattrove -g threattrove /var/lib/threattrove
254
+ install -Dm640 deploy/threattrove.env.example /etc/threattrove/threattrove.env
255
+ install -Dm644 deploy/threattrove.service /etc/systemd/system/threattrove.service
256
+ systemctl daemon-reload && systemctl enable --now threattrove
257
+
258
+ # TLS + reverse proxy
259
+ apt install nginx certbot python3-certbot-nginx
260
+ install -Dm644 deploy/nginx-threattrove.conf /etc/nginx/sites-available/threattrove.com
261
+ ln -s /etc/nginx/sites-available/threattrove.com /etc/nginx/sites-enabled/
262
+ rm -f /etc/nginx/sites-enabled/default
263
+ nginx -t && systemctl reload nginx
264
+ certbot --nginx -d threattrove.com -d www.threattrove.com
265
+ ```
266
+
267
+ `deploy/gunicorn.conf.py` binds `127.0.0.1:8080`, 2 gthread workers × 8 threads,
268
+ 120s timeout (bulk lookups are slow by nature). Nginx adds TLS/HSTS, gzip,
269
+ static caching and per-zone rate limits.
270
+
271
+ **Public-instance checklist**
272
+
273
+ 1. Set `TT_RATE_LIMIT` low (30–60) — every lookup fans out to 8+ upstreams, and
274
+ they rate-limit *you* first.
275
+ 2. Keep `TT_ENABLE_REVERSE_IP` off unless your privacy notice says otherwise.
276
+ 3. Add `X-Forwarded-For` handling (already configured) so the limiter sees real
277
+ clients.
278
+ 4. Cache is shared, so identical indicators are only queried upstream once per
279
+ `TT_CACHE_TTL`.
280
+ 5. `/health` is the liveness endpoint used by the Docker healthcheck.
281
+
282
+ ---
283
+
284
+ ## Project layout
285
+
286
+ ```
287
+ threattrove/
288
+ core.py parse indicators, Engine (threaded, paced, deadline), Store, Report
289
+ sources.py 21 source implementations + verdict helpers
290
+ bulk.py CSV/JSON/newline parsing, job runner, CSV export
291
+ cli.py `threattrove` entry point
292
+ web.py Flask app factory + `threattrove-web`
293
+ templates/ Jinja2 views
294
+ static/ app.js, style.css, favicon.svg
295
+ scripts/smoke.py offline parser/verdict checks + live lookups
296
+ deploy/ gunicorn, systemd, nginx, env example
297
+ legacy/threattrove-v1-cli.py original single-file CLI (superseded, kept for reference)
298
+ Dockerfile, docker-compose.yml
299
+ ```
300
+
301
+ ## Testing
302
+
303
+ ```bash
304
+ .venv/bin/python scripts/smoke.py --offline # 13 parsing + 6 verdict checks
305
+ .venv/bin/python scripts/smoke.py # + live hash/URL/domain lookups
306
+ ```
307
+
308
+ ## Limitations
309
+
310
+ * ThreatTrove never uploads files — it hashes locally and only sends hashes.
311
+ * Keyless sources are free, so they are shallow: absence of a hit means "no data",
312
+ never "safe". Treat `UNKNOWN` as unknown.
313
+ * No user accounts, no per-user quota: the rate limiter is per IP, meant for a
314
+ single node behind a proxy.
315
+ * Bulk jobs run inside the web worker process. Two workers means two independent
316
+ job pools (fine for this scale; swap in a real queue if you outgrow it).
317
+
318
+ ## License
319
+
320
+ MIT.