2021-12-02 16:38:40 +01:00
|
|
|
# Output backends for sigmac
|
|
|
|
|
# Copyright 2021 Datadog, Inc.
|
|
|
|
|
|
|
|
|
|
# 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/>.
|
|
|
|
|
|
2021-11-30 16:10:56 +01:00
|
|
|
import os
|
|
|
|
|
import yaml
|
2021-12-15 16:46:42 +01:00
|
|
|
import argparse
|
2021-11-30 16:10:56 +01:00
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
from sigma.configuration import SigmaConfiguration
|
|
|
|
|
from sigma.parser.rule import SigmaParser
|
2021-12-02 16:15:08 +01:00
|
|
|
from sigma.config.mapping import FieldMapping
|
2021-12-01 16:20:46 -05:00
|
|
|
from sigma.backends.datadog import DatadogLogsBackend
|
2021-11-30 16:10:56 +01:00
|
|
|
|
|
|
|
|
|
2021-12-03 12:41:49 -05:00
|
|
|
class TestDatadogLogsBackend(unittest.TestCase):
|
|
|
|
|
"""Test cases for the Datadog Logs backend."""
|
2021-11-30 16:10:56 +01:00
|
|
|
|
2021-12-21 12:02:31 +01:00
|
|
|
def generate_query(self, rule, backend_options=None, config=None, fieldmappings=None):
|
|
|
|
|
if backend_options is None:
|
|
|
|
|
backend_options = {}
|
|
|
|
|
|
|
|
|
|
if config is None:
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
if fieldmappings is None:
|
|
|
|
|
fieldmappings = {}
|
|
|
|
|
|
2021-12-15 16:26:31 +01:00
|
|
|
cfg = SigmaConfiguration()
|
|
|
|
|
cfg.config = config
|
|
|
|
|
cfg.fieldmappings = fieldmappings
|
|
|
|
|
backend = DatadogLogsBackend(cfg, backend_options)
|
|
|
|
|
parser = SigmaParser(rule, cfg)
|
|
|
|
|
|
|
|
|
|
return backend.generate(parser)
|
|
|
|
|
|
|
|
|
|
def generate_basic_rule(self):
|
|
|
|
|
return {
|
2021-12-01 11:00:27 +01:00
|
|
|
"detection": {"selection": {"attribute": "test"}, "condition": "selection"}
|
|
|
|
|
}
|
2021-12-01 11:38:38 +01:00
|
|
|
|
2021-12-02 16:38:40 +01:00
|
|
|
def test_attribute(self):
|
2021-12-15 16:26:31 +01:00
|
|
|
query = self.generate_query(self.generate_basic_rule())
|
2021-12-02 16:38:40 +01:00
|
|
|
expected_query = "@attribute:test"
|
|
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
2021-12-15 17:26:45 +01:00
|
|
|
def test_tags_backend_option(self):
|
2021-12-02 16:38:40 +01:00
|
|
|
query = self.generate_query(
|
2021-12-15 16:26:31 +01:00
|
|
|
self.generate_basic_rule(), backend_options={"index": "test_index"}
|
2021-12-02 16:38:40 +01:00
|
|
|
)
|
|
|
|
|
expected_query = "index:test_index AND @attribute:test"
|
|
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
2021-12-15 17:26:45 +01:00
|
|
|
def test_tags_config(self):
|
2021-12-15 16:26:31 +01:00
|
|
|
rule = {
|
|
|
|
|
"detection": {
|
2021-12-15 17:26:45 +01:00
|
|
|
"selection": {"attribute": "test", "test-tag": "mytag"},
|
2021-12-15 16:26:31 +01:00
|
|
|
"condition": "selection",
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-15 17:26:45 +01:00
|
|
|
query = self.generate_query(rule, config={"tags": ["test-tag"]})
|
|
|
|
|
expected_query = "@attribute:test AND test-tag:mytag"
|
2021-12-02 16:38:40 +01:00
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
|
|
|
|
def test_special_characters_escape(self):
|
2021-12-15 16:26:31 +01:00
|
|
|
rule = {
|
|
|
|
|
"detection": {
|
|
|
|
|
"selection": {
|
|
|
|
|
"attribute": "test",
|
2021-12-15 17:14:58 +01:00
|
|
|
"regex-attribute": "anything!inbetween",
|
2021-12-15 16:26:31 +01:00
|
|
|
},
|
|
|
|
|
"condition": "selection",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
query = self.generate_query(rule)
|
2021-12-15 17:14:58 +01:00
|
|
|
expected_query = """@attribute:test AND @regex-attribute:anything\\!inbetween"""
|
2021-12-02 16:38:40 +01:00
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
|
|
|
|
def test_space_escape(self):
|
2021-12-15 16:26:31 +01:00
|
|
|
rule = {
|
|
|
|
|
"detection": {
|
|
|
|
|
"selection": {
|
|
|
|
|
"attribute": "test",
|
|
|
|
|
"space-attribute": "with space",
|
|
|
|
|
},
|
|
|
|
|
"condition": "selection",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
query = self.generate_query(rule)
|
2021-12-02 16:38:40 +01:00
|
|
|
expected_query = "@attribute:test AND @space-attribute:with?space"
|
|
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
2021-12-15 16:50:43 +01:00
|
|
|
def test_field_mappings(self):
|
2021-12-02 16:38:40 +01:00
|
|
|
query = self.generate_query(
|
2021-12-15 16:26:31 +01:00
|
|
|
self.generate_basic_rule(),
|
2021-12-02 16:38:40 +01:00
|
|
|
fieldmappings={"attribute": FieldMapping("attribute", "another_attribute")},
|
|
|
|
|
)
|
|
|
|
|
expected_query = "@another_attribute:test"
|
|
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
2021-12-15 16:52:48 +01:00
|
|
|
def test_list_selection(self):
|
|
|
|
|
rule = {
|
|
|
|
|
"detection": {
|
|
|
|
|
"selection": {
|
|
|
|
|
"attribute": "test",
|
|
|
|
|
"list-attribute": [
|
|
|
|
|
"a",
|
|
|
|
|
"b",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
"condition": "selection",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
query = self.generate_query(rule)
|
|
|
|
|
expected_query = "@attribute:test AND @list-attribute:(a OR b)"
|
|
|
|
|
self.assertEqual(query, expected_query)
|
|
|
|
|
|
2021-12-15 16:50:43 +01:00
|
|
|
|
2021-12-15 16:46:42 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
"""
|
|
|
|
|
Run the Datadog backend over all the Sigma rules in the repository.
|
|
|
|
|
You can run the script simply with: `python3 -m tests.test_backend_datadog`
|
|
|
|
|
from the `tools/` directory.
|
|
|
|
|
"""
|
|
|
|
|
parser = argparse.ArgumentParser(description="Test the Datadog backend over all the Sigma rules in the repository.")
|
|
|
|
|
parser.add_argument("--verbose", default=False, action="store_true", help="Print individual results about each processed rule.")
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
verbose_report = args.verbose
|
|
|
|
|
|
|
|
|
|
skipped = 0
|
|
|
|
|
errors = 0
|
|
|
|
|
successes = 0
|
|
|
|
|
total = 0
|
|
|
|
|
|
|
|
|
|
config = SigmaConfiguration()
|
|
|
|
|
backend = DatadogLogsBackend(config)
|
|
|
|
|
|
|
|
|
|
for (dirpath, _, filenames) in os.walk("../rules"):
|
|
|
|
|
for filename in filenames:
|
|
|
|
|
if filename.endswith(".yaml") or filename.endswith(".yml"):
|
|
|
|
|
rule_path = os.path.join(dirpath, filename)
|
|
|
|
|
|
|
|
|
|
with open(rule_path, "r") as rule_file:
|
|
|
|
|
total += 1
|
|
|
|
|
parser = SigmaParser(yaml.safe_load(rule_file), config)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
query = backend.generate(parser)
|
|
|
|
|
except NotImplementedError as err:
|
|
|
|
|
if verbose_report:
|
|
|
|
|
print("[SKIPPED] {}: {}".format(rule_path, err))
|
|
|
|
|
skipped += 1
|
|
|
|
|
except BaseException as err:
|
|
|
|
|
if verbose_report:
|
|
|
|
|
print("[FAILED] {}: {}".format(rule_path, err))
|
|
|
|
|
errors += 1
|
|
|
|
|
else:
|
|
|
|
|
if verbose_report:
|
|
|
|
|
print("[OK] {}".format(rule_path))
|
|
|
|
|
successes += 1
|
|
|
|
|
|
|
|
|
|
print("\n==========Statistics==========\n")
|
|
|
|
|
print(
|
|
|
|
|
"SUCCESSES: {}/{} ({:.2f}%)".format(
|
|
|
|
|
successes, total, successes / total * 100
|
2021-11-30 16:10:56 +01:00
|
|
|
)
|
2021-12-15 16:46:42 +01:00
|
|
|
)
|
|
|
|
|
print("SKIPPED: {}/{} ({:.2f}%)".format(skipped, total, skipped / total * 100))
|
|
|
|
|
print("ERRORS: {}/{} ({:.2f}%)".format(errors, total, errors / total * 100))
|
|
|
|
|
print("\n==============================\n")
|