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
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ This document provides a comprehensive overview of all 140 controls in the CIS M

| Our Audit Type | Count | Description |
|----------------|-------|-------------|
| Automated | 46 | Fully automatable with current collectors |
| Automated | 47 | Fully automatable with current collectors |
| Deferred | 12 | Collector works but needs "compliant with review" capability |
| Blocked | 21 | Collector exists but authentication issues prevent execution |
| Manual | 14 | No API available, truly requires manual verification |
| Not Started | 47 | No collector implemented yet |
| Not Started | 46 | No collector implemented yet |

---

Expand Down Expand Up @@ -54,7 +54,7 @@ This document provides a comprehensive overview of all 140 controls in the CIS M
| 1.2.2 | L1 | Ensure sign-in to shared mailboxes is blocked | Automated | Not Started | `exchange.mailbox.mailboxes` | Not Started | Collector exists but control logic not defined |
| 1.3.1 | L1 | Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)' | Automated | Automated | `entra.domains.password_policy` | Implemented | |
| 1.3.2 | L2 | Ensure 'Idle session timeout' is set to '3 hours (or less)' for unmanaged devices | Automated | Deferred | `entra.conditional_access.policies` | Implemented | Requires CA policy coverage verification |
| 1.3.3 | L2 | Ensure 'External sharing' of calendars is not available | Automated | Not Started | `exchange.organization.sharing_policy` | Not Started | Collector exists but control logic not defined |
| 1.3.3 | L2 | Ensure 'External sharing' of calendars is not available | Automated | Automated | `exchange.organization.sharing_policy` | Implemented | `Get-SharingPolicy` Domains; flags Anonymous/wildcard + CalendarSharing on enabled policies; see metadata notes |
| 1.3.4 | L1 | Ensure 'User owned apps and services' is restricted | Automated | Automated | `entra.applications.apps_and_services_settings` | Implemented | |
| 1.3.5 | L1 | Ensure internal phishing protection for Forms is enabled | Automated | Automated | `entra.applications.forms_settings` | Implemented | |
| 1.3.6 | L2 | Ensure the customer lockbox feature is enabled | Automated | Automated | `exchange.organization.organization_config` | Implemented | |
Expand Down
105 changes: 85 additions & 20 deletions engine/collectors/exchange/organization/sharing_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,79 @@
from collectors.powershell_client import PowerShellClient


def _normalize_domains(domains: Any) -> list[str]:
if domains is None:
return []
if isinstance(domains, str):
s = domains.strip()
if not s:
return []
for sep in (";", ","):
if sep in s:
return [p.strip() for p in s.split(sep) if p.strip()]
return [s]
if isinstance(domains, list):
out: list[str] = []
for item in domains:
if not isinstance(item, str):
continue
chunk = item.strip()
if not chunk:
continue
if ";" in chunk or "," in chunk:
sep = ";" if ";" in chunk else ","
out.extend(p.strip() for p in chunk.split(sep) if p.strip())
else:
out.append(chunk)
return out
return []


def _calendar_external_sharing_violation(domain_entry: str) -> str | None:
"""Return a reason code for non-compliant Domains entries.

SharingPolicy Domains are ``domain:Capability`` pairs. Only Anonymous and wildcard
entries with CalendarSharing capabilities are flagged; named SMTP partner domains
are not evaluated here.
"""
if ":" not in domain_entry:
return None
domain_part, cap = domain_entry.split(":", 1)
domain_part = domain_part.strip()
cap = cap.strip()
if not domain_part or not cap:
return None
if "calendarsharing" not in cap.lower():
return None
low = domain_part.lower()
if low == "anonymous":
return "anonymous_calendar_sharing"
if low == "*":
return "wildcard_calendar_sharing"
return None
Comment thread
williamywccc marked this conversation as resolved.


def _violations_for_policies(policies: list[Any]) -> list[dict[str, Any]]:
violations: list[dict[str, Any]] = []
for policy in policies:
if not isinstance(policy, dict):
continue
if policy.get("Enabled") is False:
continue
name = policy.get("Name") or "Unknown"
for entry in _normalize_domains(policy.get("Domains")):
reason = _calendar_external_sharing_violation(entry)
if reason:
violations.append(
{
"policy_name": name,
"domain_entry": entry,
"reason": reason,
}
)
return violations


class SharingPolicyDataCollector(BasePowerShellCollector):
"""Collects sharing policy settings for CIS compliance evaluation.
Comment thread
williamywccc marked this conversation as resolved.

Expand All @@ -29,37 +102,29 @@ async def collect(self, client: PowerShellClient) -> dict[str, Any]:
Dict containing:
- sharing_policies: List of sharing policies
- default_policy: The default sharing policy
- policies_allowing_external: Policies that allow external sharing
- calendar_sharing_violations: Non-compliant calendar sharing rules
- external_calendar_sharing_restricted: True if no violations found
"""
policies = await client.run_cmdlet("ExchangeOnline", "Get-SharingPolicy")

# Handle None, single policy, or list
if policies is None:
policies = []
elif isinstance(policies, dict):
policies = [policies]

# Find default policy
default_policy = next(
(p for p in policies if p.get("Default")),
policies[0] if policies else None
)

# Check for policies allowing external sharing
# Domains field contains sharing rules - if it has entries, sharing is enabled
policies_allowing_external = []
for policy in policies:
domains = policy.get("Domains", [])
if domains:
policies_allowing_external.append({
"name": policy.get("Name"),
"domains": domains,
"enabled": policy.get("Enabled"),
})
default_policy = None
if policies:
default_policy = next(
(p for p in policies if p.get("Default")),
policies[0],
)

violations = _violations_for_policies(policies)

return {
"sharing_policies": policies,
"total_policies": len(policies),
"default_policy": default_policy,
"policies_allowing_external": policies_allowing_external,
"calendar_sharing_violations": violations,
"external_calendar_sharing_restricted": len(violations) == 0,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# METADATA
# title: Ensure 'External sharing' of calendars is not available
# description: |
# Calendar information must not be shared using sharing-policy rules that
# apply anonymously or to any unspecified domain. This policy flags Domains
# entries that pair Anonymous or wildcard domains with CalendarSharing
# capabilities on enabled sharing policies.
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
# description: CIS Microsoft 365 Foundations Benchmark
# custom:
# control_id: CIS-1.3.3
# framework: cis
# benchmark: microsoft-365-foundations
# version: v6.0.0
# severity: medium
# service: Exchange
# requires_permissions:
# - Exchange.Manage

package cis.microsoft_365_foundations.v6_0_0.control_1_3_3

import rego.v1

default result := {
"compliant": false,
"message": "Evaluation failed: unable to retrieve sharing policy data",
"details": {},
}

result := output if {
input.sharing_policies != null
violations := get_array(input, "calendar_sharing_violations")
affected := [sprintf("%s: %s", [v.policy_name, v.domain_entry]) |
some v in violations
v.policy_name != null
v.domain_entry != null
]
output := {
"compliant": count(violations) == 0,
"message": build_message(violations),
"affected_resources": affected,
"details": {
"total_policies": object.get(input, "total_policies", null),
"violating_count": count(violations),
"violations": violations,
},
}
}

get_array(obj, key) := obj[key] if { obj[key] } else := []

build_message(violations) := "No enabled sharing policies allow anonymous or wildcard calendar sharing" if {
count(violations) == 0
} else := sprintf("%d calendar sharing domain rule(s) allow anonymous or wildcard external-style sharing", [count(violations)])
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@
"level": "L2",
"is_manual": false,
"benchmark_audit_type": "Automated",
"automation_status": "not_started",
"automation_status": "ready",
"data_collector_id": "exchange.organization.sharing_policy",
"policy_file": null,
"policy_file": "1.3.3_external_calendar_sharing.rego",
"requires_permissions": ["Exchange.Manage"],
"notes": "Collector exists but control logic not defined"
"notes": "Flags Anonymous: or *: Domains entries with CalendarSharing capabilities on enabled policies; does not evaluate named external SMTP domains."
},
{
"control_id": "1.3.4",
Expand Down
83 changes: 83 additions & 0 deletions engine/samples/exchange_organization_sharing_policy_sample.json
Comment thread
williamywccc marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"collector_id": "exchange.organization.sharing_policy",
"timestamp": "2026-05-11T21:54:06.172189",
"elapsed_seconds": 10.545,
"data": {
"sharing_policies": [
{
"Domains": [
"Anonymous:CalendarSharingFreeBusyReviewer",
"*:CalendarSharingFreeBusySimple"
],
"Enabled": true,
"Default": true,
"AdminDisplayName": "",
"ExchangeVersion": "0.10 (14.0.100.0)",
"Name": "Default Sharing Policy",
"DistinguishedName": "CN=Default Sharing Policy,CN=Federation,CN=Configuration,CN=t8sjf.onmicrosoft.com,CN=ConfigurationUnits,DC=APCPR03A007,DC=PROD,DC=OUTLOOK,DC=COM",
"Identity": "Default Sharing Policy",
"ObjectCategory": "APCPR03A007.PROD.OUTLOOK.COM/Configuration/Schema/ms-Exch-Sharing-Policy",
"ObjectClass": [
"top",
"msExchSharingPolicy"
],
"WhenChanged": "2026-01-11T03:59:50+00:00",
"WhenCreated": "2026-01-11T03:59:43+00:00",
"WhenChangedUTC": "2026-01-11T03:59:50Z",
"WhenCreatedUTC": "2026-01-11T03:59:43Z",
"ExchangeObjectId": "512f2621-2928-4360-8510-277ea11cda72",
"OrganizationalUnitRoot": "t8sjf.onmicrosoft.com",
"OrganizationId": "APCPR03A007.PROD.OUTLOOK.COM/Microsoft Exchange Hosted Organizations/t8sjf.onmicrosoft.com - APCPR03A007.PROD.OUTLOOK.COM/ConfigurationUnits/t8sjf.onmicrosoft.com/Configuration",
"Id": "Default Sharing Policy",
"Guid": "512f2621-2928-4360-8510-277ea11cda72",
"OriginatingServer": "TYZPR03A07DC004.APCPR03A007.PROD.OUTLOOK.COM",
"IsValid": true,
"ObjectState": "Changed"
}
],
"total_policies": 1,
"default_policy": {
"Domains": [
"Anonymous:CalendarSharingFreeBusyReviewer",
"*:CalendarSharingFreeBusySimple"
],
"Enabled": true,
"Default": true,
"AdminDisplayName": "",
"ExchangeVersion": "0.10 (14.0.100.0)",
"Name": "Default Sharing Policy",
"DistinguishedName": "CN=Default Sharing Policy,CN=Federation,CN=Configuration,CN=t8sjf.onmicrosoft.com,CN=ConfigurationUnits,DC=APCPR03A007,DC=PROD,DC=OUTLOOK,DC=COM",
"Identity": "Default Sharing Policy",
"ObjectCategory": "APCPR03A007.PROD.OUTLOOK.COM/Configuration/Schema/ms-Exch-Sharing-Policy",
"ObjectClass": [
"top",
"msExchSharingPolicy"
],
"WhenChanged": "2026-01-11T03:59:50+00:00",
"WhenCreated": "2026-01-11T03:59:43+00:00",
"WhenChangedUTC": "2026-01-11T03:59:50Z",
"WhenCreatedUTC": "2026-01-11T03:59:43Z",
"ExchangeObjectId": "512f2621-2928-4360-8510-277ea11cda72",
"OrganizationalUnitRoot": "t8sjf.onmicrosoft.com",
"OrganizationId": "APCPR03A007.PROD.OUTLOOK.COM/Microsoft Exchange Hosted Organizations/t8sjf.onmicrosoft.com - APCPR03A007.PROD.OUTLOOK.COM/ConfigurationUnits/t8sjf.onmicrosoft.com/Configuration",
"Id": "Default Sharing Policy",
"Guid": "512f2621-2928-4360-8510-277ea11cda72",
"OriginatingServer": "TYZPR03A07DC004.APCPR03A007.PROD.OUTLOOK.COM",
"IsValid": true,
"ObjectState": "Changed"
},
"calendar_sharing_violations": [
{
"policy_name": "Default Sharing Policy",
"domain_entry": "Anonymous:CalendarSharingFreeBusyReviewer",
"reason": "anonymous_calendar_sharing"
},
{
"policy_name": "Default Sharing Policy",
"domain_entry": "*:CalendarSharingFreeBusySimple",
"reason": "wildcard_calendar_sharing"
}
],
"external_calendar_sharing_restricted": false
}
}
Loading