Skip to content
Open
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
11 changes: 11 additions & 0 deletions engine/collectors/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@
ReportSubmissionPolicyDataCollector,
)

# SharePoint
from collectors.sharepoint.spo_tenant import SpoTenantDataCollector
from collectors.sharepoint.spo_site import SpoSiteDataCollector
from collectors.sharepoint.spo_sync_client_restriction import (
SpoSyncClientRestrictionDataCollector,
)

# Registry mapping data_collector_id to collector class
DATA_COLLECTORS: dict[str, type[BaseDataCollector]] = {
# Applications
Expand Down Expand Up @@ -220,6 +227,10 @@
"exchange.transport.transport_rules": TransportRulesDataCollector,
# Compliance
"compliance.report_submission_policy": ReportSubmissionPolicyDataCollector,
# Sharepoint
"sharepoint.spo_tenant": SpoTenantDataCollector,
"sharepoint.spo_site": SpoSiteDataCollector,
"sharepoint.spo_sync_client_restriction": SpoSyncClientRestrictionDataCollector,
}


Expand Down
60 changes: 46 additions & 14 deletions engine/collectors/sharepoint/spo_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,10 @@
CIS Microsoft 365 Foundations Benchmark Controls:
v6.0.0: 7.2.4

Connection Method: SharePoint REST API
Authentication: Client secret via MSAL (access token)
Connection Method: Microsoft Graph
Authentication: Client secret via MSAL application permissions

CAVEAT: Access token authentication has not been fully tested.
It should work, but needs verification during implementation. Certificate-based
authentication may be required instead of client secret authentication.

NOTE: This collector uses SharePoint REST API instead of PowerShell because
SharePoint Online PowerShell does not support client secret authentication.
If certificate authentication is adopted in the future, this collector should
be updated to use the Get-SPOSite cmdlet instead.

REST Endpoints: /_api/site or SharePoint Admin API for site collections
Graph Endpoints: GET /sites/getAllSites
"""

from typing import Any
Expand All @@ -31,6 +22,14 @@ class SpoSiteDataCollector(BaseDataCollector):
sharing settings for compliance evaluation.
"""

@staticmethod
def _first_present(data: dict[str, Any], *keys: str) -> Any:
"""Return the first non-missing value from a set of possible keys."""
for key in keys:
if key in data:
return data[key]
return None

async def collect(self, client: SharePointClient) -> dict[str, Any]:
"""Collect SPO site data.

Expand All @@ -40,5 +39,38 @@ async def collect(self, client: SharePointClient) -> dict[str, Any]:
- onedrive_sites: OneDrive for Business sites
- site_sharing_settings: Per-site sharing configurations
"""
# TODO: Implement collector
raise NotImplementedError("Collector not yet implemented")
tenant_settings = await client.get_tenant_settings()
sites = await client.search_sites()

onedrive_sites = [
site for site in sites if "-my.sharepoint.com/" in str(site.get("url", ""))
]

return {
"sites": sites,
"onedrive_sites": onedrive_sites,
"site_sharing_settings": {
"sharepoint_sharing_capability": self._first_present(
tenant_settings,
"sharingCapability",
"coreSharingCapability",
"SharingCapability",
"CoreSharingCapability",
),
"onedrive_sharing_capability": self._first_present(
tenant_settings,
"oneDriveSharingCapability",
"OneDriveSharingCapability",
),
"core_default_share_link_scope": self._first_present(
tenant_settings,
"coreDefaultShareLinkScope",
"CoreDefaultShareLinkScope",
),
"onedrive_default_share_link_scope": self._first_present(
tenant_settings,
"oneDriveDefaultShareLinkScope",
"OneDriveDefaultShareLinkScope",
),
},
}
27 changes: 13 additions & 14 deletions engine/collectors/sharepoint/spo_sync_client_restriction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,10 @@
CIS Microsoft 365 Foundations Benchmark Controls:
v6.0.0: 7.3.2

Connection Method: SharePoint REST API
Authentication: Client secret via MSAL (access token)
Connection Method: Microsoft Graph
Authentication: Client secret via MSAL application permissions

CAVEAT: Access token authentication has not been fully tested.
It should work, but needs verification during implementation. Certificate-based
authentication may be required instead of client secret authentication.

NOTE: This collector uses SharePoint REST API instead of PowerShell because
SharePoint Online PowerShell does not support client secret authentication.
If certificate authentication is adopted in the future, this collector should
be updated to use the Get-SPOTenantSyncClientRestriction cmdlet instead.

REST Endpoints: SharePoint Admin API for sync client restrictions
Graph Endpoints: GET /admin/sharepoint/settings
"""

from typing import Any
Expand All @@ -41,5 +32,13 @@ async def collect(self, client: SharePointClient) -> dict[str, Any]:
- domain_guids_allowed: Allowed domain GUIDs for sync
- excluded_file_extensions: File types excluded from sync
"""
# TODO: Implement collector
raise NotImplementedError("Collector not yet implemented")
restrictions = await client.get_sync_client_restriction()

return {
"sync_client_restrictions": restrictions,
"tenant_restriction_enabled": restrictions.get("TenantRestrictionEnabled"),
"block_mac_sync": restrictions.get("BlockMacSync"),
"domain_guids_allowed": restrictions.get("AllowedDomainList", []),
"excluded_file_extensions": restrictions.get("ExcludedFileExtensions", []),
"groove_block_option": restrictions.get("GrooveBlockOption"),
}
196 changes: 180 additions & 16 deletions engine/collectors/sharepoint/spo_tenant.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
"""SPO tenant collector.

CIS Microsoft 365 Foundations Benchmark Controls:
v6.0.0: 7.2.1, 7.2.2, 7.2.3, 7.2.4, 7.2.5, 7.2.6, 7.2.7, 7.2.9, 7.2.10, 7.2.11, 7.3.1
v6.0.0: 7.2.1, 7.2.3, 7.2.4, 7.2.5, 7.2.6, 7.2.7, 7.2.8, 7.2.9, 7.2.10, 7.2.11, 7.3.1

Connection Method: SharePoint REST API
Authentication: Client secret via MSAL (access token)
Connection Method: Microsoft Graph
Authentication: Client secret via MSAL application permissions

CAVEAT: Access token authentication has not been fully tested.
It should work, but needs verification during implementation. Certificate-based
authentication may be required instead of client secret authentication.

NOTE: This collector uses SharePoint REST API instead of PowerShell because
SharePoint Online PowerShell does not support client secret authentication.
If certificate authentication is adopted in the future, this collector should
be updated to use the Get-SPOTenant cmdlet instead.

REST Endpoints: /_api/SPOTenant or SharePoint Admin API
Graph Endpoints: GET /admin/sharepoint/settings
"""

from typing import Any
Expand All @@ -31,17 +22,190 @@ class SpoTenantDataCollector(BaseDataCollector):
sharing, authentication, guest access, and other configurations.
"""

@staticmethod
def _first_present(data: dict[str, Any], *keys: str) -> Any:
"""Return the first non-missing value from a set of possible keys."""
for key in keys:
if key in data:
return data[key]
return None

@staticmethod
def _as_list(value: Any) -> list[Any]:
"""Normalize SharePoint list-like fields to plain lists."""
if value is None:
return []
if isinstance(value, list):
return value
if isinstance(value, tuple):
return list(value)
if isinstance(value, str):
if not value.strip():
return []
return [item.strip() for item in value.split(",") if item.strip()]
if isinstance(value, dict) and isinstance(value.get("results"), list):
return value["results"]
return [value]

@staticmethod
def _as_bool(value: Any) -> bool | None:
"""Normalize bool-like values from Graph/REST payloads."""
if value is None:
return None
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"true", "1", "yes"}:
return True
if normalized in {"false", "0", "no"}:
return False
return None

@classmethod
def _prevent_external_users_from_resharing(cls, tenant_settings: dict[str, Any]) -> bool | None:
"""Resolve guest resharing protection from direct or inverse field variants."""
direct_value = cls._first_present(
tenant_settings,
"PreventExternalUsersFromResharing",
"preventExternalUsersFromResharing",
)
direct_bool = cls._as_bool(direct_value)
if direct_bool is not None:
return direct_bool

inverse_value = cls._first_present(
tenant_settings,
"isResharingByExternalUsersEnabled",
"IsResharingByExternalUsersEnabled",
)
inverse_bool = cls._as_bool(inverse_value)
if inverse_bool is not None:
return not inverse_bool

return None

async def collect(self, client: SharePointClient) -> dict[str, Any]:
"""Collect SPO tenant data.

Returns:
Dict containing:
- tenant_settings: Full tenant configuration
- legacy_auth_protocols_enabled: Legacy auth status
- azure_ad_b2b_integration_enabled: B2B integration status
- sharing_capability: External sharing capability
- default_sharing_link_type: Default sharing link type
- default_link_permission: Default link permission level
"""
# TODO: Implement collector
raise NotImplementedError("Collector not yet implemented")
tenant_settings = await client.get_tenant_settings()

return {
"tenant_settings": tenant_settings,
"legacy_auth_protocols_enabled": self._first_present(
tenant_settings,
"isLegacyAuthProtocolsEnabled",
"LegacyAuthProtocolsEnabled",
"legacyAuthProtocolsEnabled",
),
"sharing_capability": self._first_present(
tenant_settings,
"SharingCapability",
"sharingCapability",
"CoreSharingCapability",
"coreSharingCapability",
),
"onedrive_sharing_capability": self._first_present(
tenant_settings,
"OneDriveSharingCapability",
"oneDriveSharingCapability",
Comment on lines +116 to +119
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Populate OneDrive sharing capability from Graph response

SpoTenantDataCollector.collect now reads onedrive_sharing_capability only from OneDriveSharingCapability/oneDriveSharingCapability, but this collector is fed by SharePointClient.get_tenant_settings() (GET /admin/sharepoint/settings), whose Graph payload does not expose those keys. That means this field is always null, and 7.2.4_onedrive_content_sharing_restricted.rego will consistently evaluate as non-compliant/unknown after 7.2.4 was marked ready, creating false failures for all tenants.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tinar10 I haven't checked this particular case mentioned here but it seems like one to double check

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tinar10 Can you please check the validity of this? Does the endpoint actually return this key?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@du-dhartley , let me double check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@du-dhartley , validated this Graph response from GET /admin/sharepoint/settings. The response includes sharingCapability, but it does not include oneDriveSharingCapability or OneDriveSharingCapability.

This confirms that the current collector mapping will resolve onedrive_sharing_capability to NULL only when using this endpoint, so the control for 7.2.4 may produce false failures unless we identify another data source for the OneDrive-specific sharing setting.
image

),
"default_sharing_link_type": self._first_present(
tenant_settings,
"DefaultSharingLinkType",
"defaultSharingLinkType",
"CoreDefaultShareLinkScope",
"coreDefaultShareLinkScope",
),
"default_link_permission": self._first_present(
tenant_settings,
"DefaultLinkPermission",
"defaultLinkPermission",
"DefaultShareLinkRole",
"defaultShareLinkRole",
"CoreDefaultShareLinkRole",
"coreDefaultShareLinkRole",
),
"prevent_external_users_from_resharing": self._prevent_external_users_from_resharing(
tenant_settings
),
"sharing_domain_restriction_mode": self._first_present(
tenant_settings,
"SharingDomainRestrictionMode",
"sharingDomainRestrictionMode",
),
"sharing_allowed_domain_list": self._as_list(
self._first_present(
tenant_settings,
"SharingAllowedDomainList",
"sharingAllowedDomainList",
)
),
"sharing_blocked_domain_list": self._as_list(
self._first_present(
tenant_settings,
"SharingBlockedDomainList",
"sharingBlockedDomainList",
)
),
"restrict_external_sharing": self._as_list(
self._first_present(
tenant_settings,
"RestrictExternalSharing",
"restrictExternalSharing",
)
),
"external_user_expiration_required": self._first_present(
tenant_settings,
"ExternalUserExpirationRequired",
"externalUserExpirationRequired",
),
"external_user_expire_in_days": self._first_present(
tenant_settings,
"ExternalUserExpireInDays",
"externalUserExpireInDays",
),
"email_attestation_required": self._first_present(
tenant_settings,
"EmailAttestationRequired",
"emailAttestationRequired",
),
"email_attestation_reauth_days": self._first_present(
tenant_settings,
"EmailAttestationReAuthDays",
"emailAttestationReAuthDays",
),
"disallow_infected_file_download": self._first_present(
tenant_settings,
"DisallowInfectedFileDownload",
"disallowInfectedFileDownload",
),
"core_default_share_link_scope": self._first_present(
tenant_settings,
"CoreDefaultShareLinkScope",
"coreDefaultShareLinkScope",
),
"core_default_share_link_role": self._first_present(
tenant_settings,
"CoreDefaultShareLinkRole",
"coreDefaultShareLinkRole",
),
"onedrive_default_share_link_scope": self._first_present(
tenant_settings,
"OneDriveDefaultShareLinkScope",
"oneDriveDefaultShareLinkScope",
),
"onedrive_default_share_link_role": self._first_present(
tenant_settings,
"OneDriveDefaultShareLinkRole",
"oneDriveDefaultShareLinkRole",
),
}
Loading