feat(microsoft365): add new check admincenter_settings_password_never_expire (#6023)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Mario Rodriguez Lopez
2025-02-04 23:24:11 +01:00
committed by Pablo Lara
parent 335980c8d8
commit 43927a62f3
11 changed files with 201 additions and 10 deletions
@@ -50,9 +50,7 @@ from prowler.providers.microsoft365.exceptions.exceptions import (
Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError,
)
from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist
from prowler.providers.microsoft365.lib.regions.microsoft365_regions import (
get_regions_config,
)
from prowler.providers.microsoft365.lib.regions.regions import get_regions_config
from prowler.providers.microsoft365.models import (
Microsoft365IdentityInfo,
Microsoft365RegionConfig,
@@ -22,11 +22,13 @@ class AdminCenter(Microsoft365Service):
gather(
self._get_directory_roles(),
self._get_groups(),
self._get_domains(),
)
)
self.directory_roles = attributes[0]
self.groups = attributes[1]
self.domains = attributes[2]
async def _get_users(self):
logger.info("Microsoft365 - Getting users...")
@@ -130,6 +132,28 @@ class AdminCenter(Microsoft365Service):
)
return groups
async def _get_domains(self):
logger.info("Microsoft365 - Getting domains...")
domains = {}
try:
domains_list = await self.client.domains.get()
domains.update({})
for domain in domains_list.value:
domains.update(
{
domain.id: Domain(
id=domain.id,
password_validity_period=domain.password_validity_period_in_days,
)
}
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return domains
class User(BaseModel):
id: str
@@ -149,3 +173,8 @@ class Group(BaseModel):
id: str
name: str
visibility: str
class Domain(BaseModel):
id: str
password_validity_period: int
@@ -0,0 +1,30 @@
{
"Provider": "microsoft",
"CheckID": "admincenter_settings_password_never_expire",
"CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'",
"CheckType": [],
"ServiceName": "admincenter",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft365Domain",
"Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.",
"Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.",
"RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide",
"Remediation": {
"Code": {
"CLI": "Set-MsolUser -UserPrincipalName <user> -PasswordNeverExpires $true",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.",
"Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_Microsoft365
from prowler.providers.microsoft365.services.admincenter.admincenter_client import (
admincenter_client,
)
class admincenter_settings_password_never_expire(Check):
def execute(self) -> Check_Report_Microsoft365:
findings = []
for domain in admincenter_client.domains.values():
report = Check_Report_Microsoft365(self.metadata(), resource=domain)
report.resource_id = domain.id
report.resource_name = domain.id
report.status = "FAIL"
report.status_extended = (
f"Domain {domain.id} does not have a Password never expires policy."
)
if domain.password_validity_period == 2147483647:
report.status = "PASS"
report.status_extended = (
f"Domain {domain.id} Password policy is set to never expire."
)
findings.append(report)
return findings
@@ -9,9 +9,7 @@ class admincenter_users_between_two_and_four_global_admins(Check):
findings = []
directory_roles = admincenter_client.directory_roles
report = Check_Report_Microsoft365(
metadata=self.metadata(), resource=admincenter_client.directory_roles
)
report = Check_Report_Microsoft365(metadata=self.metadata(), resource={})
report.status = "FAIL"
report.resource_name = "Global Administrator"
@@ -1,6 +1,6 @@
from azure.identity import AzureAuthorityHosts
from prowler.providers.microsoft365.lib.regions.microsoft365_regions import (
from prowler.providers.microsoft365.lib.regions.regions import (
MICROSOFT365_CHINA_CLOUD,
MICROSOFT365_GENERIC_CLOUD,
MICROSOFT365_US_GOV_CLOUD,
@@ -29,7 +29,6 @@ def set_mocked_microsoft365_provider(
audit_config: dict = None,
azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(),
) -> Microsoft365Provider:
provider = MagicMock()
provider.type = "microsoft365"
provider.session.credentials = credentials
@@ -212,7 +212,6 @@ class TestMicrosoft365Provider:
"prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient"
) as mock_graph_client,
):
# Mock the return value of DefaultAzureCredential
mock_credentials = MagicMock()
mock_credentials.get_token.return_value = AccessToken(
@@ -271,7 +270,6 @@ class TestMicrosoft365Provider:
with patch(
"prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session"
) as mock_setup_session:
mock_setup_session.side_effect = Microsoft365HTTPResponseError(
file="test_file", original_exception="Simulated HttpResponseError"
)
@@ -0,0 +1,112 @@
from unittest import mock
from uuid import uuid4
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_admincenter_settings_password_never_expire:
def test_admincenter_no_domains(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
admincenter_client.domains = {}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 0
def test_admincenter_domain_password_expire(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_service import (
Domain,
)
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
id_domain = str(uuid4())
admincenter_client.domains = {
id_domain: Domain(id=id_domain, password_validity_period=5),
}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Domain {id_domain} does not have a Password never expires policy."
)
assert result[0].resource_name == id_domain
assert result[0].resource_id == id_domain
def test_admincenter_password_not_expire(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_service import (
Domain,
)
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
id_domain = str(uuid4())
admincenter_client.domains = {
id_domain: Domain(id=id_domain, password_validity_period=2147483647),
}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Domain {id_domain} Password policy is set to never expire."
)
assert result[0].resource_name == id_domain
assert result[0].resource_id == id_domain