detect-droid 0.3.3__tar.gz → 0.3.4__tar.gz
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.
- {detect_droid-0.3.3/src/detect_droid.egg-info → detect_droid-0.3.4}/PKG-INFO +1 -1
- {detect_droid-0.3.3 → detect_droid-0.3.4}/setup.cfg +1 -1
- {detect_droid-0.3.3 → detect_droid-0.3.4/src/detect_droid.egg-info}/PKG-INFO +1 -1
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/detect_droid.egg-info/SOURCES.txt +3 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/convert.py +2 -1
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/integrity.py +14 -7
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/common.py +22 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/ms_xdr.py +37 -43
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/sentinel.py +8 -8
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/search.py +16 -8
- detect_droid-0.3.4/tests/test_export_error_propagation.py +142 -0
- detect_droid-0.3.4/tests/test_integrity.py +316 -0
- detect_droid-0.3.4/tests/test_search_logging.py +424 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/LICENSE +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/NOTICE +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/README.md +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/setup.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/detect_droid.egg-info/dependency_links.txt +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/detect_droid.egg-info/entry_points.txt +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/detect_droid.egg-info/requires.txt +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/detect_droid.egg-info/top_level.txt +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/__main__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/abstracts.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/color.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/convert.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/export.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/integrity.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/list_cmd.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/search.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/update.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/rules/validate.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/sources/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/commands/sources/update.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/config.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/export.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/list.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/elastic.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/registry.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/platforms/splunk.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/registry.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/rule_loader.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/sources/__init__.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/sources/sigmahq.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/src/droid/validate.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_convert.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_export.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_json_logging.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_platforms_common.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_token_hook.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_validate.py +0 -0
- {detect_droid-0.3.3 → detect_droid-0.3.4}/tests/test_version.py +0 -0
|
@@ -44,8 +44,11 @@ src/droid/sources/__init__.py
|
|
|
44
44
|
src/droid/sources/sigmahq.py
|
|
45
45
|
tests/test_convert.py
|
|
46
46
|
tests/test_export.py
|
|
47
|
+
tests/test_export_error_propagation.py
|
|
48
|
+
tests/test_integrity.py
|
|
47
49
|
tests/test_json_logging.py
|
|
48
50
|
tests/test_platforms_common.py
|
|
51
|
+
tests/test_search_logging.py
|
|
49
52
|
tests/test_token_hook.py
|
|
50
53
|
tests/test_validate.py
|
|
51
54
|
tests/test_version.py
|
|
@@ -489,8 +489,9 @@ def convert_sigma(
|
|
|
489
489
|
if parameters.export and parameters.search and rule_converted:
|
|
490
490
|
try:
|
|
491
491
|
error, search_warning = search_rule(parameters, rule_content, rule_converted, platform, rule_file, error, search_warning, logger_param)
|
|
492
|
-
except:
|
|
492
|
+
except Exception as e:
|
|
493
493
|
logger.error(f"Could not export the rule {rule_file} since the search ran into error.", extra={"rule_file": rule_file, "error": e, "rule_content": rule_content})
|
|
494
|
+
error = True
|
|
494
495
|
|
|
495
496
|
if not error:
|
|
496
497
|
error = False
|
|
@@ -29,7 +29,7 @@ def load_rule(rule_file):
|
|
|
29
29
|
print("Error reading {0}".format(rule_file))
|
|
30
30
|
return True
|
|
31
31
|
|
|
32
|
-
def check_rule_removed(rule_content, rule_file, saved_search, logger, error):
|
|
32
|
+
def check_rule_removed(rule_content, rule_file, saved_search, logger, error, tolerate_deletion_delay=False):
|
|
33
33
|
is_removed = rule_content.get("custom", {}).get("removed")
|
|
34
34
|
|
|
35
35
|
if not saved_search:
|
|
@@ -40,6 +40,13 @@ def check_rule_removed(rule_content, rule_file, saved_search, logger, error):
|
|
|
40
40
|
logger.error(f"Rule not found {rule_file}")
|
|
41
41
|
error = True
|
|
42
42
|
return error
|
|
43
|
+
|
|
44
|
+
if is_removed and tolerate_deletion_delay:
|
|
45
|
+
# The platform can take a while to actually drop the rule from its
|
|
46
|
+
# backend, comparing the content of a rule meant to be gone is pointless
|
|
47
|
+
logger.warning(f"The rule {rule_file} is marked as removed but is still present on the platform - skipping the integrity check")
|
|
48
|
+
return False
|
|
49
|
+
|
|
43
50
|
return None
|
|
44
51
|
|
|
45
52
|
def integrity_rule_splunk(rule_converted, rule_content, platform: SplunkPlatform, rule_file, parameters, logger, error):
|
|
@@ -264,9 +271,10 @@ def integrity_rule_ms_xdr_mssp(rule_converted, rule_content, platform: Microsoft
|
|
|
264
271
|
logger.error(f"Couldn't check the integrity for the rule {rule_file} on tenant {tenant_id} from {group} - error {e}")
|
|
265
272
|
return error
|
|
266
273
|
|
|
267
|
-
|
|
268
|
-
if
|
|
269
|
-
|
|
274
|
+
removed_check = check_rule_removed(rule_content, rule_file, saved_search, logger, error, tolerate_deletion_delay=True)
|
|
275
|
+
if removed_check is not None:
|
|
276
|
+
if removed_check:
|
|
277
|
+
error_occured = True
|
|
270
278
|
continue
|
|
271
279
|
|
|
272
280
|
error = integrity_rule_ms_xdr(customer_rule_converted, rule_content, platform, rule_file, parameters, logger, error, saved_search=saved_search)
|
|
@@ -274,8 +282,7 @@ def integrity_rule_ms_xdr_mssp(rule_converted, rule_content, platform: Microsoft
|
|
|
274
282
|
if error:
|
|
275
283
|
error_occured = True
|
|
276
284
|
|
|
277
|
-
|
|
278
|
-
return error
|
|
285
|
+
return error_occured
|
|
279
286
|
|
|
280
287
|
def integrity_rule_ms_xdr(rule_converted, rule_content, platform: MicrosoftXDRPlatform, rule_file, parameters, logger, error, saved_search=None):
|
|
281
288
|
try:
|
|
@@ -285,7 +292,7 @@ def integrity_rule_ms_xdr(rule_converted, rule_content, platform: MicrosoftXDRPl
|
|
|
285
292
|
logger.error(f"Couldn't check the integrity for the rule {rule_file} - error {e}")
|
|
286
293
|
return error
|
|
287
294
|
|
|
288
|
-
error = check_rule_removed(rule_content, rule_file, saved_search, logger, error)
|
|
295
|
+
error = check_rule_removed(rule_content, rule_file, saved_search, logger, error, tolerate_deletion_delay=True)
|
|
289
296
|
if error is not None:
|
|
290
297
|
return error
|
|
291
298
|
|
|
@@ -33,6 +33,28 @@ def get_token_hook_headers():
|
|
|
33
33
|
|
|
34
34
|
return {}
|
|
35
35
|
|
|
36
|
+
def get_error_message(response):
|
|
37
|
+
"""Extract the error message out of a platform API response
|
|
38
|
+
|
|
39
|
+
The response is the already decoded body. It may be None when the request
|
|
40
|
+
could not be performed at all, or may not carry the expected error
|
|
41
|
+
structure, hence the fallbacks.
|
|
42
|
+
|
|
43
|
+
Return: a str with the error message
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if isinstance(response, dict):
|
|
47
|
+
error = response.get('error')
|
|
48
|
+
if isinstance(error, dict):
|
|
49
|
+
return error.get('message', str(error))
|
|
50
|
+
if error:
|
|
51
|
+
return str(error)
|
|
52
|
+
|
|
53
|
+
if response is None:
|
|
54
|
+
return "No response from the platform"
|
|
55
|
+
|
|
56
|
+
return str(response)
|
|
57
|
+
|
|
36
58
|
def get_pipeline_group_match(rule_content: dict, fields: dict):
|
|
37
59
|
"""Retrieve the config group name based on a dict
|
|
38
60
|
|
|
@@ -10,10 +10,9 @@ import time
|
|
|
10
10
|
|
|
11
11
|
from os import environ
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
|
-
from pprint import pprint
|
|
14
13
|
from droid.abstracts import AbstractPlatform
|
|
15
14
|
from droid.color import ColorLogger
|
|
16
|
-
from droid.platforms.common import get_pipeline_group_match, get_token_hook_headers
|
|
15
|
+
from droid.platforms.common import get_error_message, get_pipeline_group_match, get_token_hook_headers
|
|
17
16
|
from msal import ConfidentialClientApplication
|
|
18
17
|
from azure.identity import DefaultAzureCredential
|
|
19
18
|
from cryptography import x509
|
|
@@ -162,7 +161,7 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
162
161
|
self.logger.info(f"Rule {rule_file} was successfully deleted from tenant {tenant_id}")
|
|
163
162
|
else:
|
|
164
163
|
self.logger.error(
|
|
165
|
-
f"Could not delete {rule_file} from tenant {tenant_id} - error: {response
|
|
164
|
+
f"Could not delete {rule_file} from tenant {tenant_id} - error: {get_error_message(response)}",
|
|
166
165
|
extra={
|
|
167
166
|
"rule_file": rule_file,
|
|
168
167
|
"rule_converted": rule_converted,
|
|
@@ -184,10 +183,10 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
184
183
|
)
|
|
185
184
|
error = True
|
|
186
185
|
if error:
|
|
187
|
-
raise
|
|
186
|
+
raise Exception(f"Could not remove the rule {rule_file} from one or more tenants")
|
|
188
187
|
else:
|
|
189
188
|
self.logger.error("Export list not found. Please provide the list of designated customers")
|
|
190
|
-
raise
|
|
189
|
+
raise Exception("Export list not found. Please provide the list of designated customers")
|
|
191
190
|
else:
|
|
192
191
|
existing_rule = self.get_rule(rule_content["id"])
|
|
193
192
|
tenant_id = self._tenant_id
|
|
@@ -207,14 +206,14 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
207
206
|
)
|
|
208
207
|
else:
|
|
209
208
|
self.logger.error(
|
|
210
|
-
f"Could not delete {rule_file} - error: {response
|
|
209
|
+
f"Could not delete {rule_file} - error: {get_error_message(response)}",
|
|
211
210
|
extra={
|
|
212
211
|
"rule_file": rule_file,
|
|
213
212
|
"rule_converted": rule_converted,
|
|
214
213
|
"rule_content": rule_content,
|
|
215
214
|
},
|
|
216
215
|
)
|
|
217
|
-
raise
|
|
216
|
+
raise Exception(f"Could not delete {rule_file} - error: {get_error_message(response)}")
|
|
218
217
|
else:
|
|
219
218
|
self.logger.info(
|
|
220
219
|
f"Rule {rule_file} was already removed",
|
|
@@ -473,12 +472,12 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
473
472
|
)
|
|
474
473
|
error = True
|
|
475
474
|
if error:
|
|
476
|
-
raise
|
|
475
|
+
raise Exception(f"Could not export the rule {rule_file} to one or more tenants")
|
|
477
476
|
else:
|
|
478
477
|
self.logger.error(
|
|
479
478
|
"Export list not found. Please provide the list of designated customers"
|
|
480
479
|
)
|
|
481
|
-
raise
|
|
480
|
+
raise Exception("Export list not found. Please provide the list of designated customers")
|
|
482
481
|
else:
|
|
483
482
|
try:
|
|
484
483
|
self.push_detection_rule(
|
|
@@ -574,28 +573,8 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
574
573
|
url=api_url, payload=alert_rule, tenant_id=tenant_id
|
|
575
574
|
)
|
|
576
575
|
|
|
577
|
-
if status_code
|
|
578
|
-
|
|
579
|
-
f"Could not export the rule {rule_file} due to a bad request. {response['error']['message']}",
|
|
580
|
-
extra={
|
|
581
|
-
"rule_file": rule_file,
|
|
582
|
-
"rule_converted": rule_converted,
|
|
583
|
-
"rule_content": rule_content,
|
|
584
|
-
"error": response,
|
|
585
|
-
},
|
|
586
|
-
)
|
|
587
|
-
elif status_code == 403:
|
|
588
|
-
self.logger.error(
|
|
589
|
-
f"Could not export the rule {rule_file} due to insufficient permissions. {response}",
|
|
590
|
-
extra={
|
|
591
|
-
"rule_file": rule_file,
|
|
592
|
-
"rule_converted": rule_converted,
|
|
593
|
-
"rule_content": rule_content,
|
|
594
|
-
"error": response,
|
|
595
|
-
},
|
|
596
|
-
)
|
|
597
|
-
elif status_code == 201 or 200:
|
|
598
|
-
if "error" in response:
|
|
576
|
+
if status_code in (200, 201, 204):
|
|
577
|
+
if isinstance(response, dict) and "error" in response:
|
|
599
578
|
self.logger.error(
|
|
600
579
|
f"Could not export the rule {rule_file}",
|
|
601
580
|
extra={
|
|
@@ -606,19 +585,34 @@ class MicrosoftXDRPlatform(AbstractPlatform):
|
|
|
606
585
|
},
|
|
607
586
|
)
|
|
608
587
|
raise Exception(response)
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
588
|
+
self.logger.info(
|
|
589
|
+
f"Successfully exported the rule {rule_file}",
|
|
590
|
+
extra={
|
|
591
|
+
"rule_file": rule_file,
|
|
592
|
+
"rule_converted": rule_converted,
|
|
593
|
+
"rule_content": rule_content,
|
|
594
|
+
},
|
|
595
|
+
)
|
|
596
|
+
time.sleep(6)
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
if status_code == 400:
|
|
600
|
+
reason = f"due to a bad request. {get_error_message(response)}"
|
|
601
|
+
elif status_code == 403:
|
|
602
|
+
reason = f"due to insufficient permissions. {get_error_message(response)}"
|
|
619
603
|
else:
|
|
620
|
-
|
|
621
|
-
|
|
604
|
+
reason = f"due to an unexpected status code {status_code}. {get_error_message(response)}"
|
|
605
|
+
|
|
606
|
+
self.logger.error(
|
|
607
|
+
f"Could not export the rule {rule_file} {reason}",
|
|
608
|
+
extra={
|
|
609
|
+
"rule_file": rule_file,
|
|
610
|
+
"rule_converted": rule_converted,
|
|
611
|
+
"rule_content": rule_content,
|
|
612
|
+
"error": response,
|
|
613
|
+
},
|
|
614
|
+
)
|
|
615
|
+
raise Exception(f"Could not export the rule {rule_file} {reason}")
|
|
622
616
|
|
|
623
617
|
def parse_actions(self, actions, rule_file=None):
|
|
624
618
|
# TODO: It might be better to have a schema validation for the actions
|
|
@@ -409,9 +409,9 @@ class SentinelPlatform(AbstractPlatform):
|
|
|
409
409
|
if results.status == LogsQueryStatus.SUCCESS:
|
|
410
410
|
result = sum(len(table.rows) for table in results.tables)
|
|
411
411
|
if result > 0:
|
|
412
|
-
self.logger.warning(f"(Sentinel MSSP)
|
|
412
|
+
self.logger.warning(f"(Sentinel MSSP) {result} hit(s) found for {rule_file} on customer '{customer_name or workspace_name}'")
|
|
413
413
|
else:
|
|
414
|
-
self.logger.info(f"(Sentinel MSSP) No
|
|
414
|
+
self.logger.info(f"(Sentinel MSSP) No hits for {rule_file} on customer '{customer_name or workspace_name}'")
|
|
415
415
|
total_result += result
|
|
416
416
|
elif results.status == LogsQueryStatus.PARTIAL:
|
|
417
417
|
self.logger.error(f"Rule {rule_file} partial error for {customer_name or workspace_name}: {results.partial_error}")
|
|
@@ -456,9 +456,9 @@ class SentinelPlatform(AbstractPlatform):
|
|
|
456
456
|
|
|
457
457
|
for customer, result in results.items():
|
|
458
458
|
if result > 0:
|
|
459
|
-
self.logger.warning(f"(Sentinel MSSP)
|
|
459
|
+
self.logger.warning(f"(Sentinel MSSP) {result} hit(s) found for {rule_file} on customer '{customer}'")
|
|
460
460
|
else:
|
|
461
|
-
self.logger.info(f"(Sentinel MSSP) No
|
|
461
|
+
self.logger.info(f"(Sentinel MSSP) No hits for {rule_file} on customer '{customer}'")
|
|
462
462
|
total_result += result
|
|
463
463
|
|
|
464
464
|
return total_result
|
|
@@ -594,10 +594,10 @@ class SentinelPlatform(AbstractPlatform):
|
|
|
594
594
|
self.logger.error(f"Failed to delete the rule {rule_file} from {workspace_name} - error: {e}")
|
|
595
595
|
error = True
|
|
596
596
|
if error:
|
|
597
|
-
raise
|
|
597
|
+
raise Exception(f"Could not delete the rule {rule_file} from one or more workspaces")
|
|
598
598
|
else:
|
|
599
599
|
self.logger.error("Export list not found. Please provide the list of designated customers")
|
|
600
|
-
raise
|
|
600
|
+
raise Exception("Export list not found. Please provide the list of designated customers")
|
|
601
601
|
else:
|
|
602
602
|
credential = self.get_credentials()
|
|
603
603
|
client = SecurityInsights(credential, self._subscription_id)
|
|
@@ -798,10 +798,10 @@ class SentinelPlatform(AbstractPlatform):
|
|
|
798
798
|
self.logger.error(f"Failed to export the rule {rule_file} to {workspace_name} - error: {e}")
|
|
799
799
|
error = True
|
|
800
800
|
if error:
|
|
801
|
-
raise
|
|
801
|
+
raise Exception(f"Could not export the rule {rule_file} to one or more workspaces")
|
|
802
802
|
else:
|
|
803
803
|
self.logger.error("Export list not found. Please provide the list of designated customers")
|
|
804
|
-
raise
|
|
804
|
+
raise Exception("Export list not found. Please provide the list of designated customers")
|
|
805
805
|
else:
|
|
806
806
|
credential = self.get_credentials()
|
|
807
807
|
client = SecurityInsights(credential, self._subscription_id)
|
|
@@ -35,7 +35,7 @@ def search_rule_splunk(rule_converted, platform: SplunkPlatform, rule_file, para
|
|
|
35
35
|
|
|
36
36
|
if result["resultCount"] > 0: # If the rule has match
|
|
37
37
|
job_url = result["jobUrl"]
|
|
38
|
-
logger.warning(f"(Splunk)
|
|
38
|
+
logger.warning(f"(Splunk) {result['resultCount']} hit(s) found for {rule_file} - {job_url}")
|
|
39
39
|
search_warning = True
|
|
40
40
|
return error, search_warning
|
|
41
41
|
else:
|
|
@@ -56,7 +56,7 @@ def search_rule_sentinel_mssp(rule_converted, rule_content, platform: SentinelPl
|
|
|
56
56
|
logger.info(f"Successfully searched the rule {rule_file}")
|
|
57
57
|
|
|
58
58
|
if result > 0: # If the rule has match
|
|
59
|
-
logger.warning(f"(Sentinel MSSP)
|
|
59
|
+
logger.warning(f"(Sentinel MSSP) {result} hit(s) found in total for {rule_file}")
|
|
60
60
|
search_warning = True
|
|
61
61
|
return error, search_warning
|
|
62
62
|
else:
|
|
@@ -76,7 +76,7 @@ def search_rule_sentinel(rule_converted, platform: SentinelPlatform, rule_file,
|
|
|
76
76
|
logger.info(f"Successfully searched the rule {rule_file}")
|
|
77
77
|
|
|
78
78
|
if result > 0: # If the rule has match
|
|
79
|
-
logger.warning(f"(Sentinel)
|
|
79
|
+
logger.warning(f"(Sentinel) {result} hit(s) found for {rule_file}")
|
|
80
80
|
search_warning = True
|
|
81
81
|
return error, search_warning
|
|
82
82
|
else:
|
|
@@ -98,11 +98,14 @@ def search_rule_ms_xdr_mssp(rule_converted, rule_content, platform: MicrosoftXDR
|
|
|
98
98
|
|
|
99
99
|
logger.info("Searching for designated customers")
|
|
100
100
|
|
|
101
|
+
total_result = 0
|
|
102
|
+
|
|
101
103
|
for group, info in export_list.items():
|
|
102
104
|
|
|
103
105
|
tenant_id = info['tenant_id']
|
|
104
106
|
customer_name = info.get('customer_name')
|
|
105
107
|
customer_filter_dir = info.get('customer_filters_directory')
|
|
108
|
+
customer = customer_name or tenant_id
|
|
106
109
|
|
|
107
110
|
logger.debug(f"Processing rule on {tenant_id} from group id {group}")
|
|
108
111
|
|
|
@@ -124,15 +127,20 @@ def search_rule_ms_xdr_mssp(rule_converted, rule_content, platform: MicrosoftXDR
|
|
|
124
127
|
logger.info(f"Successfully searched the rule {rule_file}")
|
|
125
128
|
|
|
126
129
|
if result > 0: # If the rule has a match
|
|
127
|
-
logger.warning(f"{result}
|
|
130
|
+
logger.warning(f"(XDR MSSP) {result} hit(s) found for {rule_file} on customer '{customer}'")
|
|
128
131
|
search_warning = True
|
|
129
132
|
else:
|
|
130
|
-
logger.info(f"No hits for {rule_file}")
|
|
133
|
+
logger.info(f"(XDR MSSP) No hits for {rule_file} on customer '{customer}'")
|
|
134
|
+
|
|
135
|
+
total_result += result
|
|
131
136
|
|
|
132
137
|
except Exception as e:
|
|
133
138
|
logger.error(f"Couldn't search for the rule {rule_file} for tenant {tenant_id} - error {e}")
|
|
134
139
|
error = True
|
|
135
140
|
|
|
141
|
+
if total_result > 0:
|
|
142
|
+
logger.warning(f"(XDR MSSP) {total_result} hit(s) found in total for {rule_file}")
|
|
143
|
+
|
|
136
144
|
return error, search_warning
|
|
137
145
|
|
|
138
146
|
def search_rule_ms_xdr(rule_converted, platform: MicrosoftXDRPlatform, rule_file, parameters, logger, error, search_warning):
|
|
@@ -143,11 +151,11 @@ def search_rule_ms_xdr(rule_converted, platform: MicrosoftXDRPlatform, rule_file
|
|
|
143
151
|
logger.info(f"Successfully searched the rule {rule_file}")
|
|
144
152
|
|
|
145
153
|
if result > 0: # If the rule has match
|
|
146
|
-
logger.warning(f"{result}
|
|
154
|
+
logger.warning(f"(XDR) {result} hit(s) found for {rule_file}")
|
|
147
155
|
search_warning = True
|
|
148
156
|
return error, search_warning
|
|
149
157
|
else:
|
|
150
|
-
logger.info(f"No hits for {rule_file}")
|
|
158
|
+
logger.info(f"(XDR) No hits for {rule_file}")
|
|
151
159
|
return error, search_warning
|
|
152
160
|
|
|
153
161
|
except Exception as e:
|
|
@@ -163,7 +171,7 @@ def search_rule_elastic(rule_converted, platform: ElasticPlatform, rule_file, pa
|
|
|
163
171
|
logger.info(f"Successfully searched the rule {rule_file}")
|
|
164
172
|
|
|
165
173
|
if result > 0: # If the rule has match
|
|
166
|
-
logger.warning(f"(Elastic) {result}
|
|
174
|
+
logger.warning(f"(Elastic) {result} hit(s) found for {rule_file}")
|
|
167
175
|
search_warning = True
|
|
168
176
|
return error, search_warning
|
|
169
177
|
else:
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests that export failures are propagated to the caller
|
|
3
|
+
|
|
4
|
+
A failure returned by the platform API (HTTP 400/403) must raise so that
|
|
5
|
+
export_rule() flags the error and droid exits with a non-zero code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from droid.export import export_rule
|
|
13
|
+
from droid.platforms.ms_xdr import MicrosoftXDRPlatform
|
|
14
|
+
|
|
15
|
+
LOGGER_PARAM = {
|
|
16
|
+
"debug_mode": False,
|
|
17
|
+
"json_enabled": False,
|
|
18
|
+
"json_stdout": False,
|
|
19
|
+
"log_file": None,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
RULE_FILE = "rules/sigma/delivery/anchovy_smuggling_on_neptune.yml"
|
|
23
|
+
|
|
24
|
+
RULE_CONTENT = {
|
|
25
|
+
"id": "0c1b1b1a-0000-0000-0000-0000deadbeef",
|
|
26
|
+
"title": "Anchovy smuggling",
|
|
27
|
+
"description": "Detects anchovies being delivered to Neptune",
|
|
28
|
+
"level": "medium",
|
|
29
|
+
"tags": ["attack.execution"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
RULE_CONVERTED = "DeviceNetworkEvents | where RemoteUrl has 'panucci'"
|
|
33
|
+
|
|
34
|
+
BAD_REQUEST = (
|
|
35
|
+
{"error": {"code": "BadRequest", "message": "Invalid query"}},
|
|
36
|
+
400,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_platform(export_mssp: bool):
|
|
41
|
+
parameters = {
|
|
42
|
+
"query_period": "24H",
|
|
43
|
+
"search_auth": "app",
|
|
44
|
+
"export_auth": "app",
|
|
45
|
+
"tenant_id": "tenant-new-new-york",
|
|
46
|
+
"client_id": "bender-bending-rodriguez",
|
|
47
|
+
"client_secret": "shiny-metal-secret",
|
|
48
|
+
"export_list_mssp": {
|
|
49
|
+
"group1": {"tenant_id": "tenant-planet-express", "customer_name": "Planet Express"},
|
|
50
|
+
"group2": {"tenant_id": "tenant-panucci", "customer_name": "Panucci's Pizza"},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
return MicrosoftXDRPlatform(parameters, LOGGER_PARAM, export_mssp=export_mssp)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def xdr_bad_request(monkeypatch):
|
|
58
|
+
"""Platform factory whose Graph calls always answer 400 Bad Request."""
|
|
59
|
+
|
|
60
|
+
def _factory(export_mssp):
|
|
61
|
+
platform = build_platform(export_mssp)
|
|
62
|
+
# No existing rule -> create path, which answers 400
|
|
63
|
+
monkeypatch.setattr(platform, "_get", lambda *a, **kw: ({"value": []}, 200))
|
|
64
|
+
monkeypatch.setattr(platform, "_post", lambda *a, **kw: BAD_REQUEST)
|
|
65
|
+
monkeypatch.setattr(platform, "_patch", lambda *a, **kw: BAD_REQUEST)
|
|
66
|
+
return platform
|
|
67
|
+
|
|
68
|
+
return _factory
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("export_mssp", [False, True])
|
|
72
|
+
def test_xdr_create_rule_raises_on_bad_request(xdr_bad_request, export_mssp):
|
|
73
|
+
"""A 400 from Graph must raise instead of silently returning"""
|
|
74
|
+
platform = xdr_bad_request(export_mssp)
|
|
75
|
+
|
|
76
|
+
with pytest.raises(Exception):
|
|
77
|
+
platform.create_rule(RULE_CONTENT, RULE_CONVERTED, RULE_FILE)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize("export_mssp", [False, True])
|
|
81
|
+
def test_xdr_create_rule_succeeds_on_created(monkeypatch, export_mssp):
|
|
82
|
+
"""A 201 from Graph must not raise"""
|
|
83
|
+
platform = build_platform(export_mssp)
|
|
84
|
+
monkeypatch.setattr(platform, "_get", lambda *a, **kw: ({"value": []}, 200))
|
|
85
|
+
monkeypatch.setattr(platform, "_post", lambda *a, **kw: ({"id": "rule-anchovy"}, 201))
|
|
86
|
+
monkeypatch.setattr(time, "sleep", lambda *a: None)
|
|
87
|
+
|
|
88
|
+
error = export_rule(
|
|
89
|
+
parameters={},
|
|
90
|
+
rule_content=RULE_CONTENT,
|
|
91
|
+
rule_converted=RULE_CONVERTED,
|
|
92
|
+
platform=platform,
|
|
93
|
+
rule_file=RULE_FILE,
|
|
94
|
+
error=False,
|
|
95
|
+
logger_param=LOGGER_PARAM,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert error is False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_xdr_mssp_partial_failure_flags_error(monkeypatch):
|
|
102
|
+
"""One failing tenant out of two must still flag the export as failed"""
|
|
103
|
+
platform = build_platform(export_mssp=True)
|
|
104
|
+
monkeypatch.setattr(platform, "_get", lambda *a, **kw: ({"value": []}, 200))
|
|
105
|
+
monkeypatch.setattr(time, "sleep", lambda *a: None)
|
|
106
|
+
|
|
107
|
+
def _post(*args, tenant_id=None, **kwargs):
|
|
108
|
+
if tenant_id == "tenant-panucci":
|
|
109
|
+
return BAD_REQUEST
|
|
110
|
+
return {"id": "rule-anchovy"}, 201
|
|
111
|
+
|
|
112
|
+
monkeypatch.setattr(platform, "_post", _post)
|
|
113
|
+
|
|
114
|
+
error = export_rule(
|
|
115
|
+
parameters={},
|
|
116
|
+
rule_content=RULE_CONTENT,
|
|
117
|
+
rule_converted=RULE_CONVERTED,
|
|
118
|
+
platform=platform,
|
|
119
|
+
rule_file=RULE_FILE,
|
|
120
|
+
error=False,
|
|
121
|
+
logger_param=LOGGER_PARAM,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert error is True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.parametrize("export_mssp", [False, True])
|
|
128
|
+
def test_export_rule_flags_error_on_bad_request(xdr_bad_request, export_mssp):
|
|
129
|
+
"""export_rule() must report error=True so the CLI exits non-zero"""
|
|
130
|
+
platform = xdr_bad_request(export_mssp)
|
|
131
|
+
|
|
132
|
+
error = export_rule(
|
|
133
|
+
parameters={},
|
|
134
|
+
rule_content=RULE_CONTENT,
|
|
135
|
+
rule_converted=RULE_CONVERTED,
|
|
136
|
+
platform=platform,
|
|
137
|
+
rule_file=RULE_FILE,
|
|
138
|
+
error=False,
|
|
139
|
+
logger_param=LOGGER_PARAM,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert error is True
|