This page explains how APT Watch collects, scores, attributes, and ages every indicator of compromise (IOC) it publishes. It covers the four mechanisms that determine whether an IOC ends up in a blocklist, the dashboard, or neither: attribution confidence, composite scoring, lifecycle decay, and the safelist.
Every IOC in our flat tables carries a numeric
actor_attribution_score between 0.0 and 1.0. The score is an
evidence grade, not a per-source probability; it reflects how much
trust the curator places in the claim that a given IOC belongs to a given
APT actor.
| Tier | Score | Meaning | Typical source |
|---|---|---|---|
| Curator | 1.0 | Ground truth, human-reviewed YAML submission. | CISA advisories, vendor IR reports (Mandiant, Talos, MSTIC), curator-authored baselines. |
| High supposed | 0.70 – 0.99 | Vetted per-actor feed; narrow false-positive surface. | MSTIC RSS, Mandiant per-group feeds, CERT-UA, Google TI, ESET per-actor dumps. |
| Medium supposed | 0.30 – 0.69 | Aggregated / community feed; useful but noisy. | OTX pulses, multi-actor threat feeds, open-source RSS keyword matches. |
| Weak | 0.01 – 0.29 | Best-effort; flagged but not used for blocklist inclusion without corroboration. | Individual blog posts, unverified mailing-list tips. |
| Unattributed | 0.0 / NULL | IOC captured, no actor claim made. | Collector sweeps with no per-actor context. |
Attribution is monotonic: a lower-score claim never overwrites a higher-score one. A claim at the same score from a different actor is treated as a conflict and logged; the curator resolves it manually.
Each IOC also carries a composite_score that combines
infrastructure risk with corroboration signals. It is computed per run by
the scoring pipeline and ranges 0.0–1.0.
The inputs are:
The resulting score buckets into the Critical / High / Medium / Low
labels used in the dashboard, but the underlying number is the numeric
field. Planned work (Step 15) will replace the bucketed labels with a
Bayesian posterior that exposes per-source contributions in the UI.
IOCs do not live forever. Every IOC has a lifecycle_state
that transitions through three stages:
| State | Trigger | Blocklist behaviour |
|---|---|---|
active |
Seen in the last 30 days by at least one feed. | Published in high- and medium-tier blocklists. |
stale |
Not re-observed for 30–90 days. | Published in full-tier blocklists only; excluded from the focused high-tier list. |
expired |
Not re-observed for >90 days AND no active campaign reference. | Excluded from every published blocklist. Retained in the database for historical analysis. |
8.8.8.8, Gravatar, GitHub Pages, Cloudflare). Publishing their
current A records would cause blanket false positives. We track sinkhole
status per domain (Step 7, in progress) and filter resolved-DNS merges
accordingly.
Even a perfectly-attributed IOC must never appear in a blocklist if it identifies legitimate shared infrastructure. The safelist applies to every blocklist generator and rejects, by CIDR:
| Provider | Ranges |
|---|---|
| GitHub Pages | 185.199.108.0/22 |
| Cloudflare | 104.16.0.0/12, 172.64.0.0/13, 173.245.48.0/20,
190.93.240.0/20, 197.234.240.0/22,
198.41.128.0/17 |
| Microsoft / Bing | 150.171.0.0/16, 204.79.197.0/24, 13.107.0.0/16,
20.0.0.0/11 |
8.8.8.0/24, 8.8.4.0/24, 142.250.0.0/15,
172.217.0.0/16, 216.58.192.0/19 | |
| Apple | 17.0.0.0/8 |
| Meta / Instagram | 157.240.0.0/16, 31.13.24.0/21, 179.60.192.0/22 |
| Gravatar / Automattic | 192.0.64.0/18 |
We also distinguish compromised-legitimate domains from
attacker-owned ones. Domains marked
domain_type='compromised_legitimate' (for example hacked
WordPress sites being used as C2) are tracked for research but
never published in blocklists — the underlying site is a victim, not
infrastructure to block.
Some IOCs legitimately belong to multiple actors — bulletproof hosting
providers, infrastructure handed off between groups, or VPS subnets used
for joint operations. When db_health_check.py flags an IOC
as appearing in three or more campaigns, we treat it as
shared infrastructure rather than a conflict: the IOC
stays attributed to all linked actors via campaign_iocs,
with a notes='shared infra' annotation on each row.
| Threshold | Treatment | Curator action |
|---|---|---|
| 1 campaign | Single attribution. | None. |
| 2 campaigns | Conflict — flagged for review. | Curator decides which actor owns the IOC. |
| 3+ campaigns | Shared infrastructure — all attributions retained. | Annotate with notes='shared infra'. |
Examples we currently track this way:
Documenting the 3-campaign threshold here lets curators
act consistently when db_health_check.py raises a multi-actor
flag: below the threshold it is a conflict to resolve; at or above it is
shared infrastructure to annotate. Reviewers should not silently delete
one of the attributions.
Every claim on the dashboard links back to the source record. Tooltips on
score values cite this page. Raw data is available in the
APT Watch repository
(canonical schema under database/schema.sql; blocklist
generators under scripts/generate_*_blocklist.py).
For questions, suggestions, or dispute reports on any published IOC, open
an issue on the repo or email aptwatch@pm.me.
Last revised: 2026-04-23. Step 2.E of the APT Watch rollout.