Skip to content

Commit 0c6c4b6

Browse files
Onagerberggren
andcommitted
Initial Sigma support (#1028)
* Initial work on Sigma support * Initial work on Sigma support * WIP * Initial work on Sigma support * Initial work on Sigma support * Copy sigma config into docker container * squash! Copy sigma config into docker container * squash! Copy sigma config into docker container * Update requirements.txt Co-authored-by: Johan Berggren <[email protected]>
1 parent 39de23f commit 0c6c4b6

10 files changed

+241
-9
lines changed

data/linux/recon_commands.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
title: Linux reconnaissance commands
2+
description: Commands that are run by attackers after compromising a system
3+
logsource:
4+
service: shell
5+
references:
6+
- https://github.com/mubix/post-exploitation/wiki/Linux-Post-Exploitation-Command-List
7+
detection:
8+
keywords:
9+
- 'uname -a'
10+
- 'cat /proc/version'
11+
- 'grep pass'
12+
- 'getent group'
13+
- 'getent passwd'
14+
- 'cat /home/*/.ssh/authorized_keys'
15+
- 'cat /etc/sudoers'
16+
- 'cat /etc/passwd'
17+
- 'cat /etc/resolv.conf'
18+
- 'ps aux'
19+
- 'who -a'
20+
- 'hostname -f'
21+
- 'netstat -nltupw'
22+
- 'cat /proc/net/*'
23+
timeframe: 30m
24+
conditions: count > 3

data/linux/reverse_shell.yaml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
title: Possible reverse shell command
2+
description: Commands that look like reverse shell invocations
3+
references:
4+
- https://alamot.github.io/reverse_shells/
5+
logsource:
6+
service: shell
7+
detection:
8+
keywords:
9+
- '-i >& /dev/tcp/'
10+
- 'exec 5<>/dev/tcp/'
11+
- 'nc -e /bin/sh'
12+
- "socat exec:'bash -li',pty,stderr,setsid,sigint,sane"
13+
condition: keywords

data/sigma_config.yaml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
title: Timesketch Sigma config
2+
order: 20
3+
backends:
4+
- es-dsl
5+
- es-qs
6+
logsources:
7+
sshd:
8+
service: sshd
9+
conditions:
10+
data_type: "syslog/sshd"
11+
auth:
12+
service: auth
13+
conditions:
14+
data_type: "syslog"
15+
apache:
16+
product: apache
17+
conditions:
18+
data_type: "apache:access"
19+
vsftp:
20+
service: vsftp
21+
conditions:
22+
data_type: "vsftpd:log"
23+
webserver:
24+
category: webserver
25+
conditions:
26+
data_type: "apache:access OR iis:log:line"
27+
shell:
28+
service: shell
29+
conditions:
30+
data_type: "shell:zsh:history OR bash:history:command"

docker/Dockerfile

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ RUN apt-get update && apt-get -y install plaso-tools nodejs yarn
3030
RUN pip3 install --upgrade pip
3131
ADD . /tmp/timesketch
3232
RUN cd /tmp/timesketch && yarn install && yarn run build
33-
# Remove pyyaml from requirements.txt to avoid conflits with python-yaml Ubuntu package
33+
# Remove pyyaml from requirements.txt to avoid conflicts with python-yaml Ubuntu package
3434
RUN sed -i -e '/pyyaml/d' /tmp/timesketch/requirements.txt
3535
RUN pip3 install /tmp/timesketch/
3636

3737
# Copy Timesketch config files into /etc/timesketch
3838
RUN mkdir /etc/timesketch
3939
RUN cp /tmp/timesketch/data/timesketch.conf /etc/timesketch/
4040
RUN cp /tmp/timesketch/data/features.yaml /etc/timesketch/
41+
RUN cp /tmp/timesketch/data/sigma_config.yaml /etc/timesketch/
42+
4143

4244
# Copy the entrypoint script into the container
4345
COPY docker/docker-entrypoint.sh /

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ python_dateutil==2.8.1
2424
PyYAML==5.3
2525
redis==3.3.11
2626
requests==2.21.0
27+
sigmatools==0.14 ; python_version > '3.4'
2728
six==1.12.0
2829
SQLAlchemy==1.3.12
2930
Werkzeug==0.16.0
3031
WTForms==2.2.1
31-
xlrd==1.2.0
32+
xlrd==1.2.0

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
],
6060
data_files=[
6161
('share/timesketch', glob.glob(
62-
os.path.join('data', '*'))),
62+
os.path.join('data', '*'), recursive=True)),
6363
('share/doc/timesketch', [
6464
'AUTHORS', 'LICENSE', 'README.md']),
6565
],

timesketch/lib/analyzers/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from timesketch.lib.analyzers import login
2525
from timesketch.lib.analyzers import phishy_domains
2626
from timesketch.lib.analyzers import sessionizer
27+
from timesketch.lib.analyzers import sigma_tagger
2728
from timesketch.lib.analyzers import similarity_scorer
2829
from timesketch.lib.analyzers import ssh_sessionizer
2930
from timesketch.lib.analyzers import gcp_servicekey

timesketch/lib/analyzers/interface.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ def wrapper(self, *args, **kwargs):
4343
return func_return
4444
return wrapper
4545

46+
def get_config_path(file_name):
47+
"""Returns a path to a configuration file.
48+
49+
Args:
50+
file_name: String that defines the config file name.
51+
52+
Returns:
53+
The path to the configuration file or None if the file cannot be found.
54+
"""
55+
path = os.path.join(os.path.sep, 'etc', 'timesketch', file_name)
56+
if os.path.isfile(path):
57+
return path
58+
59+
path = os.path.join(
60+
os.path.dirname(__file__), '..', '..', '..', 'data', file_name)
61+
path = os.path.abspath(path)
62+
if os.path.isfile(path):
63+
return path
64+
65+
return None
66+
4667

4768
def get_yaml_config(file_name):
4869
"""Return a dict parsed from a YAML file within the config directory.
@@ -55,12 +76,8 @@ def get_yaml_config(file_name):
5576
an empty dict if the file is not found or YAML was unable
5677
to parse it.
5778
"""
58-
root_path = os.path.join(os.path.sep, 'etc', 'timesketch')
59-
if not os.path.isdir(root_path):
60-
return {}
61-
62-
path = os.path.join(root_path, file_name)
63-
if not os.path.isfile(path):
79+
path = get_config_path(file_name)
80+
if not path:
6481
return {}
6582

6683
with open(path, 'r') as fh:
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Index analyzer plugin for sigma."""
2+
from __future__ import unicode_literals
3+
4+
import logging
5+
import os
6+
7+
from sigma.backends import elasticsearch as sigma_elasticsearch
8+
import sigma.configuration as sigma_configuration
9+
from sigma.parser import collection as sigma_collection
10+
11+
12+
from timesketch.lib.analyzers import interface
13+
from timesketch.lib.analyzers import manager
14+
15+
16+
class SigmaPlugin(interface.BaseSketchAnalyzer):
17+
"""Index analyzer for Sigma."""
18+
19+
NAME = 'sigma'
20+
21+
_CONFIG_FILE = 'sigma_config.yaml'
22+
23+
# Path to the directory containing the Sigma Rules to run, relative to
24+
# this file.
25+
_RULES_PATH = ''
26+
27+
28+
def __init__(self, index_name, sketch_id):
29+
"""Initialize the Index Analyzer.
30+
31+
Args:
32+
index_name: Elasticsearch index name.
33+
sketch_id: Sketch ID.
34+
"""
35+
super(SigmaPlugin, self).__init__(index_name, sketch_id)
36+
sigma_config_path = interface.get_config_path(self._CONFIG_FILE)
37+
logging.debug('[sigma] Loading config from {0!s}'.format(
38+
sigma_config_path))
39+
with open(sigma_config_path, 'r') as sigma_config_file:
40+
sigma_config = sigma_config_file.read()
41+
self.sigma_config = sigma_configuration.SigmaConfiguration(sigma_config)
42+
43+
def run_sigma_rule(self, query, tag_name):
44+
"""Runs a sigma rule and applies the appropriate tags.
45+
46+
Args:
47+
query: elastic search query for events to tag.
48+
tag_name: tag to apply to matching events.
49+
50+
Returns:
51+
int: number of events tagged.
52+
"""
53+
return_fields = []
54+
tagged_events = 0
55+
events = self.event_stream(
56+
query_string=query, return_fields=return_fields)
57+
for event in events:
58+
event.add_tags([tag_name])
59+
event.commit()
60+
tagged_events += 1
61+
return tagged_events
62+
63+
def run(self):
64+
"""Entry point for the analyzer.
65+
66+
Returns:
67+
String with summary of the analyzer result.
68+
"""
69+
sigma_backend = sigma_elasticsearch.ElasticsearchQuerystringBackend(
70+
self.sigma_config, {})
71+
tags_applied = {}
72+
73+
rules_path = os.path.join(os.path.dirname(__file__), self._RULES_PATH)
74+
for rule_filename in os.listdir(rules_path):
75+
tag_name, _ = rule_filename.rsplit('.')
76+
tags_applied[tag_name] = 0
77+
rule_file_path = os.path.join(rules_path, rule_filename)
78+
rule_file_path = os.path.abspath(rule_file_path)
79+
logging.info('[sigma] Reading rules from {0!s}'.format(
80+
rule_file_path))
81+
with open(rule_file_path, 'r') as rule_file:
82+
rule_file_content = rule_file.read()
83+
parser = sigma_collection.SigmaCollectionParser(
84+
rule_file_content, self.sigma_config, None)
85+
try:
86+
results = parser.generate(sigma_backend)
87+
except NotImplementedError as exception:
88+
logging.error(
89+
'Error generating rule in file {0:s}: {1!s}'.format(
90+
rule_file_path, exception))
91+
continue
92+
93+
for result in results:
94+
logging.info(
95+
'[sigma] Generated query {0:s}'.format(result))
96+
number_of_tagged_events = self.run_sigma_rule(
97+
result, tag_name)
98+
tags_applied[tag_name] += number_of_tagged_events
99+
100+
total_tagged_events = sum(tags_applied.values())
101+
output_string = 'Applied {0:d} tags\n'.format(total_tagged_events)
102+
for tag_name, number_of_tagged_events in tags_applied.items():
103+
output_string += '* {0:s}: {1:d}'.format(
104+
tag_name, number_of_tagged_events)
105+
return output_string
106+
107+
108+
class LinuxRulesSigmaPlugin(SigmaPlugin):
109+
"""Sigma plugin to run Linux rules."""
110+
111+
_RULES_PATH = '../../../data/linux'
112+
113+
NAME = 'sigma_linux'
114+
115+
116+
manager.AnalysisManager.register_analyzer(LinuxRulesSigmaPlugin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Tests for SigmaPlugin."""
2+
from __future__ import unicode_literals
3+
4+
import mock
5+
6+
from timesketch.lib.analyzers import sigma_tagger
7+
from timesketch.lib.testlib import BaseTest
8+
from timesketch.lib.testlib import MockDataStore
9+
10+
11+
class TestSigmaPlugin(BaseTest):
12+
"""Tests the functionality of the analyzer."""
13+
14+
def __init__(self, *args, **kwargs):
15+
super(TestSigmaPlugin, self).__init__(*args, **kwargs)
16+
self.test_index = 'test_index'
17+
18+
19+
# Mock the Elasticsearch datastore.
20+
@mock.patch(
21+
'timesketch.lib.analyzers.interface.ElasticsearchDataStore',
22+
MockDataStore)
23+
def test_analyzer(self):
24+
"""Test analyzer."""
25+
# TODO: Add more tests
26+
27+
_ = sigma_tagger.LinuxRulesSigmaPlugin(
28+
sketch_id=1, index_name=self.test_index)

0 commit comments

Comments
 (0)