Files
DECNET/tests/fixtures/campaigns/vpn_hopping.yaml
anti 0def6f7e37 test(clustering): fixture 2 vpn_hopping + fingerprint/asn references
One campaign, one DSL actor, ip_pool: rotating + rotation_count: 5
across 5 synthetic private-use ASNs (RFC 6996 64512-64516). Stable
JA3, HASSH, and payload_hash across every rotation — these are the
"signals the attacker can't cheaply rotate" per IDENTITY_RESOLUTION.md
and the load-bearing reason all 5 observation rows must resolve to
one identity / one campaign.

Two new reference clusterers in fixture_harness.py:

* fingerprint_clusterer — groups by (ja3, hassh). Un-fingerprinted
  rows stay singleton so it doesn't trivially fuse all noise into one
  mega-cluster. Approximates the stable-signal arm of the planned
  similarity graph.

* asn_clusterer — deliberately-bad reference for fixture 2's
  adversarial test. Group-by-ASN shatters the campaign into 5
  singletons; completeness collapses to 0.

Four tests in test_vpn_hopping_fixture.py: corpus shape (5 rows, 1
identity, 1 campaign, 5 distinct ASNs/IPs, stable fingerprints),
pass at campaign level, pass at identity level (asserts ARI exactly
1.0), asn_clusterer breaches the completeness floor.
2026-04-26 07:34:18 -04:00

56 lines
2.3 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fixture 2 (vpn_hopping) — see development/CAMPAIGN_CLUSTERING.md §2
# and development/IDENTITY_RESOLUTION.md.
#
# One campaign, one actor, rotating across 5 distinct ASNs. JA3, HASSH,
# and payload_hash are STABLE across every rotation — these are the
# signals "the attacker can't cheaply rotate" (per the identity
# resolution design doc) and they're the reason a clusterer should
# recover all 5 observation rows as ONE identity, ONE campaign.
#
# Ground truth (verified at every level):
# - 5 observations → 1 identity → 1 campaign (per truth_labels())
#
# Pass condition: a fingerprint-driven clusterer must fold all 5 rows
# into one cluster at both campaign-level and identity-level scoring.
#
# Adversarial condition: an asn_clusterer (group attackers by ASN —
# the textbook bad heuristic) must fragment the campaign into 5
# pieces and breach the completeness floor. This is what proves "ASN
# match" is correctly weighted "very low" in the planned similarity
# graph (per TODO clusterer feature list).
#
# ASN choice: synthetic private-use values (RFC 6996 6451264534) so
# the fixture never collides with real-world data and signals "not
# real" to readers at a glance.
campaign:
id: vpn-hopping-001
actors:
- id: hopper-a
asn: 64512 # primary; rotation_asns overrides per row
ip_pool: rotating
rotation_count: 5
rotation_asns: [64512, 64513, 64514, 64515, 64516]
ja3: "771,4865-4866-4867-49195-49199-49196-49200,0-23-65281-10-11-35-16-5-13-18-51-45-43-27,29-23-24,0"
hassh: "vpn-hopper-cccccccc-cccccccc-cccccccc"
hours_active_utc: [12, 13, 14, 15, 16]
jitter_seconds: 60
phases:
- name: delivery
actor: hopper-a
target_selector: { service: ssh, count: 5 }
dwell_seconds: 1
- name: exploitation
actor: hopper-a
tool_signature:
# Stable payload across every rotation — same dropper from
# whatever staging the operator uses, regardless of which VPN
# exit they emerge from.
payload_hash: "vpn-hopper-stage1-payload"
target_selector: { service: ssh, count: 5 }
dwell_seconds: 5
- name: discovery
actor: hopper-a
target_selector: { service: ssh, count: 5 }
dwell_seconds: 5
duration_days: 2