mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 15:49:46 -08:00
Merge pull request #598 from fireeye/render/json-attck-fields
parse att&ck for output doc
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
71
tests/test_render.py
Normal 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
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user