Skip to content

Commit 409de94

Browse files
authored
Compat fixes for Bokeh 3.7 (#840)
* Compat fixes for Bokeh 3.7 Also suppressing classes of mypy errors in bokeh modules since it is broadly incompatible with Bokeh. Fixing flake8 error in common/utilty/types * Fix for vt-py changing structure VTObject dictionary - causing conversion to dataframe to contain hundreds of columns. Limiting json_normalize to max_levels=0 * Fix for using AzCli credential with Managed Identity * Mypy fix for azure_auth_core.py
1 parent 87482f4 commit 409de94

File tree

14 files changed

+273
-36
lines changed

14 files changed

+273
-36
lines changed

msticpy/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version file."""
22

3-
VERSION = "2.16.1"
3+
VERSION = "2.16.2"

msticpy/auth/azure_auth_core.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
"""Azure KeyVault pre-authentication."""
7+
78
from __future__ import annotations
89

910
import logging
@@ -16,6 +17,7 @@
1617

1718
from azure.common.credentials import get_cli_profile
1819
from azure.core.credentials import TokenCredential
20+
from azure.core.exceptions import ClientAuthenticationError
1921
from azure.identity import (
2022
AzureCliCredential,
2123
AzurePowerShellCredential,
@@ -150,27 +152,62 @@ def _build_cli_client(
150152
) -> AzureCliCredential:
151153
"""Build a credential from Azure CLI."""
152154
del kwargs
153-
if tenant_id:
154-
return AzureCliCredential(tenant_id=tenant_id)
155-
return AzureCliCredential()
155+
if tenant_id is not None:
156+
try:
157+
logger.info("Creating Azure CLI credential with tenant_id")
158+
cred = AzureCliCredential(tenant_id=tenant_id)
159+
# Attempt to get a token immediately to validate the credential
160+
cred.get_token("https://management.azure.com/.default")
161+
return cred
162+
except ClientAuthenticationError as ex:
163+
logger.info("Azure CLI credential failed to authenticate: %s", str(ex))
164+
# Check if the error is related to tenant ID
165+
if "Tenant" not in str(ex).lower():
166+
raise # re-raise if it's a different error
167+
logger.info("Creating Azure CLI credential without tenant_id")
168+
cred = AzureCliCredential()
169+
cred.get_token("https://management.azure.com/.default")
170+
return cred
156171

157172

158173
def _build_msi_client(
159174
tenant_id: str | None = None,
160-
aad_uri: str | None = None,
161175
client_id: str | None = None,
162176
**kwargs,
163177
) -> ManagedIdentityCredential:
164178
"""Build a credential from Managed Identity."""
165179
msi_kwargs: dict[str, Any] = kwargs.copy()
166180
client_id = client_id or os.environ.get(AzureCredEnvNames.AZURE_CLIENT_ID)
167181

168-
return ManagedIdentityCredential(
169-
tenant_id=tenant_id,
170-
authority=aad_uri,
171-
client_id=client_id,
172-
**msi_kwargs,
173-
)
182+
try:
183+
cred = ManagedIdentityCredential(client_id=client_id)
184+
cred.get_token("https://management.azure.com/.default")
185+
return cred
186+
except ClientAuthenticationError as ex:
187+
logger.info(
188+
(
189+
"Managed Identity credential failed to authenticate: %s, retrying with args "
190+
"tenant_id=%s, client_id=%s, kwargs=%s"
191+
),
192+
str(ex),
193+
tenant_id,
194+
client_id,
195+
msi_kwargs,
196+
)
197+
198+
try:
199+
# Retry passing previous parameter set
200+
cred = ManagedIdentityCredential(
201+
client_id=client_id, tenant_id=tenant_id, **msi_kwargs
202+
)
203+
cred.get_token("https://management.azure.com/.default")
204+
return cred
205+
except ClientAuthenticationError:
206+
# If we fail again, just create with no params
207+
logger.info(
208+
"Managed Identity credential failed auth - retrying with no params"
209+
)
210+
return ManagedIdentityCredential()
174211

175212

176213
def _build_vscode_client(
@@ -380,7 +417,8 @@ def _az_connect_core(
380417
# Create the wrapped credential using the passed credential
381418
wrapped_credentials = CredentialWrapper(credential, resource_id=az_config.token_uri)
382419
return AzCredentials(
383-
wrapped_credentials, ChainedTokenCredential(credential) # type: ignore[arg-type]
420+
wrapped_credentials, # type: ignore[arg-type]
421+
ChainedTokenCredential(credential), # type: ignore[arg-type]
384422
)
385423

386424

msticpy/common/utility/types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ def singleton(cls: type) -> Callable:
280280
instances: dict[type[object], object] = {}
281281

282282
def get_instance(*args, **kwargs) -> object:
283-
nonlocal instances
284283
if cls not in instances:
285284
instances[cls] = cls(*args, **kwargs)
286285
return instances[cls]

msticpy/context/vtlookupv3/vtlookupv3.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def _parse_vt_object(
191191
}
192192
else:
193193
obj = attributes
194-
vt_df: pd.DataFrame = pd.json_normalize(data=[obj])
194+
vt_df: pd.DataFrame = pd.json_normalize(data=[obj], max_level=0)
195195
last_analysis_stats: dict[str, Any] | None = attributes.get(
196196
VTObjectProperties.LAST_ANALYSIS_STATS.value,
197197
)
@@ -207,7 +207,7 @@ def _parse_vt_object(
207207
# Format dates for pandas
208208
vt_df = timestamps_to_utcdate(vt_df)
209209
elif obj_dict:
210-
vt_df = pd.json_normalize([obj_dict])
210+
vt_df = pd.json_normalize([obj_dict], max_level=0)
211211
else:
212212
vt_df = cls._item_not_found_df(
213213
vt_type=vt_object.type,
@@ -885,7 +885,7 @@ def get_object(self: Self, vt_id: str, vt_type: str) -> pd.DataFrame:
885885
"type": [response.type],
886886
},
887887
)
888-
attribs = pd.json_normalize(response.to_dict()["attributes"])
888+
attribs = pd.json_normalize(response.to_dict()["attributes"], max_level=0)
889889
result_df = pd.concat([result_df, attribs], axis=1)
890890
result_df["context_attributes"] = response.to_dict().get(
891891
"context_attributes",
@@ -1051,7 +1051,9 @@ def _extract_response(self: Self, response_list: list) -> pd.DataFrame:
10511051
response_rows = []
10521052
for response_item in response_list:
10531053
# flatten nested dictionary and append id, type values
1054-
response_item_df = pd.json_normalize(response_item["attributes"])
1054+
response_item_df = pd.json_normalize(
1055+
response_item["attributes"], max_level=0
1056+
)
10551057
response_item_df["id"] = response_item["id"]
10561058
response_item_df["type"] = response_item["type"]
10571059

msticpy/vis/entity_graph_tools.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
__version__ = VERSION
3434
__author__ = "Pete Bryan"
3535

36+
# mypy and Bokeh are not best friends
37+
# mypy: disable-error-code="arg-type"
38+
3639
req_alert_cols = ["DisplayName", "Severity", "AlertType"]
3740
req_inc_cols = ["id", "name", "properties.severity"]
3841

@@ -140,6 +143,7 @@ def _plot_with_timeline(self, hide: bool = False, **kwargs) -> LayoutDOM:
140143
"""
141144
timeline = None
142145
tl_df = self.to_df()
146+
143147
tl_type = "duration"
144148
# pylint: disable=unsubscriptable-object
145149
if len(tl_df["EndTime"].unique()) == 1 and not tl_df["EndTime"].unique()[0]:
@@ -150,22 +154,22 @@ def _plot_with_timeline(self, hide: bool = False, **kwargs) -> LayoutDOM:
150154
):
151155
print("No timestamps available to create timeline")
152156
return self._plot_no_timeline(timeline=False, hide=hide, **kwargs)
153-
# tl_df["TimeGenerated"] = pd.to_datetime(tl_df["TimeGenerated"], utc=True)
154-
# tl_df["StartTime"] = pd.to_datetime(tl_df["StartTime"], utc=True)
155-
# tl_df["EndTime"] = pd.to_datetime(tl_df["EndTime"], utc=True)
157+
156158
graph = self._plot_no_timeline(hide=True, **kwargs)
157159
if tl_type == "duration":
160+
# remove missing time values
158161
timeline = display_timeline_duration(
159-
tl_df.dropna(subset=["TimeGenerated"]),
162+
tl_df.dropna(subset=["StartTime", "EndTime"]),
160163
group_by="Name",
161164
title="Entity Timeline",
162165
time_column="StartTime",
163166
end_time_column="EndTime",
164-
source_columns=["Name", "Description", "Type", "TimeGenerated"],
167+
source_columns=["Name", "Description", "Type", "StartTime", "EndTime"],
165168
hide=True,
166169
width=800,
167170
)
168171
elif tl_type == "discreet":
172+
tl_df = tl_df.dropna(subset=["TimeGenerated"])
169173
timeline = display_timeline(
170174
tl_df.dropna(subset=["TimeGenerated"]),
171175
group_by="Type",

msticpy/vis/matrix_plot.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
__version__ = VERSION
2222
__author__ = "Ian Hellen"
2323

24+
# mypy and Bokeh are not best friends
25+
# mypy: disable-error-code="call-arg, attr-defined"
26+
2427
# wrap figure function to handle v2/v3 parameter renaming
2528
figure = bokeh_figure(figure) # type: ignore[assignment, misc]
2629

msticpy/vis/network_plot.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
__version__ = VERSION
3434
__author__ = "Ian Hellen"
3535

36+
# mypy and Bokeh are not best friends
37+
# mypy: disable-error-code="arg-type"
38+
3639
_BOKEH_VERSION: Version = parse(version("bokeh"))
3740

3841
# wrap figure function to handle v2/v3 parameter renaming

msticpy/vis/process_tree.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
__version__ = VERSION
8080
__author__ = "Ian Hellen"
8181

82+
# mypy and Bokeh are not best friends
83+
# mypy: disable-error-code="arg-type, call-arg, attr-defined"
84+
8285
_DEFAULT_KWARGS = ["height", "title", "width", "hide_legend", "pid_fmt"]
8386

8487
# wrap figure function to handle v2/v3 parameter renaming

msticpy/vis/timeline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
__version__ = VERSION
4444
__author__ = "Ian Hellen"
4545

46+
# mypy and Bokeh are not best friends
47+
# mypy: disable-error-code="arg-type, call-arg"
48+
4649
# wrap figure function to handle v2/v3 parameter renaming
4750
figure = bokeh_figure(figure) # type: ignore[assignment, misc]
4851

msticpy/vis/timeline_common.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
__version__ = VERSION
4343
__author__ = "Ian Hellen"
4444

45+
# mypy and Bokeh are not best friends
46+
# mypy: disable-error-code="arg-type, call-arg, attr-defined"
47+
4548
TIMELINE_HELP = (
4649
"https://msticpy.readthedocs.io/en/latest/msticpy.vis.html"
4750
"#msticpy.vis.timeline.{plot_type}"
@@ -298,10 +301,11 @@ def create_range_tool(
298301
y=y,
299302
color=series_def["color"],
300303
source=series_def["source"],
304+
radius=1,
301305
)
302306
elif isinstance(data, pd.DataFrame):
303307
rng_select.circle(
304-
x=time_column, y=y, color="blue", source=ColumnDataSource(data)
308+
x=time_column, y=y, color="blue", source=ColumnDataSource(data), radius=1
305309
)
306310

307311
range_tool = RangeTool(x_range=plot_range)

0 commit comments

Comments
 (0)