feat(ttp): E.3.8 R0049-R0053 canary fingerprint cohort
5 YAMLs for the canary-fingerprint cohort per Appendix B / A.9: navigator.webdriver flag, automation canvas/audio/WebGL hash match, WebRTC IP leak, TZ/lang vs geo mismatch, platform inconsistency. CanaryFingerprintLifter (E.3.11) consumes by rule_id. test_canary_rules.py: YAML-present + inert-in-v0 + xfail(strict) gated on E.3.11.
This commit is contained in:
18
rules/ttp/R0049.yaml
Normal file
18
rules/ttp/R0049.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
rule_id: R0049
|
||||||
|
rule_version: 1
|
||||||
|
name: navigator_webdriver_flag
|
||||||
|
description: |
|
||||||
|
navigator.webdriver === true in the canary fingerprint payload —
|
||||||
|
Selenium / Puppeteer / Playwright telltale.
|
||||||
|
applies_to:
|
||||||
|
- canary_fingerprint
|
||||||
|
match:
|
||||||
|
kind: lifter:canary_webdriver
|
||||||
|
signal: navigator.webdriver_true
|
||||||
|
emits:
|
||||||
|
- tactic: TA0002
|
||||||
|
technique_id: T1059
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- navigator_webdriver
|
||||||
|
- ua_signature
|
||||||
28
rules/ttp/R0050.yaml
Normal file
28
rules/ttp/R0050.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
rule_id: R0050
|
||||||
|
rule_version: 1
|
||||||
|
name: automation_canvas_audio_hash
|
||||||
|
description: |
|
||||||
|
Canvas / audio / WebGL fingerprint hash matches a known automation
|
||||||
|
tooling cohort (Puppeteer / Playwright / Selenium / curl-impersonate).
|
||||||
|
applies_to:
|
||||||
|
- canary_fingerprint
|
||||||
|
match:
|
||||||
|
kind: lifter:canary_automation_hash
|
||||||
|
catalogues:
|
||||||
|
- puppeteer
|
||||||
|
- playwright
|
||||||
|
- selenium
|
||||||
|
- curl_impersonate
|
||||||
|
emits:
|
||||||
|
- tactic: TA0002
|
||||||
|
technique_id: T1059
|
||||||
|
confidence: 0.85
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1588
|
||||||
|
sub_technique_id: T1588.002
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- canvas_hash
|
||||||
|
- audio_hash
|
||||||
|
- webgl_hash
|
||||||
|
- matched_tool
|
||||||
21
rules/ttp/R0051.yaml
Normal file
21
rules/ttp/R0051.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
rule_id: R0051
|
||||||
|
rule_version: 1
|
||||||
|
name: webrtc_ip_leak
|
||||||
|
description: |
|
||||||
|
WebRTC-discovered private IP doesn't match the source-IP geo —
|
||||||
|
classic VPN/proxy obfuscation tell. CanaryFingerprintLifter
|
||||||
|
composes the leak with the IP geo lookup.
|
||||||
|
applies_to:
|
||||||
|
- canary_fingerprint
|
||||||
|
match:
|
||||||
|
kind: lifter:canary_webrtc_leak
|
||||||
|
require_geo_mismatch: true
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1090
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- webrtc_local_ip
|
||||||
|
- source_ip
|
||||||
|
- source_country
|
||||||
|
- leak_country
|
||||||
18
rules/ttp/R0052.yaml
Normal file
18
rules/ttp/R0052.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
rule_id: R0052
|
||||||
|
rule_version: 1
|
||||||
|
name: tz_lang_geo_mismatch
|
||||||
|
description: |
|
||||||
|
Browser timezone or accept-language doesn't match source-IP geo —
|
||||||
|
another proxy/VPN tell.
|
||||||
|
applies_to:
|
||||||
|
- canary_fingerprint
|
||||||
|
match:
|
||||||
|
kind: lifter:canary_tz_lang_mismatch
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1090
|
||||||
|
confidence: 0.7
|
||||||
|
evidence_fields:
|
||||||
|
- browser_timezone
|
||||||
|
- browser_language
|
||||||
|
- source_country
|
||||||
19
rules/ttp/R0053.yaml
Normal file
19
rules/ttp/R0053.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
rule_id: R0053
|
||||||
|
rule_version: 1
|
||||||
|
name: platform_inconsistency
|
||||||
|
description: |
|
||||||
|
navigator.platform / userAgent / WebGL renderer disagree —
|
||||||
|
classic hand-built crawler with mismatched stealth shimming.
|
||||||
|
applies_to:
|
||||||
|
- canary_fingerprint
|
||||||
|
match:
|
||||||
|
kind: lifter:canary_platform_inconsistency
|
||||||
|
emits:
|
||||||
|
- tactic: TA0005
|
||||||
|
technique_id: T1036
|
||||||
|
confidence: 0.8
|
||||||
|
evidence_fields:
|
||||||
|
- navigator_platform
|
||||||
|
- user_agent
|
||||||
|
- webgl_renderer
|
||||||
|
- mismatch_pairs
|
||||||
52
tests/ttp/rule_precision/test_canary_rules.py
Normal file
52
tests/ttp/rule_precision/test_canary_rules.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""R0049-R0053 — canary fingerprint cohort.
|
||||||
|
|
||||||
|
CanaryFingerprintLifter (E.3.11) parses the fingerprint payload
|
||||||
|
(navigator/webdriver flag, canvas/audio/WebGL hashes, WebRTC IPs,
|
||||||
|
TZ/language/geo composite) and emits per Appendix A.9. The v0
|
||||||
|
:class:`RuleEngine` cannot navigate a structured fingerprint blob —
|
||||||
|
these rules are inert under the regex matcher.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from decnet.ttp.impl.rule_engine import RuleEngine
|
||||||
|
from decnet.ttp.store.base import RuleState
|
||||||
|
from decnet.ttp.store.impl.filesystem import _parse_and_compile
|
||||||
|
from tests.ttp.rule_precision.conftest import CorpusRow, make_event
|
||||||
|
|
||||||
|
CohortLoader = Callable[[str], list[CorpusRow]]
|
||||||
|
|
||||||
|
_RULE_IDS = [f"R{n:04d}" for n in range(49, 54)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||||
|
def test_rule_yaml_present(rule_id: str) -> None:
|
||||||
|
path = Path("rules/ttp") / f"{rule_id}.yaml"
|
||||||
|
assert path.exists(), f"missing YAML: {path}"
|
||||||
|
compiled = _parse_and_compile(path, RuleState())
|
||||||
|
assert compiled.rule_id == rule_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||||
|
async def test_lifter_bound_inert_in_v0(
|
||||||
|
rule_id: str,
|
||||||
|
precision_engine: RuleEngine,
|
||||||
|
corpus_loader: CohortLoader,
|
||||||
|
) -> None:
|
||||||
|
fired: set[str] = set()
|
||||||
|
for row in corpus_loader("canary"):
|
||||||
|
tags = await precision_engine.evaluate(make_event(row))
|
||||||
|
fired.update(tag.rule_id for tag in tags)
|
||||||
|
assert rule_id not in fired
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
strict=True, reason="impl phase E.3.11 (CanaryFingerprintLifter)",
|
||||||
|
)
|
||||||
|
def test_canary_rule_precision(rule_id: str) -> None:
|
||||||
|
pytest.fail(f"{rule_id}: CanaryFingerprintLifter not yet shipped (E.3.11)")
|
||||||
Reference in New Issue
Block a user