IOCs that tell you whose they are. ~167,000 indicators from ~340 APT groups, 121 of them resolved onto 95 MITRE ATT&CK intrusion sets, refreshed hourly.
Most free IOC feeds hand you a flat list of bad domains. Actor attribution lives
somewhere else, in a different format, joined by hand through alias tables.
APTtrail ships them together: every indicator arrives tagged with the group it
belongs to, and where MITRE tracks that group, with its Gxxxx id.
# Every domain APT28 (G0007) has ever been seen using
curl -sL https://trilwu.github.io/apttrail/by-group/G0007-domain.txtBrowse it: trilwu.github.io/apttrail — every group has a profile page with description, suspected origin, targeted sectors, ATT&CK techniques, and a timeline of its indicators: each batch dated and linked to the report it came from, e.g. APT28.
Everything is a static file on a stable URL. No account, no API key, no rate limit.
Slices are served over GitHub Pages; the big single-file formats are release assets. Both have stable URLs you can construct by hand.
SITE=https://trilwu.github.io/apttrail
REL=https://github.com/trilwu/apttrail/releases/download/latest
curl -sLO $SITE/by-type/domain.txt # every domain, one per line
curl -sLO $SITE/by-type/ipv4.txt # every IP, ports stripped
curl -sLO $SITE/by-group/G0007.json # one actor, full detail
curl -sLO $SITE/index.json # what exists, with counts
curl -sLO $REL/apttrail_threat_feed_stix.json| Doing this | Grab this |
|---|---|
| Blocklist for pfBlockerNG / Pi-hole / firewall | by-type/domain.txt, by-type/ipv4.txt |
| Hunting one actor in DNS or proxy logs | by-group/<G-id>-domain.txt |
| Loading into MISP | misp-feed/ (add the URL as a MISP Feed) |
| Loading into OpenCTI or any STIX tool | apttrail_threat_feed_stix.json |
| Suricata / Snort IDS | apttrail_threat_feed.rules + suricata-datasets/ |
| Sigma-based SIEM | apttrail_threat_feed.yaml (one rule per group, ATT&CK-tagged) |
| Looking up one indicator from an alert | search.html or by-indicator/<shard>.json |
| Reading up on an actor | by-group/<G-id>.html |
| Seeing what moved this week | activity.html / activity.json |
| Asking which groups overlap | graph.html / graph.json |
| Your own tooling | apttrail_threat_feed.json, .csv, index.json |
weekly-YYYY-Wxx tags are immutable snapshots if you need a fixed point in time
for an investigation.
An alert says evil-domain.example. With a flat blocklist that is where the
story ends. Here:
$ SITE=https://trilwu.github.io/apttrail
$ curl -s $SITE/by-group/G0007.json | jq '{attack_id, attack_name, attack_url, counts}'
{
"attack_id": "G0007",
"attack_name": "APT28",
"attack_url": "https://attack.mitre.org/groups/G0007/",
"counts": { "domain": 1431, "ipv4": 240, "url": 144, "url_path": 79 }
}
$ curl -s $SITE/by-group/G0040.json | jq '.maltrail_groups'
["DONOT", "HANGOVER", "PATCHWORK"]Going the other way — an alert fires on a domain and you want to know whose it is:
# one-off lookup across every group, no local copy needed
curl -s $SITE/index.json | jq -r '.groups[] | select(.counts.domain) | .slug' \
| while read -r slug; do
curl -s "$SITE/by-group/$slug-domain.txt" \
| grep -qFx "evil-domain.example" && echo "$slug"
doneFor repeated lookups, pull by-type/domain.txt once and keep the whole set local.
Either direction, you land one hop from the actor's ATT&CK page, their full infrastructure set, and the 22 aliases other vendors' reporting will use for the same group.
The id travels into every format, so it survives the trip into your tooling:
- Suricata —
metadata:apt_group SOFACY, mitre_group_id G0007; - MISP — a
misp-galaxy:mitre-intrusion-settag, so the event pivots to the cluster - STIX — an
intrusion-setwith anexternal_referencesentry to ATT&CK - JSON / CSV —
attack_id,attack_name,attack_urlper group
Alias resolution is vendored from the MISP galaxy,
so it works offline and is reproducible. Refresh with
python scripts/refresh_attack_groups.py.
Suricata
BASE=https://github.com/trilwu/apttrail/releases/download/latest
curl -sLO $BASE/apttrail_threat_feed.rules
curl -sLO $BASE/apttrail_suricata_datasets.tar.gz
tar xzf apttrail_suricata_datasets.tar.gz -C /etc/suricata/rules/
cp apttrail_threat_feed.rules /etc/suricata/rules/
suricata -T -c /etc/suricata/suricata.yaml -S /etc/suricata/rules/apttrail_threat_feed.rules~4,100 rules, not 150,000: domains and IPs are matched through Suricata
dataset: lookups rather than one rule per indicator. SIDs occupy the
9000000+ local range, clear of Emerging Threats.
Every rule carries a content or dataset match, so the multi-pattern matcher
has something to prefilter on — CI fails the build if suricata --engine-analysis
finds a rule it would have to evaluate against every packet.
Repeat alerts are collapsed where that cannot cost you an indicator:
| Rule | Threshold | Why |
|---|---|---|
ip |
track by_both |
Beaconing to one C2 collapses to one alert an hour; a different address in the same set still alerts |
http per host/path |
track by_src |
One rule, one indicator — nothing to hide |
dns, tls, URI-path datasets |
none | One rule covers the whole set, and a DNS query's destination is the resolver, so no track key tells one domain from the next. Suppressing here would drop the second domain a host contacted |
If you want the DNS rules rate-limited too, that belongs in your own
threshold.config, where it can be decided against your traffic.
Every published rule file has been through suricata -T in CI before release —
see Verification.
MISP
Add as a feed under Sync Actions → Feeds → Add Feed:
- Input source:
Network - Format:
MISP Feed - URL:
https://trilwu.github.io/apttrail/misp-feed/
That URL is a real MISP feed — manifest.json, one event per APT group, and
hashes.csv for correlation. One event per actor, not one event with
155,000 attributes, so you can pivot from a hit to that group's full
infrastructure. Events carry tlp:clear, apt:<group> and, where the group is
mapped, the misp-galaxy:mitre-intrusion-set tag that links to the actor
cluster. Attributes have to_ids set.
Event and attribute UUIDs are deterministic, so re-fetching updates events in
place instead of duplicating them. For an air-gapped instance,
apttrail_misp_feed.tar.gz in the release has the same directory.
OpenCTI / STIX 2.1
curl -sLO https://github.com/trilwu/apttrail/releases/download/latest/apttrail_threat_feed_stix.jsonThe bundle contains intrusion-set objects (not threat-actor), so it merges
with ATT&CK data already in your platform instead of creating duplicates.
Object ids are UUIDv5 over a fixed namespace: re-ingesting updates objects
rather than multiplying them.
pfBlockerNG / Pi-hole / firewall
Point at the flat lists; they are plain text with # comments.
https://trilwu.github.io/apttrail/by-type/domain.txt
https://trilwu.github.io/apttrail/by-type/ipv4.txt
Read the caveats before blocking on these outright.
Splunk / Sentinel / anything with a lookup table
curl -sL https://github.com/trilwu/apttrail/releases/download/latest/apttrail_threat_feed.csv -o apttrail.csv
# apt_group,indicator_type,indicator[,first_seen]apttrail_threat_feed_metadata.csv maps each group to its ATT&CK id, name,
URL and aliases, so you can enrich a match without a second lookup.
| Type | Count | Notes |
|---|---|---|
domain |
~141,000 | |
url_path |
~3,600 | bare request paths, e.g. /gate.php |
ipv4 |
~3,800 | ports recorded separately, addresses are bare |
url |
~3,200 | includes scheme-less host/path |
| hashes | 0 | Maltrail's APT trails carry none today |
~340 Maltrail groups, 121 of which resolve onto 95 distinct ATT&CK intrusion
sets. Groups that ATT&CK treats as one actor are merged into one slice, so
G0040 covers DONOT, PATCHWORK and HANGOVER together. Largest: GAMAREDON (G0047, 52,028),
KIMSUKY (G0094, 25,227), TRANSPARENTTRIBE (G0134, 7,776), LAZARUS (G0032, 5,346).
Three records, none of which requires diffing a 7MB file:
-
first_seen— when an indicator entered Maltrail, with afirst_seen_precisionofexactorat-or-before.This took recovering history that upstream had thrown away. Maltrail reset its repository on 2026-01-03 ("Initial commit (fresh repo)"), and
git blamecannot see past a root commit, so a naive reading dates every older indicator to January 2026 — which is what this feed used to publish. The discarded history survives in pre-reset pull-request refs; walking one of them recovers the commit that first added each line, back to 2014.Today 126,915 indicators carry an exact date spanning 2015–2026. 4,646 fall in the gap between the recovered history and the reset and are marked
at-or-before— they may be older. 23,649 stay undated rather than guessed at. Regenerate the map with the Backfill IOC history workflow. -
the report each indicator came from. Maltrail files indicators under the write-up that published them:
# Reference: https://vendor.example/apt28-write-up evil.example 1.2.3.4That association is the only per-indicator provenance upstream carries, and flattening it into one list per group — which is what this project used to do — left an analyst with 1,600 references at the foot of a page and no way to tell which one explains the domain in front of them. It is now kept:
by-group/<G-id>.jsoncarries atimelineof(date, report, indicators)batches, and the actor page renders it, newest first, each batch linked to its source. 9,837 batches cover 155,190 indicators; exactly one of them has no report behind it.# Which report brought this domain in, and when? curl -s $SITE/by-group/G0007.json | jq '.timeline[] | select(.indicators.domain // [] | index("evil.example"))'
-
changes/YYYY-MM.jsonl— when an indicator entered or left APTtrail, whichfirst_seencannot express. Append-only, a few KB per day, kept in git:
{"action":"added","group":"BLUENOROFF","ts":"2026-07-26T04:00:00","type":"domain","value":"example.com"}grep '"value":"example.com"' feeds/changes/*.jsonl # when did we see this, and when did it go- Attribution covers 121 of ~340 groups (36%). ATT&CK tracks 191 intrusion sets; Maltrail tracks actors from vendor reporting that ATT&CK has not named. An unmapped group still carries its Maltrail name and aliases.
- These are historical indicators, not a real-time blocklist. Domains get
reused and sinkholed. Treat a hit as a lead to triage, not proof of compromise,
and expect false positives if you block
domain.txtwholesale.first_seentells you how old an indicator is — much of the feed predates 2022 — so filter on age if you intend to block rather than hunt. - Attribution is inherited, not independent. APTtrail trusts Maltrail's group assignment and the MISP galaxy's alias table. It does no analysis of its own.
- No file hashes. The YARA output therefore matches indicator strings inside files and memory, which also hits benign files that merely mention a domain — logs, PCAPs, this feed itself. Its header says so. If you want hash detection, this is not your feed.
- One upstream source. Everything here derives from Maltrail. Its coverage is your coverage.
Feeds are not published on trust:
- CI runs the real Suricata engine (
suricata -T) over rules built from a fixture covering every rule shape, including a live indicator containing a double quote and a URL with no path. - The hourly workflow runs
suricata -Tover the actual rule file and refuses to publish if it fails. - Sigma output is parsed as multi-document YAML in the test suite; every rule must have a condition naming only selections that exist and hold values.
- The MISP feed layout is asserted against what MISP reads: manifest keys matching event files, stable UUIDs across runs, galaxy tags where mapped.
- 208 tests,
mypyclean,ruffclean, coverage 91% against a gate of 80%.
pip install -e .
apttrail --output-dir feeds --suricata-dataset --collect-timestampsdocker build -t apttrail . && docker run -v $(pwd)/feeds:/app/feeds apttrailUseful flags: --json-only, --slices-only, --misp-only, --no-update,
--cache-dir, --no-changelog. Full list with apttrail --help.
See CONTRIBUTING.md.
MIT. Indicator data originates from Maltrail by Miroslav Stampar — the project this one stands on. Alias data from the MISP galaxy, CC-BY-SA.