Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Microsoft 365 Defender responder #1124

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions responders/MSDefenderOffice365/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
68 changes: 68 additions & 0 deletions responders/MSDefenderOffice365/MSDefenderOffice365_block.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
60 changes: 60 additions & 0 deletions responders/MSDefenderOffice365/MSDefenderOffice365_unblock.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
14 changes: 14 additions & 0 deletions responders/MSDefenderOffice365/README.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions responders/MSDefenderOffice365/install_deps.ps1
Original file line number Diff line number Diff line change
@@ -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
154 changes: 154 additions & 0 deletions responders/MSDefenderOffice365/ms_defender_office.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions responders/MSDefenderOffice365/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cortexutils
47 changes: 47 additions & 0 deletions responders/MSDefenderOffice365/scripts/block_sender.ps1
Original file line number Diff line number Diff line change
@@ -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
Loading