224 lines
13 KiB
Markdown
224 lines
13 KiB
Markdown
# Campaign Clustering — Design
|
||
|
||
**Status:** pre-implementation. This doc is the spec; code follows.
|
||
|
||
**Roadmap entry:** `DEVELOPMENT.md` — Detection & Intelligence → "Attack campaign clustering".
|
||
|
||
## Premise
|
||
|
||
A *campaign* is a coordinated set of attacker actions that share intent, tooling, or operator — observable at DECNET as recurring patterns across `attackers`, `sessions`, `fingerprints`, `credentials`, and `payloads`.
|
||
|
||
We will not write clustering code until we can **simulate campaigns with ground-truth labels** and run a clusterer against those labels. The simulator is the specification for what a campaign is; the algorithm is replaceable.
|
||
|
||
Order of work, strictly:
|
||
|
||
1. Campaign DSL + generator (produces synthetic events with `campaign_id` / `actor_id` labels).
|
||
2. Adversarial scenario fixtures (the 6 below).
|
||
3. Metric harness (ARI + homogeneity + completeness + singleton recall).
|
||
4. Dumbest viable clusterer (connected components on a similarity graph). Must pass all 6 fixtures.
|
||
5. Pipeline integration (`decnet clusterer` worker, `campaigns` table, dashboard).
|
||
6. Replay tier — public datasets / Honeynet SSH logs through the live collector. Reality check, not optional forever.
|
||
|
||
Steps 1–3 are the durable artifact. Step 4 is the first throwaway algorithm.
|
||
|
||
---
|
||
|
||
## Phase Vocabulary: Unified Kill Chain
|
||
|
||
Phase names use the **Unified Kill Chain** (Pols, 2017), 18 phases across 3 stages. UKC maps cleanly to MITRE ATT&CK tactics, which means the phase labels we emit in synthetic data are the same labels the future TTP-tagging worker (also in `DEVELOPMENT.md`) will produce. Fixtures become reusable across both features instead of needing renaming.
|
||
|
||
| Stage | Phases |
|
||
|---|---|
|
||
| **In** (initial foothold) | Reconnaissance, Resource Development, Weaponization, Delivery, Social Engineering, Exploitation, Persistence, Defense Evasion, Command & Control |
|
||
| **Through** (network propagation) | Pivoting, Discovery, Privilege Escalation, Execution, Credential Access, Lateral Movement |
|
||
| **Out** (action on objectives) | Collection, Exfiltration, Impact, Objectives |
|
||
|
||
**Honeypot observability.** A honeypot does not see the entire chain. Pre-target phases (OSINT Reconnaissance, Resource Development, Weaponization, Social Engineering) happen before any decky is touched. We observe roughly 14 of 18:
|
||
|
||
- **In:** Delivery, Exploitation, Persistence, Defense Evasion, Command & Control
|
||
- **Through:** Pivoting, Discovery, Privilege Escalation, Execution, Credential Access, Lateral Movement
|
||
- **Out:** Collection, Exfiltration, Impact, Objectives
|
||
|
||
The DSL allows the full enum so a campaign spec can describe an end-to-end story, but the generator emits no events for unobservable phases (and warns on them). MazeNET makes Pivoting and Lateral Movement first-class — that's where DECNET has *more* signal than a single-host honeypot, not less.
|
||
|
||
Each phase carries default tool-signature templates the DSL can override per-campaign. Examples:
|
||
|
||
- `discovery` → defaults: `whoami`, `id`, `uname -a`, `netstat -tnp`, `cat /etc/passwd`
|
||
- `persistence` → defaults: crontab edit, `~/.ssh/authorized_keys` write, systemd unit drop, `.bashrc` append
|
||
- `credential_access` → defaults: `/etc/shadow` read, browser-cred files, SSH key harvest
|
||
- `lateral_movement` → defaults: SSH/WinRM/SMB pivot to another decky in the same MazeNET segment
|
||
|
||
---
|
||
|
||
## 1. Campaign DSL
|
||
|
||
A campaign is a *causal story*, not a bag of events. Generator consumes YAML, emits a stream of synthetic records into the test DB with ground-truth labels.
|
||
|
||
```yaml
|
||
campaign:
|
||
id: c-apt-fauxbear-01
|
||
actors:
|
||
- id: a-001
|
||
asn: 14061 # DigitalOcean
|
||
ip_pool: rotating # rotating | sticky | tor
|
||
ja3: 769,4865-... # tool fingerprint, shared within campaign
|
||
hassh: aae6b9...
|
||
hours_active_utc: [22, 23, 0, 1, 2, 3]
|
||
jitter_seconds: 90
|
||
role: intrusion # intrusion | post-exploit | exfil — for multi-operator campaigns
|
||
- id: a-002
|
||
asn: 14061
|
||
ip_pool: sticky
|
||
ja3: 769,4865-... # same tool, different operator
|
||
hassh: aae6b9...
|
||
hours_active_utc: [14, 15, 16, 17]
|
||
jitter_seconds: 30
|
||
role: post-exploit
|
||
phases: # UKC phase enum
|
||
- name: delivery
|
||
actor: a-001
|
||
tool_signature: { user_agent: "Mozilla/5.0 (compatible; Nmap)" }
|
||
target_selector: { service: any, count: 50 }
|
||
dwell_seconds: 1
|
||
- name: exploitation
|
||
actor: a-001
|
||
tool_signature: { payload_hash: deadbeef..., cve: CVE-2024-XXXX }
|
||
target_selector: { service: http, port: 8080 }
|
||
success_rate: 0.2
|
||
- name: persistence
|
||
actor: a-001
|
||
tool_signature: { commands: ["wget", "chmod +x", "./", "crontab -e"] }
|
||
target_selector: { decky: previous_success }
|
||
- name: command_and_control
|
||
actor: a-001
|
||
tool_signature: { c2_callback: evil.example.com, beacon_jitter_seconds: 30 }
|
||
- name: discovery
|
||
actor: a-002 # handoff to second operator
|
||
tool_signature: { commands: ["whoami", "id", "uname -a", "ip route", "arp -a"] }
|
||
- name: lateral_movement
|
||
actor: a-002
|
||
tool_signature: { protocol: ssh, credential_source: harvested }
|
||
target_selector: { decky: adjacent_in_mazenet }
|
||
- name: collection
|
||
actor: a-002
|
||
tool_signature: { paths: ["/var/lib/mysql/*", "/home/*/Documents/*"] }
|
||
- name: exfiltration
|
||
actor: a-002
|
||
tool_signature: { c2_callback: evil.example.com, payload_hash: deadbeef... }
|
||
duration_days: 7
|
||
pause_windows: [] # for the "campaign that pauses" scenario
|
||
```
|
||
|
||
**Generator contract:**
|
||
|
||
- Input: list of campaign YAMLs + `noise: { scanner_count, ratio }`.
|
||
- Output: rows in `attackers` / `sessions` / `fingerprints` / `credentials_attempts` / `payloads`, each tagged with a `_truth_campaign_id` and `_truth_actor_id` column (test-only, stripped before clustering runs).
|
||
- Deterministic given a seed.
|
||
- Validates phase names against the UKC enum; warns on unobservable phases (emits no events for them).
|
||
|
||
The generator lives at `tests/factories/campaign_factory.py`. The DSL parser is the spec; if a real attacker pattern can't be expressed in it, the DSL is incomplete and we extend it before extending the clusterer.
|
||
|
||
---
|
||
|
||
## 2. Adversarial Scenario Fixtures
|
||
|
||
Six fixtures. Each is a YAML file under `tests/fixtures/campaigns/` plus an expected-bounds file. CI runs the clusterer against all six; any regression fails the build.
|
||
|
||
| # | Name | Setup | Pass condition |
|
||
|---|---|---|---|
|
||
| 1 | `shared_wordlist` | 2 distinct campaigns, both use rockyou-top1k for SSH brute (Credential Access phase) | Must NOT merge — credential overlap alone is insufficient signal |
|
||
| 2 | `vpn_hopping` | 1 campaign, 1 actor, IPs rotate across 5 ASNs over 3 days, JA3/HASSH stable, full Delivery→C2→Discovery chain | Must NOT split — actor identity survives IP churn |
|
||
| 3 | `lone_wolf` | 1 opportunistic scanner, Delivery phase only, no follow-up, no shared signals | Must stay singleton — not absorbed into any campaign |
|
||
| 4 | `paused_campaign` | 1 campaign, active days 1–2 (Delivery, Exploitation), silent days 3–5, active days 6–7 (Discovery, Lateral Movement, Exfiltration) | Must NOT split into two campaigns — temporal window must accommodate operator pauses |
|
||
| 5 | `multi_operator` | 1 campaign, 2 actors with distinct UKC roles: actor A handles Delivery→Exploitation→Persistence→C2 on UTC night shift, actor B handles Discovery→Lateral Movement→Collection→Exfiltration on UTC day shift, different IPs/ASNs, shared C2 callback + payload hash | Must merge — shared tooling and phase handoff > diverged infra |
|
||
| 6 | `noise_floor` | All 5 above + 10× random Delivery-only scanners drawn from a noise distribution | All 5 must still resolve correctly; scanners stay singleton |
|
||
|
||
Fixture 5 is the load-bearing one for UKC: a real campaign frequently splits operators along the In/Through/Out boundary, and a clusterer that only looks at IP/ASN will miss it. Phase-handoff is itself a feature the algorithm can use.
|
||
|
||
**Bounds per fixture** (in `expected.yaml` next to each):
|
||
|
||
```yaml
|
||
adjusted_rand_index: { min: 0.85 }
|
||
homogeneity: { min: 0.90 } # no false merges
|
||
completeness: { min: 0.80 } # no false splits
|
||
singleton_recall: { min: 0.95 } # for lone_wolf / noise scanners
|
||
```
|
||
|
||
Bounds are deliberately loose at first — we ratchet them up as the algorithm improves. Loosening a bound to make CI pass requires a PR comment justifying it.
|
||
|
||
---
|
||
|
||
## 3. Metric Harness
|
||
|
||
`tests/clustering/metrics.py`. Decided **before** any algorithm exists, so we don't pick the metric that flatters the result.
|
||
|
||
- **Adjusted Rand Index** — headline. Compares predicted vs. truth labels, corrects for chance.
|
||
- **Homogeneity** — each predicted cluster contains only members of one true campaign. Catches false merges.
|
||
- **Completeness** — all members of a true campaign land in the same predicted cluster. Catches false splits.
|
||
- **Singleton recall** — fraction of true singletons (lone wolves, noise) that stay singleton.
|
||
|
||
Homogeneity and completeness trade off; both must be reported. A single number hides which direction the algorithm is failing.
|
||
|
||
**Per-fixture report** is dumped as JSON on every CI run, not just pass/fail, so we can watch trends over time.
|
||
|
||
---
|
||
|
||
## 4. First Algorithm (after 1–3 are green)
|
||
|
||
Connected-components on a similarity graph. No ML.
|
||
|
||
- Nodes: attackers (or sessions, TBD — see open questions).
|
||
- Edges: weighted similarity, threshold to binarize.
|
||
- Edge weight = sum of:
|
||
- JA3/JA4/HASSH exact match: high
|
||
- Payload hash exact match: high
|
||
- C2 callback domain/IP exact match: high
|
||
- **Phase-handoff signal:** actor X ends in C2/Persistence on a decky, actor Y begins Discovery/Lateral Movement on the same decky within window W: medium-high. Defeats fixture 5 even when IP/ASN diverge.
|
||
- Credential-list Jaccard: low (defeated by fixture 1)
|
||
- Command-sequence Jaccard, bucketed by UKC phase: medium
|
||
- Temporal proximity (within window W): low multiplier
|
||
- ASN match: very low
|
||
- Edge threshold and feature weights are config, tuned against the 6 fixtures.
|
||
|
||
If connected-components passes all 6, ship it. DBSCAN/HDBSCAN/graph-community algorithms are deferred until a fixture proves CC inadequate.
|
||
|
||
---
|
||
|
||
## 5. Pipeline Integration
|
||
|
||
- New worker: `decnet clusterer`. Bus consumer on `attacker.scored` and `attacker.observed`.
|
||
- Re-cluster strategy: incremental on new attacker arrivals, full re-cluster nightly.
|
||
- Storage: `campaigns` table (UUID PK, per the `feedback_uuid_over_natural_keys` rule); `attackers.campaign_id` FK nullable.
|
||
- Bus signal: `campaign.{id}.formed` / `campaign.{id}.updated`. Document in `wiki-checkout/Service-Bus.md` per the `feedback_wiki_bus_signals` rule.
|
||
- Dashboard: Campaigns list page + CampaignDetail (aggregated AttackerDetail, with a UKC phase timeline visualization showing which phases each actor in the campaign executed).
|
||
|
||
---
|
||
|
||
## 6. Replay Tier (post-v1)
|
||
|
||
Public-dataset replay through the real collector. Confirms our fixtures encode realistic patterns, not just our assumptions.
|
||
|
||
Candidate sources:
|
||
- Honeynet Project SSH session corpora.
|
||
- DShield daily summaries.
|
||
- Our own production data once it accumulates.
|
||
|
||
This is where we discover whether the DSL is missing a dimension. Schedule it; don't punt forever.
|
||
|
||
---
|
||
|
||
## Risks
|
||
|
||
1. **Simulator encodes our assumptions.** Real attackers may not match. Mitigation: replay tier (§6).
|
||
2. **Bound creep.** Loosening fixture bounds to ship is the failure mode. Mitigation: bound changes require PR justification.
|
||
3. **Feature drift.** Sniffer fingerprint coverage changes the available signal. Mitigation: feature set is configurable; fixtures regenerate from the DSL when features change.
|
||
4. **UKC phase inference accuracy.** The clusterer relies on phase labels per session — those have to come from somewhere. Pre-TTP-tagging worker, the DSL emits them as ground truth in synthetic data, and the live pipeline uses heuristic phase assignment (command keywords, port/protocol). This is a known approximation; tightens once the TTP-tagging worker ships.
|
||
5. **Cost of full re-cluster.** At fleet scale, nightly re-cluster on millions of attackers is expensive. Mitigation: incremental-first, full nightly is a fallback we may drop.
|
||
|
||
## Open questions
|
||
|
||
- **Cluster nodes: attackers or sessions?** Leaning attackers (already deduped by `attacker_uuid`), but session-level may catch campaigns that span multiple attacker identities. Decide after fixture 5 (`multi_operator`).
|
||
- **Time window W** for temporal-proximity and phase-handoff edges: 24h? 7d? Tuned against fixture 4 (`paused_campaign`).
|
||
- **Phase inference at runtime.** Do we ship a heuristic phase classifier alongside the clusterer, or block on the TTP-tagging worker landing first? Heuristic is faster but is technical debt against the future ATT&CK-tagged version.
|
||
- **API exposure.** Do we expose campaigns in the public API or admin-only at first? Admin-only until we have UI for false-positive correction.
|