Merge pull request #598 from fireeye/render/json-attck-fields

parse att&ck for output doc
This commit is contained in:
Moritz
2021-06-02 16:54:31 +02:00
committed by GitHub
9 changed files with 153 additions and 24 deletions

View File

@@ -116,7 +116,7 @@ It includes many new rules, including all new techniques introduced in MITRE ATT
- linter: summarize results at the end #571 @williballenthin
- meta: added `library_functions` field, `feature_counts.functions` does not include library functions any more #562 @mr-tz
- linter: check for `or` with always true child statement, e.g. `optional`, colors #348 @mr-tz
- json: breaking change in results document; now contains parsed MBC fields instead of canonical representation #526 @mr-tz
- json: breaking change in results document; now contains parsed ATT&CK and MBC fields instead of canonical representation #526 @mr-tz
- json: breaking change: record all matching strings for regex #159 @williballenthin
- main: implement file limitations via rules not code #390 @williballenthin

View File

@@ -10,6 +10,7 @@ import json
import capa.rules
import capa.engine
import capa.render.utils
def convert_statement_to_result_document(statement):
@@ -203,26 +204,45 @@ def convert_match_to_result_document(rules, capabilities, result):
def convert_meta_to_result_document(meta):
attacks = meta.get("att&ck", [])
meta["att&ck"] = [parse_canonical_attack(attack) for attack in attacks]
mbcs = meta.get("mbc", [])
meta["mbc"] = [parse_canonical_mbc(mbc) for mbc in mbcs]
return meta
def parse_canonical_attack(attack):
"""
parse capa's canonical ATT&CK representation: `Tactic::Technique::Subtechnique [Identifier]`
"""
tactic = ""
technique = ""
subtechnique = ""
parts, id = capa.render.utils.parse_parts_id(attack)
if len(parts) > 0:
tactic = parts[0]
if len(parts) > 1:
technique = parts[1]
if len(parts) > 2:
subtechnique = parts[2]
return {
"parts": parts,
"id": id,
"tactic": tactic,
"technique": technique,
"subtechnique": subtechnique,
}
def parse_canonical_mbc(mbc):
"""
parse capa's canonical MBC representation: `Objective::Behavior::Method [Identifier]`
"""
id = ""
objective = ""
behavior = ""
method = ""
parts = mbc.split("::")
if len(parts) > 0:
last = parts.pop()
last, _, id = last.rpartition(" ")
id = id.lstrip("[").rstrip("]")
parts.append(last)
parts, id = capa.render.utils.parse_parts_id(mbc)
if len(parts) > 0:
objective = parts[0]
if len(parts) > 1:

View File

@@ -123,14 +123,10 @@ def render_attack(doc, ostream):
continue
for attack in rule["meta"]["att&ck"]:
tactic, _, rest = attack.partition("::")
if "::" in rest:
technique, _, rest = rest.partition("::")
subtechnique, _, id = rest.rpartition(" ")
tactics[tactic].add((technique, subtechnique, id))
if attack.get("subtechnique"):
tactics[attack["tactic"]].add((attack["technique"], attack["subtechnique"], attack["id"]))
else:
technique, _, id = rest.rpartition(" ")
tactics[tactic].add((technique, id))
tactics[attack["tactic"]].add((attack["technique"], attack["id"]))
rows = []
for tactic, techniques in sorted(tactics.items()):

View File

@@ -29,8 +29,22 @@ def hex(n):
return "0x%X" % n
def format_mbc(mbc):
return "%s [%s]" % ("::".join(mbc["parts"]), mbc["id"])
def parse_parts_id(s):
id = ""
parts = s.split("::")
if len(parts) > 0:
last = parts.pop()
last, _, id = last.rpartition(" ")
id = id.lstrip("[").rstrip("]")
parts.append(last)
return parts, id
def format_parts_id(data):
"""
format canonical representation of ATT&CK/MBC parts and ID
"""
return "%s [%s]" % ("::".join(data["parts"]), data["id"])
def capability_rules(doc):

View File

@@ -219,8 +219,8 @@ def render_rules(ostream, doc):
if not v:
continue
if key == "mbc":
v = [rutils.format_mbc(mbc) for mbc in v]
if key in ("att&ck", "mbc"):
v = [rutils.format_parts_id(vv) for vv in v]
if isinstance(v, list) and len(v) == 1:
v = v[0]

View File

@@ -579,8 +579,9 @@ class Rule(object):
raise InvalidRule("{:s} is not a supported scope".format(scope))
meta = d["rule"]["meta"]
mbcs = meta.get("mbc", [])
if not isinstance(mbcs, list):
if not isinstance(meta.get("att&ck", []), list):
raise InvalidRule("ATT&CK mapping must be a list")
if not isinstance(meta.get("mbc", []), list):
raise InvalidRule("MBC mapping must be a list")
return cls(name, scope, build_statements(statements[0], scope), meta, definition)

View File

@@ -9,7 +9,6 @@
import json
import textwrap
import pytest
from fixtures import *
import capa.main
@@ -361,8 +360,8 @@ def test_not_render_rules_also_matched(z9324d_extractor, capsys):
assert "create TCP socket" in std.out
# It tests main works with different backends
def test_backend_option(capsys):
# tests that main works with different backends
path = get_data_path_by_name("pma16-01")
assert capa.main.main([path, "-j", "-b", capa.main.BACKEND_VIV]) == 0
std = capsys.readouterr()

71
tests/test_render.py Normal file
View File

@@ -0,0 +1,71 @@
import textwrap
import capa.rules
from capa.render import convert_meta_to_result_document
from capa.render.utils import format_parts_id
def test_render_meta_attack():
# Persistence::Boot or Logon Autostart Execution::Registry Run Keys / Startup Folder [T1547.001]
id = "T1543.003"
tactic = "Persistence"
technique = "Create or Modify System Process"
subtechnique = "Windows Service"
canonical = "{:s}::{:s}::{:s} [{:s}]".format(tactic, technique, subtechnique, id)
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
att&ck:
- {:s}
features:
- number: 1
""".format(
canonical
)
)
r = capa.rules.Rule.from_yaml(rule)
rule_meta = convert_meta_to_result_document(r.meta)
attack = rule_meta["att&ck"][0]
assert attack["id"] == id
assert attack["tactic"] == tactic
assert attack["technique"] == technique
assert attack["subtechnique"] == subtechnique
assert format_parts_id(attack) == canonical
def test_render_meta_mbc():
# Defense Evasion::Disable or Evade Security Tools::Heavens Gate [F0004.008]
id = "F0004.008"
objective = "Defense Evasion"
behavior = "Disable or Evade Security Tools"
method = "Heavens Gate"
canonical = "{:s}::{:s}::{:s} [{:s}]".format(objective, behavior, method, id)
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
mbc:
- {:s}
features:
- number: 1
""".format(
canonical
)
)
r = capa.rules.Rule.from_yaml(rule)
rule_meta = convert_meta_to_result_document(r.meta)
attack = rule_meta["mbc"][0]
assert attack["id"] == id
assert attack["objective"] == objective
assert attack["behavior"] == behavior
assert attack["method"] == method
assert format_parts_id(attack) == canonical

View File

@@ -399,6 +399,34 @@ def test_invalid_rules():
)
)
# att&ck and mbc must be lists
with pytest.raises(capa.rules.InvalidRule):
r = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
att&ck: Tactic::Technique::Subtechnique [Identifier]
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
r = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
mbc: Objective::Behavior::Method [Identifier]
features:
- number: 1
"""
)
)
def test_number_symbol():
rule = textwrap.dedent(