Skip to content

feat(sharepoint): add new check related with OneDrive Sync #7589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
| GCP | 79 | 13 | 7 | 3 |
| Azure | 140 | 18 | 8 | 3 |
| Kubernetes | 83 | 7 | 4 | 7 |
| M365 | 5 | 2 | 1 | 0 |
| M365 | 44 | 2 | 1 | 0 |
| NHN (Unofficial) | 6 | 2 | 1 | 0 |

> You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
Expand Down
1 change: 1 addition & 0 deletions prowler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add new check `teams_meeting_presenters_restricted` [(#7613)](https://github.com/prowler-cloud/prowler/pull/7613)
- Add new check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
- Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
- Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Provider": "m365",
"CheckID": "sharepoint_onedrive_sync_restricted_unmanaged_devices",
"CheckTitle": "Ensure OneDrive sync is restricted for unmanaged devices.",
"CheckType": [],
"ServiceName": "sharepoint",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "Sharepoint Settings",
"Description": "Microsoft OneDrive allows users to sign in their cloud tenant account and begin syncing select folders or the entire contents of OneDrive to a local computer. By default, this includes any computer with OneDrive already installed, whether it is Entra Joined, Entra Hybrid Joined or Active Directory Domain joined. The recommended state for this setting is Allow syncing only on computers joined to specific domains Enabled: Specify the AD domain GUID(s).",
"Risk": "Unmanaged devices can pose a security risk by allowing users to sync sensitive data to unauthorized devices, potentially leading to data leakage or unauthorized access.",
"RelatedUrl": "https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0",
"Remediation": {
"Code": {
"CLI": "Set-SPOTenantSyncClientRestriction -Enable -DomainGuids '<domain_guid_1>; <domain_guid_2>; ...'",
"NativeIaC": "",
"Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint 2. Click Settings then select OneDrive - Sync. 3. Check the Allow syncing only on computers joined to specific domains. 4. Use the Get-ADDomain PowerShell command on the on-premises server to obtain the GUID for each on-premises domain. 5. Click Save.",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict OneDrive sync to managed devices to prevent unauthorized access to sensitive data.",
"Url": "https://learn.microsoft.com/en-us/sharepoint/allow-syncing-only-on-specific-domains"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import List

from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.sharepoint.sharepoint_client import (
sharepoint_client,
)


class sharepoint_onedrive_sync_restricted_unmanaged_devices(Check):
"""
Check if OneDrive sync is restricted for unmanaged devices.
This check verifies that OneDrive sync is restricted to managed devices only.
Unmanaged devices can pose a security risk by allowing users to sync sensitive data to unauthorized devices,
potentially leading to data leakage or unauthorized access.
The check fails if OneDrive sync is not restricted to managed devices (AllowedDomainGuidsForSyncApp is empty).
"""

def execute(self) -> List[CheckReportM365]:
"""
Execute the OneDrive sync restriction check.
Retrieves the OneDrive sync settings from the Microsoft 365 SharePoint client and
generates a report indicating whether OneDrive sync is restricted to managed devices only.
Returns:
List[CheckReportM365]: A list containing the report object with the result of the check.
"""
findings = []
settings = sharepoint_client.settings
if settings:
report = CheckReportM365(
self.metadata(),
resource=settings if settings else {},
resource_name="SharePoint Settings",
resource_id=sharepoint_client.tenant_domain,
)
report.status = "PASS"
report.status_extended = "Microsoft 365 SharePoint does not allow OneDrive sync to unmanaged devices."

if len(settings.allowedDomainGuidsForSyncApp) == 0:
report.status = "FAIL"
report.status_extended = "Microsoft 365 SharePoint allows OneDrive sync to unmanaged devices."

findings.append(report)

return findings
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from asyncio import gather, get_event_loop
from typing import List, Optional

Expand Down Expand Up @@ -26,7 +27,6 @@ async def _get_settings(self):
settings = None
try:
global_settings = await self.client.admin.sharepoint.settings.get()

settings = SharePointSettings(
sharingCapability=(
str(global_settings.sharing_capability).split(".")[-1]
Expand All @@ -38,6 +38,7 @@ async def _get_settings(self):
sharingDomainRestrictionMode=global_settings.sharing_domain_restriction_mode,
legacyAuth=global_settings.is_legacy_auth_protocols_enabled,
resharingEnabled=global_settings.is_resharing_by_external_users_enabled,
allowedDomainGuidsForSyncApp=global_settings.allowed_domain_guids_for_sync_app,
)

except ODataError as error:
Expand All @@ -60,3 +61,4 @@ class SharePointSettings(BaseModel):
sharingDomainRestrictionMode: str
resharingEnabled: bool
legacyAuth: bool
allowedDomainGuidsForSyncApp: List[uuid.UUID]
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from unittest import mock

from prowler.providers.m365.services.sharepoint.sharepoint_service import (
Expand Down Expand Up @@ -35,6 +36,7 @@ def test_external_sharing_invalid_mode(self):
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="none",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -80,6 +82,7 @@ def test_allow_list_empty(self):
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="allowList",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -125,6 +128,7 @@ def test_block_list_empty(self):
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="blockList",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -170,6 +174,7 @@ def test_allow_list_non_empty(self):
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="allowList",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -215,6 +220,7 @@ def test_block_list_non_empty(self):
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="blockList",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from unittest import mock

from prowler.providers.m365.services.sharepoint.sharepoint_service import (
Expand Down Expand Up @@ -35,6 +36,7 @@ def test_external_sharing_restricted(self):
sharingDomainRestrictionMode="allowList",
resharingEnabled=False,
legacyAuth=True,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -78,6 +80,7 @@ def test_external_sharing_not_restricted(self):
sharingDomainRestrictionMode="allowList",
resharingEnabled=False,
legacyAuth=True,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from unittest import mock

from prowler.providers.m365.services.sharepoint.sharepoint_service import (
Expand Down Expand Up @@ -35,6 +36,7 @@ def test_guest_sharing_restricted(self):
sharingDomainRestrictionMode="allowList",
legacyAuth=True,
resharingEnabled=False,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -79,6 +81,7 @@ def test_guest_sharing_not_restricted(self):
sharingDomainRestrictionMode="allowList",
legacyAuth=True,
resharingEnabled=True,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from unittest import mock

from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
Expand Down Expand Up @@ -35,6 +36,7 @@ def test_sharepoint_modern_authentication_disabled(self):
sharingDomainRestrictionMode="allowList",
resharingEnabled=False,
legacyAuth=False,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down Expand Up @@ -81,6 +83,7 @@ def test_sharepoint_modern_authentication_enabled(self):
sharingDomainRestrictionMode="allowList",
resharingEnabled=False,
legacyAuth=True,
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import uuid
from unittest import mock

from prowler.providers.m365.services.sharepoint.sharepoint_service import (
SharePointSettings,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider


class Test_sharepoint_onedrive_sync_restricted_unmanaged_devices:
def test_no_allowed_domain_guids(self):
"""
Test when there are no allowed domain guids for OneDrive sync app
"""
sharepoint_client = mock.MagicMock

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_client",
new=sharepoint_client,
),
):
from prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices import (
sharepoint_onedrive_sync_restricted_unmanaged_devices,
)

sharepoint_client.settings = SharePointSettings(
sharingCapability="ExternalUserSharingOnly",
sharingAllowedDomainList=["allowed-domain.com"],
sharingBlockedDomainList=["blocked-domain.com"],
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="none",
allowedDomainGuidsForSyncApp=[],
)
sharepoint_client.tenant_domain = DOMAIN

check = sharepoint_onedrive_sync_restricted_unmanaged_devices()
result = check.execute()

assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Microsoft 365 SharePoint allows OneDrive sync to unmanaged devices."
)
assert result[0].resource_id == DOMAIN
assert result[0].location == "global"
assert result[0].resource_name == "SharePoint Settings"
assert result[0].resource == sharepoint_client.settings.dict()

def test_allowed_domain_guids(self):
"""
Test when there are allowed domain guids for OneDrive sync app
"""
sharepoint_client = mock.MagicMock

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_client",
new=sharepoint_client,
),
):
from prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices import (
sharepoint_onedrive_sync_restricted_unmanaged_devices,
)

sharepoint_client.settings = SharePointSettings(
sharingCapability="ExternalUserSharingOnly",
sharingAllowedDomainList=[],
sharingBlockedDomainList=["blocked-domain.com"],
legacyAuth=True,
resharingEnabled=False,
sharingDomainRestrictionMode="allowList",
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
)
sharepoint_client.tenant_domain = DOMAIN

check = sharepoint_onedrive_sync_restricted_unmanaged_devices()
result = check.execute()

assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Microsoft 365 SharePoint does not allow OneDrive sync to unmanaged devices."
)
assert result[0].resource_id == DOMAIN
assert result[0].location == "global"
assert result[0].resource_name == "SharePoint Settings"
assert result[0].resource == sharepoint_client.settings.dict()

def test_empty_settings(self):
"""
Test when sharepoint_client.settings is empty:
The check should return an empty list of findings.
"""
sharepoint_client = mock.MagicMock
sharepoint_client.settings = {}
sharepoint_client.tenant_domain = DOMAIN

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_client",
new=sharepoint_client,
),
):
from prowler.providers.m365.services.sharepoint.sharepoint_onedrive_sync_restricted_unmanaged_devices.sharepoint_onedrive_sync_restricted_unmanaged_devices import (
sharepoint_onedrive_sync_restricted_unmanaged_devices,
)

check = sharepoint_onedrive_sync_restricted_unmanaged_devices()
result = check.execute()

assert len(result) == 0
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from unittest.mock import patch

from prowler.providers.m365.models import M365IdentityInfo
Expand All @@ -7,6 +8,8 @@
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider

uuid_value = uuid.uuid4()


async def mock_sharepoint_get_settings(_):
return SharePointSettings(
Expand All @@ -16,6 +19,7 @@ async def mock_sharepoint_get_settings(_):
sharingDomainRestrictionMode="allowList",
resharingEnabled=False,
legacyAuth=True,
allowedDomainGuidsForSyncApp=[uuid_value],
)


Expand All @@ -39,3 +43,5 @@ def test_get_settings(self):
assert settings.sharingDomainRestrictionMode == "allowList"
assert settings.resharingEnabled is False
assert settings.legacyAuth is True
assert settings.allowedDomainGuidsForSyncApp == [uuid_value]
assert len(settings.allowedDomainGuidsForSyncApp) == 1
Loading