Skip to content

Commit

Permalink
render maec/* fields (#2087)
Browse files Browse the repository at this point in the history
* Render maec/* fields

* add test for render_maec

---------

Co-authored-by: Soufiane Fariss <soufiane.fariss@um5s.net.ma>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 5, 2024
1 parent b3ed42f commit 30d23c4
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

- add function in capa/helpers to load plain and compressed JSON reports #1883 @Rohit1123
- document Antivirus warnings and VirusTotal false positive detections #2028 @RionEV @mr-tz
- render maec/* fields #843 @s-ff
- replace Halo spinner with Rich #2086 @s-ff

### Breaking Changes
Expand Down
62 changes: 59 additions & 3 deletions capa/render/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):

if rows:
ostream.write(
tabulate.tabulate(rows, headers=[width("Capability", 50), width("Namespace", 50)], tablefmt="mixed_outline")
tabulate.tabulate(
rows,
headers=[width("Capability", 50), width("Namespace", 50)],
tablefmt="mixed_outline",
)
)
ostream.write("\n")
else:
Expand Down Expand Up @@ -148,7 +152,55 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
if rows:
ostream.write(
tabulate.tabulate(
rows, headers=[width("ATT&CK Tactic", 20), width("ATT&CK Technique", 80)], tablefmt="mixed_grid"
rows,
headers=[width("ATT&CK Tactic", 20), width("ATT&CK Technique", 80)],
tablefmt="mixed_grid",
)
)
ostream.write("\n")


def render_maec(doc: rd.ResultDocument, ostream: StringIO):
"""
example::
+--------------------------+-----------------------------------------------------------+
| MAEC Category | MAEC Value |
|--------------------------+-----------------------------------------------------------|
| analysis-conclusion | malicious |
|--------------------------+-----------------------------------------------------------|
| malware-family | PlugX |
|--------------------------+-----------------------------------------------------------|
| malware-category | downloader |
| | launcher |
+--------------------------+-----------------------------------------------------------+
"""
maec_categories = {
"analysis_conclusion",
"analysis_conclusion_ov",
"malware_family",
"malware_category",
"malware_category_ov",
}
maec_table = collections.defaultdict(set)
for rule in rutils.maec_rules(doc):
for maec_category in maec_categories:
maec_value = getattr(rule.meta.maec, maec_category, None)
if maec_value:
maec_table[maec_category].add(maec_value)

rows = []
for category in sorted(maec_categories):
values = maec_table.get(category, set())
if values:
rows.append((rutils.bold(category.replace("_", "-")), "\n".join(sorted(values))))

if rows:
ostream.write(
tabulate.tabulate(
rows,
headers=[width("MAEC Category", 25), width("MAEC Value", 75)],
tablefmt="mixed_grid",
)
)
ostream.write("\n")
Expand Down Expand Up @@ -191,7 +243,9 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
if rows:
ostream.write(
tabulate.tabulate(
rows, headers=[width("MBC Objective", 25), width("MBC Behavior", 75)], tablefmt="mixed_grid"
rows,
headers=[width("MBC Objective", 25), width("MBC Behavior", 75)],
tablefmt="mixed_grid",
)
)
ostream.write("\n")
Expand All @@ -204,6 +258,8 @@ def render_default(doc: rd.ResultDocument):
ostream.write("\n")
render_attack(doc, ostream)
ostream.write("\n")
render_maec(doc, ostream)
ostream.write("\n")
render_mbc(doc, ostream)
ostream.write("\n")
render_capabilities(doc, ostream)
Expand Down
24 changes: 22 additions & 2 deletions capa/render/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# See the License for the specific language governing permissions and limitations under the License.

import io
from typing import Union, Iterator
from typing import Dict, List, Tuple, Union, Iterator, Optional

import termcolor

Expand Down Expand Up @@ -40,9 +40,14 @@ def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
return f"{'::'.join(data.parts)} [{data.id}]"


def sort_rules(rules: Dict[str, rd.RuleMatches]) -> List[Tuple[Optional[str], str, rd.RuleMatches]]:
"""Sort rules by namespace and name."""
return sorted((rule.meta.namespace or "", rule.meta.name, rule) for rule in rules.values())


def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
"""enumerate the rules in (namespace, name) order that are 'capability' rules (not lib/subscope/disposition/etc)."""
for _, _, rule in sorted((rule.meta.namespace or "", rule.meta.name, rule) for rule in doc.rules.values()):
for _, _, rule in sort_rules(doc.rules):
if rule.meta.lib:
continue
if rule.meta.is_subscope_rule:
Expand All @@ -61,6 +66,21 @@ def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
yield rule


def maec_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
"""enumerate 'maec' rules."""
for rule in doc.rules.values():
if any(
[
rule.meta.maec.analysis_conclusion,
rule.meta.maec.analysis_conclusion_ov,
rule.meta.maec.malware_family,
rule.meta.maec.malware_category,
rule.meta.maec.malware_category_ov,
]
):
yield rule


class StringIO(io.StringIO):
def writeln(self, s):
self.write(s)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import textwrap
from unittest.mock import Mock

import fixtures

Expand All @@ -19,6 +20,7 @@
import capa.features.address
import capa.features.basicblock
import capa.render.result_document
import capa.render.result_document as rd
import capa.features.freeze.features


Expand Down Expand Up @@ -113,6 +115,54 @@ def test_render_meta_mbc():
assert capa.render.utils.format_parts_id(mbc) == canonical


def test_render_meta_maec():
malware_family = "PlugX"
malware_category = "downloader"
analysis_conclusion = "malicious"

rule_yaml = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
authors:
- foo
maec/malware-family: {:s}
maec/malware-category: {:s}
maec/analysis-conclusion: {:s}
features:
- number: 1
""".format(
malware_family, malware_category, analysis_conclusion
)
)
rule = capa.rules.Rule.from_yaml(rule_yaml)
rm = capa.render.result_document.RuleMatches(
meta=capa.render.result_document.RuleMetadata.from_capa(rule),
source=rule_yaml,
matches=(),
)

# create a mock ResultDocument
mock_rd = Mock(spec=rd.ResultDocument)
mock_rd.rules = {"test rule": rm}

# capture the output of render_maec
output_stream = capa.render.utils.StringIO()
capa.render.default.render_maec(mock_rd, output_stream)
output = output_stream.getvalue()

assert "analysis-conclusion" in output
assert analysis_conclusion in output
assert "malware-category" in output
assert malware_category in output
assert "malware-family" in output
assert malware_family in output


@fixtures.parametrize(
"feature,expected",
[
Expand Down

0 comments on commit 30d23c4

Please sign in to comment.