Repository: mandiant/capa
File: scripts/detect_duplicate_features.py (Python)

Code before:
            if filename.endswith(".yml"):
                rule_path = os.path.join(dirpath, filename)
                with open(rule_path, "r") as f:
                    rule = yaml.safe_load(f)
                    if "rule" not in rule:
                        continue
                    rule_features = findall_features(rule["rule"]["features"])
                    count += 1
                if any([feature in rule_features for feature in new_rule_features]):
                    overlapping_rules.append(rule_path)

    result = {"overlapping_rules": overlapping_rules, "count": count}

    return result


# python script.py --base-dir /path/to/capa/rules rules/anti-analysis/reference-analysis-tools-strings.yml rules


def main():
    # usage

    parser = argparse.ArgumentParser(description="Find overlapping rules in Capa rules.")
    parser.add_argument("-b", "--base-dir", default="", help="Base directory for Capa rules.")
    parser.add_argument("-f", "--new_rule_path", required=True, help="Path to the new Capa rule.")
    parser.add_argument("-d", "--rules_path", required=True, help="Path to the directory containing Capa rules.")
    args = parser.parse_args()

    base_dir = args.base_dir
    new_rule_path = os.path.join(base_dir, args.new_rule_path)
    rules_path = os.path.join(base_dir, args.rules_path)

    try:
        result = find_overlapping_rules(new_rule_path, rules_path)
        print("New rule path : %s" % new_rule_path)
        print("Number of rules checked : %s " % result["count"])
        print("Paths to overlapping rules : ", result["overlapping_rules"])
        print("Number of rules containing same features : %s" % len(result["overlapping_rules"]))
    except Exception as e:
        print(e)
        try:
            print(result, "")
        except:
            pass


if __name__ == "__main__":
    main()


Review comment (suggestion):
I think we can remove the `base-dir` and I'd recommend to use the same names we use in other scripts and make them required, so:
```
parser.add_argument("rules", type=str, action="append", help="Path to rules")
parser.add_argument("new_rule", type=str, help="Path to new rule")
```

Code after:
        list: A list of all feature statements contained within the rule file.
    """
    feature_list = []
    with open(rule_path, "r") as f:
        try:
            new_rule = capa.rules.Rule.from_yaml(f.read())
            feature_list = get_child_features(new_rule.statement)
        except Exception as e:
            logger.error("Error: New rule " + rule_path + " " + str(type(e)) + " " + str(e))
            sys.exit(-1)
    return feature_list


def find_overlapping_rules(new_rule_path, rules_path):
    if not new_rule_path.endswith(".yml"):
        logger.error("FileNotFoundError ! New rule file name doesn't end with .yml")
        sys.exit(-1)

    # Loads features of new rule in a list.
    new_rule_features = get_features(new_rule_path)

    count = 0
    overlapping_rules = []

    # capa.rules.RuleSet stores all rules in given paths
    ruleset = capa.main.get_rules(rules_path)

    for rule_name, rule in ruleset.rules.items():
        rule_features = get_child_features(rule.statement)

        if not len(rule_features):
            continue
        count += 1
        # Checks if any features match between existing and new rule.
        if any([feature in rule_features for feature in new_rule_features]):
            overlapping_rules.append(rule_name)

    result = {"overlapping_rules": overlapping_rules, "count": count}
    return result


def main():
    parser = argparse.ArgumentParser(description="Find overlapping features in Capa rules.")

    parser.add_argument("rules", type=str, action="append", help="Path to rules")
    parser.add_argument("new_rule", type=str, help="Path to new rule")

    args = parser.parse_args()

    new_rule_path = args.new_rule
    rules_path = args.rules