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
- component.parallax.blocking-engine + component.parallax.scoring-engine → here: Same blocking + scoring engine, different inputs
- component.parallax.lens-parser → here: Counter-lens YAML extends standard lens format
- Here → ADI: Counter results can generate advisory signals
Depends on: component.parallax.blocking-engine, component.parallax.lens-parser, component.parallax.scoring-engine
Realizes: product.fusion