Skip to content

Counter-ISR (Blue/Red/Counter Modes)

Status & scope

  • Stage: POC — Design Extension
  • Module: parallax/ops/fusion/counter.py
  • Milestone: Post-POC (uses same scoring engine)
  • Reference: fusion/docs/COUNTER-ISR.md

Purpose

Three operational modes using the same scoring engine — no new match code needed:

Mode Question Who Runs It
Blue "Can we detect this entity across our federation?" Friendly analyst
Red "What can the adversary detect about our entities?" Red team / pen test
Counter "What's the cheapest perturbation to break adversary detection?" EW planner / OPSEC

All three modes call score_pair() and block_and_pair() from component.parallax.blocking-engine/04. The difference is whose lens and whose data.

Architecture

              ┌──────────────┐
              │  score_pair() │  ← Same engine for all modes
              │  block_and_  │
              │  pair()       │
              └──────┬───────┘
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
   ┌─────────┐ ┌─────────┐ ┌─────────────┐
   │  BLUE   │ │  RED    │ │  COUNTER    │
   │         │ │         │ │             │
   │ Our lens│ │ Their   │ │ Their lens  │
   │ Our data│ │ lens    │ │ Our data    │
   │         │ │ Our data│ │ + min-cost  │
   │ "detect │ │ "what   │ │   perturb   │
   │  them"  │ │  leaks" │ │ "defeat it" │
   └─────────┘ └─────────┘ └─────────────┘

The Key Insight: Blocking Defeat vs Scoring Defeat

Blocking defeat is categorically stronger than scoring defeat.

  • Scoring defeat: Lower the weighted similarity below initial_threshold. The entity pair still enters the comparison pipeline but scores too low to match. Requires changing enough fields to drop confidence by ~0.3-0.4.

  • Blocking defeat: Change a blocking key so the entity lands in a different bucket. The entity pair never enters the comparison pipeline at all. One field change can make you invisible. This is much cheaper.

BLOCKING DEFEAT (stronger):
  Change one blocking key field → entity never compared → invisible
  Cost: 1 field change
  Example: Change first name "Arthur" → "Andrew" (different soundex bucket)

SCORING DEFEAT (weaker):
  Change enough fields to drop confidence below threshold
  Cost: 3-5 field changes depending on weights
  Entity still enters comparison pipeline — just scores low

Counter-Lens YAML Extension

Extends the standard lens spec with adversary-specific sections:

# Standard lens fields (scope, field_groupings, identity_fusion, etc.)
# ... same as any lens ...

# COUNTER-ISR extension
adversary_lens:
  assumed_lens_id: adversary_pnt_detection_v1
  assumed_confidence: medium       # How confident are we in this lens model?
  source: "SIGINT collection / OSINT analysis / doctrine review"

perturbation_costs:
  # Cost of changing each field (domain-specific units)
  location_centroid:
    blocking_key: true              # This is a blocking key field
    cost: 0.3                       # Relative cost to change
    method: "physical relocation or GPS spoofing"
    constraints: "must stay within mission area"
  timestamp_start:
    blocking_key: true
    cost: 0.1
    method: "time-delay or clock manipulation"
    constraints: "must maintain operational tempo"
  affected_constellations:
    blocking_key: false
    cost: 0.8
    method: "frequency hopping or constellation switching"
    constraints: "limited by available receivers"
  anomaly_type:
    blocking_key: false
    cost: 0.9
    method: "signal masking"
    constraints: "may degrade own capability"

counter_objectives:
  mode: minimize_cost              # Find cheapest perturbation set
  target: blocking_defeat          # Prefer blocking defeat over scoring defeat
  constraint: "confidence < adversary_initial_threshold"
  max_perturbations: 3             # Max fields to perturb simultaneously

Domain Attack Surfaces

Lens Blocking Keys Vulnerable? Why
PNT geohash_prefix_3, time_bucket_1h Yes Geohash = physical location (manipulable via spoofing). Time = clock (manipulable via delay).
SIGINT frequency_band, time_bucket Yes Frequency = why FHSS exists. Time = signal timing (jitterable).
VRS soundex(name), year(DOB) Hardened Blocking key is derived from consent token (SHA-256). Can't be perturbed without changing the person's actual identity.
AML soundex(name), country_code Partially Name spelling variants can shift soundex bucket. Country is immutable.

Public API

def run_blue(
    local_df: dask.dataframe.DataFrame,
    remote_df: dask.dataframe.DataFrame,
    lens_spec: LensSpec,
) -> FusionResult:
    """Blue mode: standard fusion. Can we detect them?"""
    model = FusionMatch(lens_spec)
    return model.run(local_df, remote_df)

def run_red(
    our_data: dask.dataframe.DataFrame,
    adversary_lens: LensSpec,
) -> RedAssessment:
    """Red mode: what can adversary detect about us?

    Feed our data through adversary's assumed lens.
    Returns which of our entities would be detectable.
    """

def run_counter(
    our_data: dask.dataframe.DataFrame,
    adversary_lens: LensSpec,
    perturbation_costs: dict,
    counter_objectives: dict,
) -> CounterPlan:
    """Counter mode: cheapest way to defeat adversary detection.

    Returns minimum-cost perturbation set that either:
    - Defeats blocking (preferred): changes blocking key to land in different bucket
    - Defeats scoring (fallback): drops confidence below adversary threshold
    """

CounterPlan

@dataclass
class CounterPlan:
    defeat_type: str                    # "blocking_defeat" or "scoring_defeat"
    perturbations: list[Perturbation]   # Ordered by cost
    total_cost: float
    original_detectability: float       # Confidence before perturbation
    resulting_detectability: float      # Confidence after perturbation (0.0 if blocking defeat)
    entities_affected: int

@dataclass
class Perturbation:
    field: str
    original_value: Any
    perturbed_value: Any                # None for blocking defeat (just needs to be "different")
    cost: float
    effect: str                         # "blocking_key_change" or "score_reduction"
    score_impact: float                 # How much this drops confidence

Test Fixtures

FIX-01: Blue mode = standard fusion

def test_blue_mode_equals_standard():
    result = run_blue(df_a, df_b, vrs_lens)
    standard = FusionMatch(vrs_lens).run(df_a, df_b)
    assert result.confirmed.equals(standard.confirmed)

FIX-02: Red mode detects our entities

def test_red_mode_detection():
    assessment = run_red(our_pnt_data, assumed_adversary_lens)
    assert assessment.detectable_entities > 0
    assert all(e.confidence > 0 for e in assessment.detected)

FIX-03: Counter prefers blocking defeat

def test_counter_prefers_blocking():
    plan = run_counter(our_data, adversary_lens, costs, {"target": "blocking_defeat"})
    if plan.defeat_type == "blocking_defeat":
        assert plan.resulting_detectability == 0.0  # Invisible
        assert plan.total_cost <= scoring_defeat_plan.total_cost  # Cheaper

File Layout

parallax/ops/fusion/
├── counter.py              # run_blue, run_red, run_counter
├── counter_types.py        # CounterPlan, Perturbation, RedAssessment
└── tests/
    └── test_counter.py

Integration Points


Depends on: component.parallax.blocking-engine, component.parallax.lens-parser, component.parallax.scoring-engine

Realizes: product.fusion