diff --git a/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md index 2c6f0ce64..e68959cf7 100644 --- a/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md +++ b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md @@ -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 | --- @@ -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 | diff --git a/engine/collectors/exchange/mailbox/mailboxes.py b/engine/collectors/exchange/mailbox/mailboxes.py index 000740525..371e59536 100644 --- a/engine/collectors/exchange/mailbox/mailboxes.py +++ b/engine/collectors/exchange/mailbox/mailboxes.py @@ -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) -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, diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.2_shared_mailbox_signin_blocked.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.2_shared_mailbox_signin_blocked.rego new file mode 100644 index 000000000..48ce629dc --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.2.2_shared_mailbox_signin_blocked.rego @@ -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)]) diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json index e3d70a571..665788c0d 100644 --- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json @@ -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", diff --git a/engine/samples/exchange_mailbox_mailboxes_sample.json b/engine/samples/exchange_mailbox_mailboxes_sample.json new file mode 100644 index 000000000..e34f729a5 --- /dev/null +++ b/engine/samples/exchange_mailbox_mailboxes_sample.json @@ -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 + } + ] + } + } +}