Microsoft 365 Defender Event Collector Deprecated

Deprecated. Use 'Office 365' in the XSIAM Data Sources instead.

Analytics & SIEM · Microsoft Defender for Endpoint

Details

IDMicrosoft 365 Defender Event Collector
ProviderMicrosoft
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/auth-utils:1.0.0.116752
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Microsoft Defender for Endpoint Alerts integration for Cortex XSIAM (Deprecated).

Deprecation Announcement

Following this announcement by Microsoft about migrating from the deprecated SIEM API to the Graph API, this Event Collector is now deprecated.

Replacement Option

In XSIAM Office 365 Data Source, select Microsoft Graph API -> Alerts, and select Use Microsoft Graph API V2.


This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure Microsoft Defender for Endpoint Alerts on Cortex XSIAM

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Microsoft Defender for Endpoint Alerts.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Endpoint Type The endpoint for accessing Microsoft Defender for Endpoint. See table below. True
    Client (Application) ID The client (application) ID to use to connect. True
    Client Secret   True
    Tenant ID   True
    First fetch timestamp (<number> <time unit>, for example 12 hours, 7 days)   False
    Fetch alerts timeout The time limit in seconds for fetch alerts to run. Leave this empty to cancel the timeout limit. False
    Number of alerts for each fetch. Due to API limitations, the maximum is 10,000. False
    Fetch events   False
    Verify SSL Certificate   False
    Use system proxy settings   False
    Server URL The United States: api-us.security.microsoft.com
    Europe: api-eu.security.microsoft.com
    The United Kingdom: api-uk.security.microsoft.com
    See table below.
    True
  4. Endpoint Type options

    Endpoint Type Description
    Worldwide The publicly accessible Microsoft Defender for Endpoint
    EU Geo Proximity Microsoft Defender for Endpoint Geo proximity end point for the UK customers.
    UK Geo Proximity Microsoft Defender for Endpoint Geo proximity end point for the UK customers.
    US Geo Proximity Microsoft Defender for Endpoint Geo proximity end point for the US customers.
    US GCC Microsoft Defender for Endpoint for the USA Government Cloud Community (GCC)
    US GCC-High Microsoft Defender for Endpoint for the USA Government Cloud Community High (GCC-High)
    DoD Microsoft Defender for Endpoint for the USA Department of Defense (DoD)
    Custom Custom endpoint configuration to the Microsoft Defender for Endpoint. See note below.
    • Note: In most cases setting Endpoint type is preferred to setting Server URL. Only use it in cases where a custom URL is required for accessing a national cloud or for cases of self-deployment.
  5. Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

microsoft-365-defender-get-events


Returns a list of alerts

Base Command

microsoft-365-defender-get-events

Input

Argument Name Description Required
limit The maximum number of alerts per fetch. Default is 10000. Optional
first_fetch The first fetch time (<number> <time unit>, for example 12 hours, 1 day, 3 months). Default is 3 days. Optional

Context Output

There is no context output for this command.

Context Example

{
    "Microsoft365Defender": {
            "alerts": [
                {
                    "classification": null, 
                    "investigationState": "TerminatedBySystem", 
                    "computerDnsName": "computer-name", 
                    "evidence": [], 
                    "aadTenantId": "00000000-0000-0000-0000-000000000000", 
                    "id": "aa000000000000000000_000000000", 
                    "category": "SuspiciousActivity", 
                    "threatFamilyName": null, 
                    "lastUpdateTime": "2022-05-12T07:29:45.1466667Z", 
                    "lastEventTime": "2022-05-12T01:19:11.7046854Z", 
                    "firstEventTime": "2022-05-12T01:19:11.7046854Z", 
                    "threatName": null, 
                    "comments": [], 
                    "assignedTo": null, 
                    "detectorId": "00000000-0000-0000-0000-000000000000", 
                    "detectionSource": "AutomatedInvestigation", 
                    "resolvedTime": null, 
                    "alertCreationTime": "2022-05-12T01:19:11.8059246Z", 
                    "status": "New", 
                    "description": "MS description", 
                    "loggedOnUsers": [], 
                    "determination": null, 
                    "severity": "Informational", 
                    "mitreTechniques": [], 
                    "machineId": "abc1234567890987654321234567890987654xyz", 
                    "title": "Automated investigation started manually", 
                    "investigationId": 0000, 
                    "relatedUser": null, 
                    "rbacGroupName": "UnassignedGroup", 
                    "incidentId": 0000
                }
            ]
    }
}

microsoft-365-defender-auth-reset


Run this command if for some reason you need to rerun the authentication process.

Base Command

microsoft-365-defender-auth-reset

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Configuration parameters

  • endpoint_type — Endpoint Type
  • tenant_id — Tenant ID (required)
  • client_id — Client (Application) ID (required)
  • credentials — (required)
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • fetch_timeout — Fetch alerts timeout
  • limit — Number of alerts for each fetch.
  • isFetchEvents — Fetch events
  • url — Server URL (e.g., https://api.securitycenter.microsoft.com)
  • verify — Verify SSL Certificate
  • proxy — Use system proxy settings

Commands (2)

  • microsoft-365-defender-auth-reset Deprecated

    Run this command if for some reason you need to rerun the authentication process.

  • microsoft-365-defender-get-events Deprecated

    Returns a list of alerts.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
# pylint: disable=no-name-in-module
# pylint: disable=no-self-argument
import copy

from CommonServerUserPython import *  # noqa

from abc import ABC
from typing import Any
from collections.abc import Callable

from enum import Enum
from pydantic import BaseConfig, BaseModel, AnyUrl, validator  # type: ignore[E0611, E0611, E0611]
from requests.auth import HTTPBasicAuth
import requests
import urllib3.util

from MicrosoftApiModule import *

# Disable insecure warnings
urllib3.disable_warnings()  # pylint: disable=no-member

''' CONSTANTS '''
MAX_ALERTS_PAGE_SIZE = 1000
ALERT_CREATION_TIME = 'alertCreationTime'
DEFENDER_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
AUTH_ERROR_MSG = 'Authorization Error: make sure tenant id, client id and client secret is correctly set'
VENDOR = 'Microsoft 365'
PRODUCT = 'Defender'

''' HELPER CLASSES '''


# COPY OF SiemApiModule


class Method(str, Enum):
    GET = 'GET'
    POST = 'POST'
    PUT = 'PUT'
    HEAD = 'HEAD'
    PATCH = 'PATCH'
    DELETE = 'DELETE'


def load_json(v: Any) -> dict:
    if not isinstance(v, dict | str):
        raise ValueError('headers are not dict or a valid json')
    if isinstance(v, str):
        try:
            v = json.loads(v)
            if not isinstance(v, dict):
                raise ValueError('headers are not from dict type')
        except json.decoder.JSONDecodeError as exc:
            raise ValueError('headers are not valid Json object') from exc
    if isinstance(v, dict):
        return v
    return None


class IntegrationHTTPRequest(BaseModel):
    method: Method
    url: AnyUrl
    verify: bool = True
    headers: dict = {}  # type: ignore[type-arg]
    auth: HTTPBasicAuth | None = None
    data: Any = None

    class Config(BaseConfig):
        arbitrary_types_allowed = True

    _normalize_headers = validator('headers', pre=True, allow_reuse=True)(
        load_json
    )  # type: ignore[type-var]


class Credentials(BaseModel):
    identifier: str | None
    password: str


def set_authorization(request: IntegrationHTTPRequest, auth_credentials):
    """Automatic authorization.
    Supports {Authorization: Bearer __token__}
    or Basic Auth.
    """
    creds = Credentials.parse_obj(auth_credentials)
    if creds.password and creds.identifier:
        request.auth = HTTPBasicAuth(creds.identifier, creds.password)
    auth = {'Authorization': f'Bearer {creds.password}'}
    if request.headers:
        request.headers |= auth  # type: ignore[assignment, operator]
    else:
        request.headers = auth  # type: ignore[assignment]


class IntegrationOptions(BaseModel):
    """Add here any option you need to add to the logic"""

    proxy: bool = False
    limit: int = 1000


class IntegrationEventsClient(ABC):
    def __init__(
            self,
            request: IntegrationHTTPRequest,
            options: IntegrationOptions,
            session=requests.Session(),
    ):
        self.request = request
        self.options = options
        self.session = session
        self._set_proxy()
        self._skip_cert_verification()

    @abstractmethod
    def set_request_filter(self, after: Any):
        """TODO: set the next request's filter.
        Example:
        """
        self.request.headers['after'] = after

    def __del__(self):
        try:
            self.session.close()
        except AttributeError as err:
            demisto.debug(
                f'ignore exceptions raised due to session not used by the client. {err=}'
            )

    def call(self, request: IntegrationHTTPRequest) -> requests.Response:
        try:
            response = self.session.request(**request.dict())
            response.raise_for_status()
            return response
        except Exception as exc:
            msg = f'something went wrong with the http call {exc}'
            LOG(msg)
            raise DemistoException(msg) from exc

    def _skip_cert_verification(
            self, skip_cert_verification: Callable = skip_cert_verification
    ):
        if not self.request.verify:
            skip_cert_verification()

    def _set_proxy(self):
        if self.options.proxy:
            ensure_proxy_has_http_prefix()
        else:
            skip_proxy()


class IntegrationGetEvents(ABC):
    def __init__(
            self, client: IntegrationEventsClient, options: IntegrationOptions
    ) -> None:
        self.client = client
        self.options = options

    def run(self):
        stored = []
        for logs in self._iter_events():
            stored.extend(logs)
            if len(stored) >= self.options.limit:
                return stored[:self.options.limit]
        return stored

    def call(self) -> requests.Response:
        return self.client.call(self.client.request)

    @staticmethod
    @abstractmethod
    def get_last_run(events: list) -> dict:
        """Logic to get the last run from the events
        Example:
        """
        return {'after': events[-1]['created']}

    @abstractmethod
    def _iter_events(self):
        """Create iterators with Yield"""
        raise NotImplementedError


# END COPY OF SiemApiModule


class DefenderIntegrationOptions(IntegrationOptions):
    first_fetch: str


class DefenderAuthenticator(BaseModel):
    verify: bool
    url: str
    endpoint_type: str
    scope_url: str
    tenant_id: str
    client_id: str
    credentials: dict
    ms_client: Any = None

    def set_authorization(self, request: IntegrationHTTPRequest):
        try:
            if not self.ms_client:
                demisto.debug(f"try init the ms client for the first time, {self.url=}")
                self.ms_client = MicrosoftClient(
                    endpoint=self.endpoint_type,
                    base_url=self.url,
                    tenant_id=self.tenant_id,
                    auth_id=self.client_id,
                    enc_key=self.credentials.get('password'),
                    scope=urljoin(self.scope_url, "/windowsatpservice/.default"),
                    verify=self.verify,
                    self_deployed=True,
                    command_prefix="microsoft-365-defender",
                )

            token = self.ms_client.get_access_token()
            auth = {'Authorization': f'Bearer {token}'}
            if request.headers:
                request.headers |= auth  # type: ignore[assignment, operator]
            else:
                request.headers = auth  # type: ignore[assignment]

            demisto.debug('getting access token for Defender Authenticator - succeeded')

        except BaseException as e:
            # catch BaseException to catch also sys.exit via return_error
            demisto.error(f'Fail to authenticate with Microsoft services: {str(e)}')

            err_msg = 'Fail to authenticate with Microsoft services, see the error details in the log'
            raise DemistoException(err_msg)


class DefenderHTTPRequest(IntegrationHTTPRequest):
    params: dict | None = {}
    method: Method = Method.GET

    _normalize_url = validator('url', pre=True, allow_reuse=True)(
        lambda base_url: f'{base_url}/api/alerts'
    )  # type: ignore[type-var]


class DefenderClient(IntegrationEventsClient):
    authenticator: DefenderAuthenticator
    request: DefenderHTTPRequest
    options: DefenderIntegrationOptions

    def __init__(self, request: DefenderHTTPRequest, options: IntegrationOptions, authenticator: DefenderAuthenticator):
        self.authenticator = authenticator
        super().__init__(request, options)

    def set_request_filter(self, after: Any):
        limit = min(self.options.limit, MAX_ALERTS_PAGE_SIZE)
        if not after:
            demisto.debug(f'lastRunObj is empty, calculate the first fetch time according {self.options.first_fetch=}')
            first_fetch_date = dateparser.parse(self.options.first_fetch, settings={'TIMEZONE': 'UTC'})
            after = datetime.strftime(first_fetch_date, DEFENDER_DATE_FORMAT)  # type: ignore[arg-type]
        self.request.params = {
            '$filter': f'{ALERT_CREATION_TIME}+gt+{after}',
            '$orderby': f'{ALERT_CREATION_TIME}+asc',
            '$top': limit,
            '$expand': 'evidence',

        }
        demisto.debug(f'setting the request filter to be: {self.request.params}')

    def authenticate(self):
        self.authenticator.set_authorization(self.request)


class DefenderGetEvents(IntegrationGetEvents):
    client: DefenderClient

    def _split_evidence(self, org_alerts):
        """
        Extract evidence and create new alert entry that will contain the alert & evidence, for each evidence.
        """
        res: List[Dict] = []
        if not org_alerts:
            return res

        for alert in org_alerts:
            evidences = alert.pop('evidence', [])
            if evidences:
                for evidence in evidences:
                    updated_alert = alert.copy()
                    updated_alert['evidence'] = evidence
                    res.append(updated_alert)
            else:
                alert['evidence'] = {}
                res.append(alert)

        return res

    def _iter_events(self):

        self.client.authenticate()
        self.client.set_request_filter(demisto.getLastRun() and demisto.getLastRun().get('after'))

        response = self.client.call(self.client.request)
        value = response.json().get('value', [])
        value = self._split_evidence(value)

        demisto.debug(f'getting {len(value)} alerts from Defender Api')
        return [value]

    @staticmethod
    def get_last_run(events: list) -> dict:
        """Logic to get the last run from the events
        """
        return events and len(events) > 0 and {'after': events[-1]['alertCreationTime']} or demisto.getLastRun()


''' HELPER FUNCTIONS '''


''' COMMAND FUNCTIONS '''


def test_module(get_events: DefenderGetEvents) -> str:
    """Tests API connectivity and authentication'

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    :type get_events: ``DefenderGetEvents``
    :param get_events: the get_events instance

    :return: 'ok' if test passed, anything else will fail the test.
    :rtype: ``str``
    """
    try:
        get_events.client.request.params = {'limit': 1}
        get_events.run()
        return 'ok'
    except DemistoException as e:
        if 'Forbidden' in str(e) or 'authenticate' in str(e):
            return AUTH_ERROR_MSG
        raise


def main(command: str, params: dict):
    demisto.debug(f'Command being called is {command}')
    try:
        params_endpoint_type = params.get('endpoint_type') or 'Worldwide'
        params_url = params.get('url')
        # is_gcc wasn't supported in the event collector, thus passing it as None.
        endpoint_type, params_url = microsoft_defender_for_endpoint_get_base_url(params_endpoint_type, params_url)

        parsed_params = copy.copy(params)
        parsed_params["url"] = params_url
        parsed_params["endpoint_type"] = endpoint_type
        parsed_params["scope_url"] = MICROSOFT_DEFENDER_FOR_ENDPOINT_APT_SERVICE_ENDPOINTS[endpoint_type]

        options = DefenderIntegrationOptions.parse_obj(parsed_params)
        request = DefenderHTTPRequest.parse_obj(parsed_params)
        authenticator = DefenderAuthenticator.parse_obj(parsed_params)

        client = DefenderClient(request=request, options=options, authenticator=authenticator)
        get_events = DefenderGetEvents(client=client, options=options)

        if command == 'test-module':
            return_results(test_module(get_events=get_events))

        elif command == 'microsoft-365-defender-get-events':
            events = get_events.run()
            demisto.debug(f'{command=}, publishing events to the context')
            human_readable = tableToMarkdown(name="Alerts:", t=events)
            return_results(
                CommandResults('Microsoft365Defender.alerts', 'id', events, readable_output=human_readable))

            if argToBoolean(params.get('push_to_xsiam', False)):
                demisto.debug(f'{command=}, publishing events to XSIAM')
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)

        elif command == 'fetch-events':
            events = get_events.run()

            demisto.debug(f'{command=}, publishing events to XSIAM')
            send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)

            demisto.setLastRun(get_events.get_last_run(events))
            demisto.debug(f'Last run set to {demisto.getLastRun()}')

        elif command == 'microsoft-365-defender-auth-reset':
            return_results(reset_auth())

    # Log exceptions and return errors
    except Exception as e:
        return_error(f'Failed to execute {demisto.command()} command.\nError:\n{str(e)}')


''' ENTRY POINT '''
if __name__ in ('__main__', '__builtin__', 'builtins'):  # pragma: no cover
    # Args is always stronger. Get getIntegrationContext even stronger
    demisto_params = demisto.params() | demisto.args() | demisto.getLastRun()
    main(demisto.command(), demisto_params)