Skip to content

Commit

Permalink
Ianhelle/mssentinel auth 2023 08 01 (#690)
Browse files Browse the repository at this point in the history
* Added options for MicrosoftSentinel authentication

- az_connect now honors "cloud" parameter for auth_methods in azure_auth.py
- azure_data.py - added ability to specify cloud parameter in connect and also pass credential to az_auth
- sentinel_core.py - - added ability to specify cloud parameter in connect and also pass credential to az_auth
  also changed internal token attribute to _token
- fixed test_sentinel_dynamic_summary.py to use same msticpyconfig throughout test.

* Adding logging to azure_data and sentinel_core init and connect code
  • Loading branch information
ianhelle committed Aug 8, 2023
1 parent 08c89f4 commit 5ef1337
Show file tree
Hide file tree
Showing 14 changed files with 121 additions and 47 deletions.
6 changes: 5 additions & 1 deletion msticpy/auth/azure_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ def az_connect(
Set True to hide all output during connection, by default False
credential : AzureCredential
If an Azure credential is passed, it will be used directly.
cloud : str, optional
What Azure cloud to connect to.
By default it will attempt to use the cloud setting from config file.
If this is not set it will default to Azure Public Cloud
Returns
-------
Expand All @@ -74,7 +78,7 @@ def az_connect(
list_auth_methods
"""
az_cloud_config = AzureCloudConfig()
az_cloud_config = AzureCloudConfig(cloud=kwargs.get("cloud"))
# Use auth_methods param or configuration defaults
data_provs = get_provider_settings(config_section="DataProviders")
auth_methods = auth_methods or az_cloud_config.auth_methods
Expand Down
22 changes: 21 additions & 1 deletion msticpy/context/azure/azure_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# --------------------------------------------------------------------------
"""Uses the Azure Python SDK to collect and return details related to Azure."""
import datetime
import logging
from typing import Any, Dict, List, Optional, Tuple

import attr
Expand Down Expand Up @@ -55,6 +56,8 @@
__version__ = VERSION
__author__ = "Pete Bryan"

logger = logging.getLogger(__name__)

_CLIENT_MAPPING = {
"sub_client": SubscriptionClient,
"resource_client": ResourceManagementClient,
Expand Down Expand Up @@ -129,6 +132,7 @@ def __init__(self, connect: bool = False, cloud: Optional[str] = None):
self.compute_client: Optional[ComputeManagementClient] = None
self.cloud = cloud or AzureCloudConfig().cloud
self.endpoints = get_all_endpoints(self.cloud) # type: ignore
logger.info("Initialized AzureData")
if connect:
self.connect()

Expand All @@ -137,6 +141,7 @@ def connect(
auth_methods: Optional[List] = None,
tenant_id: Optional[str] = None,
silent: bool = False,
**kwargs,
):
"""
Authenticate to the Azure SDK.
Expand All @@ -150,17 +155,31 @@ def connect(
tenant for the identity will be used.
silent : bool, optional
Set true to prevent output during auth process, by default False
cloud : str, optional
What Azure cloud to connect to.
By default it will attempt to use the cloud setting from config file.
If this is not set it will default to Azure Public Cloud
**kwargs
Additional keyword arguments to pass to the az_connect function.
Raises
------
CloudError
If no valid credentials are found or if subscription client can't be created
See Also
--------
msticpy.auth.azure_auth.az_connect : function to authenticate to Azure SDK
"""
if kwargs.get("cloud"):
logger.info("Setting cloud to %s", kwargs["cloud"])
self.cloud = kwargs["cloud"]
self.azure_cloud_config = AzureCloudConfig(self.cloud)
auth_methods = auth_methods or self.az_cloud_config.auth_methods
tenant_id = tenant_id or self.az_cloud_config.tenant_id
self.credentials = az_connect(
auth_methods=auth_methods, tenant_id=tenant_id, silent=silent
auth_methods=auth_methods, tenant_id=tenant_id, silent=silent, **kwargs
)
if not self.credentials:
raise CloudError("Could not obtain credentials.")
Expand All @@ -175,6 +194,7 @@ def connect(
)
if not self.sub_client:
raise CloudError("Could not create a Subscription client.")
logger.info("Connected to Azure Subscription Client")
self.connected = True

def get_subscriptions(self) -> pd.DataFrame:
Expand Down
4 changes: 2 additions & 2 deletions msticpy/context/azure/sentinel_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def create_analytic_rule( # pylint: disable=too-many-arguments, too-many-locals
params = {"api-version": "2020-01-01"}
response = httpx.put(
analytic_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -305,7 +305,7 @@ def delete_analytic_rule(
params = {"api-version": "2020-01-01"}
response = httpx.delete(
analytic_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
timeout=get_http_timeout(),
)
Expand Down
4 changes: 2 additions & 2 deletions msticpy/context/azure/sentinel_bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def create_bookmark(
params = {"api-version": "2020-01-01"}
response = httpx.put(
bookmark_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -123,7 +123,7 @@ def delete_bookmark(
params = {"api-version": "2020-01-01"}
response = httpx.delete(
bookmark_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
timeout=get_http_timeout(),
)
Expand Down
66 changes: 57 additions & 9 deletions msticpy/context/azure/sentinel_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""Uses the Microsoft Sentinel APIs to interact with Microsoft Sentinel Workspaces."""

import contextlib
import logging
from typing import Any, Dict, List, Optional

import pandas as pd
Expand All @@ -32,6 +33,8 @@
__version__ = VERSION
__author__ = "Pete Bryan"

logger = logging.getLogger(__name__)


# pylint: disable=too-many-ancestors, too-many-instance-attributes
class MicrosoftSentinel(
Expand Down Expand Up @@ -95,13 +98,23 @@ def __init__(
self.sent_urls: Dict[str, str] = {}
self.sent_data_query: Optional[SentinelQueryProvider] = None # type: ignore
self.url: Optional[str] = None
self._token: Optional[str] = None

workspace = kwargs.get("workspace", ws_name)
self._default_workspace: Optional[str] = workspace
self.workspace_config = WorkspaceConfig(workspace)

logger.info("Initializing Microsoft Sentinel connector")
logger.info(
"Params: Cloud=%s; ResourceId=%s; Workspace=%s",
self.cloud,
self._resource_id,
workspace,
)

if self._resource_id:
# If a resource ID is supplied, use that
logger.info("Initializing from resource ID")
self.url = self._build_sent_paths(self._resource_id, self.base_url) # type: ignore
res_id_parts = parse_resource_id(self._resource_id)
self.default_subscription = res_id_parts["subscription_id"]
Expand All @@ -111,8 +124,10 @@ def __init__(
self.workspace_config = WorkspaceConfig(
workspace=self._default_workspace
)
logger.info("Workspace settings found for %s", self._default_workspace)
else:
# Otherwise - use details from specified workspace or default from settings
logger.info("Initializing from workspace settings")
self.default_subscription = self.workspace_config.get(
"subscription_id", sub_id
)
Expand All @@ -125,6 +140,7 @@ def __init__(
res_grp=self._default_resource_group,
ws_name=workspace_name,
)
logger.info("Resource ID set to %s", self._resource_id)
self._default_workspace = workspace_name
self.url = self._build_sent_paths(
self._resource_id, self.base_url # type: ignore
Expand All @@ -151,26 +167,48 @@ def connect(
Specify cloud tenant to use
silent : bool, optional
Set true to prevent output during auth process, by default False
cloud : str, optional
What Azure cloud to connect to.
By default it will attempt to use the cloud setting from config file.
If this is not set it will default to Azure Public Cloud
credential: AzureCredential, optional
Credentials to use for authentication. This will use the credential
directly and bypass the MSTICPy Azure credential selection process.
See Also
--------
msticpy.auth.azure_auth.az_connect : function to authenticate to Azure SDK
"""
if workspace := kwargs.get("workspace"):
# override any previous default setting
self.workspace_config = WorkspaceConfig(workspace)
logger.info("Using workspace settings found for %s", workspace)
if not self.workspace_config:
self.workspace_config = WorkspaceConfig()
logger.info(
"Using default workspace settings for %s",
self.workspace_config.get(WorkspaceConfig.CONF_WS_NAME_KEY),
)
tenant_id = (
tenant_id or self.workspace_config[WorkspaceConfig.CONF_TENANT_ID_KEY]
)

super().connect(auth_methods=auth_methods, tenant_id=tenant_id, silent=silent)
if "token" in kwargs:
self.token = kwargs["token"]
else:
self.token = get_token(
logger.info("Using tenant id %s", tenant_id)
self._token = kwargs.pop("token", None)
super().connect(
auth_methods=auth_methods, tenant_id=tenant_id, silent=silent, **kwargs
)
if not self._token:
logger.info("Getting token for %s", tenant_id)
self._token = get_token(
self.credentials, tenant_id=tenant_id, cloud=self.user_cloud # type: ignore
)

with contextlib.suppress(KeyError):
logger.info(
"Setting default subscription to %s from workspace settings",
self.default_subscription,
)
self.default_subscription = self.workspace_config[
WorkspaceConfig.CONF_SUB_ID_KEY
]
Expand All @@ -197,21 +235,25 @@ def _create_api_paths_for_workspace(
"""Save configuration and build API URLs for workspace."""
if workspace_name:
self.workspace_config = WorkspaceConfig(workspace=workspace_name)
az_resource_id = az_resource_id or self._resource_id
if not az_resource_id:
az_resource_id = self._build_sent_res_id(
az_resource_id = (
az_resource_id
or self._resource_id
or self._build_sent_res_id(
subscription_id, resource_group, workspace_name # type: ignore
)
)
az_resource_id = validate_res_id(az_resource_id)
self.url = self._build_sent_paths(az_resource_id, self.base_url) # type: ignore

self.sent_urls = {
name: f"{self.url}{mapping}" for name, mapping in _PATH_MAPPING.items()
}
logger.info("API URLs set to %s", self.sent_urls)

def set_default_subscription(self, subscription_id: str):
"""Set the default subscription to use to `subscription_id`."""
subs_df = self.get_subscriptions()
logger.info("Setting default subscription to %s", subscription_id)
if subscription_id in subs_df["Subscription ID"].values:
self.default_subscription = subscription_id
else:
Expand Down Expand Up @@ -254,6 +296,7 @@ def set_default_workspace(
ws_res_id: Optional[str] = None
# if workspace not supplied trying looking up in subscription
if not workspace:
logger.info("Trying to set default workspace from subscription %s", sub_id)
workspaces = self.get_sentinel_workspaces(sub_id=sub_id)
if len(workspaces) == 1:
# if only one, use that one
Expand All @@ -262,6 +305,7 @@ def set_default_workspace(

# if workspace is one that we have configuration for, get the details from there.
if self._default_workspace in WorkspaceConfig.list_workspaces():
logger.info("Workspace %s found in settings", self._default_workspace)
self.workspace_config = WorkspaceConfig(workspace=self._default_workspace)
elif ws_res_id:
# otherwise construct partial settings
Expand All @@ -274,6 +318,10 @@ def set_default_workspace(
"ResourceGroup": res_id_parts["resource_group"],
}
)
logger.info(
"Workspace not found in settings, using partial workspace config %s",
self.workspace_config,
)

@property
def default_workspace_settings(self) -> Optional[Dict[str, Any]]:
Expand Down
6 changes: 3 additions & 3 deletions msticpy/context/azure/sentinel_dynamic_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def get_dynamic_summary(
params = {"api-version": _DYN_SUM_API_VERSION}
response = httpx.get(
dyn_sum_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
timeout=get_http_timeout(),
)
Expand Down Expand Up @@ -219,7 +219,7 @@ def _create_dynamic_summary(
params = {"api-version": _DYN_SUM_API_VERSION}
response = httpx.put(
dyn_sum_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=summary.to_json_api(),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -325,7 +325,7 @@ def delete_dynamic_summary(
params = {"api-version": _DYN_SUM_API_VERSION}
response = httpx.delete(
dyn_sum_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
timeout=get_http_timeout(),
)
Expand Down
14 changes: 7 additions & 7 deletions msticpy/context/azure/sentinel_incidents.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def get_entities(self, incident: str) -> list:
ent_parameters = {"api-version": "2021-04-01"}
ents = httpx.post(
entities_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=ent_parameters,
timeout=get_http_timeout(),
)
Expand Down Expand Up @@ -134,7 +134,7 @@ def get_incident_alerts(self, incident: str) -> list:
alerts_parameters = {"api-version": "2021-04-01"}
alerts_resp = httpx.post(
alerts_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=alerts_parameters,
timeout=get_http_timeout(),
)
Expand Down Expand Up @@ -252,7 +252,7 @@ def update_incident(
data = _build_sent_data(update_items, etag=incident_dets.iloc[0]["etag"])
response = httpx.put(
incident_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -329,7 +329,7 @@ def create_incident( # pylint: disable=too-many-arguments, too-many-locals
data = _build_sent_data(data_items, props=True)
response = httpx.put(
incident_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand All @@ -347,7 +347,7 @@ def create_incident( # pylint: disable=too-many-arguments, too-many-locals
params = {"api-version": "2021-04-01"}
response = httpx.put(
relations_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -426,7 +426,7 @@ def post_comment(
data = _build_sent_data({"message": comment})
response = httpx.put(
comment_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down Expand Up @@ -467,7 +467,7 @@ def add_bookmark_to_incident(self, incident: str, bookmark: str):
params = {"api-version": "2021-04-01"}
response = httpx.put(
bookmark_url,
headers=get_api_headers(self.token), # type: ignore
headers=get_api_headers(self._token), # type: ignore
params=params,
content=str(data),
timeout=get_http_timeout(),
Expand Down

0 comments on commit 5ef1337

Please sign in to comment.