Skip to content

Commit

Permalink
upd: Improve exports in Microsoft Sentinel in MSSP mode (#18)
Browse files Browse the repository at this point in the history
This merge includes various bug fixes when deploying detection rules in Microsoft Sentinel using the --mssp argument. It also brings the ability to override the subscription_id and resource_group using the environment variables. The integrity check is now available using this mode.
  • Loading branch information
0xFustang authored Sep 23, 2024
1 parent 2bf019f commit 9e92d28
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 22 deletions.
13 changes: 9 additions & 4 deletions src/droid/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,23 @@ def droid_platform_config(args, config_path):
config["workspace_id"] = environ.get("DROID_AZURE_WORKSPACE_ID")
if environ.get("DROID_AZURE_WORKSPACE_NAME"):
config["workspace_name"] = environ.get("DROID_AZURE_WORKSPACE_NAME")
if environ.get("DROID_AZURE_SUBSCRIPTION_ID"):
config["subscription_id"] = environ.get("DROID_AZURE_SUBSCRIPTION_ID")
if environ.get("DROID_AZURE_RESOURCE_GROUP"):
config["resource_group"] = environ.get("DROID_AZURE_RESOURCE_GROUP")

except Exception:
raise Exception("Something unexpected happened...")

if args.export or args.search or args.integrity:

auth_methods = ["default", "app"]

if environ.get("DROID_MS_CLOUD_SEARCH_AUTH"):
config["search_auth"] = environ.get("DROID_MS_CLOUD_SEARCH_AUTH")
if environ.get("DROID_AZURE_SEARCH_AUTH"):
config["search_auth"] = environ.get("DROID_AZURE_SEARCH_AUTH")

if environ.get("DROID_MS_CLOUD_EXPORT_AUTH"):
config["export_auth"] = environ.get("DROID_MS_CLOUD_EXPORT_AUTH")
if environ.get("DROID_AZURE_EXPORT_AUTH"):
config["export_auth"] = environ.get("DROID_AZURE_EXPORT_AUTH")

if "search_auth" in config and config["search_auth"] not in auth_methods:
raise ValueError(f"Invalid search_auth: {config['search_auth']}")
Expand Down
4 changes: 2 additions & 2 deletions src/droid/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ def convert_rules(parameters, droid_config, base_config, logger_param):
platform = ElasticPlatform(droid_config, logger_param, "esql", raw=False)
elif "eql" in platform_name:
platform = ElasticPlatform(droid_config, logger_param, "eql", raw=False)
elif "microsoft_sentinel" in platform_name:
platform = SentinelPlatform(droid_config, logger_param)
elif "microsoft_sentinel" in platform_name and parameters.mssp:
platform = SentinelPlatform(droid_config, logger_param, export_mssp=True)
elif "microsoft_sentinel" in platform_name:
platform = SentinelPlatform(droid_config, logger_param, export_mssp=False)
elif "microsoft_xdr" in platform_name and parameters.sentinel_xdr:
platform = SentinelPlatform(droid_config, logger_param)
elif "microsoft_xdr" in platform_name:
Expand Down
67 changes: 57 additions & 10 deletions src/droid/integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,63 @@ def integrity_rule_splunk(rule_converted, rule_content, platform: SplunkPlatform
if error:
return error

def integrity_rule_sentinel(rule_converted, rule_content, platform: SentinelPlatform, rule_file, parameters, logger, error):
def integrity_rule_sentinel_mssp(rule_converted, rule_content, platform: SentinelPlatform, rule_file, parameters, logger, error):

try:
saved_search: dict = platform.get_rule(rule_content, rule_file)
# Mapping rule_content with a MS Sentinel saved search properties
mapping = {
"id": "name",
"detection": "query",
"description": "description"
}
export_list = platform.get_integrity_export_mssp()
except Exception as e:
logger.error(f"Couldn't get the export list for the designated customers - error {e}")
return error

logger.info("Integrity check for designated customers")

error_occured = False

for group, info in export_list.items():

tenant_id = info['tenant_id']
subscription_id = info['subscription_id']
resource_group_name = info['resource_group_name']
workspace_name = info['workspace_name']

logger.debug(f"Processing rule on {workspace_name} from group id {group}")
try:
saved_search: dict = platform.get_rule_mssp(
rule_content, rule_file, tenant_id,
subscription_id, resource_group_name,
workspace_name
)
except Exception as e:
logger.error(f"Couldn't check the integrity for the rule {rule_file} on workspace {workspace_name} from {group} - error {e}")
return error

error = integrity_rule_sentinel(rule_converted, rule_content, platform, rule_file, parameters, logger, error, saved_search=saved_search)

if error:
error_occured = True

if error_occured:
return error

def integrity_rule_sentinel(
rule_converted, rule_content, platform: SentinelPlatform,
rule_file, parameters, logger,
error, saved_search=None
):

try:
if not saved_search:
saved_search: dict = platform.get_rule(rule_content, rule_file)
except Exception as e:
logger.error(f"Couldn't check the integrity for the rule {rule_file} - error {e}")
return error

mapping = {
"id": "name",
"detection": "query",
"description": "description"
}

if saved_search:
logger.info(f"Successfully retrieved the rule {rule_file}")
else:
Expand Down Expand Up @@ -223,7 +266,6 @@ def integrity_rule_elastic(rule_converted, rule_content, platform: ElasticPlatfo
rule_content["detection"] = rule_converted

mapping = {
#"id": "name", # Not sure why this is here?
"detection": "query",
"description": "description"
}
Expand Down Expand Up @@ -275,6 +317,9 @@ def integrity_rule(parameters, rule_converted, rule_content, platform, rule_file
elif parameters.platform == "microsoft_xdr":
error = integrity_rule_ms_xdr(rule_converted, rule_content, platform, rule_file, parameters, logger, error)
return error
elif "microsoft_sentinel" in parameters.platform and parameters.mssp:
error = integrity_rule_sentinel_mssp(rule_converted, rule_content, platform, rule_file, parameters, logger, error)
return error
elif "microsoft_sentinel" in parameters.platform:
error = integrity_rule_sentinel(rule_converted, rule_content, platform, rule_file, parameters, logger, error)
return error
Expand All @@ -287,8 +332,10 @@ def integrity_rule_raw(parameters: dict, export_config: dict, logger_param: dict

if parameters.platform == "splunk":
platform = SplunkPlatform(export_config, logger_param)
elif parameters.platform == "microsoft_sentinel" and parameters.mssp:
platform = SentinelPlatform(export_config, logger_param, export_mssp=True)
elif parameters.platform == "microsoft_sentinel":
platform = SentinelPlatform(export_config, logger_param)
platform = SentinelPlatform(export_config, logger_param, export_mssp=False)
elif parameters.platform == "microsoft_xdr":
platform = MicrosoftXDRPlatform(export_config, logger_param)
elif parameters.platform == "esql" or parameters.platform == "eql":
Expand Down
56 changes: 50 additions & 6 deletions src/droid/platforms/sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,23 @@ def get_workspaces(self, credential, export_mode=False):
else:
entry_dict = {
"customer": workspace_name,
graph_key: graph_value
"workspace_id": graph_value
}

workspace_list.append(entry_dict)

return workspace_list

def get_integrity_export_mssp(self) -> list:

if self._export_list_mssp:
self.logger.info("Integrity check for designated customers")
return self._export_list_mssp
else:
self.logger.error("No export_list_mssp found")
raise
# TODO: Integrity check to all customers


def mssp_run_sentinel_search(self,
client,
Expand Down Expand Up @@ -314,9 +324,40 @@ def run_sentinel_search(self, rule_converted, rule_file, mssp_mode):
except Exception as e:
self.logger.error(f"Rule {rule_file} error: {e}")

def get_rule_mssp(self, rule_content, rule_file,
tenant_id, subscription_id, resource_group_name,
workspace_name):
"""Retrieve a scheduled alert rule in Sentinel in MSSP mode
"""

self._tenant_id = tenant_id
credential = self.get_credentials()

client = SecurityInsights(credential, subscription_id)

try:
rule = client.alert_rules.get(
resource_group_name=resource_group_name,
workspace_name=workspace_name,
rule_id=rule_content['id']
)
self.logger.info(f"Successfully retrieved the rule {rule_file} for {workspace_name}")

if rule:
return rule
else:
return None

except ResourceNotFoundError:
self.logger.error(f"Rule not found {rule_file} in {workspace_name}")
return None

except Exception as e:
self.logger.error(f"Could not retrieve the rule {rule_file} in {workspace_name}")
raise

def get_rule(self, rule_content, rule_file):
"""Retrieve a scheduled alert rule in Sentinel
Remove a scheduled alert rule in Sentinel
"""
credential = self.get_credentials()

Expand Down Expand Up @@ -435,17 +476,19 @@ def create_rule(self, rule_content, rule_converted, rule_file):
techniques=self.mitre_techniques(rule_content)
)

credential = self.get_credentials()

if self._export_mssp:
if self._export_list_mssp:
self.logger.info("Exporting to restricted customers")
self.logger.info("Exporting to designated customers")
for group, info in self._export_list_mssp.items():

workspace_name = info['workspace_name']
self._tenant_id = info['tenant_id']
resource_group_name = info['resource_group_name']
subscription_id = info['subscription_id']

self.logger.debug(f"Exporting to {workspace_name}")
self.logger.debug(f"Exporting to {workspace_name} from group id {group}")

credential = self.get_credentials()

# Create a new SecurityInsights client for the target subscription
client = SecurityInsights(credential, subscription_id)
Expand All @@ -465,6 +508,7 @@ def create_rule(self, rule_content, rule_converted, rule_file):
client_workspaces = self.get_workspaces(credential, export_mode=True)
# TODO: Export to all customers
else:
credential = self.get_credentials()
client = SecurityInsights(credential, self._subscription_id)
try:
client.alert_rules.create_or_update(
Expand Down

0 comments on commit 9e92d28

Please sign in to comment.