<#PSScriptInfo .VERSION 1.0.0 .GUID 9f3c2a8b-7e1d-4f5a-9b2c-8d3e4f5a6b7c .AUTHOR IntuneManagement Toolkit .COMPANYNAME .COPYRIGHT .TAGS CIS,M365,Security,Baseline,EntraID,Defender,Exchange,SharePoint,Teams .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES Microsoft.Graph,ExchangeOnlineManagement,PnP.PowerShell,MicrosoftTeams .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES v1.0.0 - Initial rapid baseline for CIS M365 Foundations alignment on greenfield/newly-acquired tenants. #> <# .SYNOPSIS Rapidly deploys (or assesses) a high-impact CIS M365-aligned baseline to a new or newly-acquired tenant. .DESCRIPTION This script targets the ~40 highest-impact, easily-automated CIS M365 controls across: - Entra ID (password policies, auth methods, Conditional Access) - Microsoft Defender for Office 365 (Safe Links, Safe Attachments, Anti-Phish) - Exchange Online (external forwarding block, mailbox auditing) - SharePoint Online / OneDrive (external sharing restrictions) - Microsoft Teams (anonymous meeting restrictions, federation) It is designed for NEW or NEWLY-ACQUIRED tenants where disruption risk is low. On established tenants, run in -Mode Assess first and review every change. DEFAULT BEHAVIOUR IS READ-ONLY (-Mode Assess). You must specify -Mode Deploy -Apply to make changes. .PARAMETER Mode Assess = Read-only audit against the baseline (default) Deploy = Apply the baseline configuration .PARAMETER ConfigPath Path to the .psd1 configuration file. Defaults to .\CISM365-RapidBaseline.psd1 .PARAMETER Apply Required switch when Mode is 'Deploy'. Prevents accidental execution. .PARAMETER TenantId Optional tenant ID for Graph authentication. .PARAMETER SharePointAdminUrl Optional SharePoint admin URL (e.g., https://contoso-admin.sharepoint.com). If omitted, uses the value from the config file. .PARAMETER Workloads Array of workloads to process. Default is all. Options: EntraID, ConditionalAccess, Defender, Exchange, SharePoint, Teams .EXAMPLE # Assess your tenant without making any changes .\Deploy-CISM365RapidBaseline.ps1 .EXAMPLE # Deploy the baseline after review .\Deploy-CISM365RapidBaseline.ps1 -Mode Deploy -Apply -Verbose .EXAMPLE # Assess only Entra ID and Conditional Access .\Deploy-CISM365RapidBaseline.ps1 -Workloads @('EntraID','ConditionalAccess') #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [ValidateSet('Assess','Deploy')] [string]$Mode = 'Assess', [Parameter()] [string]$ConfigPath = "$PSScriptRoot\CISM365-RapidBaseline.psd1", [Parameter()] [switch]$Apply, [Parameter()] [string]$TenantId, [Parameter()] [string]$SharePointAdminUrl, [Parameter()] [ValidateSet('EntraID','ConditionalAccess','Defender','Exchange','SharePoint','Teams')] [string[]]$Workloads = @('EntraID','ConditionalAccess','Defender','Exchange','SharePoint','Teams') ) #region Initialization $ErrorActionPreference = 'Stop' $script:Results = [System.Collections.Generic.List[object]]::new() $script:ChangesMade = 0 $script:ChangesSkipped = 0 $script:Errors = 0 function Write-SectionHeader { param([string]$Title) Write-Host "`n========================================" -ForegroundColor Cyan Write-Host " $Title" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan } function Add-Result { param( [string]$Workload, [string]$Control, [string]$Status, # Pass, Fail, Fixed, Skipped, Error [string]$Message, [string]$Remediation = '' ) $script:Results.Add([PSCustomObject]@{ Workload = $Workload Control = $Control Status = $Status Message = $Message Remediation = $Remediation }) switch ($Status) { 'Fixed' { $script:ChangesMade++ } 'Skipped' { $script:ChangesSkipped++ } 'Error' { $script:Errors++ } } } # Load configuration if (-not (Test-Path $ConfigPath)) { throw "Configuration file not found: $ConfigPath" } $Config = Import-PowerShellDataFile -Path $ConfigPath $TenantDomain = $Config.Tenant.TenantDomain if (-not $SharePointAdminUrl) { $SharePointAdminUrl = $Config.Tenant.SharePointAdminUrl } $LicenseProfile = $Config.Tenant.LicenseProfile #endregion #region Authentication Write-SectionHeader "Authentication" # Microsoft Graph Write-Host "Connecting to Microsoft Graph..." -NoNewline $GraphScopes = @( 'Directory.Read.All','Directory.ReadWrite.All','Policy.Read.All','Policy.ReadWrite.ConditionalAccess', 'Organization.Read.All','Organization.ReadWrite.All','RoleManagement.ReadWrite.Directory', 'IdentityRiskyUser.Read.All','IdentityRiskEvent.Read.All' ) if ($TenantId) { Connect-MgGraph -Scopes ($GraphScopes -join ',') -TenantId $TenantId -NoWelcome } else { Connect-MgGraph -Scopes ($GraphScopes -join ',') -NoWelcome } Write-Host " OK" -ForegroundColor Green # Exchange Online (includes Defender) if ($Workloads -contains 'Defender' -or $Workloads -contains 'Exchange') { Write-Host "Connecting to Exchange Online..." -NoNewline Connect-ExchangeOnline -ShowBanner:$false Write-Host " OK" -ForegroundColor Green } # SharePoint if ($Workloads -contains 'SharePoint') { Write-Host "Connecting to SharePoint Online..." -NoNewline Connect-PnPOnline -Url $SharePointAdminUrl -Interactive Write-Host " OK" -ForegroundColor Green } # Teams if ($Workloads -contains 'Teams') { Write-Host "Connecting to Microsoft Teams..." -NoNewline Connect-MicrosoftTeams Write-Host " OK" -ForegroundColor Green } #endregion #region Helper Functions function Test-IsGlobalAdmin { $context = Get-MgContext $myRoles = Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$($context.Account)'" -ExpandProperty RoleDefinition return ($myRoles.RoleDefinition.DisplayName -contains 'Global Administrator') } function Invoke-WithErrorHandling { param( [string]$Workload, [string]$Control, [scriptblock]$Action, [string]$Remediation = '' ) try { & $Action } catch { Add-Result -Workload $Workload -Control $Control -Status 'Error' -Message $_.Exception.Message -Remediation $Remediation Write-Warning "[$Workload/$Control] ERROR: $_" } } #endregion #region Entra ID if ($Workloads -contains 'EntraID') { Write-SectionHeader "Entra ID / Identity" # 1.3.1 - Password expiration Invoke-WithErrorHandling -Workload 'EntraID' -Control '1.3.1-PasswordExpiration' -Action { $org = Get-MgOrganization $currentPolicy = $org.PasswordPolicies $desired = if ($Config.EntraID.PasswordExpiration -eq 'NeverExpire') { 'None' } else { 'PasswordExpiration' } if ($Mode -eq 'Assess') { $pass = ($desired -eq 'None' -and $currentPolicy -contains 'DisablePasswordExpiration') Add-Result -Workload 'EntraID' -Control '1.3.1-PasswordExpiration' ` -Status $(if ($pass) { 'Pass' } else { 'Fail' }) ` -Message "Current policy: $currentPolicy | Desired: $($Config.EntraID.PasswordExpiration)" ` -Remediation "Set-MgOrganization -PasswordPolicies 'DisablePasswordExpiration'" } else { if ($PSCmdlet.ShouldProcess($TenantDomain, "Set password expiration to $($Config.EntraID.PasswordExpiration)")) { Update-MgOrganization -OrganizationId $org.Id -PasswordPolicies 'DisablePasswordExpiration' Add-Result -Workload 'EntraID' -Control '1.3.1-PasswordExpiration' -Status 'Fixed' -Message "Set to NeverExpire" } else { Add-Result -Workload 'EntraID' -Control '1.3.1-PasswordExpiration' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } # 5.2.3.2 - Banned passwords Invoke-WithErrorHandling -Workload 'EntraID' -Control '5.2.3.2-BannedPasswords' -Action { $policy = Get-MgPolicyAuthenticationMethodPolicy | Select-Object -ExpandProperty AuthenticationMethodConfigurations | Where-Object { $_.Id -eq 'MicrosoftAuthenticator' } # Banned password list is actually in directory settings $settings = Get-MgDirectorySetting | Where-Object { $_.DisplayName -eq 'Password Rule Settings' } if (-not $settings) { $template = Get-MgDirectorySettingTemplate | Where-Object { $_.DisplayName -eq 'Password Rule Settings' } $settings = New-MgDirectorySetting -TemplateId $template.Id } $currentList = ($settings.Values | Where-Object { $_.Name -eq 'BannedPasswordList' }).Value $desiredList = $Config.EntraID.BannedPasswords -join ', ' if ($Mode -eq 'Assess') { $hasAll = ($Config.EntraID.BannedPasswords | ForEach-Object { $currentList -contains $_ }) -notcontains $false Add-Result -Workload 'EntraID' -Control '5.2.3.2-BannedPasswords' ` -Status $(if ($hasAll) { 'Pass' } else { 'Fail' }) ` -Message "Current: $currentList | Desired: $desiredList" ` -Remediation "Update-MgDirectorySetting -BannedPasswordList '$desiredList'" } else { if ($PSCmdlet.ShouldProcess($TenantDomain, "Update banned password list")) { $params = @{ BannedPasswordList = $desiredList; EnableBannedPasswordCheck = $true } Update-MgDirectorySetting -DirectorySettingId $settings.Id -Values $params Add-Result -Workload 'EntraID' -Control '5.2.3.2-BannedPasswords' -Status 'Fixed' -Message "Updated banned password list" } else { Add-Result -Workload 'EntraID' -Control '5.2.3.2-BannedPasswords' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } # 5.1.2.3 - Block tenant creation by non-admins Invoke-WithErrorHandling -Workload 'EntraID' -Control '5.1.2.3-BlockTenantCreation' -Action { $setting = Get-MgPolicyAuthorizationPolicy $current = $setting.DefaultUserRolePermissions.AllowedToCreateTenants $desired = -not $Config.EntraID.BlockTenantCreation if ($Mode -eq 'Assess') { Add-Result -Workload 'EntraID' -Control '5.1.2.3-BlockTenantCreation' ` -Status $(if ($current -eq $desired) { 'Pass' } else { 'Fail' }) ` -Message "AllowedToCreateTenants = $current | Desired = $desired" ` -Remediation "Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{AllowedToCreateTenants=`$false}" } else { if ($PSCmdlet.ShouldProcess($TenantDomain, "Set AllowedToCreateTenants = $desired")) { Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateTenants = $desired } Add-Result -Workload 'EntraID' -Control '5.1.2.3-BlockTenantCreation' -Status 'Fixed' -Message "Set to $desired" } else { Add-Result -Workload 'EntraID' -Control '5.1.2.3-BlockTenantCreation' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } # 5.1.2.6 - Disable LinkedIn Invoke-WithErrorHandling -Workload 'EntraID' -Control '5.1.2.6-DisableLinkedIn' -Action { $org = Get-MgOrganization $current = $org.MarketingNotificationEmails -contains 'LinkedIn' # LinkedIn setting is in directory settings $setting = Get-MgDirectorySetting | Where-Object { $_.DisplayName -eq 'Consent Policy Settings' } # Simplified check - actual LinkedIn config varies by tenant region Add-Result -Workload 'EntraID' -Control '5.1.2.6-DisableLinkedIn' -Status 'Skipped' ` -Message "LinkedIn integration check requires UI validation or tenant-specific Graph path." ` -Remediation "Navigate to Entra admin center > Users > User settings > LinkedIn account connections" } # 5.1.4.2 - Max devices per user Invoke-WithErrorHandling -Workload 'EntraID' -Control '5.1.4.2-MaxDevicesPerUser' -Action { $setting = Get-MgPolicyDeviceRegistrationPolicy $current = $setting.UserDeviceQuota $desired = $Config.EntraID.MaxDevicesPerUser if ($Mode -eq 'Assess') { Add-Result -Workload 'EntraID' -Control '5.1.4.2-MaxDevicesPerUser' ` -Status $(if ($current -le $desired) { 'Pass' } else { 'Fail' }) ` -Message "Current quota: $current | Desired max: $desired" ` -Remediation "Update-MgPolicyDeviceRegistrationPolicy -UserDeviceQuota $desired" } else { if ($PSCmdlet.ShouldProcess($TenantDomain, "Set max devices per user to $desired")) { Update-MgPolicyDeviceRegistrationPolicy -UserDeviceQuota $desired Add-Result -Workload 'EntraID' -Control '5.1.4.2-MaxDevicesPerUser' -Status 'Fixed' -Message "Set to $desired" } else { Add-Result -Workload 'EntraID' -Control '5.1.4.2-MaxDevicesPerUser' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } # 5.1.2.2 - Block user consent Invoke-WithErrorHandling -Workload 'EntraID' -Control '5.1.2.2-BlockUserConsent' -Action { $policy = Get-MgPolicyAuthorizationPolicy $current = $policy.DefaultUserRolePermissions.AllowedToCreateApps $desired = -not $Config.EntraID.BlockUserConsent if ($Mode -eq 'Assess') { Add-Result -Workload 'EntraID' -Control '5.1.2.2-BlockUserConsent' ` -Status $(if ($current -eq $desired) { 'Pass' } else { 'Fail' }) ` -Message "AllowedToCreateApps = $current | Desired = $desired" ` -Remediation "Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{AllowedToCreateApps=`$false}" } else { if ($PSCmdlet.ShouldProcess($TenantDomain, "Set AllowedToCreateApps = $desired")) { Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateApps = $desired } Add-Result -Workload 'EntraID' -Control '5.1.2.2-BlockUserConsent' -Status 'Fixed' -Message "Set to $desired" } else { Add-Result -Workload 'EntraID' -Control '5.1.2.2-BlockUserConsent' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } } #endregion #region Conditional Access if ($Workloads -contains 'ConditionalAccess') { Write-SectionHeader "Conditional Access" foreach ($caPolicy in $Config.ConditionalAccess) { $policyName = $caPolicy.Name Invoke-WithErrorHandling -Workload 'ConditionalAccess' -Control $policyName -Action { $existing = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$policyName'" -ErrorAction SilentlyContinue if ($Mode -eq 'Assess') { if ($existing) { $stateMatch = ($existing.State -eq $caPolicy.State) Add-Result -Workload 'ConditionalAccess' -Control $policyName ` -Status $(if ($stateMatch) { 'Pass' } else { 'Fail' }) ` -Message "Policy exists. State: $($existing.State) | Desired: $($caPolicy.State)" ` -Remediation "Review policy in Entra admin center > Protection > Conditional Access" } else { Add-Result -Workload 'ConditionalAccess' -Control $policyName -Status 'Fail' ` -Message "Policy does not exist." ` -Remediation "Create policy '$policyName' via Entra admin center or Graph API" } } else { if ($existing) { if ($PSCmdlet.ShouldProcess($policyName, "Update Conditional Access policy state to $($caPolicy.State)")) { Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $existing.Id -State $caPolicy.State Add-Result -Workload 'ConditionalAccess' -Control $policyName -Status 'Fixed' -Message "Updated state to $($caPolicy.State)" } else { Add-Result -Workload 'ConditionalAccess' -Control $policyName -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { # For Deploy mode without existing policy, we provide guidance rather than auto-creating # because CA policies are complex and tenant-specific (groups, apps, exclusions) Add-Result -Workload 'ConditionalAccess' -Control $policyName -Status 'Skipped' ` -Message "Policy does not exist. Auto-creation of CA policies is intentionally manual to avoid lockouts." ` -Remediation "Use the sample JSON in this script's comments or build via Entra admin center, then re-run Assess." } } } } } #endregion #region Defender / Exchange if ($Workloads -contains 'Defender') { Write-SectionHeader "Defender for Office 365" # Safe Links Invoke-WithErrorHandling -Workload 'Defender' -Control '2.1.1-SafeLinks' -Action { $policy = Get-SafeLinksPolicy -Identity $Config.Defender.SafeLinks.Name -ErrorAction SilentlyContinue if ($Mode -eq 'Assess') { if ($policy) { $pass = $policy.EnableSafeLinksForEmail -and $policy.TrackClicks -and -not $policy.AllowClickThrough Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status $(if ($pass) { 'Pass' } else { 'Fail' }) ` -Message "Safe Links policy exists. EmailProtection=$($policy.EnableSafeLinksForEmail) TrackClicks=$($policy.TrackClicks) AllowClickThrough=$($policy.AllowClickThrough)" ` -Remediation "Set-SafeLinksPolicy -Identity '$($Config.Defender.SafeLinks.Name)' -EnableSafeLinksForEmail `$true -TrackClicks `$true -AllowClickThrough `$false" } else { Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status 'Fail' ` -Message "Safe Links policy '$($Config.Defender.SafeLinks.Name)' not found." ` -Remediation "New-SafeLinksPolicy (see script comments for full syntax)" } } else { if ($policy) { if ($PSCmdlet.ShouldProcess($Config.Defender.SafeLinks.Name, 'Update Safe Links policy')) { Set-SafeLinksPolicy -Identity $Config.Defender.SafeLinks.Name ` -EnableSafeLinksForEmail $Config.Defender.SafeLinks.Enabled ` -TrackClicks $Config.Defender.SafeLinks.TrackClicks ` -AllowClickThrough $Config.Defender.SafeLinks.AllowClickThrough ` -ScanUrls $Config.Defender.SafeLinks.ScanUrls ` -EnableForInternalSenders $Config.Defender.SafeLinks.EnableForInternalSenders Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status 'Fixed' -Message "Updated Safe Links policy" } else { Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { if ($PSCmdlet.ShouldProcess($Config.Defender.SafeLinks.Name, 'Create Safe Links policy')) { New-SafeLinksPolicy -Name $Config.Defender.SafeLinks.Name ` -EnableSafeLinksForEmail $Config.Defender.SafeLinks.Enabled ` -TrackClicks $Config.Defender.SafeLinks.TrackClicks ` -AllowClickThrough $Config.Defender.SafeLinks.AllowClickThrough ` -ScanUrls $Config.Defender.SafeLinks.ScanUrls ` -EnableForInternalSenders $Config.Defender.SafeLinks.EnableForInternalSenders # Create rule to apply it New-SafeLinksRule -Name "$($Config.Defender.SafeLinks.Name)-Rule" -SafeLinksPolicy $Config.Defender.SafeLinks.Name -RecipientDomainIs (Get-AcceptedDomain).Name Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status 'Fixed' -Message "Created Safe Links policy and rule" } else { Add-Result -Workload 'Defender' -Control '2.1.1-SafeLinks' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } } # Safe Attachments Invoke-WithErrorHandling -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Action { $policy = Get-SafeAttachmentPolicy -Identity $Config.Defender.SafeAttachments.Name -ErrorAction SilentlyContinue if ($Mode -eq 'Assess') { if ($policy) { $pass = $policy.Enable -and ($policy.Action -eq 'Block') Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status $(if ($pass) { 'Pass' } else { 'Fail' }) ` -Message "Safe Attachments exists. Enabled=$($policy.Enable) Action=$($policy.Action)" ` -Remediation "Set-SafeAttachmentPolicy -Identity '$($Config.Defender.SafeAttachments.Name)' -Enable `$true -Action Block" } else { Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status 'Fail' ` -Message "Policy not found." -Remediation "New-SafeAttachmentPolicy -Name '$($Config.Defender.SafeAttachments.Name)' -Enable `$true -Action Block" } } else { if ($policy) { if ($PSCmdlet.ShouldProcess($Config.Defender.SafeAttachments.Name, 'Update Safe Attachments policy')) { Set-SafeAttachmentPolicy -Identity $Config.Defender.SafeAttachments.Name ` -Enable $Config.Defender.SafeAttachments.Enabled -Action $Config.Defender.SafeAttachments.Action Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status 'Fixed' -Message "Updated Safe Attachments policy" } else { Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { if ($PSCmdlet.ShouldProcess($Config.Defender.SafeAttachments.Name, 'Create Safe Attachments policy')) { New-SafeAttachmentPolicy -Name $Config.Defender.SafeAttachments.Name ` -Enable $Config.Defender.SafeAttachments.Enabled -Action $Config.Defender.SafeAttachments.Action New-SafeAttachmentRule -Name "$($Config.Defender.SafeAttachments.Name)-Rule" ` -SafeAttachmentPolicy $Config.Defender.SafeAttachments.Name -RecipientDomainIs (Get-AcceptedDomain).Name Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status 'Fixed' -Message "Created Safe Attachments policy and rule" } else { Add-Result -Workload 'Defender' -Control '2.1.4-SafeAttachments' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } } # Anti-Malware (Common Attachment Types Filter) Invoke-WithErrorHandling -Workload 'Defender' -Control '2.1.2-AntiMalware' -Action { $policy = Get-MalwareFilterPolicy -Identity $Config.Defender.AntiMalware.Name -ErrorAction SilentlyContinue if ($Mode -eq 'Assess') { if ($policy) { $pass = $policy.EnableInternalSenderNotifications Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status $(if ($pass) { 'Pass' } else { 'Fail' }) ` -Message "Anti-malware policy exists. InternalNotifications=$($policy.EnableInternalSenderNotifications)" ` -Remediation "Set-MalwareFilterPolicy -Identity '$($Config.Defender.AntiMalware.Name)' -EnableInternalSenderNotifications `$true" } else { Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status 'Fail' ` -Message "Policy not found." -Remediation "New-MalwareFilterPolicy -Name '$($Config.Defender.AntiMalware.Name)' -EnableInternalSenderNotifications `$true" } } else { if ($policy) { if ($PSCmdlet.ShouldProcess($Config.Defender.AntiMalware.Name, 'Update anti-malware policy')) { Set-MalwareFilterPolicy -Identity $Config.Defender.AntiMalware.Name -EnableInternalSenderNotifications $true Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status 'Fixed' -Message "Updated anti-malware policy" } else { Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { if ($PSCmdlet.ShouldProcess($Config.Defender.AntiMalware.Name, 'Create anti-malware policy')) { New-MalwareFilterPolicy -Name $Config.Defender.AntiMalware.Name -EnableInternalSenderNotifications $true Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status 'Fixed' -Message "Created anti-malware policy" } else { Add-Result -Workload 'Defender' -Control '2.1.2-AntiMalware' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } } } } if ($Workloads -contains 'Exchange') { Write-SectionHeader "Exchange Online" # 6.2.1 - Block external forwarding Invoke-WithErrorHandling -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Action { $rule = Get-TransportRule | Where-Object { $_.Name -like '*CIS*forward*' -or $_.Name -eq 'CIS-Block-External-Forwarding' } if ($Mode -eq 'Assess') { if ($rule) { Add-Result -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Status 'Pass' ` -Message "Transport rule exists: $($rule.Name)" } else { Add-Result -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Status 'Fail' ` -Message "No transport rule blocking external forwarding." ` -Remediation "New-TransportRule -Name 'CIS-Block-External-Forwarding' -FromScope 'InOrganization' -SentToScope 'NotInOrganization' -RejectMessageReasonText 'External forwarding is disabled'" } } else { if (-not $rule) { if ($PSCmdlet.ShouldProcess('Transport Rule', 'Create external forwarding block')) { New-TransportRule -Name 'CIS-Block-External-Forwarding' ` -FromScope 'InOrganization' -SentToScope 'NotInOrganization' ` -RejectMessageReasonText 'External forwarding is disabled per security policy.' ` -RejectMessageEnhancedStatusCode '5.7.1' Add-Result -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Status 'Fixed' -Message "Created transport rule" } else { Add-Result -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'Exchange' -Control '6.2.1-BlockExternalForwarding' -Status 'Pass' -Message "Rule already exists" } } } # 6.1.2 - Enable mailbox auditing Invoke-WithErrorHandling -Workload 'Exchange' -Control '6.1.2-MailboxAudit' -Action { $orgConfig = Get-OrganizationConfig if ($Mode -eq 'Assess') { $pass = $orgConfig.AuditDisabled -eq $false Add-Result -Workload 'Exchange' -Control '6.1.2-MailboxAudit' -Status $(if ($pass) { 'Pass' } else { 'Fail' }) ` -Message "AuditDisabled = $($orgConfig.AuditDisabled)" ` -Remediation "Set-OrganizationConfig -AuditDisabled `$false" } else { if ($orgConfig.AuditDisabled -ne $false) { if ($PSCmdlet.ShouldProcess('Organization Config', 'Enable mailbox auditing')) { Set-OrganizationConfig -AuditDisabled $false Add-Result -Workload 'Exchange' -Control '6.1.2-MailboxAudit' -Status 'Fixed' -Message "Enabled mailbox auditing" } else { Add-Result -Workload 'Exchange' -Control '6.1.2-MailboxAudit' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'Exchange' -Control '6.1.2-MailboxAudit' -Status 'Pass' -Message "Already enabled" } } } } #endregion #region SharePoint if ($Workloads -contains 'SharePoint') { Write-SectionHeader "SharePoint / OneDrive" Invoke-WithErrorHandling -Workload 'SharePoint' -Control '7.x-ExternalSharing' -Action { $tenant = Get-PnPTenant # SharePoint external sharing $spoSharing = $tenant.SharingCapability $desiredSpo = $Config.SharePoint.SharePointExternalSharing if ($Mode -eq 'Assess') { Add-Result -Workload 'SharePoint' -Control '7.x-SharePointExternalSharing' ` -Status $(if ($spoSharing -eq $desiredSpo) { 'Pass' } else { 'Fail' }) ` -Message "Current: $spoSharing | Desired: $desiredSpo" ` -Remediation "Set-PnPTenant -SharingCapability $desiredSpo" } else { if ($spoSharing -ne $desiredSpo) { if ($PSCmdlet.ShouldProcess('SharePoint Tenant', "Set sharing to $desiredSpo")) { Set-PnPTenant -SharingCapability $desiredSpo Add-Result -Workload 'SharePoint' -Control '7.x-SharePointExternalSharing' -Status 'Fixed' -Message "Set to $desiredSpo" } else { Add-Result -Workload 'SharePoint' -Control '7.x-SharePointExternalSharing' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'SharePoint' -Control '7.x-SharePointExternalSharing' -Status 'Pass' -Message "Already set to $desiredSpo" } } # OneDrive external sharing $odbSharing = $tenant.OneDriveSharingCapability $desiredOdb = $Config.SharePoint.OneDriveExternalSharing if ($Mode -eq 'Assess') { Add-Result -Workload 'SharePoint' -Control '7.x-OneDriveExternalSharing' ` -Status $(if ($odbSharing -eq $desiredOdb) { 'Pass' } else { 'Fail' }) ` -Message "Current: $odbSharing | Desired: $desiredOdb" ` -Remediation "Set-PnPTenant -OneDriveSharingCapability $desiredOdb" } else { if ($odbSharing -ne $desiredOdb) { if ($PSCmdlet.ShouldProcess('OneDrive Tenant', "Set sharing to $desiredOdb")) { Set-PnPTenant -OneDriveSharingCapability $desiredOdb Add-Result -Workload 'SharePoint' -Control '7.x-OneDriveExternalSharing' -Status 'Fixed' -Message "Set to $desiredOdb" } else { Add-Result -Workload 'SharePoint' -Control '7.x-OneDriveExternalSharing' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'SharePoint' -Control '7.x-OneDriveExternalSharing' -Status 'Pass' -Message "Already set to $desiredOdb" } } } } #endregion #region Teams if ($Workloads -contains 'Teams') { Write-SectionHeader "Microsoft Teams" Invoke-WithErrorHandling -Workload 'Teams' -Control '8.x-AnonymousMeetings' -Action { $config = Get-CsTeamsMeetingConfiguration $anonJoin = (Get-CsTeamsMeetingPolicy -Identity Global).AllowAnonymousUsersToJoinMeeting $anonStart = (Get-CsTeamsMeetingPolicy -Identity Global).AllowAnonymousUsersToStartMeeting if ($Mode -eq 'Assess') { Add-Result -Workload 'Teams' -Control '8.x-AnonymousMeetingJoin' ` -Status $(if ($anonJoin -eq $Config.Teams.AllowAnonymousMeetingJoin) { 'Pass' } else { 'Fail' }) ` -Message "AllowAnonymousUsersToJoinMeeting = $anonJoin | Desired = $($Config.Teams.AllowAnonymousMeetingJoin)" ` -Remediation "Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting `$false" } else { if ($anonJoin -ne $Config.Teams.AllowAnonymousMeetingJoin) { if ($PSCmdlet.ShouldProcess('Teams Global Policy', 'Restrict anonymous meeting join')) { Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $Config.Teams.AllowAnonymousMeetingJoin Add-Result -Workload 'Teams' -Control '8.x-AnonymousMeetingJoin' -Status 'Fixed' -Message "Set to $($Config.Teams.AllowAnonymousMeetingJoin)" } else { Add-Result -Workload 'Teams' -Control '8.x-AnonymousMeetingJoin' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'Teams' -Control '8.x-AnonymousMeetingJoin' -Status 'Pass' -Message "Already set correctly" } } } Invoke-WithErrorHandling -Workload 'Teams' -Control '8.x-Federation' -Action { $fedConfig = Get-CsTenantFederationConfiguration if ($Mode -eq 'Assess') { Add-Result -Workload 'Teams' -Control '8.x-Federation' ` -Status $(if ($fedConfig.AllowFederatedUsers -eq $Config.Teams.AllowFederatedUsers) { 'Pass' } else { 'Fail' }) ` -Message "AllowFederatedUsers = $($fedConfig.AllowFederatedUsers) | Desired = $($Config.Teams.AllowFederatedUsers)" ` -Remediation "Set-CsTenantFederationConfiguration -AllowFederatedUsers `$false" } else { if ($fedConfig.AllowFederatedUsers -ne $Config.Teams.AllowFederatedUsers) { if ($PSCmdlet.ShouldProcess('Teams Federation', "Set AllowFederatedUsers to $($Config.Teams.AllowFederatedUsers)")) { Set-CsTenantFederationConfiguration -AllowFederatedUsers $Config.Teams.AllowFederatedUsers Add-Result -Workload 'Teams' -Control '8.x-Federation' -Status 'Fixed' -Message "Set to $($Config.Teams.AllowFederatedUsers)" } else { Add-Result -Workload 'Teams' -Control '8.x-Federation' -Status 'Skipped' -Message "WhatIf/Confirm declined" } } else { Add-Result -Workload 'Teams' -Control '8.x-Federation' -Status 'Pass' -Message "Already set correctly" } } } } #endregion #region Report Write-SectionHeader "Summary Report" $passCount = ($script:Results | Where-Object { $_.Status -eq 'Pass' }).Count $failCount = ($script:Results | Where-Object { $_.Status -eq 'Fail' }).Count $fixedCount = $script:ChangesMade $skippedCount = $script:ChangesSkipped $errorCount = $script:Errors Write-Host "Mode: $Mode" -ForegroundColor $(if ($Mode -eq 'Assess') { 'Green' } else { 'Yellow' }) Write-Host "Workloads: $($Workloads -join ', ')" Write-Host "" Write-Host "Results:" Write-Host " Pass: $passCount" -ForegroundColor Green Write-Host " Fail: $failCount" -ForegroundColor Red if ($Mode -eq 'Deploy') { Write-Host " Fixed: $fixedCount" -ForegroundColor Cyan Write-Host " Skipped: $skippedCount" -ForegroundColor Yellow } Write-Host " Errors: $errorCount" -ForegroundColor $(if ($errorCount -gt 0) { 'Red' } else { 'Gray' }) Write-Host "" # Export results $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $reportPath = "$PSScriptRoot\CISM365-RapidBaseline-Report_$Mode`_$timestamp.csv" $script:Results | Export-Csv -Path $reportPath -NoTypeInformation -Force Write-Host "Report saved to: $reportPath" -ForegroundColor Green # Show failures if in Assess mode if ($Mode -eq 'Assess' -and $failCount -gt 0) { Write-Host "`nFailed checks:" -ForegroundColor Red $script:Results | Where-Object { $_.Status -eq 'Fail' } | ForEach-Object { Write-Host " [$($_.Workload)] $($_.Control): $($_.Message)" -ForegroundColor Red if ($_.Remediation) { Write-Host " Remediation: $($_.Remediation)" -ForegroundColor DarkGray } } } # Show errors if ($errorCount -gt 0) { Write-Host "`nErrors encountered:" -ForegroundColor Red $script:Results | Where-Object { $_.Status -eq 'Error' } | ForEach-Object { Write-Host " [$($_.Workload)] $($_.Control): $($_.Message)" -ForegroundColor Red } } Write-Host "`nDone." -ForegroundColor Green #endregion