diff --git a/rules/ttp/R0041.yaml b/rules/ttp/R0041.yaml new file mode 100644 index 00000000..bdbbf724 --- /dev/null +++ b/rules/ttp/R0041.yaml @@ -0,0 +1,24 @@ +rule_id: R0041 +rule_version: 1 +name: open_relay_abuse +description: | + High RCPT count with a foreign From — abuse of an open relay to + push outbound mail. EmailLifter (E.3.12). +applies_to: + - email +match: + kind: lifter:open_relay + rcpt_threshold: 10 + require_foreign_from: true +emits: + - tactic: TA0040 + technique_id: T1496 + confidence: 0.85 + - tactic: TA0042 + technique_id: T1586 + sub_technique_id: T1586.002 + confidence: 0.85 +evidence_fields: + - rcpt_count + - from_domain + - mail_from_domain diff --git a/rules/ttp/R0042.yaml b/rules/ttp/R0042.yaml new file mode 100644 index 00000000..63d6a4f7 --- /dev/null +++ b/rules/ttp/R0042.yaml @@ -0,0 +1,19 @@ +rule_id: R0042 +rule_version: 1 +name: mass_phishing_campaign +description: | + RCPT count above threshold + body simhash matching across N + recipients in a window. EmailLifter (E.3.12). +applies_to: + - email +match: + kind: lifter:email_mass_phish + rcpt_threshold: 25 + body_simhash_window_h: 24 +emits: + - tactic: TA0001 + technique_id: T1566 + confidence: 0.85 +evidence_fields: + - rcpt_count + - body_simhash diff --git a/rules/ttp/R0043.yaml b/rules/ttp/R0043.yaml new file mode 100644 index 00000000..21c79be8 --- /dev/null +++ b/rules/ttp/R0043.yaml @@ -0,0 +1,22 @@ +rule_id: R0043 +rule_version: 1 +name: phishing_kit_xmailer +description: | + X-Mailer header matches a curated phishing-kit signature DB + (PHPMailer-of-known-kits, GreatHorn known-bad, etc.). +applies_to: + - email +match: + kind: lifter:email_xmailer_kit + catalog: phishing_kits +emits: + - tactic: TA0001 + technique_id: T1566 + confidence: 0.9 + - tactic: TA0042 + technique_id: T1588 + sub_technique_id: T1588.001 + confidence: 0.85 +evidence_fields: + - x_mailer + - matched_kit diff --git a/rules/ttp/R0044.yaml b/rules/ttp/R0044.yaml new file mode 100644 index 00000000..df66faa3 --- /dev/null +++ b/rules/ttp/R0044.yaml @@ -0,0 +1,23 @@ +rule_id: R0044 +rule_version: 1 +name: idn_homoglyph_url +description: | + IDN / Punycode (xn--) URL in email body. Two emits: masquerade + (T1036.005) and credential-harvest landing-page (T1566.002). +applies_to: + - email +match: + kind: lifter:email_idn_url + punycode_prefix: 'xn--' +emits: + - tactic: TA0005 + technique_id: T1036 + sub_technique_id: T1036.005 + confidence: 0.9 + - tactic: TA0001 + technique_id: T1566 + sub_technique_id: T1566.002 + confidence: 0.9 +evidence_fields: + - matched_url + - decoded_idn diff --git a/rules/ttp/R0045.yaml b/rules/ttp/R0045.yaml new file mode 100644 index 00000000..467b2da5 --- /dev/null +++ b/rules/ttp/R0045.yaml @@ -0,0 +1,25 @@ +rule_id: R0045 +rule_version: 1 +name: sender_masquerade +description: | + From / Return-Path / MAIL FROM domain mismatch, or DKIM/SPF + fail signal in Authentication-Results. Lifter composes the three + header signals into a single masquerade verdict. +applies_to: + - email +match: + kind: lifter:email_sender_masquerade + signals: + - from_returnpath_mismatch + - from_mailfrom_mismatch + - dkim_fail + - spf_fail +emits: + - tactic: TA0005 + technique_id: T1036 + confidence: 0.85 +evidence_fields: + - from_domain + - return_path_domain + - mail_from_domain + - auth_results diff --git a/rules/ttp/R0046.yaml b/rules/ttp/R0046.yaml new file mode 100644 index 00000000..682e47fa --- /dev/null +++ b/rules/ttp/R0046.yaml @@ -0,0 +1,33 @@ +rule_id: R0046 +rule_version: 1 +name: malicious_attachment +description: | + Macro-bearing Office doc, .lnk, .iso/.img, password-protected + archive, or HTML-smuggling pattern. Lifter inspects the + attachment table (file_type + ole_macros + maldoc verdict). +applies_to: + - email +match: + kind: lifter:email_malicious_attachment + triggers: + - office_macro + - lnk + - iso + - img + - protected_archive + - html_smuggling + - mal_hash_match +emits: + - tactic: TA0002 + technique_id: T1204 + sub_technique_id: T1204.002 + confidence: 0.9 + - tactic: TA0001 + technique_id: T1566 + sub_technique_id: T1566.001 + confidence: 0.9 +evidence_fields: + - filename + - mime_type + - matched_trigger + - file_hash diff --git a/rules/ttp/R0047.yaml b/rules/ttp/R0047.yaml new file mode 100644 index 00000000..c17701f0 --- /dev/null +++ b/rules/ttp/R0047.yaml @@ -0,0 +1,31 @@ +rule_id: R0047 +rule_version: 1 +name: bec_pattern +description: | + Business email compromise: urgent-wire / CEO-impersonation + language + a request-for-action shape (gift cards, wire, + payroll change). Subject + body composite signal. +applies_to: + - email +match: + kind: lifter:email_bec + subject_keywords: + - urgent + - wire + - invoice + - payroll + - gift card + body_action_keywords: + - send + - transfer + - update banking + - confidential +emits: + - tactic: TA0001 + technique_id: T1566 + sub_technique_id: T1566.003 + confidence: 0.85 +evidence_fields: + - subject + - matched_subject_kw + - matched_body_kw diff --git a/rules/ttp/R0048.yaml b/rules/ttp/R0048.yaml new file mode 100644 index 00000000..79b399b0 --- /dev/null +++ b/rules/ttp/R0048.yaml @@ -0,0 +1,23 @@ +rule_id: R0048 +rule_version: 1 +name: encoded_payload_in_body +description: | + Base64-encoded blob ≥ N bytes embedded in the email body — + classic obfuscated-payload smuggling. +applies_to: + - email +match: + kind: lifter:email_encoded_payload + min_bytes: 4096 + encoding: base64 +emits: + - tactic: TA0011 + technique_id: T1071 + sub_technique_id: T1071.003 + confidence: 0.85 + - tactic: TA0005 + technique_id: T1027 + confidence: 0.9 +evidence_fields: + - encoded_bytes + - decoded_preview diff --git a/tests/ttp/rule_precision/test_email_rules.py b/tests/ttp/rule_precision/test_email_rules.py new file mode 100644 index 00000000..af27e2d9 --- /dev/null +++ b/tests/ttp/rule_precision/test_email_rules.py @@ -0,0 +1,54 @@ +"""R0041-R0048 — email cohort. + +EmailLifter (E.3.12) consumes these by rule_id. The v0 +:class:`RuleEngine` cannot parse SMTP envelopes, walk attachment +trees, or compose header / body / attachment signals — so these +rules are inert under the regex matcher. + +Asserts each YAML compiles, none fire from the v0 engine, and a +strict-xfail precision case that flips green when E.3.12 lands. +""" +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(41, 49)] + + +@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("email"): + 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.12 (EmailLifter)") +def test_email_rule_precision(rule_id: str) -> None: + pytest.fail(f"{rule_id}: EmailLifter not yet shipped (E.3.12)")