diff --git a/capa/features/extractors/ida/basicblock.py b/capa/features/extractors/ida/basicblock.py index a6f2557d..f878ab1a 100644 --- a/capa/features/extractors/ida/basicblock.py +++ b/capa/features/extractors/ida/basicblock.py @@ -20,10 +20,10 @@ from capa.features.extractors.helpers import MIN_STACKSTRING_LEN def get_printable_len(op): - """ Return string length if all operand bytes are ascii or utf16-le printable + """Return string length if all operand bytes are ascii or utf16-le printable - args: - op (IDA op_t) + args: + op (IDA op_t) """ op_val = capa.features.extractors.ida.helpers.mask_op_val(op) @@ -62,10 +62,10 @@ def get_printable_len(op): def is_mov_imm_to_stack(insn): - """ verify instruction moves immediate onto stack + """verify instruction moves immediate onto stack - args: - insn (IDA insn_t) + args: + insn (IDA insn_t) """ if insn.Op2.type != idaapi.o_imm: return False @@ -80,13 +80,13 @@ def is_mov_imm_to_stack(insn): def bb_contains_stackstring(f, bb): - """ check basic block for stackstring indicators + """check basic block for stackstring indicators - true if basic block contains enough moves of constant bytes to the stack + true if basic block contains enough moves of constant bytes to the stack - args: - f (IDA func_t) - bb (IDA BasicBlock) + args: + f (IDA func_t) + bb (IDA BasicBlock) """ count = 0 for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea): @@ -98,33 +98,33 @@ def bb_contains_stackstring(f, bb): def extract_bb_stackstring(f, bb): - """ extract stackstring indicators from basic block + """extract stackstring indicators from basic block - args: - f (IDA func_t) - bb (IDA BasicBlock) + args: + f (IDA func_t) + bb (IDA BasicBlock) """ if bb_contains_stackstring(f, bb): yield Characteristic("stack string"), bb.start_ea def extract_bb_tight_loop(f, bb): - """ extract tight loop indicators from a basic block + """extract tight loop indicators from a basic block - args: - f (IDA func_t) - bb (IDA BasicBlock) + args: + f (IDA func_t) + bb (IDA BasicBlock) """ if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bb): yield Characteristic("tight loop"), bb.start_ea def extract_features(f, bb): - """ extract basic block features + """extract basic block features - args: - f (IDA func_t) - bb (IDA BasicBlock) + args: + f (IDA func_t) + bb (IDA BasicBlock) """ for bb_handler in BASIC_BLOCK_HANDLERS: for (feature, ea) in bb_handler(f, bb): diff --git a/capa/features/extractors/ida/file.py b/capa/features/extractors/ida/file.py index 8939fdb4..2acc398b 100644 --- a/capa/features/extractors/ida/file.py +++ b/capa/features/extractors/ida/file.py @@ -20,13 +20,13 @@ from capa.features.file import Export, Import, Section def check_segment_for_pe(seg): - """ check segment for embedded PE + """check segment for embedded PE - adapted for IDA from: - https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19 + adapted for IDA from: + https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19 - args: - seg (IDA segment_t) + args: + seg (IDA segment_t) """ seg_max = seg.end_ea mz_xor = [ @@ -67,11 +67,11 @@ def check_segment_for_pe(seg): def extract_file_embedded_pe(): - """ extract embedded PE features + """extract embedded PE features - IDA must load resource sections for this to be complete - - '-R' from console - - Check 'Load resource sections' when opening binary in IDA manually + IDA must load resource sections for this to be complete + - '-R' from console + - Check 'Load resource sections' when opening binary in IDA manually """ for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True): for (ea, _) in check_segment_for_pe(seg): @@ -85,15 +85,15 @@ def extract_file_export_names(): def extract_file_import_names(): - """ extract function imports + """extract function imports - 1. imports by ordinal: - - modulename.#ordinal + 1. imports by ordinal: + - modulename.#ordinal - 2. imports by name, results in two features to support importname-only - matching: - - modulename.importname - - importname + 2. imports by name, results in two features to support importname-only + matching: + - modulename.importname + - importname """ for (ea, info) in capa.features.extractors.ida.helpers.get_file_imports().items(): if info[1]: @@ -104,22 +104,22 @@ def extract_file_import_names(): def extract_file_section_names(): - """ extract section names + """extract section names - IDA must load resource sections for this to be complete - - '-R' from console - - Check 'Load resource sections' when opening binary in IDA manually + IDA must load resource sections for this to be complete + - '-R' from console + - Check 'Load resource sections' when opening binary in IDA manually """ for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True): yield Section(idaapi.get_segm_name(seg)), seg.start_ea def extract_file_strings(): - """ extract ASCII and UTF-16 LE strings + """extract ASCII and UTF-16 LE strings - IDA must load resource sections for this to be complete - - '-R' from console - - Check 'Load resource sections' when opening binary in IDA manually + IDA must load resource sections for this to be complete + - '-R' from console + - Check 'Load resource sections' when opening binary in IDA manually """ for seg in capa.features.extractors.ida.helpers.get_segments(): seg_buff = capa.features.extractors.ida.helpers.get_segment_buffer(seg) diff --git a/capa/features/extractors/ida/function.py b/capa/features/extractors/ida/function.py index 67234b02..670c7b73 100644 --- a/capa/features/extractors/ida/function.py +++ b/capa/features/extractors/ida/function.py @@ -15,20 +15,20 @@ from capa.features.extractors import loops def extract_function_calls_to(f): - """ extract callers to a function + """extract callers to a function - args: - f (IDA func_t) + args: + f (IDA func_t) """ for ea in idautils.CodeRefsTo(f.start_ea, True): yield Characteristic("calls to"), ea def extract_function_loop(f): - """ extract loop indicators from a function + """extract loop indicators from a function - args: - f (IDA func_t) + args: + f (IDA func_t) """ edges = [] @@ -42,20 +42,20 @@ def extract_function_loop(f): def extract_recursive_call(f): - """ extract recursive function call + """extract recursive function call - args: - f (IDA func_t) + args: + f (IDA func_t) """ if capa.features.extractors.ida.helpers.is_function_recursive(f): yield Characteristic("recursive call"), f.start_ea def extract_features(f): - """ extract function features + """extract function features - arg: - f (IDA func_t) + arg: + f (IDA func_t) """ for func_handler in FUNCTION_HANDLERS: for (feature, ea) in func_handler(f): diff --git a/capa/features/extractors/ida/helpers.py b/capa/features/extractors/ida/helpers.py index 1e364e7c..cba352a9 100644 --- a/capa/features/extractors/ida/helpers.py +++ b/capa/features/extractors/ida/helpers.py @@ -15,12 +15,12 @@ import idautils def find_byte_sequence(start, end, seq): - """ find byte sequence + """find byte sequence - args: - start: min virtual address - end: max virtual address - seq: bytes to search e.g. b'\x01\x03' + args: + start: min virtual address + end: max virtual address + seq: bytes to search e.g. b'\x01\x03' """ if sys.version_info[0] >= 3: return idaapi.find_binary(start, end, " ".join(["%02x" % b for b in seq]), 0, idaapi.SEARCH_DOWN) @@ -29,14 +29,14 @@ def find_byte_sequence(start, end, seq): def get_functions(start=None, end=None, skip_thunks=False, skip_libs=False): - """ get functions, range optional + """get functions, range optional - args: - start: min virtual address - end: max virtual address + args: + start: min virtual address + end: max virtual address - ret: - yield func_t* + ret: + yield func_t* """ for ea in idautils.Functions(start=start, end=end): f = idaapi.get_func(ea) @@ -45,10 +45,10 @@ def get_functions(start=None, end=None, skip_thunks=False, skip_libs=False): def get_segments(skip_header_segments=False): - """ get list of segments (sections) in the binary image + """get list of segments (sections) in the binary image - args: - skip_header_segments: IDA may load header segments - skip if set + args: + skip_header_segments: IDA may load header segments - skip if set """ for n in range(idaapi.get_segm_qty()): seg = idaapi.getnseg(n) @@ -57,9 +57,9 @@ def get_segments(skip_header_segments=False): def get_segment_buffer(seg): - """ return bytes stored in a given segment + """return bytes stored in a given segment - decrease buffer size until IDA is able to read bytes from the segment + decrease buffer size until IDA is able to read bytes from the segment """ buff = b"" sz = seg.end_ea - seg.start_ea @@ -97,13 +97,13 @@ def get_file_imports(): def get_instructions_in_range(start, end): - """ yield instructions in range + """yield instructions in range - args: - start: virtual address (inclusive) - end: virtual address (exclusive) - yield: - (insn_t*) + args: + start: virtual address (inclusive) + end: virtual address (exclusive) + yield: + (insn_t*) """ for head in idautils.Heads(start, end): insn = idautils.DecodeInstruction(head) @@ -183,10 +183,10 @@ def find_string_at(ea, min=4): def get_op_phrase_info(op): - """ parse phrase features from operand + """parse phrase features from operand - Pretty much dup of sark's implementation: - https://github.com/tmr232/Sark/blob/master/sark/code/instruction.py#L28-L73 + Pretty much dup of sark's implementation: + https://github.com/tmr232/Sark/blob/master/sark/code/instruction.py#L28-L73 """ if op.type not in (idaapi.o_phrase, idaapi.o_displ): return {} @@ -269,15 +269,15 @@ def is_op_stack_var(ea, index): def mask_op_val(op): - """ mask value by data type + """mask value by data type - necessary due to a bug in AMD64 + necessary due to a bug in AMD64 - Example: - .rsrc:0054C12C mov [ebp+var_4], 0FFFFFFFFh + Example: + .rsrc:0054C12C mov [ebp+var_4], 0FFFFFFFFh - insn.Op2.dtype == idaapi.dt_dword - insn.Op2.value == 0xffffffffffffffff + insn.Op2.dtype == idaapi.dt_dword + insn.Op2.value == 0xffffffffffffffff """ masks = { idaapi.dt_byte: 0xFF, @@ -289,10 +289,10 @@ def mask_op_val(op): def is_function_recursive(f): - """ check if function is recursive + """check if function is recursive - args: - f (IDA func_t) + args: + f (IDA func_t) """ for ref in idautils.CodeRefsTo(f.start_ea, True): if f.contains(ref): @@ -301,13 +301,13 @@ def is_function_recursive(f): def is_basic_block_tight_loop(bb): - """ check basic block loops to self + """check basic block loops to self - true if last instruction in basic block branches to basic block start + true if last instruction in basic block branches to basic block start - args: - f (IDA func_t) - bb (IDA BasicBlock) + args: + f (IDA func_t) + bb (IDA BasicBlock) """ bb_end = idc.prev_head(bb.end_ea) if bb.start_ea < bb_end: diff --git a/capa/features/extractors/ida/insn.py b/capa/features/extractors/ida/insn.py index 90bbe721..80844788 100644 --- a/capa/features/extractors/ida/insn.py +++ b/capa/features/extractors/ida/insn.py @@ -62,15 +62,15 @@ def check_for_api_call(ctx, insn): def extract_insn_api_features(f, bb, insn): - """ parse instruction API features + """parse instruction API features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) - example: - call dword [0x00473038] + example: + call dword [0x00473038] """ for api in check_for_api_call(f.ctx, insn): for (feature, ea) in capa.features.extractors.helpers.generate_api_features(api, insn.ea): @@ -78,15 +78,15 @@ def extract_insn_api_features(f, bb, insn): def extract_insn_number_features(f, bb, insn): - """ parse instruction number features + """parse instruction number features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) - example: - push 3136B0h ; dwControlCode + example: + push 3136B0h ; dwControlCode """ if idaapi.is_ret_insn(insn): # skip things like: @@ -109,15 +109,15 @@ def extract_insn_number_features(f, bb, insn): def extract_insn_bytes_features(f, bb, insn): - """ parse referenced byte sequences + """parse referenced byte sequences - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) - example: - push offset iid_004118d4_IShellLinkA ; riid + example: + push offset iid_004118d4_IShellLinkA ; riid """ ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn) if ref != insn.ea: @@ -127,15 +127,15 @@ def extract_insn_bytes_features(f, bb, insn): def extract_insn_string_features(f, bb, insn): - """ parse instruction string features + """parse instruction string features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) - example: - push offset aAcr ; "ACR > " + example: + push offset aAcr ; "ACR > " """ ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn) if ref != insn.ea: @@ -145,15 +145,15 @@ def extract_insn_string_features(f, bb, insn): def extract_insn_offset_features(f, bb, insn): - """ parse instruction structure offset features + """parse instruction structure offset features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) - example: - .text:0040112F cmp [esi+4], ebx + example: + .text:0040112F cmp [esi+4], ebx """ for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_phrase, idaapi.o_displ)): if capa.features.extractors.ida.helpers.is_op_stack_var(insn.ea, op.n): @@ -175,11 +175,11 @@ def extract_insn_offset_features(f, bb, insn): def contains_stack_cookie_keywords(s): - """ check if string contains stack cookie keywords + """check if string contains stack cookie keywords - Examples: - xor ecx, ebp ; StackCookie - mov eax, ___security_cookie + Examples: + xor ecx, ebp ; StackCookie + mov eax, ___security_cookie """ if not s: return False @@ -190,30 +190,30 @@ def contains_stack_cookie_keywords(s): def bb_stack_cookie_registers(bb): - """ scan basic block for stack cookie operations + """scan basic block for stack cookie operations - yield registers ids that may have been used for stack cookie operations + yield registers ids that may have been used for stack cookie operations - assume instruction that sets stack cookie and nzxor exist in same block - and stack cookie register is not modified prior to nzxor + assume instruction that sets stack cookie and nzxor exist in same block + and stack cookie register is not modified prior to nzxor - Example: - .text:004062DA mov eax, ___security_cookie <-- stack cookie - .text:004062DF mov ecx, eax - .text:004062E1 mov ebx, [esi] - .text:004062E3 and ecx, 1Fh - .text:004062E6 mov edi, [esi+4] - .text:004062E9 xor ebx, eax - .text:004062EB mov esi, [esi+8] - .text:004062EE xor edi, eax <-- ignore - .text:004062F0 xor esi, eax <-- ignore - .text:004062F2 ror edi, cl - .text:004062F4 ror esi, cl - .text:004062F6 ror ebx, cl - .text:004062F8 cmp edi, esi - .text:004062FA jnz loc_40639D + Example: + .text:004062DA mov eax, ___security_cookie <-- stack cookie + .text:004062DF mov ecx, eax + .text:004062E1 mov ebx, [esi] + .text:004062E3 and ecx, 1Fh + .text:004062E6 mov edi, [esi+4] + .text:004062E9 xor ebx, eax + .text:004062EB mov esi, [esi+8] + .text:004062EE xor edi, eax <-- ignore + .text:004062F0 xor esi, eax <-- ignore + .text:004062F2 ror edi, cl + .text:004062F4 ror esi, cl + .text:004062F6 ror ebx, cl + .text:004062F8 cmp edi, esi + .text:004062FA jnz loc_40639D - TODO: this is expensive, but necessary?... + TODO: this is expensive, but necessary?... """ for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea): if contains_stack_cookie_keywords(idc.GetDisasm(insn.ea)): @@ -239,14 +239,14 @@ def is_nzxor_stack_cookie(f, bb, insn): def extract_insn_nzxor_characteristic_features(f, bb, insn): - """ parse instruction non-zeroing XOR instruction + """parse instruction non-zeroing XOR instruction - ignore expected non-zeroing XORs, e.g. security cookies + ignore expected non-zeroing XORs, e.g. security cookies - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ if insn.itype != idaapi.NN_xor: return @@ -258,23 +258,23 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn): def extract_insn_mnemonic_features(f, bb, insn): - """ parse instruction mnemonic features + """parse instruction mnemonic features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ yield Mnemonic(insn.get_canon_mnem()), insn.ea def extract_insn_peb_access_characteristic_features(f, bb, insn): - """ parse instruction peb access + """parse instruction peb access - fs:[0x30] on x86, gs:[0x60] on x64 + fs:[0x30] on x86, gs:[0x60] on x64 - TODO: - IDA should be able to do this.. + TODO: + IDA should be able to do this.. """ if insn.itype not in (idaapi.NN_push, idaapi.NN_mov): return @@ -291,10 +291,10 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn): def extract_insn_segment_access_features(f, bb, insn): - """ parse instruction fs or gs access + """parse instruction fs or gs access - TODO: - IDA should be able to do this... + TODO: + IDA should be able to do this... """ if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)): # try to optimize for only memory references @@ -312,12 +312,12 @@ def extract_insn_segment_access_features(f, bb, insn): def extract_insn_cross_section_cflow(f, bb, insn): - """ inspect the instruction for a CALL or JMP that crosses section boundaries + """inspect the instruction for a CALL or JMP that crosses section boundaries - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ for ref in idautils.CodeRefsFrom(insn.ea, False): if ref in get_imports(f.ctx).keys(): @@ -332,14 +332,14 @@ def extract_insn_cross_section_cflow(f, bb, insn): def extract_function_calls_from(f, bb, insn): - """ extract functions calls from features + """extract functions calls from features - most relevant at the function scope, however, its most efficient to extract at the instruction scope + most relevant at the function scope, however, its most efficient to extract at the instruction scope - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ if idaapi.is_call_insn(insn): for ref in idautils.CodeRefsFrom(insn.ea, False): @@ -347,28 +347,28 @@ def extract_function_calls_from(f, bb, insn): def extract_function_indirect_call_characteristic_features(f, bb, insn): - """ extract indirect function calls (e.g., call eax or call dword ptr [edx+4]) - does not include calls like => call ds:dword_ABD4974 + """extract indirect function calls (e.g., call eax or call dword ptr [edx+4]) + does not include calls like => call ds:dword_ABD4974 - most relevant at the function or basic block scope; - however, its most efficient to extract at the instruction scope + most relevant at the function or basic block scope; + however, its most efficient to extract at the instruction scope - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ if idaapi.is_call_insn(insn) and idc.get_operand_type(insn.ea, 0) in (idc.o_reg, idc.o_phrase, idc.o_displ): yield Characteristic("indirect call"), insn.ea def extract_features(f, bb, insn): - """ extract instruction features + """extract instruction features - args: - f (IDA func_t) - bb (IDA BasicBlock) - insn (IDA insn_t) + args: + f (IDA func_t) + bb (IDA BasicBlock) + insn (IDA insn_t) """ for inst_handler in INSTRUCTION_HANDLERS: for (feature, ea) in inst_handler(f, bb, insn): diff --git a/capa/features/extractors/loops.py b/capa/features/extractors/loops.py index b8ef2816..97aa9271 100644 --- a/capa/features/extractors/loops.py +++ b/capa/features/extractors/loops.py @@ -11,14 +11,14 @@ from networkx.algorithms.components import strongly_connected_components def has_loop(edges, threshold=2): - """ check if a list of edges representing a directed graph contains a loop + """check if a list of edges representing a directed graph contains a loop - args: - edges: list of edge sets representing a directed graph i.e. [(1, 2), (2, 1)] - threshold: min number of nodes contained in loop + args: + edges: list of edge sets representing a directed graph i.e. [(1, 2), (2, 1)] + threshold: min number of nodes contained in loop - returns: - bool + returns: + bool """ g = nx.DiGraph() g.add_edges_from(edges) diff --git a/capa/features/freeze.py b/capa/features/freeze.py index bf4f0b66..0fbb0269 100644 --- a/capa/features/freeze.py +++ b/capa/features/freeze.py @@ -84,7 +84,16 @@ def dumps(extractor): returns: str: the serialized features. """ - ret = {"version": 1, "functions": {}, "scopes": {"file": [], "function": [], "basic block": [], "instruction": [],}} + ret = { + "version": 1, + "functions": {}, + "scopes": { + "file": [], + "function": [], + "basic block": [], + "instruction": [], + }, + } for feature, va in extractor.extract_file_features(): ret["scopes"]["file"].append(serialize_feature(feature) + (hex(va), ())) @@ -99,7 +108,16 @@ def dumps(extractor): ret["functions"][hex(f)][hex(bb)] = [] for feature, va in extractor.extract_basic_block_features(f, bb): - ret["scopes"]["basic block"].append(serialize_feature(feature) + (hex(va), (hex(f), hex(bb),))) + ret["scopes"]["basic block"].append( + serialize_feature(feature) + + ( + hex(va), + ( + hex(f), + hex(bb), + ), + ) + ) for insnva, insn in sorted( [(insn.__int__(), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0] @@ -108,7 +126,15 @@ def dumps(extractor): for feature, va in extractor.extract_insn_features(f, bb, insn): ret["scopes"]["instruction"].append( - serialize_feature(feature) + (hex(va), (hex(f), hex(bb), hex(insnva),)) + serialize_feature(feature) + + ( + hex(va), + ( + hex(f), + hex(bb), + hex(insnva), + ), + ) ) return json.dumps(ret) diff --git a/capa/ida/explorer/item.py b/capa/ida/explorer/item.py index 1ec49cbb..8cc9a1c5 100644 --- a/capa/ida/explorer/item.py +++ b/capa/ida/explorer/item.py @@ -17,9 +17,9 @@ import capa.ida.helpers def info_to_name(display): - """ extract root value from display name + """extract root value from display name - e.g. function(my_function) => my_function + e.g. function(my_function) => my_function """ try: return display.split("(")[1].rstrip(")") @@ -68,16 +68,16 @@ class CapaExplorerDataItem(object): return self._checked def appendChild(self, item): - """ add child item + """add child item - @param item: CapaExplorerDataItem* + @param item: CapaExplorerDataItem* """ self.children.append(item) def child(self, row): - """ get child row + """get child row - @param row: TODO + @param row: TODO """ return self.children[row] diff --git a/capa/ida/explorer/model.py b/capa/ida/explorer/model.py index 3949e9bb..ece810a0 100644 --- a/capa/ida/explorer/model.py +++ b/capa/ida/explorer/model.py @@ -65,11 +65,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): self.endResetModel() def columnCount(self, model_index): - """ get the number of columns for the children of the given parent + """get the number of columns for the children of the given parent - @param model_index: QModelIndex* + @param model_index: QModelIndex* - @retval column count + @retval column count """ if model_index.isValid(): return model_index.internalPointer().columnCount() @@ -77,12 +77,12 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return self.root_node.columnCount() def data(self, model_index, role): - """ get data stored under the given role for the item referred to by the index + """get data stored under the given role for the item referred to by the index - @param model_index: QModelIndex* - @param role: QtCore.Qt.* + @param model_index: QModelIndex* + @param role: QtCore.Qt.* - @retval data to be displayed + @retval data to be displayed """ if not model_index.isValid(): return None @@ -151,11 +151,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return None def flags(self, model_index): - """ get item flags for given index + """get item flags for given index - @param model_index: QModelIndex* + @param model_index: QModelIndex* - @retval QtCore.Qt.ItemFlags + @retval QtCore.Qt.ItemFlags """ if not model_index.isValid(): return QtCore.Qt.NoItemFlags @@ -163,13 +163,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return model_index.internalPointer().flags def headerData(self, section, orientation, role): - """ get data for the given role and section in the header with the specified orientation + """get data for the given role and section in the header with the specified orientation - @param section: int - @param orientation: QtCore.Qt.Orientation - @param role: QtCore.Qt.DisplayRole + @param section: int + @param orientation: QtCore.Qt.Orientation + @param role: QtCore.Qt.DisplayRole - @retval header data list() + @retval header data list() """ if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: return self.root_node.data(section) @@ -177,13 +177,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return None def index(self, row, column, parent): - """ get index of the item in the model specified by the given row, column and parent index + """get index of the item in the model specified by the given row, column and parent index - @param row: int - @param column: int - @param parent: QModelIndex* + @param row: int + @param column: int + @param parent: QModelIndex* - @retval QModelIndex* + @retval QModelIndex* """ if not self.hasIndex(row, column, parent): return QtCore.QModelIndex() @@ -201,13 +201,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return QtCore.QModelIndex() def parent(self, model_index): - """ get parent of the model item with the given index + """get parent of the model item with the given index - if the item has no parent, an invalid QModelIndex* is returned + if the item has no parent, an invalid QModelIndex* is returned - @param model_index: QModelIndex* + @param model_index: QModelIndex* - @retval QModelIndex* + @retval QModelIndex* """ if not model_index.isValid(): return QtCore.QModelIndex() @@ -221,12 +221,12 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return self.createIndex(parent.row(), 0, parent) def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True): - """ depth-first traversal of child nodes + """depth-first traversal of child nodes - @param model_index: QModelIndex* - @param ignore_root: if set, do not return root index + @param model_index: QModelIndex* + @param ignore_root: if set, do not return root index - @retval yield QModelIndex* + @retval yield QModelIndex* """ visited = set() stack = deque((model_index,)) @@ -248,10 +248,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): stack.append(child_index.child(idx, 0)) def reset_ida_highlighting(self, item, checked): - """ reset IDA highlight for an item + """reset IDA highlight for an item - @param item: capa explorer item - @param checked: indicates item is or not checked + @param item: capa explorer item + @param checked: indicates item is or not checked """ if not isinstance( item, (CapaExplorerStringViewItem, CapaExplorerInstructionViewItem, CapaExplorerByteViewItem) @@ -275,13 +275,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): idc.set_color(item.location, idc.CIC_ITEM, item.ida_highlight) def setData(self, model_index, value, role): - """ set the role data for the item at index to value + """set the role data for the item at index to value - @param model_index: QModelIndex* - @param value: QVariant* - @param role: QtCore.Qt.EditRole + @param model_index: QModelIndex* + @param value: QVariant* + @param role: QtCore.Qt.EditRole - @retval True/False + @retval True/False """ if not model_index.isValid(): return False @@ -316,14 +316,14 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return False def rowCount(self, model_index): - """ get the number of rows under the given parent + """get the number of rows under the given parent - when the parent is valid it means that is returning the number of - children of parent + when the parent is valid it means that is returning the number of + children of parent - @param model_index: QModelIndex* + @param model_index: QModelIndex* - @retval row count + @retval row count """ if model_index.column() > 0: return 0 @@ -336,16 +336,16 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return item.childCount() def render_capa_doc_statement_node(self, parent, statement, locations, doc): - """ render capa statement read from doc + """render capa statement read from doc - @param parent: parent to which new child is assigned - @param statement: statement read from doc - @param locations: locations of children (applies to range only?) - @param doc: capa result doc + @param parent: parent to which new child is assigned + @param statement: statement read from doc + @param locations: locations of children (applies to range only?) + @param doc: capa result doc - "statement": { - "type": "or" - }, + "statement": { + "type": "or" + }, """ if statement["type"] in ("and", "or", "optional"): return CapaExplorerDefaultItem(parent, statement["type"]) @@ -383,28 +383,28 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): raise RuntimeError("unexpected match statement type: " + str(statement)) def render_capa_doc_match(self, parent, match, doc): - """ render capa match read from doc + """render capa match read from doc - @param parent: parent node to which new child is assigned - @param match: match read from doc - @param doc: capa result doc + @param parent: parent node to which new child is assigned + @param match: match read from doc + @param doc: capa result doc - "matches": { - "0": { - "children": [], - "locations": [ - 4317184 - ], - "node": { - "feature": { - "section": ".rsrc", - "type": "section" - }, - "type": "feature" + "matches": { + "0": { + "children": [], + "locations": [ + 4317184 + ], + "node": { + "feature": { + "section": ".rsrc", + "type": "section" }, - "success": true - } - }, + "type": "feature" + }, + "success": true + } + }, """ if not match["success"]: # TODO: display failed branches at some point? Help with debugging rules? @@ -431,9 +431,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): self.render_capa_doc_match(parent2, child, doc) def render_capa_doc(self, doc): - """ render capa features specified in doc + """render capa features specified in doc - @param doc: capa result doc + @param doc: capa result doc """ # inform model that changes are about to occur self.beginResetModel() @@ -457,18 +457,18 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): self.endResetModel() def capa_doc_feature_to_display(self, feature): - """ convert capa doc feature type string to display string for ui + """convert capa doc feature type string to display string for ui - @param feature: capa feature read from doc + @param feature: capa feature read from doc - Example: - "feature": { - "bytes": "01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46", - "description": "CLSID_ShellLink", - "type": "bytes" - } + Example: + "feature": { + "bytes": "01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46", + "description": "CLSID_ShellLink", + "type": "bytes" + } - bytes(01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_ShellLink) + bytes(01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_ShellLink) """ if feature[feature["type"]]: if feature.get("description", ""): @@ -479,25 +479,31 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return "%s" % feature["type"] def render_capa_doc_feature_node(self, parent, feature, locations, doc): - """ process capa doc feature node + """process capa doc feature node - @param parent: parent node to which child is assigned - @param feature: capa doc feature node - @param locations: locations identified for feature - @param doc: capa doc + @param parent: parent node to which child is assigned + @param feature: capa doc feature node + @param locations: locations identified for feature + @param doc: capa doc - Example: - "feature": { - "description": "FILE_WRITE_DATA", - "number": "0x2", - "type": "number" - } + Example: + "feature": { + "description": "FILE_WRITE_DATA", + "number": "0x2", + "type": "number" + } """ display = self.capa_doc_feature_to_display(feature) if len(locations) == 1: # only one location for feature so no need to nest children - parent2 = self.render_capa_doc_feature(parent, feature, next(iter(locations)), doc, display=display,) + parent2 = self.render_capa_doc_feature( + parent, + feature, + next(iter(locations)), + doc, + display=display, + ) else: # feature has multiple children, nest under one parent feature node parent2 = CapaExplorerFeatureItem(parent, display) @@ -508,20 +514,20 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): return parent2 def render_capa_doc_feature(self, parent, feature, location, doc, display="-"): - """ render capa feature read from doc + """render capa feature read from doc - @param parent: parent node to which new child is assigned - @param feature: feature read from doc - @param doc: capa feature doc - @param location: address of feature - @param display: text to display in plugin ui + @param parent: parent node to which new child is assigned + @param feature: feature read from doc + @param doc: capa feature doc + @param location: address of feature + @param display: text to display in plugin ui - Example: - "feature": { - "description": "FILE_WRITE_DATA", - "number": "0x2", - "type": "number" - } + Example: + "feature": { + "description": "FILE_WRITE_DATA", + "number": "0x2", + "type": "number" + } """ # special handling for characteristic pending type if feature["type"] == "characteristic": @@ -575,10 +581,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): raise RuntimeError("unexpected feature type: " + str(feature["type"])) def update_function_name(self, old_name, new_name): - """ update all instances of old function name with new function name + """update all instances of old function name with new function name - @param old_name: previous function name - @param new_name: new function name + @param old_name: previous function name + @param new_name: new function name """ # create empty root index for search root_index = self.index(0, 0, QtCore.QModelIndex()) diff --git a/capa/ida/explorer/proxy.py b/capa/ida/explorer/proxy.py index a4978cb5..eb7ca74e 100644 --- a/capa/ida/explorer/proxy.py +++ b/capa/ida/explorer/proxy.py @@ -17,12 +17,12 @@ class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel): super(CapaExplorerSortFilterProxyModel, self).__init__(parent) def lessThan(self, left, right): - """ true if the value of the left item is less than value of right item + """true if the value of the left item is less than value of right item - @param left: QModelIndex* - @param right: QModelIndex* + @param left: QModelIndex* + @param right: QModelIndex* - @retval True/False + @retval True/False """ ldata = left.internalPointer().data(left.column()) rdata = right.internalPointer().data(right.column()) @@ -40,13 +40,13 @@ class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel): return ldata.lower() < rdata.lower() def filterAcceptsRow(self, row, parent): - """ true if the item in the row indicated by the given row and parent - should be included in the model; otherwise returns false + """true if the item in the row indicated by the given row and parent + should be included in the model; otherwise returns false - @param row: int - @param parent: QModelIndex* + @param row: int + @param parent: QModelIndex* - @retval True/False + @retval True/False """ if self.filter_accepts_row_self(row, parent): return True @@ -63,10 +63,10 @@ class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel): return False def add_single_string_filter(self, column, string): - """ add fixed string filter + """add fixed string filter - @param column: key column - @param string: string to sort + @param column: key column + @param string: string to sort """ self.setFilterKeyColumn(column) self.setFilterFixedString(string) diff --git a/capa/ida/explorer/view.py b/capa/ida/explorer/view.py index 1ec1fa8c..77a2a5c1 100644 --- a/capa/ida/explorer/view.py +++ b/capa/ida/explorer/view.py @@ -15,13 +15,13 @@ from capa.ida.explorer.model import CapaExplorerDataModel class CapaExplorerQtreeView(QtWidgets.QTreeView): - """ capa explorer QTreeView implementation + """capa explorer QTreeView implementation - view controls UI action responses and displays data from - CapaExplorerDataModel + view controls UI action responses and displays data from + CapaExplorerDataModel - view does not modify CapaExplorerDataModel directly - data - modifications should be implemented in CapaExplorerDataModel + view does not modify CapaExplorerDataModel directly - data + modifications should be implemented in CapaExplorerDataModel """ def __init__(self, model, parent=None): @@ -54,10 +54,10 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}") def reset(self): - """ reset user interface changes + """reset user interface changes - called when view should reset any user interface changes - made since the last reset e.g. IDA window highlighting + called when view should reset any user interface changes + made since the last reset e.g. IDA window highlighting """ self.collapseAll() self.resize_columns_to_content() @@ -67,31 +67,31 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents) def map_index_to_source_item(self, model_index): - """ map proxy model index to source model item + """map proxy model index to source model item - @param model_index: QModelIndex* + @param model_index: QModelIndex* - @retval QObject* + @retval QObject* """ return self.model.mapToSource(model_index).internalPointer() def send_data_to_clipboard(self, data): - """ copy data to the clipboard + """copy data to the clipboard - @param data: data to be copied + @param data: data to be copied """ clip = QtWidgets.QApplication.clipboard() clip.clear(mode=clip.Clipboard) clip.setText(data, mode=clip.Clipboard) def new_action(self, display, data, slot): - """ create action for context menu + """create action for context menu - @param display: text displayed to user in context menu - @param data: data passed to slot - @param slot: slot to connect + @param display: text displayed to user in context menu + @param data: data passed to slot + @param slot: slot to connect - @retval QAction* + @retval QAction* """ action = QtWidgets.QAction(display, self.parent) action.setData(data) @@ -100,11 +100,11 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): return action def load_default_context_menu_actions(self, data): - """ yield actions specific to function custom context menu + """yield actions specific to function custom context menu - @param data: tuple + @param data: tuple - @yield QAction* + @yield QAction* """ default_actions = ( ("Copy column", data, self.slot_copy_column), @@ -116,11 +116,11 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): yield self.new_action(*action) def load_function_context_menu_actions(self, data): - """ yield actions specific to function custom context menu + """yield actions specific to function custom context menu - @param data: tuple + @param data: tuple - @yield QAction* + @yield QAction* """ function_actions = (("Rename function", data, self.slot_rename_function),) @@ -133,15 +133,15 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): yield action def load_default_context_menu(self, pos, item, model_index): - """ create default custom context menu + """create default custom context menu - creates custom context menu containing default actions + creates custom context menu containing default actions - @param pos: TODO - @param item: TODO - @param model_index: TODO + @param pos: TODO + @param item: TODO + @param model_index: TODO - @retval QMenu* + @retval QMenu* """ menu = QtWidgets.QMenu() @@ -151,16 +151,16 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): return menu def load_function_item_context_menu(self, pos, item, model_index): - """ create function custom context menu + """create function custom context menu - creates custom context menu containing actions specific to functions - and the default actions + creates custom context menu containing actions specific to functions + and the default actions - @param pos: TODO - @param item: TODO - @param model_index: TODO + @param pos: TODO + @param item: TODO + @param model_index: TODO - @retval QMenu* + @retval QMenu* """ menu = QtWidgets.QMenu() @@ -170,43 +170,43 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): return menu def show_custom_context_menu(self, menu, pos): - """ display custom context menu in view + """display custom context menu in view - @param menu: TODO - @param pos: TODO + @param menu: TODO + @param pos: TODO """ if menu: menu.exec_(self.viewport().mapToGlobal(pos)) def slot_copy_column(self, action): - """ slot connected to custom context menu + """slot connected to custom context menu - allows user to select a column and copy the data - to clipboard + allows user to select a column and copy the data + to clipboard - @param action: QAction* + @param action: QAction* """ _, item, model_index = action.data() self.send_data_to_clipboard(item.data(model_index.column())) def slot_copy_row(self, action): - """ slot connected to custom context menu + """slot connected to custom context menu - allows user to select a row and copy the space-delimited - data to clipboard + allows user to select a row and copy the space-delimited + data to clipboard - @param action: QAction* + @param action: QAction* """ _, item, _ = action.data() self.send_data_to_clipboard(str(item)) def slot_rename_function(self, action): - """ slot connected to custom context menu + """slot connected to custom context menu - allows user to select a edit a function name and push - changes to IDA + allows user to select a edit a function name and push + changes to IDA - @param action: QAction* + @param action: QAction* """ _, item, model_index = action.data() @@ -216,12 +216,12 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): item.setIsEditable(False) def slot_custom_context_menu_requested(self, pos): - """ slot connected to custom context menu request + """slot connected to custom context menu request - displays custom context menu to user containing action - relevant to the data item selected + displays custom context menu to user containing action + relevant to the data item selected - @param pos: TODO + @param pos: TODO """ model_index = self.indexAt(pos) @@ -243,9 +243,9 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView): self.show_custom_context_menu(menu, pos) def slot_double_click(self, model_index): - """ slot connected to double click event + """slot connected to double click event - @param model_index: QModelIndex* + @param model_index: QModelIndex* """ if not model_index.isValid(): return diff --git a/capa/ida/helpers/__init__.py b/capa/ida/helpers/__init__.py index 619acdfe..d035c7b2 100644 --- a/capa/ida/helpers/__init__.py +++ b/capa/ida/helpers/__init__.py @@ -102,6 +102,9 @@ def collect_metadata(): "sha256": sha256, "path": idaapi.get_input_file_path(), }, - "analysis": {"format": idaapi.get_file_type_name(), "extractor": "ida",}, + "analysis": { + "format": idaapi.get_file_type_name(), + "extractor": "ida", + }, "version": capa.version.__version__, } diff --git a/capa/ida/ida_capa_explorer.py b/capa/ida/ida_capa_explorer.py index 5c85884d..e80dd84d 100644 --- a/capa/ida/ida_capa_explorer.py +++ b/capa/ida/ida_capa_explorer.py @@ -30,10 +30,10 @@ logger = logging.getLogger("capa") class CapaExplorerIdaHooks(idaapi.UI_Hooks): def __init__(self, screen_ea_changed_hook, action_hooks): - """ facilitate IDA UI hooks + """facilitate IDA UI hooks - @param screen_ea_changed_hook: function hook for IDA screen ea changed - @param action_hooks: dict of IDA action handles + @param screen_ea_changed_hook: function hook for IDA screen ea changed + @param action_hooks: dict of IDA action handles """ super(CapaExplorerIdaHooks, self).__init__() @@ -43,11 +43,11 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks): self.process_action_meta = {} def preprocess_action(self, name): - """ called prior to action completed + """called prior to action completed - @param name: name of action defined by idagui.cfg + @param name: name of action defined by idagui.cfg - @retval must be 0 + @retval must be 0 """ self.process_action_handle = self.process_action_hooks.get(name, None) @@ -66,10 +66,10 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks): self.reset() def screen_ea_changed(self, curr_ea, prev_ea): - """ called after screen location is changed + """called after screen location is changed - @param curr_ea: current location - @param prev_ea: prev location + @param curr_ea: current location + @param prev_ea: prev location """ self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea) @@ -300,13 +300,13 @@ class CapaExplorerForm(idaapi.PluginForm): self.ida_hooks.unhook() def ida_hook_rename(self, meta, post=False): - """ hook for IDA rename action + """hook for IDA rename action - called twice, once before action and once after - action completes + called twice, once before action and once after + action completes - @param meta: metadata cache - @param post: indicates pre or post action + @param meta: metadata cache + @param post: indicates pre or post action """ location = idaapi.get_screen_ea() if not location or not capa.ida.helpers.is_func_start(location): @@ -322,12 +322,12 @@ class CapaExplorerForm(idaapi.PluginForm): meta["prev_name"] = curr_name def ida_hook_screen_ea_changed(self, widget, new_ea, old_ea): - """ hook for IDA screen ea changed + """hook for IDA screen ea changed - @param widget: IDA widget type - @param new_ea: destination ea - @param old_ea: source ea - """ + @param widget: IDA widget type + @param new_ea: destination ea + @param old_ea: source ea + """ if not self.view_limit_results_by_function.isChecked(): # ignore if checkbox not selected return @@ -508,9 +508,9 @@ class CapaExplorerForm(idaapi.PluginForm): idaapi.info("%s reload completed." % PLUGIN_NAME) def reset(self): - """ reset UI elements + """reset UI elements - e.g. checkboxes and IDA highlighting + e.g. checkboxes and IDA highlighting """ self.ida_reset() @@ -518,21 +518,21 @@ class CapaExplorerForm(idaapi.PluginForm): idaapi.info("%s reset completed." % PLUGIN_NAME) def slot_menu_bar_hovered(self, action): - """ display menu action tooltip + """display menu action tooltip - @param action: QtWidgets.QAction* + @param action: QtWidgets.QAction* - @reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu + @reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu """ QtWidgets.QToolTip.showText( QtGui.QCursor.pos(), action.toolTip(), self.view_menu_bar, self.view_menu_bar.actionGeometry(action) ) def slot_checkbox_limit_by_changed(self): - """ slot activated if checkbox clicked + """slot activated if checkbox clicked - if checked, configure function filter if screen location is located - in function, otherwise clear filter + if checked, configure function filter if screen location is located + in function, otherwise clear filter """ match = "" if self.view_limit_results_by_function.isChecked(): diff --git a/capa/main.py b/capa/main.py index a779c9a6..910e3f31 100644 --- a/capa/main.py +++ b/capa/main.py @@ -105,7 +105,12 @@ def find_capabilities(ruleset, extractor, disable_progress=None): all_function_matches = collections.defaultdict(list) all_bb_matches = collections.defaultdict(list) - meta = {"feature_counts": {"file": 0, "functions": {},}} + meta = { + "feature_counts": { + "file": 0, + "functions": {}, + } + } for f in tqdm.tqdm(list(extractor.get_functions()), disable=disable_progress, desc="matching", unit=" functions"): function_matches, bb_matches, feature_count = find_function_capabilities(ruleset, extractor, f) diff --git a/capa/render/__init__.py b/capa/render/__init__.py index ceb79d5a..9de20bd5 100644 --- a/capa/render/__init__.py +++ b/capa/render/__init__.py @@ -16,15 +16,15 @@ import capa.engine def convert_statement_to_result_document(statement): """ - "statement": { - "type": "or" - }, + "statement": { + "type": "or" + }, - "statement": { - "max": 9223372036854775808, - "min": 2, - "type": "range" - }, + "statement": { + "max": 9223372036854775808, + "min": 2, + "type": "range" + }, """ statement_type = statement.name.lower() result = {"type": statement_type} @@ -47,28 +47,28 @@ def convert_statement_to_result_document(statement): def convert_feature_to_result_document(feature): """ - "feature": { - "number": 6, - "type": "number" - }, + "feature": { + "number": 6, + "type": "number" + }, - "feature": { - "api": "ws2_32.WSASocket", - "type": "api" - }, + "feature": { + "api": "ws2_32.WSASocket", + "type": "api" + }, - "feature": { - "match": "create TCP socket", - "type": "match" - }, + "feature": { + "match": "create TCP socket", + "type": "match" + }, - "feature": { - "characteristic": [ - "loop", - true - ], - "type": "characteristic" - }, + "feature": { + "characteristic": [ + "loop", + true + ], + "type": "characteristic" + }, """ result = {"type": feature.name, feature.name: feature.get_value_str()} if feature.description: @@ -80,15 +80,15 @@ def convert_feature_to_result_document(feature): def convert_node_to_result_document(node): """ - "node": { - "type": "statement", - "statement": { ... } - }, + "node": { + "type": "statement", + "statement": { ... } + }, - "node": { - "type": "feature", - "feature": { ... } - }, + "node": { + "type": "feature", + "feature": { ... } + }, """ if isinstance(node, capa.engine.Statement): @@ -152,7 +152,10 @@ def convert_match_to_result_document(rules, capabilities, result): scope = rule.meta["scope"] doc["node"] = { "type": "statement", - "statement": {"type": "subscope", "subscope": scope,}, + "statement": { + "type": "subscope", + "subscope": scope, + }, } for location in doc["locations"]: @@ -257,5 +260,7 @@ class CapaJsonObjectEncoder(json.JSONEncoder): def render_json(meta, rules, capabilities): return json.dumps( - convert_capabilities_to_result_document(meta, rules, capabilities), cls=CapaJsonObjectEncoder, sort_keys=True, + convert_capabilities_to_result_document(meta, rules, capabilities), + cls=CapaJsonObjectEncoder, + sort_keys=True, ) diff --git a/capa/render/default.py b/capa/render/default.py index 93c5c145..10379a93 100644 --- a/capa/render/default.py +++ b/capa/render/default.py @@ -109,7 +109,12 @@ def render_attack(doc, ostream): inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id)) else: raise RuntimeError("unexpected ATT&CK spec format") - rows.append((rutils.bold(tactic.upper()), "\n".join(inner_rows),)) + rows.append( + ( + rutils.bold(tactic.upper()), + "\n".join(inner_rows), + ) + ) if rows: ostream.write( diff --git a/scripts/lint.py b/scripts/lint.py index 862b3382..4764c49a 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -399,7 +399,11 @@ def lint_rule(ctx, rule): print("") print( "%s%s %s" - % (" (nursery) " if is_nursery_rule(rule) else "", rule.name, ("(%s)" % category) if category else "",) + % ( + " (nursery) " if is_nursery_rule(rule) else "", + rule.name, + ("(%s)" % category) if category else "", + ) ) level = "WARN" if is_nursery_rule(rule) else "FAIL" @@ -407,7 +411,12 @@ def lint_rule(ctx, rule): for violation in violations: print( "%s %s: %s: %s" - % (" " if is_nursery_rule(rule) else "", level, violation.name, violation.recommendation,) + % ( + " " if is_nursery_rule(rule) else "", + level, + violation.name, + violation.recommendation, + ) ) elif len(violations) == 0 and is_nursery_rule(rule): @@ -487,7 +496,9 @@ def main(argv=None): parser.add_argument("rules", type=str, help="Path to rules") parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples") parser.add_argument( - "--thorough", action="store_true", help="Enable thorough linting - takes more time, but does a better job", + "--thorough", + action="store_true", + help="Enable thorough linting - takes more time, but does a better job", ) parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging") parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors") diff --git a/scripts/show-capabilities-by-function.py b/scripts/show-capabilities-by-function.py index 5d60cbca..ada896e7 100644 --- a/scripts/show-capabilities-by-function.py +++ b/scripts/show-capabilities-by-function.py @@ -71,22 +71,22 @@ logger = logging.getLogger("capa.show-capabilities-by-function") def render_matches_by_function(doc): """ - like: + like: - function at 0x1000321a with 33 features: - - get hostname - - initialize Winsock library - function at 0x10003286 with 63 features: - - create thread - - terminate thread - function at 0x10003415 with 116 features: - - write file - - send data - - link function at runtime - - create HTTP request - - get common file path - - send HTTP request - - connect to HTTP server + function at 0x1000321a with 33 features: + - get hostname + - initialize Winsock library + function at 0x10003286 with 63 features: + - create thread + - terminate thread + function at 0x10003415 with 116 features: + - write file + - send data + - link function at runtime + - create HTTP request + - get common file path + - send HTTP request + - connect to HTTP server """ ostream = rutils.StringIO() diff --git a/setup.py b/setup.py index 3840ad1c..f3467562 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,11 @@ setuptools.setup( url="https://www.github.com/fireeye/capa", packages=setuptools.find_packages(exclude=["tests"]), package_dir={"capa": "capa"}, - entry_points={"console_scripts": ["capa=capa.main:main",]}, + entry_points={ + "console_scripts": [ + "capa=capa.main:main", + ] + }, include_package_data=True, install_requires=requirements, extras_require={ diff --git a/tests/fixtures.py b/tests/fixtures.py index d663ba5e..f5eb6e53 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -340,10 +340,20 @@ FEATURE_PRESENCE_TESTS = [ ("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), True), ("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True), # insn/api: x64 - ("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True,), + ( + "kernel32-64", + "function=0x180001010", + capa.features.insn.API("RtlVirtualUnwind"), + True, + ), ("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True), # insn/api: x64 thunk - ("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True,), + ( + "kernel32-64", + "function=0x1800202B0", + capa.features.insn.API("RtlCaptureContext"), + True, + ), ("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True), # insn/api: resolve indirect calls ("c91887...", "function=0x401A77", capa.features.insn.API("kernel32.CreatePipe"), True), diff --git a/tests/test_engine.py b/tests/test_engine.py index 0f868149..a04ddad3 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -59,7 +59,13 @@ def test_some(): ) assert ( Some(2, [Number(1), Number(2), Number(3)]).evaluate( - {Number(0): {1}, Number(1): {1}, Number(2): {1}, Number(3): {1}, Number(4): {1},} + { + Number(0): {1}, + Number(1): {1}, + Number(2): {1}, + Number(3): {1}, + Number(4): {1}, + } ) == True ) @@ -258,7 +264,9 @@ def test_match_matched_rules(): ] features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.insn.Number(100): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.insn.Number(100): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule1") in features assert capa.features.MatchedRule("test rule2") in features @@ -266,7 +274,9 @@ def test_match_matched_rules(): # the ordering of the rules must not matter, # the engine should match rules in an appropriate order. features, matches = capa.engine.match( - capa.engine.topologically_order_rules(reversed(rules)), {capa.features.insn.Number(100): {1}}, 0x0, + capa.engine.topologically_order_rules(reversed(rules)), + {capa.features.insn.Number(100): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule1") in features assert capa.features.MatchedRule("test rule2") in features @@ -312,22 +322,30 @@ def test_regex(): ), ] features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.insn.Number(100): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.insn.Number(100): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") not in features features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("aaaa"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("aaaa"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") not in features features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("aBBBBa"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("aBBBBa"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") not in features features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("abbbba"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("abbbba"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") in features assert capa.features.MatchedRule("rule with implied wildcards") in features @@ -350,7 +368,9 @@ def test_regex_ignorecase(): ), ] features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("aBBBBa"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("aBBBBa"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") in features @@ -429,7 +449,9 @@ def test_match_namespace(): ] features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.insn.API("CreateFile"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.insn.API("CreateFile"): {1}}, + 0x0, ) assert "CreateFile API" in matches assert "file-create" in matches @@ -439,7 +461,9 @@ def test_match_namespace(): assert capa.features.MatchedRule("file/create/CreateFile") in features features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.insn.API("WriteFile"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.insn.API("WriteFile"): {1}}, + 0x0, ) assert "WriteFile API" in matches assert "file-create" not in matches diff --git a/tests/test_freeze.py b/tests/test_freeze.py index 9138ef53..11c5b956 100644 --- a/tests/test_freeze.py +++ b/tests/test_freeze.py @@ -21,13 +21,19 @@ import capa.features.extractors EXTRACTOR = capa.features.extractors.NullFeatureExtractor( { "base address": 0x401000, - "file features": [(0x402345, capa.features.Characteristic("embedded pe")),], + "file features": [ + (0x402345, capa.features.Characteristic("embedded pe")), + ], "functions": { 0x401000: { - "features": [(0x401000, capa.features.Characteristic("indirect call")),], + "features": [ + (0x401000, capa.features.Characteristic("indirect call")), + ], "basic blocks": { 0x401000: { - "features": [(0x401000, capa.features.Characteristic("tight loop")),], + "features": [ + (0x401000, capa.features.Characteristic("tight loop")), + ], "instructions": { 0x401000: { "features": [ @@ -35,7 +41,11 @@ EXTRACTOR = capa.features.extractors.NullFeatureExtractor( (0x401000, capa.features.Characteristic("nzxor")), ], }, - 0x401002: {"features": [(0x401002, capa.features.insn.Mnemonic("mov")),],}, + 0x401002: { + "features": [ + (0x401002, capa.features.insn.Mnemonic("mov")), + ], + }, }, }, }, diff --git a/tests/test_main.py b/tests/test_main.py index 2d9ac32d..4c92c7f0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,7 +44,17 @@ def test_main_single_rule(z9324d_extractor, tmpdir): path = z9324d_extractor.path rule_file = tmpdir.mkdir("capa").join("rule.yml") rule_file.write(RULE_CONTENT) - assert capa.main.main([path, "-v", "-r", rule_file.strpath,]) == 0 + assert ( + capa.main.main( + [ + path, + "-v", + "-r", + rule_file.strpath, + ] + ) + == 0 + ) @pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2") diff --git a/tests/test_rules.py b/tests/test_rules.py index ccc81425..34d64b3f 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -680,12 +680,16 @@ def test_regex_values_always_string(): ), ] features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("123"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("123"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") in features features, matches = capa.engine.match( - capa.engine.topologically_order_rules(rules), {capa.features.String("0x123"): {1}}, 0x0, + capa.engine.topologically_order_rules(rules), + {capa.features.String("0x123"): {1}}, + 0x0, ) assert capa.features.MatchedRule("test rule") in features diff --git a/tests/test_viv_features.py b/tests/test_viv_features.py index e24687e9..0922d758 100644 --- a/tests/test_viv_features.py +++ b/tests/test_viv_features.py @@ -11,7 +11,9 @@ from fixtures import * @parametrize( - "sample,scope,feature,expected", FEATURE_PRESENCE_TESTS, indirect=["sample", "scope"], + "sample,scope,feature,expected", + FEATURE_PRESENCE_TESTS, + indirect=["sample", "scope"], ) def test_viv_features(sample, scope, feature, expected): with xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2"): @@ -19,7 +21,9 @@ def test_viv_features(sample, scope, feature, expected): @parametrize( - "sample,scope,feature,expected", FEATURE_COUNT_TESTS, indirect=["sample", "scope"], + "sample,scope,feature,expected", + FEATURE_COUNT_TESTS, + indirect=["sample", "scope"], ) def test_viv_feature_counts(sample, scope, feature, expected): with xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2"):