test(clustering): fixture 4 paused_campaign + active_days/time_window
Adds the actor.active_days primitive to the campaign factory so a DSL actor can be bound to specific day indexes. Falls back to the non-paused day pool when absent (existing fixtures unchanged). Intersects with pause_windows so the campaign-wide silence still wins if both are set. Adds time_window_clusterer reference to fixture_harness — union-find over attackers, edge if their session time-ranges are within gap_days of each other. Deliberately-bad reference for fixture 4: multi-day silent stretches fragment a single campaign because the clusterer has no signal that bridges the gap. Fixture 4 (paused_campaign): one campaign modeled as two DSL actors representing the operator's two operational windows (active days 1-2 and 6-7), separated by a silent stretch (days 3-5). Both share JA3 + HASSH + payload + C2 callback; only their active_days differ. Five tests: corpus shape (rows in their windows, shared signals), pipeline pass via fingerprint_clusterer at level=campaign, adversarial fragmentation via time_window_clusterer (1-day union threshold cannot bridge the 4-day silence → completeness collapses), huge-gap sanity (gap_days=10 unions both halves), silent-stretch invariant (no session leaks into the configured pause window). Identity-level scoring is fixture 2's job; this fixture is campaign-level only — modeling caveat documented in the YAML.
This commit is contained in:
@@ -331,21 +331,30 @@ def _emit_campaign(
|
||||
decky_choices = decky_pool
|
||||
|
||||
# Schedule sessions across the campaign window, respecting the
|
||||
# actor's hours_active_utc and pause_windows.
|
||||
# actor's hours_active_utc, pause_windows, and (if specified)
|
||||
# the actor's active_days. ``active_days`` (per-actor list of
|
||||
# day indexes) lets a fixture bind an actor to specific days
|
||||
# without affecting siblings — used by fixture 4 to model an
|
||||
# operator who pauses operations between sprints.
|
||||
active_hours = actor_spec.get("hours_active_utc", list(range(24)))
|
||||
jitter = int(actor_spec.get("jitter_seconds", 60))
|
||||
|
||||
non_paused = [
|
||||
d for d in range(duration_days)
|
||||
if not any(s <= d <= e for s, e in pause_windows)
|
||||
]
|
||||
actor_active_days = actor_spec.get("active_days")
|
||||
if actor_active_days is not None:
|
||||
# Intersect with non-paused so pause_windows still wins
|
||||
# globally if the fixture sets both (defensive).
|
||||
day_pool = [d for d in actor_active_days if d in non_paused]
|
||||
else:
|
||||
day_pool = non_paused
|
||||
|
||||
for s_idx in range(n_sessions):
|
||||
day = rng.randint(0, max(0, duration_days - 1))
|
||||
if any(start <= day <= end for start, end in pause_windows):
|
||||
# Skip into post-pause day.
|
||||
later_days = [
|
||||
d for d in range(duration_days)
|
||||
if not any(s <= d <= e for s, e in pause_windows)
|
||||
]
|
||||
if not later_days:
|
||||
continue
|
||||
day = rng.choice(later_days)
|
||||
if not day_pool:
|
||||
continue
|
||||
day = rng.choice(day_pool)
|
||||
hour = rng.choice(active_hours)
|
||||
day_start = epoch + timedelta(days=day)
|
||||
started_at = _hour_to_offset(rng, day_start, hour, jitter)
|
||||
|
||||
Reference in New Issue
Block a user