diff --git a/responders/MSDefenderOffice365/Dockerfile b/responders/MSDefenderOffice365/Dockerfile new file mode 100644 index 000000000..7a1aacb69 --- /dev/null +++ b/responders/MSDefenderOffice365/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.10-slim + +# Install system components +RUN apt-get update && apt-get install -y curl gnupg apt-transport-https + +# Import the public repository GPG keys +RUN curl -sS https://packages.microsoft.com/keys/microsoft.asc | apt-key add - + +# Register the Microsoft Product feed +RUN sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-debian-bullseye-prod bullseye main" > /etc/apt/sources.list.d/microsoft.list' + +# Install PowerShell (/usr/bin/pwsh) +RUN apt-get update && apt-get install -y powershell + +COPY install_deps.ps1 . +RUN pwsh -File install_deps.ps1 + +WORKDIR /worker +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt +COPY . . + +ENTRYPOINT ["python3", "ms_defender_office.py"] \ No newline at end of file diff --git a/responders/MSDefenderOffice365/MSDefenderOffice365_block.json b/responders/MSDefenderOffice365/MSDefenderOffice365_block.json new file mode 100644 index 000000000..640649168 --- /dev/null +++ b/responders/MSDefenderOffice365/MSDefenderOffice365_block.json @@ -0,0 +1,68 @@ +{ + "name": "MSDefenderOffice365_block", + "version": "1.0", + "author": "Joe Lazaro", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Add entries to the Tenant Allow/Block List in the Microsoft 365 Defender", + "dataTypeList": [ + "thehive:case_artifact" + ], + "command": "MSDefenderOffice365/ms_defender_office.py", + "baseConfig": "MSDefenderOffice365", + "config": { + "service": "block" + }, + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/defender-for-office-365?view=o365-worldwide", + "service_logo": { + "path": "assets/MicrosoftDefenderForOffice365_logo.png", + "caption": "logo" + }, + "screenshots": [ + { + "path": "assets/MSDefenderOffice365_Block.png", + "caption": "Example responder action result" + } + ], + "configurationItems": [ + { + "name": "certificate_base64", + "description": "Base64-encoded PFX certificate to be used for certificate-based authentication.", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "certificate_password", + "description": "Password for the certificate used to authenticate", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "app_id", + "description": "The application ID of the service principal that's used in certificate based authentication", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "organization", + "description": "Tenant ID. Example: something.onmicrosoft.com", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "block_expiration_days", + "description": "How many days out should we set the expiration? A value <= 0 means to set no expiration.", + "type": "number", + "multi": false, + "required": true, + "defaultValue": 0 + } + ] +} \ No newline at end of file diff --git a/responders/MSDefenderOffice365/MSDefenderOffice365_unblock.json b/responders/MSDefenderOffice365/MSDefenderOffice365_unblock.json new file mode 100644 index 000000000..7c0a72c14 --- /dev/null +++ b/responders/MSDefenderOffice365/MSDefenderOffice365_unblock.json @@ -0,0 +1,60 @@ +{ + "name": "MSDefenderOffice365_unblock", + "version": "1.0", + "author": "Joe Lazaro", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Add entries to the Tenant Allow/Block List in the Microsoft 365 Defender", + "dataTypeList": [ + "thehive:case_artifact" + ], + "command": "MSDefenderOffice365/ms_defender_office.py", + "baseConfig": "MSDefenderOffice365", + "config": { + "service": "unblock" + }, + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/defender-for-office-365?view=o365-worldwide", + "service_logo": { + "path": "assets/MicrosoftDefenderForOffice365_logo.png", + "caption": "logo" + }, + "screenshots": [ + { + "path": "assets/MSDefenderOffice365_Block.png", + "caption": "Example responder action result" + } + ], + "configurationItems": [ + { + "name": "certificate_base64", + "description": "Base64-encoded PFX certificate to be used for certificate-based authentication.", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "certificate_password", + "description": "Password for the certificate used to authenticate", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "app_id", + "description": "The application ID of the service principal that's used in certificate based authentication", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "organization", + "description": "Tenant ID. Example: something.onmicrosoft.com", + "type": "string", + "multi": false, + "required": true + } + ] +} \ No newline at end of file diff --git a/responders/MSDefenderOffice365/README.md b/responders/MSDefenderOffice365/README.md new file mode 100644 index 000000000..8ce86c610 --- /dev/null +++ b/responders/MSDefenderOffice365/README.md @@ -0,0 +1,14 @@ +Microsoft Defender for Office 365 safeguards your organization against malicious threats posed by email messages, links (URLs), and collaboration tools. Defender for Office 365 includes: + +* Threat protection policies: Define threat-protection policies to set the appropriate level of protection for your organization. +* Reports: View real-time reports to monitor Defender for Office 365 performance in your organization. +* Threat investigation and response capabilities: Use leading-edge tools to investigate, understand, simulate, and prevent threats. +* Automated investigation and response capabilities: Save time and effort investigating and mitigating threats. + +This responder implements support for the Tenant Allow/Block List which is used during mail flow for incoming messages to manually override the Microsoft 365 filtering verdicts. An observable with dataType 'mail' is used to block/unblock a sender, while dataType 'domain' is used to block/unblock a domain. + +You can also block or unblock multiple entries at once by using a multi-line observable with one entry per line. + +The configuration allows you to specify the number of days for a block entry to live before expiration with a value of 0 meaning no expiration. + +For further reference on this capability, see the Microsoft documentation [Allow or block emails using the Tenant Allow/Block List](https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/allow-block-email-spoof?view=o365-worldwide). diff --git a/responders/MSDefenderOffice365/assets/MSDefenderOffice365_Block.png b/responders/MSDefenderOffice365/assets/MSDefenderOffice365_Block.png new file mode 100644 index 000000000..efb82f43b Binary files /dev/null and b/responders/MSDefenderOffice365/assets/MSDefenderOffice365_Block.png differ diff --git a/responders/MSDefenderOffice365/assets/MicrosoftDefenderForOffice365_logo.png b/responders/MSDefenderOffice365/assets/MicrosoftDefenderForOffice365_logo.png new file mode 100644 index 000000000..4472a196f Binary files /dev/null and b/responders/MSDefenderOffice365/assets/MicrosoftDefenderForOffice365_logo.png differ diff --git a/responders/MSDefenderOffice365/install_deps.ps1 b/responders/MSDefenderOffice365/install_deps.ps1 new file mode 100644 index 000000000..ebf5b4930 --- /dev/null +++ b/responders/MSDefenderOffice365/install_deps.ps1 @@ -0,0 +1,4 @@ +Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted +Install-Module -Name ExchangeOnlineManagement +Install-Module -Name PSWSMan -Scope AllUsers -RequiredVersion 2.3.0 +Install-WSMan diff --git a/responders/MSDefenderOffice365/ms_defender_office.py b/responders/MSDefenderOffice365/ms_defender_office.py new file mode 100755 index 000000000..f3498b341 --- /dev/null +++ b/responders/MSDefenderOffice365/ms_defender_office.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +import json +import os +import re +import subprocess +import tempfile +from base64 import b64decode +from pathlib import Path + +from cortexutils.responder import Responder + + +class MsDefenderOffice365Responder(Responder): + def __init__(self): + Responder.__init__(self) + self.service = self.get_param( + 'config.service', None, 'Service parameter is missing') + self.certificate_base64 = self.get_param( + 'config.certificate_base64', None, + 'Config is missing certificate_base64') + self.certificate_password = self.get_param( + 'config.certificate_password', None, + 'Config is missing certificate_password') + self.app_id = self.get_param( + 'config.app_id', None, + 'Config is missing app_id') + self.organization = self.get_param( + 'config.organization', None, + 'Config missing organization') + self.block_expiration_days = self.get_param( + 'config.block_expiration_days', 0) + self.script_dir = os.path.join(Path(__file__).absolute(), 'scripts') + + def clean_output(self, stream: bytes): + """Decode byte stream and remove ANSI color codes""" + re_no_ansi = re.compile(r'\x1b[^m]*m') + return re_no_ansi.sub('', stream.decode('utf-8')) + + def run(self): + observable = self.get_data() + o_data = observable['data'] + if isinstance(o_data, str) and '\n' in o_data: + o_data = o_data.splitlines() + if not isinstance(o_data, list): + o_data = [o_data] + + if observable['dataType'] not in ['domain', 'fqdn', 'mail']: + self.error(f"Data type {observable['dataType']} not supported.") + + try: + clean_cert64 = re.sub(r'\s+', '', self.certificate_base64) + cert_bytes = b64decode(clean_cert64) + temp_cert_file = tempfile.NamedTemporaryFile(suffix='.pfx') + temp_cert_file.write(cert_bytes) + temp_cert_file.flush() + except ValueError as e: + self.error(f"While loading the certificate data: {e}") + + script_name = f'scripts/{self.service}_sender.ps1' + process_args = [ + "pwsh", + script_name, + temp_cert_file.name, + self.certificate_password, + self.app_id, + self.organization, + ] + if self.service == 'block': + caseId = observable['case']['caseId'] + process_args.append(f"TheHive case #{caseId}") + process_args.append(str(self.block_expiration_days)) + process_args += o_data + + try: + result = subprocess.run( + process_args, + capture_output=True) + except subprocess.TimeoutExpired: + self.error(f"Timeout waiting for {script_name} to complete." + f"\nstdout: {self.clean_output(result.stdout)}" + f"\nstderr: {self.clean_output(result.stderr)}") + + scriptErr = self.clean_output(result.stderr) + if result.returncode != 0: + self.error( + f"The powershell script reported an error: {scriptErr}" + "\n\nThe non-error output was the following: " + + self.clean_output(result.stdout) + ) + + try: + # We should get back an array of dictionaries, one for each + # endpoint that was submitted for action. + scriptResult = self.clean_output(result.stdout) + re_json_list = re.compile(r'\[\s*\{.*\}\s*\]', re.DOTALL) + re_json_object = re.compile(r'\{.*\}', re.DOTALL) + match_json_list = re_json_list.search(scriptResult) + match_json_object = re_json_object.search(scriptResult) + + if match_json_list is not None: + extractedJson = match_json_list.group() + scriptResultDict = json.loads(extractedJson) + elif match_json_object is not None: + extractedJson = match_json_object.group() + scriptResultDict = [json.loads(extractedJson)] + else: + self.error( + "Failed to identify JSON in script output:" + + scriptResult) + except json.JSONDecodeError as e: + self.error(f"Error decoding JSON: {e}" + f"| Input: {extractedJson}") + + successful_entries = [] + error_entries = [] + for item in scriptResultDict: + if item.get('error') is not None: + # Don't treat it as an error if the entry we're trying to + # unblock already exists + if 'Entry not found' in item['error']: + successful_entries.append( + f"{item['entry']}: Entry not found." + ) + else: + error_entries.append( + f"{item['entry']}: {item['error']}" + ) + else: + success_dict = json.loads(item['result']) + if self.service == 'block': + result_msg = (f"{item['entry']} Expiration " + + str(success_dict['ExpirationDate'])) + else: + result_msg = item['entry'] + + successful_entries.append(result_msg) + + if len(error_entries) > 0: + report = { + 'message': "At least one endpoint action had an error.", + 'errored_entries': error_entries, + 'successful_entries': successful_entries, + } + self.error(json.dumps(report)) + else: + report = { + 'message': "All endpoint actions completed with no error", + 'entries': successful_entries, + } + self.report(report) + + +if __name__ == '__main__': + MsDefenderOffice365Responder().run() diff --git a/responders/MSDefenderOffice365/requirements.txt b/responders/MSDefenderOffice365/requirements.txt new file mode 100644 index 000000000..8ad52a568 --- /dev/null +++ b/responders/MSDefenderOffice365/requirements.txt @@ -0,0 +1 @@ +cortexutils diff --git a/responders/MSDefenderOffice365/scripts/block_sender.ps1 b/responders/MSDefenderOffice365/scripts/block_sender.ps1 new file mode 100644 index 000000000..f24935460 --- /dev/null +++ b/responders/MSDefenderOffice365/scripts/block_sender.ps1 @@ -0,0 +1,47 @@ +param ( + [parameter(mandatory=$true)] + [string] $certFilePath, + [parameter(mandatory=$true)] + [string] $certPassword, + [parameter(mandatory=$true)] + [string] $appId, + [parameter(mandatory=$true)] + [string] $organization, + [string] $notes="", + [parameter(mandatory=$true)] + [int] $expirationLength, + [parameter(mandatory=$true, valueFromRemainingArguments=$true)] + [string[]] $entries +) + +$connectSplat = @{ + CertificateFilePath = $certFilePath + CertificatePassword = $(ConvertTo-SecureString -String $certPassword -AsPlainText -Force) + AppId = $appId + Organization = $organization +} + +Connect-ExchangeOnline @connectSplat + +$allResults = @() +ForEach ($entry in $entries) { + if ($expirationLength -le 0) { + # No expiration + $result = New-TenantAllowBlockListItems -ListType Sender -Block -Notes $notes -Entries $entry -NoExpiration -ErrorAction Continue | ConvertTo-Json + $allResults += @{ + entry = $entry; + result = $result; + error = If ($?) {$null} else {$Error[0].Exception.SerializedRemoteException.Message}; + } + } else { + $expiry = (Get-Date).AddDays($expirationLength) + $result = New-TenantAllowBlockListItems -ListType Sender -Block -ExpirationDate $expiry -Notes $notes -Entries $entry -ErrorAction Continue | ConvertTo-Json + $allResults += @{ + entry = $entry; + result = $result; + error = If ($?) {$null} else {$Error[0].Exception.SerializedRemoteException.Message}; + } + } +} + +$allResults | ConvertTo-Json -Depth 4 diff --git a/responders/MSDefenderOffice365/scripts/unblock_sender.ps1 b/responders/MSDefenderOffice365/scripts/unblock_sender.ps1 new file mode 100644 index 000000000..a0d6119ec --- /dev/null +++ b/responders/MSDefenderOffice365/scripts/unblock_sender.ps1 @@ -0,0 +1,33 @@ +param ( + [parameter(mandatory=$true)] + [string] $certFilePath, + [parameter(mandatory=$true)] + [string] $certPassword, + [parameter(mandatory=$true)] + [string] $appId, + [parameter(mandatory=$true)] + [string] $organization, + [parameter(mandatory=$true, valueFromRemainingArguments=$true)] + [string[]] $entries +) + +$connectSplat = @{ + CertificateFilePath = $certFilePath + CertificatePassword = $(ConvertTo-SecureString -String $certPassword -AsPlainText -Force) + AppId = $appId + Organization = $organization +} + +Connect-ExchangeOnline @connectSplat + +$allResults = @() +ForEach ($entry in $entries) { + $result = Remove-TenantAllowBlockListItems -ListType Sender -Entries $entry | ConvertTo-Json + $allResults += @{ + entry = $entry; + result = $result; + error = If ($?) {$null} else {$Error[0].Exception.SerializedRemoteException.Message}; + } +} + +$allResults | ConvertTo-Json -Depth 4