2019-01-06 22:45:53 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# Convert Sigma rules with EventIDs to rules with generic log sources
|
|
|
|
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
import yaml
|
|
|
|
import sys
|
|
|
|
from pathlib import Path
|
2019-11-11 22:35:16 +00:00
|
|
|
from sigma.output import SigmaYAMLDumper
|
2019-01-06 22:45:53 +00:00
|
|
|
|
|
|
|
class Output(object):
|
|
|
|
"""Output base class"""
|
|
|
|
def write(self, *args, **kwargs):
|
|
|
|
self.f.write(*args, **kwargs)
|
|
|
|
|
|
|
|
class SingleFileOutput(Output):
|
|
|
|
"""Output into single file with multiple YAML documents. Each input file is announced with comment."""
|
|
|
|
def __init__(self, name):
|
|
|
|
self.f = open(name, "x")
|
2019-01-08 22:27:16 +00:00
|
|
|
self.path = None
|
2019-01-06 22:45:53 +00:00
|
|
|
self.first = True
|
|
|
|
|
2019-01-08 22:27:16 +00:00
|
|
|
def new_output(self, path):
|
2019-01-06 22:45:53 +00:00
|
|
|
"""Announce new Sigma rule as input and start new YAML document."""
|
2019-01-08 22:27:16 +00:00
|
|
|
if self.path is None or self.path != path:
|
|
|
|
if self.first:
|
|
|
|
self.first = False
|
|
|
|
else:
|
|
|
|
self.f.write("---\n")
|
|
|
|
self.path = path
|
|
|
|
self.f.write("# Sigma rule: {}\n".format(path))
|
2019-01-06 22:45:53 +00:00
|
|
|
|
|
|
|
def finish(self):
|
|
|
|
self.f.close()
|
|
|
|
|
|
|
|
class StdoutOutput(SingleFileOutput):
|
|
|
|
"""Like SingleFileOutput, just for standard output"""
|
|
|
|
def __init__(self):
|
|
|
|
self.f = sys.stdout
|
2019-01-08 22:27:16 +00:00
|
|
|
self.path = None
|
2019-01-06 22:45:53 +00:00
|
|
|
self.first = True
|
|
|
|
|
|
|
|
def finish(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class DirectoryOutput(Output):
|
|
|
|
"""Output each input file into a corresponding output file in target directory."""
|
|
|
|
def __init__(self, dirpath):
|
|
|
|
self.d = dirpath
|
|
|
|
self.f = None
|
2019-01-06 22:57:09 +00:00
|
|
|
self.path = None
|
2019-01-13 22:04:55 +00:00
|
|
|
self.opened = None
|
2019-01-06 22:45:53 +00:00
|
|
|
|
|
|
|
def new_output(self, path):
|
2019-01-06 22:57:09 +00:00
|
|
|
if self.path is None or self.path != path:
|
|
|
|
if self.f is not None:
|
|
|
|
self.f.close()
|
|
|
|
self.path = path
|
2019-01-13 22:04:55 +00:00
|
|
|
self.opened = False # opening file is deferred to first write
|
|
|
|
|
|
|
|
def write(self, *args, **kwargs):
|
|
|
|
if not self.opened:
|
|
|
|
self.f = (self.d / self.path.name).open("x")
|
|
|
|
super().write(*args, **kwargs)
|
|
|
|
|
|
|
|
def finish(self):
|
|
|
|
if self.f is not None:
|
|
|
|
self.f.close()
|
2019-01-06 22:45:53 +00:00
|
|
|
|
|
|
|
def get_output(output):
|
|
|
|
if output is None:
|
|
|
|
return StdoutOutput()
|
|
|
|
|
|
|
|
path = Path(output)
|
|
|
|
if path.is_dir():
|
|
|
|
return DirectoryOutput(path)
|
|
|
|
else:
|
|
|
|
return SingleFileOutput(output)
|
|
|
|
|
|
|
|
class AmbiguousRuleException(TypeError):
|
|
|
|
def __init__(self, ids):
|
|
|
|
super().__init__()
|
|
|
|
self.ids = ids
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return(", ".join([str(eid) for eid in self.ids]))
|
|
|
|
|
|
|
|
def convert_to_generic(yamldoc):
|
|
|
|
changed = False
|
2019-01-06 22:57:09 +00:00
|
|
|
try:
|
|
|
|
product = yamldoc["logsource"]["product"]
|
|
|
|
service = yamldoc["logsource"]["service"]
|
|
|
|
except KeyError:
|
|
|
|
return False
|
|
|
|
|
2019-01-06 22:45:53 +00:00
|
|
|
if product == "windows" and service in ("sysmon", "security"):
|
|
|
|
# Currently, only Windows Security or Sysmon are relevant
|
|
|
|
eventids = set()
|
|
|
|
for name, detection in yamldoc["detection"].items(): # first collect all event ids
|
|
|
|
if name == "condition" or type(detection) is not dict:
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
eventid = detection["EventID"]
|
|
|
|
try: # expect that EventID attribute contains a list
|
|
|
|
eventids.update(eventid)
|
|
|
|
except TypeError: # if this fails, it's a plain value
|
|
|
|
eventids.add(eventid)
|
|
|
|
except KeyError: # No EventID attribute
|
|
|
|
pass
|
|
|
|
|
|
|
|
if 1 in eventids and service == "sysmon" or \
|
|
|
|
4688 in eventids and service == "security":
|
|
|
|
if len(eventids) == 1: # only convert if one EventID collected, else it gets complicated
|
|
|
|
# remove all EventID definitions
|
2019-01-13 22:04:55 +00:00
|
|
|
empty_name = list()
|
2019-01-06 22:45:53 +00:00
|
|
|
for name, detection in yamldoc["detection"].items():
|
|
|
|
if name == "condition" or type(detection) is not dict:
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
del detection["EventID"]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2019-01-13 22:04:55 +00:00
|
|
|
if detection == {}: # detection was reduced to nothing - remove it later
|
|
|
|
empty_name.append(name)
|
|
|
|
|
|
|
|
for name in empty_name: # delete empty detections
|
|
|
|
del yamldoc["detection"][name]
|
|
|
|
|
|
|
|
if yamldoc["detection"] == {}: # delete detection section if empty
|
|
|
|
del yamldoc["detection"]
|
|
|
|
|
2019-01-06 22:45:53 +00:00
|
|
|
# rewrite log source
|
|
|
|
yamldoc["logsource"] = {
|
|
|
|
"category": "process_creation",
|
|
|
|
"product": "windows"
|
|
|
|
}
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
else: # raise an exception to print a warning message to make user aware about the issue
|
|
|
|
raise AmbiguousRuleException(eventids)
|
|
|
|
return changed
|
|
|
|
|
|
|
|
def get_input_paths(args):
|
|
|
|
if args.recursive:
|
|
|
|
return [ p for pathname in args.sigma for p in Path(pathname).glob("**/*") if p.is_file() ]
|
|
|
|
else:
|
|
|
|
return [ Path(sigma) for sigma in args.sigma ]
|
|
|
|
|
|
|
|
argparser = ArgumentParser(description="Convert between classical and generic log source Sigma rules.")
|
|
|
|
argparser.add_argument("--output", "-o", help="Output file or directory. Default: standard output.")
|
|
|
|
argparser.add_argument("--recursive", "-r", action="store_true", help="Recursive traversal of directory")
|
2019-01-13 22:53:11 +00:00
|
|
|
argparser.add_argument("--converted-list", "-c", help="Write list of rule files that were successfully converted (default: stdout)")
|
2019-01-06 22:45:53 +00:00
|
|
|
argparser.add_argument("sigma", nargs="+", help="Sigma rule file(s) that should be converted")
|
|
|
|
args = argparser.parse_args()
|
|
|
|
|
2019-01-16 21:37:32 +00:00
|
|
|
# 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())
|
|
|
|
|
|
|
|
yaml.add_representer(dict, yaml_preserve_order)
|
|
|
|
|
2019-01-06 22:45:53 +00:00
|
|
|
input_paths = get_input_paths(args)
|
|
|
|
output = get_output(args.output)
|
2019-01-13 22:53:11 +00:00
|
|
|
if args.converted_list:
|
|
|
|
fconv = open(args.converted_list, "w")
|
|
|
|
else:
|
|
|
|
fconv = sys.stdout
|
2019-01-06 22:45:53 +00:00
|
|
|
|
|
|
|
for path in input_paths:
|
|
|
|
try:
|
|
|
|
f = path.open("r")
|
|
|
|
except OSError as e:
|
|
|
|
print("Error while reading Sigma rule {}: {}".format(path, str(e)), file=sys.stderr)
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
try:
|
2019-01-08 22:27:16 +00:00
|
|
|
yamldocs = list(yaml.safe_load_all(f))
|
2019-01-06 22:45:53 +00:00
|
|
|
except yaml.YAMLError as e:
|
|
|
|
print("YAML parse error while parsing Sigma rule {}: {}".format(path, str(e)), file=sys.stderr)
|
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
yamldoc_num = 0
|
2019-01-08 22:27:16 +00:00
|
|
|
changed = False
|
2019-01-06 22:45:53 +00:00
|
|
|
for yamldoc in yamldocs:
|
|
|
|
yamldoc_num += 1
|
2019-01-08 22:27:16 +00:00
|
|
|
output.new_output(path)
|
2019-01-06 22:45:53 +00:00
|
|
|
try:
|
2019-01-08 22:27:16 +00:00
|
|
|
changed |= convert_to_generic(yamldoc)
|
2019-01-06 22:45:53 +00:00
|
|
|
except AmbiguousRuleException as e:
|
2019-01-08 22:27:16 +00:00
|
|
|
changed = False
|
2019-01-06 22:45:53 +00:00
|
|
|
print("Rule {} in file {} contains multiple EventIDs: {}".format(yamldoc_num, str(path), str(e)), file=sys.stderr)
|
|
|
|
|
2019-01-13 22:04:55 +00:00
|
|
|
yamldocs_idx = list(zip(range(len(yamldocs)), yamldocs))
|
|
|
|
delete = set()
|
|
|
|
for i, yamldoc_a in yamldocs_idx: # iterate over all yaml document pairs
|
|
|
|
for j, yamldoc_b in yamldocs_idx:
|
|
|
|
if j <= i: # symmetric relation, skip same comparisons
|
|
|
|
continue
|
|
|
|
if yamldoc_a == yamldoc_b:
|
|
|
|
delete.add(j)
|
|
|
|
|
|
|
|
for i in reversed(sorted(delete)): # delete double yaml documents
|
|
|
|
del yamldocs[i]
|
|
|
|
|
|
|
|
# Common special case: two yaml docs, one global and one remainder of multiple following docs - merge them
|
|
|
|
try:
|
|
|
|
if len(yamldocs) == 2 and \
|
|
|
|
yamldocs[0]["action"] == "global" and \
|
|
|
|
"action" not in yamldocs[1] and \
|
|
|
|
set(yamldocs[0].keys()) & set(yamldocs[1].keys()) == set(): # last condition: no common keys
|
|
|
|
yamldocs[0].update(yamldocs[1])
|
|
|
|
del yamldocs[1]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2019-01-08 22:27:16 +00:00
|
|
|
if changed: # only write output if changed
|
|
|
|
try:
|
2019-03-01 23:14:20 +00:00
|
|
|
output.write(yaml.dump_all(yamldocs, Dumper=SigmaYAMLDumper, indent=4, width=160, default_flow_style=False))
|
2019-01-13 22:53:11 +00:00
|
|
|
print(path, file=fconv)
|
2019-01-08 22:27:16 +00:00
|
|
|
except OSError as e:
|
|
|
|
print("Error while writing result: {}".format(str(e)), file=sys.stderr)
|
|
|
|
sys.exit(2)
|
|
|
|
|
2019-01-06 22:45:53 +00:00
|
|
|
output.finish()
|