feat(ttp): E.3.8 R0054-R0058 intel cohort + mark step done
5 YAMLs for the intel-verdict cohort per Appendix B / A.10: AbuseIPDB category mapping, GreyNoise classification, Feodo Tracker hit, ThreatFox IOC type, aggregate-malicious bump-only. IntelLifter (E.3.10) consumes by rule_id and tolerates absence silently (null provider column → no tag). R0058 is the meta bump-only rule — emits a single confidence=0.0 sentinel so it validates and surfaces in the catalogue, but the repository's sub-0.3 drop ensures no fresh tag persists if the fanout fires accidentally. test_intel_rules.py pins that zero-confidence invariant. Marks E.3.8 done in development/TTP_TAGGING.md with the cohort- split summary.
This commit is contained in:
73
tests/ttp/rule_precision/test_intel_rules.py
Normal file
73
tests/ttp/rule_precision/test_intel_rules.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""R0054-R0058 — intel verdict cohort.
|
||||
|
||||
IntelLifter (E.3.10) reads ``AttackerIntel`` provider columns
|
||||
(AbuseIPDB, GreyNoise, Feodo, ThreatFox) and emits per the per-
|
||||
provider mapping tables in Appendix A.10. Per Appendix B every
|
||||
intel rule tolerates absence silently — a null provider column is
|
||||
"no tag from this rule", never an error. R0058 is the
|
||||
confidence-bump-only meta-rule (no fresh tag emission); the
|
||||
lifter inspects rule_id and bumps existing tags.
|
||||
|
||||
The v0 :class:`RuleEngine` cannot navigate the intel envelope —
|
||||
the rules are inert under regex.
|
||||
"""
|
||||
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(54, 59)]
|
||||
|
||||
|
||||
@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("intel"):
|
||||
tags = await precision_engine.evaluate(make_event(row))
|
||||
fired.update(tag.rule_id for tag in tags)
|
||||
assert rule_id not in fired
|
||||
|
||||
|
||||
def test_r0058_is_bump_only() -> None:
|
||||
"""R0058's only emit is a zero-confidence sentinel.
|
||||
|
||||
Per Appendix B the aggregate-malicious rule must not emit a fresh
|
||||
tag — it bumps existing rule confidences. The repository drops
|
||||
tags below 0.3 confidence, so even if the lifter accidentally
|
||||
drove the engine fanout the tag would never persist. This test
|
||||
pins that defense-in-depth property: any future edit pushing the
|
||||
R0058 emit confidence above 0 would fire here.
|
||||
"""
|
||||
compiled = _parse_and_compile(
|
||||
Path("rules/ttp/R0058.yaml"), RuleState(),
|
||||
)
|
||||
assert all(emit[3] == 0.0 for emit in compiled.emits), (
|
||||
"R0058 must keep all emit confidences at 0.0 (bump-only rule)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||
@pytest.mark.xfail(strict=True, reason="impl phase E.3.10 (IntelLifter)")
|
||||
def test_intel_rule_precision(rule_id: str) -> None:
|
||||
pytest.fail(f"{rule_id}: IntelLifter not yet shipped (E.3.10)")
|
||||
Reference in New Issue
Block a user