fix(powershell): depth truncation and parsing error (#9181)

This commit is contained in:
Hugo Pereira Brito
2025-11-07 13:19:37 +01:00
committed by GitHub
parent f0b1c4c29e
commit fb9eda208e
5 changed files with 146 additions and 63 deletions
+1
View File
@@ -48,6 +48,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
---
+12 -11
View File
@@ -220,18 +220,19 @@ class PowerShellSession:
if output == "":
return {}
json_match = re.search(r"(\[.*\]|\{.*\})", output, re.DOTALL)
if not json_match:
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
else:
decoder = json.JSONDecoder()
for index, character in enumerate(output):
if character not in ("{", "["):
continue
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError as error:
logger.error(
f"Error parsing PowerShell output as JSON: {str(error)}\n",
)
parsed_json, _ = decoder.raw_decode(output[index:])
return parsed_json
except json.JSONDecodeError:
continue
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
return {}
@@ -1,4 +1,5 @@
import os
from typing import Optional
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
@@ -11,6 +12,7 @@ from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
class M365PowerShell(PowerShellSession):
CONNECT_TIMEOUT = 15
"""
Microsoft 365 specific PowerShell session management implementation.
@@ -123,6 +125,23 @@ class M365PowerShell(PowerShellSession):
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
)
def _execute_connect_command(
self, command: str, timeout: Optional[int] = None
) -> str:
"""
Execute a PowerShell connect command ensuring empty responses surface as timeouts.
Args:
command (str): PowerShell connect command to run.
timeout (Optional[int]): Timeout in seconds for the command execution.
Returns:
str: Command output or 'Timeout' if the command produced no output.
"""
effective_timeout = timeout or self.CONNECT_TIMEOUT
result = self.execute(command, timeout=effective_timeout)
return result or "Timeout"
def test_credentials(self, credentials: M365Credentials) -> bool:
"""
Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
@@ -141,24 +160,32 @@ class M365PowerShell(PowerShellSession):
# Test Certificate Auth
if credentials.certificate_content and credentials.client_id:
try:
self.test_teams_certificate_connection() or self.test_exchange_certificate_connection()
logger.info("Testing Microsoft Graph Certificate connection...")
self.test_graph_certificate_connection()
logger.info("Microsoft Graph Certificate connection successful")
teams_connection_successful = self.test_teams_certificate_connection()
if not teams_connection_successful:
self.test_exchange_certificate_connection()
return True
except Exception as e:
logger.error(f"Exchange Online Certificate connection failed: {e}")
else:
# Test Microsoft Graph connection
try:
logger.info("Testing Microsoft Graph connection...")
self.test_graph_connection()
logger.info("Microsoft Graph connection successful")
return True
except Exception as e:
logger.error(f"Microsoft Graph connection failed: {e}")
logger.error(f"Microsoft Graph Cer connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application credentials and ensure the app has proper permissions",
message="Check your Microsoft Application Certificate and ensure the app has proper permissions",
)
else:
try:
logger.info("Testing Microsoft Graph Client Secret connection...")
self.test_graph_connection()
logger.info("Microsoft Graph Client Secret connection successful")
return True
except Exception as e:
logger.error(f"Microsoft Graph Client Secret connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application Client Secret and ensure the app has proper permissions",
)
def test_graph_connection(self) -> bool:
@@ -178,6 +205,16 @@ class M365PowerShell(PowerShellSession):
message=f"Failed to connect to Microsoft Graph API: {str(e)}",
)
def test_graph_certificate_connection(self) -> bool:
"""Test Microsoft Graph API connection using certificate and raise exception if it fails."""
result = self._execute_connect_command(
"Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
)
if "Welcome to Microsoft Graph!" not in result:
logger.error(f"Microsoft Graph Certificate connection failed: {result}")
return False
return True
def test_teams_connection(self) -> bool:
"""Test Microsoft Teams API connection and raise exception if it fails."""
try:
@@ -195,7 +232,7 @@ class M365PowerShell(PowerShellSession):
"Microsoft Teams connection failed: Please check your permissions and try again."
)
return False
self.execute(
self._execute_connect_command(
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
)
return True
@@ -207,7 +244,7 @@ class M365PowerShell(PowerShellSession):
def test_teams_certificate_connection(self) -> bool:
"""Test Microsoft Teams API connection using certificate and raise exception if it fails."""
result = self.execute(
result = self._execute_connect_command(
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
)
if self.tenant_identity.identity_id not in result:
@@ -231,8 +268,9 @@ class M365PowerShell(PowerShellSession):
"Exchange Online connection failed: Please check your permissions and try again."
)
return False
self.execute(
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
self._execute_connect_command(
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"',
timeout=self.CONNECT_TIMEOUT,
)
return True
except Exception as e:
@@ -243,8 +281,9 @@ class M365PowerShell(PowerShellSession):
def test_exchange_certificate_connection(self) -> bool:
"""Test Exchange Online API connection using certificate and raise exception if it fails."""
result = self.execute(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
result = self._execute_connect_command(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=self.CONNECT_TIMEOUT,
)
if "https://aka.ms/exov3-module" not in result:
logger.error(f"Exchange Online Certificate connection failed: {result}")
@@ -290,7 +329,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsClientConfiguration | ConvertTo-Json", json_parse=True
"Get-CsTeamsClientConfiguration | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_global_meeting_policy(self) -> dict:
@@ -309,7 +349,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsMeetingPolicy -Identity Global | ConvertTo-Json",
"Get-CsTeamsMeetingPolicy -Identity Global | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -329,7 +369,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsMessagingPolicy -Identity Global | ConvertTo-Json",
"Get-CsTeamsMessagingPolicy -Identity Global | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -349,7 +389,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTenantFederationConfiguration | ConvertTo-Json", json_parse=True
"Get-CsTenantFederationConfiguration | ConvertTo-Json -Depth 10",
json_parse=True,
)
def connect_exchange_online(self) -> dict:
@@ -389,7 +430,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled | ConvertTo-Json",
"Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -409,7 +450,9 @@ class M365PowerShell(PowerShellSession):
"Identity": "Default"
}
"""
return self.execute("Get-MalwareFilterPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-MalwareFilterPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_malware_filter_rule(self) -> dict:
"""
@@ -427,7 +470,9 @@ class M365PowerShell(PowerShellSession):
"State": "Enabled"
}
"""
return self.execute("Get-MalwareFilterRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-MalwareFilterRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_outbound_spam_filter_policy(self) -> dict:
"""
@@ -448,7 +493,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedOutboundSpamFilterPolicy | ConvertTo-Json", json_parse=True
"Get-HostedOutboundSpamFilterPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_outbound_spam_filter_rule(self) -> dict:
@@ -467,7 +513,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedOutboundSpamFilterRule | ConvertTo-Json", json_parse=True
"Get-HostedOutboundSpamFilterRule | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_antiphishing_policy(self) -> dict:
@@ -493,7 +540,9 @@ class M365PowerShell(PowerShellSession):
"IsDefault": false
}
"""
return self.execute("Get-AntiPhishPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-AntiPhishPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_antiphishing_rules(self) -> dict:
"""
@@ -511,7 +560,9 @@ class M365PowerShell(PowerShellSession):
"State": Enabled,
}
"""
return self.execute("Get-AntiPhishRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-AntiPhishRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_organization_config(self) -> dict:
"""
@@ -530,7 +581,9 @@ class M365PowerShell(PowerShellSession):
"AuditDisabled": false
}
"""
return self.execute("Get-OrganizationConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-OrganizationConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_mailbox_audit_config(self) -> dict:
"""
@@ -550,7 +603,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-MailboxAuditBypassAssociation | ConvertTo-Json", json_parse=True
"Get-MailboxAuditBypassAssociation | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_mailbox_policy(self) -> dict:
@@ -569,7 +623,9 @@ class M365PowerShell(PowerShellSession):
"AdditionalStorageProvidersAvailable": True
}
"""
return self.execute("Get-OwaMailboxPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-OwaMailboxPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_external_mail_config(self) -> dict:
"""
@@ -587,7 +643,9 @@ class M365PowerShell(PowerShellSession):
"ExternalMailTagEnabled": true
}
"""
return self.execute("Get-ExternalInOutlook | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-ExternalInOutlook | ConvertTo-Json -Depth 10", json_parse=True
)
def get_transport_rules(self) -> dict:
"""
@@ -606,7 +664,9 @@ class M365PowerShell(PowerShellSession):
"SenderDomainIs": ["example.com"]
}
"""
return self.execute("Get-TransportRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-TransportRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_connection_filter_policy(self) -> dict:
"""
@@ -625,7 +685,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedConnectionFilterPolicy -Identity Default | ConvertTo-Json",
"Get-HostedConnectionFilterPolicy -Identity Default | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -645,7 +705,9 @@ class M365PowerShell(PowerShellSession):
"Enabled": true
}
"""
return self.execute("Get-DkimSigningConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-DkimSigningConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_inbound_spam_filter_policy(self) -> dict:
"""
@@ -664,7 +726,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True
"Get-HostedContentFilterPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_inbound_spam_filter_rule(self) -> dict:
@@ -684,7 +747,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedContentFilterRule | ConvertTo-Json", json_parse=True
"Get-HostedContentFilterRule | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_report_submission_policy(self) -> dict:
@@ -715,7 +779,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-ReportSubmissionPolicy | ConvertTo-Json", json_parse=True
"Get-ReportSubmissionPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_role_assignment_policies(self) -> dict:
@@ -736,7 +801,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-RoleAssignmentPolicy | ConvertTo-Json", json_parse=True
"Get-RoleAssignmentPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_mailbox_audit_properties(self) -> dict:
@@ -801,7 +867,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-EXOMailbox -PropertySets Audit -ResultSize Unlimited | ConvertTo-Json",
"Get-EXOMailbox -PropertySets Audit -ResultSize Unlimited | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -820,7 +886,9 @@ class M365PowerShell(PowerShellSession):
"SmtpClientAuthenticationDisabled": True,
}
"""
return self.execute("Get-TransportConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-TransportConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_sharing_policy(self) -> dict:
"""
@@ -838,7 +906,9 @@ class M365PowerShell(PowerShellSession):
"Enabled": true
}
"""
return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_user_account_status(self) -> dict:
"""
@@ -850,7 +920,7 @@ class M365PowerShell(PowerShellSession):
dict: User account status settings in JSON format.
"""
return self.execute(
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json",
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10",
json_parse=True,
)
+8
View File
@@ -216,6 +216,14 @@ class TestPowerShellSession:
result = session.json_parse_output('prefix [{"key": "value"}] suffix')
assert result == [{"key": "value"}]
result = session.json_parse_output(
'INFO {context data} {"key": "value", "list": [1, 2]} extra'
)
assert result == {"key": "value", "list": [1, 2]}
result = session.json_parse_output('{"key": "value"} trailing {log}')
assert result == {"key": "value"}
# Test non-JSON text returns empty dict
result = session.json_parse_output("just some text")
assert result == {}
@@ -936,7 +936,8 @@ class Testm365PowerShell:
assert result is True
session.execute.assert_called_once_with(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -960,7 +961,8 @@ class Testm365PowerShell:
assert result is False
session.execute.assert_called_once_with(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -979,7 +981,7 @@ class Testm365PowerShell:
session = M365PowerShell(credentials, identity)
# Mock successful Teams connection - the method returns bool
def mock_execute_side_effect(command):
def mock_execute_side_effect(command, *_, **__):
if "Connect-MicrosoftTeams" in command:
# Return result that contains the identity_id for success
return "Connected successfully test_identity_id"
@@ -991,7 +993,8 @@ class Testm365PowerShell:
assert result is True
session.execute.assert_called_once_with(
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -1007,7 +1010,7 @@ class Testm365PowerShell:
session = M365PowerShell(credentials, identity)
# Mock failed Teams connection
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command, **kwargs):
if "Connect-MicrosoftTeams" in command:
raise Exception("Connection failed: Authentication error")
return ""
@@ -1090,7 +1093,7 @@ class Testm365PowerShell:
# Mock certificate variable check and teams connection
execute_calls = []
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command):
execute_calls.append(command)
if "Write-Output $certificate" in command:
return "certificate_content" # Non-empty means certificate exists
@@ -1123,7 +1126,7 @@ class Testm365PowerShell:
# Mock certificate variable check and exchange connection
execute_calls = []
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command):
execute_calls.append(command)
if "Write-Output $certificate" in command:
return "certificate_content" # Non-empty means certificate exists