#!/usr/bin/env python3 # Assign UUIDs to Sigma rules and verify UUID assignment for a Sigma rule repository from argparse import ArgumentParser from pathlib import Path from uuid import uuid4, UUID import yaml from sigma.output import SigmaYAMLDumper def print_verbose(*arg, **kwarg): print(*arg, **kwarg) # Define order-preserving representer from dicts/maps def yaml_preserve_order(self, dict_data): return self.represent_mapping("tag:yaml.org,2002:map", dict_data.items()) def main(): argparser = ArgumentParser(description="Assign and verfify UUIDs of Sigma rules") argparser.add_argument("--verify", "-V", action="store_true", help="Verify existence and uniqueness of UUID assignments. Exits with error code if verification fails.") argparser.add_argument("--verbose", "-v", action="store_true", help="Be verbose.") argparser.add_argument("--recursive", "-r", action="store_true", help="Recurse into directories.") argparser.add_argument("--error", "-e", action="store_true", help="Exit with error code 10 on verification failures.") argparser.add_argument("inputs", nargs="+", help="Sigma rule files or repository directories") args = argparser.parse_args() if args.verbose: print_verbose() if args.recursive: paths = [ p for pathname in args.inputs for p in Path(pathname).glob("**/*") if p.is_file() ] else: paths = [ Path(pathname) for pathname in args.inputs ] yaml.add_representer(dict, yaml_preserve_order) uuids = set() passed = True for path in paths: print_verbose("Rule {}".format(str(path))) with path.open("r") as f: rules = list(yaml.safe_load_all(f)) if args.verify: i = 1 for rule in rules: if "title" in rule: # Rule with a title should also have a UUID try: UUID(rule["id"]) except ValueError: # id is not a valid UUID print("Rule {} in file {} has a malformed UUID '{}'.".format(i, str(path), rule["id"])) passed = False except KeyError: # rule has no id print("Rule {} in file {} has no UUID.".format(i, str(path))) passed = False i += 1 else: newrules = list() changed = False i = 1 for rule in rules: if "title" in rule and "id" not in rule: # only assign id to rules that have a title and no id newrule = dict() changed = True for k, v in rule.items(): newrule[k] = v if k == "title": # insert id after title uuid = uuid4() newrule["id"] = str(uuid) print("Assigned UUID '{}' to rule {} in file {}.".format(uuid, i, str(path))) newrules.append(newrule) else: newrules.append(rule) i += 1 if changed: with path.open("w") as f: yaml.dump_all(newrules, f, Dumper=SigmaYAMLDumper, indent=4, width=160, default_flow_style=False) if not passed: print("The Sigma rules listed above don't have an ID. The ID must be:") print("* Contained in the 'id' attribute") print("* a valid UUIDv4 (randomly generated)") print("* Unique in this repository") print("Please generate one with the sigma_uuid tool or here: https://www.uuidgenerator.net/version4") if args.error: exit(10) if __name__ == "__main__": main()