def update_rule_status(self, rule_text): """ """ if self.view_rulegen_editor.root is None: self.set_rulegen_preview_border_neutral() self.view_rulegen_status_label.clear() return self.set_rulegen_preview_border_error() try: rule = capa.rules.Rule.from_yaml(rule_text) except Exception as e: self.set_rulegen_status("Failed to compile rule (%s)" % e) return # create deep copy of current rules, add our new rule rules = copy.copy(self.rules_cache) rules.append(rule) try: file_features = copy.copy(self.rulegen_file_features_cache) if self.rulegen_current_function: func_matches, bb_matches = find_func_matches( self.rulegen_current_function, capa.rules.RuleSet(list(capa.rules.get_rules_and_dependencies(rules, rule.name))), self.rulegen_func_features_cache, self.rulegen_bb_features_cache, ) file_features.update(copy.copy(self.rulegen_func_features_cache)) else: func_matches = {} bb_matches = {} _, file_matches = capa.engine.match( capa.rules.RuleSet(list(capa.rules.get_rules_and_dependencies(rules, rule.name))).file_rules, file_features, 0x0, ) except Exception as e: self.set_rulegen_status("Failed to match rule (%s)" % e) return if tuple( filter( lambda m: m[0] == rule.name, itertools.chain(file_matches.items(), func_matches.items(), bb_matches.items()), ) ): # made it here, rule compiled and match was found self.set_rulegen_preview_border_success() self.set_rulegen_status("Rule compiled and matched") else: # made it here, rule compiled but no match found, may be intended so we warn user self.set_rulegen_preview_border_warn() self.set_rulegen_status("Rule compiled, but not matched")
def get_rules(rule_path, disable_progress=False): if not os.path.exists(rule_path): raise IOError("rule path %s does not exist or cannot be accessed" % rule_path) rule_paths = [] if os.path.isfile(rule_path): rule_paths.append(rule_path) elif os.path.isdir(rule_path): logger.debug("reading rules from directory %s", rule_path) for root, dirs, files in os.walk(rule_path): if ".github" in root: # the .github directory contains CI config in capa-rules # this includes some .yml files # these are not rules continue for file in files: if not file.endswith(".yml"): if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): # expect to see readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue rule_path = os.path.join(root, file) rule_paths.append(rule_path) rules = [] pbar = tqdm.tqdm if disable_progress: # do not use tqdm to avoid unnecessary side effects when caller intends # to disable progress completely pbar = lambda s, *args, **kwargs: s for rule_path in pbar(list(rule_paths), desc="loading ", unit=" rules"): try: rule = capa.rules.Rule.from_yaml_file(rule_path) except capa.rules.InvalidRule: raise else: rule.meta["capa/path"] = rule_path if is_nursery_rule_path(rule_path): rule.meta["capa/nursery"] = True rules.append(rule) logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scope) return rules
def get_rules(rule_path): if not os.path.exists(rule_path): raise IOError("rule path %s does not exist or cannot be accessed" % rule_path) rule_paths = [] if os.path.isfile(rule_path): rule_paths.append(rule_path) elif os.path.isdir(rule_path): logger.debug("reading rules from directory %s", rule_path) for root, dirs, files in os.walk(rule_path): if ".github" in root: # the .github directory contains CI config in capa-rules # this includes some .yml files # these are not rules continue for file in files: if not file.endswith(".yml"): if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): # expect to see readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue rule_path = os.path.join(root, file) rule_paths.append(rule_path) rules = [] for rule_path in rule_paths: try: rule = capa.rules.Rule.from_yaml_file(rule_path) except capa.rules.InvalidRule: raise else: rule.meta["capa/path"] = rule_path if is_nursery_rule_path(rule_path): rule.meta["capa/nursery"] = True rules.append(rule) logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scope) return rules
def _extract_subscope_rules(rules): """ process the given sequence of rules. for each one, extract any embedded subscope rules into their own rule. process these recursively. then return a list of the refactored rules. note: this operation mutates the rules passed in - they may now have `match` statements for the extracted subscope rules. """ done = [] # use a queue of rules, because we'll be modifying the list (appending new items) as we go. while rules: rule = rules.pop(0) for subscope_rule in rule.extract_subscope_rules(): rules.append(subscope_rule) done.append(rule) return done
def load_capa_rules(self): """ """ self.ruleset_cache = None self.rules_cache = None try: # resolve rules directory - check self and settings first, then ask user if not os.path.exists( settings.user.get(CAPA_SETTINGS_RULE_PATH, "")): idaapi.info( "Please select a file directory containing capa rules.") path = self.ask_user_directory() if not path: logger.warning( "You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules." ) return False settings.user[CAPA_SETTINGS_RULE_PATH] = path except Exception as e: logger.error("Failed to load capa rules (error: %s).", e) return False if ida_kernwin.user_cancelled(): logger.info("User cancelled analysis.") return False rule_path = settings.user[CAPA_SETTINGS_RULE_PATH] try: # TODO refactor: this first part is identical to capa.main.get_rules if not os.path.exists(rule_path): raise IOError( "rule path %s does not exist or cannot be accessed" % rule_path) rule_paths = [] if os.path.isfile(rule_path): rule_paths.append(rule_path) elif os.path.isdir(rule_path): for root, dirs, files in os.walk(rule_path): if ".github" in root: # the .github directory contains CI config in capa-rules # this includes some .yml files # these are not rules continue for file in files: if not file.endswith(".yml"): if not (file.startswith(".git") or file.endswith( (".git", ".md", ".txt"))): # expect to see .git* files, readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue rule_path = os.path.join(root, file) rule_paths.append(rule_path) rules = [] total_paths = len(rule_paths) for (i, rule_path) in enumerate(rule_paths): update_wait_box("loading capa rules from %s (%d of %d)" % (settings.user[CAPA_SETTINGS_RULE_PATH], i + 1, total_paths)) if ida_kernwin.user_cancelled(): raise UserCancelledError("user cancelled") try: rule = capa.rules.Rule.from_yaml_file(rule_path) except capa.rules.InvalidRule: raise else: rule.meta["capa/path"] = rule_path if capa.main.is_nursery_rule_path(rule_path): rule.meta["capa/nursery"] = True rules.append(rule) _rules = copy.copy(rules) ruleset = capa.rules.RuleSet(_rules) except UserCancelledError: logger.info("User cancelled analysis.") return False except Exception as e: capa.ida.helpers.inform_user_ida_ui( "Failed to load capa rules from %s" % settings.user[CAPA_SETTINGS_RULE_PATH]) logger.error("Failed to load rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e) logger.error( "Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules." ) settings.user[CAPA_SETTINGS_RULE_PATH] = "" return False self.ruleset_cache = ruleset self.rules_cache = rules return True
def load_capa_results(self): """run capa analysis and render results in UI note: this function must always return, exception or not, in order for plugin to safely close the IDA wait box """ # new analysis, new doc self.doc = None self.process_total = 0 self.process_count = 1 def update_wait_box(text): """update the IDA wait box""" ida_kernwin.replace_wait_box("capa explorer...%s" % text) def slot_progress_feature_extraction(text): """slot function to handle feature extraction progress updates""" update_wait_box("%s (%d of %d)" % (text, self.process_count, self.process_total)) self.process_count += 1 extractor = CapaExplorerFeatureExtractor() extractor.indicator.progress.connect(slot_progress_feature_extraction) update_wait_box("calculating analysis") try: self.process_total += len(tuple(extractor.get_functions())) except Exception as e: logger.error("Failed to calculate analysis (error: %s).", e) return False if ida_kernwin.user_cancelled(): logger.info("User cancelled analysis.") return False update_wait_box("loading rules") try: # resolve rules directory - check self and settings first, then ask user if not self.rule_path: if "rule_path" in settings and os.path.exists( settings["rule_path"]): self.rule_path = settings["rule_path"] else: idaapi.info( "Please select a file directory containing capa rules." ) rule_path = self.ask_user_directory() if not rule_path: logger.warning( "You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules." ) return False self.rule_path = rule_path settings.user["rule_path"] = rule_path except Exception as e: logger.error("Failed to load capa rules (error: %s).", e) return False if ida_kernwin.user_cancelled(): logger.info("User cancelled analysis.") return False rule_path = self.rule_path try: if not os.path.exists(rule_path): raise IOError( "rule path %s does not exist or cannot be accessed" % rule_path) rule_paths = [] if os.path.isfile(rule_path): rule_paths.append(rule_path) elif os.path.isdir(rule_path): for root, dirs, files in os.walk(rule_path): if ".github" in root: # the .github directory contains CI config in capa-rules # this includes some .yml files # these are not rules continue for file in files: if not file.endswith(".yml"): if not (file.endswith(".md") or file.endswith(".git") or file.endswith(".txt")): # expect to see readme.md, format.md, and maybe a .git directory # other things maybe are rules, but are mis-named. logger.warning("skipping non-.yml file: %s", file) continue rule_path = os.path.join(root, file) rule_paths.append(rule_path) rules = [] total_paths = len(rule_paths) for (i, rule_path) in enumerate(rule_paths): update_wait_box("loading capa rules from %s (%d of %d)" % (self.rule_path, i + 1, total_paths)) if ida_kernwin.user_cancelled(): raise UserCancelledError("user cancelled") try: rule = capa.rules.Rule.from_yaml_file(rule_path) except capa.rules.InvalidRule: raise else: rule.meta["capa/path"] = rule_path if capa.main.is_nursery_rule_path(rule_path): rule.meta["capa/nursery"] = True rules.append(rule) rule_count = len(rules) rules = capa.rules.RuleSet(rules) except UserCancelledError: logger.info("User cancelled analysis.") return False except Exception as e: capa.ida.helpers.inform_user_ida_ui( "Failed to load capa rules from %s" % self.rule_path) logger.error("Failed to load rules from %s (error: %s).", self.rule_path, e) logger.error( "Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules." ) self.rule_path = "" settings.user.del_value("rule_path") return False if ida_kernwin.user_cancelled(): logger.info("User cancelled analysis.") return False update_wait_box("extracting features") try: meta = capa.ida.helpers.collect_metadata() capabilities, counts = capa.main.find_capabilities( rules, extractor, disable_progress=True) meta["analysis"].update(counts) except UserCancelledError: logger.info("User cancelled analysis.") return False except Exception as e: logger.error( "Failed to extract capabilities from database (error: %s)", e) return False update_wait_box("checking for file limitations") try: # support binary files specifically for x86/AMD64 shellcode # warn user binary file is loaded but still allow capa to process it # TODO: check specific architecture of binary files based on how user configured IDA processors if idaapi.get_file_type_name() == "Binary file": logger.warning("-" * 80) logger.warning(" Input file appears to be a binary file.") logger.warning(" ") logger.warning( " capa currently only supports analyzing binary files containing x86/AMD64 shellcode with IDA." ) logger.warning( " This means the results may be misleading or incomplete if the binary file loaded in IDA is not x86/AMD64." ) logger.warning( " If you don't know the input file type, you can try using the `file` utility to guess it." ) logger.warning("-" * 80) capa.ida.helpers.inform_user_ida_ui( "capa encountered file type warnings during analysis") if capa.main.has_file_limitation(rules, capabilities, is_standalone=False): capa.ida.helpers.inform_user_ida_ui( "capa encountered file limitation warnings during analysis" ) except Exception as e: logger.error("Failed to check for file limitations (error: %s)", e) return False if ida_kernwin.user_cancelled(): logger.info("User cancelled analysis.") return False update_wait_box("rendering results") try: self.doc = capa.render.convert_capabilities_to_result_document( meta, rules, capabilities) self.model_data.render_capa_doc(self.doc) self.render_capa_doc_mitre_summary() self.enable_controls() self.set_view_status_label("capa rules directory: %s (%d rules)" % (self.rule_path, rule_count)) except Exception as e: logger.error("Failed to render results (error: %s)", e) return False return True