Skip to content

Commit ef4e28d

Browse files
authored
fix(m365_powershell): teams connection with --sp-env-auth and enhanced timeouts error logging (#9191)
1 parent ee2d3ed commit ef4e28d

File tree

3 files changed

+66
-94
lines changed

3 files changed

+66
-94
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
4949
### Fixed
5050
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
5151
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
52+
- Fix M365 Teams `--sp-env-auth` connection error and enhanced timeout logging [(#9191)](https://github.com/prowler-cloud/prowler/pull/9191)
5253

5354
---
5455

prowler/providers/m365/lib/powershell/m365_powershell.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import os
2-
from typing import Optional
32

43
from prowler.lib.logger import logger
54
from prowler.lib.powershell.powershell import PowerShellSession
65
from prowler.providers.m365.exceptions.exceptions import (
76
M365CertificateCreationError,
87
M365GraphConnectionError,
98
)
10-
from prowler.providers.m365.lib.jwt.jwt_decoder import decode_jwt, decode_msal_token
9+
from prowler.providers.m365.lib.jwt.jwt_decoder import decode_msal_token
1110
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
1211

1312

1413
class M365PowerShell(PowerShellSession):
15-
CONNECT_TIMEOUT = 15
1614
"""
1715
Microsoft 365 specific PowerShell session management implementation.
1816
@@ -125,9 +123,7 @@ def init_credential(self, credentials: M365Credentials) -> None:
125123
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
126124
)
127125

128-
def _execute_connect_command(
129-
self, command: str, timeout: Optional[int] = None
130-
) -> str:
126+
def execute_connect(self, command: str) -> str:
131127
"""
132128
Execute a PowerShell connect command ensuring empty responses surface as timeouts.
133129
@@ -138,9 +134,9 @@ def _execute_connect_command(
138134
Returns:
139135
str: Command output or 'Timeout' if the command produced no output.
140136
"""
141-
effective_timeout = timeout or self.CONNECT_TIMEOUT
142-
result = self.execute(command, timeout=effective_timeout)
143-
return result or "Timeout"
137+
connect_timeout = 15
138+
result = self.execute(command, timeout=connect_timeout)
139+
return result or "'execute_connect' command timeout reached"
144140

145141
def test_credentials(self, credentials: M365Credentials) -> bool:
146142
"""
@@ -207,7 +203,7 @@ def test_graph_connection(self) -> bool:
207203

208204
def test_graph_certificate_connection(self) -> bool:
209205
"""Test Microsoft Graph API connection using certificate and raise exception if it fails."""
210-
result = self._execute_connect_command(
206+
result = self.execute_connect(
211207
"Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
212208
)
213209
if "Welcome to Microsoft Graph!" not in result:
@@ -221,18 +217,13 @@ def test_teams_connection(self) -> bool:
221217
self.execute(
222218
'$teamstokenBody = @{ Grant_Type = "client_credentials"; Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }'
223219
)
224-
self.execute(
220+
result = self.execute(
225221
'$teamsToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $teamstokenBody | Select-Object -ExpandProperty Access_Token'
226222
)
227-
permissions = decode_jwt(self.execute("Write-Output $teamsToken")).get(
228-
"roles", []
229-
)
230-
if "application_access" not in permissions:
231-
logger.error(
232-
"Microsoft Teams connection failed: Please check your permissions and try again."
233-
)
223+
if result != "":
224+
logger.error(f"Microsoft Teams connection failed: {result}")
234225
return False
235-
self._execute_connect_command(
226+
self.execute_connect(
236227
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
237228
)
238229
return True
@@ -244,7 +235,7 @@ def test_teams_connection(self) -> bool:
244235

245236
def test_teams_certificate_connection(self) -> bool:
246237
"""Test Microsoft Teams API connection using certificate and raise exception if it fails."""
247-
result = self._execute_connect_command(
238+
result = self.execute_connect(
248239
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
249240
)
250241
if self.tenant_identity.identity_id not in result:
@@ -268,9 +259,8 @@ def test_exchange_connection(self) -> bool:
268259
"Exchange Online connection failed: Please check your permissions and try again."
269260
)
270261
return False
271-
self._execute_connect_command(
272-
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"',
273-
timeout=self.CONNECT_TIMEOUT,
262+
self.execute_connect(
263+
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
274264
)
275265
return True
276266
except Exception as e:
@@ -281,9 +271,8 @@ def test_exchange_connection(self) -> bool:
281271

282272
def test_exchange_certificate_connection(self) -> bool:
283273
"""Test Exchange Online API connection using certificate and raise exception if it fails."""
284-
result = self._execute_connect_command(
285-
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
286-
timeout=self.CONNECT_TIMEOUT,
274+
result = self.execute_connect(
275+
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
287276
)
288277
if "https://aka.ms/exov3-module" not in result:
289278
logger.error(f"Exchange Online Certificate connection failed: {result}")

tests/providers/m365/lib/powershell/m365_powershell_test.py

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,7 @@ def test_test_graph_connection_exception(self, mock_popen):
547547
session.close()
548548

549549
@patch("subprocess.Popen")
550-
@patch("prowler.providers.m365.lib.powershell.m365_powershell.decode_jwt")
551-
def test_test_teams_connection_success(self, mock_decode_jwt, mock_popen):
550+
def test_test_teams_connection_success(self, mock_popen):
552551
"""Test test_teams_connection when token is valid"""
553552
mock_process = MagicMock()
554553
mock_popen.return_value = mock_process
@@ -567,30 +566,20 @@ def test_test_teams_connection_success(self, mock_decode_jwt, mock_popen):
567566
)
568567
session = M365PowerShell(credentials, identity)
569568

570-
# Mock execute to return valid responses
571-
def mock_execute(command, *args, **kwargs):
572-
if "Write-Output $teamsToken" in command:
573-
return "valid_teams_token"
574-
return None
575-
576-
session.execute = MagicMock(side_effect=mock_execute)
577-
# Mock JWT decode to return proper permissions
578-
mock_decode_jwt.return_value = {"roles": ["application_access"]}
569+
session.execute = MagicMock(side_effect=[None, ""])
570+
session.execute_connect = MagicMock(return_value="")
579571

580572
result = session.test_teams_connection()
581573

582574
assert result is True
583-
# Verify all expected PowerShell commands were called
584-
# 4 calls: teamstokenBody, teamsToken, Write-Output $teamsToken, Connect-MicrosoftTeams
585-
assert session.execute.call_count == 4
586-
mock_decode_jwt.assert_called_once_with("valid_teams_token")
575+
assert session.execute.call_count == 2
576+
session.execute_connect.assert_called_once_with(
577+
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
578+
)
587579
session.close()
588580

589581
@patch("subprocess.Popen")
590-
@patch("prowler.providers.m365.lib.powershell.m365_powershell.decode_jwt")
591-
def test_test_teams_connection_missing_permissions(
592-
self, mock_decode_jwt, mock_popen
593-
):
582+
def test_test_teams_connection_missing_permissions(self, mock_popen):
594583
"""Test test_teams_connection when token lacks required permissions"""
595584
mock_process = MagicMock()
596585
mock_popen.return_value = mock_process
@@ -609,23 +598,17 @@ def test_test_teams_connection_missing_permissions(
609598
)
610599
session = M365PowerShell(credentials, identity)
611600

612-
# Mock execute to return valid token but decode returns no permissions
613-
def mock_execute(command, *args, **kwargs):
614-
if "Write-Output $teamsToken" in command:
615-
return "valid_teams_token"
616-
return None
617-
618-
session.execute = MagicMock(side_effect=mock_execute)
619-
# Mock JWT decode to return missing required permission
620-
mock_decode_jwt.return_value = {"roles": ["other_permission"]}
601+
session.execute = MagicMock(side_effect=[None, "Permission denied"])
602+
session.execute_connect = MagicMock()
621603

622604
with patch("prowler.lib.logger.logger.error") as mock_error:
623605
result = session.test_teams_connection()
624606

625607
assert result is False
626608
mock_error.assert_called_once_with(
627-
"Microsoft Teams connection failed: Please check your permissions and try again."
609+
"Microsoft Teams connection failed: Permission denied"
628610
)
611+
session.execute_connect.assert_not_called()
629612
session.close()
630613

631614
@patch("subprocess.Popen")
@@ -688,15 +671,17 @@ def mock_execute(command, *args, **kwargs):
688671
return None
689672

690673
session.execute = MagicMock(side_effect=mock_execute)
674+
session.execute_connect = MagicMock(return_value=None)
691675
# Mock MSAL token decode to return proper permissions
692676
mock_decode_msal_token.return_value = {"roles": ["Exchange.ManageAsApp"]}
693677

694678
result = session.test_exchange_connection()
695679

696680
assert result is True
697-
# Verify all expected PowerShell commands were called
698-
# 4 calls: SecureSecret, exchangeToken, Write-Output $exchangeToken, Connect-ExchangeOnline
699-
assert session.execute.call_count == 4
681+
assert session.execute.call_count == 3
682+
session.execute_connect.assert_called_once_with(
683+
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
684+
)
700685
mock_decode_msal_token.assert_called_once_with("valid_exchange_token")
701686
session.close()
702687

@@ -730,13 +715,15 @@ def mock_execute(command, *args, **kwargs):
730715
return None
731716

732717
session.execute = MagicMock(side_effect=mock_execute)
718+
session.execute_connect = MagicMock(return_value=None)
733719
# Mock MSAL token decode to return missing required permission
734720
mock_decode_msal_token.return_value = {"roles": ["other_permission"]}
735721

736722
with patch("prowler.lib.logger.logger.error") as mock_error:
737723
result = session.test_exchange_connection()
738724

739725
assert result is False
726+
session.execute_connect.assert_not_called()
740727
mock_error.assert_called_once_with(
741728
"Exchange Online connection failed: Please check your permissions and try again."
742729
)
@@ -781,7 +768,7 @@ def test_clean_certificate_content(self, mock_popen):
781768
mock_popen.return_value = mock_process
782769

783770
credentials = M365Credentials()
784-
identity = M365IdentityInfo()
771+
identity = M365IdentityInfo(identity_id="expected-id")
785772
session = M365PowerShell(credentials, identity)
786773

787774
# Test with clean base64 content
@@ -924,20 +911,18 @@ def test_test_exchange_certificate_connection_success(self, mock_popen):
924911
mock_popen.return_value = mock_process
925912

926913
credentials = M365Credentials()
927-
identity = M365IdentityInfo()
914+
identity = M365IdentityInfo(identity_id="expected-id")
928915
session = M365PowerShell(credentials, identity)
929916

930-
# Mock successful Exchange connection
931-
session.execute = MagicMock(
917+
session.execute_connect = MagicMock(
932918
return_value="Connected successfully https://aka.ms/exov3-module"
933919
)
934920

935921
result = session.test_exchange_certificate_connection()
936922
assert result is True
937923

938-
session.execute.assert_called_once_with(
939-
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
940-
timeout=M365PowerShell.CONNECT_TIMEOUT,
924+
session.execute_connect.assert_called_once_with(
925+
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
941926
)
942927

943928
session.close()
@@ -949,20 +934,23 @@ def test_test_exchange_certificate_connection_failure(self, mock_popen):
949934
mock_popen.return_value = mock_process
950935

951936
credentials = M365Credentials()
952-
identity = M365IdentityInfo()
937+
identity = M365IdentityInfo(identity_id="expected-id")
953938
session = M365PowerShell(credentials, identity)
954939

955-
# Mock failed Exchange connection
956-
session.execute = MagicMock(
940+
session.execute_connect = MagicMock(
957941
return_value="Connection failed: Authentication error"
958942
)
959943

960-
result = session.test_exchange_certificate_connection()
944+
with patch("prowler.lib.logger.logger.error") as mock_error:
945+
result = session.test_exchange_certificate_connection()
946+
961947
assert result is False
962948

963-
session.execute.assert_called_once_with(
964-
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
965-
timeout=M365PowerShell.CONNECT_TIMEOUT,
949+
session.execute_connect.assert_called_once_with(
950+
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
951+
)
952+
mock_error.assert_called_once_with(
953+
"Exchange Online Certificate connection failed: Connection failed: Authentication error"
966954
)
967955

968956
session.close()
@@ -981,20 +969,15 @@ def test_test_teams_certificate_connection_success(self, mock_popen):
981969
session = M365PowerShell(credentials, identity)
982970

983971
# Mock successful Teams connection - the method returns bool
984-
def mock_execute_side_effect(command, *_, **__):
985-
if "Connect-MicrosoftTeams" in command:
986-
# Return result that contains the identity_id for success
987-
return "Connected successfully test_identity_id"
988-
return ""
989-
990-
session.execute = MagicMock(side_effect=mock_execute_side_effect)
972+
session.execute_connect = MagicMock(
973+
return_value="Connected successfully test_identity_id"
974+
)
991975

992976
result = session.test_teams_certificate_connection()
993977
assert result is True
994978

995-
session.execute.assert_called_once_with(
996-
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID",
997-
timeout=M365PowerShell.CONNECT_TIMEOUT,
979+
session.execute_connect.assert_called_once_with(
980+
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
998981
)
999982

1000983
session.close()
@@ -1006,22 +989,21 @@ def test_test_teams_certificate_connection_failure(self, mock_popen):
1006989
mock_popen.return_value = mock_process
1007990

1008991
credentials = M365Credentials()
1009-
identity = M365IdentityInfo()
992+
identity = M365IdentityInfo(identity_id="expected-id")
1010993
session = M365PowerShell(credentials, identity)
1011994

1012-
# Mock failed Teams connection
1013-
def mock_execute_side_effect(command, **kwargs):
1014-
if "Connect-MicrosoftTeams" in command:
1015-
raise Exception("Connection failed: Authentication error")
1016-
return ""
1017-
1018-
session.execute = MagicMock(side_effect=mock_execute_side_effect)
995+
session.execute_connect = MagicMock(return_value="Connection failed")
1019996

1020-
# Should raise exception on connection failure
1021-
with pytest.raises(Exception) as exc_info:
1022-
session.test_teams_certificate_connection()
997+
with patch("prowler.lib.logger.logger.error") as mock_error:
998+
result = session.test_teams_certificate_connection()
1023999

1024-
assert "Connection failed: Authentication error" in str(exc_info.value)
1000+
assert result is False
1001+
session.execute_connect.assert_called_once_with(
1002+
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
1003+
)
1004+
mock_error.assert_called_once_with(
1005+
"Microsoft Teams Certificate connection failed: Connection failed"
1006+
)
10251007

10261008
session.close()
10271009

0 commit comments

Comments
 (0)