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:

Fill in the name of your app, choose the desired account type and click on the Register button:

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 note 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

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/ossec/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
    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))
        # 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))

################################################## 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)

        response =make_api_request("GET", "{}/api/v1.0/{}/activity/feed/subscriptions/list".format(resource, args.clientId), token)
        # 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:

You can read more about building dashboards here.

References