Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 | 48 | 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 | 45 | No collector implemented yet |

---

Expand All @@ -49,9 +49,9 @@ This document provides a comprehensive overview of all 140 controls in the CIS M
| 1.1.1 | L1 | Ensure Administrative accounts are cloud-only | Automated | Automated | `entra.roles.cloud_only_admins` | Implemented | |
| 1.1.2 | L1 | Ensure two emergency access accounts have been defined | Manual | Manual | | N/A | Organizational policy; requires human designation of accounts |
| 1.1.3 | L1 | Ensure that between two and four global admins are designated | Automated | Automated | `entra.roles.privileged_roles` | Implemented | |
| 1.1.4 | L1 | Ensure administrative accounts use licenses with a reduced application footprint | Automated | Not Started | | Not Started | Need to check user license assignments |
| 1.1.4 | L1 | Ensure administrative accounts use licenses with a reduced application footprint | Automated | Automated | `entra.roles.admin_license_footprint` | Implemented | Graph `licenseDetails` per admin; sample `{"admin_accounts":[{"userPrincipalName":"admin@contoso.com","uses_reduced_license_footprint":true,"high_footprint_service_plans_enabled":[],"sku_part_numbers":["AAD_PREMIUM_P2"]}]}` |
| 1.2.1 | L2 | Ensure that only organizationally managed/approved public groups exist | Automated | Not Started | `entra.groups.groups` | Not Started | Collector exists but control logic not defined |
| 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.2.2 | L1 | Ensure sign-in to shared mailboxes is blocked | Automated | Automated | `exchange.mailbox.mailboxes` | Implemented | Collector uses `Get-EXOMailbox` + `Get-User BlockCredentials`; sample: `{"shared_mailboxes":[{"UserPrincipalName":"finance@contoso.com","SignInBlocked":true}]}` |
| 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 |
Expand Down
43 changes: 25 additions & 18 deletions engine/collectors/exchange/mailbox/mailboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
CIS Microsoft 365 Foundations Benchmark Controls:
v6.0.0: 1.2.2

Control Description:
1.2.2 - Ensure sign-in to shared mailboxes is blocked

Connection Method: Exchange Online PowerShell (via Docker container)
Comment thread
williamywccc marked this conversation as resolved.
Authentication: Client secret via MSAL -> access token passed to -AccessToken parameter
Required Cmdlets: Get-EXOMailbox
Required Cmdlets: Get-EXOMailbox, Get-User
Required Permissions: Exchange.ManageAsApp + Exchange role assignment
"""

Expand All @@ -16,35 +19,39 @@


class MailboxesDataCollector(BasePowerShellCollector):
"""Collects mailbox information for CIS compliance evaluation.
"""Collects shared mailbox sign-in settings for CIS compliance evaluation.

This collector retrieves shared mailboxes to verify
shared mailboxes have appropriate sign-in settings.
This collector retrieves all shared mailboxes and checks whether direct
sign-in is blocked for each associated Entra account via Get-User
BlockCredentials property.
"""

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

Returns:
Dict containing:
- shared_mailboxes: List of shared mailboxes
- total_shared_mailboxes: Count of shared mailboxes
"""
# Get shared mailboxes only (RecipientTypeDetails -eq 'SharedMailbox')
mailboxes = await client.run_cmdlet(
"ExchangeOnline",
"Get-EXOMailbox",
RecipientTypeDetails="SharedMailbox",
ResultSize="Unlimited",
cmdlet = (
"Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | "
"ForEach-Object { "
"$mbx = $_; "
"$user = Get-User -Identity $mbx.UserPrincipalName -ErrorAction SilentlyContinue; "
"[PSCustomObject]@{ "
"UserPrincipalName = $mbx.UserPrincipalName; "
"DisplayName = $mbx.DisplayName; "
"PrimarySmtpAddress = $mbx.PrimarySmtpAddress; "
"SignInBlocked = if ($user) { $user.BlockCredentials } else { $null } "
"} }"
)
mailboxes = await client.run_cmdlet("ExchangeOnline", cmdlet)

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

signin_blocked = [m for m in mailboxes if m.get("SignInBlocked") is True]
signin_allowed = [m for m in mailboxes if m.get("SignInBlocked") is not True]

return {
"shared_mailboxes": mailboxes,
"total_shared_mailboxes": len(mailboxes),
"mailboxes_with_signin_blocked": len(signin_blocked),
"mailboxes_with_signin_allowed": len(signin_allowed),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# METADATA
# title: Ensure sign-in to shared mailboxes is blocked
# description: |
# Shared mailboxes should not allow direct sign-in. Enabling sign-in on a
# shared mailbox creates an unnecessary authentication surface that bypasses
# individual user accountability and increases the risk of credential abuse.
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
# description: CIS Microsoft 365 Foundations Benchmark
# custom:
# control_id: CIS-1.2.2
# 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_2_2

import rego.v1

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

result := output if {
mailboxes := get_array(input, "shared_mailboxes")
violating := [m |
some m in mailboxes
object.get(m, "SignInBlocked", null) != true
]

compliant := count(violating) == 0
msg := build_message(mailboxes, violating)
affected := [m.UserPrincipalName |
some m in violating
m.UserPrincipalName != null
]

output := {
"compliant": compliant,
"message": msg,
"affected_resources": affected,
"details": {
"total_shared_mailboxes": count(mailboxes),
"violating_count": count(violating),
"violations": [
{
"UserPrincipalName": m.UserPrincipalName,
"DisplayName": object.get(m, "DisplayName", null),
"SignInBlocked": object.get(m, "SignInBlocked", null),
} |
some m in violating
],
},
}
}

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

build_message(all_mailboxes, violating) := msg if {
count(violating) == 0
msg := sprintf("All %d shared mailbox(es) have sign-in blocked", [count(all_mailboxes)])
}

build_message(_, violating) := msg if {
count(violating) > 0
msg := sprintf("%d shared mailbox(es) do not have sign-in blocked", [count(violating)])
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@
"level": "L1",
"is_manual": false,
"benchmark_audit_type": "Automated",
"automation_status": "not_started",
"data_collector_id": null,
"policy_file": null,
"requires_permissions": null,
"notes": "Need to check user license assignments"
"automation_status": "ready",
"data_collector_id": "entra.roles.admin_license_footprint",
"policy_file": "1.1.4_admin_license_footprint.rego",
Comment thread
williamywccc marked this conversation as resolved.
Outdated
"requires_permissions": ["User.Read.All", "RoleManagement.Read.Directory"],
"notes": null
},
{
"control_id": "1.2.1",
Expand All @@ -91,11 +91,11 @@
"level": "L1",
"is_manual": false,
"benchmark_audit_type": "Automated",
"automation_status": "not_started",
"automation_status": "ready",
"data_collector_id": "exchange.mailbox.mailboxes",
"policy_file": null,
"policy_file": "1.2.2_shared_mailbox_signin_blocked.rego",
"requires_permissions": ["Exchange.Manage"],
"notes": "Collector exists but control logic not defined"
"notes": null
},
{
"control_id": "1.3.1",
Expand Down
39 changes: 39 additions & 0 deletions engine/samples/exchange_mailbox_mailboxes_sample.json
Comment thread
williamywccc marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"collector_id": "exchange.mailbox.mailboxes",
"description": "Sample output from MailboxesDataCollector against a mock M365 tenant",
"data": {
"shared_mailboxes": [
{
"UserPrincipalName": "finance@contoso.com",
"DisplayName": "Finance Team",
"PrimarySmtpAddress": "finance@contoso.com",
"SignInBlocked": true
},
{
"UserPrincipalName": "reception@contoso.com",
"DisplayName": "Reception",
"PrimarySmtpAddress": "reception@contoso.com",
"SignInBlocked": false
}
],
"total_shared_mailboxes": 2,
"mailboxes_with_signin_blocked": 1,
"mailboxes_with_signin_allowed": 1
},
"opa_result": {
"compliant": false,
"message": "1 shared mailbox(es) do not have sign-in blocked",
"affected_resources": ["reception@contoso.com"],
"details": {
"total_shared_mailboxes": 2,
"violating_count": 1,
"violations": [
{
"UserPrincipalName": "reception@contoso.com",
"DisplayName": "Reception",
"SignInBlocked": false
}
]
}
}
}
Loading