From 8329abd3c8fcee6ce872f2160ee848ac57b8e3d8 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 16 Jan 2025 12:53:42 +0000 Subject: [PATCH 01/17] rd: debugging helper formatting --- capa/features/common.py | 19 +++++++++++++++++++ capa/render/result_document.py | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/capa/features/common.py b/capa/features/common.py index 5be8ffca9..f431336af 100644 --- a/capa/features/common.py +++ b/capa/features/common.py @@ -105,6 +105,25 @@ def __bool__(self): def __nonzero__(self): return self.success + def __str__(self): + # as this object isn't user facing, this formatting is just to help with debugging + + lines = [] + def rec(m: "Result", indent: int): + if isinstance(m.statement, capa.engine.Statement): + line = (" " * indent) + str(m.statement.name) + " " + str(m.success) + else: + line = (" " * indent) + str(m.statement) + " " + str(m.success) + " " + str(m.locations) + + lines.append(line) + + for child in m.children: + rec(child, indent + 1) + + rec(self, 0) + return "\n".join(lines) + + class Feature(abc.ABC): # noqa: B024 # this is an abstract class, since we don't want anyone to instantiate it directly, diff --git a/capa/render/result_document.py b/capa/render/result_document.py index 9ef529bef..ce3677eea 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -474,6 +474,26 @@ def to_capa(self, rules_by_name: dict[str, capa.rules.Rule]) -> capa.engine.Resu children=children, ) + def __str__(self): + # as this object isn't user facing, this formatting is just to help with debugging + + lines = [] + def rec(m: "Match", indent: int): + if isinstance(m.node, StatementNode): + line = (" " * indent) + str(m.node.statement.type) + " " + str(m.success) + elif isinstance(m.node, FeatureNode): + line = (" " * indent) + str(m.node.feature) + " " + str(m.success) + " " + str(m.locations) + else: + raise ValueError("unexpected node type") + + lines.append(line) + + for child in m.children: + rec(child, indent + 1) + + rec(self, 0) + return "\n".join(lines) + def parse_parts_id(s: str): id_ = "" From 4896ff01d82045a2f665f1750bd014a2100e9383 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 16 Jan 2025 12:49:29 +0000 Subject: [PATCH 02/17] result: make copy of locations to ensure its not modified by reference after we expect it to be --- capa/features/common.py | 8 ++++++-- capa/rules/__init__.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/capa/features/common.py b/capa/features/common.py index f431336af..674400a4e 100644 --- a/capa/features/common.py +++ b/capa/features/common.py @@ -92,7 +92,7 @@ def __init__( self.success = success self.statement = statement self.children = children - self.locations = locations if locations is not None else set() + self.locations = frozenset(locations) if locations is not None else frozenset() def __eq__(self, other): if isinstance(other, bool): @@ -194,7 +194,11 @@ def __repr__(self): def evaluate(self, features: "capa.engine.FeatureSet", short_circuit=True) -> Result: capa.perf.counters["evaluate.feature"] += 1 capa.perf.counters["evaluate.feature." + self.name] += 1 - return Result(self in features, self, [], locations=features.get(self, set())) + success = self in features + if success: + return Result(True, self, [], locations=features[self]) + else: + return Result(False, self, [], locations=None) class MatchedRule(Feature): diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 291927d22..54b8f9baf 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -853,18 +853,18 @@ def __str__(self): def __repr__(self): return f"Rule(scope={self.scopes}, name={self.name})" - def get_dependencies(self, namespaces): + def get_dependencies(self, namespaces: dict[str, list["Rule"]]) -> set[str]: """ fetch the names of rules this rule relies upon. these are only the direct dependencies; a user must compute the transitive dependency graph themself, if they want it. Args: - namespaces(dict[str, list[Rule]]): mapping from namespace name to rules in it. + namespaces: mapping from namespace name to rules in it. see `index_rules_by_namespace`. Returns: - list[str]: names of rules upon which this rule depends. + set[str]: names of rules upon which this rule depends. """ deps: set[str] = set() From 8d17319128bf531f72841063b3d7375da608f613 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Tue, 10 Dec 2024 15:58:32 +0000 Subject: [PATCH 03/17] capabilities: use dataclasses to represent complicated return types foo --- capa/capabilities/common.py | 30 ++++++-- capa/capabilities/dynamic.py | 88 +++++++++++---------- capa/capabilities/static.py | 94 +++++++++++++---------- capa/ghidra/capa_explorer.py | 6 +- capa/ghidra/capa_ghidra.py | 34 ++++---- capa/ida/plugin/form.py | 14 ++-- capa/loader.py | 14 ++-- capa/main.py | 48 ++++++------ capa/render/result_document.py | 20 ++++- rules | 2 +- scripts/bulk-process.py | 8 +- scripts/capa-as-library.py | 12 +-- scripts/detect-binexport2-capabilities.py | 8 +- scripts/import-to-ida.py | 2 +- scripts/lint.py | 4 +- scripts/show-capabilities-by-function.py | 10 +-- tests/data | 2 +- tests/test_capabilities.py | 48 ++++++------ tests/test_freeze_dynamic.py | 4 +- tests/test_freeze_static.py | 4 +- tests/test_result_document.py | 3 +- 21 files changed, 256 insertions(+), 199 deletions(-) diff --git a/capa/capabilities/common.py b/capa/capabilities/common.py index 4dd1ee58f..9acbd68f5 100644 --- a/capa/capabilities/common.py +++ b/capa/capabilities/common.py @@ -16,17 +16,28 @@ import logging import itertools import collections -from typing import Any +from typing import Optional +from dataclasses import dataclass from capa.rules import Scope, RuleSet from capa.engine import FeatureSet, MatchResults from capa.features.address import NO_ADDRESS +from capa.render.result_document import LibraryFunction, StaticFeatureCounts, DynamicFeatureCounts from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor, DynamicFeatureExtractor logger = logging.getLogger(__name__) -def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet): +@dataclass +class FileCapabilities: + features: FeatureSet + matches: MatchResults + feature_count: int + + +def find_file_capabilities( + ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet +) -> FileCapabilities: file_features: FeatureSet = collections.defaultdict(set) for feature, va in itertools.chain(extractor.extract_file_features(), extractor.extract_global_features()): @@ -43,8 +54,8 @@ def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, functi file_features.update(function_features) - _, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS) - return matches, len(file_features) + features, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS) + return FileCapabilities(features, matches, len(file_features)) def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalone=True) -> bool: @@ -69,9 +80,14 @@ def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalon return False -def find_capabilities( - ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None, **kwargs -) -> tuple[MatchResults, Any]: +@dataclass +class Capabilities: + matches: MatchResults + feature_counts: StaticFeatureCounts | DynamicFeatureCounts + library_functions: Optional[tuple[LibraryFunction, ...]] = None + + +def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None, **kwargs) -> Capabilities: from capa.capabilities.static import find_static_capabilities from capa.capabilities.dynamic import find_dynamic_capabilities diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 566a8e597..95aa795d9 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -16,26 +16,30 @@ import logging import itertools import collections -from typing import Any +from dataclasses import dataclass import capa.perf import capa.features.freeze as frz import capa.render.result_document as rdoc from capa.rules import Scope, RuleSet from capa.engine import FeatureSet, MatchResults -from capa.capabilities.common import find_file_capabilities +from capa.capabilities.common import Capabilities, find_file_capabilities from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle, DynamicFeatureExtractor logger = logging.getLogger(__name__) +@dataclass +class CallCapabilities: + features: FeatureSet + matches: MatchResults + + def find_call_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle -) -> tuple[FeatureSet, MatchResults]: +) -> CallCapabilities: """ find matches for the given rules for the given call. - - returns: tuple containing (features for call, match results for call) """ # all features found for the call. features: FeatureSet = collections.defaultdict(set) @@ -53,16 +57,21 @@ def find_call_capabilities( for addr, _ in res: capa.engine.index_rule_matches(features, rule, [addr]) - return features, matches + return CallCapabilities(features, matches) + + +@dataclass +class ThreadCapabilities: + features: FeatureSet + thread_matches: MatchResults + call_matches: MatchResults def find_thread_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle -) -> tuple[FeatureSet, MatchResults, MatchResults]: +) -> ThreadCapabilities: """ find matches for the given rules within the given thread. - - returns: tuple containing (features for thread, match results for thread, match results for calls) """ # all features found within this thread, # includes features found within calls. @@ -73,11 +82,11 @@ def find_thread_capabilities( call_matches: MatchResults = collections.defaultdict(list) for ch in extractor.get_calls(ph, th): - ifeatures, imatches = find_call_capabilities(ruleset, extractor, ph, th, ch) - for feature, vas in ifeatures.items(): + call_capabilities = find_call_capabilities(ruleset, extractor, ph, th, ch) + for feature, vas in call_capabilities.features.items(): features[feature].update(vas) - for rule_name, res in imatches.items(): + for rule_name, res in call_capabilities.matches.items(): call_matches[rule_name].extend(res) for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()): @@ -91,16 +100,22 @@ def find_thread_capabilities( for va, _ in res: capa.engine.index_rule_matches(features, rule, [va]) - return features, matches, call_matches + return ThreadCapabilities(features, matches, call_matches) + + +@dataclass +class ProcessCapabilities: + process_matches: MatchResults + thread_matches: MatchResults + call_matches: MatchResults + feature_count: int def find_process_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle -) -> tuple[MatchResults, MatchResults, MatchResults, int]: +) -> ProcessCapabilities: """ find matches for the given rules within the given process. - - returns: tuple containing (match results for process, match results for threads, match results for calls, number of features) """ # all features found within this process, # includes features found within threads (and calls). @@ -115,26 +130,26 @@ def find_process_capabilities( call_matches: MatchResults = collections.defaultdict(list) for th in extractor.get_threads(ph): - features, tmatches, cmatches = find_thread_capabilities(ruleset, extractor, ph, th) - for feature, vas in features.items(): + thread_capabilities = find_thread_capabilities(ruleset, extractor, ph, th) + for feature, vas in thread_capabilities.features.items(): process_features[feature].update(vas) - for rule_name, res in tmatches.items(): + for rule_name, res in thread_capabilities.thread_matches.items(): thread_matches[rule_name].extend(res) - for rule_name, res in cmatches.items(): + for rule_name, res in thread_capabilities.call_matches.items(): call_matches[rule_name].extend(res) for feature, va in itertools.chain(extractor.extract_process_features(ph), extractor.extract_global_features()): process_features[feature].add(va) _, process_matches = ruleset.match(Scope.PROCESS, process_features, ph.address) - return process_matches, thread_matches, call_matches, len(process_features) + return ProcessCapabilities(process_matches, thread_matches, call_matches, len(process_features)) def find_dynamic_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, disable_progress=None -) -> tuple[MatchResults, Any]: +) -> Capabilities: all_process_matches: MatchResults = collections.defaultdict(list) all_thread_matches: MatchResults = collections.defaultdict(list) all_call_matches: MatchResults = collections.defaultdict(list) @@ -150,19 +165,19 @@ def find_dynamic_capabilities( ) as pbar: task = pbar.add_task("matching", total=n_processes, unit="processes") for p in processes: - process_matches, thread_matches, call_matches, feature_count = find_process_capabilities( - ruleset, extractor, p - ) + process_capabilities = find_process_capabilities(ruleset, extractor, p) feature_counts.processes += ( - rdoc.ProcessFeatureCount(address=frz.Address.from_capa(p.address), count=feature_count), + rdoc.ProcessFeatureCount( + address=frz.Address.from_capa(p.address), count=process_capabilities.feature_count + ), ) - logger.debug("analyzed %s and extracted %d features", p.address, feature_count) + logger.debug("analyzed %s and extracted %d features", p.address, process_capabilities.feature_count) - for rule_name, res in process_matches.items(): + for rule_name, res in process_capabilities.process_matches.items(): all_process_matches[rule_name].extend(res) - for rule_name, res in thread_matches.items(): + for rule_name, res in process_capabilities.thread_matches.items(): all_thread_matches[rule_name].extend(res) - for rule_name, res in call_matches.items(): + for rule_name, res in process_capabilities.call_matches.items(): all_call_matches[rule_name].extend(res) pbar.advance(task) @@ -177,8 +192,8 @@ def find_dynamic_capabilities( rule = ruleset[rule_name] capa.engine.index_rule_matches(process_and_lower_features, rule, locations) - all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, process_and_lower_features) - feature_counts.file = feature_count + all_file_capabilities = find_file_capabilities(ruleset, extractor, process_and_lower_features) + feature_counts.file = all_file_capabilities.feature_count matches = dict( itertools.chain( @@ -187,13 +202,8 @@ def find_dynamic_capabilities( # and we can merge the dictionaries naively. all_thread_matches.items(), all_process_matches.items(), - all_call_matches.items(), - all_file_matches.items(), + all_file_capabilities.matches.items(), ) ) - meta = { - "feature_counts": feature_counts, - } - - return matches, meta + return Capabilities(matches, feature_counts) diff --git a/capa/capabilities/static.py b/capa/capabilities/static.py index b39b7b2eb..d485aa48c 100644 --- a/capa/capabilities/static.py +++ b/capa/capabilities/static.py @@ -17,7 +17,7 @@ import logging import itertools import collections -from typing import Any +from dataclasses import dataclass import capa.perf import capa.helpers @@ -25,19 +25,23 @@ import capa.render.result_document as rdoc from capa.rules import Scope, RuleSet from capa.engine import FeatureSet, MatchResults -from capa.capabilities.common import find_file_capabilities +from capa.capabilities.common import Capabilities, find_file_capabilities from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, StaticFeatureExtractor logger = logging.getLogger(__name__) +@dataclass +class InstructionCapabilities: + features: FeatureSet + matches: MatchResults + + def find_instruction_capabilities( ruleset: RuleSet, extractor: StaticFeatureExtractor, f: FunctionHandle, bb: BBHandle, insn: InsnHandle -) -> tuple[FeatureSet, MatchResults]: +) -> InstructionCapabilities: """ find matches for the given rules for the given instruction. - - returns: tuple containing (features for instruction, match results for instruction) """ # all features found for the instruction. features: FeatureSet = collections.defaultdict(set) @@ -55,16 +59,21 @@ def find_instruction_capabilities( for addr, _ in res: capa.engine.index_rule_matches(features, rule, [addr]) - return features, matches + return InstructionCapabilities(features, matches) + + +@dataclass +class BasicBlockCapabilities: + features: FeatureSet + basic_block_matches: MatchResults + instruction_matches: MatchResults def find_basic_block_capabilities( ruleset: RuleSet, extractor: StaticFeatureExtractor, f: FunctionHandle, bb: BBHandle -) -> tuple[FeatureSet, MatchResults, MatchResults]: +) -> BasicBlockCapabilities: """ find matches for the given rules within the given basic block. - - returns: tuple containing (features for basic block, match results for basic block, match results for instructions) """ # all features found within this basic block, # includes features found within instructions. @@ -75,11 +84,11 @@ def find_basic_block_capabilities( insn_matches: MatchResults = collections.defaultdict(list) for insn in extractor.get_instructions(f, bb): - ifeatures, imatches = find_instruction_capabilities(ruleset, extractor, f, bb, insn) - for feature, vas in ifeatures.items(): + instruction_capabilities = find_instruction_capabilities(ruleset, extractor, f, bb, insn) + for feature, vas in instruction_capabilities.features.items(): features[feature].update(vas) - for rule_name, res in imatches.items(): + for rule_name, res in instruction_capabilities.matches.items(): insn_matches[rule_name].extend(res) for feature, va in itertools.chain( @@ -95,16 +104,20 @@ def find_basic_block_capabilities( for va, _ in res: capa.engine.index_rule_matches(features, rule, [va]) - return features, matches, insn_matches + return BasicBlockCapabilities(features, matches, insn_matches) + + +@dataclass +class CodeCapabilities: + function_matches: MatchResults + basic_block_matches: MatchResults + instruction_matches: MatchResults + feature_count: int -def find_code_capabilities( - ruleset: RuleSet, extractor: StaticFeatureExtractor, fh: FunctionHandle -) -> tuple[MatchResults, MatchResults, MatchResults, int]: +def find_code_capabilities(ruleset: RuleSet, extractor: StaticFeatureExtractor, fh: FunctionHandle) -> CodeCapabilities: """ find matches for the given rules within the given function. - - returns: tuple containing (match results for function, match results for basic blocks, match results for instructions, number of features) """ # all features found within this function, # includes features found within basic blocks (and instructions). @@ -119,26 +132,26 @@ def find_code_capabilities( insn_matches: MatchResults = collections.defaultdict(list) for bb in extractor.get_basic_blocks(fh): - features, bmatches, imatches = find_basic_block_capabilities(ruleset, extractor, fh, bb) - for feature, vas in features.items(): + basic_block_capabilities = find_basic_block_capabilities(ruleset, extractor, fh, bb) + for feature, vas in basic_block_capabilities.features.items(): function_features[feature].update(vas) - for rule_name, res in bmatches.items(): + for rule_name, res in basic_block_capabilities.basic_block_matches.items(): bb_matches[rule_name].extend(res) - for rule_name, res in imatches.items(): + for rule_name, res in basic_block_capabilities.instruction_matches.items(): insn_matches[rule_name].extend(res) for feature, va in itertools.chain(extractor.extract_function_features(fh), extractor.extract_global_features()): function_features[feature].add(va) _, function_matches = ruleset.match(Scope.FUNCTION, function_features, fh.address) - return function_matches, bb_matches, insn_matches, len(function_features) + return CodeCapabilities(function_matches, bb_matches, insn_matches, len(function_features)) def find_static_capabilities( ruleset: RuleSet, extractor: StaticFeatureExtractor, disable_progress=None -) -> tuple[MatchResults, Any]: +) -> Capabilities: all_function_matches: MatchResults = collections.defaultdict(list) all_bb_matches: MatchResults = collections.defaultdict(list) all_insn_matches: MatchResults = collections.defaultdict(list) @@ -172,30 +185,36 @@ def find_static_capabilities( pbar.advance(task) continue - function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f) + code_capabilities = find_code_capabilities(ruleset, extractor, f) feature_counts.functions += ( - rdoc.FunctionFeatureCount(address=frz.Address.from_capa(f.address), count=feature_count), + rdoc.FunctionFeatureCount( + address=frz.Address.from_capa(f.address), count=code_capabilities.feature_count + ), ) t1 = time.time() match_count = 0 - for name, matches_ in itertools.chain(function_matches.items(), bb_matches.items(), insn_matches.items()): + for name, matches_ in itertools.chain( + code_capabilities.function_matches.items(), + code_capabilities.basic_block_matches.items(), + code_capabilities.instruction_matches.items(), + ): if not ruleset.rules[name].is_subscope_rule(): match_count += len(matches_) logger.debug( "analyzed function 0x%x and extracted %d features, %d matches in %0.02fs", f.address, - feature_count, + code_capabilities.feature_count, match_count, t1 - t0, ) - for rule_name, res in function_matches.items(): + for rule_name, res in code_capabilities.function_matches.items(): all_function_matches[rule_name].extend(res) - for rule_name, res in bb_matches.items(): + for rule_name, res in code_capabilities.basic_block_matches.items(): all_bb_matches[rule_name].extend(res) - for rule_name, res in insn_matches.items(): + for rule_name, res in code_capabilities.instruction_matches.items(): all_insn_matches[rule_name].extend(res) pbar.advance(task) @@ -210,8 +229,8 @@ def find_static_capabilities( rule = ruleset[rule_name] capa.engine.index_rule_matches(function_and_lower_features, rule, locations) - all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, function_and_lower_features) - feature_counts.file = feature_count + all_file_capabilities = find_file_capabilities(ruleset, extractor, function_and_lower_features) + feature_counts.file = all_file_capabilities.feature_count matches: MatchResults = dict( itertools.chain( @@ -221,13 +240,8 @@ def find_static_capabilities( all_insn_matches.items(), all_bb_matches.items(), all_function_matches.items(), - all_file_matches.items(), + all_file_capabilities.matches.items(), ) ) - meta = { - "feature_counts": feature_counts, - "library_functions": library_functions, - } - - return matches, meta + return Capabilities(matches, feature_counts, library_functions) diff --git a/capa/ghidra/capa_explorer.py b/capa/ghidra/capa_explorer.py index 5bac2e316..b07b34ba8 100644 --- a/capa/ghidra/capa_explorer.py +++ b/capa/ghidra/capa_explorer.py @@ -245,13 +245,13 @@ def get_capabilities(): meta = capa.ghidra.helpers.collect_metadata([rules_path]) extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, True) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, True) - if capa.capabilities.common.has_file_limitation(rules, capabilities, is_standalone=False): + if capa.capabilities.common.has_file_limitation(rules, capabilities.matches, is_standalone=False): popup("capa explorer encountered warnings during analysis. Please check the console output for more information.") # type: ignore [name-defined] # noqa: F821 logger.info("capa encountered warnings during analysis") - return capa.render.json.render(meta, rules, capabilities) + return capa.render.json.render(meta, rules, capabilities.matches) def get_locations(match_dict): diff --git a/capa/ghidra/capa_ghidra.py b/capa/ghidra/capa_ghidra.py index 6f47d98c0..8d095347d 100644 --- a/capa/ghidra/capa_ghidra.py +++ b/capa/ghidra/capa_ghidra.py @@ -81,23 +81,23 @@ def run_headless(): meta = capa.ghidra.helpers.collect_metadata([rules_path]) extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, False) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, False) - meta.analysis.feature_counts = counts["feature_counts"] - meta.analysis.library_functions = counts["library_functions"] - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) - if capa.capabilities.common.has_file_limitation(rules, capabilities, is_standalone=True): + if capa.capabilities.common.has_file_limitation(rules, capabilities.matches, is_standalone=True): logger.info("capa encountered warnings during analysis") if args.json: - print(capa.render.json.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.json.render(meta, rules, capabilities.matches)) # noqa: T201 elif args.vverbose: - print(capa.render.vverbose.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.vverbose.render(meta, rules, capabilities.matches)) # noqa: T201 elif args.verbose: - print(capa.render.verbose.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.verbose.render(meta, rules, capabilities.matches)) # noqa: T201 else: - print(capa.render.default.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.default.render(meta, rules, capabilities.matches)) # noqa: T201 return 0 @@ -131,21 +131,21 @@ def run_ui(): meta = capa.ghidra.helpers.collect_metadata([rules_path]) extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, True) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, True) - meta.analysis.feature_counts = counts["feature_counts"] - meta.analysis.library_functions = counts["library_functions"] - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) - if capa.capabilities.common.has_file_limitation(rules, capabilities, is_standalone=False): + if capa.capabilities.common.has_file_limitation(rules, capabilities.matches, is_standalone=False): logger.info("capa encountered warnings during analysis") if verbose == "vverbose": - print(capa.render.vverbose.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.vverbose.render(meta, rules, capabilities.matches)) # noqa: T201 elif verbose == "verbose": - print(capa.render.verbose.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.verbose.render(meta, rules, capabilities.matches)) # noqa: T201 else: - print(capa.render.default.render(meta, rules, capabilities)) # noqa: T201 + print(capa.render.default.render(meta, rules, capabilities.matches)) # noqa: T201 return 0 diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 2cb65de95..d82f76db5 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -776,13 +776,15 @@ def slot_progress_feature_extraction(text): try: meta = capa.ida.helpers.collect_metadata([Path(settings.user[CAPA_SETTINGS_RULE_PATH])]) - capabilities, counts = capa.capabilities.common.find_capabilities( + capabilities = capa.capabilities.common.find_capabilities( ruleset, self.feature_extractor, disable_progress=True ) - meta.analysis.feature_counts = counts["feature_counts"] - meta.analysis.library_functions = counts["library_functions"] - meta.analysis.layout = capa.loader.compute_layout(ruleset, self.feature_extractor, capabilities) + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions + meta.analysis.layout = capa.loader.compute_layout( + ruleset, self.feature_extractor, capabilities.matches + ) except UserCancelledError: logger.info("User cancelled analysis.") return False @@ -818,7 +820,7 @@ def slot_progress_feature_extraction(text): capa.ida.helpers.inform_user_ida_ui("capa encountered file type warnings during analysis") - if capa.capabilities.common.has_file_limitation(ruleset, capabilities, is_standalone=False): + if capa.capabilities.common.has_file_limitation(ruleset, capabilities.matches, is_standalone=False): capa.ida.helpers.inform_user_ida_ui("capa encountered file limitation warnings during analysis") except Exception as e: logger.exception("Failed to check for file limitations (error: %s)", e) @@ -832,7 +834,7 @@ def slot_progress_feature_extraction(text): try: self.resdoc_cache = capa.render.result_document.ResultDocument.from_capa( - meta, ruleset, capabilities + meta, ruleset, capabilities.matches ) except Exception as e: logger.exception("Failed to collect results (error: %s)", e) diff --git a/capa/loader.py b/capa/loader.py index a8a7db9f3..ec0295ac8 100644 --- a/capa/loader.py +++ b/capa/loader.py @@ -59,6 +59,7 @@ FORMAT_BINEXPORT2, ) from capa.features.address import Address +from capa.capabilities.common import Capabilities from capa.features.extractors.base_extractor import ( SampleHashes, FeatureExtractor, @@ -450,7 +451,7 @@ def get_signatures(sigs_path: Path) -> list[Path]: return paths -def get_sample_analysis(format_, arch, os_, extractor, rules_path, counts): +def get_sample_analysis(format_, arch, os_, extractor, rules_path, feature_counts, library_functions): if isinstance(extractor, StaticFeatureExtractor): return rdoc.StaticAnalysis( format=format_, @@ -466,8 +467,8 @@ def get_sample_analysis(format_, arch, os_, extractor, rules_path, counts): # # "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... } ), - feature_counts=counts["feature_counts"], - library_functions=counts["library_functions"], + feature_counts=feature_counts, + library_functions=library_functions, ) elif isinstance(extractor, DynamicFeatureExtractor): return rdoc.DynamicAnalysis( @@ -479,7 +480,7 @@ def get_sample_analysis(format_, arch, os_, extractor, rules_path, counts): layout=rdoc.DynamicLayout( processes=(), ), - feature_counts=counts["feature_counts"], + feature_counts=feature_counts, ) else: raise ValueError("invalid extractor type") @@ -492,7 +493,7 @@ def collect_metadata( os_: str, rules_path: list[Path], extractor: FeatureExtractor, - counts: dict, + capabilities: Capabilities, ) -> rdoc.Metadata: # if it's a binary sample we hash it, if it's a report # we fetch the hashes from the report @@ -535,7 +536,8 @@ def collect_metadata( os_, extractor, rules, - counts, + capabilities.feature_counts, + capabilities.library_functions, ), ) diff --git a/capa/main.py b/capa/main.py index c8b24595c..f962e5708 100644 --- a/capa/main.py +++ b/capa/main.py @@ -22,7 +22,7 @@ import textwrap import contextlib from types import TracebackType -from typing import Any, Optional, TypedDict +from typing import Optional, TypedDict from pathlib import Path import colorama @@ -47,7 +47,6 @@ import capa.render.result_document as rdoc import capa.features.extractors.common from capa.rules import RuleSet -from capa.engine import MatchResults from capa.loader import ( BACKEND_IDA, BACKEND_VIV, @@ -100,7 +99,7 @@ FORMAT_BINJA_DB, FORMAT_BINEXPORT2, ) -from capa.capabilities.common import find_capabilities, has_file_limitation, find_file_capabilities +from capa.capabilities.common import Capabilities, find_capabilities, has_file_limitation, find_file_capabilities from capa.features.extractors.base_extractor import ( ProcessFilter, FunctionFilter, @@ -761,7 +760,7 @@ def find_file_limitations_from_cli(args, rules: RuleSet, file_extractors: list[F found_file_limitation = False for file_extractor in file_extractors: try: - pure_file_capabilities, _ = find_file_capabilities(rules, file_extractor, {}) + pure_file_capabilities = find_file_capabilities(rules, file_extractor, {}) except PEFormatError as e: logger.error("Input file '%s' is not a valid PE file: %s", args.input_file, str(e)) raise ShouldExitError(E_CORRUPT_FILE) from e @@ -771,7 +770,7 @@ def find_file_limitations_from_cli(args, rules: RuleSet, file_extractors: list[F # file limitations that rely on non-file scope won't be detected here. # nor on FunctionName features, because pefile doesn't support this. - found_file_limitation = has_file_limitation(rules, pure_file_capabilities) + found_file_limitation = has_file_limitation(rules, pure_file_capabilities.matches) if found_file_limitation: # bail if capa encountered file limitation e.g. a packed binary # do show the output in verbose mode, though. @@ -974,8 +973,7 @@ def main(argv: Optional[list[str]] = None): return e.status_code meta: rdoc.Metadata - capabilities: MatchResults - counts: dict[str, Any] + capabilities: Capabilities if input_format == FORMAT_RESULT: # result document directly parses into meta, capabilities @@ -997,10 +995,12 @@ def main(argv: Optional[list[str]] = None): except ShouldExitError as e: return e.status_code - capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet) + capabilities = find_capabilities(rules, extractor, disable_progress=args.quiet) - meta = capa.loader.collect_metadata(argv, args.input_file, input_format, os_, args.rules, extractor, counts) - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta = capa.loader.collect_metadata( + argv, args.input_file, input_format, os_, args.rules, extractor, capabilities + ) + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) if isinstance(extractor, StaticFeatureExtractor) and found_file_limitation: # bail if capa's static feature extractor encountered file limitation e.g. a packed binary @@ -1009,13 +1009,13 @@ def main(argv: Optional[list[str]] = None): return E_FILE_LIMITATION if args.json: - print(capa.render.json.render(meta, rules, capabilities)) + print(capa.render.json.render(meta, rules, capabilities.matches)) elif args.vverbose: - print(capa.render.vverbose.render(meta, rules, capabilities)) + print(capa.render.vverbose.render(meta, rules, capabilities.matches)) elif args.verbose: - print(capa.render.verbose.render(meta, rules, capabilities)) + print(capa.render.verbose.render(meta, rules, capabilities.matches)) else: - print(capa.render.default.render(meta, rules, capabilities)) + print(capa.render.default.render(meta, rules, capabilities.matches)) colorama.deinit() logger.debug("done.") @@ -1051,16 +1051,16 @@ def ida_main(): meta = capa.ida.helpers.collect_metadata([rules_path]) - capabilities, counts = find_capabilities(rules, capa.features.extractors.ida.extractor.IdaFeatureExtractor()) + capabilities = find_capabilities(rules, capa.features.extractors.ida.extractor.IdaFeatureExtractor()) - meta.analysis.feature_counts = counts["feature_counts"] - meta.analysis.library_functions = counts["library_functions"] + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions - if has_file_limitation(rules, capabilities, is_standalone=False): + if has_file_limitation(rules, capabilities.matches, is_standalone=False): capa.ida.helpers.inform_user_ida_ui("capa encountered warnings during analysis") colorama.init(strip=True) - print(capa.render.default.render(meta, rules, capabilities)) + print(capa.render.default.render(meta, rules, capabilities.matches)) def ghidra_main(): @@ -1085,19 +1085,19 @@ def ghidra_main(): meta = capa.ghidra.helpers.collect_metadata([rules_path]) - capabilities, counts = find_capabilities( + capabilities = find_capabilities( rules, capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor(), not capa.ghidra.helpers.is_running_headless(), ) - meta.analysis.feature_counts = counts["feature_counts"] - meta.analysis.library_functions = counts["library_functions"] + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions - if has_file_limitation(rules, capabilities, is_standalone=False): + if has_file_limitation(rules, capabilities.matches, is_standalone=False): logger.info("capa encountered warnings during analysis") - print(capa.render.default.render(meta, rules, capabilities)) + print(capa.render.default.render(meta, rules, capabilities.matches)) if __name__ == "__main__": diff --git a/capa/render/result_document.py b/capa/render/result_document.py index ce3677eea..8bf6a948e 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -15,7 +15,7 @@ import datetime import collections from enum import Enum -from typing import Union, Literal, Optional, TypeAlias +from typing import TYPE_CHECKING, Union, Literal, Optional, TypeAlias from pathlib import Path from pydantic import Field, BaseModel, ConfigDict @@ -30,6 +30,9 @@ from capa.engine import MatchResults from capa.helpers import assert_never, load_json_from_path +if TYPE_CHECKING: + from capa.capabilities.common import Capabilities + class FrozenModel(BaseModel): model_config = ConfigDict(frozen=True, extra="forbid") @@ -674,8 +677,10 @@ def from_capa(cls, meta: Metadata, rules: RuleSet, capabilities: MatchResults) - return ResultDocument(meta=meta, rules=rule_matches) - def to_capa(self) -> tuple[Metadata, dict]: - capabilities: dict[str, list[tuple[capa.features.address.Address, capa.features.common.Result]]] = ( + def to_capa(self) -> tuple[Metadata, "Capabilities"]: + from capa.capabilities.common import Capabilities + + matches: dict[str, list[tuple[capa.features.address.Address, capa.features.common.Result]]] = ( collections.defaultdict(list) ) @@ -688,7 +693,14 @@ def to_capa(self) -> tuple[Metadata, dict]: for addr, match in rule_match.matches: result: capa.engine.Result = match.to_capa(rules_by_name) - capabilities[rule_name].append((addr.to_capa(), result)) + matches[rule_name].append((addr.to_capa(), result)) + + if isinstance(self.meta.analysis, StaticAnalysis): + capabilities = Capabilities( + matches, self.meta.analysis.feature_counts, self.meta.analysis.library_functions + ) + elif isinstance(self.meta.analysis, DynamicAnalysis): + capabilities = Capabilities(matches, self.meta.analysis.feature_counts) return self.meta, capabilities diff --git a/rules b/rules index b4e0c8cdf..6cb2ec010 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit b4e0c8cdf89a9d1fc41c2fc151bbb9c985d59a9e +Subproject commit 6cb2ec010b4d0679612295301d0b3e6336da4f33 diff --git a/scripts/bulk-process.py b/scripts/bulk-process.py index f7324d7ee..768a551b2 100644 --- a/scripts/bulk-process.py +++ b/scripts/bulk-process.py @@ -146,12 +146,12 @@ def get_capa_results(args): "error": f"unexpected error: {e}", } - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True) - meta = capa.loader.collect_metadata(argv, args.input_file, format_, os_, [], extractor, counts) - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta = capa.loader.collect_metadata(argv, args.input_file, format_, os_, [], extractor, capabilities) + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) - doc = rd.ResultDocument.from_capa(meta, rules, capabilities) + doc = rd.ResultDocument.from_capa(meta, rules, capabilities.matches) return {"path": input_file, "status": "ok", "ok": doc.model_dump()} diff --git a/scripts/capa-as-library.py b/scripts/capa-as-library.py index 3d556e21a..1d935a8e8 100644 --- a/scripts/capa-as-library.py +++ b/scripts/capa-as-library.py @@ -184,25 +184,25 @@ def capa_details(rules_path: Path, input_file: Path, output_format="dictionary") extractor = capa.loader.get_extractor( input_file, FORMAT_AUTO, OS_AUTO, capa.main.BACKEND_VIV, [], should_save_workspace=False, disable_progress=True ) - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, disable_progress=True) # collect metadata (used only to make rendering more complete) - meta = capa.loader.collect_metadata([], input_file, FORMAT_AUTO, OS_AUTO, [rules_path], extractor, counts) - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta = capa.loader.collect_metadata([], input_file, FORMAT_AUTO, OS_AUTO, [rules_path], extractor, capabilities) + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) capa_output: Any = False if output_format == "dictionary": # ...as python dictionary, simplified as textable but in dictionary - doc = rd.ResultDocument.from_capa(meta, rules, capabilities) + doc = rd.ResultDocument.from_capa(meta, rules, capabilities.matches) capa_output = render_dictionary(doc) elif output_format == "json": # render results # ...as json - capa_output = json.loads(capa.render.json.render(meta, rules, capabilities)) + capa_output = json.loads(capa.render.json.render(meta, rules, capabilities.matches)) elif output_format == "texttable": # ...as human readable text table - capa_output = capa.render.default.render(meta, rules, capabilities) + capa_output = capa.render.default.render(meta, rules, capabilities.matches) return capa_output diff --git a/scripts/detect-binexport2-capabilities.py b/scripts/detect-binexport2-capabilities.py index 6cf47789f..0a170d299 100644 --- a/scripts/detect-binexport2-capabilities.py +++ b/scripts/detect-binexport2-capabilities.py @@ -100,12 +100,12 @@ def main(argv=None): except capa.main.ShouldExitError as e: return e.status_code - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor) - meta = capa.loader.collect_metadata(argv, args.input_file, input_format, os_, args.rules, extractor, counts) - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta = capa.loader.collect_metadata(argv, args.input_file, input_format, os_, args.rules, extractor, capabilities) + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) - doc = rd.ResultDocument.from_capa(meta, rules, capabilities) + doc = rd.ResultDocument.from_capa(meta, rules, capabilities.matches) pb = capa.render.proto.doc_to_pb2(doc) sys.stdout.buffer.write(pb.SerializeToString(deterministic=True)) diff --git a/scripts/import-to-ida.py b/scripts/import-to-ida.py index d3eb92c7c..89ba19454 100644 --- a/scripts/import-to-ida.py +++ b/scripts/import-to-ida.py @@ -91,7 +91,7 @@ def main(): return -2 rows = [] - for name in capabilities.keys(): + for name in capabilities.matches.keys(): rule = result_doc.rules[name] if rule.meta.lib: continue diff --git a/scripts/lint.py b/scripts/lint.py index 6aad72cd3..8f8627b56 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -357,10 +357,10 @@ def get_sample_capabilities(ctx: Context, path: Path) -> set[str]: disable_progress=True, ) - capabilities, _ = capa.capabilities.common.find_capabilities(ctx.rules, extractor, disable_progress=True) + capabilities = capa.capabilities.common.find_capabilities(ctx.rules, extractor, disable_progress=True) # mypy doesn't seem to be happy with the MatchResults type alias & set(...keys())? # so we ignore a few types here. - capabilities = set(capabilities.keys()) # type: ignore + capabilities = set(capabilities.matches.keys()) # type: ignore assert isinstance(capabilities, set) logger.debug("computed results: %s: %d capabilities", nice_path, len(capabilities)) diff --git a/scripts/show-capabilities-by-function.py b/scripts/show-capabilities-by-function.py index bf6261257..5ae5c5d08 100644 --- a/scripts/show-capabilities-by-function.py +++ b/scripts/show-capabilities-by-function.py @@ -155,18 +155,18 @@ def main(argv=None): except capa.main.ShouldExitError as e: return e.status_code - capabilities, counts = capa.capabilities.common.find_capabilities(rules, extractor) + capabilities = capa.capabilities.common.find_capabilities(rules, extractor) - meta = capa.loader.collect_metadata(argv, args.input_file, input_format, os_, args.rules, extractor, counts) - meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities) + meta = capa.loader.collect_metadata(argv, args.input_file, input_format, os_, args.rules, extractor, capabilities) + meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches) - if capa.capabilities.common.has_file_limitation(rules, capabilities): + if capa.capabilities.common.has_file_limitation(rules, capabilities.matches): # bail if capa encountered file limitation e.g. a packed binary # do show the output in verbose mode, though. if not (args.verbose or args.vverbose or args.json): return capa.main.E_FILE_LIMITATION - doc = rd.ResultDocument.from_capa(meta, rules, capabilities) + doc = rd.ResultDocument.from_capa(meta, rules, capabilities.matches) print(render_matches_by_function(doc)) colorama.deinit() diff --git a/tests/data b/tests/data index 6cf615dd0..ea10c47b3 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 6cf615dd014de8b1979d2619d7f832b4133bfa11 +Subproject commit ea10c47b32c00441529cea69d9ad92bf47f11ac9 diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 41abd5606..809173da2 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -82,10 +82,10 @@ def test_match_across_scopes_file_function(z9324d_extractor): ), ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "install service" in capabilities - assert ".text section" in capabilities - assert ".text section and install service" in capabilities + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "install service" in capabilities.matches + assert ".text section" in capabilities.matches + assert ".text section and install service" in capabilities.matches def test_match_across_scopes(z9324d_extractor): @@ -150,10 +150,10 @@ def test_match_across_scopes(z9324d_extractor): ), ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "tight loop" in capabilities - assert "kill thread loop" in capabilities - assert "kill thread program" in capabilities + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "tight loop" in capabilities.matches + assert "kill thread loop" in capabilities.matches + assert "kill thread program" in capabilities.matches def test_subscope_bb_rules(z9324d_extractor): @@ -178,8 +178,8 @@ def test_subscope_bb_rules(z9324d_extractor): ] ) # tight loop at 0x403685 - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "test rule" in capabilities + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "test rule" in capabilities.matches def test_match_specific_functions(z9324d_extractor): @@ -205,8 +205,8 @@ def test_match_specific_functions(z9324d_extractor): ] ) extractor = FunctionFilter(z9324d_extractor, {0x4019C0}) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, extractor) - matches = capabilities["receive data"] + capabilities = capa.capabilities.common.find_capabilities(rules, extractor) + matches = capabilities.matches["receive data"] # test that we received only one match assert len(matches) == 1 # and that this match is from the specified function @@ -233,8 +233,8 @@ def test_byte_matching(z9324d_extractor): ) ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "byte match test" in capabilities + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "byte match test" in capabilities.matches def test_com_feature_matching(z395eb_extractor): @@ -259,8 +259,8 @@ def test_com_feature_matching(z395eb_extractor): ) ] ) - capabilities, meta = capa.main.find_capabilities(rules, z395eb_extractor) - assert "initialize IWebBrowser2" in capabilities + capabilities = capa.main.find_capabilities(rules, z395eb_extractor) + assert "initialize IWebBrowser2" in capabilities.matches def test_count_bb(z9324d_extractor): @@ -284,8 +284,8 @@ def test_count_bb(z9324d_extractor): ) ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "count bb" in capabilities + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "count bb" in capabilities.matches def test_instruction_scope(z9324d_extractor): @@ -311,9 +311,9 @@ def test_instruction_scope(z9324d_extractor): ) ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "push 1000" in capabilities - assert 0x4071A4 in {result[0] for result in capabilities["push 1000"]} + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "push 1000" in capabilities.matches + assert 0x4071A4 in {result[0] for result in capabilities.matches["push 1000"]} def test_instruction_subscope(z9324d_extractor): @@ -343,6 +343,6 @@ def test_instruction_subscope(z9324d_extractor): ) ] ) - capabilities, meta = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) - assert "push 1000 on i386" in capabilities - assert 0x406F60 in {result[0] for result in capabilities["push 1000 on i386"]} + capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) + assert "push 1000 on i386" in capabilities.matches + assert 0x406F60 in {result[0] for result in capabilities.matches["push 1000 on i386"]} diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index eca671574..9ed84fa4c 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -125,8 +125,8 @@ def test_null_feature_extractor(): ), ] ) - capabilities, _ = capa.main.find_capabilities(rules, EXTRACTOR) - assert "create file" in capabilities + capabilities = capa.main.find_capabilities(rules, EXTRACTOR) + assert "create file" in capabilities.matches def compare_extractors(a: DynamicFeatureExtractor, b: DynamicFeatureExtractor): diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index fe1aafa03..e9f58bc42 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -107,8 +107,8 @@ def test_null_feature_extractor(): ), ] ) - capabilities, meta = capa.main.find_capabilities(rules, EXTRACTOR) - assert "xor loop" in capabilities + capabilities = capa.main.find_capabilities(rules, EXTRACTOR) + assert "xor loop" in capabilities.matches def compare_extractors(a, b): diff --git a/tests/test_result_document.py b/tests/test_result_document.py index 0c1d842fd..7775c0330 100644 --- a/tests/test_result_document.py +++ b/tests/test_result_document.py @@ -21,6 +21,7 @@ import capa.engine as ceng import capa.render.result_document as rdoc import capa.features.freeze.features as frzf +from capa.capabilities.common import Capabilities def test_optional_node_from_capa(): @@ -289,4 +290,4 @@ def test_rdoc_to_capa(): meta, capabilites = rd.to_capa() assert isinstance(meta, rdoc.Metadata) - assert isinstance(capabilites, dict) + assert isinstance(capabilites, Capabilities) From b06fea130ca692acee09cc5fbcf88c1a1222da87 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Dec 2024 13:20:22 +0000 Subject: [PATCH 04/17] dynamic: add sequence scope addresses discussion in https://github.com/mandiant/capa-rules/discussions/951 pep8 sequence: add test showing multiple sequences overlapping a single event --- capa/capabilities/dynamic.py | 36 +- capa/features/extractors/base_extractor.py | 12 + capa/render/proto/__init__.py | 4 + capa/render/proto/capa.proto | 1 + capa/render/proto/capa_pb2.py | 306 ++++++------ capa/render/proto/capa_pb2.pyi | 512 +++++++++++---------- capa/render/verbose.py | 12 +- capa/render/vverbose.py | 33 ++ capa/rules/__init__.py | 28 +- scripts/lint.py | 1 + tests/test_dynamic_sequence_scope.py | 256 +++++++++++ tests/test_proto.py | 1 + 12 files changed, 793 insertions(+), 409 deletions(-) create mode 100644 tests/test_dynamic_sequence_scope.py diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 95aa795d9..cb553e30b 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -35,6 +35,10 @@ class CallCapabilities: matches: MatchResults +# The number of calls that make up a sequence. +SEQUENCE_SIZE = 5 + + def find_call_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle ) -> CallCapabilities: @@ -64,6 +68,7 @@ def find_call_capabilities( class ThreadCapabilities: features: FeatureSet thread_matches: MatchResults + sequence_matches: MatchResults call_matches: MatchResults @@ -81,6 +86,11 @@ def find_thread_capabilities( # might be found at different calls, that's ok. call_matches: MatchResults = collections.defaultdict(list) + # matches found at the sequence scope. + sequence_matches: MatchResults = collections.defaultdict(list) + + sequence: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + for ch in extractor.get_calls(ph, th): call_capabilities = find_call_capabilities(ruleset, extractor, ph, th, ch) for feature, vas in call_capabilities.features.items(): @@ -89,6 +99,16 @@ def find_thread_capabilities( for rule_name, res in call_capabilities.matches.items(): call_matches[rule_name].extend(res) + sequence.append(call_capabilities.features) + sequence_features: FeatureSet = collections.defaultdict(set) + for call in sequence: + for feature, vas in call.items(): + sequence_features[feature].update(vas) + + _, smatches = ruleset.match(Scope.SEQUENCE, sequence_features, ch.address) + for rule_name, res in smatches.items(): + sequence_matches[rule_name].extend(res) + for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()): features[feature].add(va) @@ -100,7 +120,7 @@ def find_thread_capabilities( for va, _ in res: capa.engine.index_rule_matches(features, rule, [va]) - return ThreadCapabilities(features, matches, call_matches) + return ThreadCapabilities(features, matches, sequence_matches, call_matches) @dataclass @@ -125,6 +145,10 @@ def find_process_capabilities( # might be found at different threads, that's ok. thread_matches: MatchResults = collections.defaultdict(list) + # matches found at the sequence scope. + # might be found at different sequences, that's ok. + sequence_matches: MatchResults = collections.defaultdict(list) + # matches found at the call scope. # might be found at different calls, that's ok. call_matches: MatchResults = collections.defaultdict(list) @@ -137,6 +161,9 @@ def find_process_capabilities( for rule_name, res in thread_capabilities.thread_matches.items(): thread_matches[rule_name].extend(res) + for rule_name, res in thread_capabilities.sequence_matches.items(): + sequence_matches[rule_name].extend(res) + for rule_name, res in thread_capabilities.call_matches.items(): call_matches[rule_name].extend(res) @@ -152,6 +179,7 @@ def find_dynamic_capabilities( ) -> Capabilities: all_process_matches: MatchResults = collections.defaultdict(list) all_thread_matches: MatchResults = collections.defaultdict(list) + all_sequence_matches: MatchResults = collections.defaultdict(list) all_call_matches: MatchResults = collections.defaultdict(list) feature_counts = rdoc.DynamicFeatureCounts(file=0, processes=()) @@ -177,6 +205,8 @@ def find_dynamic_capabilities( all_process_matches[rule_name].extend(res) for rule_name, res in process_capabilities.thread_matches.items(): all_thread_matches[rule_name].extend(res) + for rule_name, res in process_capabilities.sequence_matches.items(): + all_sequence_matches[rule_name].extend(res) for rule_name, res in process_capabilities.call_matches.items(): all_call_matches[rule_name].extend(res) @@ -186,7 +216,7 @@ def find_dynamic_capabilities( # mapping from feature (matched rule) to set of addresses at which it matched. process_and_lower_features: FeatureSet = collections.defaultdict(set) for rule_name, results in itertools.chain( - all_process_matches.items(), all_thread_matches.items(), all_call_matches.items() + all_process_matches.items(), all_thread_matches.items(), all_sequence_matches.items(), all_call_matches.items() ): locations = {p[0] for p in results} rule = ruleset[rule_name] @@ -200,6 +230,8 @@ def find_dynamic_capabilities( # each rule exists in exactly one scope, # so there won't be any overlap among these following MatchResults, # and we can merge the dictionaries naively. + all_call_matches.items(), + all_sequence_matches.items(), all_thread_matches.items(), all_process_matches.items(), all_file_capabilities.matches.items(), diff --git a/capa/features/extractors/base_extractor.py b/capa/features/extractors/base_extractor.py index ba9b905b8..35c9b665c 100644 --- a/capa/features/extractors/base_extractor.py +++ b/capa/features/extractors/base_extractor.py @@ -504,4 +504,16 @@ def filtered_get_processes(self): return new_extractor +def ThreadFilter(extractor: DynamicFeatureExtractor, threads: set) -> DynamicFeatureExtractor: + original_get_threads = extractor.get_threads + + def filtered_get_threads(self, ph: ProcessHandle): + yield from (t for t in original_get_threads(ph) if t.address in threads) + + new_extractor = copy(extractor) + new_extractor.get_threads = MethodType(filtered_get_threads, extractor) # type: ignore + + return new_extractor + + FeatureExtractor: TypeAlias = Union[StaticFeatureExtractor, DynamicFeatureExtractor] diff --git a/capa/render/proto/__init__.py b/capa/render/proto/__init__.py index e72add4d5..2541c944c 100644 --- a/capa/render/proto/__init__.py +++ b/capa/render/proto/__init__.py @@ -163,6 +163,8 @@ def scope_to_pb2(scope: capa.rules.Scope) -> capa_pb2.Scope.ValueType: return capa_pb2.Scope.SCOPE_PROCESS elif scope == capa.rules.Scope.THREAD: return capa_pb2.Scope.SCOPE_THREAD + elif scope == capa.rules.Scope.SEQUENCE: + return capa_pb2.Scope.SCOPE_SEQUENCE elif scope == capa.rules.Scope.CALL: return capa_pb2.Scope.SCOPE_CALL else: @@ -655,6 +657,8 @@ def scope_from_pb2(scope: capa_pb2.Scope.ValueType) -> capa.rules.Scope: return capa.rules.Scope.PROCESS elif scope == capa_pb2.Scope.SCOPE_THREAD: return capa.rules.Scope.THREAD + elif scope == capa_pb2.Scope.SCOPE_SEQUENCE: + return capa.rules.Scope.SEQUENCE elif scope == capa_pb2.Scope.SCOPE_CALL: return capa.rules.Scope.CALL else: diff --git a/capa/render/proto/capa.proto b/capa/render/proto/capa.proto index 2c0964c7b..e824c42e6 100644 --- a/capa/render/proto/capa.proto +++ b/capa/render/proto/capa.proto @@ -378,6 +378,7 @@ enum Scope { SCOPE_PROCESS = 5; SCOPE_THREAD = 6; SCOPE_CALL = 7; + SCOPE_SEQUENCE = 8; } message Scopes { diff --git a/capa/render/proto/capa_pb2.py b/capa/render/proto/capa_pb2.py index 8b55fed4f..35d017934 100644 --- a/capa/render/proto/capa_pb2.py +++ b/capa/render/proto/capa_pb2.py @@ -2,10 +2,10 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: capa/render/proto/capa.proto """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -13,159 +13,159 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63\x61pa/render/proto/capa.proto\x12\rmandiant.capa\"Q\n\nAPIFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03\x61pi\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xb3\x02\n\x07\x41\x64\x64ress\x12(\n\x04type\x18\x01 \x01(\x0e\x32\x1a.mandiant.capa.AddressType\x12#\n\x01v\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.IntegerH\x00\x12\x33\n\x0ctoken_offset\x18\x03 \x01(\x0b\x32\x1b.mandiant.capa.Token_OffsetH\x00\x12+\n\x08ppid_pid\x18\x04 \x01(\x0b\x32\x17.mandiant.capa.Ppid_PidH\x00\x12\x33\n\x0cppid_pid_tid\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.Ppid_Pid_TidH\x00\x12\x39\n\x0fppid_pid_tid_id\x18\x06 \x01(\x0b\x32\x1e.mandiant.capa.Ppid_Pid_Tid_IdH\x00\x42\x07\n\x05value\"\x9c\x02\n\x08\x41nalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12%\n\x06layout\x18\x07 \x01(\x0b\x32\x15.mandiant.capa.Layout\x12\x34\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.FeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"S\n\x0b\x41rchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"`\n\nAttackSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x0e\n\x06tactic\x18\x02 \x01(\t\x12\x11\n\ttechnique\x18\x03 \x01(\t\x12\x14\n\x0csubtechnique\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"K\n\x11\x42\x61sicBlockFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\";\n\x10\x42\x61sicBlockLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\"U\n\x0c\x42ytesFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x62ytes\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"g\n\x15\x43haracteristicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\x0e\x63haracteristic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\x0c\x43lassFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x63lass_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"K\n\x11\x43ompoundStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xc8\x01\n\x0f\x44ynamicAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x06layout\x18\x06 \x01(\x0b\x32\x1c.mandiant.capa.DynamicLayout\x12;\n\x0e\x66\x65\x61ture_counts\x18\x07 \x01(\x0b\x32#.mandiant.capa.DynamicFeatureCounts\"[\n\x14\x44ynamicFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x35\n\tprocesses\x18\x02 \x03(\x0b\x32\".mandiant.capa.ProcessFeatureCount\"@\n\rDynamicLayout\x12/\n\tprocesses\x18\x01 \x03(\x0b\x32\x1c.mandiant.capa.ProcessLayout\"W\n\rExportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x65xport\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\rFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"\xb9\t\n\x0b\x46\x65\x61tureNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x02os\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.OSFeatureH\x00\x12*\n\x04\x61rch\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.ArchFeatureH\x00\x12.\n\x06\x66ormat\x18\x04 \x01(\x0b\x32\x1c.mandiant.capa.FormatFeatureH\x00\x12,\n\x05match\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.MatchFeatureH\x00\x12>\n\x0e\x63haracteristic\x18\x06 \x01(\x0b\x32$.mandiant.capa.CharacteristicFeatureH\x00\x12.\n\x06\x65xport\x18\x07 \x01(\x0b\x32\x1c.mandiant.capa.ExportFeatureH\x00\x12/\n\x07import_\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.ImportFeatureH\x00\x12\x30\n\x07section\x18\t \x01(\x0b\x32\x1d.mandiant.capa.SectionFeatureH\x00\x12;\n\rfunction_name\x18\n \x01(\x0b\x32\".mandiant.capa.FunctionNameFeatureH\x00\x12\x34\n\tsubstring\x18\x0b \x01(\x0b\x32\x1f.mandiant.capa.SubstringFeatureH\x00\x12,\n\x05regex\x18\x0c \x01(\x0b\x32\x1b.mandiant.capa.RegexFeatureH\x00\x12.\n\x06string\x18\r \x01(\x0b\x32\x1c.mandiant.capa.StringFeatureH\x00\x12-\n\x06\x63lass_\x18\x0e \x01(\x0b\x32\x1b.mandiant.capa.ClassFeatureH\x00\x12\x34\n\tnamespace\x18\x0f \x01(\x0b\x32\x1f.mandiant.capa.NamespaceFeatureH\x00\x12(\n\x03\x61pi\x18\x10 \x01(\x0b\x32\x19.mandiant.capa.APIFeatureH\x00\x12\x33\n\tproperty_\x18\x11 \x01(\x0b\x32\x1e.mandiant.capa.PropertyFeatureH\x00\x12.\n\x06number\x18\x12 \x01(\x0b\x32\x1c.mandiant.capa.NumberFeatureH\x00\x12,\n\x05\x62ytes\x18\x13 \x01(\x0b\x32\x1b.mandiant.capa.BytesFeatureH\x00\x12.\n\x06offset\x18\x14 \x01(\x0b\x32\x1c.mandiant.capa.OffsetFeatureH\x00\x12\x32\n\x08mnemonic\x18\x15 \x01(\x0b\x32\x1e.mandiant.capa.MnemonicFeatureH\x00\x12=\n\x0eoperand_number\x18\x16 \x01(\x0b\x32#.mandiant.capa.OperandNumberFeatureH\x00\x12=\n\x0eoperand_offset\x18\x17 \x01(\x0b\x32#.mandiant.capa.OperandOffsetFeatureH\x00\x12\x37\n\x0b\x62\x61sic_block\x18\x18 \x01(\x0b\x32 .mandiant.capa.BasicBlockFeatureH\x00\x42\t\n\x07\x66\x65\x61ture\"W\n\rFormatFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"N\n\x14\x46unctionFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"x\n\x0e\x46unctionLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12=\n\x14matched_basic_blocks\x18\x02 \x03(\x0b\x32\x1f.mandiant.capa.BasicBlockLayout\"d\n\x13\x46unctionNameFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rfunction_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"X\n\rImportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07import_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\":\n\x06Layout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"H\n\x0fLibraryFunction\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"Y\n\x07MBCSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x11\n\tobjective\x18\x02 \x01(\t\x12\x10\n\x08\x62\x65havior\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"\x9a\x01\n\x0cMaecMetadata\x12\x1b\n\x13\x61nalysis_conclusion\x18\x01 \x01(\t\x12\x1e\n\x16\x61nalysis_conclusion_ov\x18\x02 \x01(\t\x12\x16\n\x0emalware_family\x18\x03 \x01(\t\x12\x18\n\x10malware_category\x18\x04 \x01(\t\x12\x1b\n\x13malware_category_ov\x18\x05 \x01(\t\"\xd6\x02\n\x05Match\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x31\n\tstatement\x18\x02 \x01(\x0b\x32\x1c.mandiant.capa.StatementNodeH\x00\x12-\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNodeH\x00\x12&\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x14.mandiant.capa.Match\x12)\n\tlocations\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x08\x63\x61ptures\x18\x07 \x03(\x0b\x32\".mandiant.capa.Match.CapturesEntry\x1aI\n\rCapturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.Addresses:\x02\x38\x01\x42\x06\n\x04node\"U\n\x0cMatchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xbc\x02\n\x08Metadata\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgv\x18\x03 \x03(\t\x12%\n\x06sample\x18\x04 \x01(\x0b\x32\x15.mandiant.capa.Sample\x12-\n\x08\x61nalysis\x18\x05 \x01(\x0b\x32\x17.mandiant.capa.AnalysisB\x02\x18\x01\x12%\n\x06\x66lavor\x18\x06 \x01(\x0e\x32\x15.mandiant.capa.Flavor\x12\x38\n\x0fstatic_analysis\x18\x07 \x01(\x0b\x32\x1d.mandiant.capa.StaticAnalysisH\x00\x12:\n\x10\x64ynamic_analysis\x18\x08 \x01(\x0b\x32\x1e.mandiant.capa.DynamicAnalysisH\x00\x42\x0b\n\tanalysis2\"[\n\x0fMnemonicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x10\n\x08mnemonic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10NamespaceFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"n\n\rNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12%\n\x06number\x18\x02 \x01(\x0b\x32\x15.mandiant.capa.Number\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"O\n\tOSFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\n\n\x02os\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"o\n\rOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x06offset\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_number\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_offset\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"M\n\x13ProcessFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"|\n\rProcessLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x0fmatched_threads\x18\x02 \x03(\x0b\x32\x1b.mandiant.capa.ThreadLayout\x12\x0c\n\x04name\x18\x03 \x01(\t\"|\n\x0fPropertyFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tproperty_\x18\x02 \x01(\t\x12\x13\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_accessB\x0e\n\x0c_description\"\x8d\x01\n\x0eRangeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03min\x18\x02 \x01(\x04\x12\x0b\n\x03max\x18\x03 \x01(\x04\x12)\n\x05\x63hild\x18\x04 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNode\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\x0cRegexFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05regex\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xba\x01\n\x0eResultDocument\x12%\n\x04meta\x18\x01 \x01(\x0b\x32\x17.mandiant.capa.Metadata\x12\x37\n\x05rules\x18\x02 \x03(\x0b\x32(.mandiant.capa.ResultDocument.RulesEntry\x1aH\n\nRulesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.mandiant.capa.RuleMatches:\x02\x38\x01\"|\n\x0bRuleMatches\x12)\n\x04meta\x18\x01 \x01(\x0b\x32\x1b.mandiant.capa.RuleMetadata\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x32\n\x07matches\x18\x03 \x03(\x0b\x32!.mandiant.capa.Pair_Address_Match\"\xed\x02\n\x0cRuleMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x0f\n\x07\x61uthors\x18\x03 \x03(\t\x12\'\n\x05scope\x18\x04 \x01(\x0e\x32\x14.mandiant.capa.ScopeB\x02\x18\x01\x12)\n\x06\x61ttack\x18\x05 \x03(\x0b\x32\x19.mandiant.capa.AttackSpec\x12#\n\x03mbc\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.MBCSpec\x12\x12\n\nreferences\x18\x07 \x03(\t\x12\x10\n\x08\x65xamples\x18\x08 \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x0b\n\x03lib\x18\n \x01(\x08\x12)\n\x04maec\x18\x0b \x01(\x0b\x32\x1b.mandiant.capa.MaecMetadata\x12\x18\n\x10is_subscope_rule\x18\x0c \x01(\x08\x12%\n\x06scopes\x18\r \x01(\x0b\x32\x15.mandiant.capa.Scopes\"A\n\x06Sample\x12\x0b\n\x03md5\x18\x01 \x01(\t\x12\x0c\n\x04sha1\x18\x02 \x01(\t\x12\x0e\n\x06sha256\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\"v\n\x06Scopes\x12)\n\x06static\x18\x01 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x00\x88\x01\x01\x12*\n\x07\x64ynamic\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x01\x88\x01\x01\x42\t\n\x07_staticB\n\n\x08_dynamic\"Y\n\x0eSectionFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07section\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\rSomeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xf4\x01\n\rStatementNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12.\n\x05range\x18\x02 \x01(\x0b\x32\x1d.mandiant.capa.RangeStatementH\x00\x12,\n\x04some\x18\x03 \x01(\x0b\x32\x1c.mandiant.capa.SomeStatementH\x00\x12\x34\n\x08subscope\x18\x04 \x01(\x0b\x32 .mandiant.capa.SubscopeStatementH\x00\x12\x34\n\x08\x63ompound\x18\x05 \x01(\x0b\x32 .mandiant.capa.CompoundStatementH\x00\x42\x0b\n\tstatement\"\xae\x02\n\x0eStaticAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12+\n\x06layout\x18\x07 \x01(\x0b\x32\x1b.mandiant.capa.StaticLayout\x12:\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\".mandiant.capa.StaticFeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"[\n\x13StaticFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"@\n\x0cStaticLayout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"W\n\rStringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"p\n\x11SubscopeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12#\n\x05scope\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.Scope\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10SubstringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tsubstring\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"C\n\nCallLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"i\n\x0cThreadLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x30\n\rmatched_calls\x18\x02 \x03(\x0b\x32\x19.mandiant.capa.CallLayout\"4\n\tAddresses\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x03(\x0b\x32\x16.mandiant.capa.Address\"b\n\x12Pair_Address_Match\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12#\n\x05match\x18\x02 \x01(\x0b\x32\x14.mandiant.capa.Match\"E\n\x0cToken_Offset\x12%\n\x05token\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x0e\n\x06offset\x18\x02 \x01(\x04\"U\n\x08Ppid_Pid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\"~\n\x0cPpid_Pid_Tid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\"\xa5\x01\n\x0fPpid_Pid_Tid_Id\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\"\n\x02id\x18\x04 \x01(\x0b\x32\x16.mandiant.capa.Integer\",\n\x07Integer\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x42\x07\n\x05value\"8\n\x06Number\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x12\x0b\n\x01\x66\x18\x03 \x01(\x01H\x00\x42\x07\n\x05value*\x92\x02\n\x0b\x41\x64\x64ressType\x12\x1b\n\x17\x41\x44\x44RESSTYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x44\x44RESSTYPE_ABSOLUTE\x10\x01\x12\x18\n\x14\x41\x44\x44RESSTYPE_RELATIVE\x10\x02\x12\x14\n\x10\x41\x44\x44RESSTYPE_FILE\x10\x03\x12\x18\n\x14\x41\x44\x44RESSTYPE_DN_TOKEN\x10\x04\x12\x1f\n\x1b\x41\x44\x44RESSTYPE_DN_TOKEN_OFFSET\x10\x05\x12\x1a\n\x16\x41\x44\x44RESSTYPE_NO_ADDRESS\x10\x06\x12\x17\n\x13\x41\x44\x44RESSTYPE_PROCESS\x10\x07\x12\x16\n\x12\x41\x44\x44RESSTYPE_THREAD\x10\x08\x12\x14\n\x10\x41\x44\x44RESSTYPE_CALL\x10\t*G\n\x06\x46lavor\x12\x16\n\x12\x46LAVOR_UNSPECIFIED\x10\x00\x12\x11\n\rFLAVOR_STATIC\x10\x01\x12\x12\n\x0e\x46LAVOR_DYNAMIC\x10\x02*\xa5\x01\n\x05Scope\x12\x15\n\x11SCOPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSCOPE_FILE\x10\x01\x12\x12\n\x0eSCOPE_FUNCTION\x10\x02\x12\x15\n\x11SCOPE_BASIC_BLOCK\x10\x03\x12\x15\n\x11SCOPE_INSTRUCTION\x10\x04\x12\x11\n\rSCOPE_PROCESS\x10\x05\x12\x10\n\x0cSCOPE_THREAD\x10\x06\x12\x0e\n\nSCOPE_CALL\x10\x07\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63\x61pa/render/proto/capa.proto\x12\rmandiant.capa\"Q\n\nAPIFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03\x61pi\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xb3\x02\n\x07\x41\x64\x64ress\x12(\n\x04type\x18\x01 \x01(\x0e\x32\x1a.mandiant.capa.AddressType\x12#\n\x01v\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.IntegerH\x00\x12\x33\n\x0ctoken_offset\x18\x03 \x01(\x0b\x32\x1b.mandiant.capa.Token_OffsetH\x00\x12+\n\x08ppid_pid\x18\x04 \x01(\x0b\x32\x17.mandiant.capa.Ppid_PidH\x00\x12\x33\n\x0cppid_pid_tid\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.Ppid_Pid_TidH\x00\x12\x39\n\x0fppid_pid_tid_id\x18\x06 \x01(\x0b\x32\x1e.mandiant.capa.Ppid_Pid_Tid_IdH\x00\x42\x07\n\x05value\"\x9c\x02\n\x08\x41nalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12%\n\x06layout\x18\x07 \x01(\x0b\x32\x15.mandiant.capa.Layout\x12\x34\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.FeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"S\n\x0b\x41rchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"`\n\nAttackSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x0e\n\x06tactic\x18\x02 \x01(\t\x12\x11\n\ttechnique\x18\x03 \x01(\t\x12\x14\n\x0csubtechnique\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"K\n\x11\x42\x61sicBlockFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\";\n\x10\x42\x61sicBlockLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\"U\n\x0c\x42ytesFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x62ytes\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"g\n\x15\x43haracteristicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\x0e\x63haracteristic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\x0c\x43lassFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x63lass_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"K\n\x11\x43ompoundStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xc8\x01\n\x0f\x44ynamicAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x06layout\x18\x06 \x01(\x0b\x32\x1c.mandiant.capa.DynamicLayout\x12;\n\x0e\x66\x65\x61ture_counts\x18\x07 \x01(\x0b\x32#.mandiant.capa.DynamicFeatureCounts\"[\n\x14\x44ynamicFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x35\n\tprocesses\x18\x02 \x03(\x0b\x32\".mandiant.capa.ProcessFeatureCount\"@\n\rDynamicLayout\x12/\n\tprocesses\x18\x01 \x03(\x0b\x32\x1c.mandiant.capa.ProcessLayout\"W\n\rExportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x65xport\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\rFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"\xb9\t\n\x0b\x46\x65\x61tureNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x02os\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.OSFeatureH\x00\x12*\n\x04\x61rch\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.ArchFeatureH\x00\x12.\n\x06\x66ormat\x18\x04 \x01(\x0b\x32\x1c.mandiant.capa.FormatFeatureH\x00\x12,\n\x05match\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.MatchFeatureH\x00\x12>\n\x0e\x63haracteristic\x18\x06 \x01(\x0b\x32$.mandiant.capa.CharacteristicFeatureH\x00\x12.\n\x06\x65xport\x18\x07 \x01(\x0b\x32\x1c.mandiant.capa.ExportFeatureH\x00\x12/\n\x07import_\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.ImportFeatureH\x00\x12\x30\n\x07section\x18\t \x01(\x0b\x32\x1d.mandiant.capa.SectionFeatureH\x00\x12;\n\rfunction_name\x18\n \x01(\x0b\x32\".mandiant.capa.FunctionNameFeatureH\x00\x12\x34\n\tsubstring\x18\x0b \x01(\x0b\x32\x1f.mandiant.capa.SubstringFeatureH\x00\x12,\n\x05regex\x18\x0c \x01(\x0b\x32\x1b.mandiant.capa.RegexFeatureH\x00\x12.\n\x06string\x18\r \x01(\x0b\x32\x1c.mandiant.capa.StringFeatureH\x00\x12-\n\x06\x63lass_\x18\x0e \x01(\x0b\x32\x1b.mandiant.capa.ClassFeatureH\x00\x12\x34\n\tnamespace\x18\x0f \x01(\x0b\x32\x1f.mandiant.capa.NamespaceFeatureH\x00\x12(\n\x03\x61pi\x18\x10 \x01(\x0b\x32\x19.mandiant.capa.APIFeatureH\x00\x12\x33\n\tproperty_\x18\x11 \x01(\x0b\x32\x1e.mandiant.capa.PropertyFeatureH\x00\x12.\n\x06number\x18\x12 \x01(\x0b\x32\x1c.mandiant.capa.NumberFeatureH\x00\x12,\n\x05\x62ytes\x18\x13 \x01(\x0b\x32\x1b.mandiant.capa.BytesFeatureH\x00\x12.\n\x06offset\x18\x14 \x01(\x0b\x32\x1c.mandiant.capa.OffsetFeatureH\x00\x12\x32\n\x08mnemonic\x18\x15 \x01(\x0b\x32\x1e.mandiant.capa.MnemonicFeatureH\x00\x12=\n\x0eoperand_number\x18\x16 \x01(\x0b\x32#.mandiant.capa.OperandNumberFeatureH\x00\x12=\n\x0eoperand_offset\x18\x17 \x01(\x0b\x32#.mandiant.capa.OperandOffsetFeatureH\x00\x12\x37\n\x0b\x62\x61sic_block\x18\x18 \x01(\x0b\x32 .mandiant.capa.BasicBlockFeatureH\x00\x42\t\n\x07\x66\x65\x61ture\"W\n\rFormatFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"N\n\x14\x46unctionFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"x\n\x0e\x46unctionLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12=\n\x14matched_basic_blocks\x18\x02 \x03(\x0b\x32\x1f.mandiant.capa.BasicBlockLayout\"d\n\x13\x46unctionNameFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rfunction_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"X\n\rImportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07import_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\":\n\x06Layout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"H\n\x0fLibraryFunction\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"Y\n\x07MBCSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x11\n\tobjective\x18\x02 \x01(\t\x12\x10\n\x08\x62\x65havior\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"\x9a\x01\n\x0cMaecMetadata\x12\x1b\n\x13\x61nalysis_conclusion\x18\x01 \x01(\t\x12\x1e\n\x16\x61nalysis_conclusion_ov\x18\x02 \x01(\t\x12\x16\n\x0emalware_family\x18\x03 \x01(\t\x12\x18\n\x10malware_category\x18\x04 \x01(\t\x12\x1b\n\x13malware_category_ov\x18\x05 \x01(\t\"\xd6\x02\n\x05Match\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x31\n\tstatement\x18\x02 \x01(\x0b\x32\x1c.mandiant.capa.StatementNodeH\x00\x12-\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNodeH\x00\x12&\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x14.mandiant.capa.Match\x12)\n\tlocations\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x08\x63\x61ptures\x18\x07 \x03(\x0b\x32\".mandiant.capa.Match.CapturesEntry\x1aI\n\rCapturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.Addresses:\x02\x38\x01\x42\x06\n\x04node\"U\n\x0cMatchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xbc\x02\n\x08Metadata\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgv\x18\x03 \x03(\t\x12%\n\x06sample\x18\x04 \x01(\x0b\x32\x15.mandiant.capa.Sample\x12-\n\x08\x61nalysis\x18\x05 \x01(\x0b\x32\x17.mandiant.capa.AnalysisB\x02\x18\x01\x12%\n\x06\x66lavor\x18\x06 \x01(\x0e\x32\x15.mandiant.capa.Flavor\x12\x38\n\x0fstatic_analysis\x18\x07 \x01(\x0b\x32\x1d.mandiant.capa.StaticAnalysisH\x00\x12:\n\x10\x64ynamic_analysis\x18\x08 \x01(\x0b\x32\x1e.mandiant.capa.DynamicAnalysisH\x00\x42\x0b\n\tanalysis2\"[\n\x0fMnemonicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x10\n\x08mnemonic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10NamespaceFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"n\n\rNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12%\n\x06number\x18\x02 \x01(\x0b\x32\x15.mandiant.capa.Number\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"O\n\tOSFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\n\n\x02os\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"o\n\rOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x06offset\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_number\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_offset\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"M\n\x13ProcessFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"|\n\rProcessLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x0fmatched_threads\x18\x02 \x03(\x0b\x32\x1b.mandiant.capa.ThreadLayout\x12\x0c\n\x04name\x18\x03 \x01(\t\"|\n\x0fPropertyFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tproperty_\x18\x02 \x01(\t\x12\x13\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_accessB\x0e\n\x0c_description\"\x8d\x01\n\x0eRangeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03min\x18\x02 \x01(\x04\x12\x0b\n\x03max\x18\x03 \x01(\x04\x12)\n\x05\x63hild\x18\x04 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNode\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\x0cRegexFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05regex\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xba\x01\n\x0eResultDocument\x12%\n\x04meta\x18\x01 \x01(\x0b\x32\x17.mandiant.capa.Metadata\x12\x37\n\x05rules\x18\x02 \x03(\x0b\x32(.mandiant.capa.ResultDocument.RulesEntry\x1aH\n\nRulesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.mandiant.capa.RuleMatches:\x02\x38\x01\"|\n\x0bRuleMatches\x12)\n\x04meta\x18\x01 \x01(\x0b\x32\x1b.mandiant.capa.RuleMetadata\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x32\n\x07matches\x18\x03 \x03(\x0b\x32!.mandiant.capa.Pair_Address_Match\"\xed\x02\n\x0cRuleMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x0f\n\x07\x61uthors\x18\x03 \x03(\t\x12\'\n\x05scope\x18\x04 \x01(\x0e\x32\x14.mandiant.capa.ScopeB\x02\x18\x01\x12)\n\x06\x61ttack\x18\x05 \x03(\x0b\x32\x19.mandiant.capa.AttackSpec\x12#\n\x03mbc\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.MBCSpec\x12\x12\n\nreferences\x18\x07 \x03(\t\x12\x10\n\x08\x65xamples\x18\x08 \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x0b\n\x03lib\x18\n \x01(\x08\x12)\n\x04maec\x18\x0b \x01(\x0b\x32\x1b.mandiant.capa.MaecMetadata\x12\x18\n\x10is_subscope_rule\x18\x0c \x01(\x08\x12%\n\x06scopes\x18\r \x01(\x0b\x32\x15.mandiant.capa.Scopes\"A\n\x06Sample\x12\x0b\n\x03md5\x18\x01 \x01(\t\x12\x0c\n\x04sha1\x18\x02 \x01(\t\x12\x0e\n\x06sha256\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\"v\n\x06Scopes\x12)\n\x06static\x18\x01 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x00\x88\x01\x01\x12*\n\x07\x64ynamic\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x01\x88\x01\x01\x42\t\n\x07_staticB\n\n\x08_dynamic\"Y\n\x0eSectionFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07section\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\rSomeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xf4\x01\n\rStatementNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12.\n\x05range\x18\x02 \x01(\x0b\x32\x1d.mandiant.capa.RangeStatementH\x00\x12,\n\x04some\x18\x03 \x01(\x0b\x32\x1c.mandiant.capa.SomeStatementH\x00\x12\x34\n\x08subscope\x18\x04 \x01(\x0b\x32 .mandiant.capa.SubscopeStatementH\x00\x12\x34\n\x08\x63ompound\x18\x05 \x01(\x0b\x32 .mandiant.capa.CompoundStatementH\x00\x42\x0b\n\tstatement\"\xae\x02\n\x0eStaticAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12+\n\x06layout\x18\x07 \x01(\x0b\x32\x1b.mandiant.capa.StaticLayout\x12:\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\".mandiant.capa.StaticFeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"[\n\x13StaticFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"@\n\x0cStaticLayout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"W\n\rStringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"p\n\x11SubscopeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12#\n\x05scope\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.Scope\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10SubstringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tsubstring\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"C\n\nCallLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"i\n\x0cThreadLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x30\n\rmatched_calls\x18\x02 \x03(\x0b\x32\x19.mandiant.capa.CallLayout\"4\n\tAddresses\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x03(\x0b\x32\x16.mandiant.capa.Address\"b\n\x12Pair_Address_Match\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12#\n\x05match\x18\x02 \x01(\x0b\x32\x14.mandiant.capa.Match\"E\n\x0cToken_Offset\x12%\n\x05token\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x0e\n\x06offset\x18\x02 \x01(\x04\"U\n\x08Ppid_Pid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\"~\n\x0cPpid_Pid_Tid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\"\xa5\x01\n\x0fPpid_Pid_Tid_Id\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\"\n\x02id\x18\x04 \x01(\x0b\x32\x16.mandiant.capa.Integer\",\n\x07Integer\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x42\x07\n\x05value\"8\n\x06Number\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x12\x0b\n\x01\x66\x18\x03 \x01(\x01H\x00\x42\x07\n\x05value*\x92\x02\n\x0b\x41\x64\x64ressType\x12\x1b\n\x17\x41\x44\x44RESSTYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x44\x44RESSTYPE_ABSOLUTE\x10\x01\x12\x18\n\x14\x41\x44\x44RESSTYPE_RELATIVE\x10\x02\x12\x14\n\x10\x41\x44\x44RESSTYPE_FILE\x10\x03\x12\x18\n\x14\x41\x44\x44RESSTYPE_DN_TOKEN\x10\x04\x12\x1f\n\x1b\x41\x44\x44RESSTYPE_DN_TOKEN_OFFSET\x10\x05\x12\x1a\n\x16\x41\x44\x44RESSTYPE_NO_ADDRESS\x10\x06\x12\x17\n\x13\x41\x44\x44RESSTYPE_PROCESS\x10\x07\x12\x16\n\x12\x41\x44\x44RESSTYPE_THREAD\x10\x08\x12\x14\n\x10\x41\x44\x44RESSTYPE_CALL\x10\t*G\n\x06\x46lavor\x12\x16\n\x12\x46LAVOR_UNSPECIFIED\x10\x00\x12\x11\n\rFLAVOR_STATIC\x10\x01\x12\x12\n\x0e\x46LAVOR_DYNAMIC\x10\x02*\xb9\x01\n\x05Scope\x12\x15\n\x11SCOPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSCOPE_FILE\x10\x01\x12\x12\n\x0eSCOPE_FUNCTION\x10\x02\x12\x15\n\x11SCOPE_BASIC_BLOCK\x10\x03\x12\x15\n\x11SCOPE_INSTRUCTION\x10\x04\x12\x11\n\rSCOPE_PROCESS\x10\x05\x12\x10\n\x0cSCOPE_THREAD\x10\x06\x12\x0e\n\nSCOPE_CALL\x10\x07\x12\x12\n\x0eSCOPE_SEQUENCE\x10\x08\x62\x06proto3') -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'capa.render.proto.capa_pb2', globals()) +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'capa.render.proto.capa_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _MATCH_CAPTURESENTRY._options = None - _MATCH_CAPTURESENTRY._serialized_options = b'8\001' - _METADATA.fields_by_name['analysis']._options = None - _METADATA.fields_by_name['analysis']._serialized_options = b'\030\001' - _RESULTDOCUMENT_RULESENTRY._options = None - _RESULTDOCUMENT_RULESENTRY._serialized_options = b'8\001' - _RULEMETADATA.fields_by_name['scope']._options = None - _RULEMETADATA.fields_by_name['scope']._serialized_options = b'\030\001' - _ADDRESSTYPE._serialized_start=9062 - _ADDRESSTYPE._serialized_end=9336 - _FLAVOR._serialized_start=9338 - _FLAVOR._serialized_end=9409 - _SCOPE._serialized_start=9412 - _SCOPE._serialized_end=9577 - _APIFEATURE._serialized_start=47 - _APIFEATURE._serialized_end=128 - _ADDRESS._serialized_start=131 - _ADDRESS._serialized_end=438 - _ANALYSIS._serialized_start=441 - _ANALYSIS._serialized_end=725 - _ARCHFEATURE._serialized_start=727 - _ARCHFEATURE._serialized_end=810 - _ATTACKSPEC._serialized_start=812 - _ATTACKSPEC._serialized_end=908 - _BASICBLOCKFEATURE._serialized_start=910 - _BASICBLOCKFEATURE._serialized_end=985 - _BASICBLOCKLAYOUT._serialized_start=987 - _BASICBLOCKLAYOUT._serialized_end=1046 - _BYTESFEATURE._serialized_start=1048 - _BYTESFEATURE._serialized_end=1133 - _CHARACTERISTICFEATURE._serialized_start=1135 - _CHARACTERISTICFEATURE._serialized_end=1238 - _CLASSFEATURE._serialized_start=1240 - _CLASSFEATURE._serialized_end=1326 - _COMPOUNDSTATEMENT._serialized_start=1328 - _COMPOUNDSTATEMENT._serialized_end=1403 - _DYNAMICANALYSIS._serialized_start=1406 - _DYNAMICANALYSIS._serialized_end=1606 - _DYNAMICFEATURECOUNTS._serialized_start=1608 - _DYNAMICFEATURECOUNTS._serialized_end=1699 - _DYNAMICLAYOUT._serialized_start=1701 - _DYNAMICLAYOUT._serialized_end=1765 - _EXPORTFEATURE._serialized_start=1767 - _EXPORTFEATURE._serialized_end=1854 - _FEATURECOUNTS._serialized_start=1856 - _FEATURECOUNTS._serialized_end=1941 - _FEATURENODE._serialized_start=1944 - _FEATURENODE._serialized_end=3153 - _FORMATFEATURE._serialized_start=3155 - _FORMATFEATURE._serialized_end=3242 - _FUNCTIONFEATURECOUNT._serialized_start=3244 - _FUNCTIONFEATURECOUNT._serialized_end=3322 - _FUNCTIONLAYOUT._serialized_start=3324 - _FUNCTIONLAYOUT._serialized_end=3444 - _FUNCTIONNAMEFEATURE._serialized_start=3446 - _FUNCTIONNAMEFEATURE._serialized_end=3546 - _IMPORTFEATURE._serialized_start=3548 - _IMPORTFEATURE._serialized_end=3636 - _LAYOUT._serialized_start=3638 - _LAYOUT._serialized_end=3696 - _LIBRARYFUNCTION._serialized_start=3698 - _LIBRARYFUNCTION._serialized_end=3770 - _MBCSPEC._serialized_start=3772 - _MBCSPEC._serialized_end=3861 - _MAECMETADATA._serialized_start=3864 - _MAECMETADATA._serialized_end=4018 - _MATCH._serialized_start=4021 - _MATCH._serialized_end=4363 - _MATCH_CAPTURESENTRY._serialized_start=4282 - _MATCH_CAPTURESENTRY._serialized_end=4355 - _MATCHFEATURE._serialized_start=4365 - _MATCHFEATURE._serialized_end=4450 - _METADATA._serialized_start=4453 - _METADATA._serialized_end=4769 - _MNEMONICFEATURE._serialized_start=4771 - _MNEMONICFEATURE._serialized_end=4862 - _NAMESPACEFEATURE._serialized_start=4864 - _NAMESPACEFEATURE._serialized_end=4957 - _NUMBERFEATURE._serialized_start=4959 - _NUMBERFEATURE._serialized_end=5069 - _OSFEATURE._serialized_start=5071 - _OSFEATURE._serialized_end=5150 - _OFFSETFEATURE._serialized_start=5152 - _OFFSETFEATURE._serialized_end=5263 - _OPERANDNUMBERFEATURE._serialized_start=5266 - _OPERANDNUMBERFEATURE._serialized_end=5407 - _OPERANDOFFSETFEATURE._serialized_start=5410 - _OPERANDOFFSETFEATURE._serialized_end=5551 - _PROCESSFEATURECOUNT._serialized_start=5553 - _PROCESSFEATURECOUNT._serialized_end=5630 - _PROCESSLAYOUT._serialized_start=5632 - _PROCESSLAYOUT._serialized_end=5756 - _PROPERTYFEATURE._serialized_start=5758 - _PROPERTYFEATURE._serialized_end=5882 - _RANGESTATEMENT._serialized_start=5885 - _RANGESTATEMENT._serialized_end=6026 - _REGEXFEATURE._serialized_start=6028 - _REGEXFEATURE._serialized_end=6113 - _RESULTDOCUMENT._serialized_start=6116 - _RESULTDOCUMENT._serialized_end=6302 - _RESULTDOCUMENT_RULESENTRY._serialized_start=6230 - _RESULTDOCUMENT_RULESENTRY._serialized_end=6302 - _RULEMATCHES._serialized_start=6304 - _RULEMATCHES._serialized_end=6428 - _RULEMETADATA._serialized_start=6431 - _RULEMETADATA._serialized_end=6796 - _SAMPLE._serialized_start=6798 - _SAMPLE._serialized_end=6863 - _SCOPES._serialized_start=6865 - _SCOPES._serialized_end=6983 - _SECTIONFEATURE._serialized_start=6985 - _SECTIONFEATURE._serialized_end=7074 - _SOMESTATEMENT._serialized_start=7076 - _SOMESTATEMENT._serialized_end=7162 - _STATEMENTNODE._serialized_start=7165 - _STATEMENTNODE._serialized_end=7409 - _STATICANALYSIS._serialized_start=7412 - _STATICANALYSIS._serialized_end=7714 - _STATICFEATURECOUNTS._serialized_start=7716 - _STATICFEATURECOUNTS._serialized_end=7807 - _STATICLAYOUT._serialized_start=7809 - _STATICLAYOUT._serialized_end=7873 - _STRINGFEATURE._serialized_start=7875 - _STRINGFEATURE._serialized_end=7962 - _SUBSCOPESTATEMENT._serialized_start=7964 - _SUBSCOPESTATEMENT._serialized_end=8076 - _SUBSTRINGFEATURE._serialized_start=8078 - _SUBSTRINGFEATURE._serialized_end=8171 - _CALLLAYOUT._serialized_start=8173 - _CALLLAYOUT._serialized_end=8240 - _THREADLAYOUT._serialized_start=8242 - _THREADLAYOUT._serialized_end=8347 - _ADDRESSES._serialized_start=8349 - _ADDRESSES._serialized_end=8401 - _PAIR_ADDRESS_MATCH._serialized_start=8403 - _PAIR_ADDRESS_MATCH._serialized_end=8501 - _TOKEN_OFFSET._serialized_start=8503 - _TOKEN_OFFSET._serialized_end=8572 - _PPID_PID._serialized_start=8574 - _PPID_PID._serialized_end=8659 - _PPID_PID_TID._serialized_start=8661 - _PPID_PID_TID._serialized_end=8787 - _PPID_PID_TID_ID._serialized_start=8790 - _PPID_PID_TID_ID._serialized_end=8955 - _INTEGER._serialized_start=8957 - _INTEGER._serialized_end=9001 - _NUMBER._serialized_start=9003 - _NUMBER._serialized_end=9059 + _globals['_MATCH_CAPTURESENTRY']._options = None + _globals['_MATCH_CAPTURESENTRY']._serialized_options = b'8\001' + _globals['_METADATA'].fields_by_name['analysis']._options = None + _globals['_METADATA'].fields_by_name['analysis']._serialized_options = b'\030\001' + _globals['_RESULTDOCUMENT_RULESENTRY']._options = None + _globals['_RESULTDOCUMENT_RULESENTRY']._serialized_options = b'8\001' + _globals['_RULEMETADATA'].fields_by_name['scope']._options = None + _globals['_RULEMETADATA'].fields_by_name['scope']._serialized_options = b'\030\001' + _globals['_ADDRESSTYPE']._serialized_start=9062 + _globals['_ADDRESSTYPE']._serialized_end=9336 + _globals['_FLAVOR']._serialized_start=9338 + _globals['_FLAVOR']._serialized_end=9409 + _globals['_SCOPE']._serialized_start=9412 + _globals['_SCOPE']._serialized_end=9597 + _globals['_APIFEATURE']._serialized_start=47 + _globals['_APIFEATURE']._serialized_end=128 + _globals['_ADDRESS']._serialized_start=131 + _globals['_ADDRESS']._serialized_end=438 + _globals['_ANALYSIS']._serialized_start=441 + _globals['_ANALYSIS']._serialized_end=725 + _globals['_ARCHFEATURE']._serialized_start=727 + _globals['_ARCHFEATURE']._serialized_end=810 + _globals['_ATTACKSPEC']._serialized_start=812 + _globals['_ATTACKSPEC']._serialized_end=908 + _globals['_BASICBLOCKFEATURE']._serialized_start=910 + _globals['_BASICBLOCKFEATURE']._serialized_end=985 + _globals['_BASICBLOCKLAYOUT']._serialized_start=987 + _globals['_BASICBLOCKLAYOUT']._serialized_end=1046 + _globals['_BYTESFEATURE']._serialized_start=1048 + _globals['_BYTESFEATURE']._serialized_end=1133 + _globals['_CHARACTERISTICFEATURE']._serialized_start=1135 + _globals['_CHARACTERISTICFEATURE']._serialized_end=1238 + _globals['_CLASSFEATURE']._serialized_start=1240 + _globals['_CLASSFEATURE']._serialized_end=1326 + _globals['_COMPOUNDSTATEMENT']._serialized_start=1328 + _globals['_COMPOUNDSTATEMENT']._serialized_end=1403 + _globals['_DYNAMICANALYSIS']._serialized_start=1406 + _globals['_DYNAMICANALYSIS']._serialized_end=1606 + _globals['_DYNAMICFEATURECOUNTS']._serialized_start=1608 + _globals['_DYNAMICFEATURECOUNTS']._serialized_end=1699 + _globals['_DYNAMICLAYOUT']._serialized_start=1701 + _globals['_DYNAMICLAYOUT']._serialized_end=1765 + _globals['_EXPORTFEATURE']._serialized_start=1767 + _globals['_EXPORTFEATURE']._serialized_end=1854 + _globals['_FEATURECOUNTS']._serialized_start=1856 + _globals['_FEATURECOUNTS']._serialized_end=1941 + _globals['_FEATURENODE']._serialized_start=1944 + _globals['_FEATURENODE']._serialized_end=3153 + _globals['_FORMATFEATURE']._serialized_start=3155 + _globals['_FORMATFEATURE']._serialized_end=3242 + _globals['_FUNCTIONFEATURECOUNT']._serialized_start=3244 + _globals['_FUNCTIONFEATURECOUNT']._serialized_end=3322 + _globals['_FUNCTIONLAYOUT']._serialized_start=3324 + _globals['_FUNCTIONLAYOUT']._serialized_end=3444 + _globals['_FUNCTIONNAMEFEATURE']._serialized_start=3446 + _globals['_FUNCTIONNAMEFEATURE']._serialized_end=3546 + _globals['_IMPORTFEATURE']._serialized_start=3548 + _globals['_IMPORTFEATURE']._serialized_end=3636 + _globals['_LAYOUT']._serialized_start=3638 + _globals['_LAYOUT']._serialized_end=3696 + _globals['_LIBRARYFUNCTION']._serialized_start=3698 + _globals['_LIBRARYFUNCTION']._serialized_end=3770 + _globals['_MBCSPEC']._serialized_start=3772 + _globals['_MBCSPEC']._serialized_end=3861 + _globals['_MAECMETADATA']._serialized_start=3864 + _globals['_MAECMETADATA']._serialized_end=4018 + _globals['_MATCH']._serialized_start=4021 + _globals['_MATCH']._serialized_end=4363 + _globals['_MATCH_CAPTURESENTRY']._serialized_start=4282 + _globals['_MATCH_CAPTURESENTRY']._serialized_end=4355 + _globals['_MATCHFEATURE']._serialized_start=4365 + _globals['_MATCHFEATURE']._serialized_end=4450 + _globals['_METADATA']._serialized_start=4453 + _globals['_METADATA']._serialized_end=4769 + _globals['_MNEMONICFEATURE']._serialized_start=4771 + _globals['_MNEMONICFEATURE']._serialized_end=4862 + _globals['_NAMESPACEFEATURE']._serialized_start=4864 + _globals['_NAMESPACEFEATURE']._serialized_end=4957 + _globals['_NUMBERFEATURE']._serialized_start=4959 + _globals['_NUMBERFEATURE']._serialized_end=5069 + _globals['_OSFEATURE']._serialized_start=5071 + _globals['_OSFEATURE']._serialized_end=5150 + _globals['_OFFSETFEATURE']._serialized_start=5152 + _globals['_OFFSETFEATURE']._serialized_end=5263 + _globals['_OPERANDNUMBERFEATURE']._serialized_start=5266 + _globals['_OPERANDNUMBERFEATURE']._serialized_end=5407 + _globals['_OPERANDOFFSETFEATURE']._serialized_start=5410 + _globals['_OPERANDOFFSETFEATURE']._serialized_end=5551 + _globals['_PROCESSFEATURECOUNT']._serialized_start=5553 + _globals['_PROCESSFEATURECOUNT']._serialized_end=5630 + _globals['_PROCESSLAYOUT']._serialized_start=5632 + _globals['_PROCESSLAYOUT']._serialized_end=5756 + _globals['_PROPERTYFEATURE']._serialized_start=5758 + _globals['_PROPERTYFEATURE']._serialized_end=5882 + _globals['_RANGESTATEMENT']._serialized_start=5885 + _globals['_RANGESTATEMENT']._serialized_end=6026 + _globals['_REGEXFEATURE']._serialized_start=6028 + _globals['_REGEXFEATURE']._serialized_end=6113 + _globals['_RESULTDOCUMENT']._serialized_start=6116 + _globals['_RESULTDOCUMENT']._serialized_end=6302 + _globals['_RESULTDOCUMENT_RULESENTRY']._serialized_start=6230 + _globals['_RESULTDOCUMENT_RULESENTRY']._serialized_end=6302 + _globals['_RULEMATCHES']._serialized_start=6304 + _globals['_RULEMATCHES']._serialized_end=6428 + _globals['_RULEMETADATA']._serialized_start=6431 + _globals['_RULEMETADATA']._serialized_end=6796 + _globals['_SAMPLE']._serialized_start=6798 + _globals['_SAMPLE']._serialized_end=6863 + _globals['_SCOPES']._serialized_start=6865 + _globals['_SCOPES']._serialized_end=6983 + _globals['_SECTIONFEATURE']._serialized_start=6985 + _globals['_SECTIONFEATURE']._serialized_end=7074 + _globals['_SOMESTATEMENT']._serialized_start=7076 + _globals['_SOMESTATEMENT']._serialized_end=7162 + _globals['_STATEMENTNODE']._serialized_start=7165 + _globals['_STATEMENTNODE']._serialized_end=7409 + _globals['_STATICANALYSIS']._serialized_start=7412 + _globals['_STATICANALYSIS']._serialized_end=7714 + _globals['_STATICFEATURECOUNTS']._serialized_start=7716 + _globals['_STATICFEATURECOUNTS']._serialized_end=7807 + _globals['_STATICLAYOUT']._serialized_start=7809 + _globals['_STATICLAYOUT']._serialized_end=7873 + _globals['_STRINGFEATURE']._serialized_start=7875 + _globals['_STRINGFEATURE']._serialized_end=7962 + _globals['_SUBSCOPESTATEMENT']._serialized_start=7964 + _globals['_SUBSCOPESTATEMENT']._serialized_end=8076 + _globals['_SUBSTRINGFEATURE']._serialized_start=8078 + _globals['_SUBSTRINGFEATURE']._serialized_end=8171 + _globals['_CALLLAYOUT']._serialized_start=8173 + _globals['_CALLLAYOUT']._serialized_end=8240 + _globals['_THREADLAYOUT']._serialized_start=8242 + _globals['_THREADLAYOUT']._serialized_end=8347 + _globals['_ADDRESSES']._serialized_start=8349 + _globals['_ADDRESSES']._serialized_end=8401 + _globals['_PAIR_ADDRESS_MATCH']._serialized_start=8403 + _globals['_PAIR_ADDRESS_MATCH']._serialized_end=8501 + _globals['_TOKEN_OFFSET']._serialized_start=8503 + _globals['_TOKEN_OFFSET']._serialized_end=8572 + _globals['_PPID_PID']._serialized_start=8574 + _globals['_PPID_PID']._serialized_end=8659 + _globals['_PPID_PID_TID']._serialized_start=8661 + _globals['_PPID_PID_TID']._serialized_end=8787 + _globals['_PPID_PID_TID_ID']._serialized_start=8790 + _globals['_PPID_PID_TID_ID']._serialized_end=8955 + _globals['_INTEGER']._serialized_start=8957 + _globals['_INTEGER']._serialized_end=9001 + _globals['_NUMBER']._serialized_start=9003 + _globals['_NUMBER']._serialized_end=9059 # @@protoc_insertion_point(module_scope) diff --git a/capa/render/proto/capa_pb2.pyi b/capa/render/proto/capa_pb2.pyi index ecb330bc6..c881561a3 100644 --- a/capa/render/proto/capa_pb2.pyi +++ b/capa/render/proto/capa_pb2.pyi @@ -2,6 +2,7 @@ @generated by mypy-protobuf. Do not edit manually! isort:skip_file """ + import builtins import collections.abc import google.protobuf.descriptor @@ -80,6 +81,7 @@ class _ScopeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumType SCOPE_PROCESS: _Scope.ValueType # 5 SCOPE_THREAD: _Scope.ValueType # 6 SCOPE_CALL: _Scope.ValueType # 7 + SCOPE_SEQUENCE: _Scope.ValueType # 8 class Scope(_Scope, metaclass=_ScopeEnumTypeWrapper): ... @@ -91,9 +93,10 @@ SCOPE_INSTRUCTION: Scope.ValueType # 4 SCOPE_PROCESS: Scope.ValueType # 5 SCOPE_THREAD: Scope.ValueType # 6 SCOPE_CALL: Scope.ValueType # 7 +SCOPE_SEQUENCE: Scope.ValueType # 8 global___Scope = Scope -@typing_extensions.final +@typing.final class APIFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -110,13 +113,13 @@ class APIFeature(google.protobuf.message.Message): api: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "api", b"api", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "api", b"api", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___APIFeature = APIFeature -@typing_extensions.final +@typing.final class Address(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -147,13 +150,13 @@ class Address(google.protobuf.message.Message): ppid_pid_tid: global___Ppid_Pid_Tid | None = ..., ppid_pid_tid_id: global___Ppid_Pid_Tid_Id | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "v", b"v", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "type", b"type", "v", b"v", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["v", "token_offset", "ppid_pid", "ppid_pid_tid", "ppid_pid_tid_id"] | None: ... + def HasField(self, field_name: typing.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "v", b"v", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["ppid_pid", b"ppid_pid", "ppid_pid_tid", b"ppid_pid_tid", "ppid_pid_tid_id", b"ppid_pid_tid_id", "token_offset", b"token_offset", "type", b"type", "v", b"v", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["value", b"value"]) -> typing.Literal["v", "token_offset", "ppid_pid", "ppid_pid_tid", "ppid_pid_tid_id"] | None: ... global___Address = Address -@typing_extensions.final +@typing.final class Analysis(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -193,12 +196,12 @@ class Analysis(google.protobuf.message.Message): feature_counts: global___FeatureCounts | None = ..., library_functions: collections.abc.Iterable[global___LibraryFunction] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["base_address", b"base_address", "feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["arch", b"arch", "base_address", b"base_address", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "library_functions", b"library_functions", "os", b"os", "rules", b"rules"]) -> None: ... + def HasField(self, field_name: typing.Literal["base_address", b"base_address", "feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["arch", b"arch", "base_address", b"base_address", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "library_functions", b"library_functions", "os", b"os", "rules", b"rules"]) -> None: ... global___Analysis = Analysis -@typing_extensions.final +@typing.final class ArchFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -215,13 +218,13 @@ class ArchFeature(google.protobuf.message.Message): arch: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "arch", b"arch", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "arch", b"arch", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___ArchFeature = ArchFeature -@typing_extensions.final +@typing.final class AttackSpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -230,12 +233,12 @@ class AttackSpec(google.protobuf.message.Message): TECHNIQUE_FIELD_NUMBER: builtins.int SUBTECHNIQUE_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int - @property - def parts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... tactic: builtins.str technique: builtins.str subtechnique: builtins.str id: builtins.str + @property + def parts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... def __init__( self, *, @@ -245,11 +248,11 @@ class AttackSpec(google.protobuf.message.Message): subtechnique: builtins.str = ..., id: builtins.str = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["id", b"id", "parts", b"parts", "subtechnique", b"subtechnique", "tactic", b"tactic", "technique", b"technique"]) -> None: ... + def ClearField(self, field_name: typing.Literal["id", b"id", "parts", b"parts", "subtechnique", b"subtechnique", "tactic", b"tactic", "technique", b"technique"]) -> None: ... global___AttackSpec = AttackSpec -@typing_extensions.final +@typing.final class BasicBlockFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -263,13 +266,13 @@ class BasicBlockFeature(google.protobuf.message.Message): type: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___BasicBlockFeature = BasicBlockFeature -@typing_extensions.final +@typing.final class BasicBlockLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -281,12 +284,12 @@ class BasicBlockLayout(google.protobuf.message.Message): *, address: global___Address | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address"]) -> None: ... global___BasicBlockLayout = BasicBlockLayout -@typing_extensions.final +@typing.final class BytesFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -303,13 +306,13 @@ class BytesFeature(google.protobuf.message.Message): bytes: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "bytes", b"bytes", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "bytes", b"bytes", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___BytesFeature = BytesFeature -@typing_extensions.final +@typing.final class CharacteristicFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -326,13 +329,13 @@ class CharacteristicFeature(google.protobuf.message.Message): characteristic: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "characteristic", b"characteristic", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "characteristic", b"characteristic", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___CharacteristicFeature = CharacteristicFeature -@typing_extensions.final +@typing.final class ClassFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -350,13 +353,13 @@ class ClassFeature(google.protobuf.message.Message): class_: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "class_", b"class_", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "class_", b"class_", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___ClassFeature = ClassFeature -@typing_extensions.final +@typing.final class CompoundStatement(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -370,13 +373,13 @@ class CompoundStatement(google.protobuf.message.Message): type: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___CompoundStatement = CompoundStatement -@typing_extensions.final +@typing.final class DynamicAnalysis(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -408,12 +411,12 @@ class DynamicAnalysis(google.protobuf.message.Message): layout: global___DynamicLayout | None = ..., feature_counts: global___DynamicFeatureCounts | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["arch", b"arch", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "os", b"os", "rules", b"rules"]) -> None: ... + def HasField(self, field_name: typing.Literal["feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["arch", b"arch", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "os", b"os", "rules", b"rules"]) -> None: ... global___DynamicAnalysis = DynamicAnalysis -@typing_extensions.final +@typing.final class DynamicFeatureCounts(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -428,11 +431,11 @@ class DynamicFeatureCounts(google.protobuf.message.Message): file: builtins.int = ..., processes: collections.abc.Iterable[global___ProcessFeatureCount] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "processes", b"processes"]) -> None: ... + def ClearField(self, field_name: typing.Literal["file", b"file", "processes", b"processes"]) -> None: ... global___DynamicFeatureCounts = DynamicFeatureCounts -@typing_extensions.final +@typing.final class DynamicLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -444,11 +447,11 @@ class DynamicLayout(google.protobuf.message.Message): *, processes: collections.abc.Iterable[global___ProcessLayout] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["processes", b"processes"]) -> None: ... + def ClearField(self, field_name: typing.Literal["processes", b"processes"]) -> None: ... global___DynamicLayout = DynamicLayout -@typing_extensions.final +@typing.final class ExportFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -465,13 +468,13 @@ class ExportFeature(google.protobuf.message.Message): export: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "export", b"export", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "export", b"export", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___ExportFeature = ExportFeature -@typing_extensions.final +@typing.final class FeatureCounts(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -486,11 +489,11 @@ class FeatureCounts(google.protobuf.message.Message): file: builtins.int = ..., functions: collections.abc.Iterable[global___FunctionFeatureCount] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "functions", b"functions"]) -> None: ... + def ClearField(self, field_name: typing.Literal["file", b"file", "functions", b"functions"]) -> None: ... global___FeatureCounts = FeatureCounts -@typing_extensions.final +@typing.final class FeatureNode(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -534,6 +537,7 @@ class FeatureNode(google.protobuf.message.Message): @property def import_(self) -> global___ImportFeature: """import is Python keyword""" + @property def section(self) -> global___SectionFeature: ... @property @@ -553,6 +557,7 @@ class FeatureNode(google.protobuf.message.Message): @property def property_(self) -> global___PropertyFeature: """property is a Python top-level decorator name""" + @property def number(self) -> global___NumberFeature: ... @property @@ -595,13 +600,13 @@ class FeatureNode(google.protobuf.message.Message): operand_offset: global___OperandOffsetFeature | None = ..., basic_block: global___BasicBlockFeature | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["api", b"api", "arch", b"arch", "basic_block", b"basic_block", "bytes", b"bytes", "characteristic", b"characteristic", "class_", b"class_", "export", b"export", "feature", b"feature", "format", b"format", "function_name", b"function_name", "import_", b"import_", "match", b"match", "mnemonic", b"mnemonic", "namespace", b"namespace", "number", b"number", "offset", b"offset", "operand_number", b"operand_number", "operand_offset", b"operand_offset", "os", b"os", "property_", b"property_", "regex", b"regex", "section", b"section", "string", b"string", "substring", b"substring"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["api", b"api", "arch", b"arch", "basic_block", b"basic_block", "bytes", b"bytes", "characteristic", b"characteristic", "class_", b"class_", "export", b"export", "feature", b"feature", "format", b"format", "function_name", b"function_name", "import_", b"import_", "match", b"match", "mnemonic", b"mnemonic", "namespace", b"namespace", "number", b"number", "offset", b"offset", "operand_number", b"operand_number", "operand_offset", b"operand_offset", "os", b"os", "property_", b"property_", "regex", b"regex", "section", b"section", "string", b"string", "substring", b"substring", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["feature", b"feature"]) -> typing_extensions.Literal["os", "arch", "format", "match", "characteristic", "export", "import_", "section", "function_name", "substring", "regex", "string", "class_", "namespace", "api", "property_", "number", "bytes", "offset", "mnemonic", "operand_number", "operand_offset", "basic_block"] | None: ... + def HasField(self, field_name: typing.Literal["api", b"api", "arch", b"arch", "basic_block", b"basic_block", "bytes", b"bytes", "characteristic", b"characteristic", "class_", b"class_", "export", b"export", "feature", b"feature", "format", b"format", "function_name", b"function_name", "import_", b"import_", "match", b"match", "mnemonic", b"mnemonic", "namespace", b"namespace", "number", b"number", "offset", b"offset", "operand_number", b"operand_number", "operand_offset", b"operand_offset", "os", b"os", "property_", b"property_", "regex", b"regex", "section", b"section", "string", b"string", "substring", b"substring"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["api", b"api", "arch", b"arch", "basic_block", b"basic_block", "bytes", b"bytes", "characteristic", b"characteristic", "class_", b"class_", "export", b"export", "feature", b"feature", "format", b"format", "function_name", b"function_name", "import_", b"import_", "match", b"match", "mnemonic", b"mnemonic", "namespace", b"namespace", "number", b"number", "offset", b"offset", "operand_number", b"operand_number", "operand_offset", b"operand_offset", "os", b"os", "property_", b"property_", "regex", b"regex", "section", b"section", "string", b"string", "substring", b"substring", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["feature", b"feature"]) -> typing.Literal["os", "arch", "format", "match", "characteristic", "export", "import_", "section", "function_name", "substring", "regex", "string", "class_", "namespace", "api", "property_", "number", "bytes", "offset", "mnemonic", "operand_number", "operand_offset", "basic_block"] | None: ... global___FeatureNode = FeatureNode -@typing_extensions.final +@typing.final class FormatFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -618,33 +623,33 @@ class FormatFeature(google.protobuf.message.Message): format: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "format", b"format", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "format", b"format", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___FormatFeature = FormatFeature -@typing_extensions.final +@typing.final class FunctionFeatureCount(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ADDRESS_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int + count: builtins.int @property def address(self) -> global___Address: ... - count: builtins.int def __init__( self, *, address: global___Address | None = ..., count: builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "count", b"count"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "count", b"count"]) -> None: ... global___FunctionFeatureCount = FunctionFeatureCount -@typing_extensions.final +@typing.final class FunctionLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -660,12 +665,12 @@ class FunctionLayout(google.protobuf.message.Message): address: global___Address | None = ..., matched_basic_blocks: collections.abc.Iterable[global___BasicBlockLayout] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "matched_basic_blocks", b"matched_basic_blocks"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "matched_basic_blocks", b"matched_basic_blocks"]) -> None: ... global___FunctionLayout = FunctionLayout -@typing_extensions.final +@typing.final class FunctionNameFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -682,13 +687,13 @@ class FunctionNameFeature(google.protobuf.message.Message): function_name: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "function_name", b"function_name", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "function_name", b"function_name", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___FunctionNameFeature = FunctionNameFeature -@typing_extensions.final +@typing.final class ImportFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -705,13 +710,13 @@ class ImportFeature(google.protobuf.message.Message): import_: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "import_", b"import_", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "import_", b"import_", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___ImportFeature = ImportFeature -@typing_extensions.final +@typing.final class Layout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -723,31 +728,31 @@ class Layout(google.protobuf.message.Message): *, functions: collections.abc.Iterable[global___FunctionLayout] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["functions", b"functions"]) -> None: ... + def ClearField(self, field_name: typing.Literal["functions", b"functions"]) -> None: ... global___Layout = Layout -@typing_extensions.final +@typing.final class LibraryFunction(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ADDRESS_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int + name: builtins.str @property def address(self) -> global___Address: ... - name: builtins.str def __init__( self, *, address: global___Address | None = ..., name: builtins.str = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "name", b"name"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "name", b"name"]) -> None: ... global___LibraryFunction = LibraryFunction -@typing_extensions.final +@typing.final class MBCSpec(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -756,12 +761,12 @@ class MBCSpec(google.protobuf.message.Message): BEHAVIOR_FIELD_NUMBER: builtins.int METHOD_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int - @property - def parts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... objective: builtins.str behavior: builtins.str method: builtins.str id: builtins.str + @property + def parts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... def __init__( self, *, @@ -771,11 +776,11 @@ class MBCSpec(google.protobuf.message.Message): method: builtins.str = ..., id: builtins.str = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["behavior", b"behavior", "id", b"id", "method", b"method", "objective", b"objective", "parts", b"parts"]) -> None: ... + def ClearField(self, field_name: typing.Literal["behavior", b"behavior", "id", b"id", "method", b"method", "objective", b"objective", "parts", b"parts"]) -> None: ... global___MBCSpec = MBCSpec -@typing_extensions.final +@typing.final class MaecMetadata(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -798,15 +803,15 @@ class MaecMetadata(google.protobuf.message.Message): malware_category: builtins.str = ..., malware_category_ov: builtins.str = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["analysis_conclusion", b"analysis_conclusion", "analysis_conclusion_ov", b"analysis_conclusion_ov", "malware_category", b"malware_category", "malware_category_ov", b"malware_category_ov", "malware_family", b"malware_family"]) -> None: ... + def ClearField(self, field_name: typing.Literal["analysis_conclusion", b"analysis_conclusion", "analysis_conclusion_ov", b"analysis_conclusion_ov", "malware_category", b"malware_category", "malware_category_ov", b"malware_category_ov", "malware_family", b"malware_family"]) -> None: ... global___MaecMetadata = MaecMetadata -@typing_extensions.final +@typing.final class Match(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - @typing_extensions.final + @typing.final class CapturesEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -821,8 +826,8 @@ class Match(google.protobuf.message.Message): key: builtins.str = ..., value: global___Addresses | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... SUCCESS_FIELD_NUMBER: builtins.int STATEMENT_FIELD_NUMBER: builtins.int @@ -851,13 +856,13 @@ class Match(google.protobuf.message.Message): locations: collections.abc.Iterable[global___Address] | None = ..., captures: collections.abc.Mapping[builtins.str, global___Addresses] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["feature", b"feature", "node", b"node", "statement", b"statement"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["captures", b"captures", "children", b"children", "feature", b"feature", "locations", b"locations", "node", b"node", "statement", b"statement", "success", b"success"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["node", b"node"]) -> typing_extensions.Literal["statement", "feature"] | None: ... + def HasField(self, field_name: typing.Literal["feature", b"feature", "node", b"node", "statement", b"statement"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["captures", b"captures", "children", b"children", "feature", b"feature", "locations", b"locations", "node", b"node", "statement", b"statement", "success", b"success"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["node", b"node"]) -> typing.Literal["statement", "feature"] | None: ... global___Match = Match -@typing_extensions.final +@typing.final class MatchFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -874,13 +879,13 @@ class MatchFeature(google.protobuf.message.Message): match: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "match", b"match", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "match", b"match", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___MatchFeature = MatchFeature -@typing_extensions.final +@typing.final class Metadata(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -895,6 +900,7 @@ class Metadata(google.protobuf.message.Message): timestamp: builtins.str """iso8601 format, like: 2019-01-01T00:00:00Z""" version: builtins.str + flavor: global___Flavor.ValueType @property def argv(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property @@ -904,10 +910,11 @@ class Metadata(google.protobuf.message.Message): """deprecated in v7.0. use analysis2 instead. """ - flavor: global___Flavor.ValueType + @property def static_analysis(self) -> global___StaticAnalysis: """use analysis2 instead of analysis (deprecated in v7.0).""" + @property def dynamic_analysis(self) -> global___DynamicAnalysis: ... def __init__( @@ -922,13 +929,13 @@ class Metadata(google.protobuf.message.Message): static_analysis: global___StaticAnalysis | None = ..., dynamic_analysis: global___DynamicAnalysis | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "analysis2", b"analysis2", "dynamic_analysis", b"dynamic_analysis", "sample", b"sample", "static_analysis", b"static_analysis"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["analysis", b"analysis", "analysis2", b"analysis2", "argv", b"argv", "dynamic_analysis", b"dynamic_analysis", "flavor", b"flavor", "sample", b"sample", "static_analysis", b"static_analysis", "timestamp", b"timestamp", "version", b"version"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["analysis2", b"analysis2"]) -> typing_extensions.Literal["static_analysis", "dynamic_analysis"] | None: ... + def HasField(self, field_name: typing.Literal["analysis", b"analysis", "analysis2", b"analysis2", "dynamic_analysis", b"dynamic_analysis", "sample", b"sample", "static_analysis", b"static_analysis"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["analysis", b"analysis", "analysis2", b"analysis2", "argv", b"argv", "dynamic_analysis", b"dynamic_analysis", "flavor", b"flavor", "sample", b"sample", "static_analysis", b"static_analysis", "timestamp", b"timestamp", "version", b"version"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["analysis2", b"analysis2"]) -> typing.Literal["static_analysis", "dynamic_analysis"] | None: ... global___Metadata = Metadata -@typing_extensions.final +@typing.final class MnemonicFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -945,13 +952,13 @@ class MnemonicFeature(google.protobuf.message.Message): mnemonic: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "mnemonic", b"mnemonic", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "mnemonic", b"mnemonic", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___MnemonicFeature = MnemonicFeature -@typing_extensions.final +@typing.final class NamespaceFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -968,13 +975,13 @@ class NamespaceFeature(google.protobuf.message.Message): namespace: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "namespace", b"namespace", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "namespace", b"namespace", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___NamespaceFeature = NamespaceFeature -@typing_extensions.final +@typing.final class NumberFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -982,10 +989,11 @@ class NumberFeature(google.protobuf.message.Message): NUMBER_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int type: builtins.str + description: builtins.str @property def number(self) -> global___Number: """this can be positive (range: u64), negative (range: i64), or a double.""" - description: builtins.str + def __init__( self, *, @@ -993,13 +1001,13 @@ class NumberFeature(google.protobuf.message.Message): number: global___Number | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "number", b"number"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "number", b"number", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "number", b"number"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "number", b"number", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___NumberFeature = NumberFeature -@typing_extensions.final +@typing.final class OSFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1016,13 +1024,13 @@ class OSFeature(google.protobuf.message.Message): os: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "os", b"os", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "os", b"os", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___OSFeature = OSFeature -@typing_extensions.final +@typing.final class OffsetFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1030,10 +1038,11 @@ class OffsetFeature(google.protobuf.message.Message): OFFSET_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int type: builtins.str + description: builtins.str @property def offset(self) -> global___Integer: """offset can be negative""" - description: builtins.str + def __init__( self, *, @@ -1041,13 +1050,13 @@ class OffsetFeature(google.protobuf.message.Message): offset: global___Integer | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "offset", b"offset"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "offset", b"offset", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "offset", b"offset"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "offset", b"offset", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___OffsetFeature = OffsetFeature -@typing_extensions.final +@typing.final class OperandNumberFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1057,10 +1066,11 @@ class OperandNumberFeature(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int type: builtins.str index: builtins.int + description: builtins.str @property def operand_number(self) -> global___Integer: """this can be positive (range: u64), negative (range: i64), or a double.""" - description: builtins.str + def __init__( self, *, @@ -1069,13 +1079,13 @@ class OperandNumberFeature(google.protobuf.message.Message): operand_number: global___Integer | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "operand_number", b"operand_number"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "index", b"index", "operand_number", b"operand_number", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "operand_number", b"operand_number"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "index", b"index", "operand_number", b"operand_number", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___OperandNumberFeature = OperandNumberFeature -@typing_extensions.final +@typing.final class OperandOffsetFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1085,9 +1095,9 @@ class OperandOffsetFeature(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int type: builtins.str index: builtins.int + description: builtins.str @property def operand_offset(self) -> global___Integer: ... - description: builtins.str def __init__( self, *, @@ -1096,44 +1106,44 @@ class OperandOffsetFeature(google.protobuf.message.Message): operand_offset: global___Integer | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "operand_offset", b"operand_offset"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "index", b"index", "operand_offset", b"operand_offset", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "operand_offset", b"operand_offset"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "index", b"index", "operand_offset", b"operand_offset", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___OperandOffsetFeature = OperandOffsetFeature -@typing_extensions.final +@typing.final class ProcessFeatureCount(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ADDRESS_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int + count: builtins.int @property def address(self) -> global___Address: ... - count: builtins.int def __init__( self, *, address: global___Address | None = ..., count: builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "count", b"count"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "count", b"count"]) -> None: ... global___ProcessFeatureCount = ProcessFeatureCount -@typing_extensions.final +@typing.final class ProcessLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ADDRESS_FIELD_NUMBER: builtins.int MATCHED_THREADS_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int + name: builtins.str @property def address(self) -> global___Address: ... @property def matched_threads(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ThreadLayout]: ... - name: builtins.str def __init__( self, *, @@ -1141,12 +1151,12 @@ class ProcessLayout(google.protobuf.message.Message): matched_threads: collections.abc.Iterable[global___ThreadLayout] | None = ..., name: builtins.str = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "matched_threads", b"matched_threads", "name", b"name"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "matched_threads", b"matched_threads", "name", b"name"]) -> None: ... global___ProcessLayout = ProcessLayout -@typing_extensions.final +@typing.final class PropertyFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1167,16 +1177,16 @@ class PropertyFeature(google.protobuf.message.Message): access: builtins.str | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_access", b"_access", "_description", b"_description", "access", b"access", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_access", b"_access", "_description", b"_description", "access", b"access", "description", b"description", "property_", b"property_", "type", b"type"]) -> None: ... + def HasField(self, field_name: typing.Literal["_access", b"_access", "_description", b"_description", "access", b"access", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_access", b"_access", "_description", b"_description", "access", b"access", "description", b"description", "property_", b"property_", "type", b"type"]) -> None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_access", b"_access"]) -> typing_extensions.Literal["access"] | None: ... + def WhichOneof(self, oneof_group: typing.Literal["_access", b"_access"]) -> typing.Literal["access"] | None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___PropertyFeature = PropertyFeature -@typing_extensions.final +@typing.final class RangeStatement(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1188,10 +1198,11 @@ class RangeStatement(google.protobuf.message.Message): type: builtins.str min: builtins.int max: builtins.int + description: builtins.str @property def child(self) -> global___FeatureNode: """reusing FeatureNode here to avoid duplication and list all features OSFeature, ArchFeature, ... again.""" - description: builtins.str + def __init__( self, *, @@ -1201,13 +1212,13 @@ class RangeStatement(google.protobuf.message.Message): child: global___FeatureNode | None = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "child", b"child", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "child", b"child", "description", b"description", "max", b"max", "min", b"min", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "child", b"child", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "child", b"child", "description", b"description", "max", b"max", "min", b"min", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___RangeStatement = RangeStatement -@typing_extensions.final +@typing.final class RegexFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1224,17 +1235,17 @@ class RegexFeature(google.protobuf.message.Message): regex: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "regex", b"regex", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "regex", b"regex", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___RegexFeature = RegexFeature -@typing_extensions.final +@typing.final class ResultDocument(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - @typing_extensions.final + @typing.final class RulesEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1249,8 +1260,8 @@ class ResultDocument(google.protobuf.message.Message): key: builtins.str = ..., value: global___RuleMatches | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... META_FIELD_NUMBER: builtins.int RULES_FIELD_NUMBER: builtins.int @@ -1264,21 +1275,21 @@ class ResultDocument(google.protobuf.message.Message): meta: global___Metadata | None = ..., rules: collections.abc.Mapping[builtins.str, global___RuleMatches] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "rules", b"rules"]) -> None: ... + def HasField(self, field_name: typing.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["meta", b"meta", "rules", b"rules"]) -> None: ... global___ResultDocument = ResultDocument -@typing_extensions.final +@typing.final class RuleMatches(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor META_FIELD_NUMBER: builtins.int SOURCE_FIELD_NUMBER: builtins.int MATCHES_FIELD_NUMBER: builtins.int + source: builtins.str @property def meta(self) -> global___RuleMetadata: ... - source: builtins.str @property def matches(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Pair_Address_Match]: ... def __init__( @@ -1288,12 +1299,12 @@ class RuleMatches(google.protobuf.message.Message): source: builtins.str = ..., matches: collections.abc.Iterable[global___Pair_Address_Match] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["matches", b"matches", "meta", b"meta", "source", b"source"]) -> None: ... + def HasField(self, field_name: typing.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["matches", b"matches", "meta", b"meta", "source", b"source"]) -> None: ... global___RuleMatches = RuleMatches -@typing_extensions.final +@typing.final class RuleMetadata(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1312,12 +1323,15 @@ class RuleMetadata(google.protobuf.message.Message): SCOPES_FIELD_NUMBER: builtins.int name: builtins.str namespace: builtins.str - @property - def authors(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... scope: global___Scope.ValueType """deprecated in v7.0. use scopes instead. """ + description: builtins.str + lib: builtins.bool + is_subscope_rule: builtins.bool + @property + def authors(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property def attack(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AttackSpec]: ... @property @@ -1326,14 +1340,12 @@ class RuleMetadata(google.protobuf.message.Message): def references(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property def examples(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... - description: builtins.str - lib: builtins.bool @property def maec(self) -> global___MaecMetadata: ... - is_subscope_rule: builtins.bool @property def scopes(self) -> global___Scopes: """use scopes over scope (deprecated in v7.0).""" + def __init__( self, *, @@ -1351,12 +1363,12 @@ class RuleMetadata(google.protobuf.message.Message): is_subscope_rule: builtins.bool = ..., scopes: global___Scopes | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["maec", b"maec", "scopes", b"scopes"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attack", b"attack", "authors", b"authors", "description", b"description", "examples", b"examples", "is_subscope_rule", b"is_subscope_rule", "lib", b"lib", "maec", b"maec", "mbc", b"mbc", "name", b"name", "namespace", b"namespace", "references", b"references", "scope", b"scope", "scopes", b"scopes"]) -> None: ... + def HasField(self, field_name: typing.Literal["maec", b"maec", "scopes", b"scopes"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["attack", b"attack", "authors", b"authors", "description", b"description", "examples", b"examples", "is_subscope_rule", b"is_subscope_rule", "lib", b"lib", "maec", b"maec", "mbc", b"mbc", "name", b"name", "namespace", b"namespace", "references", b"references", "scope", b"scope", "scopes", b"scopes"]) -> None: ... global___RuleMetadata = RuleMetadata -@typing_extensions.final +@typing.final class Sample(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1376,11 +1388,11 @@ class Sample(google.protobuf.message.Message): sha256: builtins.str = ..., path: builtins.str = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["md5", b"md5", "path", b"path", "sha1", b"sha1", "sha256", b"sha256"]) -> None: ... + def ClearField(self, field_name: typing.Literal["md5", b"md5", "path", b"path", "sha1", b"sha1", "sha256", b"sha256"]) -> None: ... global___Sample = Sample -@typing_extensions.final +@typing.final class Scopes(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1394,16 +1406,16 @@ class Scopes(google.protobuf.message.Message): static: global___Scope.ValueType | None = ..., dynamic: global___Scope.ValueType | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> None: ... + def HasField(self, field_name: typing.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_dynamic", b"_dynamic", "_static", b"_static", "dynamic", b"dynamic", "static", b"static"]) -> None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_dynamic", b"_dynamic"]) -> typing_extensions.Literal["dynamic"] | None: ... + def WhichOneof(self, oneof_group: typing.Literal["_dynamic", b"_dynamic"]) -> typing.Literal["dynamic"] | None: ... @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_static", b"_static"]) -> typing_extensions.Literal["static"] | None: ... + def WhichOneof(self, oneof_group: typing.Literal["_static", b"_static"]) -> typing.Literal["static"] | None: ... global___Scopes = Scopes -@typing_extensions.final +@typing.final class SectionFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1420,13 +1432,13 @@ class SectionFeature(google.protobuf.message.Message): section: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "section", b"section", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "section", b"section", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___SectionFeature = SectionFeature -@typing_extensions.final +@typing.final class SomeStatement(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1443,13 +1455,13 @@ class SomeStatement(google.protobuf.message.Message): count: builtins.int = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "count", b"count", "description", b"description", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "count", b"count", "description", b"description", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___SomeStatement = SomeStatement -@typing_extensions.final +@typing.final class StatementNode(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1476,13 +1488,13 @@ class StatementNode(google.protobuf.message.Message): subscope: global___SubscopeStatement | None = ..., compound: global___CompoundStatement | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["compound", b"compound", "range", b"range", "some", b"some", "statement", b"statement", "subscope", b"subscope"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["compound", b"compound", "range", b"range", "some", b"some", "statement", b"statement", "subscope", b"subscope", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["statement", b"statement"]) -> typing_extensions.Literal["range", "some", "subscope", "compound"] | None: ... + def HasField(self, field_name: typing.Literal["compound", b"compound", "range", b"range", "some", b"some", "statement", b"statement", "subscope", b"subscope"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["compound", b"compound", "range", b"range", "some", b"some", "statement", b"statement", "subscope", b"subscope", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["statement", b"statement"]) -> typing.Literal["range", "some", "subscope", "compound"] | None: ... global___StatementNode = StatementNode -@typing_extensions.final +@typing.final class StaticAnalysis(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1522,12 +1534,12 @@ class StaticAnalysis(google.protobuf.message.Message): feature_counts: global___StaticFeatureCounts | None = ..., library_functions: collections.abc.Iterable[global___LibraryFunction] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["base_address", b"base_address", "feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["arch", b"arch", "base_address", b"base_address", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "library_functions", b"library_functions", "os", b"os", "rules", b"rules"]) -> None: ... + def HasField(self, field_name: typing.Literal["base_address", b"base_address", "feature_counts", b"feature_counts", "layout", b"layout"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["arch", b"arch", "base_address", b"base_address", "extractor", b"extractor", "feature_counts", b"feature_counts", "format", b"format", "layout", b"layout", "library_functions", b"library_functions", "os", b"os", "rules", b"rules"]) -> None: ... global___StaticAnalysis = StaticAnalysis -@typing_extensions.final +@typing.final class StaticFeatureCounts(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1542,11 +1554,11 @@ class StaticFeatureCounts(google.protobuf.message.Message): file: builtins.int = ..., functions: collections.abc.Iterable[global___FunctionFeatureCount] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "functions", b"functions"]) -> None: ... + def ClearField(self, field_name: typing.Literal["file", b"file", "functions", b"functions"]) -> None: ... global___StaticFeatureCounts = StaticFeatureCounts -@typing_extensions.final +@typing.final class StaticLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1558,11 +1570,11 @@ class StaticLayout(google.protobuf.message.Message): *, functions: collections.abc.Iterable[global___FunctionLayout] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["functions", b"functions"]) -> None: ... + def ClearField(self, field_name: typing.Literal["functions", b"functions"]) -> None: ... global___StaticLayout = StaticLayout -@typing_extensions.final +@typing.final class StringFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1579,13 +1591,13 @@ class StringFeature(google.protobuf.message.Message): string: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "string", b"string", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "string", b"string", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___StringFeature = StringFeature -@typing_extensions.final +@typing.final class SubscopeStatement(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1602,13 +1614,13 @@ class SubscopeStatement(google.protobuf.message.Message): scope: global___Scope.ValueType = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "scope", b"scope", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "scope", b"scope", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___SubscopeStatement = SubscopeStatement -@typing_extensions.final +@typing.final class SubstringFeature(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1625,33 +1637,33 @@ class SubstringFeature(google.protobuf.message.Message): substring: builtins.str = ..., description: builtins.str | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_description", b"_description", "description", b"description", "substring", b"substring", "type", b"type"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["_description", b"_description"]) -> typing_extensions.Literal["description"] | None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "substring", b"substring", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... global___SubstringFeature = SubstringFeature -@typing_extensions.final +@typing.final class CallLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ADDRESS_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int + name: builtins.str @property def address(self) -> global___Address: ... - name: builtins.str def __init__( self, *, address: global___Address | None = ..., name: builtins.str = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "name", b"name"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "name", b"name"]) -> None: ... global___CallLayout = CallLayout -@typing_extensions.final +@typing.final class ThreadLayout(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1667,12 +1679,12 @@ class ThreadLayout(google.protobuf.message.Message): address: global___Address | None = ..., matched_calls: collections.abc.Iterable[global___CallLayout] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "matched_calls", b"matched_calls"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "matched_calls", b"matched_calls"]) -> None: ... global___ThreadLayout = ThreadLayout -@typing_extensions.final +@typing.final class Addresses(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1684,11 +1696,11 @@ class Addresses(google.protobuf.message.Message): *, address: collections.abc.Iterable[global___Address] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address"]) -> None: ... + def ClearField(self, field_name: typing.Literal["address", b"address"]) -> None: ... global___Addresses = Addresses -@typing_extensions.final +@typing.final class Pair_Address_Match(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1704,33 +1716,33 @@ class Pair_Address_Match(google.protobuf.message.Message): address: global___Address | None = ..., match: global___Match | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["address", b"address", "match", b"match"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "match", b"match"]) -> None: ... + def HasField(self, field_name: typing.Literal["address", b"address", "match", b"match"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["address", b"address", "match", b"match"]) -> None: ... global___Pair_Address_Match = Pair_Address_Match -@typing_extensions.final +@typing.final class Token_Offset(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor TOKEN_FIELD_NUMBER: builtins.int OFFSET_FIELD_NUMBER: builtins.int - @property - def token(self) -> global___Integer: ... offset: builtins.int """offset is always >= 0""" + @property + def token(self) -> global___Integer: ... def __init__( self, *, token: global___Integer | None = ..., offset: builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["token", b"token"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["offset", b"offset", "token", b"token"]) -> None: ... + def HasField(self, field_name: typing.Literal["token", b"token"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["offset", b"offset", "token", b"token"]) -> None: ... global___Token_Offset = Token_Offset -@typing_extensions.final +@typing.final class Ppid_Pid(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1746,12 +1758,12 @@ class Ppid_Pid(google.protobuf.message.Message): ppid: global___Integer | None = ..., pid: global___Integer | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid"]) -> None: ... + def HasField(self, field_name: typing.Literal["pid", b"pid", "ppid", b"ppid"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["pid", b"pid", "ppid", b"ppid"]) -> None: ... global___Ppid_Pid = Ppid_Pid -@typing_extensions.final +@typing.final class Ppid_Pid_Tid(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1771,12 +1783,12 @@ class Ppid_Pid_Tid(google.protobuf.message.Message): pid: global___Integer | None = ..., tid: global___Integer | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ... + def HasField(self, field_name: typing.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ... global___Ppid_Pid_Tid = Ppid_Pid_Tid -@typing_extensions.final +@typing.final class Ppid_Pid_Tid_Id(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1800,12 +1812,12 @@ class Ppid_Pid_Tid_Id(google.protobuf.message.Message): tid: global___Integer | None = ..., id: global___Integer | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ... + def HasField(self, field_name: typing.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["id", b"id", "pid", b"pid", "ppid", b"ppid", "tid", b"tid"]) -> None: ... global___Ppid_Pid_Tid_Id = Ppid_Pid_Tid_Id -@typing_extensions.final +@typing.final class Integer(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1819,13 +1831,13 @@ class Integer(google.protobuf.message.Message): u: builtins.int = ..., i: builtins.int = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["i", b"i", "u", b"u", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["i", b"i", "u", b"u", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["u", "i"] | None: ... + def HasField(self, field_name: typing.Literal["i", b"i", "u", b"u", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["i", b"i", "u", b"u", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["value", b"value"]) -> typing.Literal["u", "i"] | None: ... global___Integer = Integer -@typing_extensions.final +@typing.final class Number(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -1842,8 +1854,8 @@ class Number(google.protobuf.message.Message): i: builtins.int = ..., f: builtins.float = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["f", b"f", "i", b"i", "u", b"u", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["f", b"f", "i", b"i", "u", b"u", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["u", "i", "f"] | None: ... + def HasField(self, field_name: typing.Literal["f", b"f", "i", b"i", "u", b"u", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["f", b"f", "i", b"i", "u", b"u", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["value", b"value"]) -> typing.Literal["u", "i", "f"] | None: ... global___Number = Number diff --git a/capa/render/verbose.py b/capa/render/verbose.py index 08c0707cb..a89db5039 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -126,6 +126,16 @@ def render_thread(layout: rd.DynamicLayout, addr: frz.Address) -> str: return f"{name}{{pid:{thread.process.pid},tid:{thread.tid}}}" +def render_sequence(layout: rd.DynamicLayout, addrs: list[frz.Address]) -> str: + calls: list[capa.features.address.DynamicCallAddress] = [addr.to_capa() for addr in addrs] # type: ignore + for call in calls: + assert isinstance(call, capa.features.address.DynamicCallAddress) + + pname = _get_process_name(layout, frz.Address.from_capa(calls[0].thread.process)) + call_ids = [str(call.id) for call in calls] + return f"{pname}{{pid:{call.thread.process.pid},tid:{call.thread.tid},calls:{{{','.join(call_ids)}}}}}" + + def render_call(layout: rd.DynamicLayout, addr: frz.Address) -> str: call = addr.to_capa() assert isinstance(call, capa.features.address.DynamicCallAddress) @@ -318,7 +328,7 @@ def render_rules(console: Console, doc: rd.ResultDocument): lines = [render_process(doc.meta.analysis.layout, loc) for loc in locations] elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD: lines = [render_thread(doc.meta.analysis.layout, loc) for loc in locations] - elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL: + elif rule.meta.scopes.dynamic in (capa.rules.Scope.CALL, capa.rules.Scope.SEQUENCE): # because we're only in verbose mode, we won't show the full call details (name, args, retval) # we'll only show the details of the thread in which the calls are found. # so select the thread locations and render those. diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index 428dffcbc..5a2f1718d 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -311,6 +311,36 @@ def render_match( render_match(console, layout, rule, child, indent=indent + 1, mode=child_mode) +def collect_call_locations( + match: rd.Match, + mode=MODE_SUCCESS, +): + """ + Find all the DynamicCallAddress locations in the given match, recursively. + Useful to collect the calls used to match a sequence scoped rule. + """ + if isinstance(match.node, rd.StatementNode): + if ( + isinstance(match.node.statement, rd.CompoundStatement) + and match.node.statement.type == rd.CompoundStatementType.NOT + ): + child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS + for child in match.children: + yield from collect_call_locations(child, child_mode) + else: + for child in match.children: + yield from collect_call_locations(child, mode) + elif isinstance(match.node, rd.FeatureNode): + for location in match.locations: + if location.type != frz.AddressType.CALL: + continue + if mode == MODE_FAILURE: + continue + yield location + else: + raise ValueError("unexpected node type") + + def render_rules(console: Console, doc: rd.ResultDocument): """ like: @@ -450,6 +480,9 @@ def render_rules(console: Console, doc: rd.ResultDocument): console.write(v.render_process(doc.meta.analysis.layout, location)) elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD: console.write(v.render_thread(doc.meta.analysis.layout, location)) + elif rule.meta.scopes.dynamic == capa.rules.Scope.SEQUENCE: + calls = sorted(set(collect_call_locations(match))) + console.write(hanging_indent(v.render_sequence(doc.meta.analysis.layout, calls), indent=1)) elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL: console.write(hanging_indent(v.render_call(doc.meta.analysis.layout, location), indent=1)) else: diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 54b8f9baf..2b0813b64 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -86,6 +86,7 @@ class Scope(str, Enum): FILE = "file" PROCESS = "process" THREAD = "thread" + SEQUENCE = "sequence" CALL = "call" FUNCTION = "function" BASIC_BLOCK = "basic block" @@ -114,6 +115,7 @@ def to_yaml(cls, representer, node): Scope.GLOBAL, Scope.PROCESS, Scope.THREAD, + Scope.SEQUENCE, Scope.CALL, } @@ -199,6 +201,7 @@ def from_dict(self, scopes: dict[str, str]) -> "Scopes": capa.features.common.MatchedRule, }, Scope.THREAD: set(), + Scope.SEQUENCE: set(), Scope.CALL: { capa.features.common.MatchedRule, capa.features.common.Regex, @@ -253,11 +256,14 @@ def from_dict(self, scopes: dict[str, str]) -> "Scopes": SUPPORTED_FEATURES[Scope.FILE].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.GLOBAL]) +SUPPORTED_FEATURES[Scope.SEQUENCE].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.CALL].update(SUPPORTED_FEATURES[Scope.GLOBAL]) -# all call scope features are also thread features -SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.CALL]) +# all call scope features are also sequence features +SUPPORTED_FEATURES[Scope.SEQUENCE].update(SUPPORTED_FEATURES[Scope.CALL]) +# all sequence scope features (and therefore, call features) are also thread features +SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.SEQUENCE]) # all thread scope features are also process features SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.THREAD]) @@ -636,8 +642,19 @@ def build_statements(d, scopes: Scopes): Scope.THREAD, build_statements(d[key][0], Scopes(dynamic=Scope.THREAD)), description=description ) + elif key == "sequence": + if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD)): + raise InvalidRule("sequence subscope supported only for the process and thread scopes") + + if len(d[key]) != 1: + raise InvalidRule("subscope must have exactly one child statement") + + return ceng.Subscope( + Scope.SEQUENCE, build_statements(d[key][0], Scopes(dynamic=Scope.SEQUENCE)), description=description + ) + elif key == "call": - if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.CALL)): + if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SEQUENCE, Scope.CALL)): raise InvalidRule("call subscope supported only for the process, thread, and call scopes") if len(d[key]) != 1: @@ -1383,6 +1400,7 @@ def __init__( scopes = ( Scope.CALL, + Scope.SEQUENCE, Scope.THREAD, Scope.PROCESS, Scope.INSTRUCTION, @@ -1413,6 +1431,10 @@ def process_rules(self): def thread_rules(self): return self.rules_by_scope[Scope.THREAD] + @property + def sequence_rules(self): + return self.rules_by_scope[Scope.SEQUENCE] + @property def call_rules(self): return self.rules_by_scope[Scope.CALL] diff --git a/scripts/lint.py b/scripts/lint.py index 8f8627b56..7bf432e08 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -194,6 +194,7 @@ def check_rule(self, ctx: Context, rule: Rule): "file", "process", "thread", + "sequence", "call", "unsupported", ) diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_sequence_scope.py new file mode 100644 index 000000000..810dc5b34 --- /dev/null +++ b/tests/test_dynamic_sequence_scope.py @@ -0,0 +1,256 @@ +# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# Unless required by applicable law or agreed to in writing, software distributed under the License +# 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. + + +# tests/data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# ... +# thread: 3064 +# call 8: GetSystemTimeAsFileTime() +# call 9: GetSystemInfo() +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: LdrGetDllHandle(1974337536, kernel32.dll) +# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) +# call 15: GetSystemTime() +# call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) +# call 17: NtAllocateVirtualMemory(no, 4, 12288, 4784128, 4294967295) +# call 18: GetSystemInfo() +# ... +# ... + +import textwrap +from functools import lru_cache + +import fixtures + +import capa.main +import capa.capabilities.dynamic +from capa.features.extractors.base_extractor import ThreadFilter, DynamicFeatureExtractor + + +def filter_threads(extractor: DynamicFeatureExtractor, ppid: int, pid: int, tid: int) -> DynamicFeatureExtractor: + for ph in extractor.get_processes(): + if (ph.address.ppid, ph.address.pid) != (ppid, pid): + continue + + for th in extractor.get_threads(ph): + if th.address.tid != tid: + continue + + return ThreadFilter( + extractor, + { + th.address, + }, + ) + + raise ValueError("failed to find target thread") + + +@lru_cache(maxsize=1) +def get_0000a657_thread3064(): + extractor = fixtures.get_cape_extractor(fixtures.get_data_path_by_name("0000a657")) + extractor = filter_threads(extractor, 2456, 3052, 3064) + return extractor + + +def get_call_ids(matches): + for address, _ in matches: + yield address.id + + +# sanity check: match the first call +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# call 8: GetSystemTimeAsFileTime() +def test_dynamic_call_scope(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: call + features: + - api: GetSystemTimeAsFileTime + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in matches + assert 8 in get_call_ids(matches[r.name]) + + +# match the first 5-tuple sequence. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# call 8: GetSystemTimeAsFileTime() +# call 9: GetSystemInfo() +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: LdrGetDllHandle(1974337536, kernel32.dll) +def test_dynamic_sequence_scope(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - api: GetSystemTimeAsFileTime + - api: GetSystemInfo + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - count(api(LdrGetDllHandle)): 2 + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in matches + assert 12 in get_call_ids(matches[r.name]) + + +# show the sequence is only 5 calls long, and doesn't match beyond that 5-tuple. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# call 8: GetSystemTimeAsFileTime() +# call 9: GetSystemInfo() +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: LdrGetDllHandle(1974337536, kernel32.dll) +# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) +# call 15: GetSystemTime() +# call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) +def test_dynamic_sequence_scope2(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - api: GetSystemTimeAsFileTime + - api: RtlAddVectoredExceptionHandler + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name not in matches + + +# show how you might use a sequence rule: to match a small window for a collection of features. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: ... +# call 13: ... +# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) +def test_dynamic_sequence_example(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - call: + - and: + - api: LdrGetDllHandle + - string: "kernel32.dll" + - call: + - and: + - api: LdrGetProcedureAddress + - string: "AddVectoredExceptionHandler" + - api: RtlAddVectoredExceptionHandler + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in matches + assert 14 in get_call_ids(matches[r.name]) + + +# show how sequences that overlap a single event are handled. +# TODO(williballenthin): but I think we really just want one match for this, not copies of the same thing. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# ... +# call 10: ... +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: ... +# call 13: ... +# call 14: ... +# call 15: ... +# ... +def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - call: + - and: + - api: LdrGetProcedureAddress + - string: "AddVectoredExceptionHandler" + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in matches + assert [11, 12, 13, 14, 15] == list(get_call_ids(matches[r.name])) + diff --git a/tests/test_proto.py b/tests/test_proto.py index 8e6e12bd4..474efc017 100644 --- a/tests/test_proto.py +++ b/tests/test_proto.py @@ -129,6 +129,7 @@ def test_scope_to_pb2(): assert capa.render.proto.scope_to_pb2(capa.rules.Scope.INSTRUCTION) == capa_pb2.SCOPE_INSTRUCTION assert capa.render.proto.scope_to_pb2(capa.rules.Scope.PROCESS) == capa_pb2.SCOPE_PROCESS assert capa.render.proto.scope_to_pb2(capa.rules.Scope.THREAD) == capa_pb2.SCOPE_THREAD + assert capa.render.proto.scope_to_pb2(capa.rules.Scope.SEQUENCE) == capa_pb2.SCOPE_SEQUENCE assert capa.render.proto.scope_to_pb2(capa.rules.Scope.CALL) == capa_pb2.SCOPE_CALL From 294ff34a30083566d48617a999c2c9e00b8e2392 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 12 Dec 2024 15:13:10 +0000 Subject: [PATCH 05/17] sequence: only match first overlapping sequence also, for repeating behavior, match only the first instance. --- capa/capabilities/dynamic.py | 39 +++++++++++++++++++++++++++- tests/test_dynamic_sequence_scope.py | 21 +++++++++------ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index cb553e30b..8ab14ea03 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -29,6 +29,13 @@ logger = logging.getLogger(__name__) +# The number of calls that make up a sequence. +# +# The larger this is, the more calls are grouped together to match rule logic. +# This means a longer chain can be recognized; however, its a bit more expensive. +SEQUENCE_SIZE = 20 + + @dataclass class CallCapabilities: features: FeatureSet @@ -76,7 +83,8 @@ def find_thread_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle ) -> ThreadCapabilities: """ - find matches for the given rules within the given thread. + find matches for the given rules within the given thread, + which includes matches for all the sequences and calls within it. """ # all features found within this thread, # includes features found within calls. @@ -89,8 +97,18 @@ def find_thread_capabilities( # matches found at the sequence scope. sequence_matches: MatchResults = collections.defaultdict(list) + # We matches sequences as the sliding window of calls with size SEQUENCE_SIZE. + # + # For each call, we consider the window of SEQUENCE_SIZE calls leading up to it, + # merging all their features and doing a match. + # Here's the primary data structure: a deque of those features found in the prior calls. + # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. sequence: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + # the names of rules matched at the last sequence, + # so that we can deduplicate long strings of the same matche. + last_sequence_matches: set[str] = set() + for ch in extractor.get_calls(ph, th): call_capabilities = find_call_capabilities(ruleset, extractor, ph, th, ch) for feature, vas in call_capabilities.features.items(): @@ -99,7 +117,13 @@ def find_thread_capabilities( for rule_name, res in call_capabilities.matches.items(): call_matches[rule_name].extend(res) + # + # sequence scope matching + # + # as we add items to the end of the deque, the oldest items will overflow and get dropped. sequence.append(call_capabilities.features) + # collect all the features seen across the last SEQUENCE_SIZE calls, + # and match against them. sequence_features: FeatureSet = collections.defaultdict(set) for call in sequence: for feature, vas in call.items(): @@ -107,8 +131,21 @@ def find_thread_capabilities( _, smatches = ruleset.match(Scope.SEQUENCE, sequence_features, ch.address) for rule_name, res in smatches.items(): + if rule_name in last_sequence_matches: + # don't emit match results for rules seen during the immediately preceeding sequence. + # + # This means that we won't emit duplicate matches when there are multiple sequences + # that overlap a single matching event. + # It also handles the case of a tight loop containing matched logic; + # only the first match will be recorded. + # + # In theory, this means the result document doesn't have *every* possible match location, + # but in practice, humans will only be interested in the first handful anyways. + continue sequence_matches[rule_name].extend(res) + last_sequence_matches = set(smatches.keys()) + for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()): features[feature].add(va) diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_sequence_scope.py index 810dc5b34..4b423fe0d 100644 --- a/tests/test_dynamic_sequence_scope.py +++ b/tests/test_dynamic_sequence_scope.py @@ -134,7 +134,7 @@ def test_dynamic_sequence_scope(): assert 12 in get_call_ids(matches[r.name]) -# show the sequence is only 5 calls long, and doesn't match beyond that 5-tuple. +# show that when the sequence is only 5 calls long (for example), it doesn't match beyond that 5-tuple. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -168,8 +168,14 @@ def test_dynamic_sequence_scope2(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name not in matches + # patch SEQUENCE_SIZE since we may use a much larger value in the real world. + from pytest import MonkeyPatch + + with MonkeyPatch.context() as m: + m.setattr(capa.capabilities.dynamic, "SEQUENCE_SIZE", 5) + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + + assert r.name not in capabilities.matches # show how you might use a sequence rule: to match a small window for a collection of features. @@ -215,7 +221,6 @@ def test_dynamic_sequence_example(): # show how sequences that overlap a single event are handled. -# TODO(williballenthin): but I think we really just want one match for this, not copies of the same thing. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -250,7 +255,7 @@ def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in matches - assert [11, 12, 13, 14, 15] == list(get_call_ids(matches[r.name])) - + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + # we only match the first overlapping sequence + assert [11] == list(get_call_ids(capabilities.matches[r.name])) From 86908c90255ade3f63032eec8b7d02a36a3fb251 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 16 Dec 2024 13:43:56 +0000 Subject: [PATCH 06/17] sequence scope: optimize matching --- capa/capabilities/dynamic.py | 48 +++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 8ab14ea03..47029cb4d 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -18,11 +18,13 @@ import collections from dataclasses import dataclass +from capa.features.address import NO_ADDRESS import capa.perf import capa.features.freeze as frz import capa.render.result_document as rdoc from capa.rules import Scope, RuleSet from capa.engine import FeatureSet, MatchResults +from capa.features.common import Feature from capa.capabilities.common import Capabilities, find_file_capabilities from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle, DynamicFeatureExtractor @@ -101,9 +103,18 @@ def find_thread_capabilities( # # For each call, we consider the window of SEQUENCE_SIZE calls leading up to it, # merging all their features and doing a match. - # Here's the primary data structure: a deque of those features found in the prior calls. - # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. - sequence: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + # + # We track these features in two data structures: + # 1. a deque of those features found in the prior calls. + # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. + # 2. a live set of features seen in the sequence. + # As we pop from the deque, we remove features from the current set, + # and as we push to the deque, we insert features to the current set. + # With this approach, our algorithm performance is independent of SEQUENCE_SIZE. + # The naive algorithm, of merging all the trailing feature sets at each call, is dependent upon SEQUENCE_SIZE + # (that is, runtime gets slower the larger SEQUENCE_SIZE is). + sequence_feature_sets: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + sequence_features: FeatureSet = collections.defaultdict(set) # the names of rules matched at the last sequence, # so that we can deduplicate long strings of the same matche. @@ -120,14 +131,29 @@ def find_thread_capabilities( # # sequence scope matching # - # as we add items to the end of the deque, the oldest items will overflow and get dropped. - sequence.append(call_capabilities.features) - # collect all the features seen across the last SEQUENCE_SIZE calls, - # and match against them. - sequence_features: FeatureSet = collections.defaultdict(set) - for call in sequence: - for feature, vas in call.items(): - sequence_features[feature].update(vas) + # As we add items to the end of the deque, overflow and drop the oldest items (at the left end). + # While we could rely on `deque.append` with `maxlen` set (which we provide above), + # we want to use the dropped item first, to remove the old features, so we manually pop it here. + if len(sequence_feature_sets) == SEQUENCE_SIZE: + overflowing_feature_set = sequence_feature_sets.popleft() + + # these are the top-level features that will no longer have any associated addresses. + for feature, vas in overflowing_feature_set.items(): + if vas == { NO_ADDRESS, }: + # ignore the common case of global features getting added/removed/trimmed repeatedly, + # like arch/os/format. + continue + + feature_vas = sequence_features[feature] + feature_vas.difference_update(vas) + if not feature_vas: + del sequence_features[feature] + + # update the deque and set of features with the latest call's worth of features. + latest_features = call_capabilities.features + sequence_feature_sets.append(latest_features) + for feature, vas in latest_features.items(): + sequence_features[feature].update(vas) _, smatches = ruleset.match(Scope.SEQUENCE, sequence_features, ch.address) for rule_name, res in smatches.items(): From 39319c57a4d7450c87bcc9bb4e670fb3f79d759b Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 16 Dec 2024 15:51:35 +0000 Subject: [PATCH 07/17] sequence: documentation and tests sequence: add more tests --- capa/capabilities/dynamic.py | 10 +- capa/features/extractors/base_extractor.py | 6 +- capa/rules/__init__.py | 2 +- tests/test_dynamic_sequence_scope.py | 111 ++++++++++++++++++++- 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 47029cb4d..39b538c5b 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -18,13 +18,12 @@ import collections from dataclasses import dataclass -from capa.features.address import NO_ADDRESS import capa.perf import capa.features.freeze as frz import capa.render.result_document as rdoc from capa.rules import Scope, RuleSet from capa.engine import FeatureSet, MatchResults -from capa.features.common import Feature +from capa.features.address import _NoAddress from capa.capabilities.common import Capabilities, find_file_capabilities from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle, DynamicFeatureExtractor @@ -137,9 +136,10 @@ def find_thread_capabilities( if len(sequence_feature_sets) == SEQUENCE_SIZE: overflowing_feature_set = sequence_feature_sets.popleft() - # these are the top-level features that will no longer have any associated addresses. for feature, vas in overflowing_feature_set.items(): - if vas == { NO_ADDRESS, }: + if len(vas) == 1 and isinstance(next(iter(vas)), _NoAddress): + # `vas == { NO_ADDRESS }` without the garbage. + # # ignore the common case of global features getting added/removed/trimmed repeatedly, # like arch/os/format. continue @@ -238,7 +238,7 @@ def find_process_capabilities( def find_dynamic_capabilities( - ruleset: RuleSet, extractor: DynamicFeatureExtractor, disable_progress=None + ruleset: RuleSet, extractor: DynamicFeatureExtractor, disable_progress: bool = False ) -> Capabilities: all_process_matches: MatchResults = collections.defaultdict(list) all_thread_matches: MatchResults = collections.defaultdict(list) diff --git a/capa/features/extractors/base_extractor.py b/capa/features/extractors/base_extractor.py index 35c9b665c..1be52d06b 100644 --- a/capa/features/extractors/base_extractor.py +++ b/capa/features/extractors/base_extractor.py @@ -488,11 +488,11 @@ def get_call_name(self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> raise NotImplementedError() -def ProcessFilter(extractor: DynamicFeatureExtractor, processes: set) -> DynamicFeatureExtractor: +def ProcessFilter(extractor: DynamicFeatureExtractor, pids: set[int]) -> DynamicFeatureExtractor: original_get_processes = extractor.get_processes def filtered_get_processes(self): - yield from (f for f in original_get_processes() if f.address.pid in processes) + yield from (f for f in original_get_processes() if f.address.pid in pids) # we make a copy of the original extractor object and then update its get_processes() method with the decorated filter one. # this is in order to preserve the original extractor object's get_processes() method, in case it is used elsewhere in the code. @@ -504,7 +504,7 @@ def filtered_get_processes(self): return new_extractor -def ThreadFilter(extractor: DynamicFeatureExtractor, threads: set) -> DynamicFeatureExtractor: +def ThreadFilter(extractor: DynamicFeatureExtractor, threads: set[Address]) -> DynamicFeatureExtractor: original_get_threads = extractor.get_threads def filtered_get_threads(self, ph: ProcessHandle): diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 2b0813b64..2ca4e0bc6 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -643,7 +643,7 @@ def build_statements(d, scopes: Scopes): ) elif key == "sequence": - if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD)): + if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SEQUENCE)): raise InvalidRule("sequence subscope supported only for the process and thread scopes") if len(d[key]) != 1: diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_sequence_scope.py index 4b423fe0d..09ba62bc6 100644 --- a/tests/test_dynamic_sequence_scope.py +++ b/tests/test_dynamic_sequence_scope.py @@ -27,11 +27,14 @@ # ... import textwrap +from typing import Iterator from functools import lru_cache +import pytest import fixtures import capa.main +import capa.rules import capa.capabilities.dynamic from capa.features.extractors.base_extractor import ThreadFilter, DynamicFeatureExtractor @@ -62,7 +65,7 @@ def get_0000a657_thread3064(): return extractor -def get_call_ids(matches): +def get_call_ids(matches) -> Iterator[int]: for address, _ in matches: yield address.id @@ -96,7 +99,7 @@ def test_dynamic_call_scope(): assert 8 in get_call_ids(matches[r.name]) -# match the first 5-tuple sequence. +# match the first sequence. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -147,7 +150,7 @@ def test_dynamic_sequence_scope(): # call 14: RtlAddVectoredExceptionHandler(1921490089, 0) # call 15: GetSystemTime() # call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) -def test_dynamic_sequence_scope2(): +def test_dynamic_sequence_scope_length(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -178,6 +181,108 @@ def test_dynamic_sequence_scope2(): assert r.name not in capabilities.matches +# show that you can use a call subscope in sequence rules. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# ... +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# ... +def test_dynamic_sequence_call_subscope(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - call: + - and: + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + assert 11 in get_call_ids(capabilities.matches[r.name]) + + +# show that you can use a sequence subscope in sequence rules. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# ... +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: LdrGetDllHandle(1974337536, kernel32.dll) +# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) +# ... +def test_dynamic_sequence_scope_sequence_subscope(): + extractor = get_0000a657_thread3064() + + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - sequence: + - description: resolve add VEH # should match at 11 + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + - sequence: + - description: resolve remove VEH # should match at 13 + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: RemoveVectoredExceptionHandler + """ + ) + + r = capa.rules.Rule.from_yaml(rule) + ruleset = capa.rules.RuleSet([r]) + + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + assert 13 in get_call_ids(capabilities.matches[r.name]) + + +# show that you can't use thread subscope in sequence rules. +def test_dynamic_sequence_scope_thread_subscope(): + rule = textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - thread: + - string: "foo" + """ + ) + + with pytest.raises(capa.rules.InvalidRule): + capa.rules.Rule.from_yaml(rule) + + # show how you might use a sequence rule: to match a small window for a collection of features. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) From f55086c2121011948d1beeb378e81ab049d73599 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Wed, 18 Dec 2024 12:54:11 +0000 Subject: [PATCH 08/17] sequence: refactor into SequenceMatcher contains the call ids for all the calls within the sequence, so we know where to look for related matched. sequence: refactor SequenceMatcher sequence: don't use sequence addresses sequence: remove sequence address --- capa/capabilities/dynamic.py | 175 ++++++++++++++++----------- capa/features/address.py | 3 +- capa/features/freeze/__init__.py | 9 +- capa/render/proto/capa_pb2.py | 23 +++- capa/render/proto/capa_pb2.pyi | 13 ++ capa/render/result_document.py | 48 +++++++- capa/render/verbose.py | 1 - capa/render/vverbose.py | 17 +-- tests/test_dynamic_sequence_scope.py | 98 +++++++++++++++ 9 files changed, 296 insertions(+), 91 deletions(-) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 39b538c5b..a4e97be68 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -80,6 +80,89 @@ class ThreadCapabilities: call_matches: MatchResults +class SequenceMatcher: + def __init__(self, ruleset: RuleSet): + super().__init__() + self.ruleset = ruleset + + # matches found at the sequence scope. + self.matches: MatchResults = collections.defaultdict(list) + + # We matches sequences as the sliding window of calls with size SEQUENCE_SIZE. + # + # For each call, we consider the window of SEQUENCE_SIZE calls leading up to it, + # merging all their features and doing a match. + # + # We track these features in two data structures: + # 1. a deque of those features found in the prior calls. + # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. + # 2. a live set of features seen in the sequence. + # As we pop from the deque, we remove features from the current set, + # and as we push to the deque, we insert features to the current set. + # With this approach, our algorithm performance is independent of SEQUENCE_SIZE. + # The naive algorithm, of merging all the trailing feature sets at each call, is dependent upon SEQUENCE_SIZE + # (that is, runtime gets slower the larger SEQUENCE_SIZE is). + self.current_feature_sets: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + self.current_features: FeatureSet = collections.defaultdict(set) + + # the names of rules matched at the last sequence, + # so that we can deduplicate long strings of the same matche. + self.last_sequence_matches: set[str] = set() + + def next(self, ch: CallHandle, call_features: FeatureSet): + # As we add items to the end of the deque, overflow and drop the oldest items (at the left end). + # While we could rely on `deque.append` with `maxlen` set (which we provide above), + # we want to use the dropped item first, to remove the old features, so we manually pop it here. + if len(self.current_feature_sets) == SEQUENCE_SIZE: + overflowing_feature_set = self.current_feature_sets.popleft() + + for feature, vas in overflowing_feature_set.items(): + if len(vas) == 1 and isinstance(next(iter(vas)), _NoAddress): + # `vas == { NO_ADDRESS }` without the garbage. + # + # ignore the common case of global features getting added/removed/trimmed repeatedly, + # like arch/os/format. + continue + + feature_vas = self.current_features[feature] + feature_vas.difference_update(vas) + if not feature_vas: + del self.current_features[feature] + + # update the deque and set of features with the latest call's worth of features. + self.current_feature_sets.append(call_features) + for feature, vas in call_features.items(): + self.current_features[feature].update(vas) + + _, matches = self.ruleset.match(Scope.SEQUENCE, self.current_features, ch.address) + + newly_encountered_rules = set(matches.keys()) - self.last_sequence_matches + + # don't emit match results for rules seen during the immediately preceeding sequence. + # + # This means that we won't emit duplicate matches when there are multiple sequences + # that overlap a single matching event. + # It also handles the case of a tight loop containing matched logic; + # only the first match will be recorded. + # + # In theory, this means the result document doesn't have *every* possible match location, + # but in practice, humans will only be interested in the first handful anyways. + suppressed_rules = set(self.last_sequence_matches) + + # however, if a newly encountered rule depends on a suppressed rule, + # don't suppress that rule match, or we won't be able to reconstruct the vverbose output. + # see: https://github.com/mandiant/capa/pull/2532#issuecomment-2548508130 + for new_rule in newly_encountered_rules: + suppressed_rules -= set(self.ruleset.rules[new_rule].get_dependencies(self.ruleset.rules_by_namespace)) + + for rule_name, res in matches.items(): + if rule_name in suppressed_rules: + continue + self.matches[rule_name].extend(res) + + self.last_sequence_matches = set(matches.keys()) + + def find_thread_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle ) -> ThreadCapabilities: @@ -95,31 +178,11 @@ def find_thread_capabilities( # might be found at different calls, that's ok. call_matches: MatchResults = collections.defaultdict(list) - # matches found at the sequence scope. - sequence_matches: MatchResults = collections.defaultdict(list) - - # We matches sequences as the sliding window of calls with size SEQUENCE_SIZE. - # - # For each call, we consider the window of SEQUENCE_SIZE calls leading up to it, - # merging all their features and doing a match. - # - # We track these features in two data structures: - # 1. a deque of those features found in the prior calls. - # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. - # 2. a live set of features seen in the sequence. - # As we pop from the deque, we remove features from the current set, - # and as we push to the deque, we insert features to the current set. - # With this approach, our algorithm performance is independent of SEQUENCE_SIZE. - # The naive algorithm, of merging all the trailing feature sets at each call, is dependent upon SEQUENCE_SIZE - # (that is, runtime gets slower the larger SEQUENCE_SIZE is). - sequence_feature_sets: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) - sequence_features: FeatureSet = collections.defaultdict(set) - - # the names of rules matched at the last sequence, - # so that we can deduplicate long strings of the same matche. - last_sequence_matches: set[str] = set() + sequence_matcher = SequenceMatcher(ruleset) + call_count = 0 for ch in extractor.get_calls(ph, th): + call_count += 1 call_capabilities = find_call_capabilities(ruleset, extractor, ph, th, ch) for feature, vas in call_capabilities.features.items(): features[feature].update(vas) @@ -127,50 +190,7 @@ def find_thread_capabilities( for rule_name, res in call_capabilities.matches.items(): call_matches[rule_name].extend(res) - # - # sequence scope matching - # - # As we add items to the end of the deque, overflow and drop the oldest items (at the left end). - # While we could rely on `deque.append` with `maxlen` set (which we provide above), - # we want to use the dropped item first, to remove the old features, so we manually pop it here. - if len(sequence_feature_sets) == SEQUENCE_SIZE: - overflowing_feature_set = sequence_feature_sets.popleft() - - for feature, vas in overflowing_feature_set.items(): - if len(vas) == 1 and isinstance(next(iter(vas)), _NoAddress): - # `vas == { NO_ADDRESS }` without the garbage. - # - # ignore the common case of global features getting added/removed/trimmed repeatedly, - # like arch/os/format. - continue - - feature_vas = sequence_features[feature] - feature_vas.difference_update(vas) - if not feature_vas: - del sequence_features[feature] - - # update the deque and set of features with the latest call's worth of features. - latest_features = call_capabilities.features - sequence_feature_sets.append(latest_features) - for feature, vas in latest_features.items(): - sequence_features[feature].update(vas) - - _, smatches = ruleset.match(Scope.SEQUENCE, sequence_features, ch.address) - for rule_name, res in smatches.items(): - if rule_name in last_sequence_matches: - # don't emit match results for rules seen during the immediately preceeding sequence. - # - # This means that we won't emit duplicate matches when there are multiple sequences - # that overlap a single matching event. - # It also handles the case of a tight loop containing matched logic; - # only the first match will be recorded. - # - # In theory, this means the result document doesn't have *every* possible match location, - # but in practice, humans will only be interested in the first handful anyways. - continue - sequence_matches[rule_name].extend(res) - - last_sequence_matches = set(smatches.keys()) + sequence_matcher.next(ch, call_capabilities.features) for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()): features[feature].add(va) @@ -183,13 +203,22 @@ def find_thread_capabilities( for va, _ in res: capa.engine.index_rule_matches(features, rule, [va]) - return ThreadCapabilities(features, matches, sequence_matches, call_matches) + logger.debug( + "analyzed thread %d[%d] with %d events, %d features, and %d matches", + th.address.process.pid, + th.address.tid, + call_count, + len(features), + len(matches) + len(sequence_matcher.matches) + len(call_matches), + ) + return ThreadCapabilities(features, matches, sequence_matcher.matches, call_matches) @dataclass class ProcessCapabilities: process_matches: MatchResults thread_matches: MatchResults + sequence_matches: MatchResults call_matches: MatchResults feature_count: int @@ -234,7 +263,14 @@ def find_process_capabilities( process_features[feature].add(va) _, process_matches = ruleset.match(Scope.PROCESS, process_features, ph.address) - return ProcessCapabilities(process_matches, thread_matches, call_matches, len(process_features)) + + logger.debug( + "analyzed process %d and extracted %d features with %d matches", + ph.address.pid, + len(process_features), + len(process_matches), + ) + return ProcessCapabilities(process_matches, thread_matches, sequence_matches, call_matches, len(process_features)) def find_dynamic_capabilities( @@ -262,7 +298,6 @@ def find_dynamic_capabilities( address=frz.Address.from_capa(p.address), count=process_capabilities.feature_count ), ) - logger.debug("analyzed %s and extracted %d features", p.address, process_capabilities.feature_count) for rule_name, res in process_capabilities.process_matches.items(): all_process_matches[rule_name].extend(res) diff --git a/capa/features/address.py b/capa/features/address.py index 447b936f6..eb708a3dc 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -114,8 +114,7 @@ def __hash__(self): return hash((self.thread, self.id)) def __eq__(self, other): - assert isinstance(other, DynamicCallAddress) - return (self.thread, self.id) == (other.thread, other.id) + return isinstance(other, DynamicCallAddress) and (self.thread, self.id) == (other.thread, other.id) def __lt__(self, other): assert isinstance(other, DynamicCallAddress) diff --git a/capa/features/freeze/__init__.py b/capa/features/freeze/__init__.py index 08612f8ea..2e12d2ffd 100644 --- a/capa/features/freeze/__init__.py +++ b/capa/features/freeze/__init__.py @@ -64,7 +64,14 @@ class AddressType(str, Enum): class Address(HashableModel): type: AddressType - value: Union[int, tuple[int, ...], None] = None # None default value to support deserialization of NO_ADDRESS + value: Union[ + # for absolute, relative, file + int, + # for DNToken, Process, Thread, Call + tuple[int, ...], + # for NO_ADDRESS, + None, + ] = None # None default value to support deserialization of NO_ADDRESS @classmethod def from_capa(cls, a: capa.features.address.Address) -> "Address": diff --git a/capa/render/proto/capa_pb2.py b/capa/render/proto/capa_pb2.py index 35d017934..e695a27a7 100644 --- a/capa/render/proto/capa_pb2.py +++ b/capa/render/proto/capa_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: capa/render/proto/capa.proto +# Protobuf Python Version: 5.28.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 28, + 3, + '', + 'capa/render/proto/capa.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -18,15 +29,15 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'capa.render.proto.capa_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _globals['_MATCH_CAPTURESENTRY']._options = None +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_MATCH_CAPTURESENTRY']._loaded_options = None _globals['_MATCH_CAPTURESENTRY']._serialized_options = b'8\001' - _globals['_METADATA'].fields_by_name['analysis']._options = None + _globals['_METADATA'].fields_by_name['analysis']._loaded_options = None _globals['_METADATA'].fields_by_name['analysis']._serialized_options = b'\030\001' - _globals['_RESULTDOCUMENT_RULESENTRY']._options = None + _globals['_RESULTDOCUMENT_RULESENTRY']._loaded_options = None _globals['_RESULTDOCUMENT_RULESENTRY']._serialized_options = b'8\001' - _globals['_RULEMETADATA'].fields_by_name['scope']._options = None + _globals['_RULEMETADATA'].fields_by_name['scope']._loaded_options = None _globals['_RULEMETADATA'].fields_by_name['scope']._serialized_options = b'\030\001' _globals['_ADDRESSTYPE']._serialized_start=9062 _globals['_ADDRESSTYPE']._serialized_end=9336 diff --git a/capa/render/proto/capa_pb2.pyi b/capa/render/proto/capa_pb2.pyi index c881561a3..06f846170 100644 --- a/capa/render/proto/capa_pb2.pyi +++ b/capa/render/proto/capa_pb2.pyi @@ -1,6 +1,19 @@ """ @generated by mypy-protobuf. Do not edit manually! isort:skip_file +Copyright 2023 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License 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 builtins diff --git a/capa/render/result_document.py b/capa/render/result_document.py index 8bf6a948e..b2b35c00d 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -29,6 +29,7 @@ from capa.rules import RuleSet from capa.engine import MatchResults from capa.helpers import assert_never, load_json_from_path +from capa.features.address import DynamicCallAddress if TYPE_CHECKING: from capa.capabilities.common import Capabilities @@ -392,7 +393,33 @@ def from_capa( ) for location in result.locations: - children.append(Match.from_capa(rules, capabilities, rule_matches[location])) + + # keep this in sync with the copy below + if isinstance(location, DynamicCallAddress): + if location in rule_matches: + # exact match, such as matching a call-scoped rule. + children.append(Match.from_capa(rules, capabilities, rule_matches[location])) + # we'd like to assert the scope of the current rule is "sequence" + # but we don't have that data here. + else: + # Sequence scopes can match each other, but they don't strictly contain each other, + # like the way a function contains a basic block. + # So when we have a match within a sequence for another sequence, we need to look + # for all the places it might be found. + # + # Despite the edge cases (like API hammering), this turns out to be pretty easy: + # collect the most recent match (with the given name) prior to the wanted location. + matches_in_thread = sorted([ + (a.id, m) for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ]) + _, most_recent_match = matches_in_thread[-1] + children.append(Match.from_capa(rules, capabilities, most_recent_match)) + + else: + children.append(Match.from_capa(rules, capabilities, rule_matches[location])) else: # this is a namespace that we're matching # @@ -433,8 +460,23 @@ def from_capa( # this is a subset of doc[locations]. # # so, grab only the locations for current rule. - if location in rule_matches: - children.append(Match.from_capa(rules, capabilities, rule_matches[location])) + + # keep this in sync with the block above. + if isinstance(location, DynamicCallAddress): + if location in rule_matches: + children.append(Match.from_capa(rules, capabilities, rule_matches[location])) + else: + matches_in_thread = sorted([ + (a.id, m) for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ]) + _, most_recent_match = matches_in_thread[-1] + children.append(Match.from_capa(rules, capabilities, most_recent_match)) + else: + if location in rule_matches: + children.append(Match.from_capa(rules, capabilities, rule_matches[location])) return cls( success=success, diff --git a/capa/render/verbose.py b/capa/render/verbose.py index a89db5039..81bf93e05 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -43,7 +43,6 @@ from capa.engine import MatchResults from capa.render.utils import Console - def format_address(address: frz.Address) -> str: if address.type == frz.AddressType.ABSOLUTE: assert isinstance(address.value, int) diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index 5a2f1718d..3b907a37c 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -230,7 +230,8 @@ def render_feature( # if we're in call scope, then the call will have been rendered at the top # of the output, so don't re-render it again for each feature. pass - elif isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)): + elif isinstance(layout, rd.DynamicLayout) and isinstance(feature, frzf.MatchFeature): + # don't render copies of the sequence address for submatches pass else: render_locations(console, layout, match.locations, indent) @@ -311,13 +312,13 @@ def render_match( render_match(console, layout, rule, child, indent=indent + 1, mode=child_mode) -def collect_call_locations( +def collect_sequence_locations( match: rd.Match, mode=MODE_SUCCESS, ): """ - Find all the DynamicCallAddress locations in the given match, recursively. - Useful to collect the calls used to match a sequence scoped rule. + Find all the (call, sequence) locations used in a given sequence match, recursively. + Useful to collect the events used to match a sequence scoped rule. """ if isinstance(match.node, rd.StatementNode): if ( @@ -326,13 +327,13 @@ def collect_call_locations( ): child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS for child in match.children: - yield from collect_call_locations(child, child_mode) + yield from collect_sequence_locations(child, child_mode) else: for child in match.children: - yield from collect_call_locations(child, mode) + yield from collect_sequence_locations(child, mode) elif isinstance(match.node, rd.FeatureNode): for location in match.locations: - if location.type != frz.AddressType.CALL: + if location.type not in (frz.AddressType.CALL, ): continue if mode == MODE_FAILURE: continue @@ -481,7 +482,7 @@ def render_rules(console: Console, doc: rd.ResultDocument): elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD: console.write(v.render_thread(doc.meta.analysis.layout, location)) elif rule.meta.scopes.dynamic == capa.rules.Scope.SEQUENCE: - calls = sorted(set(collect_call_locations(match))) + calls = sorted(set(collect_sequence_locations(match))) console.write(hanging_indent(v.render_sequence(doc.meta.analysis.layout, calls), indent=1)) elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL: console.write(hanging_indent(v.render_call(doc.meta.analysis.layout, location), indent=1)) diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_sequence_scope.py index 09ba62bc6..8d5341e02 100644 --- a/tests/test_dynamic_sequence_scope.py +++ b/tests/test_dynamic_sequence_scope.py @@ -364,3 +364,101 @@ def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): assert r.name in capabilities.matches # we only match the first overlapping sequence assert [11] == list(get_call_ids(capabilities.matches[r.name])) + + +# show that you can use match statements in sequence rules. +# +# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) +# thread: 3064 +# ... +# call 10: LdrGetDllHandle(1974337536, kernel32.dll) +# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) +# call 12: LdrGetDllHandle(1974337536, kernel32.dll) +# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) +# ... +def test_dynamic_sequence_scope_match_statements(): + extractor = get_0000a657_thread3064() + + ruleset = capa.rules.RuleSet( + [ + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: resolve add VEH + namespace: linking/runtime-linking/veh + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + """ + ) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: resolve remove VEH + namespace: linking/runtime-linking/veh + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: RemoveVectoredExceptionHandler + """ + ) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: resolve add and remove VEH + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - match: resolve add VEH + - match: resolve remove VEH + """ + ) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: has VEH runtime linking + scopes: + static: unsupported + dynamic: sequence + features: + - and: + - match: linking/runtime-linking/veh + """ + ) + ), + ] + ) + + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + + # basic functionality, already known to work + assert "resolve add VEH" in capabilities.matches + assert "resolve remove VEH" in capabilities.matches + + # requires `match: ` to be working + assert "resolve add and remove VEH" in capabilities.matches + + # requires `match: ` to be working + assert "has VEH runtime linking" in capabilities.matches From e6bdcff5d97ca27027ad3ec6daf75effc75641b3 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 16 Jan 2025 15:42:48 +0000 Subject: [PATCH 09/17] sequence: better collect sequence-related addresses from Range statements --- capa/render/vverbose.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index 3b907a37c..c7e376155 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -328,6 +328,13 @@ def collect_sequence_locations( child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS for child in match.children: yield from collect_sequence_locations(child, child_mode) + elif isinstance(match.node.statement, rd.RangeStatement): + for location in match.locations: + if location.type not in (frz.AddressType.CALL, ): + continue + if mode == MODE_FAILURE: + continue + yield location else: for child in match.children: yield from collect_sequence_locations(child, mode) From a1d46bc3c0f515339270bf7b80e9b03986e4c657 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 17 Jan 2025 11:25:41 +0000 Subject: [PATCH 10/17] sequence: don't update feature locations in place pep8 --- capa/capabilities/dynamic.py | 16 +++++++------- capa/features/common.py | 4 ++-- capa/render/result_document.py | 33 +++++++++++++++++----------- capa/render/verbose.py | 1 + capa/render/vverbose.py | 4 ++-- capa/rules/__init__.py | 1 + tests/test_dynamic_sequence_scope.py | 19 ++++++++++------ 7 files changed, 46 insertions(+), 32 deletions(-) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index a4e97be68..c261c1e5d 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -19,6 +19,8 @@ from dataclasses import dataclass import capa.perf +import capa.engine +import capa.helpers import capa.features.freeze as frz import capa.render.result_document as rdoc from capa.rules import Scope, RuleSet @@ -106,7 +108,7 @@ def __init__(self, ruleset: RuleSet): self.current_features: FeatureSet = collections.defaultdict(set) # the names of rules matched at the last sequence, - # so that we can deduplicate long strings of the same matche. + # so that we can deduplicate long strings of the same matches. self.last_sequence_matches: set[str] = set() def next(self, ch: CallHandle, call_features: FeatureSet): @@ -124,15 +126,14 @@ def next(self, ch: CallHandle, call_features: FeatureSet): # like arch/os/format. continue - feature_vas = self.current_features[feature] - feature_vas.difference_update(vas) - if not feature_vas: + self.current_features[feature] -= vas + if not self.current_features[feature]: del self.current_features[feature] # update the deque and set of features with the latest call's worth of features. self.current_feature_sets.append(call_features) for feature, vas in call_features.items(): - self.current_features[feature].update(vas) + self.current_features[feature] |= vas _, matches = self.ruleset.match(Scope.SEQUENCE, self.current_features, ch.address) @@ -154,7 +155,7 @@ def next(self, ch: CallHandle, call_features: FeatureSet): # see: https://github.com/mandiant/capa/pull/2532#issuecomment-2548508130 for new_rule in newly_encountered_rules: suppressed_rules -= set(self.ruleset.rules[new_rule].get_dependencies(self.ruleset.rules_by_namespace)) - + for rule_name, res in matches.items(): if rule_name in suppressed_rules: continue @@ -181,8 +182,7 @@ def find_thread_capabilities( sequence_matcher = SequenceMatcher(ruleset) call_count = 0 - for ch in extractor.get_calls(ph, th): - call_count += 1 + for call_count, ch in enumerate(extractor.get_calls(ph, th)): # noqa: B007 call_capabilities = find_call_capabilities(ruleset, extractor, ph, th, ch) for feature, vas in call_capabilities.features.items(): features[feature].update(vas) diff --git a/capa/features/common.py b/capa/features/common.py index 674400a4e..44d42cceb 100644 --- a/capa/features/common.py +++ b/capa/features/common.py @@ -108,7 +108,8 @@ def __nonzero__(self): def __str__(self): # as this object isn't user facing, this formatting is just to help with debugging - lines = [] + lines: list[str] = [] + def rec(m: "Result", indent: int): if isinstance(m.statement, capa.engine.Statement): line = (" " * indent) + str(m.statement.name) + " " + str(m.success) @@ -124,7 +125,6 @@ def rec(m: "Result", indent: int): return "\n".join(lines) - class Feature(abc.ABC): # noqa: B024 # this is an abstract class, since we don't want anyone to instantiate it directly, # but it doesn't have any abstract methods. diff --git a/capa/render/result_document.py b/capa/render/result_document.py index b2b35c00d..d1bce9add 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -406,15 +406,18 @@ def from_capa( # like the way a function contains a basic block. # So when we have a match within a sequence for another sequence, we need to look # for all the places it might be found. - # + # # Despite the edge cases (like API hammering), this turns out to be pretty easy: # collect the most recent match (with the given name) prior to the wanted location. - matches_in_thread = sorted([ - (a.id, m) for a, m in rule_matches.items() - if isinstance(a, DynamicCallAddress) - and a.thread == location.thread - and a.id <= location.id - ]) + matches_in_thread = sorted( + [ + (a.id, m) + for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ] + ) _, most_recent_match = matches_in_thread[-1] children.append(Match.from_capa(rules, capabilities, most_recent_match)) @@ -466,12 +469,15 @@ def from_capa( if location in rule_matches: children.append(Match.from_capa(rules, capabilities, rule_matches[location])) else: - matches_in_thread = sorted([ - (a.id, m) for a, m in rule_matches.items() - if isinstance(a, DynamicCallAddress) - and a.thread == location.thread - and a.id <= location.id - ]) + matches_in_thread = sorted( + [ + (a.id, m) + for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ] + ) _, most_recent_match = matches_in_thread[-1] children.append(Match.from_capa(rules, capabilities, most_recent_match)) else: @@ -523,6 +529,7 @@ def __str__(self): # as this object isn't user facing, this formatting is just to help with debugging lines = [] + def rec(m: "Match", indent: int): if isinstance(m.node, StatementNode): line = (" " * indent) + str(m.node.statement.type) + " " + str(m.success) diff --git a/capa/render/verbose.py b/capa/render/verbose.py index 81bf93e05..a89db5039 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -43,6 +43,7 @@ from capa.engine import MatchResults from capa.render.utils import Console + def format_address(address: frz.Address) -> str: if address.type == frz.AddressType.ABSOLUTE: assert isinstance(address.value, int) diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index c7e376155..ad8ff4964 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -330,7 +330,7 @@ def collect_sequence_locations( yield from collect_sequence_locations(child, child_mode) elif isinstance(match.node.statement, rd.RangeStatement): for location in match.locations: - if location.type not in (frz.AddressType.CALL, ): + if location.type not in (frz.AddressType.CALL,): continue if mode == MODE_FAILURE: continue @@ -340,7 +340,7 @@ def collect_sequence_locations( yield from collect_sequence_locations(child, mode) elif isinstance(match.node, rd.FeatureNode): for location in match.locations: - if location.type not in (frz.AddressType.CALL, ): + if location.type not in (frz.AddressType.CALL,): continue if mode == MODE_FAILURE: continue diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 2ca4e0bc6..907ae9be4 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -897,6 +897,7 @@ def rec(statement): # but, namespaces tend to use `-` while rule names use ` `. so, unlikely, but possible. if statement.value in namespaces: # matches a namespace, so take precedence and don't even check rule names. + assert isinstance(statement.value, str) deps.update(r.name for r in namespaces[statement.value]) else: # not a namespace, assume it's a rule name. diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_sequence_scope.py index 8d5341e02..98b3eaf4e 100644 --- a/tests/test_dynamic_sequence_scope.py +++ b/tests/test_dynamic_sequence_scope.py @@ -1,11 +1,16 @@ -# Copyright (C) 2024 Mandiant, Inc. All Rights Reserved. +# Copyright 2022 Google LLC +# # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: [package root]/LICENSE.txt -# Unless required by applicable law or agreed to in writing, software distributed under the License -# 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. - +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. # tests/data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz # From 277504c7b7f01bf6ff00bcef8b192b00eb389afd Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 17 Jan 2025 12:02:36 +0000 Subject: [PATCH 11/17] changelog: add sequence scope --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f83c595f3..ca511f63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,13 @@ ### New Features +- add sequence scope to match features against a across a sliding window of API calls within a thread @williballenthin #2532 + ### Breaking Changes +- add sequence scope to rule format +- capabilities functions return dataclasses instead of tuples + ### New Rules (2) - data-manipulation/encryption/rsa/encrypt-data-using-rsa-via-embedded-library Ana06 From cdc1cb7afd8199a8b9b22eea6bd520c581394c6c Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 17 Jan 2025 12:44:47 +0000 Subject: [PATCH 12/17] rename "sequence" scope to "span of calls" scope pep8 fix ref update submodules update testfiles submodule duplicate variable --- CHANGELOG.md | 4 +- capa/capabilities/dynamic.py | 82 +++++++++---------- capa/render/proto/__init__.py | 8 +- capa/render/proto/capa.proto | 2 +- capa/render/proto/capa_pb2.py | 4 +- capa/render/proto/capa_pb2.pyi | 4 +- capa/render/result_document.py | 6 +- capa/render/verbose.py | 4 +- capa/render/vverbose.py | 18 ++-- capa/rules/__init__.py | 46 ++++++----- scripts/lint.py | 2 +- ...py => test_dynamic_span_of_calls_scope.py} | 82 +++++++++---------- tests/test_proto.py | 2 +- 13 files changed, 131 insertions(+), 133 deletions(-) rename tests/{test_dynamic_sequence_scope.py => test_dynamic_span_of_calls_scope.py} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca511f63d..925005a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ ### New Features -- add sequence scope to match features against a across a sliding window of API calls within a thread @williballenthin #2532 +- add span-of-calls scope to match features against a across a sliding window of API calls within a thread @williballenthin #2532 ### Breaking Changes -- add sequence scope to rule format +- add span-of-calls scope to rule format - capabilities functions return dataclasses instead of tuples ### New Rules (2) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index c261c1e5d..a84f5d3f7 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -32,11 +32,11 @@ logger = logging.getLogger(__name__) -# The number of calls that make up a sequence. +# The number of calls that make up a span of calls. # # The larger this is, the more calls are grouped together to match rule logic. # This means a longer chain can be recognized; however, its a bit more expensive. -SEQUENCE_SIZE = 20 +SPAN_SIZE = 20 @dataclass @@ -45,10 +45,6 @@ class CallCapabilities: matches: MatchResults -# The number of calls that make up a sequence. -SEQUENCE_SIZE = 5 - - def find_call_capabilities( ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle ) -> CallCapabilities: @@ -78,44 +74,44 @@ def find_call_capabilities( class ThreadCapabilities: features: FeatureSet thread_matches: MatchResults - sequence_matches: MatchResults + span_matches: MatchResults call_matches: MatchResults -class SequenceMatcher: +class SpanOfCallsMatcher: def __init__(self, ruleset: RuleSet): super().__init__() self.ruleset = ruleset - # matches found at the sequence scope. + # matches found at the span scope. self.matches: MatchResults = collections.defaultdict(list) - # We matches sequences as the sliding window of calls with size SEQUENCE_SIZE. + # We match spans as the sliding window of calls with size SPAN_SIZE. # - # For each call, we consider the window of SEQUENCE_SIZE calls leading up to it, + # For each call, we consider the window of SPAN_SIZE calls leading up to it, # merging all their features and doing a match. # # We track these features in two data structures: # 1. a deque of those features found in the prior calls. - # We'll append to it, and as it grows larger than SEQUENCE_SIZE, the oldest items are removed. - # 2. a live set of features seen in the sequence. + # We'll append to it, and as it grows larger than SPAN_SIZE, the oldest items are removed. + # 2. a live set of features seen in the span. # As we pop from the deque, we remove features from the current set, # and as we push to the deque, we insert features to the current set. - # With this approach, our algorithm performance is independent of SEQUENCE_SIZE. - # The naive algorithm, of merging all the trailing feature sets at each call, is dependent upon SEQUENCE_SIZE - # (that is, runtime gets slower the larger SEQUENCE_SIZE is). - self.current_feature_sets: collections.deque[FeatureSet] = collections.deque(maxlen=SEQUENCE_SIZE) + # With this approach, our algorithm performance is independent of SPAN_SIZE. + # The naive algorithm, of merging all the trailing feature sets at each call, is dependent upon SPAN_SIZE + # (that is, runtime gets slower the larger SPAN_SIZE is). + self.current_feature_sets: collections.deque[FeatureSet] = collections.deque(maxlen=SPAN_SIZE) self.current_features: FeatureSet = collections.defaultdict(set) - # the names of rules matched at the last sequence, + # the names of rules matched at the last span, # so that we can deduplicate long strings of the same matches. - self.last_sequence_matches: set[str] = set() + self.last_span_matches: set[str] = set() def next(self, ch: CallHandle, call_features: FeatureSet): # As we add items to the end of the deque, overflow and drop the oldest items (at the left end). # While we could rely on `deque.append` with `maxlen` set (which we provide above), # we want to use the dropped item first, to remove the old features, so we manually pop it here. - if len(self.current_feature_sets) == SEQUENCE_SIZE: + if len(self.current_feature_sets) == SPAN_SIZE: overflowing_feature_set = self.current_feature_sets.popleft() for feature, vas in overflowing_feature_set.items(): @@ -135,20 +131,20 @@ def next(self, ch: CallHandle, call_features: FeatureSet): for feature, vas in call_features.items(): self.current_features[feature] |= vas - _, matches = self.ruleset.match(Scope.SEQUENCE, self.current_features, ch.address) + _, matches = self.ruleset.match(Scope.SPAN_OF_CALLS, self.current_features, ch.address) - newly_encountered_rules = set(matches.keys()) - self.last_sequence_matches + newly_encountered_rules = set(matches.keys()) - self.last_span_matches - # don't emit match results for rules seen during the immediately preceeding sequence. + # don't emit match results for rules seen during the immediately preceeding spans. # - # This means that we won't emit duplicate matches when there are multiple sequences + # This means that we won't emit duplicate matches when there are multiple spans # that overlap a single matching event. # It also handles the case of a tight loop containing matched logic; # only the first match will be recorded. # # In theory, this means the result document doesn't have *every* possible match location, # but in practice, humans will only be interested in the first handful anyways. - suppressed_rules = set(self.last_sequence_matches) + suppressed_rules = set(self.last_span_matches) # however, if a newly encountered rule depends on a suppressed rule, # don't suppress that rule match, or we won't be able to reconstruct the vverbose output. @@ -161,7 +157,7 @@ def next(self, ch: CallHandle, call_features: FeatureSet): continue self.matches[rule_name].extend(res) - self.last_sequence_matches = set(matches.keys()) + self.last_span_matches = set(matches.keys()) def find_thread_capabilities( @@ -169,7 +165,7 @@ def find_thread_capabilities( ) -> ThreadCapabilities: """ find matches for the given rules within the given thread, - which includes matches for all the sequences and calls within it. + which includes matches for all the spans and calls within it. """ # all features found within this thread, # includes features found within calls. @@ -179,7 +175,7 @@ def find_thread_capabilities( # might be found at different calls, that's ok. call_matches: MatchResults = collections.defaultdict(list) - sequence_matcher = SequenceMatcher(ruleset) + span_matcher = SpanOfCallsMatcher(ruleset) call_count = 0 for call_count, ch in enumerate(extractor.get_calls(ph, th)): # noqa: B007 @@ -190,7 +186,7 @@ def find_thread_capabilities( for rule_name, res in call_capabilities.matches.items(): call_matches[rule_name].extend(res) - sequence_matcher.next(ch, call_capabilities.features) + span_matcher.next(ch, call_capabilities.features) for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()): features[feature].add(va) @@ -209,16 +205,16 @@ def find_thread_capabilities( th.address.tid, call_count, len(features), - len(matches) + len(sequence_matcher.matches) + len(call_matches), + len(matches) + len(span_matcher.matches) + len(call_matches), ) - return ThreadCapabilities(features, matches, sequence_matcher.matches, call_matches) + return ThreadCapabilities(features, matches, span_matcher.matches, call_matches) @dataclass class ProcessCapabilities: process_matches: MatchResults thread_matches: MatchResults - sequence_matches: MatchResults + span_matches: MatchResults call_matches: MatchResults feature_count: int @@ -237,9 +233,9 @@ def find_process_capabilities( # might be found at different threads, that's ok. thread_matches: MatchResults = collections.defaultdict(list) - # matches found at the sequence scope. - # might be found at different sequences, that's ok. - sequence_matches: MatchResults = collections.defaultdict(list) + # matches found at the span-of-calls scope. + # might be found at different spans, that's ok. + span_matches: MatchResults = collections.defaultdict(list) # matches found at the call scope. # might be found at different calls, that's ok. @@ -253,8 +249,8 @@ def find_process_capabilities( for rule_name, res in thread_capabilities.thread_matches.items(): thread_matches[rule_name].extend(res) - for rule_name, res in thread_capabilities.sequence_matches.items(): - sequence_matches[rule_name].extend(res) + for rule_name, res in thread_capabilities.span_matches.items(): + span_matches[rule_name].extend(res) for rule_name, res in thread_capabilities.call_matches.items(): call_matches[rule_name].extend(res) @@ -270,7 +266,7 @@ def find_process_capabilities( len(process_features), len(process_matches), ) - return ProcessCapabilities(process_matches, thread_matches, sequence_matches, call_matches, len(process_features)) + return ProcessCapabilities(process_matches, thread_matches, span_matches, call_matches, len(process_features)) def find_dynamic_capabilities( @@ -278,7 +274,7 @@ def find_dynamic_capabilities( ) -> Capabilities: all_process_matches: MatchResults = collections.defaultdict(list) all_thread_matches: MatchResults = collections.defaultdict(list) - all_sequence_matches: MatchResults = collections.defaultdict(list) + all_span_matches: MatchResults = collections.defaultdict(list) all_call_matches: MatchResults = collections.defaultdict(list) feature_counts = rdoc.DynamicFeatureCounts(file=0, processes=()) @@ -303,8 +299,8 @@ def find_dynamic_capabilities( all_process_matches[rule_name].extend(res) for rule_name, res in process_capabilities.thread_matches.items(): all_thread_matches[rule_name].extend(res) - for rule_name, res in process_capabilities.sequence_matches.items(): - all_sequence_matches[rule_name].extend(res) + for rule_name, res in process_capabilities.span_matches.items(): + all_span_matches[rule_name].extend(res) for rule_name, res in process_capabilities.call_matches.items(): all_call_matches[rule_name].extend(res) @@ -314,7 +310,7 @@ def find_dynamic_capabilities( # mapping from feature (matched rule) to set of addresses at which it matched. process_and_lower_features: FeatureSet = collections.defaultdict(set) for rule_name, results in itertools.chain( - all_process_matches.items(), all_thread_matches.items(), all_sequence_matches.items(), all_call_matches.items() + all_process_matches.items(), all_thread_matches.items(), all_span_matches.items(), all_call_matches.items() ): locations = {p[0] for p in results} rule = ruleset[rule_name] @@ -329,7 +325,7 @@ def find_dynamic_capabilities( # so there won't be any overlap among these following MatchResults, # and we can merge the dictionaries naively. all_call_matches.items(), - all_sequence_matches.items(), + all_span_matches.items(), all_thread_matches.items(), all_process_matches.items(), all_file_capabilities.matches.items(), diff --git a/capa/render/proto/__init__.py b/capa/render/proto/__init__.py index 2541c944c..31b272e52 100644 --- a/capa/render/proto/__init__.py +++ b/capa/render/proto/__init__.py @@ -163,8 +163,8 @@ def scope_to_pb2(scope: capa.rules.Scope) -> capa_pb2.Scope.ValueType: return capa_pb2.Scope.SCOPE_PROCESS elif scope == capa.rules.Scope.THREAD: return capa_pb2.Scope.SCOPE_THREAD - elif scope == capa.rules.Scope.SEQUENCE: - return capa_pb2.Scope.SCOPE_SEQUENCE + elif scope == capa.rules.Scope.SPAN_OF_CALLS: + return capa_pb2.Scope.SCOPE_SPAN_OF_CALLS elif scope == capa.rules.Scope.CALL: return capa_pb2.Scope.SCOPE_CALL else: @@ -657,8 +657,8 @@ def scope_from_pb2(scope: capa_pb2.Scope.ValueType) -> capa.rules.Scope: return capa.rules.Scope.PROCESS elif scope == capa_pb2.Scope.SCOPE_THREAD: return capa.rules.Scope.THREAD - elif scope == capa_pb2.Scope.SCOPE_SEQUENCE: - return capa.rules.Scope.SEQUENCE + elif scope == capa_pb2.Scope.SCOPE_SPAN_OF_CALLS: + return capa.rules.Scope.SPAN_OF_CALLS elif scope == capa_pb2.Scope.SCOPE_CALL: return capa.rules.Scope.CALL else: diff --git a/capa/render/proto/capa.proto b/capa/render/proto/capa.proto index e824c42e6..b07234748 100644 --- a/capa/render/proto/capa.proto +++ b/capa/render/proto/capa.proto @@ -378,7 +378,7 @@ enum Scope { SCOPE_PROCESS = 5; SCOPE_THREAD = 6; SCOPE_CALL = 7; - SCOPE_SEQUENCE = 8; + SCOPE_SPAN_OF_CALLS = 8; } message Scopes { diff --git a/capa/render/proto/capa_pb2.py b/capa/render/proto/capa_pb2.py index e695a27a7..b581c9ea7 100644 --- a/capa/render/proto/capa_pb2.py +++ b/capa/render/proto/capa_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63\x61pa/render/proto/capa.proto\x12\rmandiant.capa\"Q\n\nAPIFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03\x61pi\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xb3\x02\n\x07\x41\x64\x64ress\x12(\n\x04type\x18\x01 \x01(\x0e\x32\x1a.mandiant.capa.AddressType\x12#\n\x01v\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.IntegerH\x00\x12\x33\n\x0ctoken_offset\x18\x03 \x01(\x0b\x32\x1b.mandiant.capa.Token_OffsetH\x00\x12+\n\x08ppid_pid\x18\x04 \x01(\x0b\x32\x17.mandiant.capa.Ppid_PidH\x00\x12\x33\n\x0cppid_pid_tid\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.Ppid_Pid_TidH\x00\x12\x39\n\x0fppid_pid_tid_id\x18\x06 \x01(\x0b\x32\x1e.mandiant.capa.Ppid_Pid_Tid_IdH\x00\x42\x07\n\x05value\"\x9c\x02\n\x08\x41nalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12%\n\x06layout\x18\x07 \x01(\x0b\x32\x15.mandiant.capa.Layout\x12\x34\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.FeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"S\n\x0b\x41rchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"`\n\nAttackSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x0e\n\x06tactic\x18\x02 \x01(\t\x12\x11\n\ttechnique\x18\x03 \x01(\t\x12\x14\n\x0csubtechnique\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"K\n\x11\x42\x61sicBlockFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\";\n\x10\x42\x61sicBlockLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\"U\n\x0c\x42ytesFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x62ytes\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"g\n\x15\x43haracteristicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\x0e\x63haracteristic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\x0c\x43lassFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x63lass_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"K\n\x11\x43ompoundStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xc8\x01\n\x0f\x44ynamicAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x06layout\x18\x06 \x01(\x0b\x32\x1c.mandiant.capa.DynamicLayout\x12;\n\x0e\x66\x65\x61ture_counts\x18\x07 \x01(\x0b\x32#.mandiant.capa.DynamicFeatureCounts\"[\n\x14\x44ynamicFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x35\n\tprocesses\x18\x02 \x03(\x0b\x32\".mandiant.capa.ProcessFeatureCount\"@\n\rDynamicLayout\x12/\n\tprocesses\x18\x01 \x03(\x0b\x32\x1c.mandiant.capa.ProcessLayout\"W\n\rExportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x65xport\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\rFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"\xb9\t\n\x0b\x46\x65\x61tureNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x02os\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.OSFeatureH\x00\x12*\n\x04\x61rch\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.ArchFeatureH\x00\x12.\n\x06\x66ormat\x18\x04 \x01(\x0b\x32\x1c.mandiant.capa.FormatFeatureH\x00\x12,\n\x05match\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.MatchFeatureH\x00\x12>\n\x0e\x63haracteristic\x18\x06 \x01(\x0b\x32$.mandiant.capa.CharacteristicFeatureH\x00\x12.\n\x06\x65xport\x18\x07 \x01(\x0b\x32\x1c.mandiant.capa.ExportFeatureH\x00\x12/\n\x07import_\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.ImportFeatureH\x00\x12\x30\n\x07section\x18\t \x01(\x0b\x32\x1d.mandiant.capa.SectionFeatureH\x00\x12;\n\rfunction_name\x18\n \x01(\x0b\x32\".mandiant.capa.FunctionNameFeatureH\x00\x12\x34\n\tsubstring\x18\x0b \x01(\x0b\x32\x1f.mandiant.capa.SubstringFeatureH\x00\x12,\n\x05regex\x18\x0c \x01(\x0b\x32\x1b.mandiant.capa.RegexFeatureH\x00\x12.\n\x06string\x18\r \x01(\x0b\x32\x1c.mandiant.capa.StringFeatureH\x00\x12-\n\x06\x63lass_\x18\x0e \x01(\x0b\x32\x1b.mandiant.capa.ClassFeatureH\x00\x12\x34\n\tnamespace\x18\x0f \x01(\x0b\x32\x1f.mandiant.capa.NamespaceFeatureH\x00\x12(\n\x03\x61pi\x18\x10 \x01(\x0b\x32\x19.mandiant.capa.APIFeatureH\x00\x12\x33\n\tproperty_\x18\x11 \x01(\x0b\x32\x1e.mandiant.capa.PropertyFeatureH\x00\x12.\n\x06number\x18\x12 \x01(\x0b\x32\x1c.mandiant.capa.NumberFeatureH\x00\x12,\n\x05\x62ytes\x18\x13 \x01(\x0b\x32\x1b.mandiant.capa.BytesFeatureH\x00\x12.\n\x06offset\x18\x14 \x01(\x0b\x32\x1c.mandiant.capa.OffsetFeatureH\x00\x12\x32\n\x08mnemonic\x18\x15 \x01(\x0b\x32\x1e.mandiant.capa.MnemonicFeatureH\x00\x12=\n\x0eoperand_number\x18\x16 \x01(\x0b\x32#.mandiant.capa.OperandNumberFeatureH\x00\x12=\n\x0eoperand_offset\x18\x17 \x01(\x0b\x32#.mandiant.capa.OperandOffsetFeatureH\x00\x12\x37\n\x0b\x62\x61sic_block\x18\x18 \x01(\x0b\x32 .mandiant.capa.BasicBlockFeatureH\x00\x42\t\n\x07\x66\x65\x61ture\"W\n\rFormatFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"N\n\x14\x46unctionFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"x\n\x0e\x46unctionLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12=\n\x14matched_basic_blocks\x18\x02 \x03(\x0b\x32\x1f.mandiant.capa.BasicBlockLayout\"d\n\x13\x46unctionNameFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rfunction_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"X\n\rImportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07import_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\":\n\x06Layout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"H\n\x0fLibraryFunction\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"Y\n\x07MBCSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x11\n\tobjective\x18\x02 \x01(\t\x12\x10\n\x08\x62\x65havior\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"\x9a\x01\n\x0cMaecMetadata\x12\x1b\n\x13\x61nalysis_conclusion\x18\x01 \x01(\t\x12\x1e\n\x16\x61nalysis_conclusion_ov\x18\x02 \x01(\t\x12\x16\n\x0emalware_family\x18\x03 \x01(\t\x12\x18\n\x10malware_category\x18\x04 \x01(\t\x12\x1b\n\x13malware_category_ov\x18\x05 \x01(\t\"\xd6\x02\n\x05Match\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x31\n\tstatement\x18\x02 \x01(\x0b\x32\x1c.mandiant.capa.StatementNodeH\x00\x12-\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNodeH\x00\x12&\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x14.mandiant.capa.Match\x12)\n\tlocations\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x08\x63\x61ptures\x18\x07 \x03(\x0b\x32\".mandiant.capa.Match.CapturesEntry\x1aI\n\rCapturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.Addresses:\x02\x38\x01\x42\x06\n\x04node\"U\n\x0cMatchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xbc\x02\n\x08Metadata\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgv\x18\x03 \x03(\t\x12%\n\x06sample\x18\x04 \x01(\x0b\x32\x15.mandiant.capa.Sample\x12-\n\x08\x61nalysis\x18\x05 \x01(\x0b\x32\x17.mandiant.capa.AnalysisB\x02\x18\x01\x12%\n\x06\x66lavor\x18\x06 \x01(\x0e\x32\x15.mandiant.capa.Flavor\x12\x38\n\x0fstatic_analysis\x18\x07 \x01(\x0b\x32\x1d.mandiant.capa.StaticAnalysisH\x00\x12:\n\x10\x64ynamic_analysis\x18\x08 \x01(\x0b\x32\x1e.mandiant.capa.DynamicAnalysisH\x00\x42\x0b\n\tanalysis2\"[\n\x0fMnemonicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x10\n\x08mnemonic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10NamespaceFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"n\n\rNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12%\n\x06number\x18\x02 \x01(\x0b\x32\x15.mandiant.capa.Number\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"O\n\tOSFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\n\n\x02os\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"o\n\rOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x06offset\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_number\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_offset\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"M\n\x13ProcessFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"|\n\rProcessLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x0fmatched_threads\x18\x02 \x03(\x0b\x32\x1b.mandiant.capa.ThreadLayout\x12\x0c\n\x04name\x18\x03 \x01(\t\"|\n\x0fPropertyFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tproperty_\x18\x02 \x01(\t\x12\x13\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_accessB\x0e\n\x0c_description\"\x8d\x01\n\x0eRangeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03min\x18\x02 \x01(\x04\x12\x0b\n\x03max\x18\x03 \x01(\x04\x12)\n\x05\x63hild\x18\x04 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNode\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\x0cRegexFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05regex\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xba\x01\n\x0eResultDocument\x12%\n\x04meta\x18\x01 \x01(\x0b\x32\x17.mandiant.capa.Metadata\x12\x37\n\x05rules\x18\x02 \x03(\x0b\x32(.mandiant.capa.ResultDocument.RulesEntry\x1aH\n\nRulesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.mandiant.capa.RuleMatches:\x02\x38\x01\"|\n\x0bRuleMatches\x12)\n\x04meta\x18\x01 \x01(\x0b\x32\x1b.mandiant.capa.RuleMetadata\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x32\n\x07matches\x18\x03 \x03(\x0b\x32!.mandiant.capa.Pair_Address_Match\"\xed\x02\n\x0cRuleMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x0f\n\x07\x61uthors\x18\x03 \x03(\t\x12\'\n\x05scope\x18\x04 \x01(\x0e\x32\x14.mandiant.capa.ScopeB\x02\x18\x01\x12)\n\x06\x61ttack\x18\x05 \x03(\x0b\x32\x19.mandiant.capa.AttackSpec\x12#\n\x03mbc\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.MBCSpec\x12\x12\n\nreferences\x18\x07 \x03(\t\x12\x10\n\x08\x65xamples\x18\x08 \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x0b\n\x03lib\x18\n \x01(\x08\x12)\n\x04maec\x18\x0b \x01(\x0b\x32\x1b.mandiant.capa.MaecMetadata\x12\x18\n\x10is_subscope_rule\x18\x0c \x01(\x08\x12%\n\x06scopes\x18\r \x01(\x0b\x32\x15.mandiant.capa.Scopes\"A\n\x06Sample\x12\x0b\n\x03md5\x18\x01 \x01(\t\x12\x0c\n\x04sha1\x18\x02 \x01(\t\x12\x0e\n\x06sha256\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\"v\n\x06Scopes\x12)\n\x06static\x18\x01 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x00\x88\x01\x01\x12*\n\x07\x64ynamic\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x01\x88\x01\x01\x42\t\n\x07_staticB\n\n\x08_dynamic\"Y\n\x0eSectionFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07section\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\rSomeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xf4\x01\n\rStatementNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12.\n\x05range\x18\x02 \x01(\x0b\x32\x1d.mandiant.capa.RangeStatementH\x00\x12,\n\x04some\x18\x03 \x01(\x0b\x32\x1c.mandiant.capa.SomeStatementH\x00\x12\x34\n\x08subscope\x18\x04 \x01(\x0b\x32 .mandiant.capa.SubscopeStatementH\x00\x12\x34\n\x08\x63ompound\x18\x05 \x01(\x0b\x32 .mandiant.capa.CompoundStatementH\x00\x42\x0b\n\tstatement\"\xae\x02\n\x0eStaticAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12+\n\x06layout\x18\x07 \x01(\x0b\x32\x1b.mandiant.capa.StaticLayout\x12:\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\".mandiant.capa.StaticFeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"[\n\x13StaticFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"@\n\x0cStaticLayout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"W\n\rStringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"p\n\x11SubscopeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12#\n\x05scope\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.Scope\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10SubstringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tsubstring\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"C\n\nCallLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"i\n\x0cThreadLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x30\n\rmatched_calls\x18\x02 \x03(\x0b\x32\x19.mandiant.capa.CallLayout\"4\n\tAddresses\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x03(\x0b\x32\x16.mandiant.capa.Address\"b\n\x12Pair_Address_Match\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12#\n\x05match\x18\x02 \x01(\x0b\x32\x14.mandiant.capa.Match\"E\n\x0cToken_Offset\x12%\n\x05token\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x0e\n\x06offset\x18\x02 \x01(\x04\"U\n\x08Ppid_Pid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\"~\n\x0cPpid_Pid_Tid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\"\xa5\x01\n\x0fPpid_Pid_Tid_Id\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\"\n\x02id\x18\x04 \x01(\x0b\x32\x16.mandiant.capa.Integer\",\n\x07Integer\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x42\x07\n\x05value\"8\n\x06Number\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x12\x0b\n\x01\x66\x18\x03 \x01(\x01H\x00\x42\x07\n\x05value*\x92\x02\n\x0b\x41\x64\x64ressType\x12\x1b\n\x17\x41\x44\x44RESSTYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x44\x44RESSTYPE_ABSOLUTE\x10\x01\x12\x18\n\x14\x41\x44\x44RESSTYPE_RELATIVE\x10\x02\x12\x14\n\x10\x41\x44\x44RESSTYPE_FILE\x10\x03\x12\x18\n\x14\x41\x44\x44RESSTYPE_DN_TOKEN\x10\x04\x12\x1f\n\x1b\x41\x44\x44RESSTYPE_DN_TOKEN_OFFSET\x10\x05\x12\x1a\n\x16\x41\x44\x44RESSTYPE_NO_ADDRESS\x10\x06\x12\x17\n\x13\x41\x44\x44RESSTYPE_PROCESS\x10\x07\x12\x16\n\x12\x41\x44\x44RESSTYPE_THREAD\x10\x08\x12\x14\n\x10\x41\x44\x44RESSTYPE_CALL\x10\t*G\n\x06\x46lavor\x12\x16\n\x12\x46LAVOR_UNSPECIFIED\x10\x00\x12\x11\n\rFLAVOR_STATIC\x10\x01\x12\x12\n\x0e\x46LAVOR_DYNAMIC\x10\x02*\xb9\x01\n\x05Scope\x12\x15\n\x11SCOPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSCOPE_FILE\x10\x01\x12\x12\n\x0eSCOPE_FUNCTION\x10\x02\x12\x15\n\x11SCOPE_BASIC_BLOCK\x10\x03\x12\x15\n\x11SCOPE_INSTRUCTION\x10\x04\x12\x11\n\rSCOPE_PROCESS\x10\x05\x12\x10\n\x0cSCOPE_THREAD\x10\x06\x12\x0e\n\nSCOPE_CALL\x10\x07\x12\x12\n\x0eSCOPE_SEQUENCE\x10\x08\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63\x61pa/render/proto/capa.proto\x12\rmandiant.capa\"Q\n\nAPIFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03\x61pi\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xb3\x02\n\x07\x41\x64\x64ress\x12(\n\x04type\x18\x01 \x01(\x0e\x32\x1a.mandiant.capa.AddressType\x12#\n\x01v\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.IntegerH\x00\x12\x33\n\x0ctoken_offset\x18\x03 \x01(\x0b\x32\x1b.mandiant.capa.Token_OffsetH\x00\x12+\n\x08ppid_pid\x18\x04 \x01(\x0b\x32\x17.mandiant.capa.Ppid_PidH\x00\x12\x33\n\x0cppid_pid_tid\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.Ppid_Pid_TidH\x00\x12\x39\n\x0fppid_pid_tid_id\x18\x06 \x01(\x0b\x32\x1e.mandiant.capa.Ppid_Pid_Tid_IdH\x00\x42\x07\n\x05value\"\x9c\x02\n\x08\x41nalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12%\n\x06layout\x18\x07 \x01(\x0b\x32\x15.mandiant.capa.Layout\x12\x34\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.FeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"S\n\x0b\x41rchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"`\n\nAttackSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x0e\n\x06tactic\x18\x02 \x01(\t\x12\x11\n\ttechnique\x18\x03 \x01(\t\x12\x14\n\x0csubtechnique\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"K\n\x11\x42\x61sicBlockFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\";\n\x10\x42\x61sicBlockLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\"U\n\x0c\x42ytesFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x62ytes\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"g\n\x15\x43haracteristicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\x0e\x63haracteristic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\x0c\x43lassFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x63lass_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"K\n\x11\x43ompoundStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xc8\x01\n\x0f\x44ynamicAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x06layout\x18\x06 \x01(\x0b\x32\x1c.mandiant.capa.DynamicLayout\x12;\n\x0e\x66\x65\x61ture_counts\x18\x07 \x01(\x0b\x32#.mandiant.capa.DynamicFeatureCounts\"[\n\x14\x44ynamicFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x35\n\tprocesses\x18\x02 \x03(\x0b\x32\".mandiant.capa.ProcessFeatureCount\"@\n\rDynamicLayout\x12/\n\tprocesses\x18\x01 \x03(\x0b\x32\x1c.mandiant.capa.ProcessLayout\"W\n\rExportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x65xport\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\rFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"\xb9\t\n\x0b\x46\x65\x61tureNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x02os\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.OSFeatureH\x00\x12*\n\x04\x61rch\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.ArchFeatureH\x00\x12.\n\x06\x66ormat\x18\x04 \x01(\x0b\x32\x1c.mandiant.capa.FormatFeatureH\x00\x12,\n\x05match\x18\x05 \x01(\x0b\x32\x1b.mandiant.capa.MatchFeatureH\x00\x12>\n\x0e\x63haracteristic\x18\x06 \x01(\x0b\x32$.mandiant.capa.CharacteristicFeatureH\x00\x12.\n\x06\x65xport\x18\x07 \x01(\x0b\x32\x1c.mandiant.capa.ExportFeatureH\x00\x12/\n\x07import_\x18\x08 \x01(\x0b\x32\x1c.mandiant.capa.ImportFeatureH\x00\x12\x30\n\x07section\x18\t \x01(\x0b\x32\x1d.mandiant.capa.SectionFeatureH\x00\x12;\n\rfunction_name\x18\n \x01(\x0b\x32\".mandiant.capa.FunctionNameFeatureH\x00\x12\x34\n\tsubstring\x18\x0b \x01(\x0b\x32\x1f.mandiant.capa.SubstringFeatureH\x00\x12,\n\x05regex\x18\x0c \x01(\x0b\x32\x1b.mandiant.capa.RegexFeatureH\x00\x12.\n\x06string\x18\r \x01(\x0b\x32\x1c.mandiant.capa.StringFeatureH\x00\x12-\n\x06\x63lass_\x18\x0e \x01(\x0b\x32\x1b.mandiant.capa.ClassFeatureH\x00\x12\x34\n\tnamespace\x18\x0f \x01(\x0b\x32\x1f.mandiant.capa.NamespaceFeatureH\x00\x12(\n\x03\x61pi\x18\x10 \x01(\x0b\x32\x19.mandiant.capa.APIFeatureH\x00\x12\x33\n\tproperty_\x18\x11 \x01(\x0b\x32\x1e.mandiant.capa.PropertyFeatureH\x00\x12.\n\x06number\x18\x12 \x01(\x0b\x32\x1c.mandiant.capa.NumberFeatureH\x00\x12,\n\x05\x62ytes\x18\x13 \x01(\x0b\x32\x1b.mandiant.capa.BytesFeatureH\x00\x12.\n\x06offset\x18\x14 \x01(\x0b\x32\x1c.mandiant.capa.OffsetFeatureH\x00\x12\x32\n\x08mnemonic\x18\x15 \x01(\x0b\x32\x1e.mandiant.capa.MnemonicFeatureH\x00\x12=\n\x0eoperand_number\x18\x16 \x01(\x0b\x32#.mandiant.capa.OperandNumberFeatureH\x00\x12=\n\x0eoperand_offset\x18\x17 \x01(\x0b\x32#.mandiant.capa.OperandOffsetFeatureH\x00\x12\x37\n\x0b\x62\x61sic_block\x18\x18 \x01(\x0b\x32 .mandiant.capa.BasicBlockFeatureH\x00\x42\t\n\x07\x66\x65\x61ture\"W\n\rFormatFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"N\n\x14\x46unctionFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"x\n\x0e\x46unctionLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12=\n\x14matched_basic_blocks\x18\x02 \x03(\x0b\x32\x1f.mandiant.capa.BasicBlockLayout\"d\n\x13\x46unctionNameFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x15\n\rfunction_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"X\n\rImportFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07import_\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\":\n\x06Layout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"H\n\x0fLibraryFunction\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"Y\n\x07MBCSpec\x12\r\n\x05parts\x18\x01 \x03(\t\x12\x11\n\tobjective\x18\x02 \x01(\t\x12\x10\n\x08\x62\x65havior\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\"\x9a\x01\n\x0cMaecMetadata\x12\x1b\n\x13\x61nalysis_conclusion\x18\x01 \x01(\t\x12\x1e\n\x16\x61nalysis_conclusion_ov\x18\x02 \x01(\t\x12\x16\n\x0emalware_family\x18\x03 \x01(\t\x12\x18\n\x10malware_category\x18\x04 \x01(\t\x12\x1b\n\x13malware_category_ov\x18\x05 \x01(\t\"\xd6\x02\n\x05Match\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x31\n\tstatement\x18\x02 \x01(\x0b\x32\x1c.mandiant.capa.StatementNodeH\x00\x12-\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNodeH\x00\x12&\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x14.mandiant.capa.Match\x12)\n\tlocations\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x08\x63\x61ptures\x18\x07 \x03(\x0b\x32\".mandiant.capa.Match.CapturesEntry\x1aI\n\rCapturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.mandiant.capa.Addresses:\x02\x38\x01\x42\x06\n\x04node\"U\n\x0cMatchFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05match\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xbc\x02\n\x08Metadata\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgv\x18\x03 \x03(\t\x12%\n\x06sample\x18\x04 \x01(\x0b\x32\x15.mandiant.capa.Sample\x12-\n\x08\x61nalysis\x18\x05 \x01(\x0b\x32\x17.mandiant.capa.AnalysisB\x02\x18\x01\x12%\n\x06\x66lavor\x18\x06 \x01(\x0e\x32\x15.mandiant.capa.Flavor\x12\x38\n\x0fstatic_analysis\x18\x07 \x01(\x0b\x32\x1d.mandiant.capa.StaticAnalysisH\x00\x12:\n\x10\x64ynamic_analysis\x18\x08 \x01(\x0b\x32\x1e.mandiant.capa.DynamicAnalysisH\x00\x42\x0b\n\tanalysis2\"[\n\x0fMnemonicFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x10\n\x08mnemonic\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10NamespaceFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"n\n\rNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12%\n\x06number\x18\x02 \x01(\x0b\x32\x15.mandiant.capa.Number\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"O\n\tOSFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\n\n\x02os\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"o\n\rOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12&\n\x06offset\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandNumberFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_number\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\x8d\x01\n\x14OperandOffsetFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\r\x12.\n\x0eoperand_offset\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"M\n\x13ProcessFeatureCount\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"|\n\rProcessLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x34\n\x0fmatched_threads\x18\x02 \x03(\x0b\x32\x1b.mandiant.capa.ThreadLayout\x12\x0c\n\x04name\x18\x03 \x01(\t\"|\n\x0fPropertyFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tproperty_\x18\x02 \x01(\t\x12\x13\n\x06\x61\x63\x63\x65ss\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_accessB\x0e\n\x0c_description\"\x8d\x01\n\x0eRangeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03min\x18\x02 \x01(\x04\x12\x0b\n\x03max\x18\x03 \x01(\x04\x12)\n\x05\x63hild\x18\x04 \x01(\x0b\x32\x1a.mandiant.capa.FeatureNode\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"U\n\x0cRegexFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05regex\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xba\x01\n\x0eResultDocument\x12%\n\x04meta\x18\x01 \x01(\x0b\x32\x17.mandiant.capa.Metadata\x12\x37\n\x05rules\x18\x02 \x03(\x0b\x32(.mandiant.capa.ResultDocument.RulesEntry\x1aH\n\nRulesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.mandiant.capa.RuleMatches:\x02\x38\x01\"|\n\x0bRuleMatches\x12)\n\x04meta\x18\x01 \x01(\x0b\x32\x1b.mandiant.capa.RuleMetadata\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x32\n\x07matches\x18\x03 \x03(\x0b\x32!.mandiant.capa.Pair_Address_Match\"\xed\x02\n\x0cRuleMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12\x0f\n\x07\x61uthors\x18\x03 \x03(\t\x12\'\n\x05scope\x18\x04 \x01(\x0e\x32\x14.mandiant.capa.ScopeB\x02\x18\x01\x12)\n\x06\x61ttack\x18\x05 \x03(\x0b\x32\x19.mandiant.capa.AttackSpec\x12#\n\x03mbc\x18\x06 \x03(\x0b\x32\x16.mandiant.capa.MBCSpec\x12\x12\n\nreferences\x18\x07 \x03(\t\x12\x10\n\x08\x65xamples\x18\x08 \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x0b\n\x03lib\x18\n \x01(\x08\x12)\n\x04maec\x18\x0b \x01(\x0b\x32\x1b.mandiant.capa.MaecMetadata\x12\x18\n\x10is_subscope_rule\x18\x0c \x01(\x08\x12%\n\x06scopes\x18\r \x01(\x0b\x32\x15.mandiant.capa.Scopes\"A\n\x06Sample\x12\x0b\n\x03md5\x18\x01 \x01(\t\x12\x0c\n\x04sha1\x18\x02 \x01(\t\x12\x0e\n\x06sha256\x18\x03 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\"v\n\x06Scopes\x12)\n\x06static\x18\x01 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x00\x88\x01\x01\x12*\n\x07\x64ynamic\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.ScopeH\x01\x88\x01\x01\x42\t\n\x07_staticB\n\n\x08_dynamic\"Y\n\x0eSectionFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07section\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"V\n\rSomeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"\xf4\x01\n\rStatementNode\x12\x0c\n\x04type\x18\x01 \x01(\t\x12.\n\x05range\x18\x02 \x01(\x0b\x32\x1d.mandiant.capa.RangeStatementH\x00\x12,\n\x04some\x18\x03 \x01(\x0b\x32\x1c.mandiant.capa.SomeStatementH\x00\x12\x34\n\x08subscope\x18\x04 \x01(\x0b\x32 .mandiant.capa.SubscopeStatementH\x00\x12\x34\n\x08\x63ompound\x18\x05 \x01(\x0b\x32 .mandiant.capa.CompoundStatementH\x00\x42\x0b\n\tstatement\"\xae\x02\n\x0eStaticAnalysis\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\x12\x0c\n\x04\x61rch\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x11\n\textractor\x18\x04 \x01(\t\x12\r\n\x05rules\x18\x05 \x03(\t\x12,\n\x0c\x62\x61se_address\x18\x06 \x01(\x0b\x32\x16.mandiant.capa.Address\x12+\n\x06layout\x18\x07 \x01(\x0b\x32\x1b.mandiant.capa.StaticLayout\x12:\n\x0e\x66\x65\x61ture_counts\x18\x08 \x01(\x0b\x32\".mandiant.capa.StaticFeatureCounts\x12\x39\n\x11library_functions\x18\t \x03(\x0b\x32\x1e.mandiant.capa.LibraryFunction\"[\n\x13StaticFeatureCounts\x12\x0c\n\x04\x66ile\x18\x01 \x01(\x04\x12\x36\n\tfunctions\x18\x02 \x03(\x0b\x32#.mandiant.capa.FunctionFeatureCount\"@\n\x0cStaticLayout\x12\x30\n\tfunctions\x18\x01 \x03(\x0b\x32\x1d.mandiant.capa.FunctionLayout\"W\n\rStringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"p\n\x11SubscopeStatement\x12\x0c\n\x04type\x18\x01 \x01(\t\x12#\n\x05scope\x18\x02 \x01(\x0e\x32\x14.mandiant.capa.Scope\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"]\n\x10SubstringFeature\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x11\n\tsubstring\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"C\n\nCallLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x0c\n\x04name\x18\x02 \x01(\t\"i\n\x0cThreadLayout\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12\x30\n\rmatched_calls\x18\x02 \x03(\x0b\x32\x19.mandiant.capa.CallLayout\"4\n\tAddresses\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x03(\x0b\x32\x16.mandiant.capa.Address\"b\n\x12Pair_Address_Match\x12\'\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Address\x12#\n\x05match\x18\x02 \x01(\x0b\x32\x14.mandiant.capa.Match\"E\n\x0cToken_Offset\x12%\n\x05token\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\x0e\n\x06offset\x18\x02 \x01(\x04\"U\n\x08Ppid_Pid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\"~\n\x0cPpid_Pid_Tid\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\"\xa5\x01\n\x0fPpid_Pid_Tid_Id\x12$\n\x04ppid\x18\x01 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03pid\x18\x02 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12#\n\x03tid\x18\x03 \x01(\x0b\x32\x16.mandiant.capa.Integer\x12\"\n\x02id\x18\x04 \x01(\x0b\x32\x16.mandiant.capa.Integer\",\n\x07Integer\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x42\x07\n\x05value\"8\n\x06Number\x12\x0b\n\x01u\x18\x01 \x01(\x04H\x00\x12\x0b\n\x01i\x18\x02 \x01(\x12H\x00\x12\x0b\n\x01\x66\x18\x03 \x01(\x01H\x00\x42\x07\n\x05value*\x92\x02\n\x0b\x41\x64\x64ressType\x12\x1b\n\x17\x41\x44\x44RESSTYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x44\x44RESSTYPE_ABSOLUTE\x10\x01\x12\x18\n\x14\x41\x44\x44RESSTYPE_RELATIVE\x10\x02\x12\x14\n\x10\x41\x44\x44RESSTYPE_FILE\x10\x03\x12\x18\n\x14\x41\x44\x44RESSTYPE_DN_TOKEN\x10\x04\x12\x1f\n\x1b\x41\x44\x44RESSTYPE_DN_TOKEN_OFFSET\x10\x05\x12\x1a\n\x16\x41\x44\x44RESSTYPE_NO_ADDRESS\x10\x06\x12\x17\n\x13\x41\x44\x44RESSTYPE_PROCESS\x10\x07\x12\x16\n\x12\x41\x44\x44RESSTYPE_THREAD\x10\x08\x12\x14\n\x10\x41\x44\x44RESSTYPE_CALL\x10\t*G\n\x06\x46lavor\x12\x16\n\x12\x46LAVOR_UNSPECIFIED\x10\x00\x12\x11\n\rFLAVOR_STATIC\x10\x01\x12\x12\n\x0e\x46LAVOR_DYNAMIC\x10\x02*\xbe\x01\n\x05Scope\x12\x15\n\x11SCOPE_UNSPECIFIED\x10\x00\x12\x0e\n\nSCOPE_FILE\x10\x01\x12\x12\n\x0eSCOPE_FUNCTION\x10\x02\x12\x15\n\x11SCOPE_BASIC_BLOCK\x10\x03\x12\x15\n\x11SCOPE_INSTRUCTION\x10\x04\x12\x11\n\rSCOPE_PROCESS\x10\x05\x12\x10\n\x0cSCOPE_THREAD\x10\x06\x12\x0e\n\nSCOPE_CALL\x10\x07\x12\x17\n\x13SCOPE_SPAN_OF_CALLS\x10\x08\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -44,7 +44,7 @@ _globals['_FLAVOR']._serialized_start=9338 _globals['_FLAVOR']._serialized_end=9409 _globals['_SCOPE']._serialized_start=9412 - _globals['_SCOPE']._serialized_end=9597 + _globals['_SCOPE']._serialized_end=9602 _globals['_APIFEATURE']._serialized_start=47 _globals['_APIFEATURE']._serialized_end=128 _globals['_ADDRESS']._serialized_start=131 diff --git a/capa/render/proto/capa_pb2.pyi b/capa/render/proto/capa_pb2.pyi index 06f846170..98420a292 100644 --- a/capa/render/proto/capa_pb2.pyi +++ b/capa/render/proto/capa_pb2.pyi @@ -94,7 +94,7 @@ class _ScopeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumType SCOPE_PROCESS: _Scope.ValueType # 5 SCOPE_THREAD: _Scope.ValueType # 6 SCOPE_CALL: _Scope.ValueType # 7 - SCOPE_SEQUENCE: _Scope.ValueType # 8 + SCOPE_SPAN_OF_CALLS: _Scope.ValueType # 8 class Scope(_Scope, metaclass=_ScopeEnumTypeWrapper): ... @@ -106,7 +106,7 @@ SCOPE_INSTRUCTION: Scope.ValueType # 4 SCOPE_PROCESS: Scope.ValueType # 5 SCOPE_THREAD: Scope.ValueType # 6 SCOPE_CALL: Scope.ValueType # 7 -SCOPE_SEQUENCE: Scope.ValueType # 8 +SCOPE_SPAN_OF_CALLS: Scope.ValueType # 8 global___Scope = Scope @typing.final diff --git a/capa/render/result_document.py b/capa/render/result_document.py index d1bce9add..2acfa8cf3 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -399,12 +399,12 @@ def from_capa( if location in rule_matches: # exact match, such as matching a call-scoped rule. children.append(Match.from_capa(rules, capabilities, rule_matches[location])) - # we'd like to assert the scope of the current rule is "sequence" + # we'd like to assert the scope of the current rule is span-of-calls # but we don't have that data here. else: - # Sequence scopes can match each other, but they don't strictly contain each other, + # Span-of-calls scopes can match each other, but they don't strictly contain each other, # like the way a function contains a basic block. - # So when we have a match within a sequence for another sequence, we need to look + # So when we have a match within a span for another span, we need to look # for all the places it might be found. # # Despite the edge cases (like API hammering), this turns out to be pretty easy: diff --git a/capa/render/verbose.py b/capa/render/verbose.py index a89db5039..f33ea7304 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -126,7 +126,7 @@ def render_thread(layout: rd.DynamicLayout, addr: frz.Address) -> str: return f"{name}{{pid:{thread.process.pid},tid:{thread.tid}}}" -def render_sequence(layout: rd.DynamicLayout, addrs: list[frz.Address]) -> str: +def render_span_of_calls(layout: rd.DynamicLayout, addrs: list[frz.Address]) -> str: calls: list[capa.features.address.DynamicCallAddress] = [addr.to_capa() for addr in addrs] # type: ignore for call in calls: assert isinstance(call, capa.features.address.DynamicCallAddress) @@ -328,7 +328,7 @@ def render_rules(console: Console, doc: rd.ResultDocument): lines = [render_process(doc.meta.analysis.layout, loc) for loc in locations] elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD: lines = [render_thread(doc.meta.analysis.layout, loc) for loc in locations] - elif rule.meta.scopes.dynamic in (capa.rules.Scope.CALL, capa.rules.Scope.SEQUENCE): + elif rule.meta.scopes.dynamic in (capa.rules.Scope.CALL, capa.rules.Scope.SPAN_OF_CALLS): # because we're only in verbose mode, we won't show the full call details (name, args, retval) # we'll only show the details of the thread in which the calls are found. # so select the thread locations and render those. diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index ad8ff4964..6779200ff 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -231,7 +231,7 @@ def render_feature( # of the output, so don't re-render it again for each feature. pass elif isinstance(layout, rd.DynamicLayout) and isinstance(feature, frzf.MatchFeature): - # don't render copies of the sequence address for submatches + # don't render copies of the span of calls address for submatches pass else: render_locations(console, layout, match.locations, indent) @@ -312,13 +312,13 @@ def render_match( render_match(console, layout, rule, child, indent=indent + 1, mode=child_mode) -def collect_sequence_locations( +def collect_span_of_calls_locations( match: rd.Match, mode=MODE_SUCCESS, ): """ - Find all the (call, sequence) locations used in a given sequence match, recursively. - Useful to collect the events used to match a sequence scoped rule. + Find all the call locations used in a given span-of-calls match, recursively. + Useful to collect the events used to match a span-of-calls scoped rule. """ if isinstance(match.node, rd.StatementNode): if ( @@ -327,7 +327,7 @@ def collect_sequence_locations( ): child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS for child in match.children: - yield from collect_sequence_locations(child, child_mode) + yield from collect_span_of_calls_locations(child, child_mode) elif isinstance(match.node.statement, rd.RangeStatement): for location in match.locations: if location.type not in (frz.AddressType.CALL,): @@ -337,7 +337,7 @@ def collect_sequence_locations( yield location else: for child in match.children: - yield from collect_sequence_locations(child, mode) + yield from collect_span_of_calls_locations(child, mode) elif isinstance(match.node, rd.FeatureNode): for location in match.locations: if location.type not in (frz.AddressType.CALL,): @@ -488,9 +488,9 @@ def render_rules(console: Console, doc: rd.ResultDocument): console.write(v.render_process(doc.meta.analysis.layout, location)) elif rule.meta.scopes.dynamic == capa.rules.Scope.THREAD: console.write(v.render_thread(doc.meta.analysis.layout, location)) - elif rule.meta.scopes.dynamic == capa.rules.Scope.SEQUENCE: - calls = sorted(set(collect_sequence_locations(match))) - console.write(hanging_indent(v.render_sequence(doc.meta.analysis.layout, calls), indent=1)) + elif rule.meta.scopes.dynamic == capa.rules.Scope.SPAN_OF_CALLS: + calls = sorted(set(collect_span_of_calls_locations(match))) + console.write(hanging_indent(v.render_span_of_calls(doc.meta.analysis.layout, calls), indent=1)) elif rule.meta.scopes.dynamic == capa.rules.Scope.CALL: console.write(hanging_indent(v.render_call(doc.meta.analysis.layout, location), indent=1)) else: diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 907ae9be4..7c1025256 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -86,7 +86,7 @@ class Scope(str, Enum): FILE = "file" PROCESS = "process" THREAD = "thread" - SEQUENCE = "sequence" + SPAN_OF_CALLS = "span of calls" CALL = "call" FUNCTION = "function" BASIC_BLOCK = "basic block" @@ -115,7 +115,7 @@ def to_yaml(cls, representer, node): Scope.GLOBAL, Scope.PROCESS, Scope.THREAD, - Scope.SEQUENCE, + Scope.SPAN_OF_CALLS, Scope.CALL, } @@ -201,7 +201,7 @@ def from_dict(self, scopes: dict[str, str]) -> "Scopes": capa.features.common.MatchedRule, }, Scope.THREAD: set(), - Scope.SEQUENCE: set(), + Scope.SPAN_OF_CALLS: set(), Scope.CALL: { capa.features.common.MatchedRule, capa.features.common.Regex, @@ -256,14 +256,14 @@ def from_dict(self, scopes: dict[str, str]) -> "Scopes": SUPPORTED_FEATURES[Scope.FILE].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.GLOBAL]) -SUPPORTED_FEATURES[Scope.SEQUENCE].update(SUPPORTED_FEATURES[Scope.GLOBAL]) +SUPPORTED_FEATURES[Scope.SPAN_OF_CALLS].update(SUPPORTED_FEATURES[Scope.GLOBAL]) SUPPORTED_FEATURES[Scope.CALL].update(SUPPORTED_FEATURES[Scope.GLOBAL]) -# all call scope features are also sequence features -SUPPORTED_FEATURES[Scope.SEQUENCE].update(SUPPORTED_FEATURES[Scope.CALL]) -# all sequence scope features (and therefore, call features) are also thread features -SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.SEQUENCE]) +# all call scope features are also span-of-calls features +SUPPORTED_FEATURES[Scope.SPAN_OF_CALLS].update(SUPPORTED_FEATURES[Scope.CALL]) +# all span-of-calls scope features (and therefore, call features) are also thread features +SUPPORTED_FEATURES[Scope.THREAD].update(SUPPORTED_FEATURES[Scope.SPAN_OF_CALLS]) # all thread scope features are also process features SUPPORTED_FEATURES[Scope.PROCESS].update(SUPPORTED_FEATURES[Scope.THREAD]) @@ -622,7 +622,7 @@ def build_statements(d, scopes: Scopes): elif key == "process": if Scope.FILE not in scopes: - raise InvalidRule("process subscope supported only for file scope") + raise InvalidRule("`process` subscope supported only for `file` scope") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") @@ -633,7 +633,7 @@ def build_statements(d, scopes: Scopes): elif key == "thread": if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS)): - raise InvalidRule("thread subscope supported only for the process scope") + raise InvalidRule("`thread` subscope supported only for the `process` scope") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") @@ -642,20 +642,22 @@ def build_statements(d, scopes: Scopes): Scope.THREAD, build_statements(d[key][0], Scopes(dynamic=Scope.THREAD)), description=description ) - elif key == "sequence": - if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SEQUENCE)): - raise InvalidRule("sequence subscope supported only for the process and thread scopes") + elif key == "span of calls": + if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SPAN_OF_CALLS)): + raise InvalidRule("`span of calls` subscope supported only for the `process` and `thread` scopes") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") return ceng.Subscope( - Scope.SEQUENCE, build_statements(d[key][0], Scopes(dynamic=Scope.SEQUENCE)), description=description + Scope.SPAN_OF_CALLS, + build_statements(d[key][0], Scopes(dynamic=Scope.SPAN_OF_CALLS)), + description=description, ) elif key == "call": - if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SEQUENCE, Scope.CALL)): - raise InvalidRule("call subscope supported only for the process, thread, and call scopes") + if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SPAN_OF_CALLS, Scope.CALL)): + raise InvalidRule("`call` subscope supported only for the `process`, `thread`, and `call` scopes") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") @@ -666,7 +668,7 @@ def build_statements(d, scopes: Scopes): elif key == "function": if Scope.FILE not in scopes: - raise InvalidRule("function subscope supported only for file scope") + raise InvalidRule("`function` subscope supported only for `file` scope") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") @@ -677,7 +679,7 @@ def build_statements(d, scopes: Scopes): elif key == "basic block": if Scope.FUNCTION not in scopes: - raise InvalidRule("basic block subscope supported only for function scope") + raise InvalidRule("`basic block` subscope supported only for `function` scope") if len(d[key]) != 1: raise InvalidRule("subscope must have exactly one child statement") @@ -688,7 +690,7 @@ def build_statements(d, scopes: Scopes): elif key == "instruction": if all(s not in scopes for s in (Scope.FUNCTION, Scope.BASIC_BLOCK)): - raise InvalidRule("instruction subscope supported only for function and basic block scope") + raise InvalidRule("`instruction` subscope supported only for `function` and `basic block` scope") if len(d[key]) == 1: statements = build_statements(d[key][0], Scopes(static=Scope.INSTRUCTION)) @@ -1401,7 +1403,7 @@ def __init__( scopes = ( Scope.CALL, - Scope.SEQUENCE, + Scope.SPAN_OF_CALLS, Scope.THREAD, Scope.PROCESS, Scope.INSTRUCTION, @@ -1433,8 +1435,8 @@ def thread_rules(self): return self.rules_by_scope[Scope.THREAD] @property - def sequence_rules(self): - return self.rules_by_scope[Scope.SEQUENCE] + def span_of_calls_rules(self): + return self.rules_by_scope[Scope.SPAN_OF_CALLS] @property def call_rules(self): diff --git a/scripts/lint.py b/scripts/lint.py index 7bf432e08..4c4e09322 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -194,7 +194,7 @@ def check_rule(self, ctx: Context, rule: Rule): "file", "process", "thread", - "sequence", + "span of calls", "call", "unsupported", ) diff --git a/tests/test_dynamic_sequence_scope.py b/tests/test_dynamic_span_of_calls_scope.py similarity index 86% rename from tests/test_dynamic_sequence_scope.py rename to tests/test_dynamic_span_of_calls_scope.py index 98b3eaf4e..5f1a19a70 100644 --- a/tests/test_dynamic_sequence_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -99,12 +99,12 @@ def test_dynamic_call_scope(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in matches - assert 8 in get_call_ids(matches[r.name]) + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + assert 8 in get_call_ids(capabilities.matches[r.name]) -# match the first sequence. +# match the first span. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -113,7 +113,7 @@ def test_dynamic_call_scope(): # call 10: LdrGetDllHandle(1974337536, kernel32.dll) # call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) # call 12: LdrGetDllHandle(1974337536, kernel32.dll) -def test_dynamic_sequence_scope(): +def test_dynamic_span_scope(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -123,7 +123,7 @@ def test_dynamic_sequence_scope(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - api: GetSystemTimeAsFileTime @@ -137,12 +137,12 @@ def test_dynamic_sequence_scope(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in matches - assert 12 in get_call_ids(matches[r.name]) + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + assert 12 in get_call_ids(capabilities.matches[r.name]) -# show that when the sequence is only 5 calls long (for example), it doesn't match beyond that 5-tuple. +# show that when the span is only 5 calls long (for example), it doesn't match beyond that 5-tuple. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -155,7 +155,7 @@ def test_dynamic_sequence_scope(): # call 14: RtlAddVectoredExceptionHandler(1921490089, 0) # call 15: GetSystemTime() # call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) -def test_dynamic_sequence_scope_length(): +def test_dynamic_span_scope_length(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -165,7 +165,7 @@ def test_dynamic_sequence_scope_length(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - api: GetSystemTimeAsFileTime @@ -176,24 +176,24 @@ def test_dynamic_sequence_scope_length(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - # patch SEQUENCE_SIZE since we may use a much larger value in the real world. + # patch SPAN_SIZE since we may use a much larger value in the real world. from pytest import MonkeyPatch with MonkeyPatch.context() as m: - m.setattr(capa.capabilities.dynamic, "SEQUENCE_SIZE", 5) + m.setattr(capa.capabilities.dynamic, "SPAN_SIZE", 5) capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) assert r.name not in capabilities.matches -# show that you can use a call subscope in sequence rules. +# show that you can use a call subscope in span-of-calls rules. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 # ... # call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) # ... -def test_dynamic_sequence_call_subscope(): +def test_dynamic_span_call_subscope(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -203,7 +203,7 @@ def test_dynamic_sequence_call_subscope(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - call: @@ -221,7 +221,7 @@ def test_dynamic_sequence_call_subscope(): assert 11 in get_call_ids(capabilities.matches[r.name]) -# show that you can use a sequence subscope in sequence rules. +# show that you can use a span subscope in span rules. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -231,7 +231,7 @@ def test_dynamic_sequence_call_subscope(): # call 12: LdrGetDllHandle(1974337536, kernel32.dll) # call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) # ... -def test_dynamic_sequence_scope_sequence_subscope(): +def test_dynamic_span_scope_span_subscope(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -241,16 +241,16 @@ def test_dynamic_sequence_scope_sequence_subscope(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - - sequence: + - span of calls: - description: resolve add VEH # should match at 11 - and: - api: LdrGetDllHandle - api: LdrGetProcedureAddress - string: AddVectoredExceptionHandler - - sequence: + - span of calls: - description: resolve remove VEH # should match at 13 - and: - api: LdrGetDllHandle @@ -267,8 +267,8 @@ def test_dynamic_sequence_scope_sequence_subscope(): assert 13 in get_call_ids(capabilities.matches[r.name]) -# show that you can't use thread subscope in sequence rules. -def test_dynamic_sequence_scope_thread_subscope(): +# show that you can't use thread subscope in span rules. +def test_dynamic_span_scope_thread_subscope(): rule = textwrap.dedent( """ rule: @@ -276,7 +276,7 @@ def test_dynamic_sequence_scope_thread_subscope(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - thread: @@ -288,7 +288,7 @@ def test_dynamic_sequence_scope_thread_subscope(): capa.rules.Rule.from_yaml(rule) -# show how you might use a sequence rule: to match a small window for a collection of features. +# show how you might use a span-of-calls rule: to match a small window for a collection of features. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -297,7 +297,7 @@ def test_dynamic_sequence_scope_thread_subscope(): # call 12: ... # call 13: ... # call 14: RtlAddVectoredExceptionHandler(1921490089, 0) -def test_dynamic_sequence_example(): +def test_dynamic_span_example(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -307,7 +307,7 @@ def test_dynamic_sequence_example(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - call: @@ -325,12 +325,12 @@ def test_dynamic_sequence_example(): r = capa.rules.Rule.from_yaml(rule) ruleset = capa.rules.RuleSet([r]) - matches, features = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in matches - assert 14 in get_call_ids(matches[r.name]) + capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) + assert r.name in capabilities.matches + assert 14 in get_call_ids(capabilities.matches[r.name]) -# show how sequences that overlap a single event are handled. +# show how spans that overlap a single event are handled. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -342,7 +342,7 @@ def test_dynamic_sequence_example(): # call 14: ... # call 15: ... # ... -def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): +def test_dynamic_span_multiple_spans_overlapping_single_event(): extractor = get_0000a657_thread3064() rule = textwrap.dedent( @@ -352,7 +352,7 @@ def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): name: test rule scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - call: @@ -367,11 +367,11 @@ def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) assert r.name in capabilities.matches - # we only match the first overlapping sequence + # we only match the first overlapping span assert [11] == list(get_call_ids(capabilities.matches[r.name])) -# show that you can use match statements in sequence rules. +# show that you can use match statements in span-of-calls rules. # # proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) # thread: 3064 @@ -381,7 +381,7 @@ def test_dynamic_sequence_multiple_sequences_overlapping_single_event(): # call 12: LdrGetDllHandle(1974337536, kernel32.dll) # call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) # ... -def test_dynamic_sequence_scope_match_statements(): +def test_dynamic_span_scope_match_statements(): extractor = get_0000a657_thread3064() ruleset = capa.rules.RuleSet( @@ -395,7 +395,7 @@ def test_dynamic_sequence_scope_match_statements(): namespace: linking/runtime-linking/veh scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - api: LdrGetDllHandle @@ -413,7 +413,7 @@ def test_dynamic_sequence_scope_match_statements(): namespace: linking/runtime-linking/veh scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - api: LdrGetDllHandle @@ -430,7 +430,7 @@ def test_dynamic_sequence_scope_match_statements(): name: resolve add and remove VEH scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - match: resolve add VEH @@ -446,7 +446,7 @@ def test_dynamic_sequence_scope_match_statements(): name: has VEH runtime linking scopes: static: unsupported - dynamic: sequence + dynamic: span of calls features: - and: - match: linking/runtime-linking/veh diff --git a/tests/test_proto.py b/tests/test_proto.py index 474efc017..b0dc10604 100644 --- a/tests/test_proto.py +++ b/tests/test_proto.py @@ -129,7 +129,7 @@ def test_scope_to_pb2(): assert capa.render.proto.scope_to_pb2(capa.rules.Scope.INSTRUCTION) == capa_pb2.SCOPE_INSTRUCTION assert capa.render.proto.scope_to_pb2(capa.rules.Scope.PROCESS) == capa_pb2.SCOPE_PROCESS assert capa.render.proto.scope_to_pb2(capa.rules.Scope.THREAD) == capa_pb2.SCOPE_THREAD - assert capa.render.proto.scope_to_pb2(capa.rules.Scope.SEQUENCE) == capa_pb2.SCOPE_SEQUENCE + assert capa.render.proto.scope_to_pb2(capa.rules.Scope.SPAN_OF_CALLS) == capa_pb2.SCOPE_SPAN_OF_CALLS assert capa.render.proto.scope_to_pb2(capa.rules.Scope.CALL) == capa_pb2.SCOPE_CALL From 9a0c4f712d599c78ae776eaf6d310ba8202ccbec Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 20 Jan 2025 13:20:26 +0000 Subject: [PATCH 13/17] vverbose: fix rendering of span-of-calls summaries https://github.com/mandiant/capa/pull/2532#discussion_r1920711965 vverbose: fix collection of span-of-calls call match locations --- capa/render/vverbose.py | 47 ++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index 6779200ff..b3ca445a5 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -320,33 +320,26 @@ def collect_span_of_calls_locations( Find all the call locations used in a given span-of-calls match, recursively. Useful to collect the events used to match a span-of-calls scoped rule. """ - if isinstance(match.node, rd.StatementNode): - if ( - isinstance(match.node.statement, rd.CompoundStatement) - and match.node.statement.type == rd.CompoundStatementType.NOT - ): - child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS - for child in match.children: - yield from collect_span_of_calls_locations(child, child_mode) - elif isinstance(match.node.statement, rd.RangeStatement): - for location in match.locations: - if location.type not in (frz.AddressType.CALL,): - continue - if mode == MODE_FAILURE: - continue - yield location - else: - for child in match.children: - yield from collect_span_of_calls_locations(child, mode) - elif isinstance(match.node, rd.FeatureNode): - for location in match.locations: - if location.type not in (frz.AddressType.CALL,): - continue - if mode == MODE_FAILURE: - continue - yield location - else: - raise ValueError("unexpected node type") + if not match.success: + return + + for location in match.locations: + if location.type != frz.AddressType.CALL: + continue + + if mode == MODE_FAILURE: + # only collect positive evidence, + # not things that filter out branches. + continue + + yield location + + child_mode = mode + if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT: + child_mode = MODE_FAILURE if mode == MODE_SUCCESS else MODE_SUCCESS + + for child in match.children: + yield from collect_span_of_calls_locations(child, child_mode) def render_rules(console: Console, doc: rd.ResultDocument): From 4f844533c52e6f03df92fa24e39781d5fa9b3671 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 20 Jan 2025 13:51:18 +0000 Subject: [PATCH 14/17] vverbose: don't use plural "calls" when there's a single call --- capa/render/verbose.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/capa/render/verbose.py b/capa/render/verbose.py index f33ea7304..b96dff12b 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -128,12 +128,18 @@ def render_thread(layout: rd.DynamicLayout, addr: frz.Address) -> str: def render_span_of_calls(layout: rd.DynamicLayout, addrs: list[frz.Address]) -> str: calls: list[capa.features.address.DynamicCallAddress] = [addr.to_capa() for addr in addrs] # type: ignore + assert len(calls) > 0 for call in calls: assert isinstance(call, capa.features.address.DynamicCallAddress) + call = calls[0] pname = _get_process_name(layout, frz.Address.from_capa(calls[0].thread.process)) call_ids = [str(call.id) for call in calls] - return f"{pname}{{pid:{call.thread.process.pid},tid:{call.thread.tid},calls:{{{','.join(call_ids)}}}}}" + if len(call_ids) == 1: + call_id = call_ids[0] + return f"{pname}{{pid:{call.thread.process.pid},tid:{call.thread.tid},call:{call_id}}}" + else: + return f"{pname}{{pid:{call.thread.process.pid},tid:{call.thread.tid},calls:{{{','.join(call_ids)}}}}}" def render_call(layout: rd.DynamicLayout, addr: frz.Address) -> str: From caae77dab6587432ce1297e96383694f00462b49 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 20 Jan 2025 13:52:16 +0000 Subject: [PATCH 15/17] vverbose: don't render full ppid/pid/tid in nested blocks, only callid --- capa/render/verbose.py | 19 +++++++++++++++++++ capa/render/vverbose.py | 28 ++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/capa/render/verbose.py b/capa/render/verbose.py index b96dff12b..11f244237 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -164,6 +164,25 @@ def render_call(layout: rd.DynamicLayout, addr: frz.Address) -> str: ) +def render_short_call(layout: rd.DynamicLayout, addr: frz.Address) -> str: + call = addr.to_capa() + assert isinstance(call, capa.features.address.DynamicCallAddress) + + cname = _get_call_name(layout, addr) + + fname, _, rest = cname.partition("(") + args, _, rest = rest.rpartition(")") + + s = [] + s.append(f"{fname}(") + for arg in args.split(", "): + s.append(f" {arg},") + s.append(f"){rest}") + + newline = "\n" + return f"call:{call.id}\n{rutils.mute(newline.join(s))}" + + def render_static_meta(console: Console, meta: rd.StaticMetadata): """ like: diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index b3ca445a5..e905f8c0f 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -54,7 +54,19 @@ def hanging_indent(s: str, indent: int) -> str: return textwrap.indent(s, prefix=prefix)[len(prefix) :] -def render_locations(console: Console, layout: rd.Layout, locations: Iterable[frz.Address], indent: int): +def render_locations( + console: Console, layout: rd.Layout, locations: Iterable[frz.Address], indent: int, use_short_format: bool = False +): + """ + Render the given locations, such as virtual address or pid/tid/callid with process name. + + If `use_short_format` is True: + render a small representation of the given locations, such as just the call id, + assuming another region of output specified the full location details (like pid/tid). + + In effect, rather than rendering ppid/pid/tid/callid everywhere, + describe ppid/pid/tid once, and then refer to just callid in the subsequent blocklin the subsequent block. + """ import capa.render.verbose as v # it's possible to have an empty locations array here, @@ -73,7 +85,11 @@ def render_locations(console: Console, layout: rd.Layout, locations: Iterable[fr if location.type == frz.AddressType.CALL: assert isinstance(layout, rd.DynamicLayout) - console.write(hanging_indent(v.render_call(layout, location), indent + 1)) + if use_short_format: + render_call = v.render_short_call + else: + render_call = v.render_call + console.write(hanging_indent(render_call(layout, location), indent + 1)) else: console.write(v.format_address(locations[0])) @@ -81,6 +97,10 @@ def render_locations(console: Console, layout: rd.Layout, locations: Iterable[fr location = locations[0] assert isinstance(layout, rd.DynamicLayout) + if use_short_format: + render_call = v.render_short_call + else: + render_call = v.render_call s = f"{v.render_call(layout, location)}\nand {(len(locations) - 1)} more..." console.write(hanging_indent(s, indent + 1)) @@ -234,7 +254,7 @@ def render_feature( # don't render copies of the span of calls address for submatches pass else: - render_locations(console, layout, match.locations, indent) + render_locations(console, layout, match.locations, indent, use_short_format=True) console.writeln() else: # like: @@ -251,7 +271,7 @@ def render_feature( # like above, don't re-render calls when in call scope. pass else: - render_locations(console, layout, locations, indent=indent) + render_locations(console, layout, locations, indent=indent + 1, use_short_format=True) console.writeln() From 990fd2075776e6298d21da0ea7ddba011ca6f2a0 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Wed, 29 Jan 2025 09:23:51 +0000 Subject: [PATCH 16/17] update submodules --- rules | 2 +- tests/data | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rules b/rules index 6cb2ec010..b4e0c8cdf 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit 6cb2ec010b4d0679612295301d0b3e6336da4f33 +Subproject commit b4e0c8cdf89a9d1fc41c2fc151bbb9c985d59a9e diff --git a/tests/data b/tests/data index ea10c47b3..6cf615dd0 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit ea10c47b32c00441529cea69d9ad92bf47f11ac9 +Subproject commit 6cf615dd014de8b1979d2619d7f832b4133bfa11 From 83ec75c49d7e93f6838b54b90477f747cb5a6bbf Mon Sep 17 00:00:00 2001 From: Capa Bot Date: Wed, 29 Jan 2025 09:41:14 +0000 Subject: [PATCH 17/17] Sync capa rules submodule --- CHANGELOG.md | 3 ++- rules | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925005a10..c8ddce6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,11 @@ - add span-of-calls scope to rule format - capabilities functions return dataclasses instead of tuples -### New Rules (2) +### New Rules (3) - data-manipulation/encryption/rsa/encrypt-data-using-rsa-via-embedded-library Ana06 - data-manipulation/encryption/use-bigint-function Ana06 +- nursery/dynamic-add-veh wballenthin@google.com - ### Bug Fixes diff --git a/rules b/rules index b4e0c8cdf..ff76d01d6 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit b4e0c8cdf89a9d1fc41c2fc151bbb9c985d59a9e +Subproject commit ff76d01d62095839d8f430cc5538bf5c678e138b