feat(ttp): E.3.8 R0001-R0030 command cohort
30 YAMLs for the shell/command rule cohort per Appendix B (rules/ttp/). Splits into engine-active (R0007-R0029, regex on command_text / raw_url / user_agent) and lifter-bound (R0001-R0006, R0030 — the v0 RuleEngine cannot count auth attempts, do identity rollups, or parse fingerprint blobs; the BehavioralLifter / IdentityLifter / CredentialLifter consume them by rule_id at E.3.9 / E.3.13). test_command_rules.py asserts: - every R000N has a YAML that compiles - lifter-bound rules NEVER fire from the v0 engine (regression guard against a YAML drifting into a regex match.spec) - engine-active rules meet their Appendix-C precision target against the seed corpus (≥0.95 high-conf, ≥0.80 medium) Conftest fixes: precision_engine moved to module-scope so module- scope precomputed dispatch fixture (fired_by_label) can request it; _RULES_DIR path bumped from parents[2] to parents[3] so the loader resolves the project root regardless of pytest cwd; make_event synthesizes attacker_uuid so TTPTag's anchor invariant is satisfied. Seed corpus broadened: positive examples for every regex rule plus 6 negative examples across innocuous shell verbs (ls, echo, cd, ps, df, free) so FPs surface in precision rather than passing vacuously.
This commit is contained in:
@@ -32,7 +32,7 @@ from decnet.ttp.impl.rule_engine import CompiledRule, RuleEngine
|
||||
from decnet.ttp.store.base import RuleState
|
||||
from decnet.ttp.store.impl.filesystem import _parse_and_compile
|
||||
|
||||
_RULES_DIR = Path(__file__).resolve().parents[2] / "rules" / "ttp"
|
||||
_RULES_DIR = Path(__file__).resolve().parents[3] / "rules" / "ttp"
|
||||
_CORPUS_DIR = Path(__file__).resolve().parent / "corpus"
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ def compiled_rules() -> list[CompiledRule]:
|
||||
return _load_compiled_rules()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
@pytest_asyncio.fixture(scope="module")
|
||||
async def precision_engine(
|
||||
compiled_rules: list[CompiledRule],
|
||||
) -> RuleEngine:
|
||||
@@ -167,11 +167,19 @@ def corpus_loader() -> Callable[[str], list[CorpusRow]]:
|
||||
|
||||
|
||||
def make_event(row: CorpusRow, source_id: str = "src") -> TaggerEvent:
|
||||
"""Materialise a :class:`CorpusRow` into a :class:`TaggerEvent`."""
|
||||
"""Materialise a :class:`CorpusRow` into a :class:`TaggerEvent`.
|
||||
|
||||
Sets a deterministic ``attacker_uuid`` derived from the row label so
|
||||
the downstream ``TTPTag`` constructor's "at least one of
|
||||
attacker_uuid/identity_uuid" invariant is satisfied. The corpus
|
||||
rows themselves don't carry attacker identity — they're per-payload
|
||||
fixtures, not per-attacker — so this synthesis is purely a test
|
||||
plumbing concern.
|
||||
"""
|
||||
return TaggerEvent(
|
||||
source_kind=row.source_kind,
|
||||
source_id=source_id,
|
||||
attacker_uuid=None,
|
||||
attacker_uuid=f"corpus-{row.label}",
|
||||
identity_uuid=None,
|
||||
session_id=None,
|
||||
decky_id=None,
|
||||
|
||||
Reference in New Issue
Block a user