django-nativemojo 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
from urllib.parse import urlparse, parse_qs
|
4
|
+
from mojo.apps.incident.handlers import TaskHandler, EmailHandler, NotifyHandler
|
5
|
+
import re
|
6
|
+
|
7
|
+
|
8
|
+
class RuleSet(models.Model, MojoModel):
|
9
|
+
"""
|
10
|
+
A RuleSet represents a collection of rules that are applied to events.
|
11
|
+
This model supports categorizing and prioritizing sets of rules to be checked against events.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
created (datetime): The timestamp when the RuleSet was created.
|
15
|
+
modified (datetime): The timestamp when the RuleSet was last modified.
|
16
|
+
priority (int): The priority of the RuleSet. Lower numbers indicate higher priority.
|
17
|
+
category (str): The category to which this RuleSet belongs.
|
18
|
+
name (str): The name of the RuleSet.
|
19
|
+
bundle (int): Indicator of whether events should be bundled. 0 means bundling is off.
|
20
|
+
bundle_by (int): Defines how events are bundled.
|
21
|
+
0=none, 1=hostname, 2=model, 3=model and hostname.
|
22
|
+
match_by (int): Defines the matching behavior for events.
|
23
|
+
0 for all rules must match, 1 for any rule can match.
|
24
|
+
handler (str): A field specifying a chain of handlers to process the event,
|
25
|
+
formatted as URL-like strings, e.g.,
|
26
|
+
task://handler_name?param1=value1¶m2=value2.
|
27
|
+
Handlers are separated by commas, and they can include
|
28
|
+
different schemes like email or notify.
|
29
|
+
metadata (json): A JSON field to store additional metadata about the RuleSet.
|
30
|
+
"""
|
31
|
+
|
32
|
+
created = models.DateTimeField(auto_now_add=True)
|
33
|
+
modified = models.DateTimeField(auto_now=True)
|
34
|
+
priority = models.IntegerField(default=0, db_index=True)
|
35
|
+
category = models.CharField(max_length=124, db_index=True)
|
36
|
+
name = models.TextField(default=None, null=True)
|
37
|
+
bundle_minutes = models.IntegerField(default=0) # 0=off
|
38
|
+
# 0=none, 1=hostname, 2=model_name, 3=model_name and model_id, 4=source_ip
|
39
|
+
# 5=hostname and model_name, 6=hostname and model_name and model_id,
|
40
|
+
# 7=source_ip and model_name, 8=source_ip and model_name and model_id
|
41
|
+
# 9=source_ip and hostname
|
42
|
+
bundle_by = models.IntegerField(default=3)
|
43
|
+
match_by = models.IntegerField(default=0) # 0=all, 1=any
|
44
|
+
# handler syntax is a url like string that can be chained by commas
|
45
|
+
# task://handler_name?param1=value1¶m2=value2
|
46
|
+
# email://user@example.com
|
47
|
+
# notify://perm@permission,user@example.com,task://handler_name?param1=value1¶m2=value2
|
48
|
+
handler = models.TextField(default=None, null=True)
|
49
|
+
metadata = models.JSONField(default=dict, blank=True)
|
50
|
+
|
51
|
+
class RestMeta:
|
52
|
+
SEARCH_FIELDS = ["name"]
|
53
|
+
VIEW_PERMS = ["view_incidents"]
|
54
|
+
CREATE_PERMS = None
|
55
|
+
|
56
|
+
def run_handler(self, event, incident=None):
|
57
|
+
"""
|
58
|
+
Runs the handler for this rule.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
event (Event): The event to run the handler for.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
bool: True if the handler was successfully run, False otherwise.
|
65
|
+
"""
|
66
|
+
if not self.handler:
|
67
|
+
return False
|
68
|
+
try:
|
69
|
+
handler_url = urlparse(self.handler)
|
70
|
+
handler_type = handler_url.scheme
|
71
|
+
handler_params = parse_qs(handler_url.query)
|
72
|
+
if handler_type == "task":
|
73
|
+
handler_name = handler_url.netloc
|
74
|
+
handler_params = {k: v[0] for k, v in handler_params.items()}
|
75
|
+
handler = TaskHandler(handler_name, **handler_params)
|
76
|
+
elif handler_type == "email":
|
77
|
+
handler = EmailHandler(handler_url.netloc)
|
78
|
+
elif handler_type == "notify":
|
79
|
+
handler = NotifyHandler(handler_url.netloc)
|
80
|
+
else:
|
81
|
+
raise ValueError(f"Unsupported handler type: {handler_type}")
|
82
|
+
return handler.run(event)
|
83
|
+
except Exception as e:
|
84
|
+
# logger.error(f"Error running handler for rule {self.id}: {e}")
|
85
|
+
return False
|
86
|
+
|
87
|
+
def check_rules(self, event):
|
88
|
+
"""
|
89
|
+
Checks if an event satisfies the rules in this RuleSet based
|
90
|
+
on the match_by configuration.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
event (Event): The event to check against the RuleSet.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
bool: True if the event matches the RuleSet, False otherwise.
|
97
|
+
"""
|
98
|
+
if self.match_by == 0:
|
99
|
+
return self.check_all_match(event)
|
100
|
+
return self.check_any_match(event)
|
101
|
+
|
102
|
+
def check_all_match(self, event):
|
103
|
+
"""
|
104
|
+
Checks if an event satisfies all rules in this RuleSet.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
event (Event): The event to check.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
bool: True if the event matches all rules, False otherwise.
|
111
|
+
"""
|
112
|
+
if not self.rules.exists():
|
113
|
+
return False
|
114
|
+
return all(rule.check_rule(event) for rule in self.rules.all())
|
115
|
+
|
116
|
+
def check_any_match(self, event):
|
117
|
+
"""
|
118
|
+
Checks if an event satisfies any rule in this RuleSet.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
event (Event): The event to check.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
bool: True if the event matches any rule, False otherwise.
|
125
|
+
"""
|
126
|
+
if not self.rules.exists():
|
127
|
+
return False
|
128
|
+
return any(rule.check_rule(event) for rule in self.rules.all())
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def check_by_category(cls, category, event):
|
132
|
+
"""
|
133
|
+
Iterates over RuleSets in a category ordered by priority, checking
|
134
|
+
if the event satisfies any of the RuleSets.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
category (str): The category of the RuleSets to check.
|
138
|
+
event (Event): The event to check.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
RuleSet: The first RuleSet that matches the event, or None if no matches are found.
|
142
|
+
"""
|
143
|
+
for rule_set in cls.objects.filter(category=category).order_by("priority"):
|
144
|
+
if rule_set.check_rules(event):
|
145
|
+
return rule_set
|
146
|
+
return None
|
147
|
+
|
148
|
+
|
149
|
+
class Rule(models.Model, MojoModel):
|
150
|
+
"""
|
151
|
+
A Rule represents a single condition that can be checked against an event.
|
152
|
+
Each rule belongs to a specific RuleSet and defines how to compare event data fields.
|
153
|
+
|
154
|
+
Attributes:
|
155
|
+
created (datetime): The timestamp when the Rule was created.
|
156
|
+
modified (datetime): The timestamp when the Rule was last modified.
|
157
|
+
parent (RuleSet): The RuleSet to which this Rule belongs.
|
158
|
+
name (str): The name of the Rule.
|
159
|
+
index (int): The order in which this Rule should be checked within its RuleSet.
|
160
|
+
comparator (str): The operation used to compare the event field value with a target value.
|
161
|
+
field_name (str): The name of the field in the event to check against.
|
162
|
+
value (str): The target value to compare the event field value with.
|
163
|
+
value_type (str): The type of the target value (e.g., int, float).
|
164
|
+
is_required (int): Indicates if this Rule is mandatory for an event to match. 0=no, 1=yes.
|
165
|
+
"""
|
166
|
+
|
167
|
+
created = models.DateTimeField(auto_now_add=True)
|
168
|
+
modified = models.DateTimeField(auto_now=True)
|
169
|
+
parent = models.ForeignKey(RuleSet, on_delete=models.CASCADE, related_name="rules")
|
170
|
+
name = models.TextField(default=None, null=True)
|
171
|
+
index = models.IntegerField(default=0, db_index=True)
|
172
|
+
comparator = models.CharField(max_length=32, default="==")
|
173
|
+
field_name = models.CharField(max_length=124, default=None, null=True)
|
174
|
+
value = models.CharField(max_length=124, default="")
|
175
|
+
value_type = models.CharField(max_length=10, default="int")
|
176
|
+
is_required = models.IntegerField(default=0) # 0=no 1=yes
|
177
|
+
|
178
|
+
class RestMeta:
|
179
|
+
SEARCH_FIELDS = ["details"]
|
180
|
+
VIEW_PERMS = ["view_incidents"]
|
181
|
+
CREATE_PERMS = ["manage_incidents"]
|
182
|
+
|
183
|
+
def check_rule(self, event):
|
184
|
+
"""
|
185
|
+
Checks if a field in the event matches the criteria defined in this Rule.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
event (Event): The event to check.
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
bool: True if the event field matches the criteria, False otherwise.
|
192
|
+
"""
|
193
|
+
field_value = event.metadata.get(self.field_name, None)
|
194
|
+
if field_value is None:
|
195
|
+
return False
|
196
|
+
|
197
|
+
comp_value = self.value
|
198
|
+
field_value, comp_value = self._convert_values(field_value, comp_value)
|
199
|
+
|
200
|
+
if field_value is None or comp_value is None:
|
201
|
+
return False
|
202
|
+
|
203
|
+
return self._compare(field_value, comp_value)
|
204
|
+
|
205
|
+
def _convert_values(self, field_value, comp_value):
|
206
|
+
"""
|
207
|
+
Converts the field and comparison values to the appropriate types.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
field_value: The value from the event to be converted.
|
211
|
+
comp_value: The value defined in the Rule for comparison.
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
tuple: A tuple containing the converted field and comparison values.
|
215
|
+
"""
|
216
|
+
if self.comparator != "contains":
|
217
|
+
try:
|
218
|
+
if self.value_type == "int":
|
219
|
+
return int(field_value), int(comp_value)
|
220
|
+
elif self.value_type == "float":
|
221
|
+
return float(field_value), float(comp_value)
|
222
|
+
except ValueError:
|
223
|
+
return None, None
|
224
|
+
return field_value, comp_value
|
225
|
+
|
226
|
+
def _compare(self, field_value, comp_value):
|
227
|
+
"""
|
228
|
+
Compares the field value to the comparison value using the specified comparator.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
field_value: The value from the event to compare.
|
232
|
+
comp_value: The target value defined in the Rule for comparison.
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
bool: True if the comparison is successful, False otherwise.
|
236
|
+
"""
|
237
|
+
comparators = {
|
238
|
+
"==": field_value == comp_value,
|
239
|
+
"eq": field_value == comp_value,
|
240
|
+
">": field_value > comp_value,
|
241
|
+
">=": field_value >= comp_value,
|
242
|
+
"<": field_value < comp_value,
|
243
|
+
"<=": field_value <= comp_value,
|
244
|
+
"contains": str(comp_value) in str(field_value),
|
245
|
+
"regex": re.search(str(comp_value), str(field_value), re.IGNORECASE) is not None,
|
246
|
+
}
|
247
|
+
return comparators.get(self.comparator, False)
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
from .core import parse_incoming_alert as parse
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# parser_core.py
|
2
|
+
from . import rules, utils
|
3
|
+
from .parsed import ParsedAlert
|
4
|
+
from objict import objict
|
5
|
+
|
6
|
+
def parse_incoming_alert(data):
|
7
|
+
if "batch" in data:
|
8
|
+
alerts = []
|
9
|
+
for item in data["batch"]:
|
10
|
+
alerts.append(parse_incoming_alert(item))
|
11
|
+
return alerts
|
12
|
+
|
13
|
+
alert = ParsedAlert(parse_alert_json(data))
|
14
|
+
if utils.ignore_alert(alert):
|
15
|
+
return None
|
16
|
+
|
17
|
+
details = utils.parse_rule_details(alert.text)
|
18
|
+
for key, value in details.items():
|
19
|
+
if key not in alert:
|
20
|
+
alert[key] = value
|
21
|
+
alert.update(details)
|
22
|
+
if not alert.title:
|
23
|
+
return None
|
24
|
+
|
25
|
+
alert.update(parse_alert_metadata(alert))
|
26
|
+
alert.normalize_fields()
|
27
|
+
|
28
|
+
update_by_rule(alert)
|
29
|
+
|
30
|
+
return alert
|
31
|
+
|
32
|
+
|
33
|
+
def parse_alert_metadata(alert):
|
34
|
+
rule_id = str(alert.rule_id)
|
35
|
+
|
36
|
+
# Try exact match first: parse_rule_2501
|
37
|
+
func_name = f"parse_rule_{rule_id}"
|
38
|
+
if hasattr(rules, func_name):
|
39
|
+
return getattr(rules, func_name)(alert)
|
40
|
+
|
41
|
+
# Optional: try prefix-based match
|
42
|
+
for i in range(len(rule_id), 1, -1): # e.g., 31151 → 3115 → 311 → 31
|
43
|
+
fallback_func = f"parse_rule_{rule_id[:i]}_default"
|
44
|
+
if hasattr(rules, fallback_func):
|
45
|
+
return getattr(rules, fallback_func)(alert)
|
46
|
+
|
47
|
+
# Fallback to generic matching
|
48
|
+
return utils.match_patterns(utils.DEFAULT_META_PATTERNS, alert.text)
|
49
|
+
|
50
|
+
|
51
|
+
def update_by_rule(alert, geoip=None):
|
52
|
+
rule_id = str(alert.rule_id)
|
53
|
+
func_name = f"update_rule_{rule_id}"
|
54
|
+
if hasattr(rules, func_name):
|
55
|
+
getattr(rules, func_name)(alert, geoip)
|
56
|
+
return
|
57
|
+
|
58
|
+
for i in range(len(rule_id), 1, -1):
|
59
|
+
fallback_func = f"update_rule_{rule_id[:i]}_default"
|
60
|
+
if hasattr(rules, fallback_func):
|
61
|
+
getattr(rules, fallback_func)(alert, geoip)
|
62
|
+
return
|
63
|
+
|
64
|
+
if hasattr(alert, 'source_ip') and alert.source_ip and alert.source_ip not in getattr(alert, 'title', ''):
|
65
|
+
alert.title = f"{alert.title} Source IP: {alert.source_ip}"
|
66
|
+
|
67
|
+
if hasattr(alert, 'title'):
|
68
|
+
alert.truncate('title')
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
def parse_alert_json(data):
|
73
|
+
if isinstance(data, str):
|
74
|
+
data = objict.from_json(utils.remove_non_ascii(data.replace('\n', '\\n')))
|
75
|
+
|
76
|
+
for key in data:
|
77
|
+
if isinstance(data[key], str):
|
78
|
+
data[key] = data[key].strip() # .replace('\\/', '/')
|
79
|
+
|
80
|
+
if hasattr(data, 'text'):
|
81
|
+
data.text = utils.remove_non_ascii(data.text) # .replace('\\/', '/')
|
82
|
+
return data
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# parsed_alert.py
|
2
|
+
|
3
|
+
from objict import objict
|
4
|
+
|
5
|
+
class ParsedAlert(objict):
|
6
|
+
def normalize_fields(self):
|
7
|
+
if self.source_ip in [None, "-"] and hasattr(self, "client"):
|
8
|
+
self.source_ip = self.client
|
9
|
+
if not hasattr(self, "ext_ip") or self.ext_ip in [None, "-"]:
|
10
|
+
self.ext_ip = self.source_ip
|
11
|
+
|
12
|
+
def truncate(self, field, max_len=199):
|
13
|
+
value = getattr(self, field, "")
|
14
|
+
if isinstance(value, str) and len(value) > max_len:
|
15
|
+
value = value[:max_len]
|
16
|
+
value = value[:value.rfind(' ')] + "..."
|
17
|
+
setattr(self, field, value)
|
18
|
+
|
19
|
+
def truncate_str(self, text, max_len=199):
|
20
|
+
if len(text) > max_len:
|
21
|
+
text = text[:max_len]
|
22
|
+
text = text[:text.rfind(' ')] + "..."
|
23
|
+
return text
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import re
|
2
|
+
from .utils import parse_nginx_line, match_patterns, DEFAULT_META_PATTERNS
|
3
|
+
|
4
|
+
# ---- Rule-specific handlers ----
|
5
|
+
|
6
|
+
def parse_rule_2501(alert):
|
7
|
+
match = re.search(r'user (\w+) (\d+\.\d+\.\d+\.\d+) port (\d+)', alert.text)
|
8
|
+
if match:
|
9
|
+
return dict(username=match.group(1), source_ip=match.group(2), source_port=match.group(3))
|
10
|
+
return {}
|
11
|
+
|
12
|
+
def parse_rule_31301(alert):
|
13
|
+
match = re.search(r'\((?P<error_code>\d+): (?P<error_message>.*?)\)', alert.text)
|
14
|
+
data = match_patterns(DEFAULT_META_PATTERNS, alert.text)
|
15
|
+
data["action"] = "error"
|
16
|
+
if match:
|
17
|
+
data.update(match.groupdict())
|
18
|
+
return data
|
19
|
+
|
20
|
+
def parse_rule_31302(alert):
|
21
|
+
return _parse_nginx_error(alert)
|
22
|
+
|
23
|
+
def parse_rule_31303(alert):
|
24
|
+
return _parse_nginx_error(alert)
|
25
|
+
|
26
|
+
def _parse_nginx_error(alert):
|
27
|
+
match = re.search(r'\[(warn|crit|error)\].*?: (.*?),', alert.text)
|
28
|
+
data = match_patterns(DEFAULT_META_PATTERNS, alert.text)
|
29
|
+
if match:
|
30
|
+
data["action"] = match.group(1)
|
31
|
+
emsg = match.group(2)
|
32
|
+
if emsg.startswith("*"):
|
33
|
+
emsg = emsg.split(" ", 1)[-1]
|
34
|
+
data["error_message"] = emsg
|
35
|
+
return data
|
36
|
+
|
37
|
+
def parse_rule_551(alert):
|
38
|
+
match = re.search(r"Integrity checksum changed for: '(\S+)'", alert.text)
|
39
|
+
if match:
|
40
|
+
return dict(filename=match.group(1), action="changed")
|
41
|
+
return {}
|
42
|
+
|
43
|
+
def parse_rule_554(alert):
|
44
|
+
match = re.search(r"New file '(\S+)' added", alert.text)
|
45
|
+
if match:
|
46
|
+
return dict(filename=match.group(1), action="added")
|
47
|
+
return {}
|
48
|
+
|
49
|
+
def parse_rule_5402(alert):
|
50
|
+
match = re.search(r'(?P<username>[\w-]+) : PWD=(?P<pwd>\S+) ; USER=(?P<user>\w+) ; COMMAND=(?P<command>.+)', alert.text)
|
51
|
+
if match:
|
52
|
+
return match.groupdict()
|
53
|
+
match = re.search(r'(?P<username>[\w-]+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>\S+) ; USER=(?P<user>\w+) ; COMMAND=(?P<command>.+)', alert.text)
|
54
|
+
if match:
|
55
|
+
return match.groupdict()
|
56
|
+
return {}
|
57
|
+
|
58
|
+
def parse_rule_5501(alert):
|
59
|
+
return _parse_session_action(alert)
|
60
|
+
|
61
|
+
def parse_rule_5502(alert):
|
62
|
+
return _parse_session_action(alert)
|
63
|
+
|
64
|
+
def _parse_session_action(alert):
|
65
|
+
match = re.search(r"session (?P<action>\S+) for user (?P<username>\S+)*", alert.text)
|
66
|
+
if match:
|
67
|
+
return match.groupdict()
|
68
|
+
return {}
|
69
|
+
|
70
|
+
def parse_rule_5704(alert):
|
71
|
+
return _parse_src_ip_port(alert)
|
72
|
+
|
73
|
+
def parse_rule_5705(alert):
|
74
|
+
return _parse_src_ip_port(alert)
|
75
|
+
|
76
|
+
def _parse_src_ip_port(alert):
|
77
|
+
match = re.search(r"(?P<source_ip>\d{1,3}(?:\.\d{1,3}){3}) port (?P<source_port>\d+)", alert.text)
|
78
|
+
if match:
|
79
|
+
return match.groupdict()
|
80
|
+
return {}
|
81
|
+
|
82
|
+
def parse_rule_5715(alert):
|
83
|
+
match = re.search(r'Accepted publickey for (?P<username>\S+) from (?P<source_ip>\d+\.\d+\.\d+\.\d+) .*: (?P<ssh_key_type>\S+) (?P<ssh_signature>\S+)', alert.text)
|
84
|
+
if match:
|
85
|
+
return match.groupdict()
|
86
|
+
return {}
|
87
|
+
|
88
|
+
def parse_rule_2932(alert):
|
89
|
+
match = re.search(r"Installed: (\S+)", alert.text)
|
90
|
+
if match:
|
91
|
+
return dict(package=match.group(1))
|
92
|
+
return {}
|
93
|
+
|
94
|
+
# ---- Prefix-based default handlers ----
|
95
|
+
|
96
|
+
def parse_rule_311_default(alert):
|
97
|
+
data = parse_nginx_line(alert.text)
|
98
|
+
return data if data else {}
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
def update_rule_2501(alert, geoip=None):
|
103
|
+
alert.title = f"SSH Auth Attempt {alert.username}@{alert.hostname} from {alert.src_ip}"
|
104
|
+
|
105
|
+
def update_rule_2503(alert, geoip=None):
|
106
|
+
alert.title = f"SSH Auth Blocked from {alert.source_ip}"
|
107
|
+
|
108
|
+
def update_rule_31101(alert, geoip=None):
|
109
|
+
alert.title = f"Web {alert.http_status} {alert.http_method} {alert.http_url} from {alert.src_ip}"
|
110
|
+
|
111
|
+
def update_rule_31104(alert, geoip=None):
|
112
|
+
alert.title = f"Web Attack {alert.http_status} {alert.http_method} {alert.http_url} from {alert.src_ip}"
|
113
|
+
|
114
|
+
def update_rule_31111(alert, geoip=None):
|
115
|
+
if geoip and geoip.isp:
|
116
|
+
alert.title = f"No referrer for .js - {alert.http_status} {alert.http_method} {alert.http_url} from {alert.src_ip}({geoip.isp})"
|
117
|
+
else:
|
118
|
+
alert.title = f"No referrer for .js - {alert.http_status} {alert.http_method} {alert.http_url} from {alert.src_ip}"
|
119
|
+
|
120
|
+
def update_rule_311_default(alert, geoip=None):
|
121
|
+
if not alert.http_status:
|
122
|
+
return
|
123
|
+
url = alert.truncate_str(alert.http_url, 50)
|
124
|
+
alert.title = f"Web {alert.http_status} {alert.http_method} {url} from {alert.src_ip}"
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import re
|
2
|
+
from objict import objict
|
3
|
+
|
4
|
+
# -------------------------------
|
5
|
+
# JSON Parsing & Normalization
|
6
|
+
# -------------------------------
|
7
|
+
|
8
|
+
def remove_non_ascii(input_str, replacement=''):
|
9
|
+
"""
|
10
|
+
Replace all non-ASCII characters and escaped byte sequences with a replacement.
|
11
|
+
"""
|
12
|
+
# Replace escaped byte sequences like \xHH
|
13
|
+
cleaned_str = re.sub(r'\\x[0-9a-fA-F]{2}', replacement, input_str)
|
14
|
+
return ''.join(
|
15
|
+
char if (32 <= ord(char) < 128 or char in '\n\r\t')
|
16
|
+
else f"<r{ord(char)}>"
|
17
|
+
for char in cleaned_str
|
18
|
+
)
|
19
|
+
|
20
|
+
def parse_alert_json(data):
|
21
|
+
"""
|
22
|
+
Convert JSON (string or obj) into objict, removing non-ASCII content and normalizing fields.
|
23
|
+
"""
|
24
|
+
try:
|
25
|
+
if isinstance(data, str):
|
26
|
+
data = objict.from_json(remove_non_ascii(data.replace('\n', '\\n')))
|
27
|
+
except Exception:
|
28
|
+
data = objict.from_json(remove_non_ascii(data))
|
29
|
+
|
30
|
+
for key in data:
|
31
|
+
if isinstance(data[key], str):
|
32
|
+
data[key] = data[key].strip()
|
33
|
+
|
34
|
+
if hasattr(data, 'text'):
|
35
|
+
data.text = remove_non_ascii(data.text)
|
36
|
+
|
37
|
+
return data
|
38
|
+
|
39
|
+
# -------------------------------
|
40
|
+
# Rule Details Parsing
|
41
|
+
# -------------------------------
|
42
|
+
|
43
|
+
def parse_alert_id(details):
|
44
|
+
"""
|
45
|
+
Extract the alert ID (float string) from a log message.
|
46
|
+
"""
|
47
|
+
match = re.search(r"Alert (\d+\.\d+):", details)
|
48
|
+
return match.group(1) if match else ""
|
49
|
+
|
50
|
+
def parse_rule_details(details):
|
51
|
+
"""
|
52
|
+
Extract rule_id, level, and title from the Rule line in OSSEC logs.
|
53
|
+
"""
|
54
|
+
# alert_id = parse_alert_id(details)
|
55
|
+
rule_pattern = r"Rule: (\d+) \(level (\d+)\) -> '([^']+)'"
|
56
|
+
match = re.search(rule_pattern, details)
|
57
|
+
if match:
|
58
|
+
return objict(
|
59
|
+
rule_id=int(match.group(1)),
|
60
|
+
level=int(match.group(2)),
|
61
|
+
title=match.group(3)
|
62
|
+
)
|
63
|
+
return objict()
|
64
|
+
|
65
|
+
# -------------------------------
|
66
|
+
# Generic Pattern Matching
|
67
|
+
# -------------------------------
|
68
|
+
|
69
|
+
DEFAULT_META_PATTERNS = {
|
70
|
+
"source_ip": re.compile(r"Src IP: (\S+)"),
|
71
|
+
"source_port": re.compile(r"Src Port: (\S+)"),
|
72
|
+
"user": re.compile(r"User: (\S+)"),
|
73
|
+
"http_path": re.compile(r"request: (\S+ \S+)"),
|
74
|
+
"http_server": re.compile(r"server: (\S+),"),
|
75
|
+
"http_host": re.compile(r"host: (\S+)"),
|
76
|
+
"http_referrer": re.compile(r"referrer: (\S+),"),
|
77
|
+
"client": re.compile(r"client: (\S+),"),
|
78
|
+
"upstream": re.compile(r"upstream: (\S+),")
|
79
|
+
}
|
80
|
+
|
81
|
+
def match_patterns(patterns, text):
|
82
|
+
"""
|
83
|
+
Apply regex patterns to a given string and return matched named groups.
|
84
|
+
"""
|
85
|
+
return {
|
86
|
+
key: match.group(1)
|
87
|
+
for key, pattern in patterns.items()
|
88
|
+
if (match := pattern.search(text))
|
89
|
+
}
|
90
|
+
|
91
|
+
# -------------------------------
|
92
|
+
# NGINX Log Parsing
|
93
|
+
# -------------------------------
|
94
|
+
|
95
|
+
NGINX_PARSE_PATTERN = re.compile(
|
96
|
+
r'(?P<source_ip>\d+\.\d+\.\d+\.\d+) - - \[(?P<http_time>.+?)\] '
|
97
|
+
r'(?P<http_method>\w+) (?P<http_url>.+?) (?P<http_protocol>[\w/.]+) '
|
98
|
+
r'(?P<http_status>\d+) (?P<http_bytes>\d+) (?P<http_referer>.+?) '
|
99
|
+
r'(?P<user_agent>.+?) (?P<http_elapsed>\d\.\d{3})'
|
100
|
+
)
|
101
|
+
|
102
|
+
def parse_nginx_line(line):
|
103
|
+
"""
|
104
|
+
Attempt to parse an NGINX access log line into a structured dict.
|
105
|
+
Supports multiline input by splitting and scanning each.
|
106
|
+
"""
|
107
|
+
lines = line.splitlines()
|
108
|
+
for l in lines:
|
109
|
+
match = NGINX_PARSE_PATTERN.match(l)
|
110
|
+
if match:
|
111
|
+
return match.groupdict()
|
112
|
+
return None
|
113
|
+
|
114
|
+
def extract_url(text):
|
115
|
+
"""
|
116
|
+
Extracts a full URL from a line that includes an HTTP method and version.
|
117
|
+
Example match: 'GET https://example.com/path HTTP/1.1'
|
118
|
+
"""
|
119
|
+
match = re.search(r"(GET|POST|DELETE|PUT)\s+(https?://[^\s]+)\s+HTTP/\d\.\d", text)
|
120
|
+
return match.group(2) if match else None
|
121
|
+
|
122
|
+
def extract_domain(text):
|
123
|
+
"""
|
124
|
+
Extracts the domain from a full URL (e.g. 'https://example.com/path' → 'example.com').
|
125
|
+
"""
|
126
|
+
match = re.search(r"https?://([^/:]+)", text)
|
127
|
+
return match.group(1) if match else None
|
128
|
+
|
129
|
+
def extract_ip(text):
|
130
|
+
"""
|
131
|
+
Extracts the first IPv4 address found in a string.
|
132
|
+
"""
|
133
|
+
match = re.search(r"\b((?:[0-9]{1,3}\.){3}[0-9]{1,3})\b", text)
|
134
|
+
return match.group(1) if match else None
|
135
|
+
|
136
|
+
def extract_url_path(text):
|
137
|
+
"""
|
138
|
+
Extracts the path component from a URL (e.g. 'https://example.com/foo/bar?q=1' → '/foo/bar').
|
139
|
+
"""
|
140
|
+
match = re.search(r"https?://[^/]+(/[^?]*)", text)
|
141
|
+
return match.group(1) if match else None
|
142
|
+
|
143
|
+
def extract_user_agent(text):
|
144
|
+
"""
|
145
|
+
Attempts to extract a user agent string following a NGINX-style referrer '-' marker.
|
146
|
+
Only works for logs like: '... '-' <user agent> 1.234 200'
|
147
|
+
"""
|
148
|
+
match = re.search(r"' - (.+?) \d+\.\d+ \d+'", text)
|
149
|
+
return match.group(1) if match else None
|
150
|
+
|
151
|
+
|
152
|
+
IGNORE_RULES = {
|
153
|
+
"100020",
|
154
|
+
}
|
155
|
+
|
156
|
+
def ignore_alert(alert):
|
157
|
+
"""
|
158
|
+
Returns True if this alert should be ignored based on known rule exclusions
|
159
|
+
or content patterns.
|
160
|
+
"""
|
161
|
+
rule_id = str(alert.rule_id)
|
162
|
+
|
163
|
+
if rule_id in IGNORE_RULES:
|
164
|
+
return True
|
165
|
+
|
166
|
+
if rule_id == "510" and "/dev/.mount/utab" in alert.text:
|
167
|
+
return True
|
168
|
+
|
169
|
+
return False
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
def report_event(details, title=None, category="api_error", level=1, request=None, **kwargs):
|
3
|
+
from .models import Event
|
4
|
+
event_data = _create_event_dict(details, title, category, level, request, **kwargs)
|
5
|
+
event = Event(**event_data)
|
6
|
+
event.sync_metadata()
|
7
|
+
event.save()
|
8
|
+
event.publish()
|
9
|
+
|
10
|
+
|
11
|
+
def _create_event_dict(details, title=None, category="api_error", level=1, request=None, **kwargs):
|
12
|
+
if title is None:
|
13
|
+
title = details[:50]
|
14
|
+
|
15
|
+
event_data = {
|
16
|
+
"details": details,
|
17
|
+
"title": title,
|
18
|
+
"category": category,
|
19
|
+
"level": level,
|
20
|
+
"hostname": kwargs.pop("hostname", None),
|
21
|
+
"model_name": kwargs.pop("model_name", None),
|
22
|
+
"model_id": kwargs.pop("model_id", None),
|
23
|
+
"source_ip": kwargs.pop("source_ip", None)
|
24
|
+
}
|
25
|
+
|
26
|
+
event_metadata = {}
|
27
|
+
|
28
|
+
if request:
|
29
|
+
event_data["source_ip"] = request.ip if event_data["source_ip"] is None else event_data["source_ip"]
|
30
|
+
event_metadata.update({
|
31
|
+
"request_ip": request.ip,
|
32
|
+
"http_path": request.path,
|
33
|
+
"http_protocol": request.META.get("SERVER_PROTOCOL", ""),
|
34
|
+
"http_method": request.method,
|
35
|
+
"http_query_string": request.META.get("QUERY_STRING", ""),
|
36
|
+
"http_user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
37
|
+
"http_host": request.META.get("HTTP_HOST", "")
|
38
|
+
})
|
39
|
+
|
40
|
+
event_metadata.update({k: v for k, v in kwargs.items() if k not in event_data})
|
41
|
+
event_data['metadata'] = event_metadata
|
42
|
+
return event_data
|