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:
23
rules/ttp/R0031.yaml
Normal file
23
rules/ttp/R0031.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0031
|
||||||
|
rule_version: 1
|
||||||
|
name: beaconing
|
||||||
|
description: |
|
||||||
|
Periodic outbound activity with low jitter — classic C2 beacon.
|
||||||
|
Read from AttackerBehavior.beacon_interval_s / .beacon_jitter_pct
|
||||||
|
by the BehavioralLifter (E.3.9).
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:beaconing
|
||||||
|
max_jitter_pct: 0.15
|
||||||
|
min_interval_s: 10
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1071
|
||||||
|
confidence: 0.8
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1029
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- beacon_interval_s
|
||||||
|
- beacon_jitter_pct
|
||||||
24
rules/ttp/R0032.yaml
Normal file
24
rules/ttp/R0032.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
rule_id: R0032
|
||||||
|
rule_version: 1
|
||||||
|
name: data_destruction
|
||||||
|
description: |
|
||||||
|
Mass destructive ops: Redis FLUSHALL, SQL DROP DATABASE, MongoDB
|
||||||
|
dropDatabase(), bulk DELETE without WHERE. Cross-event because we
|
||||||
|
want to confirm the verb landed on real data, not just a parse.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:data_destruction
|
||||||
|
patterns:
|
||||||
|
- 'FLUSHALL'
|
||||||
|
- 'DROP\\s+DATABASE'
|
||||||
|
- 'TRUNCATE\\s+TABLE'
|
||||||
|
- 'dropDatabase\\(\\)'
|
||||||
|
- 'DELETE\\s+/\\_all'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0040
|
||||||
|
technique_id: T1485
|
||||||
|
confidence: 0.95
|
||||||
|
evidence_fields:
|
||||||
|
- matched_op
|
||||||
|
- target
|
||||||
26
rules/ttp/R0033.yaml
Normal file
26
rules/ttp/R0033.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
rule_id: R0033
|
||||||
|
rule_version: 1
|
||||||
|
name: ransom_note_pattern
|
||||||
|
description: |
|
||||||
|
Bitcoin/Monero address + payment-demand language inserted into a
|
||||||
|
honeydoc, mail body, or DB collection. EmailLifter (R0033 fires
|
||||||
|
from email_body too) and BehavioralLifter share the same rule_id.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:ransom_note
|
||||||
|
require_btc_or_xmr: true
|
||||||
|
payment_keywords:
|
||||||
|
- bitcoin
|
||||||
|
- btc
|
||||||
|
- monero
|
||||||
|
- ransom
|
||||||
|
- decrypt
|
||||||
|
emits:
|
||||||
|
- tactic: TA0040
|
||||||
|
technique_id: T1486
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- btc_address
|
||||||
|
- matched_keywords
|
||||||
21
rules/ttp/R0034.yaml
Normal file
21
rules/ttp/R0034.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
rule_id: R0034
|
||||||
|
rule_version: 1
|
||||||
|
name: exfil_over_web
|
||||||
|
description: |
|
||||||
|
Outbound data exfil to a web service (POST with large body /
|
||||||
|
Content-Length, or many GETs encoding payload in path). Read
|
||||||
|
from session aggregates, not single requests.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:exfil_over_web
|
||||||
|
min_payload_bytes: 1048576
|
||||||
|
request_threshold: 50
|
||||||
|
emits:
|
||||||
|
- tactic: TA0010
|
||||||
|
technique_id: T1567
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- bytes_out
|
||||||
|
- request_count
|
||||||
|
- target_host
|
||||||
21
rules/ttp/R0035.yaml
Normal file
21
rules/ttp/R0035.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
rule_id: R0035
|
||||||
|
rule_version: 1
|
||||||
|
name: db_mass_read
|
||||||
|
description: |
|
||||||
|
SELECT/COPY/RETR pulling many rows or whole tables from a DB
|
||||||
|
honeypot. BehavioralLifter (E.3.9) reads the per-session
|
||||||
|
query-byte aggregate.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:db_mass_read
|
||||||
|
min_rows: 10000
|
||||||
|
min_bytes: 5242880
|
||||||
|
emits:
|
||||||
|
- tactic: TA0009
|
||||||
|
technique_id: T1213
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- service
|
||||||
|
- rows_read
|
||||||
|
- bytes_read
|
||||||
25
rules/ttp/R0036.yaml
Normal file
25
rules/ttp/R0036.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
rule_id: R0036
|
||||||
|
rule_version: 1
|
||||||
|
name: credentials_in_files
|
||||||
|
description: |
|
||||||
|
Reading .env / .git/config / cloud credential files —
|
||||||
|
credential-from-file harvesting. Lifter-driven so the rule
|
||||||
|
composes the file-access signal with the path discriminator.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
- http_request
|
||||||
|
match:
|
||||||
|
kind: lifter:credentials_in_files
|
||||||
|
paths:
|
||||||
|
- '\\.env'
|
||||||
|
- '\\.git/config'
|
||||||
|
- '\\.aws/credentials'
|
||||||
|
- '\\.ssh/id_rsa'
|
||||||
|
- 'wp-config\\.php'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0006
|
||||||
|
technique_id: T1552
|
||||||
|
sub_technique_id: T1552.001
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- matched_path
|
||||||
22
rules/ttp/R0037.yaml
Normal file
22
rules/ttp/R0037.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
rule_id: R0037
|
||||||
|
rule_version: 1
|
||||||
|
name: k8s_service_account_token
|
||||||
|
description: |
|
||||||
|
Reading /api/v1/namespaces/*/secrets or /var/run/secrets/k8s.io/
|
||||||
|
serviceaccount/token — kube SA harvest.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
- http_request
|
||||||
|
match:
|
||||||
|
kind: lifter:k8s_sa_token
|
||||||
|
paths:
|
||||||
|
- '/api/v1/namespaces/[^/]+/secrets'
|
||||||
|
- '/var/run/secrets/kubernetes\\.io/serviceaccount'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0006
|
||||||
|
technique_id: T1552
|
||||||
|
sub_technique_id: T1552.007
|
||||||
|
confidence: 0.95
|
||||||
|
evidence_fields:
|
||||||
|
- matched_path
|
||||||
|
- namespace
|
||||||
23
rules/ttp/R0038.yaml
Normal file
23
rules/ttp/R0038.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0038
|
||||||
|
rule_version: 1
|
||||||
|
name: docker_host_escape
|
||||||
|
description: |
|
||||||
|
Privileged container creation or bind-mount of host /, /etc, or
|
||||||
|
/var/run/docker.sock — host-escape primitive. Lifter inspects
|
||||||
|
the structured Docker-API event payload.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:docker_escape
|
||||||
|
signals:
|
||||||
|
- 'privileged:true'
|
||||||
|
- 'bind:/:/'
|
||||||
|
- 'bind:/etc'
|
||||||
|
- 'bind:/var/run/docker.sock'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0004
|
||||||
|
technique_id: T1611
|
||||||
|
confidence: 0.95
|
||||||
|
evidence_fields:
|
||||||
|
- matched_signal
|
||||||
|
- container_image
|
||||||
19
rules/ttp/R0039.yaml
Normal file
19
rules/ttp/R0039.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
rule_id: R0039
|
||||||
|
rule_version: 1
|
||||||
|
name: llmnr_poisoning
|
||||||
|
description: |
|
||||||
|
Responder-style LLMNR/NBT-NS spoofed reply pattern observed
|
||||||
|
on the network sniffer. BehavioralLifter (E.3.9) reads the
|
||||||
|
network-event aggregate.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:llmnr_poisoning
|
||||||
|
emits:
|
||||||
|
- tactic: TA0009
|
||||||
|
technique_id: T1557
|
||||||
|
sub_technique_id: T1557.001
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- poisoned_query
|
||||||
|
- victim_host
|
||||||
23
rules/ttp/R0040.yaml
Normal file
23
rules/ttp/R0040.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0040
|
||||||
|
rule_version: 1
|
||||||
|
name: tftp_router_config_retrieval
|
||||||
|
description: |
|
||||||
|
TFTP RRQ for a router-config-shaped filename (*-confg, *.cfg,
|
||||||
|
startup-config, running-config). Per Appendix A.4.
|
||||||
|
applies_to:
|
||||||
|
- session
|
||||||
|
match:
|
||||||
|
kind: lifter:tftp_router_config
|
||||||
|
filename_patterns:
|
||||||
|
- '.*-confg$'
|
||||||
|
- '.*\\.cfg$'
|
||||||
|
- 'startup-config'
|
||||||
|
- 'running-config'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0009
|
||||||
|
technique_id: T1602
|
||||||
|
sub_technique_id: T1602.002
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- tftp_filename
|
||||||
|
- source_host
|
||||||
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