feat(ttp): E.3.8 R0031-R0040 behavioral cohort
10 YAMLs for the behavioral / cross-event cohort per Appendix B: beaconing, data destruction, ransom note, web exfil, DB mass-read, credentials-in-files, k8s SA token harvest, Docker host escape, LLMNR poisoning, TFTP router-config retrieval. Every rule is lifter-bound (BehavioralLifter / IdentityLifter) — the v0 RuleEngine cannot count, aggregate, or compose cross-event signals, so these YAMLs declare the technique mappings the lifter will consume by rule_id at E.3.9. Their match specs use a 'kind: lifter:*' shape inert to the regex matcher. test_behavioral_rules.py asserts each YAML compiles, none fire from the v0 engine (FP regression guard against a YAML drifting into a regex), and an xfail(strict=True, reason='impl phase E.3.9') precision case that will flip green when the lifter lands.
This commit is contained in:
77
tests/ttp/rule_precision/test_behavioral_rules.py
Normal file
77
tests/ttp/rule_precision/test_behavioral_rules.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""R0031-R0040 — behavioral / cross-event cohort.
|
||||
|
||||
Every rule here is consumed by the BehavioralLifter (or an
|
||||
identity-rollup variant) at E.3.9. The v0 :class:`RuleEngine` has no
|
||||
counter / aggregator — it can only regex over a single event
|
||||
payload — so these rules cannot fire from the engine alone. Their
|
||||
``match.kind`` keys (``lifter:beaconing`` etc.) are inert to the
|
||||
regex matcher by design.
|
||||
|
||||
This file asserts:
|
||||
|
||||
* every R003N has a YAML on disk that compiles
|
||||
* the v0 engine NEVER fires any of them (regression guard against a
|
||||
YAML drifting into a regex match)
|
||||
* the precision target test is :pyfunc:`pytest.xfail`-gated until
|
||||
the BehavioralLifter ships, matching the CDD pattern at
|
||||
``development/TTP_TAGGING.md:2450``.
|
||||
"""
|
||||
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(31, 41)]
|
||||
|
||||
|
||||
@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:
|
||||
"""Behavioral rules MUST NOT fire from the regex engine.
|
||||
|
||||
Walks both the behavioral and the command corpora — if any event
|
||||
in either set lights up a behavioral rule, a YAML drifted into a
|
||||
regex match.spec.
|
||||
"""
|
||||
fired: set[str] = set()
|
||||
for cohort in ("behavioral", "commands"):
|
||||
for row in corpus_loader(cohort):
|
||||
tags = await precision_engine.evaluate(make_event(row))
|
||||
fired.update(tag.rule_id for tag in tags)
|
||||
assert rule_id not in fired, (
|
||||
f"{rule_id} is lifter-bound but fired from the regex engine"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||
@pytest.mark.xfail(strict=True, reason="impl phase E.3.9 (BehavioralLifter)")
|
||||
def test_behavioral_rule_precision(rule_id: str) -> None:
|
||||
"""Will live once the BehavioralLifter ships at E.3.9.
|
||||
|
||||
The lifter consumes ``AttackerBehavior`` / session aggregates and
|
||||
emits one tag per matching rule_id. This test will then load the
|
||||
behavioral corpus, drive the lifter, and assert the per-rule
|
||||
precision target. Until that day this xfails strict so the suite
|
||||
flips green automatically when E.3.9 wires it up.
|
||||
"""
|
||||
pytest.fail(f"{rule_id}: BehavioralLifter not yet shipped (E.3.9)")
|
||||
Reference in New Issue
Block a user