Skip to content

Commit 337632d

Browse files
ryan-detect-dot-devRyan Cobbianhelle
authored
Update MDATP Driver for delegated auth (#784)
* Add driver and docs * Add M365DGraph data environment to compatible driver list * Remove warnings import * Fix numpy 2.0 NaN now being nan test failure * Default to delegated auth when username is present in config or cs --------- Co-authored-by: Ryan Cobb <[email protected]> Co-authored-by: Ian Hellen <[email protected]>
1 parent f197a68 commit 337632d

File tree

4 files changed

+138
-57
lines changed

4 files changed

+138
-57
lines changed

docs/source/data_acquisition/DataProv-MSDefender.rst

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,37 @@ M365 Defender Configuration
1616
Creating a Client App for M365 Defender
1717
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1818

19-
Details on registering an Azure AD application for MS 365 Defender can be found
20-
`here <https://docs.microsoft.com/windows/security/threat-protection/microsoft-defender-atp/exposed-apis-create-app-webapp>`__.
19+
Microsoft 365 Defender APIs can be accessed in both `application <https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-create-app-webapp>`
20+
and `delegated user contexts <https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-create-app-nativeapp>`.
21+
Accessing Microsoft 365 Defender APIs as an application requires
22+
either a client secret or certificate, while delegated user auth requires
23+
an interactive signin through a browser or via device code.
24+
25+
As such, the details on registering an Azure AD application for MS 365 Defender
26+
are different for application and delegated user auth scenarios. Please
27+
see the above links for more information. Notably, delegated user auth
28+
scenarios do not require a application credential and thus is preferrable.
29+
30+
For delegated user auth scenarios, ensure that the application has a
31+
"Mobile or Desktop Application" redirect URI configured as `http://localhost`.
32+
A redirect URI is not required for applications with their own credentials.
33+
34+
API permissions for the client application will require tenant admin consent.
35+
Ensure that the consented permissions are correct for the chosen data environment
36+
and auth scenario (application or delegated user):
37+
38+
+-----------------------------+------------------------+------------------+
39+
| API Name | Permission | Data Environment |
40+
+=============================+========================+==================+
41+
| WindowsDefenderATP | AdvancedQuery.Read | MDE, MDATP |
42+
+-----------------------------+------------------------+------------------+
43+
| Microsoft Threat Protection | AdvancedHunting.Read | M365D |
44+
+-----------------------------+------------------------+------------------+
45+
| Microsoft Graph | ThreatHunting.Read.All | M365DGraph |
46+
+-----------------------------+------------------------+------------------+
47+
2148
Once you have registered the application, you can use it to connect to
22-
the MS Defender API.
49+
the MS Defender API using the chosen data environment.
2350

2451
M365 Defender Configuration in MSTICPy
2552
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -39,13 +66,13 @@ The settings in the file should look like the following:
3966
MicrosoftDefender:
4067
Args:
4168
ClientId: "CLIENT ID"
42-
ClientSecret: "CLIENT SECRET"
4369
TenantId: "TENANT ID"
4470
UserName: "User Name"
4571
Cloud: "global"
4672
4773
48-
We strongly recommend storing the client secret value
74+
If connecting to the MS Defender 365 API using application auth,
75+
we strongly recommend storing the client secret value
4976
in Azure Key Vault. You can replace the text value with a referenced
5077
to a Key Vault secret using the MSTICPy configuration editor.
5178
See :doc:`msticpy Settings Editor <../getting_started/SettingsEditor>`)
@@ -166,6 +193,7 @@ the required parameters are:
166193
* client_secret -- The secret used for by the application.
167194
* username -- If using delegated auth for your application.
168195

196+
The client_secret and username parameters are mutually exclusive.
169197

170198
.. code:: ipython3
171199

msticpy/data/core/data_providers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"mde": ["m365d"],
4242
"mssentinel_new": ["mssentinel", "m365d"],
4343
"kusto_new": ["kusto"],
44+
"m365dgraph": ["mde", "m365d"],
4445
}
4546

4647
logger: logging.Logger = logging.getLogger(__name__)

msticpy/data/drivers/mdatp_driver.py

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
"""MDATP OData Driver class."""
7-
from typing import Any, Optional, Union
7+
from dataclasses import dataclass, field
8+
from typing import Any, Dict, List, Optional, Union
89

910
import pandas as pd
1011

@@ -24,6 +25,33 @@
2425
__author__ = "Pete Bryan"
2526

2627

28+
@dataclass
29+
class M365DConfiguration:
30+
"""A container for M365D API settings.
31+
32+
This is based on the data environment of the query provider.
33+
"""
34+
35+
login_uri: str
36+
resource_uri: str
37+
api_version: str
38+
api_endpoint: str
39+
api_uri: str
40+
scopes: List[str]
41+
oauth_v2: bool = field(init=False)
42+
43+
def __post_init__(self):
44+
"""Determine if the selected API supports Entra ID OAuth v2.0.
45+
46+
This is important because the fields in the request body
47+
are different between the two versions.
48+
"""
49+
if "/oauth2/v2.0" in self.login_uri:
50+
self.oauth_v2 = True
51+
else:
52+
self.oauth_v2 = False
53+
54+
2755
@export
2856
class MDATPDriver(OData):
2957
"""KqlDriver class to retrieve date from MS Defender APIs."""
@@ -46,6 +74,7 @@ def __init__(
4674
4775
"""
4876
super().__init__(**kwargs)
77+
4978
cs_dict = _get_driver_settings(
5079
self.CONFIG_NAME, self._ALT_CONFIG_NAMES, instance
5180
)
@@ -54,40 +83,37 @@ def __init__(
5483
if "cloud" in kwargs and kwargs["cloud"]:
5584
self.cloud = kwargs["cloud"]
5685

57-
api_uri, oauth_uri, api_suffix = _select_api_uris(
58-
self.data_environment, self.cloud
59-
)
86+
m365d_params = _select_api(self.data_environment, self.cloud)
87+
self._m365d_params: M365DConfiguration = m365d_params
88+
self.oauth_url = m365d_params.login_uri
89+
self.api_root = m365d_params.resource_uri
90+
self.api_ver = m365d_params.api_version
91+
self.api_suffix = m365d_params.api_endpoint
92+
self.scopes = m365d_params.scopes
93+
6094
self.add_query_filter(
61-
"data_environments", ("MDE", "M365D", "MDATP", "GraphHunting")
95+
"data_environments", ("MDE", "M365D", "MDATP", "M365DGraph", "GraphHunting")
6296
)
6397

64-
self.req_body = {
65-
"client_id": None,
66-
"client_secret": None,
67-
"grant_type": "client_credentials",
68-
"resource": api_uri,
69-
}
70-
self.oauth_url = oauth_uri
71-
self.api_root = api_uri
72-
self.api_ver = "api"
73-
self.api_suffix = api_suffix
74-
if self.data_environment == DataEnvironment.M365D:
75-
self.scopes = [f"{api_uri}/AdvancedHunting.Read"]
76-
elif self.data_environment == DataEnvironment.M365DGraph:
77-
self.api_ver = kwargs.get("api_ver", "v1.0")
78-
self.req_body = {
79-
"client_id": None,
80-
"client_secret": None,
81-
"grant_type": "client_credentials",
82-
"scope": f"{self.api_root}.default",
83-
}
84-
self.scopes = [f"{api_uri}/ThreatHunting.Read.All"]
98+
self.req_body: Dict[str, Any] = {}
99+
if "username" in cs_dict:
100+
delegated_auth = True
101+
85102
else:
86-
self.scopes = [f"{api_uri}/AdvancedQuery.Read"]
103+
delegated_auth = False
104+
self.req_body["grant_type"] = "client_credentials"
105+
106+
if not m365d_params.oauth_v2:
107+
self.req_body["resource"] = self.scopes
87108

88109
if connection_str:
89110
self.current_connection = connection_str
90-
self.connect(connection_str)
111+
self.connect(
112+
connection_str,
113+
delegated_auth=delegated_auth,
114+
auth_type=kwargs.get("auth_type", "interactive"),
115+
location=cs_dict.get("location", "token_cache.bin"),
116+
)
91117

92118
def query(
93119
self, query: str, query_source: Optional[QuerySource] = None, **kwargs
@@ -135,26 +161,49 @@ def query(
135161
return response
136162

137163

138-
def _select_api_uris(data_environment, cloud):
139-
"""Return API and login URIs for selected provider type."""
140-
login_uri = get_m365d_login_endpoint(cloud)
141-
if data_environment == DataEnvironment.M365D:
142-
return (
143-
get_m365d_endpoint(cloud),
144-
f"{login_uri}{{tenantId}}/oauth2/token",
145-
"/advancedhunting/run",
146-
)
164+
def _select_api(data_environment, cloud) -> M365DConfiguration:
165+
# pylint: disable=line-too-long
166+
"""Return API and login URIs for selected provider type.
167+
168+
Note that the Microsoft Graph is the preferred API.
169+
170+
| API Name | Resource ID | Scopes Requested | API URI (global cloud) | API Endpoint | Login URI | MSTICpy Data Environment |
171+
| -------- | ----------- | ---------------- | ---------------------- | ------------ | --------- | ------------------------ |
172+
| WindowsDefenderATP | fc780465-2017-40d4-a0c5-307022471b92 | `AdvancedQuery.Read` | `https://api.securitycenter.microsoft.com` | `/advancedqueries/run` | `https://login.microsoftonline.com/<tenantId>/oauth2/token` | `MDE`, `MDATP` |
173+
| Microsoft Threat Protection | 8ee8fdad-f234-4243-8f3b-15c294843740 | `AdvancedHunting.Read` | `https://api.security.microsoft.com` | `/advancedhunting/run` | `https://login.microsoftonline.com/<tenantId>/oauth2/token` | `M365D` |
174+
| Microsoft Graph | 00000003-0000-0000-c000-000000000000 | `ThreatHunting.Read.All` | `https://graph.microsoft.com/<version>/` | `/security/runHuntingQuery` | `https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token` | `M365DGraph` |
175+
176+
"""
177+
# pylint: enable=line-too-long
147178
if data_environment == DataEnvironment.M365DGraph:
148179
az_cloud_config = AzureCloudConfig(cloud=cloud)
149-
api_uri = az_cloud_config.endpoints.get("microsoftGraphResourceId")
150-
graph_login = az_cloud_config.authority_uri
151-
return (
152-
api_uri,
153-
f"{graph_login}{{tenantId}}/oauth2/v2.0/token",
154-
"/security/runHuntingQuery",
155-
)
156-
return (
157-
get_defender_endpoint(cloud),
158-
f"{login_uri}{{tenantId}}/oauth2/token",
159-
"/advancedqueries/run",
180+
login_uri = f"{az_cloud_config.authority_uri}{{tenantId}}/oauth2/v2.0/token"
181+
resource_uri = az_cloud_config.endpoints["microsoftGraphResourceId"]
182+
api_version = "v1.0"
183+
api_endpoint = "/security/runHuntingQuery"
184+
scopes = [f"{resource_uri}ThreatHunting.Read.All"]
185+
186+
elif data_environment == DataEnvironment.M365D:
187+
login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/token"
188+
resource_uri = get_m365d_endpoint(cloud)
189+
api_version = "api"
190+
api_endpoint = "/advancedhunting/run"
191+
scopes = [f"{resource_uri}AdvancedHunting.Read"]
192+
193+
else:
194+
login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/token"
195+
resource_uri = get_defender_endpoint(cloud)
196+
api_version = "api"
197+
api_endpoint = "/advancedqueries/run"
198+
scopes = [f"{resource_uri}AdvancedQuery.Read"]
199+
200+
api_uri = f"{resource_uri}{api_version}{api_endpoint}"
201+
202+
return M365DConfiguration(
203+
login_uri=login_uri,
204+
resource_uri=resource_uri,
205+
api_version=api_version,
206+
api_endpoint=api_endpoint,
207+
api_uri=api_uri,
208+
scopes=scopes,
160209
)

msticpy/data/drivers/odata_driver.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,14 @@ def connect(
153153
help_uri=("Connecting to OData sources.", _HELP_URI),
154154
)
155155

156-
# Default to using application based authentication
157-
if not delegated_auth:
158-
json_response = self._get_token_standard_auth(kwargs, cs_dict)
159-
else:
156+
# Default to using delegated auth if username is present
157+
if "username" in cs_dict:
158+
delegated_auth = True
159+
160+
if delegated_auth:
160161
json_response = self._get_token_delegate_auth(kwargs, cs_dict)
162+
else:
163+
json_response = self._get_token_standard_auth(kwargs, cs_dict)
161164

162165
self.req_headers["Authorization"] = f"Bearer {self.aad_token}"
163166
self.api_root = cs_dict.get("apiRoot", self.api_root)

0 commit comments

Comments
 (0)