diff --git a/.github/workflows/create-backport-label.yml b/.github/workflows/create-backport-label.yml new file mode 100644 index 0000000000..0bc93bd45d --- /dev/null +++ b/.github/workflows/create-backport-label.yml @@ -0,0 +1,67 @@ +name: Create Backport Label + +on: + release: + types: [published] + +jobs: + create_label: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + steps: + - name: Create backport label + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + OWNER_REPO: ${{ github.repository }} + run: | + VERSION_ONLY=${RELEASE_TAG#v} # Remove 'v' prefix if present (e.g., v3.2.0 -> 3.2.0) + + # Check if it's a minor version (X.Y.0) + if [[ "$VERSION_ONLY" =~ ^[0-9]+\.[0-9]+\.0$ ]]; then + echo "Release ${RELEASE_TAG} (version ${VERSION_ONLY}) is a minor version. Proceeding to create backport label." + + TWO_DIGIT_VERSION=${VERSION_ONLY%.0} # Extract X.Y from X.Y.0 (e.g., 5.6 from 5.6.0) + + FINAL_LABEL_NAME="backport-to-v${TWO_DIGIT_VERSION}" + FINAL_DESCRIPTION="Backport PR to the v${TWO_DIGIT_VERSION} branch" + + echo "Effective label name will be: ${FINAL_LABEL_NAME}" + echo "Effective description will be: ${FINAL_DESCRIPTION}" + + # Check if the label already exists + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${GITHUB_TOKEN}" "https://api.github.com/repos/${OWNER_REPO}/labels/${FINAL_LABEL_NAME}") + + if [ "${STATUS_CODE}" -eq 200 ]; then + echo "Label '${FINAL_LABEL_NAME}' already exists." + elif [ "${STATUS_CODE}" -eq 404 ]; then + echo "Label '${FINAL_LABEL_NAME}' does not exist. Creating it..." + # Prepare JSON data payload + JSON_DATA=$(printf '{"name":"%s","description":"%s","color":"B60205"}' "${FINAL_LABEL_NAME}" "${FINAL_DESCRIPTION}") + + CREATE_STATUS_CODE=$(curl -s -o /tmp/curl_create_response.json -w "%{http_code}" -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + --data "${JSON_DATA}" \ + "https://api.github.com/repos/${OWNER_REPO}/labels") + + CREATE_RESPONSE_BODY=$(cat /tmp/curl_create_response.json) + rm -f /tmp/curl_create_response.json + + if [ "$CREATE_STATUS_CODE" -eq 201 ]; then + echo "Label '${FINAL_LABEL_NAME}' created successfully." + else + echo "Error creating label '${FINAL_LABEL_NAME}'. Status: $CREATE_STATUS_CODE" + echo "Response: $CREATE_RESPONSE_BODY" + exit 1 + fi + else + echo "Error checking for label '${FINAL_LABEL_NAME}'. HTTP Status: ${STATUS_CODE}" + exit 1 + fi + else + echo "Release ${RELEASE_TAG} (version ${VERSION_ONLY}) is not a minor version. Skipping backport label creation." + exit 0 + fi diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 93f1439904..54aeeb3392 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to the **Prowler SDK** are documented in this file. - Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695) - Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692) - Add new check `defender_domain_spf_records_published`. [(#7724)](https://github.com/prowler-cloud/prowler/pull/7724) +- Add new check `admincenter_organization_customer_lockbox_enabled`. [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732) +- Add new check `admincenter_external_calendar_sharing_disabled`. [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733) - Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787) - Add `repository_default_branch_requires_multiple_approvals` check for GitHub provider. [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160) - Add `repository_default_branch_protection_enabled` check for GitHub provider. [(#6161)](https://github.com/prowler-cloud/prowler/pull/6161) @@ -28,6 +30,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Enhance defender policies checks logic. [(#7719)](https://github.com/prowler-cloud/prowler/pull/7719) - Update and upgrade CIS for all the providers [(#7738)](https://github.com/prowler-cloud/prowler/pull/7738) - Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750) +- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761) - Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764) --- diff --git a/prowler/compliance/m365/cis_4.0_m365.json b/prowler/compliance/m365/cis_4.0_m365.json index 2612148782..48a6555ed0 100644 --- a/prowler/compliance/m365/cis_4.0_m365.json +++ b/prowler/compliance/m365/cis_4.0_m365.json @@ -185,7 +185,9 @@ { "Id": "1.3.3", "Description": "External calendar sharing allows an administrator to enable the ability for users to share calendars with anyone outside of the organization. Outside users will be sent a URL that can be used to view the calendar.", - "Checks": [], + "Checks": [ + "admincenter_external_calendar_sharing_disabled" + ], "Attributes": [ { "Section": "1 Microsoft 365 admin center", @@ -248,7 +250,9 @@ { "Id": "1.3.6", "Description": "Customer Lockbox is a security feature that provides an additional layer of control and transparency to customer data in Microsoft 365. It offers an approval process for Microsoft support personnel to access organization data and creates an audited trail to meet compliance requirements.", - "Checks": [], + "Checks": [ + "admincenter_organization_customer_lockbox_enabled" + ], "Attributes": [ { "Section": "1 Microsoft 365 admin center", diff --git a/prowler/config/config.py b/prowler/config/config.py index 51f52ae3ca..de54add886 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -12,7 +12,7 @@ from prowler.lib.logger import logger timestamp = datetime.today() timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc) -prowler_version = "5.7.0" +prowler_version = "5.8.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 9dbda1c01e..a91e25d52d 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -87,10 +87,10 @@ class M365PowerShell(PowerShellSession): bool: True if credentials are valid and authentication succeeds, False otherwise. """ self.execute( - f'$securePassword = "{credentials.passwd}" | ConvertTo-SecureString' + f'$securePassword = "{self.sanitize(credentials.passwd)}" | ConvertTo-SecureString' ) self.execute( - f'$credential = New-Object System.Management.Automation.PSCredential("{credentials.user}", $securePassword)\n' + f'$credential = New-Object System.Management.Automation.PSCredential("{self.sanitize(credentials.user)}", $securePassword)' ) decrypted_password = self.execute( 'Write-Output "$($credential.GetNetworkCredential().Password)"' @@ -685,6 +685,24 @@ class M365PowerShell(PowerShellSession): """ return self.execute("Get-TransportConfig | ConvertTo-Json", json_parse=True) + def get_sharing_policy(self) -> dict: + """ + Get Exchange Online Sharing Policy. + + Retrieves the current sharing policy settings for Exchange Online. + + Returns: + dict: Sharing policy settings in JSON format. + + Example: + >>> get_sharing_policy() + { + "Identity": "Default", + "Enabled": true + } + """ + return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True) + # This function is used to install the required M365 PowerShell modules in Docker containers def initialize_m365_powershell_modules(): diff --git a/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/__init__.py b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json new file mode 100644 index 0000000000..64f9d85c74 --- /dev/null +++ b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "m365", + "CheckID": "admincenter_external_calendar_sharing_disabled", + "CheckTitle": "Ensure external sharing of calendars is disabled", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Sharing Policy", + "Description": "Restrict the ability for users to share their calendars externally in Microsoft 365. This prevents users from sending calendar sharing links to external recipients, reducing information exposure.", + "Risk": "Allowing calendar sharing outside the organization can help attackers build knowledge of personnel availability, relationships, and activity patterns, aiding social engineering or targeted attacks.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide", + "Remediation": { + "Code": { + "CLI": "Set-SharingPolicy -Identity \"Default Sharing Policy\" -Enabled $False", + "NativeIaC": "", + "Other": "1. Navigate to https://admin.microsoft.com. 2. Click Settings > Org settings. 3. Select Calendar in the Services section. 4. Uncheck 'Let your users share their calendars with people outside of your organization who have Office 365 or Exchange'. 5. Click Save.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable external calendar sharing by setting the Default Sharing Policy to disabled.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide" + } + }, + "Categories": [ + "e5" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.py b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.py new file mode 100644 index 0000000000..ff2af49aca --- /dev/null +++ b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_external_calendar_sharing_disabled(Check): + """ + Ensure that external calendar sharing is disabled for the organization. + + Disabling external calendar sharing restricts the ability for users to share their + calendars externally in Microsoft 365. This prevents users from sending calendar + sharing links to external recipients, reducing information exposure. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check for external calendar sharing in Microsoft 365. + + This method checks if external calendar sharing is disabled in the organization configuration. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + sharing_policy = admincenter_client.sharing_policy + if sharing_policy: + report = CheckReportM365( + metadata=self.metadata(), + resource=sharing_policy, + resource_name=sharing_policy.name, + resource_id=sharing_policy.guid, + ) + report.status = "FAIL" + report.status_extended = ( + "External calendar sharing is enabled at the organization level." + ) + + if not sharing_policy.enabled: + report.status = "PASS" + report.status_extended = ( + "External calendar sharing is disabled at the organization level." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/__init__.py b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json new file mode 100644 index 0000000000..2c0ba3ebac --- /dev/null +++ b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "m365", + "CheckID": "admincenter_organization_customer_lockbox_enabled", + "CheckTitle": "Ensure that customer lockbox is enabled for the organization", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Exchange Organization Configuration", + "Description": "Customer Lockbox ensures that Microsoft support engineers cannot access content in your tenant to perform a service operation without explicit approval. This feature provides an additional layer of control and transparency over data access requests.", + "Risk": "If Customer Lockbox is not enabled, Microsoft support personnel can access your organization's data for troubleshooting without explicit approval, potentially increasing the risk of unauthorized access or data exfiltration.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/security/fundamentals/customer-lockbox-overview", + "Remediation": { + "Code": { + "CLI": "Set-OrganizationConfig -CustomerLockBoxEnabled $true", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click Settings > Org settings. 3. Select the Security & privacy tab. 4. Click Customer lockbox. 5. Check the box 'Require approval for all data access requests'. 6. Click Save.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the Customer Lockbox feature to ensure explicit approval is required before Microsoft engineers can access your data during support operations.", + "Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/customer-lockbox-overview" + } + }, + "Categories": [ + "e5" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.py b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.py new file mode 100644 index 0000000000..532b053b14 --- /dev/null +++ b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_organization_customer_lockbox_enabled(Check): + """ + Ensure the customer lockbox feature is enabled. + + Customer Lockbox ensures that Microsoft support engineers cannot access content + in your tenant to perform a service operation without explicit approval. This feature + provides an additional layer of control and transparency over data access requests. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check for the Customer Lockbox feature in Microsoft 365. + + This method checks if the Customer Lockbox feature is enabled in the organization configuration. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + organization_config = admincenter_client.organization_config + if organization_config: + report = CheckReportM365( + metadata=self.metadata(), + resource=organization_config, + resource_name=organization_config.name, + resource_id=organization_config.guid, + ) + report.status = "FAIL" + report.status_extended = ( + "Customer Lockbox is not enabled at organization level." + ) + + if organization_config.customer_lockbox_enabled: + report.status = "PASS" + report.status_extended = ( + "Customer Lockbox is enabled at organization level." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/admincenter/admincenter_service.py b/prowler/providers/m365/services/admincenter/admincenter_service.py index ed8e4ad4a2..bb1cad48ba 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_service.py +++ b/prowler/providers/m365/services/admincenter/admincenter_service.py @@ -11,6 +11,13 @@ from prowler.providers.m365.m365_provider import M365Provider class AdminCenter(M365Service): def __init__(self, provider: M365Provider): super().__init__(provider) + self.organization_config = None + self.sharing_policy = None + if self.powershell: + self.powershell.connect_exchange_online() + self.organization_config = self._get_organization_config() + self.sharing_policy = self._get_sharing_policy() + self.powershell.close() loop = get_event_loop() @@ -29,6 +36,42 @@ class AdminCenter(M365Service): self.groups = attributes[1] self.domains = attributes[2] + def _get_organization_config(self): + logger.info("Microsoft365 - Getting Exchange Organization configuration...") + organization_config = None + try: + organization_configuration = self.powershell.get_organization_config() + if organization_configuration: + organization_config = Organization( + name=organization_configuration.get("Name", ""), + guid=organization_configuration.get("Guid", ""), + customer_lockbox_enabled=organization_configuration.get( + "CustomerLockboxEnabled", False + ), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return organization_config + + def _get_sharing_policy(self): + logger.info("M365 - Getting sharing policy...") + sharing_policy = None + try: + sharing_policy_data = self.powershell.get_sharing_policy() + if sharing_policy_data: + sharing_policy = SharingPolicy( + name=sharing_policy_data.get("Name", ""), + guid=sharing_policy_data.get("Guid", ""), + enabled=sharing_policy_data.get("Enabled", False), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return sharing_policy + async def _get_users(self): logger.info("M365 - Getting users...") users = {} @@ -163,3 +206,15 @@ class Group(BaseModel): class Domain(BaseModel): id: str password_validity_period: int + + +class Organization(BaseModel): + name: str + guid: str + customer_lockbox_enabled: bool + + +class SharingPolicy(BaseModel): + name: str + guid: str + enabled: bool diff --git a/pyproject.toml b/pyproject.toml index b2e9b9feb9..26cc8d967e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"} name = "prowler" readme = "README.md" requires-python = ">3.9.1,<3.13" -version = "5.7.0" +version = "5.8.0" [project.scripts] prowler = "prowler.__main__:prowler" diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index b841602914..7bc3236433 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -145,7 +145,7 @@ class Testm365PowerShell: f'$securePassword = "{credentials.passwd}" | ConvertTo-SecureString' ) session.execute.assert_any_call( - f'$credential = New-Object System.Management.Automation.PSCredential("{credentials.user}", $securePassword)\n' + f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)' ) session.execute.assert_any_call( 'Write-Output "$($credential.GetNetworkCredential().Password)"' diff --git a/tests/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled_test.py b/tests/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled_test.py new file mode 100644 index 0000000000..0c76f2f696 --- /dev/null +++ b/tests/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled_test.py @@ -0,0 +1,120 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_admincenter_external_calendar_sharing_disabled: + def test_admincenter_no_sharing_policy(self): + admincenter_client = mock.MagicMock() + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + admincenter_client.sharing_policy = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled import ( + admincenter_external_calendar_sharing_disabled, + ) + + check = admincenter_external_calendar_sharing_disabled() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_calendar_sharing_disabled(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_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled import ( + admincenter_external_calendar_sharing_disabled, + ) + from prowler.providers.m365.services.admincenter.admincenter_service import ( + SharingPolicy, + ) + + admincenter_client.sharing_policy = SharingPolicy( + name="test-org", + guid="org-guid", + enabled=False, + ) + + check = admincenter_external_calendar_sharing_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "External calendar sharing is disabled at the organization level." + ) + assert result[0].resource == admincenter_client.sharing_policy.dict() + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + + def test_admincenter_calendar_sharing_enabled(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_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_external_calendar_sharing_disabled.admincenter_external_calendar_sharing_disabled import ( + admincenter_external_calendar_sharing_disabled, + ) + from prowler.providers.m365.services.admincenter.admincenter_service import ( + SharingPolicy, + ) + + admincenter_client.sharing_policy = SharingPolicy( + name="test-org", + guid="org-guid", + enabled=True, + ) + + check = admincenter_external_calendar_sharing_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "External calendar sharing is enabled at the organization level." + ) + assert result[0].resource == admincenter_client.sharing_policy.dict() + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" diff --git a/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py b/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py index 9e8ccfbc7d..81209d67a4 100644 --- a/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py @@ -15,6 +15,9 @@ class Test_admincenter_groups_not_public_visibility: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", new=admincenter_client, @@ -40,6 +43,9 @@ class Test_admincenter_groups_not_public_visibility: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", new=admincenter_client, @@ -78,6 +84,9 @@ class Test_admincenter_groups_not_public_visibility: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", new=admincenter_client, diff --git a/tests/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled_test.py b/tests/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled_test.py new file mode 100644 index 0000000000..48d3f1e745 --- /dev/null +++ b/tests/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled_test.py @@ -0,0 +1,120 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_admincenter_organization_customer_lockbox_enabled: + def test_admincenter_no_org_config(self): + admincenter_client = mock.MagicMock() + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + admincenter_client.organization_config = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled import ( + admincenter_organization_customer_lockbox_enabled, + ) + + check = admincenter_organization_customer_lockbox_enabled() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_customer_lockbox_enabled(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_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled import ( + admincenter_organization_customer_lockbox_enabled, + ) + from prowler.providers.m365.services.admincenter.admincenter_service import ( + Organization, + ) + + admincenter_client.organization_config = Organization( + name="test-org", + guid="org-guid", + customer_lockbox_enabled=True, + ) + + check = admincenter_organization_customer_lockbox_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Customer Lockbox is enabled at organization level." + ) + assert result[0].resource == admincenter_client.organization_config.dict() + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + + def test_admincenter_customer_lockbox_disabled(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_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.m365.services.admincenter.admincenter_organization_customer_lockbox_enabled.admincenter_organization_customer_lockbox_enabled import ( + admincenter_organization_customer_lockbox_enabled, + ) + from prowler.providers.m365.services.admincenter.admincenter_service import ( + Organization, + ) + + admincenter_client.organization_config = Organization( + name="test-org", + guid="org-guid", + customer_lockbox_enabled=False, + ) + + check = admincenter_organization_customer_lockbox_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Customer Lockbox is not enabled at organization level." + ) + assert result[0].resource == admincenter_client.organization_config.dict() + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" diff --git a/tests/providers/m365/services/admincenter/admincenter_service_test.py b/tests/providers/m365/services/admincenter/admincenter_service_test.py index f72c14ce71..287c91d661 100644 --- a/tests/providers/m365/services/admincenter/admincenter_service_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_service_test.py @@ -1,3 +1,4 @@ +from unittest import mock from unittest.mock import patch from prowler.providers.m365.models import M365IdentityInfo @@ -5,6 +6,8 @@ from prowler.providers.m365.services.admincenter.admincenter_service import ( AdminCenter, DirectoryRole, Group, + Organization, + SharingPolicy, User, ) from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider @@ -36,6 +39,22 @@ async def mock_admincenter_get_groups(_): } +def mock_admincenter_get_organization(_): + return Organization( + guid="id-1", + name="Test", + customer_lockbox_enabled=False, + ) + + +def mock_admincenter_get_sharing_policy(_): + return SharingPolicy( + guid="id-1", + name="Test", + enabled=False, + ) + + @patch( "prowler.providers.m365.services.admincenter.admincenter_service.AdminCenter._get_users", new=mock_admincenter_get_users, @@ -48,32 +67,97 @@ async def mock_admincenter_get_groups(_): "prowler.providers.m365.services.admincenter.admincenter_service.AdminCenter._get_groups", new=mock_admincenter_get_groups, ) +@patch( + "prowler.providers.m365.services.admincenter.admincenter_service.AdminCenter._get_organization_config", + new=mock_admincenter_get_organization, +) +@patch( + "prowler.providers.m365.services.admincenter.admincenter_service.AdminCenter._get_sharing_policy", + new=mock_admincenter_get_sharing_policy, +) class Test_AdminCenter_Service: def test_get_client(self): - admincenter_client = AdminCenter( - set_mocked_m365_provider(identity=M365IdentityInfo(tenant_domain=DOMAIN)) - ) - assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" def test_get_users(self): - admincenter_client = AdminCenter(set_mocked_m365_provider()) - assert len(admincenter_client.users) == 1 - assert admincenter_client.users["user-1@tenant1.es"].id == "id-1" - assert admincenter_client.users["user-1@tenant1.es"].name == "User 1" + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter(set_mocked_m365_provider()) + assert len(admincenter_client.users) == 1 + assert admincenter_client.users["user-1@tenant1.es"].id == "id-1" + assert admincenter_client.users["user-1@tenant1.es"].name == "User 1" def test_get_group_settings(self): - admincenter_client = AdminCenter(set_mocked_m365_provider()) - assert len(admincenter_client.groups) == 1 - assert admincenter_client.groups["id-1"].id == "id-1" - assert admincenter_client.groups["id-1"].name == "Test" - assert admincenter_client.groups["id-1"].visibility == "Public" + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter(set_mocked_m365_provider()) + assert len(admincenter_client.groups) == 1 + assert admincenter_client.groups["id-1"].id == "id-1" + assert admincenter_client.groups["id-1"].name == "Test" + assert admincenter_client.groups["id-1"].visibility == "Public" def test_get_directory_roles(self): - admincenter_client = AdminCenter(set_mocked_m365_provider()) - assert ( - admincenter_client.directory_roles["GlobalAdministrator"].id - == "id-directory-role" - ) - assert ( - len(admincenter_client.directory_roles["GlobalAdministrator"].members) == 0 - ) + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter(set_mocked_m365_provider()) + assert ( + admincenter_client.directory_roles["GlobalAdministrator"].id + == "id-directory-role" + ) + assert ( + len(admincenter_client.directory_roles["GlobalAdministrator"].members) + == 0 + ) + + def test_get_organization(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.organization_config.guid == "id-1" + assert admincenter_client.organization_config.name == "Test" + assert ( + admincenter_client.organization_config.customer_lockbox_enabled is False + ) + admincenter_client.powershell.close() + + def test_get_sharing_policy(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + ): + admincenter_client = AdminCenter( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.sharing_policy.guid == "id-1" + assert admincenter_client.sharing_policy.name == "Test" + assert admincenter_client.sharing_policy.enabled is False + admincenter_client.powershell.close() diff --git a/tests/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py b/tests/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py index 7c7cb7506b..e0537249a1 100644 --- a/tests/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py @@ -15,6 +15,9 @@ class Test_admincenter_settings_password_never_expire: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", new=admincenter_client, @@ -40,6 +43,9 @@ class Test_admincenter_settings_password_never_expire: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", new=admincenter_client, @@ -81,6 +87,9 @@ class Test_admincenter_settings_password_never_expire: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", new=admincenter_client, diff --git a/tests/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py b/tests/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py index 06f807c1c2..6b93429b3d 100644 --- a/tests/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py @@ -15,6 +15,9 @@ class Test_admincenter_users_admins_reduced_license_footprint: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", new=admincenter_client, @@ -40,6 +43,9 @@ class Test_admincenter_users_admins_reduced_license_footprint: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", new=admincenter_client, @@ -77,6 +83,9 @@ class Test_admincenter_users_admins_reduced_license_footprint: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", new=admincenter_client, @@ -123,6 +132,9 @@ class Test_admincenter_users_admins_reduced_license_footprint: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", new=admincenter_client, @@ -169,6 +181,9 @@ class Test_admincenter_users_admins_reduced_license_footprint: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", new=admincenter_client, diff --git a/tests/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py b/tests/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py index 994dbbcf49..98cb224900 100644 --- a/tests/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py +++ b/tests/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py @@ -15,6 +15,9 @@ class Test_admincenter_users_between_two_and_four_global_admins: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", new=admincenter_client, @@ -40,6 +43,9 @@ class Test_admincenter_users_between_two_and_four_global_admins: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", new=admincenter_client, @@ -91,6 +97,9 @@ class Test_admincenter_users_between_two_and_four_global_admins: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", new=admincenter_client, @@ -153,6 +162,9 @@ class Test_admincenter_users_between_two_and_four_global_admins: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), mock.patch( "prowler.providers.m365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", new=admincenter_client, diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 629b18aaec..52b2818551 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,7 +7,8 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixes - Added validation to AWS IAM role. [(#7787)](https://github.com/prowler-cloud/prowler/pull/7787) - +- Retrieve more than 10 providers in /scans, /manage-groups and /findings pages. [(#7793)](https://github.com/prowler-cloud/prowler/pull/7793) + --- ## [v1.7.0] (Prowler v5.7.0) diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 3615c77b3b..f137b451cc 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -43,7 +43,7 @@ export default async function Findings({ sort: encodedSort, filters, }), - getProviders({}), + getProviders({ pageSize: 50 }), getScans({}), ]); diff --git a/ui/app/(prowler)/manage-groups/page.tsx b/ui/app/(prowler)/manage-groups/page.tsx index 178baf320e..c5e5be9932 100644 --- a/ui/app/(prowler)/manage-groups/page.tsx +++ b/ui/app/(prowler)/manage-groups/page.tsx @@ -58,7 +58,7 @@ export default function ManageGroupsPage({ } const SSRAddGroupForm = async () => { - const providersResponse = await getProviders({}); + const providersResponse = await getProviders({ pageSize: 50 }); const rolesResponse = await getRoles({}); const providersData = @@ -95,7 +95,7 @@ const SSRDataEditGroup = async ({ return