Skip to content

Commit

Permalink
check rule format consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-tz committed Jan 26, 2021
1 parent 059ec8f commit 80f7184
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 12 deletions.
19 changes: 13 additions & 6 deletions capa/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,16 +614,20 @@ def _get_ruamel_yaml_parser():
return y

@classmethod
def from_yaml(cls, s):
# use pyyaml because it can be much faster than ruamel (pure python)
doc = yaml.load(s, Loader=cls._get_yaml_loader())
def from_yaml(cls, s, use_ruamel=False):
if use_ruamel:
# ruamel enables nice formatting and doc roundtripping with comments
doc = cls._get_ruamel_yaml_parser().load(s)
else:
# use pyyaml because it can be much faster than ruamel (pure python)
doc = yaml.load(s, Loader=cls._get_yaml_loader())
return cls.from_dict(doc, s)

@classmethod
def from_yaml_file(cls, path):
def from_yaml_file(cls, path, use_ruamel=False):
with open(path, "rb") as f:
try:
return cls.from_yaml(f.read().decode("utf-8"))
return cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
except InvalidRule as e:
raise InvalidRuleWithPath(path, str(e))

Expand Down Expand Up @@ -716,7 +720,10 @@ def move_to_end(m, k):
# tweaking `ruamel.indent()` doesn't quite give us the control we want.
# so, add the two extra spaces that we've determined we need through experimentation.
# see #263
doc = doc.replace(" description:", " description:")
# only do this for the features section, so the meta description doesn't get reformatted
# assumes features section always exists
features_offset = doc.find("features")
doc = doc[:features_offset] + doc[features_offset:].replace(" description:", " description:")
return doc


Expand Down
2 changes: 1 addition & 1 deletion scripts/capafmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def main(argv=None):
logging.basicConfig(level=level)
logging.getLogger("capafmt").setLevel(level)

rule = capa.rules.Rule.from_yaml_file(args.path)
rule = capa.rules.Rule.from_yaml_file(args.path, use_ruamel=True)
if args.in_place:
with open(args.path, "wb") as f:
f.write(rule.to_yaml().encode("utf-8"))
Expand Down
56 changes: 51 additions & 5 deletions scripts/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import os
import sys
import string
import difflib
import hashlib
import logging
import os.path
Expand All @@ -24,6 +25,7 @@
import posixpath

import capa.main
import capa.rules
import capa.engine
import capa.features
import capa.features.insn
Expand Down Expand Up @@ -194,7 +196,7 @@ def check_rule(self, ctx, rule):
continue

try:
extractor = capa.main.get_extractor(path, "auto")
extractor = capa.main.get_extractor(path, "auto", disable_progress=True)
capabilities, meta = capa.main.find_capabilities(ctx["rules"], extractor, disable_progress=True)
except Exception as e:
logger.error("failed to extract capabilities: %s %s %s", rule.name, path, e)
Expand Down Expand Up @@ -276,6 +278,39 @@ def check_features(self, ctx, features):
return False


class FormatSingleEmptyLineEOF(Lint):
name = "EOF format"
recommendation = "end file with a single empty line"

def check_rule(self, ctx, rule):
if rule.definition.endswith("\n") and not rule.definition.endswith("\n\n"):
return False
return True


class FormatIncorrect(Lint):
name = "rule format incorrect"
recommendation_template = "use scripts/capafmt.py or adjust as follows\n{:s}"

def check_rule(self, ctx, rule):
actual = rule.definition
expected = capa.rules.Rule.from_yaml(rule.definition, use_ruamel=True).to_yaml().encode("utf-8")

# ignore different quote characters
actual = actual.replace("'", '"')
expected = expected.replace("'", '"')

diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(1))

# deltas begin with two-letter code; " " means common line
difflen = len(list(filter(lambda l: l[0] != " ", diff)))
if difflen > 0:
self.recommendation = self.recommendation_template.format("".join(diff))
return True

return False


def run_lints(lints, ctx, rule):
for lint in lints:
if lint.check_rule(ctx, rule):
Expand Down Expand Up @@ -331,15 +366,25 @@ def lint_meta(ctx, rule):
)


def get_normpath(path):
return posixpath.normpath(path).replace(os.sep, "/")


def lint_features(ctx, rule):
features = get_features(ctx, rule)
return run_feature_lints(FEATURE_LINTS, ctx, features)


FORMAT_LINTS = (
FormatSingleEmptyLineEOF(),
FormatIncorrect(),
)


def lint_format(ctx, rule):
return run_lints(FORMAT_LINTS, ctx, rule)


def get_normpath(path):
return posixpath.normpath(path).replace(os.sep, "/")


def get_features(ctx, rule):
# get features from rule and all dependencies including subscopes and matched rules
features = []
Expand Down Expand Up @@ -390,6 +435,7 @@ def lint_rule(ctx, rule):
lint_meta(ctx, rule),
lint_logic(ctx, rule),
lint_features(ctx, rule),
lint_format(ctx, rule),
)
)

Expand Down

0 comments on commit 80f7184

Please sign in to comment.