mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 23:59:48 -08:00
code style : update remaining files (#1353)
* code style: update string formatting using fstrings --------- Co-authored-by: Willi Ballenthin <willi.ballenthin@gmail.com> Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
This commit is contained in:
@@ -43,10 +43,12 @@ class Statement:
|
|||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
name = self.name.lower()
|
||||||
|
children = ",".join(map(str, self.get_children()))
|
||||||
if self.description:
|
if self.description:
|
||||||
return "%s(%s = %s)" % (self.name.lower(), ",".join(map(str, self.get_children())), self.description)
|
return f"{name}({children} = {self.description})"
|
||||||
else:
|
else:
|
||||||
return "%s(%s)" % (self.name.lower(), ",".join(map(str, self.get_children())))
|
return f"{name}({children})"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -232,9 +234,9 @@ class Range(Statement):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.max == (1 << 64 - 1):
|
if self.max == (1 << 64 - 1):
|
||||||
return "range(%s, min=%d, max=infinity)" % (str(self.child), self.min)
|
return f"range({str(self.child)}, min={self.min}, max=infinity)"
|
||||||
else:
|
else:
|
||||||
return "range(%s, min=%d, max=%d)" % (str(self.child), self.min, self.max)
|
return f"range({str(self.child)}, min={self.min}, max={self.max})"
|
||||||
|
|
||||||
|
|
||||||
class Subscope(Statement):
|
class Subscope(Statement):
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ class Feature(abc.ABC):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.value is not None:
|
if self.value is not None:
|
||||||
if self.description:
|
if self.description:
|
||||||
return "%s(%s = %s)" % (self.get_name_str(), self.get_value_str(), self.description)
|
return f"{self.get_name_str()}({self.get_value_str()} = {self.description})"
|
||||||
else:
|
else:
|
||||||
return "%s(%s)" % (self.get_name_str(), self.get_value_str())
|
return f"{self.get_name_str()}({self.get_value_str()})"
|
||||||
else:
|
else:
|
||||||
return "%s" % self.get_name_str()
|
return f"{self.get_name_str()}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -242,7 +242,7 @@ class Substring(String):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
assert isinstance(self.value, str)
|
assert isinstance(self.value, str)
|
||||||
return "substring(%s)" % escape_string(self.value)
|
return f"substring({escape_string(self.value)})"
|
||||||
|
|
||||||
|
|
||||||
class _MatchedSubstring(Substring):
|
class _MatchedSubstring(Substring):
|
||||||
@@ -267,11 +267,9 @@ class _MatchedSubstring(Substring):
|
|||||||
self.matches = matches
|
self.matches = matches
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
matches = ", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys()))
|
||||||
assert isinstance(self.value, str)
|
assert isinstance(self.value, str)
|
||||||
return 'substring("%s", matches = %s)' % (
|
return f'substring("{self.value}", matches = {matches})'
|
||||||
self.value,
|
|
||||||
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Regex(String):
|
class Regex(String):
|
||||||
@@ -290,7 +288,7 @@ class Regex(String):
|
|||||||
if value.endswith("/i"):
|
if value.endswith("/i"):
|
||||||
value = value[: -len("i")]
|
value = value[: -len("i")]
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"invalid regular expression: %s it should use Python syntax, try it at https://pythex.org" % value
|
f"invalid regular expression: {value} it should use Python syntax, try it at https://pythex.org"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def evaluate(self, ctx, short_circuit=True):
|
def evaluate(self, ctx, short_circuit=True):
|
||||||
@@ -336,7 +334,7 @@ class Regex(String):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
assert isinstance(self.value, str)
|
assert isinstance(self.value, str)
|
||||||
return "regex(string =~ %s)" % self.value
|
return f"regex(string =~ {self.value})"
|
||||||
|
|
||||||
|
|
||||||
class _MatchedRegex(Regex):
|
class _MatchedRegex(Regex):
|
||||||
@@ -361,11 +359,9 @@ class _MatchedRegex(Regex):
|
|||||||
self.matches = matches
|
self.matches = matches
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
matches = ", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys()))
|
||||||
assert isinstance(self.value, str)
|
assert isinstance(self.value, str)
|
||||||
return "regex(string =~ %s, matches = %s)" % (
|
return f"regex(string =~ {self.value}, matches = {matches})"
|
||||||
self.value,
|
|
||||||
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StringFactory:
|
class StringFactory:
|
||||||
|
|||||||
@@ -121,14 +121,14 @@ class ELF:
|
|||||||
elif ei_class == 2:
|
elif ei_class == 2:
|
||||||
self.bitness = 64
|
self.bitness = 64
|
||||||
else:
|
else:
|
||||||
raise CorruptElfFile("invalid ei_class: 0x%02x" % ei_class)
|
raise CorruptElfFile(f"invalid ei_class: 0x{ei_class:02x}")
|
||||||
|
|
||||||
if ei_data == 1:
|
if ei_data == 1:
|
||||||
self.endian = "<"
|
self.endian = "<"
|
||||||
elif ei_data == 2:
|
elif ei_data == 2:
|
||||||
self.endian = ">"
|
self.endian = ">"
|
||||||
else:
|
else:
|
||||||
raise CorruptElfFile("not an ELF file: invalid ei_data: 0x%02x" % ei_data)
|
raise CorruptElfFile(f"not an ELF file: invalid ei_data: 0x{ei_data:02x}")
|
||||||
|
|
||||||
if self.bitness == 32:
|
if self.bitness == 32:
|
||||||
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
|
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
|||||||
dll = dll.lower()
|
dll = dll.lower()
|
||||||
|
|
||||||
# kernel32.CreateFileA
|
# kernel32.CreateFileA
|
||||||
yield "%s.%s" % (dll, symbol)
|
yield f"{dll}.{symbol}"
|
||||||
|
|
||||||
if not is_ordinal(symbol):
|
if not is_ordinal(symbol):
|
||||||
# CreateFileA
|
# CreateFileA
|
||||||
@@ -63,7 +63,7 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
|||||||
|
|
||||||
if is_aw_function(symbol):
|
if is_aw_function(symbol):
|
||||||
# kernel32.CreateFile
|
# kernel32.CreateFile
|
||||||
yield "%s.%s" % (dll, symbol[:-1])
|
yield f"{dll}.{symbol[:-1]}"
|
||||||
|
|
||||||
if not is_ordinal(symbol):
|
if not is_ordinal(symbol):
|
||||||
# CreateFile
|
# CreateFile
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def get_printable_len(op: idaapi.op_t) -> int:
|
|||||||
elif op.dtype == idaapi.dt_qword:
|
elif op.dtype == idaapi.dt_qword:
|
||||||
chars = struct.pack("<Q", op_val)
|
chars = struct.pack("<Q", op_val)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
|
raise ValueError(f"Unhandled operand data type 0x{op.dtype:x}.")
|
||||||
|
|
||||||
def is_printable_ascii(chars_: bytes):
|
def is_printable_ascii(chars_: bytes):
|
||||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
|||||||
end: max virtual address
|
end: max virtual address
|
||||||
seq: bytes to search e.g. b"\x01\x03"
|
seq: bytes to search e.g. b"\x01\x03"
|
||||||
"""
|
"""
|
||||||
seqstr = " ".join(["%02x" % b for b in seq])
|
seqstr = " ".join([f"{b:02x}" for b in seq])
|
||||||
while True:
|
while True:
|
||||||
# TODO find_binary: Deprecated. Please use ida_bytes.bin_search() instead.
|
# TODO find_binary: Deprecated. Please use ida_bytes.bin_search() instead.
|
||||||
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
|
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def extract_file_import_names(pe, **kwargs):
|
|||||||
|
|
||||||
for imp in dll.imports:
|
for imp in dll.imports:
|
||||||
if imp.import_by_ordinal:
|
if imp.import_by_ordinal:
|
||||||
impname = "#%s" % imp.ordinal
|
impname = f"#{imp.ordinal}"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
impname = imp.name.partition(b"\x00")[0].decode("ascii")
|
impname = imp.name.partition(b"\x00")[0].decode("ascii")
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def get_printable_len(oper: envi.archs.i386.disasm.i386ImmOper) -> int:
|
|||||||
elif oper.tsize == 8:
|
elif oper.tsize == 8:
|
||||||
chars = struct.pack("<Q", oper.imm)
|
chars = struct.pack("<Q", oper.imm)
|
||||||
else:
|
else:
|
||||||
raise ValueError("unexpected oper.tsize: %d" % (oper.tsize))
|
raise ValueError(f"unexpected oper.tsize: {oper.tsize}")
|
||||||
|
|
||||||
if is_printable_ascii(chars):
|
if is_printable_ascii(chars):
|
||||||
return oper.tsize
|
return oper.tsize
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]
|
|||||||
modname, impname = tinfo.split(".", 1)
|
modname, impname = tinfo.split(".", 1)
|
||||||
if is_viv_ord_impname(impname):
|
if is_viv_ord_impname(impname):
|
||||||
# replace ord prefix with #
|
# replace ord prefix with #
|
||||||
impname = "#%s" % impname[len("ord") :]
|
impname = "#" + impname[len("ord") :]
|
||||||
|
|
||||||
addr = AbsoluteVirtualAddress(va)
|
addr = AbsoluteVirtualAddress(va)
|
||||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ def loads(s: str) -> capa.features.extractors.base_extractor.FeatureExtractor:
|
|||||||
|
|
||||||
freeze = Freeze.parse_raw(s)
|
freeze = Freeze.parse_raw(s)
|
||||||
if freeze.version != 2:
|
if freeze.version != 2:
|
||||||
raise ValueError("unsupported freeze format version: %d", freeze.version)
|
raise ValueError(f"unsupported freeze format version: {freeze.version}")
|
||||||
|
|
||||||
return null.NullFeatureExtractor(
|
return null.NullFeatureExtractor(
|
||||||
base_address=freeze.base_address.to_capa(),
|
base_address=freeze.base_address.to_capa(),
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from capa.features.common import VALID_FEATURE_ACCESS, Feature
|
|||||||
def hex(n: int) -> str:
|
def hex(n: int) -> str:
|
||||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||||
if n < 0:
|
if n < 0:
|
||||||
return "-0x%X" % (-n)
|
return f"-0x{(-n):X}"
|
||||||
else:
|
else:
|
||||||
return "0x%X" % n
|
return f"0x{(n):X}"
|
||||||
|
|
||||||
|
|
||||||
class API(Feature):
|
class API(Feature):
|
||||||
@@ -105,7 +105,7 @@ class _Operand(Feature, abc.ABC):
|
|||||||
|
|
||||||
class OperandNumber(_Operand):
|
class OperandNumber(_Operand):
|
||||||
# cached names so we don't do extra string formatting every ctor
|
# cached names so we don't do extra string formatting every ctor
|
||||||
NAMES = ["operand[%d].number" % i for i in range(MAX_OPERAND_COUNT)]
|
NAMES = [f"operand[{i}].number" for i in range(MAX_OPERAND_COUNT)]
|
||||||
|
|
||||||
# operand[i].number: 0x12
|
# operand[i].number: 0x12
|
||||||
def __init__(self, index: int, value: int, description=None):
|
def __init__(self, index: int, value: int, description=None):
|
||||||
@@ -119,7 +119,7 @@ class OperandNumber(_Operand):
|
|||||||
|
|
||||||
class OperandOffset(_Operand):
|
class OperandOffset(_Operand):
|
||||||
# cached names so we don't do extra string formatting every ctor
|
# cached names so we don't do extra string formatting every ctor
|
||||||
NAMES = ["operand[%d].offset" % i for i in range(MAX_OPERAND_COUNT)]
|
NAMES = [f"operand[{i}].offset" for i in range(MAX_OPERAND_COUNT)]
|
||||||
|
|
||||||
# operand[i].offset: 0x12
|
# operand[i].offset: 0x12
|
||||||
def __init__(self, index: int, value: int, description=None):
|
def __init__(self, index: int, value: int, description=None):
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ logger = logging.getLogger("capa")
|
|||||||
def hex(n: int) -> str:
|
def hex(n: int) -> str:
|
||||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||||
if n < 0:
|
if n < 0:
|
||||||
return "-0x%X" % (-n)
|
return f"-0x{(-n):X}"
|
||||||
else:
|
else:
|
||||||
return "0x%X" % n
|
return f"0x{(n):X}"
|
||||||
|
|
||||||
|
|
||||||
def get_file_taste(sample_path: str) -> bytes:
|
def get_file_taste(sample_path: str) -> bytes:
|
||||||
if not os.path.exists(sample_path):
|
if not os.path.exists(sample_path):
|
||||||
raise IOError("sample path %s does not exist or cannot be accessed" % sample_path)
|
raise IOError(f"sample path {sample_path} does not exist or cannot be accessed")
|
||||||
with open(sample_path, "rb") as f:
|
with open(sample_path, "rb") as f:
|
||||||
taste = f.read(8)
|
taste = f.read(8)
|
||||||
return taste
|
return taste
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ NETNODE_RULES_CACHE_ID = "rules-cache-id"
|
|||||||
|
|
||||||
|
|
||||||
def inform_user_ida_ui(message):
|
def inform_user_ida_ui(message):
|
||||||
idaapi.info("%s. Please refer to IDA Output window for more information." % message)
|
idaapi.info(f"{message}. Please refer to IDA Output window for more information.")
|
||||||
|
|
||||||
|
|
||||||
def is_supported_ida_version():
|
def is_supported_ida_version():
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class CapaExplorerProgressIndicator(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
if ida_kernwin.user_cancelled():
|
if ida_kernwin.user_cancelled():
|
||||||
raise UserCancelledError("user cancelled")
|
raise UserCancelledError("user cancelled")
|
||||||
self.progress.emit("extracting features from %s" % text)
|
self.progress.emit(f"extracting features from {text}")
|
||||||
|
|
||||||
|
|
||||||
class CapaExplorerFeatureExtractor(IdaFeatureExtractor):
|
class CapaExplorerFeatureExtractor(IdaFeatureExtractor):
|
||||||
@@ -40,5 +40,5 @@ class CapaExplorerFeatureExtractor(IdaFeatureExtractor):
|
|||||||
self.indicator = CapaExplorerProgressIndicator()
|
self.indicator = CapaExplorerProgressIndicator()
|
||||||
|
|
||||||
def extract_function_features(self, fh: FunctionHandle):
|
def extract_function_features(self, fh: FunctionHandle):
|
||||||
self.indicator.update("function at 0x%X" % fh.inner.start_ea)
|
self.indicator.update(f"function at {hex(fh.inner.start_ea)}")
|
||||||
return super().extract_function_features(fh)
|
return super().extract_function_features(fh)
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ def trim_function_name(f, max_length=25):
|
|||||||
""" """
|
""" """
|
||||||
n = idaapi.get_name(f.start_ea)
|
n = idaapi.get_name(f.start_ea)
|
||||||
if len(n) > max_length:
|
if len(n) > max_length:
|
||||||
n = "%s..." % n[:max_length]
|
n = f"{n[:max_length]}..."
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
def update_wait_box(text):
|
def update_wait_box(text):
|
||||||
"""update the IDA wait box"""
|
"""update the IDA wait box"""
|
||||||
ida_kernwin.replace_wait_box("capa explorer...%s" % text)
|
ida_kernwin.replace_wait_box(f"capa explorer...{text}")
|
||||||
|
|
||||||
|
|
||||||
class QLineEditClicked(QtWidgets.QLineEdit):
|
class QLineEditClicked(QtWidgets.QLineEdit):
|
||||||
@@ -630,7 +630,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
def on_load_rule(_, i, total):
|
def on_load_rule(_, i, total):
|
||||||
update_wait_box("loading capa rules from %s (%d of %d)" % (rule_path, i + 1, total))
|
update_wait_box(f"loading capa rules from {rule_path} ({i+1} of {total})")
|
||||||
if ida_kernwin.user_cancelled():
|
if ida_kernwin.user_cancelled():
|
||||||
raise UserCancelledError("user cancelled")
|
raise UserCancelledError("user cancelled")
|
||||||
|
|
||||||
@@ -640,7 +640,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capa.ida.helpers.inform_user_ida_ui(
|
capa.ida.helpers.inform_user_ida_ui(
|
||||||
"Failed to load capa rules from %s" % settings.user[CAPA_SETTINGS_RULE_PATH]
|
f"Failed to load capa rules from {settings.user[CAPA_SETTINGS_RULE_PATH]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error("Failed to load capa rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
|
logger.error("Failed to load capa rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e)
|
||||||
@@ -691,10 +691,9 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
update_wait_box("verifying cached results")
|
update_wait_box("verifying cached results")
|
||||||
|
|
||||||
view_status_rules: str = "%s (%d rules)" % (
|
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
|
||||||
settings.user[CAPA_SETTINGS_RULE_PATH],
|
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||||
self.program_analysis_ruleset_cache.source_rule_count,
|
view_status_rules: str = f"{user_settings} ({count_source_rules} rules)"
|
||||||
)
|
|
||||||
|
|
||||||
# warn user about potentially outdated rules, depending on the use-case this may be expected
|
# warn user about potentially outdated rules, depending on the use-case this may be expected
|
||||||
if (
|
if (
|
||||||
@@ -710,10 +709,8 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
)
|
)
|
||||||
view_status_rules = "no rules matched for cache"
|
view_status_rules = "no rules matched for cache"
|
||||||
|
|
||||||
new_view_status = "capa rules: %s, cached results (created %s)" % (
|
cached_results_time = self.resdoc_cache.meta.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
view_status_rules,
|
new_view_status = f"capa rules: {view_status_rules}, cached results (created {cached_results_time})"
|
||||||
self.resdoc_cache.meta.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to load cached capa results (error: %s).", e, exc_info=True)
|
logger.error("Failed to load cached capa results (error: %s).", e, exc_info=True)
|
||||||
return False
|
return False
|
||||||
@@ -725,7 +722,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
|
|
||||||
def slot_progress_feature_extraction(text):
|
def slot_progress_feature_extraction(text):
|
||||||
"""slot function to handle feature extraction progress updates"""
|
"""slot function to handle feature extraction progress updates"""
|
||||||
update_wait_box("%s (%d of %d)" % (text, self.process_count, self.process_total))
|
update_wait_box(f"{text} ({self.process_count} of {self.process_total})")
|
||||||
self.process_count += 1
|
self.process_count += 1
|
||||||
|
|
||||||
update_wait_box("initializing feature extractor")
|
update_wait_box("initializing feature extractor")
|
||||||
@@ -843,12 +840,9 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to save results to database (error: %s)", e, exc_info=True)
|
logger.error("Failed to save results to database (error: %s)", e, exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH]
|
||||||
new_view_status = "capa rules: %s (%d rules)" % (
|
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
|
||||||
settings.user[CAPA_SETTINGS_RULE_PATH],
|
new_view_status = f"capa rules: {user_settings} ({count_source_rules} rules)"
|
||||||
self.program_analysis_ruleset_cache.source_rule_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
# regardless of new analysis, render results - e.g. we may only want to render results after checking
|
# regardless of new analysis, render results - e.g. we may only want to render results after checking
|
||||||
# show results by function
|
# show results by function
|
||||||
|
|
||||||
@@ -1094,7 +1088,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
|||||||
self.view_rulegen_features.load_features(all_file_features, all_function_features)
|
self.view_rulegen_features.load_features(all_file_features, all_function_features)
|
||||||
|
|
||||||
self.set_view_status_label(
|
self.set_view_status_label(
|
||||||
"capa rules: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], ruleset.source_rule_count)
|
f"capa rules: {settings.user[CAPA_SETTINGS_RULE_PATH]} ({settings.user[CAPA_SETTINGS_RULE_PATH]} rules)"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to render views (error: %s)", e, exc_info=True)
|
logger.error("Failed to render views (error: %s)", e, exc_info=True)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def info_to_name(display):
|
|||||||
|
|
||||||
def ea_to_hex(ea):
|
def ea_to_hex(ea):
|
||||||
"""convert effective address (ea) to hex for display"""
|
"""convert effective address (ea) to hex for display"""
|
||||||
return "%08X" % ea
|
return f"{hex(ea)}"
|
||||||
|
|
||||||
|
|
||||||
class CapaExplorerDataItem:
|
class CapaExplorerDataItem:
|
||||||
|
|||||||
@@ -369,34 +369,34 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
if statement.type != rd.CompoundStatementType.NOT:
|
if statement.type != rd.CompoundStatementType.NOT:
|
||||||
display = statement.type
|
display = statement.type
|
||||||
if statement.description:
|
if statement.description:
|
||||||
display += " (%s)" % statement.description
|
display += f" ({statement.description})"
|
||||||
return CapaExplorerDefaultItem(parent, display)
|
return CapaExplorerDefaultItem(parent, display)
|
||||||
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
|
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
|
||||||
# TODO: do we display 'not'
|
# TODO: do we display 'not'
|
||||||
pass
|
pass
|
||||||
elif isinstance(statement, rd.SomeStatement):
|
elif isinstance(statement, rd.SomeStatement):
|
||||||
display = "%d or more" % statement.count
|
display = f"{statement.count} or more"
|
||||||
if statement.description:
|
if statement.description:
|
||||||
display += " (%s)" % statement.description
|
display += f" ({statement.description})"
|
||||||
return CapaExplorerDefaultItem(parent, display)
|
return CapaExplorerDefaultItem(parent, display)
|
||||||
elif isinstance(statement, rd.RangeStatement):
|
elif isinstance(statement, rd.RangeStatement):
|
||||||
# `range` is a weird node, its almost a hybrid of statement + feature.
|
# `range` is a weird node, its almost a hybrid of statement + feature.
|
||||||
# it is a specific feature repeated multiple times.
|
# it is a specific feature repeated multiple times.
|
||||||
# there's no additional logic in the feature part, just the existence of a feature.
|
# there's no additional logic in the feature part, just the existence of a feature.
|
||||||
# so, we have to inline some of the feature rendering here.
|
# so, we have to inline some of the feature rendering here.
|
||||||
display = "count(%s): " % self.capa_doc_feature_to_display(statement.child)
|
display = f"count({self.capa_doc_feature_to_display(statement.child)}): "
|
||||||
|
|
||||||
if statement.max == statement.min:
|
if statement.max == statement.min:
|
||||||
display += "%d" % (statement.min)
|
display += f"{statement.min}"
|
||||||
elif statement.min == 0:
|
elif statement.min == 0:
|
||||||
display += "%d or fewer" % (statement.max)
|
display += f"{statement.max} or fewer"
|
||||||
elif statement.max == (1 << 64 - 1):
|
elif statement.max == (1 << 64 - 1):
|
||||||
display += "%d or more" % (statement.min)
|
display += f"{statement.min} or more"
|
||||||
else:
|
else:
|
||||||
display += "between %d and %d" % (statement.min, statement.max)
|
display += f"between {statement.min} and {statement.max}"
|
||||||
|
|
||||||
if statement.description:
|
if statement.description:
|
||||||
display += " (%s)" % statement.description
|
display += f" ({statement.description})"
|
||||||
|
|
||||||
parent2 = CapaExplorerFeatureItem(parent, display=display)
|
parent2 = CapaExplorerFeatureItem(parent, display=display)
|
||||||
|
|
||||||
@@ -408,7 +408,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
elif isinstance(statement, rd.SubscopeStatement):
|
elif isinstance(statement, rd.SubscopeStatement):
|
||||||
display = str(statement.scope)
|
display = str(statement.scope)
|
||||||
if statement.description:
|
if statement.description:
|
||||||
display += " (%s)" % statement.description
|
display += f" ({statement.description})"
|
||||||
return CapaExplorerSubscopeItem(parent, display)
|
return CapaExplorerSubscopeItem(parent, display)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unexpected match statement type: " + str(statement))
|
raise RuntimeError("unexpected match statement type: " + str(statement))
|
||||||
@@ -537,7 +537,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
|
|
||||||
if value:
|
if value:
|
||||||
if isinstance(feature, frzf.StringFeature):
|
if isinstance(feature, frzf.StringFeature):
|
||||||
value = '"%s"' % capa.features.common.escape_string(value)
|
value = f'"{capa.features.common.escape_string(value)}"'
|
||||||
|
|
||||||
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
|
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
|
||||||
key = f"property/{feature.access}"
|
key = f"property/{feature.access}"
|
||||||
@@ -547,11 +547,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
key = f"operand[{feature.index}].offset"
|
key = f"operand[{feature.index}].offset"
|
||||||
|
|
||||||
if feature.description:
|
if feature.description:
|
||||||
return "%s(%s = %s)" % (key, value, feature.description)
|
return f"{key}({value} = {feature.description})"
|
||||||
else:
|
else:
|
||||||
return "%s(%s)" % (key, value)
|
return f"{key}({value})"
|
||||||
else:
|
else:
|
||||||
return "%s" % key
|
return f"{key}"
|
||||||
|
|
||||||
def render_capa_doc_feature_node(
|
def render_capa_doc_feature_node(
|
||||||
self,
|
self,
|
||||||
@@ -669,7 +669,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
|||||||
elif isinstance(feature, frzf.StringFeature):
|
elif isinstance(feature, frzf.StringFeature):
|
||||||
# display string preview
|
# display string preview
|
||||||
return CapaExplorerStringViewItem(
|
return CapaExplorerStringViewItem(
|
||||||
parent, display, location, '"%s"' % capa.features.common.escape_string(feature.string)
|
parent, display, location, f'"{capa.features.common.escape_string(feature.string)}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def parse_yaml_line(feature):
|
|||||||
if m:
|
if m:
|
||||||
# reconstruct count without description
|
# reconstruct count without description
|
||||||
feature, value, description, count = m.groups()
|
feature, value, description, count = m.groups()
|
||||||
feature = "- count(%s(%s)): %s" % (feature, value, count)
|
feature = f"- count({feature}({value})): {count}"
|
||||||
elif not feature.startswith("#"):
|
elif not feature.startswith("#"):
|
||||||
feature, _, comment = feature.partition("#")
|
feature, _, comment = feature.partition("#")
|
||||||
feature, _, description = feature.partition("=")
|
feature, _, description = feature.partition("=")
|
||||||
@@ -72,18 +72,18 @@ def parse_node_for_feature(feature, description, comment, depth):
|
|||||||
display = ""
|
display = ""
|
||||||
|
|
||||||
if feature.startswith("#"):
|
if feature.startswith("#"):
|
||||||
display += "%s%s\n" % (" " * depth, feature)
|
display += f"{' '*depth}{feature}\n"
|
||||||
elif description:
|
elif description:
|
||||||
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not", "- instruction:")):
|
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not", "- instruction:")):
|
||||||
display += "%s%s" % (" " * depth, feature)
|
display += f"{' '*depth}{feature}\n"
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s" % comment
|
display += f" # {comment}"
|
||||||
display += "\n%s- description: %s\n" % (" " * (depth + 2), description)
|
display += f"\n{' '*(depth+2)}- description: {description}\n"
|
||||||
elif feature.startswith("- string"):
|
elif feature.startswith("- string"):
|
||||||
display += "%s%s" % (" " * depth, feature)
|
display += f"{' '*depth}{feature}\n"
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s" % comment
|
display += f" # {comment}"
|
||||||
display += "\n%sdescription: %s\n" % (" " * (depth + 2), description)
|
display += f"\n{' '*(depth+2)}description: {description}\n"
|
||||||
elif feature.startswith("- count"):
|
elif feature.startswith("- count"):
|
||||||
# count is weird, we need to format description based on feature type, so we parse with regex
|
# count is weird, we need to format description based on feature type, so we parse with regex
|
||||||
# assume format - count(<feature_name>(<feature_value>)): <count>
|
# assume format - count(<feature_name>(<feature_value>)): <count>
|
||||||
@@ -91,28 +91,22 @@ def parse_node_for_feature(feature, description, comment, depth):
|
|||||||
if m:
|
if m:
|
||||||
name, value, count = m.groups()
|
name, value, count = m.groups()
|
||||||
if name in ("string",):
|
if name in ("string",):
|
||||||
display += "%s%s" % (" " * depth, feature)
|
display += f"{' '*depth}{feature}"
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s" % comment
|
display += " # %s" % comment
|
||||||
display += "\n%sdescription: %s\n" % (" " * (depth + 2), description)
|
display += f"\n{' '*(depth+2)}description: {description}\n"
|
||||||
else:
|
else:
|
||||||
display += "%s- count(%s(%s = %s)): %s" % (
|
display += f"{' '*depth}- count({name}({value} = {description})): {count}"
|
||||||
" " * depth,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
description,
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s\n" % comment
|
display += f" # {comment}\n"
|
||||||
else:
|
else:
|
||||||
display += "%s%s = %s" % (" " * depth, feature, description)
|
display += f"{' '*depth}{feature} = {description}"
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s\n" % comment
|
display += f" # {comment}\n"
|
||||||
else:
|
else:
|
||||||
display += "%s%s" % (" " * depth, feature)
|
display += f"{' '*depth}{feature}"
|
||||||
if comment:
|
if comment:
|
||||||
display += " # %s\n" % comment
|
display += f" # {comment}\n"
|
||||||
|
|
||||||
return display if display.endswith("\n") else display + "\n"
|
return display if display.endswith("\n") else display + "\n"
|
||||||
|
|
||||||
@@ -198,14 +192,14 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
|||||||
" name: <insert_name>",
|
" name: <insert_name>",
|
||||||
" namespace: <insert_namespace>",
|
" namespace: <insert_namespace>",
|
||||||
" authors:",
|
" authors:",
|
||||||
" - %s" % author,
|
f" - {author}",
|
||||||
" scope: %s" % scope,
|
f" scope: {scope}",
|
||||||
" references:",
|
" references:",
|
||||||
" - <insert_references>",
|
" - <insert_references>",
|
||||||
" examples:",
|
" examples:",
|
||||||
" - %s:0x%X" % (capa.ida.helpers.get_file_md5().upper(), ea)
|
f" - {capa.ida.helpers.get_file_md5().upper()}:{hex(ea)}"
|
||||||
if ea
|
if ea
|
||||||
else " - %s" % (capa.ida.helpers.get_file_md5().upper()),
|
else f" - {capa.ida.helpers.get_file_md5().upper()}",
|
||||||
" features:",
|
" features:",
|
||||||
]
|
]
|
||||||
self.setText("\n".join(metadata_default))
|
self.setText("\n".join(metadata_default))
|
||||||
@@ -539,7 +533,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
|||||||
|
|
||||||
# build submenu with modify actions
|
# build submenu with modify actions
|
||||||
sub_menu = build_context_menu(self.parent(), sub_actions)
|
sub_menu = build_context_menu(self.parent(), sub_actions)
|
||||||
sub_menu.setTitle("Nest feature%s" % ("" if len(tuple(self.get_features(selected=True))) == 1 else "s"))
|
sub_menu.setTitle(f"Nest feature{'' if len(tuple(self.get_features(selected=True))) == 1 else 's'}")
|
||||||
|
|
||||||
# build main menu with submenu + main actions
|
# build main menu with submenu + main actions
|
||||||
menu = build_context_menu(self.parent(), (sub_menu,) + actions)
|
menu = build_context_menu(self.parent(), (sub_menu,) + actions)
|
||||||
@@ -654,21 +648,21 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
|||||||
# single features
|
# single features
|
||||||
for k, v in filter(lambda t: t[1] == 1, counted):
|
for k, v in filter(lambda t: t[1] == 1, counted):
|
||||||
if isinstance(k, (capa.features.common.String,)):
|
if isinstance(k, (capa.features.common.String,)):
|
||||||
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
|
value = f'"{capa.features.common.escape_string(k.get_value_str())}"'
|
||||||
else:
|
else:
|
||||||
value = k.get_value_str()
|
value = k.get_value_str()
|
||||||
self.new_feature_node(top_node, ("- %s: %s" % (k.name.lower(), value), ""))
|
self.new_feature_node(top_node, (f"- {k.name.lower()}: {value}", ""))
|
||||||
|
|
||||||
# n > 1 features
|
# n > 1 features
|
||||||
for k, v in filter(lambda t: t[1] > 1, counted):
|
for k, v in filter(lambda t: t[1] > 1, counted):
|
||||||
if k.value:
|
if k.value:
|
||||||
if isinstance(k, (capa.features.common.String,)):
|
if isinstance(k, (capa.features.common.String,)):
|
||||||
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
|
value = f'"{capa.features.common.escape_string(k.get_value_str())}"'
|
||||||
else:
|
else:
|
||||||
value = k.get_value_str()
|
value = k.get_value_str()
|
||||||
display = "- count(%s(%s)): %d" % (k.name.lower(), value, v)
|
display = f"- count({k.name.lower()}({value})): {v}"
|
||||||
else:
|
else:
|
||||||
display = "- count(%s): %d" % (k.name.lower(), v)
|
display = f"- count({k.name.lower()}): {v}"
|
||||||
self.new_feature_node(top_node, (display, ""))
|
self.new_feature_node(top_node, (display, ""))
|
||||||
|
|
||||||
self.update_preview()
|
self.update_preview()
|
||||||
@@ -880,7 +874,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
if isinstance(self.selectedItems()[0].data(0, 0x100), capa.features.common.Bytes):
|
if isinstance(self.selectedItems()[0].data(0, 0x100), capa.features.common.Bytes):
|
||||||
actions.append(("Add n bytes...", (), self.slot_add_n_bytes_feature))
|
actions.append(("Add n bytes...", (), self.slot_add_n_bytes_feature))
|
||||||
else:
|
else:
|
||||||
action_add_features_fmt = "Add %d features" % selected_items_count
|
action_add_features_fmt = f"Add {selected_items_count} features"
|
||||||
|
|
||||||
actions.append((action_add_features_fmt, (), self.slot_add_selected_features))
|
actions.append((action_add_features_fmt, (), self.slot_add_selected_features))
|
||||||
|
|
||||||
@@ -1029,7 +1023,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
|
|
||||||
def format_address(e):
|
def format_address(e):
|
||||||
if isinstance(e, AbsoluteVirtualAddress):
|
if isinstance(e, AbsoluteVirtualAddress):
|
||||||
return "%X" % int(e)
|
return f"{hex(int(e))}"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -1038,8 +1032,8 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
|||||||
name = feature.name.lower()
|
name = feature.name.lower()
|
||||||
value = feature.get_value_str()
|
value = feature.get_value_str()
|
||||||
if isinstance(feature, (capa.features.common.String,)):
|
if isinstance(feature, (capa.features.common.String,)):
|
||||||
value = '"%s"' % capa.features.common.escape_string(value)
|
value = f'"{capa.features.common.escape_string(value)}"'
|
||||||
return "%s(%s)" % (name, value)
|
return f"{name}({value})"
|
||||||
|
|
||||||
for feature, addrs in sorted(features.items(), key=lambda k: sorted(k[1])):
|
for feature, addrs in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||||
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
||||||
|
|||||||
16
capa/main.py
16
capa/main.py
@@ -261,9 +261,9 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
|
|||||||
logger.debug("skipping library function 0x%x (%s)", f.address, function_name)
|
logger.debug("skipping library function 0x%x (%s)", f.address, function_name)
|
||||||
meta["library_functions"][f.address] = function_name
|
meta["library_functions"][f.address] = function_name
|
||||||
n_libs = len(meta["library_functions"])
|
n_libs = len(meta["library_functions"])
|
||||||
percentage = 100 * (n_libs / n_funcs)
|
percentage = round(100 * (n_libs / n_funcs))
|
||||||
if isinstance(pb, tqdm.tqdm):
|
if isinstance(pb, tqdm.tqdm):
|
||||||
pb.set_postfix_str("skipped %d library functions (%d%%)" % (n_libs, percentage))
|
pb.set_postfix_str(f"skipped {n_libs} library functions ({percentage}%)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f)
|
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f)
|
||||||
@@ -397,8 +397,8 @@ def get_meta_str(vw):
|
|||||||
meta = []
|
meta = []
|
||||||
for k in ["Format", "Platform", "Architecture"]:
|
for k in ["Format", "Platform", "Architecture"]:
|
||||||
if k in vw.metadata:
|
if k in vw.metadata:
|
||||||
meta.append("%s: %s" % (k.lower(), vw.metadata[k]))
|
meta.append(f"{k.lower()}: {vw.metadata[k]}")
|
||||||
return "%s, number of functions: %d" % (", ".join(meta), len(vw.getFunctions()))
|
return f"{', '.join(meta)}, number of functions: {len(vw.getFunctions())}"
|
||||||
|
|
||||||
|
|
||||||
def is_running_standalone() -> bool:
|
def is_running_standalone() -> bool:
|
||||||
@@ -569,7 +569,7 @@ def collect_rule_file_paths(rule_paths: List[str]) -> List[str]:
|
|||||||
rule_file_paths = []
|
rule_file_paths = []
|
||||||
for rule_path in rule_paths:
|
for rule_path in rule_paths:
|
||||||
if not os.path.exists(rule_path):
|
if not os.path.exists(rule_path):
|
||||||
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
|
raise IOError(f"rule path {rule_path} does not exist or cannot be accessed")
|
||||||
|
|
||||||
if os.path.isfile(rule_path):
|
if os.path.isfile(rule_path):
|
||||||
rule_file_paths.append(rule_path)
|
rule_file_paths.append(rule_path)
|
||||||
@@ -660,7 +660,7 @@ def get_rules(
|
|||||||
|
|
||||||
def get_signatures(sigs_path):
|
def get_signatures(sigs_path):
|
||||||
if not os.path.exists(sigs_path):
|
if not os.path.exists(sigs_path):
|
||||||
raise IOError("signatures path %s does not exist or cannot be accessed" % sigs_path)
|
raise IOError(f"signatures path {sigs_path} does not exist or cannot be accessed")
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
if os.path.isfile(sigs_path):
|
if os.path.isfile(sigs_path):
|
||||||
@@ -844,13 +844,13 @@ def install_common_args(parser, wanted=None):
|
|||||||
(FORMAT_SC64, "64-bit shellcode"),
|
(FORMAT_SC64, "64-bit shellcode"),
|
||||||
(FORMAT_FREEZE, "features previously frozen by capa"),
|
(FORMAT_FREEZE, "features previously frozen by capa"),
|
||||||
]
|
]
|
||||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
format_help = ", ".join([f"{f[0]}: {f[1]}" for f in formats])
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-f",
|
"-f",
|
||||||
"--format",
|
"--format",
|
||||||
choices=[f[0] for f in formats],
|
choices=[f[0] for f in formats],
|
||||||
default=FORMAT_AUTO,
|
default=FORMAT_AUTO,
|
||||||
help="select sample format, %s" % format_help,
|
help=f"select sample format, {format_help}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if "backend" in wanted:
|
if "backend" in wanted:
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
|
|||||||
if count == 1:
|
if count == 1:
|
||||||
capability = rutils.bold(rule.meta.name)
|
capability = rutils.bold(rule.meta.name)
|
||||||
else:
|
else:
|
||||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
capability = f"{rutils.bold(rule.meta.name)} ({count} matches)"
|
||||||
rows.append((capability, rule.meta.namespace))
|
rows.append((capability, rule.meta.namespace))
|
||||||
|
|
||||||
if rows:
|
if rows:
|
||||||
@@ -135,9 +135,9 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
|
|||||||
inner_rows = []
|
inner_rows = []
|
||||||
for technique, subtechnique, id in sorted(techniques):
|
for technique, subtechnique, id in sorted(techniques):
|
||||||
if not subtechnique:
|
if not subtechnique:
|
||||||
inner_rows.append("%s %s" % (rutils.bold(technique), id))
|
inner_rows.append(f"{rutils.bold(technique)} {id}")
|
||||||
else:
|
else:
|
||||||
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
|
inner_rows.append(f"{rutils.bold(technique)}::{subtechnique} {id}")
|
||||||
rows.append(
|
rows.append(
|
||||||
(
|
(
|
||||||
rutils.bold(tactic.upper()),
|
rutils.bold(tactic.upper()),
|
||||||
@@ -178,9 +178,9 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
|
|||||||
inner_rows = []
|
inner_rows = []
|
||||||
for behavior, method, id in sorted(behaviors):
|
for behavior, method, id in sorted(behaviors):
|
||||||
if not method:
|
if not method:
|
||||||
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
|
inner_rows.append(f"{rutils.bold(behavior)} [{id}]")
|
||||||
else:
|
else:
|
||||||
inner_rows.append("%s::%s [%s]" % (rutils.bold(behavior), method, id))
|
inner_rows.append(f"{rutils.bold(behavior)}::{method} [{id}]")
|
||||||
rows.append(
|
rows.append(
|
||||||
(
|
(
|
||||||
rutils.bold(objective.upper()),
|
rutils.bold(objective.upper()),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
|
|||||||
"""
|
"""
|
||||||
format canonical representation of ATT&CK/MBC parts and ID
|
format canonical representation of ATT&CK/MBC parts and ID
|
||||||
"""
|
"""
|
||||||
return "%s [%s]" % ("::".join(data.parts), data.id)
|
return f"{'::'.join(data.parts)} [{data.id}]"
|
||||||
|
|
||||||
|
|
||||||
def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
|
def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
|||||||
if count == 1:
|
if count == 1:
|
||||||
capability = rutils.bold(rule.meta.name)
|
capability = rutils.bold(rule.meta.name)
|
||||||
else:
|
else:
|
||||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
capability = f"{rutils.bold(rule.meta.name)} ({count} matches)"
|
||||||
|
|
||||||
ostream.writeln(capability)
|
ostream.writeln(capability)
|
||||||
had_match = True
|
had_match = True
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def render_locations(ostream, locations: Iterable[frz.Address]):
|
|||||||
# don't display too many locations, because it becomes very noisy.
|
# don't display too many locations, because it becomes very noisy.
|
||||||
# probably only the first handful of locations will be useful for inspection.
|
# probably only the first handful of locations will be useful for inspection.
|
||||||
ostream.write(", ".join(map(v.format_address, locations[0:4])))
|
ostream.write(", ".join(map(v.format_address, locations[0:4])))
|
||||||
ostream.write(", and %d more..." % (len(locations) - 4))
|
ostream.write(f", and {(len(locations) - 4)} more...")
|
||||||
|
|
||||||
elif len(locations) > 1:
|
elif len(locations) > 1:
|
||||||
ostream.write(", ".join(map(v.format_address, locations)))
|
ostream.write(", ".join(map(v.format_address, locations)))
|
||||||
@@ -62,7 +62,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
|||||||
|
|
||||||
ostream.write(":")
|
ostream.write(":")
|
||||||
if statement.description:
|
if statement.description:
|
||||||
ostream.write(" = %s" % statement.description)
|
ostream.write(f" = {statement.description}")
|
||||||
ostream.writeln("")
|
ostream.writeln("")
|
||||||
|
|
||||||
elif isinstance(statement, (rd.CompoundStatement)):
|
elif isinstance(statement, (rd.CompoundStatement)):
|
||||||
@@ -71,14 +71,14 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
|||||||
|
|
||||||
ostream.write(":")
|
ostream.write(":")
|
||||||
if statement.description:
|
if statement.description:
|
||||||
ostream.write(" = %s" % statement.description)
|
ostream.write(f" = {statement.description}")
|
||||||
ostream.writeln("")
|
ostream.writeln("")
|
||||||
|
|
||||||
elif isinstance(statement, rd.SomeStatement):
|
elif isinstance(statement, rd.SomeStatement):
|
||||||
ostream.write("%d or more:" % (statement.count))
|
ostream.write(f"{statement.count} or more:")
|
||||||
|
|
||||||
if statement.description:
|
if statement.description:
|
||||||
ostream.write(" = %s" % statement.description)
|
ostream.write(f" = {statement.description}")
|
||||||
ostream.writeln("")
|
ostream.writeln("")
|
||||||
|
|
||||||
elif isinstance(statement, rd.RangeStatement):
|
elif isinstance(statement, rd.RangeStatement):
|
||||||
@@ -92,28 +92,28 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
|||||||
|
|
||||||
if value:
|
if value:
|
||||||
if isinstance(child, frzf.StringFeature):
|
if isinstance(child, frzf.StringFeature):
|
||||||
value = '"%s"' % capa.features.common.escape_string(value)
|
value = f'"{capa.features.common.escape_string(value)}"'
|
||||||
|
|
||||||
value = rutils.bold2(value)
|
value = rutils.bold2(value)
|
||||||
|
|
||||||
if child.description:
|
if child.description:
|
||||||
ostream.write("count(%s(%s = %s)): " % (child.type, value, child.description))
|
ostream.write(f"count({child.type}({value} = {child.description})): ")
|
||||||
else:
|
else:
|
||||||
ostream.write("count(%s(%s)): " % (child.type, value))
|
ostream.write(f"count({child.type}({value})): ")
|
||||||
else:
|
else:
|
||||||
ostream.write("count(%s): " % child.type)
|
ostream.write(f"count({child.type}): ")
|
||||||
|
|
||||||
if statement.max == statement.min:
|
if statement.max == statement.min:
|
||||||
ostream.write("%d" % (statement.min))
|
ostream.write(f"{statement.min}")
|
||||||
elif statement.min == 0:
|
elif statement.min == 0:
|
||||||
ostream.write("%d or fewer" % (statement.max))
|
ostream.write(f"{statement.max} or fewer")
|
||||||
elif statement.max == (1 << 64 - 1):
|
elif statement.max == (1 << 64 - 1):
|
||||||
ostream.write("%d or more" % (statement.min))
|
ostream.write(f"{statement.min} or more")
|
||||||
else:
|
else:
|
||||||
ostream.write("between %d and %d" % (statement.min, statement.max))
|
ostream.write(f"between {statement.min} and {statement.max}")
|
||||||
|
|
||||||
if statement.description:
|
if statement.description:
|
||||||
ostream.write(" = %s" % statement.description)
|
ostream.write(f" = {statement.description}")
|
||||||
render_locations(ostream, match.locations)
|
render_locations(ostream, match.locations)
|
||||||
ostream.writeln("")
|
ostream.writeln("")
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
|||||||
|
|
||||||
|
|
||||||
def render_string_value(s: str) -> str:
|
def render_string_value(s: str) -> str:
|
||||||
return '"%s"' % capa.features.common.escape_string(s)
|
return f'"{capa.features.common.escape_string(s)}"'
|
||||||
|
|
||||||
|
|
||||||
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
||||||
@@ -143,7 +143,7 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
|||||||
value = feature.dict(by_alias=True).get(key, None)
|
value = feature.dict(by_alias=True).get(key, None)
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
raise ValueError("%s contains None" % key)
|
raise ValueError(f"{key} contains None")
|
||||||
|
|
||||||
if not isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
|
if not isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
|
||||||
# like:
|
# like:
|
||||||
@@ -290,11 +290,11 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
|||||||
if count == 1:
|
if count == 1:
|
||||||
if rule.meta.lib:
|
if rule.meta.lib:
|
||||||
lib_info = " (library rule)"
|
lib_info = " (library rule)"
|
||||||
capability = "%s%s" % (rutils.bold(rule.meta.name), lib_info)
|
capability = f"{rutils.bold(rule.meta.name)}{lib_info}"
|
||||||
else:
|
else:
|
||||||
if rule.meta.lib:
|
if rule.meta.lib:
|
||||||
lib_info = ", only showing first match of library rule"
|
lib_info = ", only showing first match of library rule"
|
||||||
capability = "%s (%d matches%s)" % (rutils.bold(rule.meta.name), count, lib_info)
|
capability = f"{rutils.bold(rule.meta.name)} ({count} matches{lib_info})"
|
||||||
|
|
||||||
ostream.writeln(capability)
|
ostream.writeln(capability)
|
||||||
had_match = True
|
had_match = True
|
||||||
@@ -345,7 +345,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
|||||||
# because we do the file-scope evaluation a single time.
|
# because we do the file-scope evaluation a single time.
|
||||||
# but i'm not 100% sure if this is/will always be true.
|
# but i'm not 100% sure if this is/will always be true.
|
||||||
# so, lets be explicit about our assumptions and raise an exception if they fail.
|
# so, lets be explicit about our assumptions and raise an exception if they fail.
|
||||||
raise RuntimeError("unexpected file scope match count: %d" % (len(matches)))
|
raise RuntimeError(f"unexpected file scope match count: {len(matches)}")
|
||||||
first_address, first_match = matches[0]
|
first_address, first_match = matches[0]
|
||||||
render_match(ostream, first_match, indent=0)
|
render_match(ostream, first_match, indent=0)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class InvalidRule(ValueError):
|
|||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "invalid rule: %s" % (self.msg)
|
return f"invalid rule: {self.msg}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -177,7 +177,7 @@ class InvalidRuleWithPath(InvalidRule):
|
|||||||
self.__cause__ = None
|
self.__cause__ = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "invalid rule: %s: %s" % (self.path, self.msg)
|
return f"invalid rule: {self.path}: {self.msg}"
|
||||||
|
|
||||||
|
|
||||||
class InvalidRuleSet(ValueError):
|
class InvalidRuleSet(ValueError):
|
||||||
@@ -186,7 +186,7 @@ class InvalidRuleSet(ValueError):
|
|||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "invalid rule set: %s" % (self.msg)
|
return f"invalid rule set: {self.msg}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -200,14 +200,14 @@ def ensure_feature_valid_for_scope(scope: str, feature: Union[Feature, Statement
|
|||||||
and isinstance(feature.value, str)
|
and isinstance(feature.value, str)
|
||||||
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
|
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
|
||||||
):
|
):
|
||||||
raise InvalidRule("feature %s not supported for scope %s" % (feature, scope))
|
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
|
||||||
|
|
||||||
if not isinstance(feature, capa.features.common.Characteristic):
|
if not isinstance(feature, capa.features.common.Characteristic):
|
||||||
# features of this scope that are not Characteristics will be Type instances.
|
# features of this scope that are not Characteristics will be Type instances.
|
||||||
# check that the given feature is one of these types.
|
# check that the given feature is one of these types.
|
||||||
types_for_scope = filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope])
|
types_for_scope = filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope])
|
||||||
if not isinstance(feature, tuple(types_for_scope)): # type: ignore
|
if not isinstance(feature, tuple(types_for_scope)): # type: ignore
|
||||||
raise InvalidRule("feature %s not supported for scope %s" % (feature, scope))
|
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
|
||||||
|
|
||||||
|
|
||||||
def parse_int(s: str) -> int:
|
def parse_int(s: str) -> int:
|
||||||
@@ -224,10 +224,10 @@ def parse_range(s: str):
|
|||||||
"""
|
"""
|
||||||
# we want to use `{` characters, but this is a dict in yaml.
|
# we want to use `{` characters, but this is a dict in yaml.
|
||||||
if not s.startswith("("):
|
if not s.startswith("("):
|
||||||
raise InvalidRule("invalid range: %s" % (s))
|
raise InvalidRule(f"invalid range: {s}")
|
||||||
|
|
||||||
if not s.endswith(")"):
|
if not s.endswith(")"):
|
||||||
raise InvalidRule("invalid range: %s" % (s))
|
raise InvalidRule(f"invalid range: {s}")
|
||||||
|
|
||||||
s = s[len("(") : -len(")")]
|
s = s[len("(") : -len(")")]
|
||||||
min_spec, _, max_spec = s.partition(",")
|
min_spec, _, max_spec = s.partition(",")
|
||||||
@@ -296,7 +296,7 @@ def parse_feature(key: str):
|
|||||||
elif key == "property":
|
elif key == "property":
|
||||||
return capa.features.insn.Property
|
return capa.features.insn.Property
|
||||||
else:
|
else:
|
||||||
raise InvalidRule("unexpected statement: %s" % key)
|
raise InvalidRule(f"unexpected statement: {key}")
|
||||||
|
|
||||||
|
|
||||||
# this is the separator between a feature value and its description
|
# this is the separator between a feature value and its description
|
||||||
@@ -310,11 +310,11 @@ def parse_bytes(s: str) -> bytes:
|
|||||||
try:
|
try:
|
||||||
b = codecs.decode(s.replace(" ", "").encode("ascii"), "hex")
|
b = codecs.decode(s.replace(" ", "").encode("ascii"), "hex")
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
raise InvalidRule('unexpected bytes value: must be a valid hex sequence: "%s"' % s)
|
raise InvalidRule(f'unexpected bytes value: must be a valid hex sequence: "{s}"')
|
||||||
|
|
||||||
if len(b) > MAX_BYTES_FEATURE_SIZE:
|
if len(b) > MAX_BYTES_FEATURE_SIZE:
|
||||||
raise InvalidRule(
|
raise InvalidRule(
|
||||||
"unexpected bytes value: byte sequences must be no larger than %s bytes" % MAX_BYTES_FEATURE_SIZE
|
f"unexpected bytes value: byte sequences must be no larger than {MAX_BYTES_FEATURE_SIZE} bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
@@ -337,15 +337,14 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
|
|||||||
# - number: 10 = CONST_FOO
|
# - number: 10 = CONST_FOO
|
||||||
# description: CONST_FOO
|
# description: CONST_FOO
|
||||||
raise InvalidRule(
|
raise InvalidRule(
|
||||||
'unexpected value: "%s", only one description allowed (inline description with `%s`)'
|
f'unexpected value: "{s}", only one description allowed (inline description with `{DESCRIPTION_SEPARATOR}`)'
|
||||||
% (s, DESCRIPTION_SEPARATOR)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
value, _, description = s.partition(DESCRIPTION_SEPARATOR)
|
value, _, description = s.partition(DESCRIPTION_SEPARATOR)
|
||||||
if description == "":
|
if description == "":
|
||||||
# sanity check:
|
# sanity check:
|
||||||
# there is an empty description, like `number: 10 =`
|
# there is an empty description, like `number: 10 =`
|
||||||
raise InvalidRule('unexpected value: "%s", description cannot be empty' % s)
|
raise InvalidRule(f'unexpected value: "{s}", description cannot be empty')
|
||||||
else:
|
else:
|
||||||
# this is a string, but there is no description,
|
# this is a string, but there is no description,
|
||||||
# like: `api: CreateFileA`
|
# like: `api: CreateFileA`
|
||||||
@@ -372,7 +371,7 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
|
|||||||
try:
|
try:
|
||||||
value = parse_int(value)
|
value = parse_int(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise InvalidRule('unexpected value: "%s", must begin with numerical value' % value)
|
raise InvalidRule(f'unexpected value: "{value}", must begin with numerical value')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# the value might be a number, like: `number: 10`
|
# the value might be a number, like: `number: 10`
|
||||||
@@ -532,9 +531,9 @@ def build_statements(d, scope: str):
|
|||||||
min, max = parse_range(count)
|
min, max = parse_range(count)
|
||||||
return ceng.Range(feature, min=min, max=max, description=description)
|
return ceng.Range(feature, min=min, max=max, description=description)
|
||||||
else:
|
else:
|
||||||
raise InvalidRule("unexpected range: %s" % (count))
|
raise InvalidRule(f"unexpected range: {count}")
|
||||||
elif key == "string" and not isinstance(d[key], str):
|
elif key == "string" and not isinstance(d[key], str):
|
||||||
raise InvalidRule("ambiguous string value %s, must be defined as explicit string" % d[key])
|
raise InvalidRule(f"ambiguous string value {d[key]}, must be defined as explicit string")
|
||||||
|
|
||||||
elif key.startswith("operand[") and key.endswith("].number"):
|
elif key.startswith("operand[") and key.endswith("].number"):
|
||||||
index = key[len("operand[") : -len("].number")]
|
index = key[len("operand[") : -len("].number")]
|
||||||
@@ -573,12 +572,12 @@ def build_statements(d, scope: str):
|
|||||||
or (key == "format" and d[key] not in capa.features.common.VALID_FORMAT)
|
or (key == "format" and d[key] not in capa.features.common.VALID_FORMAT)
|
||||||
or (key == "arch" and d[key] not in capa.features.common.VALID_ARCH)
|
or (key == "arch" and d[key] not in capa.features.common.VALID_ARCH)
|
||||||
):
|
):
|
||||||
raise InvalidRule("unexpected %s value %s" % (key, d[key]))
|
raise InvalidRule(f"unexpected {key} value {d[key]}")
|
||||||
|
|
||||||
elif key.startswith("property/"):
|
elif key.startswith("property/"):
|
||||||
access = key[len("property/") :]
|
access = key[len("property/") :]
|
||||||
if access not in capa.features.common.VALID_FEATURE_ACCESS:
|
if access not in capa.features.common.VALID_FEATURE_ACCESS:
|
||||||
raise InvalidRule("unexpected %s access %s" % (key, access))
|
raise InvalidRule(f"unexpected {key} access {access}")
|
||||||
|
|
||||||
value, description = parse_description(d[key], key, d.get("description"))
|
value, description = parse_description(d[key], key, d.get("description"))
|
||||||
try:
|
try:
|
||||||
@@ -617,10 +616,10 @@ class Rule:
|
|||||||
self.definition = definition
|
self.definition = definition
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Rule(name=%s)" % (self.name)
|
return f"Rule(name={self.name})"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Rule(scope=%s, name=%s)" % (self.scope, self.name)
|
return f"Rule(scope={self.scope}, name={self.name})"
|
||||||
|
|
||||||
def get_dependencies(self, namespaces):
|
def get_dependencies(self, namespaces):
|
||||||
"""
|
"""
|
||||||
@@ -998,7 +997,7 @@ def ensure_rule_dependencies_are_met(rules: List[Rule]) -> None:
|
|||||||
for rule in rules_by_name.values():
|
for rule in rules_by_name.values():
|
||||||
for dep in rule.get_dependencies(namespaces):
|
for dep in rule.get_dependencies(namespaces):
|
||||||
if dep not in rules_by_name:
|
if dep not in rules_by_name:
|
||||||
raise InvalidRule('rule "%s" depends on missing rule "%s"' % (rule.name, dep))
|
raise InvalidRule(f'rule "{rule.name}" depends on missing rule "{dep}"')
|
||||||
|
|
||||||
|
|
||||||
def index_rules_by_namespace(rules: List[Rule]) -> Dict[str, List[Rule]]:
|
def index_rules_by_namespace(rules: List[Rule]) -> Dict[str, List[Rule]]:
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ def get_capa_results(args):
|
|||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "input file does not appear to be a PE file: %s" % path,
|
"error": f"input file does not appear to be a PE file: {path}",
|
||||||
}
|
}
|
||||||
except capa.main.UnsupportedRuntimeError:
|
except capa.main.UnsupportedRuntimeError:
|
||||||
return {
|
return {
|
||||||
@@ -124,7 +124,7 @@ def get_capa_results(args):
|
|||||||
return {
|
return {
|
||||||
"path": path,
|
"path": path,
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "unexpected error: %s" % (e),
|
"error": f"unexpected error: {e}",
|
||||||
}
|
}
|
||||||
|
|
||||||
meta = capa.main.collect_metadata([], path, [], extractor)
|
meta = capa.main.collect_metadata([], path, [], extractor)
|
||||||
@@ -202,7 +202,7 @@ def main(argv=None):
|
|||||||
elif result["status"] == "ok":
|
elif result["status"] == "ok":
|
||||||
results[result["path"]] = rd.ResultDocument.parse_obj(result["ok"]).json(exclude_none=True)
|
results[result["path"]] = rd.ResultDocument.parse_obj(result["ok"]).json(exclude_none=True)
|
||||||
else:
|
else:
|
||||||
raise ValueError("unexpected status: %s" % (result["status"]))
|
raise ValueError(f"unexpected status: {result['status']}")
|
||||||
|
|
||||||
print(json.dumps(results))
|
print(json.dumps(results))
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def render_capabilities(doc: rd.ResultDocument, result):
|
|||||||
if count == 1:
|
if count == 1:
|
||||||
capability = rule.meta.name
|
capability = rule.meta.name
|
||||||
else:
|
else:
|
||||||
capability = "%s (%d matches)" % (rule.meta.name, count)
|
capability = f"{rule.meta.name} ({count} matches)"
|
||||||
|
|
||||||
result["CAPABILITY"].setdefault(rule.meta.namespace, list())
|
result["CAPABILITY"].setdefault(rule.meta.namespace, list())
|
||||||
result["CAPABILITY"][rule.meta.namespace].append(capability)
|
result["CAPABILITY"][rule.meta.namespace].append(capability)
|
||||||
@@ -108,9 +108,9 @@ def render_attack(doc, result):
|
|||||||
inner_rows = []
|
inner_rows = []
|
||||||
for technique, subtechnique, id in sorted(techniques):
|
for technique, subtechnique, id in sorted(techniques):
|
||||||
if subtechnique is None:
|
if subtechnique is None:
|
||||||
inner_rows.append("%s %s" % (technique, id))
|
inner_rows.append(f"{technique} {id}")
|
||||||
else:
|
else:
|
||||||
inner_rows.append("%s::%s %s" % (technique, subtechnique, id))
|
inner_rows.append(f"{technique}::{subtechnique} {id}")
|
||||||
result["ATTCK"].setdefault(tactic.upper(), inner_rows)
|
result["ATTCK"].setdefault(tactic.upper(), inner_rows)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,9 +142,9 @@ def render_mbc(doc, result):
|
|||||||
inner_rows = []
|
inner_rows = []
|
||||||
for behavior, method, id in sorted(behaviors):
|
for behavior, method, id in sorted(behaviors):
|
||||||
if method is None:
|
if method is None:
|
||||||
inner_rows.append("%s [%s]" % (behavior, id))
|
inner_rows.append(f"{behavior} [{id}]")
|
||||||
else:
|
else:
|
||||||
inner_rows.append("%s::%s [%s]" % (behavior, method, id))
|
inner_rows.append(f"{behavior}::{method} [{id}]")
|
||||||
result["MBC"].setdefault(objective.upper(), inner_rows)
|
result["MBC"].setdefault(objective.upper(), inner_rows)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def load_analysis(bv):
|
|||||||
if not path or not os.access(path, os.R_OK):
|
if not path or not os.access(path, os.R_OK):
|
||||||
binaryninja.log_error("Invalid filename.")
|
binaryninja.log_error("Invalid filename.")
|
||||||
return 0
|
return 0
|
||||||
binaryninja.log_info("Using capa file %s" % path)
|
binaryninja.log_info(f"Using capa file {path}")
|
||||||
|
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
doc = json.loads(f.read().decode("utf-8"))
|
doc = json.loads(f.read().decode("utf-8"))
|
||||||
@@ -97,7 +97,7 @@ def load_analysis(bv):
|
|||||||
else:
|
else:
|
||||||
cmt = f"{name}"
|
cmt = f"{name}"
|
||||||
|
|
||||||
binaryninja.log_info("0x%x: %s" % (va, cmt))
|
binaryninja.log_info(f"{hex(va)}: {cmt}")
|
||||||
try:
|
try:
|
||||||
# message will look something like:
|
# message will look something like:
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -101,9 +101,9 @@ def main():
|
|||||||
rows = sorted(rows)
|
rows = sorted(rows)
|
||||||
for ns, name, va in rows:
|
for ns, name, va in rows:
|
||||||
if ns:
|
if ns:
|
||||||
cmt = f"{name} ({ns})"
|
cmt = name + f"({ns})"
|
||||||
else:
|
else:
|
||||||
cmt = f"{name}"
|
cmt = name
|
||||||
|
|
||||||
logger.info("0x%x: %s", va, cmt)
|
logger.info("0x%x: %s", va, cmt)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ def display_top(snapshot, key_type="lineno", limit=10):
|
|||||||
print(f"Top {limit} lines")
|
print(f"Top {limit} lines")
|
||||||
for index, stat in enumerate(top_stats[:limit], 1):
|
for index, stat in enumerate(top_stats[:limit], 1):
|
||||||
frame = stat.traceback[0]
|
frame = stat.traceback[0]
|
||||||
print(f"#{index}: {frame.filename}:{frame.lineno}: {stat.size / 1024:.1f} KiB")
|
print(f"#{index}: {frame.filename}:{frame.lineno}: {(stat.size/1024):.1f} KiB")
|
||||||
line = linecache.getline(frame.filename, frame.lineno).strip()
|
line = linecache.getline(frame.filename, frame.lineno).strip()
|
||||||
if line:
|
if line:
|
||||||
print(" %s" % line)
|
print(f" {line}")
|
||||||
|
|
||||||
other = top_stats[limit:]
|
other = top_stats[limit:]
|
||||||
if other:
|
if other:
|
||||||
size = sum(stat.size for stat in other)
|
size = sum(stat.size for stat in other)
|
||||||
print(f"{len(other)} other: {size / 1024:.1f} KiB")
|
print(f"{len(other)} other: {(size/1024):.1f} KiB")
|
||||||
total = sum(stat.size for stat in top_stats)
|
total = sum(stat.size for stat in top_stats)
|
||||||
print(f"Total allocated size: {total / 1024:.1f} KiB")
|
print(f"Total allocated size: {(total/1024):.1f} KiB")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -49,7 +49,7 @@ def main():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
print(f"iteration {i + 1}/{count}...")
|
print(f"iteration {i+1}/{count}...")
|
||||||
with contextlib.redirect_stdout(io.StringIO()):
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
with contextlib.redirect_stderr(io.StringIO()):
|
with contextlib.redirect_stderr(io.StringIO()):
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -59,9 +59,9 @@ def main():
|
|||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
process = psutil.Process(os.getpid())
|
process = psutil.Process(os.getpid())
|
||||||
print(f" duration: {t1 - t0:.02f}s")
|
print(f" duration: {(t1-t0):.2f}")
|
||||||
print(f" rss: {process.memory_info().rss / 1024 / 1024:.1f} MiB")
|
print(f" rss: {(process.memory_info().rss / 1024 / 1024):.1f} MiB")
|
||||||
print(f" vms: {process.memory_info().vms / 1024 / 1024:.1f} MiB")
|
print(f" vms: {(process.memory_info().vms / 1024 / 1024):.1f} MiB")
|
||||||
|
|
||||||
print("done.")
|
print("done.")
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|||||||
@@ -133,9 +133,9 @@ def main(argv=None):
|
|||||||
# so lets put that first.
|
# so lets put that first.
|
||||||
#
|
#
|
||||||
# https://docs.python.org/3/library/timeit.html#timeit.Timer.repeat
|
# https://docs.python.org/3/library/timeit.html#timeit.Timer.repeat
|
||||||
"%0.2fs" % (min(samples) / float(args.number)),
|
f"{(min(samples) / float(args.number)):.2f}s",
|
||||||
"%0.2fs" % (sum(samples) / float(args.repeat) / float(args.number)),
|
f"{(sum(samples) / float(args.repeat) / float(args.number)):.2f}s",
|
||||||
"%0.2fs" % (max(samples) / float(args.number)),
|
f"{(max(samples) / float(args.number)):.2f}s",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
headers=["label", "count(evaluations)", "min(time)", "avg(time)", "max(time)"],
|
headers=["label", "count(evaluations)", "min(time)", "avg(time)", "max(time)"],
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ def render_matches_by_function(doc: rd.ResultDocument):
|
|||||||
for f in doc.meta.analysis.feature_counts.functions:
|
for f in doc.meta.analysis.feature_counts.functions:
|
||||||
if not matches_by_function.get(f.address, {}):
|
if not matches_by_function.get(f.address, {}):
|
||||||
continue
|
continue
|
||||||
ostream.writeln("function at %s with %d features: " % (capa.render.verbose.format_address(addr), f.count))
|
ostream.writeln(f"function at {capa.render.verbose.format_address(addr)} with {f.count} features: ")
|
||||||
for rule_name in sorted(matches_by_function[f.address]):
|
for rule_name in sorted(matches_by_function[f.address]):
|
||||||
ostream.writeln(" - " + rule_name)
|
ostream.writeln(" - " + rule_name)
|
||||||
|
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ def main(argv=None):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
for feature, addr in extractor.extract_global_features():
|
for feature, addr in extractor.extract_global_features():
|
||||||
print("global: %s: %s" % (format_address(addr), feature))
|
print(f"global: {format_address(addr)}: {feature}")
|
||||||
|
|
||||||
if not args.function:
|
if not args.function:
|
||||||
for feature, addr in extractor.extract_file_features():
|
for feature, addr in extractor.extract_file_features():
|
||||||
print("file: %s: %s" % (format_address(addr), feature))
|
print(f"file: {format_address(addr)}: {feature}")
|
||||||
|
|
||||||
function_handles = tuple(extractor.get_functions())
|
function_handles = tuple(extractor.get_functions())
|
||||||
|
|
||||||
@@ -146,11 +146,11 @@ def main(argv=None):
|
|||||||
function_handles = tuple(filter(lambda fh: format_address(fh.address) == args.function, function_handles))
|
function_handles = tuple(filter(lambda fh: format_address(fh.address) == args.function, function_handles))
|
||||||
|
|
||||||
if args.function not in [format_address(fh.address) for fh in function_handles]:
|
if args.function not in [format_address(fh.address) for fh in function_handles]:
|
||||||
print("%s not a function" % args.function)
|
print(f"{args.function} not a function")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
if len(function_handles) == 0:
|
if len(function_handles) == 0:
|
||||||
print("%s not a function", args.function)
|
print(f"{args.function} not a function")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
print_features(function_handles, extractor)
|
print_features(function_handles, extractor)
|
||||||
@@ -164,13 +164,13 @@ def ida_main():
|
|||||||
import capa.features.extractors.ida.extractor
|
import capa.features.extractors.ida.extractor
|
||||||
|
|
||||||
function = idc.get_func_attr(idc.here(), idc.FUNCATTR_START)
|
function = idc.get_func_attr(idc.here(), idc.FUNCATTR_START)
|
||||||
print("getting features for current function 0x%X" % function)
|
print(f"getting features for current function {hex(function)}")
|
||||||
|
|
||||||
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
||||||
|
|
||||||
if not function:
|
if not function:
|
||||||
for feature, addr in extractor.extract_file_features():
|
for feature, addr in extractor.extract_file_features():
|
||||||
print("file: %s: %s" % (format_address(addr), feature))
|
print(f"file: {format_address(addr)}: {feature}")
|
||||||
return
|
return
|
||||||
|
|
||||||
function_handles = tuple(extractor.get_functions())
|
function_handles = tuple(extractor.get_functions())
|
||||||
@@ -179,7 +179,7 @@ def ida_main():
|
|||||||
function_handles = tuple(filter(lambda fh: fh.inner.start_ea == function, function_handles))
|
function_handles = tuple(filter(lambda fh: fh.inner.start_ea == function, function_handles))
|
||||||
|
|
||||||
if len(function_handles) == 0:
|
if len(function_handles) == 0:
|
||||||
print("0x%X not a function" % function)
|
print(f"{hex(function)} not a function")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
print_features(function_handles, extractor)
|
print_features(function_handles, extractor)
|
||||||
@@ -194,16 +194,16 @@ def print_features(functions, extractor: capa.features.extractors.base_extractor
|
|||||||
logger.debug("skipping library function %s (%s)", format_address(f.address), function_name)
|
logger.debug("skipping library function %s (%s)", format_address(f.address), function_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("func: %s" % (format_address(f.address)))
|
print(f"func: {format_address(f.address)}")
|
||||||
|
|
||||||
for feature, addr in extractor.extract_function_features(f):
|
for feature, addr in extractor.extract_function_features(f):
|
||||||
if capa.features.common.is_global_feature(feature):
|
if capa.features.common.is_global_feature(feature):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if f.address != addr:
|
if f.address != addr:
|
||||||
print(" func: %s: %s -> %s" % (format_address(f.address), feature, format_address(addr)))
|
print(f" func: {format_address(f.address)}: {feature} -> {format_address(addr)}")
|
||||||
else:
|
else:
|
||||||
print(" func: %s: %s" % (format_address(f.address), feature))
|
print(f" func: {format_address(f.address)}: {feature}")
|
||||||
|
|
||||||
for bb in extractor.get_basic_blocks(f):
|
for bb in extractor.get_basic_blocks(f):
|
||||||
for feature, addr in extractor.extract_basic_block_features(f, bb):
|
for feature, addr in extractor.extract_basic_block_features(f, bb):
|
||||||
@@ -211,9 +211,9 @@ def print_features(functions, extractor: capa.features.extractors.base_extractor
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if bb.address != addr:
|
if bb.address != addr:
|
||||||
print(" bb: %s: %s -> %s" % (format_address(bb.address), feature, format_address(addr)))
|
print(f" bb: {format_address(bb.address)}: {feature} -> {format_address(addr)}")
|
||||||
else:
|
else:
|
||||||
print(" bb: %s: %s" % (format_address(bb.address), feature))
|
print(f" bb: {format_address(bb.address)}: {feature}")
|
||||||
|
|
||||||
for insn in extractor.get_instructions(f, bb):
|
for insn in extractor.get_instructions(f, bb):
|
||||||
for feature, addr in extractor.extract_insn_features(f, bb, insn):
|
for feature, addr in extractor.extract_insn_features(f, bb, insn):
|
||||||
@@ -223,16 +223,10 @@ def print_features(functions, extractor: capa.features.extractors.base_extractor
|
|||||||
try:
|
try:
|
||||||
if insn.address != addr:
|
if insn.address != addr:
|
||||||
print(
|
print(
|
||||||
" insn: %s: %s: %s -> %s"
|
f" insn: {format_address(f.address)}: {format_address(insn.address)}: {feature} -> {format_address(addr)}"
|
||||||
% (
|
|
||||||
format_address(f.address),
|
|
||||||
format_address(insn.address),
|
|
||||||
feature,
|
|
||||||
format_address(addr),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(" insn: %s: %s" % (format_address(insn.address), feature))
|
print(f" insn: {format_address(insn.address)}: {feature}")
|
||||||
|
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
# may be an issue while piping to less and encountering non-ascii characters
|
# may be an issue while piping to less and encountering non-ascii characters
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ def get_data_path_by_name(name):
|
|||||||
elif name.startswith("294b8d"):
|
elif name.startswith("294b8d"):
|
||||||
return os.path.join(CD, "data", "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_")
|
return os.path.join(CD, "data", "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_")
|
||||||
else:
|
else:
|
||||||
raise ValueError("unexpected sample fixture: %s" % name)
|
raise ValueError(f"unexpected sample fixture: {name}")
|
||||||
|
|
||||||
|
|
||||||
def get_sample_md5_by_name(name):
|
def get_sample_md5_by_name(name):
|
||||||
@@ -341,7 +341,7 @@ def get_sample_md5_by_name(name):
|
|||||||
# file name is SHA256 hash
|
# file name is SHA256 hash
|
||||||
return "3db3e55b16a7b1b1afb970d5e77c5d98"
|
return "3db3e55b16a7b1b1afb970d5e77c5d98"
|
||||||
else:
|
else:
|
||||||
raise ValueError("unexpected sample fixture: %s" % name)
|
raise ValueError(f"unexpected sample fixture: {name}")
|
||||||
|
|
||||||
|
|
||||||
def resolve_sample(sample):
|
def resolve_sample(sample):
|
||||||
@@ -981,21 +981,16 @@ def do_test_feature_presence(get_extractor, sample, scope, feature, expected):
|
|||||||
extractor = get_extractor(sample)
|
extractor = get_extractor(sample)
|
||||||
features = scope(extractor)
|
features = scope(extractor)
|
||||||
if expected:
|
if expected:
|
||||||
msg = "%s should be found in %s" % (str(feature), scope.__name__)
|
msg = f"{str(feature)} should be found in {scope.__name__}"
|
||||||
else:
|
else:
|
||||||
msg = "%s should not be found in %s" % (str(feature), scope.__name__)
|
msg = f"{str(feature)} should not be found in {scope.__name__}"
|
||||||
assert feature.evaluate(features) == expected, msg
|
assert feature.evaluate(features) == expected, msg
|
||||||
|
|
||||||
|
|
||||||
def do_test_feature_count(get_extractor, sample, scope, feature, expected):
|
def do_test_feature_count(get_extractor, sample, scope, feature, expected):
|
||||||
extractor = get_extractor(sample)
|
extractor = get_extractor(sample)
|
||||||
features = scope(extractor)
|
features = scope(extractor)
|
||||||
msg = "%s should be found %d times in %s, found: %d" % (
|
msg = f"{str(feature)} should be found {expected} times in {scope.__name__}, found: {len(features[feature])}"
|
||||||
str(feature),
|
|
||||||
expected,
|
|
||||||
scope.__name__,
|
|
||||||
len(features[feature]),
|
|
||||||
)
|
|
||||||
assert len(features[feature]) == expected, msg
|
assert len(features[feature]) == expected, msg
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def check_input_file(wanted):
|
|||||||
found = binascii.hexlify(idautils.GetInputFileMD5()[:15]).decode("ascii").lower()
|
found = binascii.hexlify(idautils.GetInputFileMD5()[:15]).decode("ascii").lower()
|
||||||
|
|
||||||
if not wanted.startswith(found):
|
if not wanted.startswith(found):
|
||||||
raise RuntimeError("please run the tests against sample with MD5: `%s`" % (wanted))
|
raise RuntimeError(f"please run the tests against sample with MD5: `{wanted}`")
|
||||||
|
|
||||||
|
|
||||||
def get_ida_extractor(_path):
|
def get_ida_extractor(_path):
|
||||||
@@ -51,7 +51,7 @@ def test_ida_features():
|
|||||||
try:
|
try:
|
||||||
check_input_file(fixtures.get_sample_md5_by_name(sample))
|
check_input_file(fixtures.get_sample_md5_by_name(sample))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
print("SKIP %s" % (id))
|
print(f"SKIP {id}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
scope = fixtures.resolve_scope(scope)
|
scope = fixtures.resolve_scope(scope)
|
||||||
@@ -60,10 +60,10 @@ def test_ida_features():
|
|||||||
try:
|
try:
|
||||||
fixtures.do_test_feature_presence(get_ida_extractor, sample, scope, feature, expected)
|
fixtures.do_test_feature_presence(get_ida_extractor, sample, scope, feature, expected)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("FAIL %s" % (id))
|
print(f"FAIL {id}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("OK %s" % (id))
|
print(f"OK {id}")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
@pytest.mark.skip(reason="IDA Pro tests must be run within IDA")
|
||||||
@@ -74,7 +74,7 @@ def test_ida_feature_counts():
|
|||||||
try:
|
try:
|
||||||
check_input_file(fixtures.get_sample_md5_by_name(sample))
|
check_input_file(fixtures.get_sample_md5_by_name(sample))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
print("SKIP %s" % (id))
|
print(f"SKIP {id}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
scope = fixtures.resolve_scope(scope)
|
scope = fixtures.resolve_scope(scope)
|
||||||
@@ -83,10 +83,10 @@ def test_ida_feature_counts():
|
|||||||
try:
|
try:
|
||||||
fixtures.do_test_feature_count(get_ida_extractor, sample, scope, feature, expected)
|
fixtures.do_test_feature_count(get_ida_extractor, sample, scope, feature, expected)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("FAIL %s" % (id))
|
print(f"FAIL {id}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("OK %s" % (id))
|
print(f"OK {id}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -62,5 +62,5 @@ def test_bulk_process(tmpdir):
|
|||||||
|
|
||||||
def run_program(script_path, args):
|
def run_program(script_path, args):
|
||||||
args = [sys.executable] + [script_path] + args
|
args = [sys.executable] + [script_path] + args
|
||||||
print("running: '%s'" % args)
|
print(f"running: '{args}'")
|
||||||
return subprocess.run(args)
|
return subprocess.run(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user