APT Watch — Methodology

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.

1. Attribution confidence

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.

TierScoreMeaningTypical source
Curator1.0 Ground truth, human-reviewed YAML submission. CISA advisories, vendor IR reports (Mandiant, Talos, MSTIC), curator-authored baselines.
High supposed0.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 supposed0.30 – 0.69 Aggregated / community feed; useful but noisy. OTX pulses, multi-actor threat feeds, open-source RSS keyword matches.
Weak0.01 – 0.29 Best-effort; flagged but not used for blocklist inclusion without corroboration. Individual blog posts, unverified mailing-list tips.
Unattributed0.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.

2. Composite scoring

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.

3. Lifecycle decay

IOCs do not live forever. Every IOC has a lifecycle_state that transitions through three stages:

StateTriggerBlocklist 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.
Why we don't follow sinkholed domains: expired malicious domains frequently resolve to legitimate infrastructure (Google Public DNS 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.

4. The safelist

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:

ProviderRanges
GitHub Pages185.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
Google 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
Apple17.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.

5b. Shared infrastructure attribution

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.

ThresholdTreatmentCurator 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.

5. Where to find the numbers

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.