2017-02-13 22:14:40 +00:00
#!/usr/bin/env python3
# A Sigma to SIEM converter
2017-12-07 20:55:43 +00:00
# Copyright 2016-2017 Thomas Patzke, Florian Roth
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2017-02-13 22:14:40 +00:00
import sys
import argparse
import yaml
import json
2017-03-06 08:36:10 +00:00
import pathlib
import itertools
2017-10-31 21:13:20 +00:00
import logging
2018-07-26 22:02:07 +00:00
from sigma.parser.collection import SigmaCollectionParser
from sigma.parser.exceptions import SigmaCollectionParseError, SigmaParseError
2018-08-26 22:17:27 +00:00
from sigma.configuration import SigmaConfiguration, SigmaConfigurationChain
2019-05-17 07:13:59 +00:00
from sigma.config.collection import SigmaConfigurationManager
2018-07-27 21:54:18 +00:00
from sigma.config.exceptions import SigmaConfigParseError, SigmaRuleFilterParseException
from sigma.filter import SigmaRuleFilter
2018-07-20 21:30:32 +00:00
import sigma.backends.discovery as backends
from sigma.backends.base import BackendOptions
from sigma.backends.exceptions import BackendError, NotSupportedError, PartialMatchError, FullMatchError
2019-05-26 21:58:56 +00:00
from sigma.parser.modifiers import modifiers
2017-12-12 23:12:56 +00:00
import codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach())
2017-02-13 22:14:40 +00:00
2019-04-22 21:15:35 +00:00
# Error codes
2019-04-22 22:53:52 +00:00
ERR_OUTPUT = 1
ERR_INVALID_YAML = 3
ERR_SIGMA_PARSING = 4
ERR_OPEN_SIGMA_RULE = 5
ERR_OPEN_CONFIG_FILE = 5
2019-04-22 22:52:31 +00:00
ERR_CONFIG_INVALID_YAML = 6
2019-04-22 22:53:52 +00:00
ERR_CONFIG_PARSING = 6
ERR_BACKEND = 8
ERR_NOT_SUPPORTED = 9
ERR_NO_TARGET = 10
2019-04-22 21:15:35 +00:00
ERR_RULE_FILTER_PARSING = 11
2019-04-22 22:53:52 +00:00
ERR_CONFIG_REQUIRED = 20
ERR_CONFIG_ORDER = 21
2019-05-19 23:00:33 +00:00
ERR_CONFIG_BACKEND = 22
2019-04-22 22:53:52 +00:00
ERR_NOT_IMPLEMENTED = 42
2019-04-22 21:15:35 +00:00
ERR_PARTIAL_FIELD_MATCH = 80
2019-04-22 22:53:52 +00:00
ERR_FULL_FIELD_MATCH = 90
2019-04-22 21:15:35 +00:00
2017-03-06 08:36:10 +00:00
def alliter(path):
for sub in path.iterdir():
2017-12-14 21:39:51 +00:00
if sub.name.startswith("."):
continue
2017-03-06 08:36:10 +00:00
if sub.is_dir():
yield from alliter(sub)
else:
yield sub
def get_inputs(paths, recursive):
2018-10-07 15:11:47 +00:00
if paths == ['-']:
return [sys.stdin]
2017-03-06 08:36:10 +00:00
if recursive:
return list(itertools.chain.from_iterable([list(alliter(pathlib.Path(p))) for p in paths]))
else:
2017-03-07 08:41:46 +00:00
return [pathlib.Path(p) for p in paths]
2017-03-06 08:36:10 +00:00
2018-03-20 23:53:44 +00:00
class SigmacArgumentParser(argparse.ArgumentParser):
def format_help(self):
helptext = super().format_help() + "\nBackend options:\n"
for backend in backends.getBackendList():
if len(backend.options) > 0:
helptext += " " + backend.identifier + "\n"
for option, default, help, _ in backend.options:
helptext += " {:10}: {} (default: {})".format(option, help, default) + "\n"
return helptext
2019-06-10 06:55:52 +00:00
def set_argparser():
"""Sets up and parses the command line arguments for Sigmac.
Returns the argparser"""
argparser = SigmacArgumentParser(description="Convert Sigma rules into SIEM signatures.")
argparser.add_argument("--recurse", "-r", action="store_true", help="Use directory as input (recurse into subdirectories is not implemented yet)")
argparser.add_argument("--filter", "-f", help="""
Define comma-separated filters that must match (AND-linked) to rule to be processed.
Valid filters: level<=x, level>=x, level=x, status=y, logsource=z, tag=t.
x is one of: low, medium, high, critical.
y is one of: experimental, testing, stable.
z is a word appearing in an arbitrary log source attribute.
t is a tag that must appear in the rules tag list, case-insensitive matching.
Multiple log source specifications are AND linked.
""")
argparser.add_argument("--target", "-t", choices=backends.getBackendDict().keys(), help="Output target format")
argparser.add_argument("--lists", "-l", action="store_true", help="List available output target formats and configurations")
argparser.add_argument("--config", "-c", action="append", help="Configurations with field name and index mapping for target environment. Multiple configurations are merged into one. Last config is authorative in case of conflicts.")
argparser.add_argument("--output", "-o", default=None, help="Output file or filename prefix if multiple files are generated")
argparser.add_argument("--backend-option", "-O", action="append", help="Options and switches that are passed to the backend")
2019-10-07 20:30:57 +00:00
argparser.add_argument("--backend-config", "-C", help="Configuration file (YAML format) containing options to pass to the backend")
2019-06-10 06:55:52 +00:00
argparser.add_argument("--defer-abort", "-d", action="store_true", help="Don't abort on parse or conversion errors, proceed with next rule. The exit code from the last error is returned")
argparser.add_argument("--ignore-backend-errors", "-I", action="store_true", help="Only return error codes for parse errors and ignore errors for rules that cause backend errors. Useful, when you want to get as much queries as possible.")
argparser.add_argument("--shoot-yourself-in-the-foot", action="store_true", help=argparse.SUPPRESS)
argparser.add_argument("--verbose", "-v", action="store_true", help="Be verbose")
argparser.add_argument("--debug", "-D", action="store_true", help="Debugging output")
argparser.add_argument("inputs", nargs="*", help="Sigma input files ('-' for stdin)")
return argparser
argparser = set_argparser()
2017-02-13 22:14:40 +00:00
cmdargs = argparser.parse_args()
2019-05-17 07:13:59 +00:00
scm = SigmaConfigurationManager()
2019-06-10 06:55:52 +00:00
logger = logging.getLogger(__name__)
2018-11-04 22:28:40 +00:00
if cmdargs.debug: # pragma: no cover
2017-10-31 22:06:18 +00:00
logger.setLevel(logging.DEBUG)
2019-05-24 20:41:47 +00:00
def list_backends():
2017-02-13 22:14:40 +00:00
for backend in backends.getBackendList():
2020-02-03 21:16:00 +00:00
if cmdargs.debug:
print("{:>15} : {} ({})".format(backend.identifier, backend.__doc__, backend.__name__))
else:
print("{:>15} : {}".format(backend.identifier, backend.__doc__))
2019-05-17 07:13:59 +00:00
2019-05-24 20:41:47 +00:00
def list_configurations(backend=None):
for conf_id, title, backends in scm.list():
if backend is not None and backend in backends or backend is None or len(backends) == 0:
print("{:>30} : {}".format(conf_id, title))
2019-05-26 21:58:56 +00:00
def list_modifiers():
for modifier_id, modifier in modifiers.items():
print("{:>10} : {}".format(modifier_id, modifier.__doc__))
2019-05-24 20:41:47 +00:00
if cmdargs.lists:
print("Backends:")
list_backends()
2019-05-17 07:13:59 +00:00
print()
print("Configurations:")
2019-05-24 20:41:47 +00:00
list_configurations(cmdargs.target)
2019-05-26 21:58:56 +00:00
print()
print("Modifiers:")
list_modifiers()
2017-02-13 22:14:40 +00:00
sys.exit(0)
2017-12-08 22:50:08 +00:00
elif len(cmdargs.inputs) == 0:
print("Nothing to do!")
argparser.print_usage()
2018-08-02 20:41:32 +00:00
sys.exit(0)
2017-02-13 22:14:40 +00:00
2019-04-14 21:50:07 +00:00
if cmdargs.target is None:
print("No target selected, select one with -t/--target")
argparser.print_usage()
2019-04-22 21:15:35 +00:00
sys.exit(ERR_NO_TARGET)
2019-04-14 21:50:07 +00:00
2017-11-01 23:02:15 +00:00
rulefilter = None
if cmdargs.filter:
try:
rulefilter = SigmaRuleFilter(cmdargs.filter)
except SigmaRuleFilterParseException as e:
print("Parse error in Sigma rule filter expression: %s" % str(e), file=sys.stderr)
2019-04-22 21:15:35 +00:00
sys.exit(ERR_RULE_FILTER_PARSING)
2017-11-01 23:02:15 +00:00
2018-08-26 22:17:27 +00:00
sigmaconfigs = SigmaConfigurationChain()
2019-11-03 22:32:50 +00:00
backend_class = backends.getBackend(cmdargs.target)
if cmdargs.config is None:
if backend_class.config_required and not cmdargs.shoot_yourself_in_the_foot:
print("The backend you want to use usually requires a configuration to generate valid results. Please provide one with --config/-c.", file=sys.stderr)
print("Available choices for this backend (get complete list with --lists/-l):")
list_configurations(cmdargs.target)
sys.exit(ERR_CONFIG_REQUIRED)
if backend_class.default_config is not None:
cmdargs.config = backend_class.default_config
2017-03-04 22:36:46 +00:00
if cmdargs.config:
2019-04-22 22:54:10 +00:00
order = 0
2019-05-19 22:27:35 +00:00
for conf_name in cmdargs.config:
2018-08-26 22:17:27 +00:00
try:
2019-05-19 22:27:35 +00:00
sigmaconfig = scm.get(conf_name)
2019-04-22 22:54:10 +00:00
if sigmaconfig.order is not None:
if sigmaconfig.order <= order and not cmdargs.shoot_yourself_in_the_foot:
print("The configurations were provided in the wrong order (order key check in config file)", file=sys.stderr)
sys.exit(ERR_CONFIG_ORDER)
order = sigmaconfig.order
2019-05-19 23:00:33 +00:00
try:
if cmdargs.target not in sigmaconfig.config["backends"]:
print("The configuration '{}' is not valid for backend '{}'. Valid choices are: {}".format(conf_name, cmdargs.target, ", ".join(sigmaconfig.config["backends"])), file=sys.stderr)
sys.exit(ERR_CONFIG_ORDER)
except KeyError:
pass
2018-08-26 22:17:27 +00:00
sigmaconfigs.append(sigmaconfig)
except OSError as e:
2019-05-19 22:27:35 +00:00
print("Failed to open Sigma configuration file %s: %s" % (conf_name, str(e)), file=sys.stderr)
2019-04-22 22:52:31 +00:00
exit(ERR_OPEN_CONFIG_FILE)
2018-08-26 22:17:27 +00:00
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
2019-05-19 22:27:35 +00:00
print("Sigma configuration file %s is no valid YAML: %s" % (conf_name, str(e)), file=sys.stderr)
2019-04-22 22:52:31 +00:00
exit(ERR_CONFIG_INVALID_YAML)
2018-08-26 22:17:27 +00:00
except SigmaConfigParseError as e:
2019-05-19 22:27:35 +00:00
print("Sigma configuration parse error in %s: %s" % (conf_name, str(e)), file=sys.stderr)
2019-04-22 22:52:31 +00:00
exit(ERR_CONFIG_PARSING)
2017-03-04 22:36:46 +00:00
2019-02-23 12:20:20 +00:00
backend_options = BackendOptions(cmdargs.backend_option, cmdargs.backend_config)
2019-11-03 22:32:50 +00:00
backend = backend_class(sigmaconfigs, backend_options)
2019-04-22 21:40:21 +00:00
2018-08-02 20:41:32 +00:00
filename = cmdargs.output
if filename:
try:
out = open(filename, "w", encoding='utf-8')
except (IOError, OSError) as e:
print("Failed to open output file '%s': %s" % (filename, str(e)), file=sys.stderr)
2019-04-22 22:52:31 +00:00
exit(ERR_OUTPUT)
2018-08-02 20:41:32 +00:00
else:
out = sys.stdout
2017-02-22 21:47:12 +00:00
2017-08-01 22:56:22 +00:00
error = 0
2017-03-06 08:36:10 +00:00
for sigmafile in get_inputs(cmdargs.inputs, cmdargs.recurse):
2019-06-10 06:55:52 +00:00
logger.debug("* Processing Sigma input %s" % (sigmafile))
2017-02-13 22:14:40 +00:00
try:
2018-10-07 15:11:47 +00:00
if cmdargs.inputs == ['-']:
f = sigmafile
else:
f = sigmafile.open(encoding='utf-8')
2019-01-14 21:54:26 +00:00
parser = SigmaCollectionParser(f, sigmaconfigs, rulefilter)
2018-08-02 20:41:32 +00:00
results = parser.generate(backend)
for result in results:
print(result, file=out)
2017-02-13 22:14:40 +00:00
except OSError as e:
2017-03-06 22:01:33 +00:00
print("Failed to open Sigma file %s: %s" % (sigmafile, str(e)), file=sys.stderr)
2019-04-22 21:15:35 +00:00
error = ERR_OPEN_SIGMA_RULE
2017-10-19 15:42:56 +00:00
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
2017-03-06 22:01:33 +00:00
print("Sigma file %s is no valid YAML: %s" % (sigmafile, str(e)), file=sys.stderr)
2019-04-22 21:15:35 +00:00
error = ERR_INVALID_YAML
2017-08-01 22:56:22 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2017-11-01 23:02:15 +00:00
except (SigmaParseError, SigmaCollectionParseError) as e:
2017-03-06 22:01:33 +00:00
print("Sigma parse error in %s: %s" % (sigmafile, str(e)), file=sys.stderr)
2019-04-22 21:15:35 +00:00
error = ERR_SIGMA_PARSING
2017-08-01 22:56:22 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2018-07-20 21:30:32 +00:00
except NotSupportedError as e:
2018-06-21 22:22:45 +00:00
print("The Sigma rule requires a feature that is not supported by the target system: " + str(e), file=sys.stderr)
2018-06-21 22:41:21 +00:00
if not cmdargs.ignore_backend_errors:
2019-04-22 21:15:35 +00:00
error = ERR_NOT_SUPPORTED
2018-06-21 22:22:45 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2018-07-20 21:30:32 +00:00
except BackendError as e:
2017-10-22 22:45:01 +00:00
print("Backend error in %s: %s" % (sigmafile, str(e)), file=sys.stderr)
2018-06-07 21:32:52 +00:00
if not cmdargs.ignore_backend_errors:
2019-04-22 21:15:35 +00:00
error = ERR_BACKEND
2018-06-07 21:32:52 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2019-11-08 22:05:24 +00:00
except (NotImplementedError, TypeError) as e:
2018-09-23 17:12:50 +00:00
print("An unsupported feature is required for this Sigma rule (%s): " % (sigmafile) + str(e), file=sys.stderr)
2017-03-06 22:01:33 +00:00
print("Feel free to contribute for fun and fame, this is open source :) -> https://github.com/Neo23x0/sigma", file=sys.stderr)
2018-06-07 21:32:52 +00:00
if not cmdargs.ignore_backend_errors:
2019-04-22 21:15:35 +00:00
error = ERR_NOT_IMPLEMENTED
2017-08-01 22:56:22 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2018-07-20 21:30:32 +00:00
except PartialMatchError as e:
2018-06-07 21:32:52 +00:00
print("Partial field match error: %s" % str(e), file=sys.stderr)
if not cmdargs.ignore_backend_errors:
2019-04-22 21:15:35 +00:00
error = ERR_PARTIAL_FIELD_MATCH
2018-06-07 21:32:52 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2018-07-20 21:30:32 +00:00
except FullMatchError as e:
2018-06-07 21:32:52 +00:00
print("Full field match error", file=sys.stderr)
if not cmdargs.ignore_backend_errors:
2019-04-22 21:15:35 +00:00
error = ERR_FULL_FIELD_MATCH
2018-06-07 21:32:52 +00:00
if not cmdargs.defer_abort:
sys.exit(error)
2017-02-13 22:14:40 +00:00
finally:
2017-08-07 06:54:18 +00:00
try:
f.close()
except:
pass
2018-08-02 20:41:32 +00:00
result = backend.finalize()
if result:
print(result, file=out)
out.close()
2017-08-01 22:56:22 +00:00
sys.exit(error)