Skip to content

Commit 664b49c

Browse files
committed
Implement CIS 1.3.7 (third-party storage services restriction)
- metadata.json incorrectly mapped 1.3.7 to the OWA mailbox policy collector (which actually powers 6.5.3). Confirmed via CIS benchmark audit text that 1.3.7 checks the 'Third Party Storage Services' service principal in Entra ID instead. - Add new Graph-based collector (entra.applications.third_party_storage_services) - Add Rego policy + unit tests (4 cases, all passing) - Update metadata.json: automation_status=ready, correct collector, policy_file, and requires_permissions - Verified end-to-end via structural pytest suite (438 passed) and a live scan against the sandbox tenant
1 parent ef7d4a6 commit 664b49c

5 files changed

Lines changed: 225 additions & 6 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Third-party storage services collector.
2+
3+
CIS Microsoft 365 Foundations Benchmark Controls:
4+
v6.0.0: 1.3.7
5+
6+
Connection Method: Microsoft Graph API
7+
Required Scopes: Application.Read.All
8+
Graph Endpoint: /servicePrincipals (v1.0)
9+
"""
10+
11+
from typing import Any
12+
13+
from collectors.base import BaseDataCollector
14+
from collectors.graph_client import GraphClient
15+
16+
# Well-known appId for the "Third Party Storage Services" service principal
17+
# referenced by CIS control 1.3.7 (Microsoft 365 on the web).
18+
THIRD_PARTY_STORAGE_APP_ID = "c1f33bc0-bdb4-4248-ba9b-096807ddb43e"
19+
20+
21+
class ThirdPartyStorageServicesDataCollector(BaseDataCollector):
22+
"""Collects the Third Party Storage Services service principal state.
23+
24+
This collector retrieves the Entra ID service principal that governs
25+
whether users can open files stored in third-party storage providers
26+
(e.g. Dropbox) from Microsoft 365 on the web, to verify it is disabled.
27+
"""
28+
29+
async def collect(self, client: GraphClient) -> dict[str, Any]:
30+
"""Collect third-party storage service principal data.
31+
32+
Returns:
33+
Dict containing:
34+
- service_principal_exists: Whether the SP has been created in the tenant
35+
- account_enabled: The SP's accountEnabled value (None if not found)
36+
- service_principal: Raw service principal object, if found
37+
"""
38+
collector_error: str | None = None
39+
service_principal: dict[str, Any] | None = None
40+
41+
try:
42+
resp = await client.get(
43+
f"/servicePrincipals?$filter=appId eq '{THIRD_PARTY_STORAGE_APP_ID}'"
44+
)
45+
results = resp.get("value") if isinstance(resp, dict) else None
46+
if isinstance(results, list) and results:
47+
service_principal = results[0]
48+
except Exception as exc:
49+
collector_error = str(exc)
50+
51+
account_enabled = (
52+
service_principal.get("accountEnabled")
53+
if isinstance(service_principal, dict)
54+
else None
55+
)
56+
57+
return {
58+
"service_principal_exists": service_principal is not None,
59+
"account_enabled": account_enabled,
60+
"service_principal": service_principal,
61+
"collector_error": collector_error,
62+
}

engine/collectors/registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
)
99
from collectors.entra.applications.forms_settings import FormsSettingsDataCollector
1010

11+
from collectors.entra.applications.third_party_storage_services import (
12+
ThirdPartyStorageServicesDataCollector,
13+
)
1114
# Authentication
1215
from collectors.entra.authentication.authentication_methods import (
1316
AuthenticationMethodsDataCollector,
@@ -144,6 +147,7 @@
144147
# Applications
145148
"entra.applications.apps_and_services_settings": AppsAndServicesSettingsDataCollector,
146149
"entra.applications.forms_settings": FormsSettingsDataCollector,
150+
"entra.applications.third_party_storage_services": ThirdPartyStorageServicesDataCollector,
147151
# Authentication
148152
"entra.authentication.authentication_methods": AuthenticationMethodsDataCollector,
149153
"entra.authentication.mfa_fatigue_protection": MfaFatigueProtectionDataCollector,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# METADATA
2+
# title: Ensure 'third-party storage services' are restricted in 'Microsoft 365 on the web'
3+
# description: |
4+
# Third-party storage services (Dropbox, Google Drive, Box, etc.) can be
5+
# enabled for users in Microsoft 365 on the web, allowing them to store and
6+
# share documents outside organizational control alongside OneDrive and
7+
# team sites. This increases the risk of data breaches and makes it
8+
# difficult to maintain data privacy and security. Restrict the "Third
9+
# Party Storage Services" service principal in Entra ID to prevent this.
10+
# related_resources:
11+
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
12+
# description: CIS Microsoft 365 Foundations Benchmark
13+
# custom:
14+
# control_id: CIS-1.3.7
15+
# framework: cis
16+
# benchmark: microsoft-365-foundations
17+
# version: v6.0.0
18+
# severity: medium
19+
# service: EntraID
20+
# requires_permissions:
21+
# - Application.Read.All
22+
23+
package cis.microsoft_365_foundations.v6_0_0.control_1_3_7
24+
25+
default result := {"compliant": false, "message": "Evaluation failed"}
26+
27+
result := output if {
28+
exists := input.service_principal_exists
29+
enabled := input.account_enabled
30+
is_compliant := is_restricted(exists, enabled)
31+
32+
output := {
33+
"compliant": is_compliant,
34+
"message": generate_message(exists, enabled),
35+
"affected_resources": affected_resources(exists, enabled),
36+
"details": {
37+
"service_principal_exists": exists,
38+
"account_enabled": enabled,
39+
"app_id": "c1f33bc0-bdb4-4248-ba9b-096807ddb43e"
40+
}
41+
}
42+
}
43+
44+
# Compliant only when the service principal has been created AND disabled
45+
is_restricted(exists, enabled) := true if {
46+
exists == true
47+
enabled == false
48+
} else := false
49+
50+
generate_message(exists, enabled) := msg if {
51+
exists == true
52+
enabled == false
53+
msg := "The 'Third Party Storage Services' service principal is disabled; third-party storage is restricted."
54+
}
55+
56+
generate_message(exists, _) := msg if {
57+
exists == false
58+
msg := "The 'Third Party Storage Services' service principal has not been created; third-party storage remains available by default."
59+
}
60+
61+
generate_message(exists, enabled) := msg if {
62+
exists == true
63+
enabled == true
64+
msg := "The 'Third Party Storage Services' service principal exists but is still enabled (accountEnabled: true)."
65+
}
66+
67+
affected_resources(exists, enabled) := [] if {
68+
exists == true
69+
enabled == false
70+
} else := ["Third Party Storage Services (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e)"]

engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,15 @@
192192
"title": "Ensure 'third-party storage services' are restricted in 'Microsoft 365 on the web'",
193193
"description": "Restrict third-party storage providers in Office web apps.",
194194
"severity": "medium",
195-
"service": "Exchange",
195+
"service": "EntraID",
196196
"level": "L2",
197197
"is_manual": false,
198198
"benchmark_audit_type": "Automated",
199-
"automation_status": "not_started",
200-
"data_collector_id": "exchange.organization.owa_mailbox_policy",
201-
"policy_file": null,
202-
"requires_permissions": ["Exchange.Manage"],
203-
"notes": "Collector exists but control logic not defined"
199+
"automation_status": "ready",
200+
"data_collector_id": "entra.applications.third_party_storage_services",
201+
"policy_file": "1.3.7_third_party_storage_services.rego",
202+
"requires_permissions": ["Application.Read.All"],
203+
"notes": null
204204
},
205205
{
206206
"control_id": "1.3.8",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cis.microsoft_365_foundations.v6_0_0.test_1_3_7
2+
3+
import rego.v1
4+
5+
# ---------------------------------------------------------------------------
6+
# Test: compliant — service principal exists and is disabled
7+
# ---------------------------------------------------------------------------
8+
9+
test_compliant_service_principal_disabled if {
10+
result := data.cis.microsoft_365_foundations.v6_0_0.control_1_3_7.result with input as {
11+
"service_principal_exists": true,
12+
"account_enabled": false,
13+
"service_principal": {
14+
"id": "sp-001",
15+
"appId": "c1f33bc0-bdb4-4248-ba9b-096807ddb43e",
16+
"accountEnabled": false,
17+
},
18+
"collector_error": null,
19+
}
20+
result.compliant == true
21+
result.details.service_principal_exists == true
22+
result.details.account_enabled == false
23+
count(result.affected_resources) == 0
24+
}
25+
26+
# ---------------------------------------------------------------------------
27+
# Test: non-compliant — service principal has never been created (default state)
28+
# ---------------------------------------------------------------------------
29+
30+
test_non_compliant_service_principal_not_created if {
31+
result := data.cis.microsoft_365_foundations.v6_0_0.control_1_3_7.result with input as {
32+
"service_principal_exists": false,
33+
"account_enabled": null,
34+
"service_principal": null,
35+
"collector_error": null,
36+
}
37+
result.compliant == false
38+
result.details.service_principal_exists == false
39+
count(result.affected_resources) == 1
40+
}
41+
42+
# ---------------------------------------------------------------------------
43+
# Test: non-compliant — service principal exists but is still enabled
44+
# ---------------------------------------------------------------------------
45+
46+
test_non_compliant_service_principal_enabled if {
47+
result := data.cis.microsoft_365_foundations.v6_0_0.control_1_3_7.result with input as {
48+
"service_principal_exists": true,
49+
"account_enabled": true,
50+
"service_principal": {
51+
"id": "sp-001",
52+
"appId": "c1f33bc0-bdb4-4248-ba9b-096807ddb43e",
53+
"accountEnabled": true,
54+
},
55+
"collector_error": null,
56+
}
57+
result.compliant == false
58+
result.details.account_enabled == true
59+
count(result.affected_resources) == 1
60+
}
61+
62+
# ---------------------------------------------------------------------------
63+
# Test: details include correct evidence fields
64+
# ---------------------------------------------------------------------------
65+
66+
test_result_details_structure if {
67+
result := data.cis.microsoft_365_foundations.v6_0_0.control_1_3_7.result with input as {
68+
"service_principal_exists": true,
69+
"account_enabled": false,
70+
"service_principal": {
71+
"id": "sp-001",
72+
"appId": "c1f33bc0-bdb4-4248-ba9b-096807ddb43e",
73+
"accountEnabled": false,
74+
},
75+
"collector_error": null,
76+
}
77+
_ = result.compliant
78+
_ = result.message
79+
_ = result.affected_resources
80+
_ = result.details.service_principal_exists
81+
_ = result.details.account_enabled
82+
_ = result.details.app_id
83+
}

0 commit comments

Comments
 (0)