diff --git a/rules/ttp/R0049.yaml b/rules/ttp/R0049.yaml new file mode 100644 index 00000000..cc11b9a0 --- /dev/null +++ b/rules/ttp/R0049.yaml @@ -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 diff --git a/rules/ttp/R0050.yaml b/rules/ttp/R0050.yaml new file mode 100644 index 00000000..0738623e --- /dev/null +++ b/rules/ttp/R0050.yaml @@ -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 diff --git a/rules/ttp/R0051.yaml b/rules/ttp/R0051.yaml new file mode 100644 index 00000000..f81097b0 --- /dev/null +++ b/rules/ttp/R0051.yaml @@ -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 diff --git a/rules/ttp/R0052.yaml b/rules/ttp/R0052.yaml new file mode 100644 index 00000000..27388436 --- /dev/null +++ b/rules/ttp/R0052.yaml @@ -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 diff --git a/rules/ttp/R0053.yaml b/rules/ttp/R0053.yaml new file mode 100644 index 00000000..f68b72e6 --- /dev/null +++ b/rules/ttp/R0053.yaml @@ -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 diff --git a/tests/ttp/rule_precision/test_canary_rules.py b/tests/ttp/rule_precision/test_canary_rules.py new file mode 100644 index 00000000..e533b14a --- /dev/null +++ b/tests/ttp/rule_precision/test_canary_rules.py @@ -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)")