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.
- threattrove-2.0.1/LICENSE +21 -0
- threattrove-2.0.1/PKG-INFO +347 -0
- threattrove-2.0.1/README.md +320 -0
- threattrove-2.0.1/pyproject.toml +53 -0
- threattrove-2.0.1/setup.cfg +4 -0
- threattrove-2.0.1/threattrove/__init__.py +23 -0
- threattrove-2.0.1/threattrove/__main__.py +11 -0
- threattrove-2.0.1/threattrove/bulk.py +401 -0
- threattrove-2.0.1/threattrove/cli.py +700 -0
- threattrove-2.0.1/threattrove/core.py +729 -0
- threattrove-2.0.1/threattrove/sources.py +972 -0
- threattrove-2.0.1/threattrove/static/app.js +75 -0
- threattrove-2.0.1/threattrove/static/favicon.svg +7 -0
- threattrove-2.0.1/threattrove/static/style.css +303 -0
- threattrove-2.0.1/threattrove/templates/api.html +88 -0
- threattrove-2.0.1/threattrove/templates/base.html +66 -0
- threattrove-2.0.1/threattrove/templates/bulk.html +71 -0
- threattrove-2.0.1/threattrove/templates/bulk_result.html +106 -0
- threattrove-2.0.1/threattrove/templates/error.html +14 -0
- threattrove-2.0.1/threattrove/templates/index.html +86 -0
- threattrove-2.0.1/threattrove/templates/local.html +68 -0
- threattrove-2.0.1/threattrove/templates/recent.html +30 -0
- threattrove-2.0.1/threattrove/templates/result.html +125 -0
- threattrove-2.0.1/threattrove/templates/sources.html +63 -0
- threattrove-2.0.1/threattrove/web.py +578 -0
- threattrove-2.0.1/threattrove.egg-info/PKG-INFO +347 -0
- threattrove-2.0.1/threattrove.egg-info/SOURCES.txt +29 -0
- threattrove-2.0.1/threattrove.egg-info/dependency_links.txt +1 -0
- threattrove-2.0.1/threattrove.egg-info/entry_points.txt +3 -0
- threattrove-2.0.1/threattrove.egg-info/requires.txt +7 -0
- 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.
|