Skip to content

Commit

Permalink
cli: link to rule names to capa rules website
Browse files Browse the repository at this point in the history
  • Loading branch information
williballenthin committed Aug 29, 2024
1 parent d6b90e7 commit 7c2291e
Showing 1 changed file with 134 additions and 81 deletions.
215 changes: 134 additions & 81 deletions capa/render/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,36 @@
# 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 io
import collections
import urllib.parse

import rich
import rich.text
import rich.table
import rich.console
import tabulate
from rich.text import Text
from rich.console import Console

import capa.render.utils as rutils
import capa.render.result_document as rd
import capa.features.freeze.features as frzf
from capa.rules import RuleSet
from capa.rules import RuleSet, Rule
from capa.engine import MatchResults
from capa.render.utils import StringIO

tabulate.PRESERVE_WHITESPACE = True


def bold_markup(s) -> str:
return f"[cyan]{s}[/cyan]"


def link_markup(s: str, href: str) -> str:
return f"[link={href}]{s}[/link]"


def width(s: str, character_count: int) -> str:
"""pad the given string to at least `character_count`"""
if len(s) < character_count:
Expand All @@ -28,20 +44,31 @@ def width(s: str, character_count: int) -> str:
return s


def render_meta(doc: rd.ResultDocument, ostream: StringIO):
def render_sample_link(hash: str) -> str:
url = "https://www.virustotal.com/gui/file/" + hash
return link_markup(hash, url)


def render_meta(doc: rd.ResultDocument, console: Console):
rows = [
(width("md5", 22), width(doc.meta.sample.md5, 82)),
("sha1", doc.meta.sample.sha1),
("sha256", doc.meta.sample.sha256),
("md5", Text.from_markup(render_sample_link(doc.meta.sample.md5))),
("sha1", Text.from_markup(render_sample_link(doc.meta.sample.sha1))),
("sha256", Text.from_markup(render_sample_link(doc.meta.sample.sha256))),
("analysis", doc.meta.flavor.value),
("os", doc.meta.analysis.os),
("format", doc.meta.analysis.format),
("arch", doc.meta.analysis.arch),
("path", doc.meta.sample.path),
]

ostream.write(tabulate.tabulate(rows, tablefmt="mixed_outline"))
ostream.write("\n")
table = rich.table.Table(show_header=False, min_width=100)
table.add_column()
table.add_column()

for row in rows:
table.add_row(*row)

console.print(table)


def find_subrule_matches(doc: rd.ResultDocument):
Expand Down Expand Up @@ -71,7 +98,12 @@ def rec(match: rd.Match):
return matches


def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
def render_rule_name(rule: Rule) -> str:
url = f"https://mandiant.github.io/capa/rules/{urllib.parse.quote(rule.meta.name)}/"
return bold_markup(link_markup(rule.meta.name, url))


def render_capabilities(doc: rd.ResultDocument, console: Console):
"""
example::
Expand All @@ -95,25 +127,30 @@ def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):

count = len(rule.matches)
if count == 1:
capability = rutils.bold(rule.meta.name)
capability = render_rule_name(rule)
else:
capability = f"{rutils.bold(rule.meta.name)} ({count} matches)"
rows.append((capability, rule.meta.namespace))
capability = render_rule_name(rule) + f" ({count} matches)"
rows.append((Text.from_markup(capability), rule.meta.namespace))

if rows:
ostream.write(
tabulate.tabulate(
rows,
headers=[width("Capability", 50), width("Namespace", 50)],
tablefmt="mixed_outline",
)
)
ostream.write("\n")
table = rich.table.Table(min_width=100)
table.add_column("Capability")
table.add_column("Namespace", 50)

for row in rows:
table.add_row(*row)

console.print(table)
else:
ostream.writeln(rutils.bold("no capabilities found"))
console.print(bold_markup("no capabilities found"))


def render_attack(doc: rd.ResultDocument, ostream: StringIO):
def render_attack_link(id: str) -> str:
url = f"https://attack.mitre.org/techniques/{id.replace('.', '/')}/"
return rf"\[{link_markup(id, url)}]"


def render_attack(doc: rd.ResultDocument, console: Console):
"""
example::
Expand All @@ -132,35 +169,36 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
tactics = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
for attack in rule.meta.attack:
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id))
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id.strip("[").strip("]")))

rows = []
for tactic, techniques in sorted(tactics.items()):
inner_rows = []
for technique, subtechnique, id in sorted(techniques):
if not subtechnique:
inner_rows.append(f"{rutils.bold(technique)} {id}")
# example: File and Directory Discovery [T1083]
inner_rows.append(bold_markup(technique) + " " + render_attack_link(id))
else:
inner_rows.append(f"{rutils.bold(technique)}::{subtechnique} {id}")
rows.append(
(
rutils.bold(tactic.upper()),
"\n".join(inner_rows),
)
)
# example: Code Discovery::Enumerate PE Sections [T1084.001]
inner_rows.append(bold_markup(technique) + "::" + subtechnique + " " + render_attack_link(id))

tactic = bold_markup(tactic.upper())
technique = "\n".join(inner_rows)

rows.append((Text.from_markup(tactic), Text.from_markup(technique)))

if rows:
ostream.write(
tabulate.tabulate(
rows,
headers=[width("ATT&CK Tactic", 20), width("ATT&CK Technique", 80)],
tablefmt="mixed_grid",
)
)
ostream.write("\n")
table = rich.table.Table(min_width=100)
table.add_column(width("ATT&CK Tactic", 20))
table.add_column("ATT&CK Technique")

for row in rows:
table.add_row(*row)

def render_maec(doc: rd.ResultDocument, ostream: StringIO):
console.print(table)


def render_maec(doc: rd.ResultDocument, console: Console):
"""
example::
Expand Down Expand Up @@ -193,20 +231,37 @@ def render_maec(doc: rd.ResultDocument, ostream: StringIO):
for category in sorted(maec_categories):
values = maec_table.get(category, set())
if values:
rows.append((rutils.bold(category.replace("_", "-")), "\n".join(sorted(values))))
rows.append((Text.from_markup(bold_markup(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")
table = rich.table.Table(min_width=100)
table.add_column(width("MAEC Category", 20))
table.add_column("MAEC Value")

for row in rows:
table.add_row(*row)

console.print(table)


def render_mbc_link(id: str, objective: str, behavior: str) -> Text:
if id[0] in {"B", "T", "E", "F"}:
# behavior
base_url = "https://github.com/MBCProject/mbc-markdown/blob/main"
elif id[0] in {"C", }:
# micro-behavior
base_url = "https://github.com/MBCProject/mbc-markdown/blob/main/micro-behaviors"
else:
raise ValueError("unexpected MBC prefix")

objective_fragment = objective.lower().replace(" ", "-")
behavior_fragment = behavior.lower().replace(" ", "-")

url = f"{base_url}/{objective_fragment}/{behavior_fragment}.md"
return rf"\[{link_markup(id, url)}]"

def render_mbc(doc: rd.ResultDocument, ostream: StringIO):

def render_mbc(doc: rd.ResultDocument, console: Console):
"""
example::
Expand All @@ -223,48 +278,46 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
objectives = collections.defaultdict(set)
for rule in rutils.capability_rules(doc):
for mbc in rule.meta.mbc:
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id))
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id.strip("[").strip("]")))

rows = []
for objective, behaviors in sorted(objectives.items()):
inner_rows = []
for behavior, method, id in sorted(behaviors):
if not method:
inner_rows.append(f"{rutils.bold(behavior)} [{id}]")
for technique, subtechnique, id in sorted(behaviors):
if not subtechnique:
# example: File and Directory Discovery [T1083]
inner_rows.append(bold_markup(technique) + " " + render_mbc_link(id, objective, technique))
else:
inner_rows.append(f"{rutils.bold(behavior)}::{method} [{id}]")
rows.append(
(
rutils.bold(objective.upper()),
"\n".join(inner_rows),
)
)
# example: Code Discovery::Enumerate PE Sections [T1084.001]
inner_rows.append(bold_markup(technique) + "::" + subtechnique + " " + render_mbc_link(id, objective, technique))

objective = bold_markup(objective.upper())
technique = "\n".join(inner_rows)

rows.append((Text.from_markup(objective), Text.from_markup(technique)))

if rows:
ostream.write(
tabulate.tabulate(
rows,
headers=[width("MBC Objective", 25), width("MBC Behavior", 75)],
tablefmt="mixed_grid",
)
)
ostream.write("\n")
table = rich.table.Table(min_width=100)
table.add_column(width("MBC Objective", 20))
table.add_column("MBC Behavior")

for row in rows:
table.add_row(*row)

console.print(table)


def render_default(doc: rd.ResultDocument):
ostream = rutils.StringIO()

render_meta(doc, ostream)
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)

return ostream.getvalue()
f = io.StringIO()
console = rich.console.Console()

render_meta(doc, console)
render_attack(doc, console)
render_maec(doc, console)
render_mbc(doc, console)
render_capabilities(doc, console)

return f.getvalue()


def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
Expand Down

0 comments on commit 7c2291e

Please sign in to comment.