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 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 | Not Started | | Not Started | |
| 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 | Exchange Online PowerShell; `Get-EXOMailbox -RecipientTypeDetails SharedMailbox` then `Get-User -Identity {UPN}` for `BlockCredentials` |
| 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
63 changes: 29 additions & 34 deletions engine/collectors/exchange/mailbox/mailboxes.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
"""Mailboxes collector.

CIS Microsoft 365 Foundations Benchmark Controls:
v6.0.0: 1.2.2

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 Permissions: Exchange.ManageAsApp + Exchange role assignment
"""

from typing import Any
from typing import TYPE_CHECKING, Any

from collectors.powershell_base import BasePowerShellCollector
from collectors.powershell_client import PowerShellClient

if TYPE_CHECKING:
from collectors.powershell_client import PowerShellClient


class MailboxesDataCollector(BasePowerShellCollector):
"""Collects mailbox information for CIS compliance evaluation.

This collector retrieves shared mailboxes to verify
shared mailboxes have appropriate sign-in settings.
"""

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(

async def collect(self, client: "PowerShellClient") -> dict[str, Any]:
raw = await client.run_cmdlet(
"ExchangeOnline",
"Get-EXOMailbox",
RecipientTypeDetails="SharedMailbox",
ResultSize="Unlimited",
)

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

mailboxes: list[dict[str, Any]] = []
for m in raw:
upn = m.get("UserPrincipalName")
sign_in_blocked: bool | None = None
if upn:
user = await client.run_cmdlet("ExchangeOnline", "Get-User", Identity=upn)
if isinstance(user, dict):
block_credentials = user.get("BlockCredentials")
if block_credentials is not None:
sign_in_blocked = bool(block_credentials)

mailboxes.append({
"UserPrincipalName": upn,
"DisplayName": m.get("DisplayName"),
"PrimarySmtpAddress": m.get("PrimarySmtpAddress"),
"SignInBlocked": sign_in_blocked,
})

return {
"shared_mailboxes": mailboxes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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:
# - User.Read.All

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) := obj[key] if { obj[key] } else := []

build_message(all_mailboxes, violating) := sprintf("All %d shared mailbox(es) have sign-in blocked", [count(all_mailboxes)]) if {
count(violating) == 0
} else := 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 @@ -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,
"requires_permissions": ["Exchange.Manage"],
"notes": "Collector exists but control logic not defined"
"policy_file": "1.2.2_shared_mailbox_signin_blocked.rego",
"requires_permissions": ["Exchange.ManageAsApp"],
"notes": null
},
{
"control_id": "1.3.1",
Expand Down
43 changes: 43 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,43 @@
{
"collector_id": "exchange.mailbox.mailboxes",
"description": "Sample Graph-based output for CIS 1.2.2 evaluation",
"data": {
"shared_mailboxes": [
{
"UserPrincipalName": "finance@contoso.com",
"DisplayName": "Finance",
"PrimarySmtpAddress": "finance@contoso.com",
"accountEnabled": false,
"SignInBlocked": true
},
{
"UserPrincipalName": "reception@contoso.com",
"DisplayName": "Reception",
"PrimarySmtpAddress": "reception@contoso.com",
"accountEnabled": true,
"SignInBlocked": false
}
],
"total_shared_mailboxes": 2,
"mailboxes_with_signin_blocked": 1,
"mailboxes_with_signin_allowed": 1,
"detection_method": "graph_user_heuristic_unlicensed_mail_members",
"detection_limitations": "Shared mailbox identification is approximate without Exchange recipient metadata. Only member users with mail and no assignedLicenses are evaluated; licensed shared mailboxes may be omitted."
},
"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