From adfde591af85f40d9d6ffb93a42261439677f105 Mon Sep 17 00:00:00 2001 From: ianhelle Date: Mon, 15 Sep 2025 10:17:28 -0700 Subject: [PATCH 01/12] Updating Defender drriver for OAuth2 Fix old scopes that were causing authentication errors. --- msticpy/data/drivers/mdatp_driver.py | 41 +++++++++++++++--------- tests/data/drivers/test_odata_drivers.py | 2 +- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index 862cea2ad..9fd82d0b4 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -123,7 +123,11 @@ def __init__( self.req_body["grant_type"] = "client_credentials" if not m365d_params.oauth_v2: - self.req_body["resource"] = self.scopes + # For OAuth 1.0, use resource parameter with the base URI (without .default) + resource_base = self.scopes[0].rstrip(".default") + self.req_body["resource"] = resource_base + # For OAuth 2.0, scopes are handled by the OData parent class authentication methods + # No additional processing needed here as self.scopes is already set correctly if connection_str: self.current_connection = connection_str @@ -184,19 +188,26 @@ def query( def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfiguration: - # pylint: disable=line-too-long """Return API and login URIs for selected provider type. - Note that the Microsoft Graph is the preferred API. + OAuth 2.0 (v2.0 endpoint + scope) is now the default for all Defender APIs. - | API Name | Resource ID | Scopes Requested | API URI (global cloud) | API Endpoint | Login URI | MSTICpy Data Environment | - | -------- | ----------- | ---------------- | ---------------------- | ------------ | --------- | ------------------------ | - | WindowsDefenderATP | fc780465-2017-40d4-a0c5-307022471b92 | `AdvancedQuery.Read` | `https://api.securitycenter.microsoft.com` | `/advancedqueries/run` | `https://login.microsoftonline.com//oauth2/token` | `MDE`, `MDATP` | - | Microsoft Threat Protection | 8ee8fdad-f234-4243-8f3b-15c294843740 | `AdvancedHunting.Read` | `https://api.security.microsoft.com` | `/advancedhunting/run` | `https://login.microsoftonline.com//oauth2/token` | `M365D` | - | Microsoft Graph | 00000003-0000-0000-c000-000000000000 | `ThreatHunting.Read.All` | `https://graph.microsoft.com//` | `/security/runHuntingQuery` | `https://login.microsoftonline.com//oauth2/v2.0/token` | `M365DGraph` | + Summary (all use /.default scope): + - MDE (MDATP): api base https://api.securitycenter.microsoft.com + token: https://login.microsoftonline.com//oauth2/v2.0/token + endpoint: /advancedqueries/run + - M365D: api base https://api.security.microsoft.com + token: https://login.microsoftonline.com//oauth2/v2.0/token + endpoint: /advancedhunting/run + - M365DGraph: api base https://graph.microsoft.com (v1.0) + token: https://login.microsoftonline.com//oauth2/v2.0/token + endpoint: /security/runHuntingQuery + + Scopes use: {resource_uri}/.default (normalized to avoid double slashes). + If a legacy v1 (resource) flow is ever needed, supply a v1 token endpoint + (oauth_v2 flag will become False and caller will add resource parameter). """ - # pylint: enable=line-too-long if data_environment == DataEnvironment.M365DGraph: az_cloud_config = AzureCloudConfig(cloud=cloud) login_uri: str = ( @@ -205,22 +216,20 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura resource_uri: str = az_cloud_config.endpoints["microsoftGraphResourceId"] api_version = "v1.0" api_endpoint = "/security/runHuntingQuery" - scopes: list[str] = [f"{resource_uri}ThreatHunting.Read.All"] - elif data_environment == DataEnvironment.M365D: - login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/token" + login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/v2.0/token" resource_uri = get_m365d_endpoint(cloud) api_version = "api" api_endpoint = "/advancedhunting/run" - scopes = [f"{resource_uri}AdvancedHunting.Read"] - else: - login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/token" + # Default to MDE + login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/v2.0/token" resource_uri = get_defender_endpoint(cloud) api_version = "api" api_endpoint = "/advancedqueries/run" - scopes = [f"{resource_uri}AdvancedQuery.Read"] + scope_base = resource_uri.rstrip("/") + scopes: list[str] = [f"{scope_base}/.default"] api_uri: str = f"{resource_uri}{api_version}{api_endpoint}" return M365DConfiguration( diff --git a/tests/data/drivers/test_odata_drivers.py b/tests/data/drivers/test_odata_drivers.py index 81d7f9eaf..7aa200ddd 100644 --- a/tests/data/drivers/test_odata_drivers.py +++ b/tests/data/drivers/test_odata_drivers.py @@ -84,7 +84,7 @@ def _mde_post_checks(mde_drv, api, httpx): httpx.post.assert_called_once() check.equal( httpx.post.call_args[1]["url"], - "https://login.microsoftonline.com/8360dd21-0294-4240-9128-89611f415c53/oauth2/token", + "https://login.microsoftonline.com/8360dd21-0294-4240-9128-89611f415c53/oauth2/v2.0/token", ) check.is_instance( httpx.post.call_args[1]["content"], From 1dbe2fb0cc6393f15547c1927536012c9716bb92 Mon Sep 17 00:00:00 2001 From: ianhelle Date: Mon, 15 Sep 2025 10:18:41 -0700 Subject: [PATCH 02/12] Updating version to 2.17.0 --- msticpy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msticpy/_version.py b/msticpy/_version.py index 491a8fecd..870a1bed4 100644 --- a/msticpy/_version.py +++ b/msticpy/_version.py @@ -1,3 +1,3 @@ """Version file.""" -VERSION = "2.16.2" +VERSION = "2.17.0" From 09bacbc4af90b3bd9e7284fff487cbacd0fdfc92 Mon Sep 17 00:00:00 2001 From: Ian Hellen Date: Mon, 15 Sep 2025 10:25:31 -0700 Subject: [PATCH 03/12] Update msticpy/data/drivers/mdatp_driver.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- msticpy/data/drivers/mdatp_driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index 9fd82d0b4..d509a5771 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -124,6 +124,8 @@ def __init__( if not m365d_params.oauth_v2: # For OAuth 1.0, use resource parameter with the base URI (without .default) + if not self.scopes: + raise ValueError("No scopes provided in configuration; cannot set resource parameter.") resource_base = self.scopes[0].rstrip(".default") self.req_body["resource"] = resource_base # For OAuth 2.0, scopes are handled by the OData parent class authentication methods From 611e80e8c91d20341d05b03a7d50b16f1fcd3fbd Mon Sep 17 00:00:00 2001 From: ianhelle Date: Mon, 15 Sep 2025 12:31:41 -0700 Subject: [PATCH 04/12] Fixing linting error from copilot change Using urljoin to join URL parts --- msticpy/data/drivers/mdatp_driver.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index d509a5771..158fa18a1 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -3,11 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -"""MDATP OData Driver class.""" +"""MS Defender/Defender 365 OData Driver class.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any, ClassVar, Iterable +from urllib.parse import urljoin import pandas as pd from typing_extensions import Self @@ -125,7 +126,9 @@ def __init__( if not m365d_params.oauth_v2: # For OAuth 1.0, use resource parameter with the base URI (without .default) if not self.scopes: - raise ValueError("No scopes provided in configuration; cannot set resource parameter.") + raise ValueError( + "No scopes provided in configuration; cannot set resource parameter." + ) resource_base = self.scopes[0].rstrip(".default") self.req_body["resource"] = resource_base # For OAuth 2.0, scopes are handled by the OData parent class authentication methods @@ -212,27 +215,31 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura """ if data_environment == DataEnvironment.M365DGraph: az_cloud_config = AzureCloudConfig(cloud=cloud) - login_uri: str = ( - f"{az_cloud_config.authority_uri}{{tenantId}}/oauth2/v2.0/token" + login_uri: str = urljoin( + az_cloud_config.authority_uri, "{tenantId}/oauth2/v2.0/token" ) resource_uri: str = az_cloud_config.endpoints["microsoftGraphResourceId"] api_version = "v1.0" api_endpoint = "/security/runHuntingQuery" elif data_environment == DataEnvironment.M365D: - login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/v2.0/token" + login_uri = urljoin( + get_m365d_login_endpoint(cloud), "{tenantId}/oauth2/v2.0/token" + ) resource_uri = get_m365d_endpoint(cloud) api_version = "api" api_endpoint = "/advancedhunting/run" else: # Default to MDE - login_uri = f"{get_m365d_login_endpoint(cloud)}{{tenantId}}/oauth2/v2.0/token" + login_uri = urljoin( + get_m365d_login_endpoint(cloud), "{tenantId}/oauth2/v2.0/token" + ) resource_uri = get_defender_endpoint(cloud) api_version = "api" api_endpoint = "/advancedqueries/run" scope_base = resource_uri.rstrip("/") scopes: list[str] = [f"{scope_base}/.default"] - api_uri: str = f"{resource_uri}{api_version}{api_endpoint}" + api_uri: str = urljoin(urljoin(resource_uri, api_version), api_endpoint) return M365DConfiguration( login_uri=login_uri, From 3673cf960c3da2bef8ac68ec05a86be05d6dd0ae Mon Sep 17 00:00:00 2001 From: Ian Hellen Date: Mon, 15 Sep 2025 12:33:31 -0700 Subject: [PATCH 05/12] Update msticpy/data/drivers/mdatp_driver.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- msticpy/data/drivers/mdatp_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index 158fa18a1..aae78e450 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -237,7 +237,7 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura api_version = "api" api_endpoint = "/advancedqueries/run" - scope_base = resource_uri.rstrip("/") + scope_base = resource_uri.removesuffix("/") scopes: list[str] = [f"{scope_base}/.default"] api_uri: str = urljoin(urljoin(resource_uri, api_version), api_endpoint) From 7610202e86ba1bdc821372fb5a9c6adb82449140 Mon Sep 17 00:00:00 2001 From: ianhelle Date: Mon, 15 Sep 2025 19:35:21 -0700 Subject: [PATCH 06/12] Some more fixes for OAuthv2 --- msticpy/data/drivers/mdatp_driver.py | 56 ++++----- msticpy/data/drivers/odata_driver.py | 17 ++- tests/data/drivers/test_mdatp_driver.py | 159 ++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 32 deletions(-) create mode 100644 tests/data/drivers/test_mdatp_driver.py diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index aae78e450..6db7d7c6f 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -109,7 +109,9 @@ def __init__( self.api_root = m365d_params.resource_uri self.api_ver = m365d_params.api_version self.api_suffix: str = m365d_params.api_endpoint + # Use scopes from configuration (already normalized '/.default') self.scopes = m365d_params.scopes + resource_base = self.api_root.strip().rstrip("/") self.add_query_filter( "data_environments", ("MDE", "M365D", "MDATP", "M365DGraph", "GraphHunting") @@ -124,15 +126,9 @@ def __init__( self.req_body["grant_type"] = "client_credentials" if not m365d_params.oauth_v2: - # For OAuth 1.0, use resource parameter with the base URI (without .default) - if not self.scopes: - raise ValueError( - "No scopes provided in configuration; cannot set resource parameter." - ) - resource_base = self.scopes[0].rstrip(".default") + # OAuth v1 flow: send 'resource' (base URI, no /.default) not scopes self.req_body["resource"] = resource_base - # For OAuth 2.0, scopes are handled by the OData parent class authentication methods - # No additional processing needed here as self.scopes is already set correctly + # OAuth v2: scopes handled by parent auth logic; do not add 'resource' if connection_str: self.current_connection = connection_str @@ -195,23 +191,23 @@ def query( def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfiguration: """Return API and login URIs for selected provider type. - OAuth 2.0 (v2.0 endpoint + scope) is now the default for all Defender APIs. - - Summary (all use /.default scope): - - MDE (MDATP): api base https://api.securitycenter.microsoft.com - token: https://login.microsoftonline.com//oauth2/v2.0/token - endpoint: /advancedqueries/run - - M365D: api base https://api.security.microsoft.com - token: https://login.microsoftonline.com//oauth2/v2.0/token - endpoint: /advancedhunting/run - - M365DGraph: api base https://graph.microsoft.com (v1.0) - token: https://login.microsoftonline.com//oauth2/v2.0/token - endpoint: /security/runHuntingQuery - - Scopes use: {resource_uri}/.default (normalized to avoid double slashes). - If a legacy v1 (resource) flow is ever needed, supply a v1 token endpoint - (oauth_v2 flag will become False and caller will add resource parameter). - + Validated against Microsoft documentation: + MDE Advanced Queries: + https://learn.microsoft.com/microsoft-365/security/defender-endpoint/run-advanced-query-api + POST https://api.securitycenter.microsoft.com/api/advancedqueries/run + M365 Defender Advanced Hunting: + https://learn.microsoft.com/microsoft-365/security/defender/api/run-advanced-hunting-query + POST https://api.security.microsoft.com/api/advancedhunting/run + Microsoft Graph Security Hunting: + https://learn.microsoft.com/graph/api/security-security-runhuntingquery + POST https://graph.microsoft.com/v1.0/security/runHuntingQuery + + Token acquisition (OAuth 2.0): + https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow + Endpoint: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token + Scope format: https:///.default + + Manual path join retained (urljoin would drop api_version if api_endpoint starts with '/'). """ if data_environment == DataEnvironment.M365DGraph: az_cloud_config = AzureCloudConfig(cloud=cloud) @@ -237,10 +233,14 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura api_version = "api" api_endpoint = "/advancedqueries/run" - scope_base = resource_uri.removesuffix("/") - scopes: list[str] = [f"{scope_base}/.default"] - api_uri: str = urljoin(urljoin(resource_uri, api_version), api_endpoint) + # Do not add '.default' here; keep raw base URI for reuse in both flows. + resource_base = resource_uri.strip().rstrip("/") + scopes: list[str] = [f"{resource_base}/.default"] + # Construct final query execution URI: + # // + api_endpoint_part = api_endpoint.lstrip("/") + api_uri: str = f"{resource_base}/{api_version.strip('/')}/{api_endpoint_part}" return M365DConfiguration( login_uri=login_uri, resource_uri=resource_uri, diff --git a/msticpy/data/drivers/odata_driver.py b/msticpy/data/drivers/odata_driver.py index 1c07555ef..3f8cbf73d 100644 --- a/msticpy/data/drivers/odata_driver.py +++ b/msticpy/data/drivers/odata_driver.py @@ -209,7 +209,11 @@ def connect( err_msg = f"Sub class {self.__class__.__name__} did not set self.api_root" raise ValueError(err_msg) api_ver: str | None = cs_dict.get("apiVersion", self.api_ver) - self.request_uri = self.api_root + str(api_ver) + # Normalize join of api_root and api_ver (api_ver should normally be set) + if api_ver: + self.request_uri = f"{self.api_root.rstrip('/')}/{api_ver.strip('/')}" + else: + self.request_uri = self.api_root.rstrip("/") print("Connected.") self._connected = True @@ -246,9 +250,14 @@ def _get_token_certificate_auth(self: Self, cs_dict: dict[str, Any]) -> None: client_credential=client_credential, authority=authority, ) - result = app.acquire_token_for_client( - scopes=[self.api_root + "/.default"], - ) + # Prefer precomputed scopes (OAuth v2). If not present, derive from api_root. + scopes: list[str] + if self.scopes: + scopes = self.scopes + else: + base_root = (self.api_root or "").rstrip("/") + scopes = [f"{base_root}/.default"] + result = app.acquire_token_for_client(scopes=scopes) if not result or "access_token" not in result: err_msg = "Could not obtain access token" raise MsticpyConnectionError(err_msg) diff --git a/tests/data/drivers/test_mdatp_driver.py b/tests/data/drivers/test_mdatp_driver.py new file mode 100644 index 000000000..e5664ab4f --- /dev/null +++ b/tests/data/drivers/test_mdatp_driver.py @@ -0,0 +1,159 @@ +"""Unit tests for MDATPDriver.""" + +from __future__ import annotations + +import urllib.parse +from typing import Any + +import pandas as pd +import pytest +import respx +from httpx import Response + +from msticpy.data.core.query_defns import DataEnvironment # type: ignore +from msticpy.data.drivers.mdatp_driver import ( + M365DConfiguration, + MDATPDriver, + _select_api, +) + + +@pytest.fixture +def dummy_conn_str() -> str: + """Return a minimal client credential connection string.""" + return "tenant_id=tenant123;client_id=client123;client_secret=secret123" + + +@pytest.fixture +def mock_token_response() -> dict[str, Any]: + """Return a minimal successful token response.""" + return {"access_token": "fake_token_value", "expires_in": 3600} + + +@pytest.fixture +def mock_query_response() -> dict[str, Any]: + """Return a fake query response including Schema for datetime conversion.""" + return { + "Schema": [{"Name": "Timestamp", "Type": "DateTime"}], + "Results": [{"Timestamp": "2024-01-01T12:34:56Z"}], + } + + +def test_select_api_mde() -> None: + """Test API selection for MDE environment.""" + cfg = _select_api(DataEnvironment.MDE, "global") + assert cfg.api_version == "api" + assert cfg.api_endpoint == "/advancedqueries/run" + assert cfg.scopes[0].endswith("/.default") + assert cfg.oauth_v2 is True + + +def test_select_api_m365d() -> None: + """Test API selection for M365 Defender unified environment.""" + cfg = _select_api(DataEnvironment.M365D, "global") + assert cfg.api_endpoint == "/advancedhunting/run" + assert "/api/advancedhunting/run" in cfg.api_uri + + +def test_select_api_graph() -> None: + """Test API selection for Microsoft Graph security hunting.""" + cfg = _select_api(DataEnvironment.M365DGraph, "global") + assert cfg.api_version == "v1.0" + assert cfg.api_endpoint == "/security/runHuntingQuery" + assert "graph.microsoft.com" in cfg.resource_uri + + +@respx.mock +def test_driver_client_credentials_oauth_v2( + dummy_conn_str, mock_token_response, mock_query_response +) -> None: + """Test client credentials auth and query execution (OAuth v2).""" + respx.post(r"https://login\.microsoftonline\.com/tenant123/oauth2/v2.0/token").mock( + return_value=Response(200, json=mock_token_response) + ) + # Instantiate driver (defaults to DataEnvironment.MDE) + driver = MDATPDriver() + + # Mock token endpoint + token_url = driver.oauth_url.format(tenantId="tenant123") + respx.post(token_url).mock(return_value=Response(200, json=mock_token_response)) + driver.connect(connection_str=dummy_conn_str) + + # Mock query endpoint + query_url = ( + f"{driver.api_root.rstrip('/')}/{driver.api_ver.strip('/')}{driver.api_suffix}" + ) + respx.post(query_url).mock(return_value=Response(200, json=mock_query_response)) + + df = driver.query("DeviceNetworkEvents | take 1") + assert isinstance(df, pd.DataFrame) + assert "Timestamp" in df.columns + assert pd.api.types.is_datetime64_any_dtype(df["Timestamp"]) + assert driver.scopes and driver.scopes[0].endswith("/.default") + + +@respx.mock +def test_driver_legacy_oauth_v1_resource_param( + monkeypatch, dummy_conn_str, mock_token_response +) -> None: + """Test legacy OAuth v1 flow sets 'resource' instead of relying on scopes.""" + + def legacy_select_api(env: DataEnvironment, cloud: str) -> M365DConfiguration: # type: ignore + resource_uri = "https://api.securitycenter.microsoft.com/" + return M365DConfiguration( + login_uri="https://login.microsoftonline.com/{tenantId}/oauth2/token", # v1 endpoint (no v2.0) + resource_uri=resource_uri, + api_version="api", + api_endpoint="/advancedqueries/run", + api_uri="https://api.securitycenter.microsoft.com/api/advancedqueries/run", + scopes=[ + f"{resource_uri.rstrip('/')}/.default" + ], # still populated but not used in v1 request body + ) + + monkeypatch.setattr( + "msticpy.data.drivers.mdatp_driver._select_api", legacy_select_api + ) + + driver = MDATPDriver() + assert driver._m365d_params.oauth_v2 is False + assert "resource" in driver.req_body + assert driver.req_body["resource"].endswith("api.securitycenter.microsoft.com") + + token_url = driver.oauth_url.format(tenantId="tenant123") + captured_body: dict[str, str] = {} + + def token_side_effect(request) -> Response: + decoded = urllib.parse.parse_qs(request.content.decode("utf-8")) + captured_body.update({k: v[0] for k, v in decoded.items()}) + return Response(200, json=mock_token_response) + + respx.post(token_url).mock(side_effect=token_side_effect) + driver.connect(connection_str=dummy_conn_str) + + # Force auth call + # Re-run standard auth to capture body (connect already ran in __init__) + driver._get_token_standard_auth( + {}, + { # type: ignore + "tenant_id": "tenant123", + "client_id": "client123", + "client_secret": "secret123", + }, + ) + + assert "resource" in captured_body + assert "scope" not in captured_body # v1 flow should not send scope + assert driver.aad_token == "fake_token_value" + + +@respx.mock +def test_request_uri_join_normalization(dummy_conn_str, mock_token_response) -> None: + """Test request_uri does not duplicate slashes.""" + driver = MDATPDriver() + token_url = driver.oauth_url.format(tenantId="tenant123") + respx.post(token_url).mock(return_value=Response(200, json=mock_token_response)) + driver.connect(connection_str=dummy_conn_str) + assert "//" not in driver.api_root.replace("://", "ยง") # ignore scheme part + assert "//" not in driver.api_ver + assert not driver.api_root.endswith("//") From 57ef19404c90eb29e675cc9c8e79597b92b22323 Mon Sep 17 00:00:00 2001 From: Ian Hellen Date: Mon, 15 Sep 2025 19:38:48 -0700 Subject: [PATCH 07/12] Potential fix for code scanning alert no. 223: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- tests/data/drivers/test_mdatp_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/drivers/test_mdatp_driver.py b/tests/data/drivers/test_mdatp_driver.py index e5664ab4f..d5b3013af 100644 --- a/tests/data/drivers/test_mdatp_driver.py +++ b/tests/data/drivers/test_mdatp_driver.py @@ -60,7 +60,7 @@ def test_select_api_graph() -> None: cfg = _select_api(DataEnvironment.M365DGraph, "global") assert cfg.api_version == "v1.0" assert cfg.api_endpoint == "/security/runHuntingQuery" - assert "graph.microsoft.com" in cfg.resource_uri + assert urllib.parse.urlparse(cfg.resource_uri).hostname == "graph.microsoft.com" @respx.mock From ecc89f6124068475cfdb7d376cb740aa1418cd25 Mon Sep 17 00:00:00 2001 From: Ian Hellen Date: Mon, 15 Sep 2025 19:42:00 -0700 Subject: [PATCH 08/12] Potential fix for code scanning alert no. 224: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- tests/data/drivers/test_mdatp_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/drivers/test_mdatp_driver.py b/tests/data/drivers/test_mdatp_driver.py index d5b3013af..1af25da10 100644 --- a/tests/data/drivers/test_mdatp_driver.py +++ b/tests/data/drivers/test_mdatp_driver.py @@ -118,7 +118,7 @@ def legacy_select_api(env: DataEnvironment, cloud: str) -> M365DConfiguration: driver = MDATPDriver() assert driver._m365d_params.oauth_v2 is False assert "resource" in driver.req_body - assert driver.req_body["resource"].endswith("api.securitycenter.microsoft.com") + assert urllib.parse.urlparse(driver.req_body["resource"]).hostname == "api.securitycenter.microsoft.com" token_url = driver.oauth_url.format(tenantId="tenant123") captured_body: dict[str, str] = {} From 777c64802dfba1502c01ccc22f67443e6d484208 Mon Sep 17 00:00:00 2001 From: ianhelle Date: Mon, 15 Sep 2025 19:43:14 -0700 Subject: [PATCH 09/12] Reformatting after Copilot fix --- tests/data/drivers/test_mdatp_driver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/data/drivers/test_mdatp_driver.py b/tests/data/drivers/test_mdatp_driver.py index 1af25da10..c24646b8f 100644 --- a/tests/data/drivers/test_mdatp_driver.py +++ b/tests/data/drivers/test_mdatp_driver.py @@ -118,7 +118,10 @@ def legacy_select_api(env: DataEnvironment, cloud: str) -> M365DConfiguration: driver = MDATPDriver() assert driver._m365d_params.oauth_v2 is False assert "resource" in driver.req_body - assert urllib.parse.urlparse(driver.req_body["resource"]).hostname == "api.securitycenter.microsoft.com" + assert ( + urllib.parse.urlparse(driver.req_body["resource"]).hostname + == "api.securitycenter.microsoft.com" + ) token_url = driver.oauth_url.format(tenantId="tenant123") captured_body: dict[str, str] = {} From 4588a1be49f43c0dcfe4f9ad692fac610379f61f Mon Sep 17 00:00:00 2001 From: Florian BRACQ Date: Wed, 24 Sep 2025 12:48:27 +0000 Subject: [PATCH 10/12] Fix logical issue --- msticpy/data/drivers/mdatp_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index 6db7d7c6f..457eb2289 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -168,7 +168,7 @@ def query( ) if isinstance(data, pd.DataFrame): # If we got a schema we should convert the DateTimes to pandas datetimes - if ("Schema" or "schema") not in response: + if "Schema" not in response and "schema" not in response: return data if self.data_environment == DataEnvironment.M365DGraph: From 91185ac4ace416cfe40fded1948ca3bc7775dd0e Mon Sep 17 00:00:00 2001 From: Florian BRACQ Date: Wed, 24 Sep 2025 12:48:54 +0000 Subject: [PATCH 11/12] Use logger instead of print --- msticpy/data/drivers/odata_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msticpy/data/drivers/odata_driver.py b/msticpy/data/drivers/odata_driver.py index 3f8cbf73d..b45a0fb4c 100644 --- a/msticpy/data/drivers/odata_driver.py +++ b/msticpy/data/drivers/odata_driver.py @@ -215,7 +215,7 @@ def connect( else: self.request_uri = self.api_root.rstrip("/") - print("Connected.") + LOGGER.info("Connected.") self._connected = True def _get_token_certificate_auth(self: Self, cs_dict: dict[str, Any]) -> None: From 372fd5e458884c019f4a82b642952f88528cf19c Mon Sep 17 00:00:00 2001 From: Florian BRACQ Date: Wed, 24 Sep 2025 12:49:36 +0000 Subject: [PATCH 12/12] Minor linting changes --- msticpy/data/drivers/mdatp_driver.py | 31 +++++++++++++++++++--------- msticpy/data/drivers/odata_driver.py | 9 +++++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/msticpy/data/drivers/mdatp_driver.py b/msticpy/data/drivers/mdatp_driver.py index 457eb2289..8fe3cb590 100644 --- a/msticpy/data/drivers/mdatp_driver.py +++ b/msticpy/data/drivers/mdatp_driver.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, ClassVar, Iterable +from typing import TYPE_CHECKING, Any, ClassVar from urllib.parse import urljoin import pandas as pd @@ -25,6 +25,9 @@ from ..core.query_defns import DataEnvironment from .odata_driver import OData, QuerySource, _get_driver_settings +if TYPE_CHECKING: + from collections.abc import Iterable + __version__ = VERSION __author__ = "Pete Bryan" @@ -94,7 +97,9 @@ def __init__( ) cs_dict: dict[str, str] = _get_driver_settings( - self.CONFIG_NAME, self._ALT_CONFIG_NAMES, instance + self.CONFIG_NAME, + self._ALT_CONFIG_NAMES, + instance, ) self.cloud: str = cs_dict.pop("cloud", "global") @@ -102,7 +107,8 @@ def __init__( self.cloud = cloud m365d_params: M365DConfiguration = _select_api( - self.data_environment, self.cloud + self.data_environment, + self.cloud, ) self._m365d_params: M365DConfiguration = m365d_params self.oauth_url = m365d_params.login_uri @@ -114,7 +120,8 @@ def __init__( resource_base = self.api_root.strip().rstrip("/") self.add_query_filter( - "data_environments", ("MDE", "M365D", "MDATP", "M365DGraph", "GraphHunting") + "data_environments", + ("MDE", "M365D", "MDATP", "M365DGraph", "GraphHunting"), ) self.req_body: dict[str, Any] = {} @@ -164,7 +171,9 @@ def query( """ del query_source, kwargs data, response = self.query_with_results( - query, body=True, api_end=self.api_suffix + query, + body=True, + api_end=self.api_suffix, ) if isinstance(data, pd.DataFrame): # If we got a schema we should convert the DateTimes to pandas datetimes @@ -183,8 +192,7 @@ def query( for field in response["Schema"] if field["Type"] == "DateTime" ] - data = ensure_df_datetimes(data, columns=date_fields) - return data + return ensure_df_datetimes(data, columns=date_fields) return str(response) @@ -212,14 +220,16 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura if data_environment == DataEnvironment.M365DGraph: az_cloud_config = AzureCloudConfig(cloud=cloud) login_uri: str = urljoin( - az_cloud_config.authority_uri, "{tenantId}/oauth2/v2.0/token" + az_cloud_config.authority_uri, + "{tenantId}/oauth2/v2.0/token", ) resource_uri: str = az_cloud_config.endpoints["microsoftGraphResourceId"] api_version = "v1.0" api_endpoint = "/security/runHuntingQuery" elif data_environment == DataEnvironment.M365D: login_uri = urljoin( - get_m365d_login_endpoint(cloud), "{tenantId}/oauth2/v2.0/token" + get_m365d_login_endpoint(cloud), + "{tenantId}/oauth2/v2.0/token", ) resource_uri = get_m365d_endpoint(cloud) api_version = "api" @@ -227,7 +237,8 @@ def _select_api(data_environment: DataEnvironment, cloud: str) -> M365DConfigura else: # Default to MDE login_uri = urljoin( - get_m365d_login_endpoint(cloud), "{tenantId}/oauth2/v2.0/token" + get_m365d_login_endpoint(cloud), + "{tenantId}/oauth2/v2.0/token", ) resource_uri = get_defender_endpoint(cloud) api_version = "api" diff --git a/msticpy/data/drivers/odata_driver.py b/msticpy/data/drivers/odata_driver.py index b45a0fb4c..7b9170964 100644 --- a/msticpy/data/drivers/odata_driver.py +++ b/msticpy/data/drivers/odata_driver.py @@ -11,7 +11,7 @@ import re import urllib.parse from pathlib import Path -from typing import Any, ClassVar, Iterable +from typing import TYPE_CHECKING, Any, ClassVar import httpx import pandas as pd @@ -21,8 +21,6 @@ from msal.application import ConfidentialClientApplication from typing_extensions import Self -from msticpy.common.provider_settings import ProviderSettings - from ..._version import VERSION from ...auth.msal_auth import MSALDelegatedAuth from ...common.exceptions import MsticpyConnectionError, MsticpyUserConfigError @@ -31,6 +29,11 @@ from ...common.utility import mp_ua_header from .driver_base import DriverBase, DriverProps, QuerySource +if TYPE_CHECKING: + from collections.abc import Iterable + + from msticpy.common.provider_settings import ProviderSettings + __version__: str = VERSION __author__: str = "Pete Bryan"