This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 328
/
generic_alert_loader.py
149 lines (124 loc) · 5.46 KB
/
generic_alert_loader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# Copyright (c) 2017 Mozilla Corporation
# TODO: Dont use query_models, nicer fixes for AlertTask
from lib.alerttask import AlertTask
from mozdef_util.query_models import SearchQuery, TermMatch, QueryStringMatch
from mozdef_util.utilities.dot_dict import DotDict
from mozdef_util.utilities.logger import logger
import hjson
import sys
import traceback
import glob
import os
from os.path import basename
# Minimum data needed for an alert (this is an example alert json)
'''
{
// Lucene search string
'search_string': 'field1: matchingvalue and field2: matchingothervalue',
// ES Filters as such: [['field', 'value'], ['field', 'value']]
'filters': [],
// What to aggregate on if we get multiple matches?
'aggregation_key': 'summary',
// Number of minutes from current time to look for events
"time_window": 5,
// Max number of samples to include in alert
// If the total number of events is less than this, the alert
// will still throw
"num_samples": 10,
// Total number of different type of values from aggregation key
"num_aggregations": 1,
// This is the category that will show up in mozdef, and the severity
'alert_category': 'generic_alerts',
'alert_severity': 'INFO',
// This will show up as the alert text when it trigger
'alert_summary': 'Example summary that shows up in the alert',
// This helps sorting out alerts, so it's nice if you fill this in
'alert_tags': ['generic'],
// This is the alert documentation
'alert_url': 'https://mozilla.org'
}
'''
class AlertGenericLoader(AlertTask):
required_fields = [
"search_string",
"filters",
"aggregation_key",
"time_window",
"num_samples",
"num_aggregations",
"alert_category",
"alert_tags",
"alert_severity",
"alert_summary",
"alert_url",
]
def validate_alert(self, alert):
for key in self.required_fields:
if key not in alert:
logger.error('Your alert does not have the required field {}'.format(key))
raise KeyError
def load_configs(self):
'''Load all configured rules'''
self.configs = []
rules_location = os.path.join(self.config.alert_data_location, "rules")
files = glob.glob(rules_location + "/*.json")
for f in files:
with open(f) as fd:
try:
cfg = DotDict(hjson.load(fd))
self.validate_alert(cfg)
# We set the alert name to the filename (excluding .json)
alert_name = basename(f).replace('.json', '')
cfg['custom_alert_name'] = alert_name
self.configs.append(cfg)
except Exception:
logger.error("Loading rule file {} failed".format(f))
def process_alert(self, alert_config):
# Set instance variable to populate event attributes about an alert
self.custom_alert_name = "{0}:{1}".format(self.classname(), alert_config['custom_alert_name'])
search_query = SearchQuery(minutes=int(alert_config.time_window))
terms = []
for i in alert_config.filters:
terms.append(TermMatch(i[0], i[1]))
terms.append(QueryStringMatch(str(alert_config.search_string)))
search_query.add_must(terms)
self.filtersManual(search_query)
self.searchEventsAggregated(alert_config.aggregation_key, samplesLimit=int(alert_config.num_samples))
self.walkAggregations(threshold=int(alert_config.num_aggregations), config=alert_config)
def main(self):
self.parse_config('generic_alert_loader.conf', ['alert_data_location'])
self.load_configs()
for cfg in self.configs:
try:
self.process_alert(cfg)
except Exception:
traceback.print_exc(file=sys.stdout)
logger.error("Processing rule file {} failed".format(cfg.__str__()))
def onAggregation(self, aggreg):
# aggreg['count']: number of items in the aggregation, ex: number of failed login attempts
# aggreg['value']: value of the aggregation field, ex: toto@example.com
# aggreg['events']: list of events in the aggregation
category = aggreg['config']['alert_category']
tags = aggreg['config']['alert_tags']
severity = aggreg['config']['alert_severity']
url = aggreg['config']['alert_url']
# Find all affected hosts
# Normally, the hostname data is in e.hostname so try that first,
# but fall back to e.hostname if it is missing, or nothing at all if there's no hostname! ;-)
hostnames = []
for e in aggreg['events']:
event_source = e['_source']
if 'hostname' in event_source:
hostnames.append(event_source['hostname'])
summary = '{} ({}): {}'.format(
aggreg['config']['alert_summary'],
aggreg['count'],
aggreg['value'],
)
if hostnames:
summary += ' [{}]'.format(', '.join(set(hostnames)))
return self.createAlertDict(summary, category, tags, aggreg['events'], severity, url)