Note
Update 7/4/2022: Wazuh 4.3 natively supports Office 365 with a more robust and complete integration. If you are working with Wazuh 4.3 or newer, go to the Using Wazuh to monitor Office 365 section in our documentation.
Follow this blog post while working with Wazuh older versions or as an example of how to create a custom integration with cloud services.
Note
Update 6/8/2020: The Office 365 management API changed the status code of some of the endpoints and the integration script had to be properly updated.
Microsoft provides a single pane of glass for all Office 365 tasks through the Office 365 management APIs
. This includes service communications, security, compliance, reporting and auditing related events.
Wazuh can help you get insight into this vast array of information by ingesting it and alerting based on custom rules.
Register your app
To authenticate with the Microsoft identity platform endpoint you need to register an app in your Microsoft Azure portal app registrations section. Once there click on New registration
:
The app is now registered and you can see information about it in its overview section:
Take note of the tenant and client IDs
as you will use them later on.
Certificates & secrets
You can generate a password to use during the authentication process. Go to Certificates & secrets
and click on New client secret
:
Note
Make sure you write it down because the UI won’t let you copy it afterwards.
API permissions
The application needs specific API permissions to be able to request the Office 365 activity events. In this case you are looking for permissions related to the https://manage.office.com
resource.
To configure the application permissions go to the API permissions
page, choose Add a permission
, then select the Office 365 Management APIs and click on Application permissions
:
You need to add the following permissions under the ActivityFeed
group:
ActivityFeed.Read
. Read activity data for your organization.ActivityFeed.ReadDlp
. Read DLP policy events including detected sensitive data.
Content types
The Office 365 management activity API aggregates actions and events into tenant-specific content blobs. There are five categories depending on the type and source of the content:
Audit.AzureActiveDirectory
. User identity management.Audit.Exchange
. Mail and calendaring server.Audit.SharePoint
. Web-based collaborative platform.Audit.General
. Includes all other workloads not included in the previous content types.DLP.All
. Data loss prevention workloads.
You can find more details about the events and their properties associated with these here.
Fetching the events
Note
Update 6/9/2021. Since Wazuh v4.2.0 the wazuh-analysisd input socket was moved from /var/ossec/queue/ossec/queue
to /var/ossec/queue/sockets/queue
. If you are using a version prior to 4.2.0, edit the script accordingly.
The following script takes care of enabling and collecting Office 365 content type subscriptions. It is designed to run on the Wazuh manager without you needing to install any dependency in it:
#!/var/ossec/framework/python/bin/python3 # Copyright (C) 2015-2020, Wazuh Inc. # Created by Wazuh, Inc. <info@wazuh.com>. # This program is a free software; you can redistribute it and/or modify it under the terms of GPLv2 import argparse import sys import json import requests import logging import datetime from socket import socket, AF_UNIX, SOCK_DGRAM ################################################## Global variables ################################################## # Microsoft resource resource = "https://manage.office.com" # Office 365 management activity API available content types availableContentTypes = ["Audit.AzureActiveDirectory", "Audit.Exchange", "Audit.SharePoint", "Audit.General", "DLP.All"] # Wazuh manager analisysd socket address socketAddr = '/var/ossec/queue/sockets/queue' ################################################## Common functions ################################################## # Send event to Wazuh manager def send_event(msg): logging.debug('Sending {} to {} socket.'.format(msg, socketAddr)) string = '1:office_365:{}'.format(msg) sock = socket(AF_UNIX, SOCK_DGRAM) sock.connect(socketAddr) sock.send(string.encode()) sock.close() # Perform HTTP request def make_request(method, url, headers, data=None): response = requests.request(method, url, headers=headers, data=data) # If the request succeed if response.status_code >= 200 and response.status_code < 210: return response if method == "POST" and response.status_code == 400: return response else: raise Exception('Request ', method, ' ', url, ' failed with ', response.status_code, ' - ', response.text) # Obtain a token for accessing the Office 365 management activity API def obtain_access_token(tenantId, clientId, clientSecret): # Add header and payload headers = {'Content-Type':'application/x-www-form-urlencoded'} payload = 'client_id={}&scope={}/.default&grant_type=client_credentials&client_secret={}'.format(clientId, resource, clientSecret) # Request token response = make_request("POST", "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(tenantId), headers=headers, data=payload) logging.info("Microsoft token was successfully fetched.") return json.loads(response.text)['access_token'] # Perform an API request to Office 365 management API def make_api_request(method, url, token): # Create a valid header using the token headers = {'Content-Type':'application/json', 'Authorization':'Bearer {0}'.format(token)} # Make API request response = make_request(method, url, headers=headers) # If this is a POST request just return if (method == "POST"): return None json_data = json.loads(response.text) # If NextPageUri is included in the header and it has content in it if 'NextPageUri' in response.headers.keys() and response.headers['NextPageUri']: logging.info("New data page detected in {}.".format(url)) # Request new page and append to existing data record = make_api_request(method, response.headers['NextPageUri'], token) json_data.extend(record) return json_data # Manage content type subscriptions def manage_content_type_subscriptions(contentTypes, clientId, token): # For every available content type for contentType in availableContentTypes: # If it was added as a parameter then start the subscription if contentType in contentTypes: make_api_request("POST", "{}/api/v1.0/{}/activity/feed/subscriptions/start?contentType={}".format(resource, clientId, contentType), token) logging.info("{} subscription was successfully started.".format(contentType)) ################################################## Main workflow ################################################## if __name__ == "__main__": # Parse arguments parser = argparse.ArgumentParser(description='Wazuh - Office 365 activity information.') parser.add_argument('--contentTypes', metavar='contentTypes', type=str, nargs='+', required = True, help='Office 365 activity content type subscriptions.') parser.add_argument('--hours', metavar='hours', type=int, required = True, help='How many hours to fetch activity logs.') parser.add_argument('--tenantId', metavar='tenantId', type=str, required = True, help='Application tenant ID.') parser.add_argument('--clientId', metavar='clientId', type=str, required = True, help='Application client ID.') parser.add_argument('--clientSecret', metavar='clientSecret', type=str, required = True, help='Client secret.') parser.add_argument('--debug', action='store_true', required = False, help='Enable debug mode logging.') args = parser.parse_args() # Start logging config if args.debug: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s: [%(levelname)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S",) else: logging.basicConfig(level=logging.INFO, format='%(asctime)s: [%(levelname)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S",) # Disable warnings requests.packages.urllib3.disable_warnings() try: # Obtain access token token = obtain_access_token(args.tenantId, args.clientId, args.clientSecret) # Start/stop subscriptions depending on the content_types parameter manage_content_type_subscriptions(args.contentTypes, args.clientId, token) # Build time range filter currentTime = datetime.datetime.now(datetime.timezone.utc) endTime = str(currentTime).replace(' ', 'T').rsplit('.', maxsplit=1)[0] startTime = str(currentTime - datetime.timedelta(hours=args.hours)).replace(' ', 'T').rsplit('.', maxsplit=1)[0] # For every content_type in the content_types parameter for contentType in args.contentTypes: # If it is a valid content_type if contentType in availableContentTypes: # List the subscription content subscription_content = make_api_request("GET", "{}/api/v1.0/{}/activity/feed/subscriptions/content?contentType={}&startTime={}&endTime={}".format(resource, args.clientId, contentType, startTime, endTime), token) logging.info("{} subscription was successfully listed.".format(contentType)) # For every blob in the subscription for blob in subscription_content: # Request activity information data = make_api_request("GET", blob["contentUri"], token) logging.info("Blob in {} subscription was successfully fetched.".format(contentType)) # Loop every event and send it to the Wazuh manager for event in data: office_365_event = {} office_365_event['office_365'] = event send_event(json.dumps(office_365_event)) except Exception as e: logging.error("Error while retrieving Office 365 activity logs: {}.".format(e))
Script usage
These are the parameters you can/need to pass to the script:
tenantId
. Your globally unique AAD identifier. Required.clientId
. Your application identifier. Required.clientSecret
. Password to authenticate the application. Required.contentTypes
. Space separated list with the content types that you want to list events for. Required.hours
. Time range to search events for. Max 24h. Required.debug
. Debug flag for the script. Optional.
And this is an execution example:
chmod +x office_365.py ./office_365.py --contentType DLP.All Audit.General Audit.AzureActiveDirectory --hours 24 --tenantId your_tenant_id --clientId your_client_id --clientSecret your_client_secret
Script overview
The first step is to request a token from the Microsoft identity platform for accessing the https://manage.office.com
resource:
# Obtain a token for accessing the Office 365 management activity API def obtain_access_token(tenantId, clientId, clientSecret): # Add header and payload headers = {'Content-Type':'application/x-www-form-urlencoded'} payload = 'client_id={}&scope={}/.default&grant_type=client_credentials&client_secret={}'.format(clientId, resource, clientSecret) # Request token response = make_request("POST", "https://login.microsoftonline.com/{}/oauth2/v2.0/token".format(tenantId), headers=headers, data=payload) logging.info("Microsoft token was successfully fetched.") return json.loads(response.text)['access_token']
Then it starts/stops the content type subscriptions specified in the parameters:
# Manage content type subscriptions def manage_content_type_subscriptions(contentTypes, clientId, token): # For every available content type for contentType in availableContentTypes: # If it was added as a parameter then start the subscription if contentType in contentTypes: make_api_request("POST", "{}/api/v1.0/{}/activity/feed/subscriptions/start?contentType={}".format(resource, clientId, contentType), token) logging.info("{} subscription was successfully started.".format(contentType)) # Otherwise stop the subscription else: make_api_request("POST", "{}/api/v1.0/{}/activity/feed/subscriptions/stop?contentType={}".format(resource, clientId, contentType), token) logging.debug("{} subscription was successfully stopped.".format(contentType))
After that it lists every subscription event and sends them to the Wazuh manager via socket:
# For every content_type in the content_types parameter for contentType in args.contentTypes: # If it is a valid content_type if contentType in availableContentTypes: # List the subscription content subscription_content = make_api_request("GET", "{}/api/v1.0/{}/activity/feed/subscriptions/content?contentType={}&startTime={}&endTime={}".format(resource, args.clientId, contentType, startTime, endTime), token) logging.info("{} subscription was successfully listed.".format(contentType)) # For every blob in the subscription for blob in subscription_content: # Request activity information data = make_api_request("GET", blob["contentUri"], token) logging.info("Blob in {} subscription was successfully fetched.".format(contentType)) # Loop every event and send it to the Wazuh manager for event in data: office_365_event = {} office_365_event['office_365'] = event send_event(json.dumps(office_365_event))
Wazuh manager configuration
Script execution
You can configure the Wazuh manager to schedule commands and scripts by using the command module. You will use it to run the previous script on an interval basis.
For that you need to add the following configuration block in your /var/ossec/etc/ossec.conf
file (don’t forget to restart the Wazuh manager afterwards):
<wodle name="command"> <disabled>no</disabled> <command>/path/to/script/office_365.py --contentType Audit.Exchange Audit.SharePoint DLP.All Audit.General Audit.AzureActiveDirectory --hours 24 --tenantId your_tenant_id --clientId your_client_id --clientSecret your_client_secret</command> <interval>24h</interval> <ignore_output>yes</ignore_output> <run_on_start>yes</run_on_start> <timeout>0</timeout> </wodle>
Note
Modify the script parameters with your credentials, content types and time range options.
You can read more about scheduling commands here.
Rules
Office 365 logs conform to the JSON schema and Wazuh will automatically decode them. For more information please refer to Wazuh JSON decoder.
This is a generic rule that will trigger an alert regardless of the event type. Place it in your Wazuh manager /var/ossec/etc/rules/
folder:
<group name="office_365,"> <rule id="100002" level="5"> <location>office_365</location> <description>$(office_365.Workload) $(office_365.Operation) operation.</description> <options>no_full_log</options> </rule> </group>
Don’t forget to restart the Wazuh manager afterwards.
Use cases
Azure Active Directory logins
This is a sample alert for a UserLoggedIn
operation:
{ "agent": { "name": "eridu", "id": "000" }, "manager": { "name": "eridu" }, "data": { "office_365": { "AzureActiveDirectoryEventType": "1", "ResultStatus": "Succeeded", "ObjectId": "sanitized", "UserKey": "sanitized", "ActorIpAddress": "sanitized", "Operation": "UserLoggedIn", "OrganizationId": "sanitized", "ClientIP": "sanitized", "Workload": "AzureActiveDirectory", "IntraSystemId": "sanitized", "RecordType": "15", "Version": "1", "UserId": "javier@wazuh.com", "TargetContextId": "sanitized", "CreationTime": "2020-03-19T16:48:02", "Id": "sanitized", "InterSystemsId": "sanitized", "ApplicationId": "sanitized", "UserType": "0", "ActorContextId": "sanitized" } }, "rule": { "firedtimes": 116, "mail": false, "level": 5, "description": "AzureActiveDirectory UserLoggedIn operation.", "groups": [ "office_365" ], "id": "100002" }, "location": "office_365", "decoder": { "name": "json" }, "timestamp": "2020-03-20T12:16:44.042+0000" }
Sharepoint file access
This other alert represents a FileAccessed
event from a Microsoft Excel file:
{ "agent": { "name": "eridu", "id": "000" }, "manager": { "name": "eridu" }, "data": { "office_365": { "Site": "sanitized", "ObjectId": "sanitized/Documents/Book.xlsx", "SourceFileName": "Book.xlsx", "UserKey": "sanitized", "ItemType": "File", "Operation": "FileAccessed", "OrganizationId": "sanitized", "SiteUrl": "sanitized, "ClientIP": "sanitized", "SourceFileExtension": "xlsx", "Workload": "OneDrive", "SourceRelativeUrl": "Documents", "EventSource": "SharePoint", "RecordType": "6", "ListId": "sanitized", "Version": "1", "UserId": "javier@wazuh.com", "WebId": "sanitized", "CreationTime": "2020-03-20T10:52:44", "UserAgent": "MSWAC", "Id": "sanitized", "CorrelationId": "sanitized", "UserType": "0", "ListItemUniqueId": "sanitized" } }, "rule": { "firedtimes": 5, "mail": false, "level": 5, "description": "OneDrive FileAccessed operation.", "groups": [ "office_365" ], "id": "100002" }, "location": "office_365", "decoder": { "name": "json" }, "timestamp": "2020-03-20T12:18:10.492+0000" }
Exchange mailbox operation
Here you can see a New-Mailbox
alert from Microsoft Exchange:
{ "agent": { "name": "eridu", "id": "000" }, "manager": { "name": "eridu" }, "data": { "office_365": { "OrganizationName": "wazuh.onmicrosoft.com", "ResultStatus": "True", "ObjectId": "EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.onmicrosoft.com/PeopleDirectoryShard_eur_replica_2", "UserKey": "NT AUTHORITY\\SYSTEM (w3wp)", "ExternalAccess": "true", "Operation": "New-Mailbox", "OrganizationId": "sanitized", "ClientIP": "sanitized", "Workload": "Exchange", "RecordType": "1", "OriginatingServer": "sanitized", "Version": "1", "UserId": "NT AUTHORITY\\SYSTEM (w3wp)", "CreationTime": "2020-03-19T15:32:02", "Id": "sanitized", "UserType": "3" } }, "rule": { "firedtimes": 26, "mail": false, "level": 5, "description": "Exchange New-Mailbox operation.", "groups": [ "office_365" ], "id": "100002" }, "location": "office_365", "decoder": { "name": "json" }, "timestamp": "2020-03-20T12:19:08.694+0000" }
Sample dashboard
You can easily build custom visualizations and dashboards from these alerts by taking advantage of Kibana capabilities:
Read more about building dashboards here.
References
- Office 365 management activity API schema.
- Wazuh command module.
- Customize Wazuh rules.
- Wazuh JSON decoder.
- Scheduling remote commands.
- Azure app registration.
- Kibana dashboards.
- Microsoft API access without a user.
- Office 365 Management Activity API reference.
If you have any questions about this, join our community. Our team and contributors will help you.