ORCA.psm1
#Requires -Version 5.1 <# .SYNOPSIS The Office 365 Recommended Configuration Analyzer (ORCA) .DESCRIPTION .NOTES Cam Murray Field Engineer - Microsoft camurray@microsoft.com Output report uses open source components for HTML formatting - bootstrap - MIT License - https://getbootstrap.com/docs/4.0/about/license/ - fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free ############################################################################ This sample script is not supported under any Microsoft standard support program or service. This sample script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample script and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample script or documentation, even if Microsoft has been advised of the possibility of such damages. ############################################################################ .LINK about_functions_advanced #> function Get-ORCADirectory { <# Gets or creates the ORCA directory in AppData #> $Directory = "$($env:LOCALAPPDATA)\Microsoft\ORCA" If(Test-Path $Directory) { Return $Directory } else { mkdir $Directory | out-null Return $Directory } } <# Check modules #> function Invoke-CheckAllowedSenderDomains { Param( $ContentFilterPolicies ) $Check = "Content Filter AllowedSenderDomains" $CID = "118-1" $return = @() ForEach($Policy in $ContentFilterPolicies) { # Fail if AllowedSenderDomains is not null If(($Policy.AllowedSenderDomains).Count -gt 0) { ForEach($Domain in $Policy.AllowedSenderDomains) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="$($Domain.Domain)" Rule="AllowedSenderDomains is not empty" Control=$CID } } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="0 Allowed Sender Domains" Rule="AllowedSenderDomains is empty" Control=$CID } } } return $return } function Invoke-CheckZAP { Param( $MalwareFilterPolicies, $ContentFilterPolicies ) $Check = "ZAP" $return = @() ForEach($Policy in $MalwareFilterPolicies) { if($Policy.ZapEnabled -eq $true) { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) Rule="ZAP Malware Enabled" Control="120-malware" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) Control="120-malware" SupplementText="Zero Hour Autopurge for Malware is disabled in the Malware Policy $($Policy.Name)" } } } ForEach($Policy in $ContentFilterPolicies) { if($Policy.ZapEnabled -eq $true) { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) Rule="ZAP Phishing/Spam Enabled" Control="120-spam" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) Rule="ZAP Phishing/Spam Disabled" Control="120-spam" } } # Check requirement of Spam ZAP - MoveToJmf, redirect, delete, quarantine If($Policy.SpamAction -eq "MoveToJmf" -or $Policy.SpamAction -eq "Redirect" -or $Policy.SpamAction -eq "Delete" -or $Policy.SpamAction -eq "Quarantine") { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.SpamAction) Rule="SpamAction set to an action necessary to move to JMF- ZAP Requirement" Control="121" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.SpamAction) SupplementText="Spam Action on policy $($Policy.Name) is set to $($Policy.SpamAction)" Control="121" } } # Check requirement of Phish ZAP - MoveToJmf, redirect, delete, quarantine If($Policy.PhishSpamAction -eq "MoveToJmf" -or $Policy.PhishSpamAction -eq "Redirect" -or $Policy.PhishSpamAction -eq "Delete" -or $Policy.PhishSpamAction -eq "Quarantine") { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishSpamAction) Rule="PhishSpamAction set to an action necessary to move to JMF - ZAP Requirement" Control="121" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishSpamAction) Rule="PhishSpamAction not set to an action necessary to move to JMF- ZAP Requirement" Control="121" } } } return $return } function Invoke-CheckIPAllowList { Param( $HostedConnectionFilterPolicies ) $Check = "IP Allow List Size" $Return = @() ForEach($HostedConnectionFilterPolicy in $HostedConnectionFilterPolicies) { # Check if IPAllowList < 0 and return inconclusive for manual checking of size If($HostedConnectionFilterPolicy.IPAllowList.Count -gt 0) { # IP Allow list present ForEach($IPAddr in @($HostedConnectionFilterPolicy.IPAllowList)) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($HostedConnectionFilterPolicy.Name) ConfigData=$IPAddr Rule="IP Allow List contains too many IPs" Control="114" } } } else { # IPAllowList is blank, so pass. $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($HostedConnectionFilterPolicy.Name) ConfigData="IP Entries $($HostedConnectionFilterPolicy.IPAllowList.Count)" Rule="IP Allow List empty" Control="114" } } } return $Return } function Invoke-CheckATPForSPOTeamsODB { <# 158 Checks to determine if ATP is enabled for SharePoint, Teams, and OD4B as per 'tickbox' in the ATP configuration. #> Param( $ATPPolicy, $Questions ) $Check = "ATP" $Cid = "158" $Return = @() ForEach($Policy in $ATPPolicy) { # Determine if ATP is enabled or not If($Policy.EnableATPForSPOTeamsODB -eq $true) { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy["EnableATPForSPOTeamsODB"]) Rule="ATP enabled for SPO - OD4B - Teams" Control=$Cid } } else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy["EnableATPForSPOTeamsODB"]) Rule="ATP enabled for SPO - OD4B - Teams" Control=$Cid } } } Return $Return } function Invoke-CheckContentFilterActions { Param( $ContentFilterPolicies ) $Check = "Content Filter Actions" $return = @() ForEach($Policy in $ContentFilterPolicies) { # Fail if HighConfidenceSpamAction is not set to Quarantine or MoveToJmf If($Policy.HighConfidenceSpamAction -ne "MoveToJmf" -and $Policy.HighConfidenceSpamAction -ne "Quarantine" -and $Policy.HighConfidenceSpamAction -ne "Delete") { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.HighConfidenceSpamAction) Rule="HighConfidenceSpamAction set to $($Policy.HighConfidenceSpamAction)" Control="140" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.HighConfidenceSpamAction) Rule="HighConfidenceSpamAction set to $($Policy.HighConfidenceSpamAction)" Control="140" } } # Fail if SpamAction is not set to Quarantine or MoveToJmf If($Policy.SpamAction -ne "MoveToJmf" -and $Policy.SpamAction -ne "Quarantine" -and $Policy.SpamAction -ne "Delete") { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.SpamAction) Rule="SpamAction set to $($Policy.SpamAction)" Control="139" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.SpamAction) Rule="SpamAction set to $($Policy.SpamAction)" Control="139" } } # Fail if BulkSpamAction is not set to Quarantine or MoveToJmf If($Policy.BulkSpamAction -ne "MoveToJmf" -and $Policy.BulkSpamAction -ne "Quarantine" -and $Policy.BulkSpamAction -ne "Delete") { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.BulkSpamAction) Rule="BulkSpamAction set to $($Policy.BulkSpamAction)" Control="141" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.BulkSpamAction) Rule="BulkSpamAction set to $($Policy.BulkSpamAction)" Control="141" } } # Fail if PhishSpamAction is not set to Quarantine or MoveToJmf If($Policy.PhishSpamAction -ne "Quarantine") { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishSpamAction) Rule="PhishSpamAction set to $($Policy.PhishSpamAction)" Control="142" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishSpamAction) Rule="PhishSpamAction set to $($Policy.PhishSpamAction)" Control="142" } } } return $return } function Invoke-CheckATPSafeLinksTrackingInternal { <# 179 Checks to determine if SafeLinks is re-wring internal to internal emails. Does not however, check to determine if there is a rule enforcing this. #> Param( $ATPPolicy ) $Check = "ATP" $Cid = "179" $Return = @() # Determine if ATP license ForEach($Policy in $ATPPolicy) { # Determine if ATP link tracking is on for this safelinks policy If($Policy.EnableForInternalSenders -eq $true) { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$Policy.EnableForInternalSenders Rule="SafeLinks Enabled for Internal Senders" Control=$Cid } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$Policy.EnableForInternalSenders Rule="SafeLinks Disabled for Internal Senders" Control=$Cid } } } Return $Return } function Invoke-CheckATPSafeLinksTrackingOfficeApps { <# 169 Determines if ATP SafeLinks protection extends to Office Apps in each policy, Does not however determine if SafeLinks policy extends to all users. #> Param( $ATPPolicy ) $Check = "ATP" $Cid = "169" $Return = @() If($ATPPolicy.EnableSafeLinksForClients -eq $true) { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem="365 ATP Policy EnableSafeLinksForClients" ConfigData=$ATPPolicy.EnableSafeLinksForClients Rule="SafeLinks URL Tracking Enabled for Office Clients" Control=$Cid } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem="365 ATP Policy EnableSafeLinksForClients" ConfigData=$ATPPolicy.EnableSafeLinksForClients Rule="SafeLinks URL Tracking Enabled for Office Clients" Control=$Cid } } Return $Return } function Invoke-CheckATPSafeAttachmentsBypass { <# 189 Checks to determine if SafeAttachments is being bypassed by injecting X-MS-Exchange-Organization-SkipSafeAttachmentProcessing header in to emails using a mail flow rule. #> Param( $TransportRules ) $Check = "ATP" $Cid = "189" $Return = @() $BypassRules = @($TransportRules | Where-Object {$_.SetHeaderName -eq "X-MS-Exchange-Organization-SkipSafeAttachmentProcessing"}) If($BypassRules.Count -gt 0) { # Rules exist to bypass ForEach($Rule in $BypassRules) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem="Transport Rule - $($Rule.Name)" ConfigData="Setting X-MS-Exchange-Organization-SkipSafeAttachmentProcessing to $($Rule.SetHeaderValue)" Rule="SafeAttachments not bypassed" Control=$Cid } } } Else { # Rules do not exist to bypass $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem="Transport Rules" Rule="SafeAttachments not bypassed" Control=$Cid } } Return $Return } function Invoke-CheckATPSafeLinksTracking { <# SOA-158 Determines if SafeLinks URL tracing is enabled on a Policy, does not however check that there is a rule enforcing this policy. #> Param( $ATPPolicy, $Questions ) $Check = "ATP" $Cid = "SOA-156" $Return = @() # Determine if ATP license ForEach($Policy in $ATPPolicy) { # Determine if ATP link tracking is on for this safelinks policy If($Policy.DoNotTrackUserClicks -eq $false) { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$Policy.DoNotTrackUserClicks Rule="SafeLinks URL Tracking Enabled" Control=$Cid } } else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$Policy.DoNotTrackUserClicks Rule="SafeLinks URL Tracking Enabled" Control=$Cid } } } Return $Return } function Invoke-CheckContentFilterSafetyTips { Param( $ContentFilterPolicies ) $Check = "Content Filter Safety Tips" $CID = "143" $return = @() ForEach($Policy in $ContentFilterPolicies) { # Fail if InlineSafetyTipsEnabled is not set to true If($Policy.InlineSafetyTipsEnabled -eq $false) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.InlineSafetyTipsEnabled) Rule="InlineSafetyTipsEnabled is false - Safety Tips Disabled" Control=$CID } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.InlineSafetyTipsEnabled) Rule="InlineSafetyTipsEnabled is true - Safety Tips Enabled" Control=$CID } } } return $return } function Invoke-CheckMalwareNotifications { Param( $MalwareFilterPolicies ) $Check = "Malware Notifications" $return = @() ForEach($Policy in $MalwareFilterPolicies) { # Fail if EnableExternalSenderNotifications is set to true in the policy If($Policy.EnableExternalSenderNotifications -eq $true) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) Rule="EnableExternalSenderNotifications set to True" Control="125" } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) Rule="EnableExternalSenderNotifications set to False" Control="125" } } } return $return } function Invoke-CheckCommonAttachmentTypeFilter { Param( $MalwareFilterPolicies ) $Check = "Common Attachment Type Filter" $Control = "205" $return = @() ForEach($Policy in $MalwareFilterPolicies) { # Fail if NotifyOutboundSpam is not set to true in the policy If($Policy.EnableFileFilter -eq $false -or @($Policy.FileTypes).Count -eq 0) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="" Rule="EnableFilterFilter eq false or FileType count is zero" Control=$Control } } else { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableFileFilter $($Policy.EnableFileFilter) FileTypes Count $(($Policy.FileTypes).Count)" Rule="EnableFileFilter eq true and FileType count is greater than zero" Control=$Control } } } return $return } function Invoke-CheckUnifiedLogging { Param( $AdminAuditLogConfig ) $Check = "Unified Logging" $ConfigItem = "Admin Audit Log Settings" $CID="122" If($AdminAuditLogConfig.UnifiedAuditLogIngestionEnabled -eq $true) { # Unified audit logging turned on $return = New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$ConfigItem Rule="UnifiedAuditLogIngestionEnabled is true" Control=$CID } } else { # Unified audit logging turned off $return = New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$ConfigItem Rule="UnifiedAuditLogIngestionEnabled is false" Control=$CID } } return $return } Function Invoke-CheckTransportRuleSCL { Param( $TransportRules ) $Check = "Transport Rule SCL" $CID = "118-2" $return = @() # Look through Transport Rule for an action SetSCL -1 ForEach($TransportRule in $TransportRules) { If($TransportRule.SetSCL -eq "-1") { #Rules that apply to the sender domain #From Address notmatch is to include if just domain name is value If($TransportRule.SenderDomainIs -ne $null -or ($TransportRule.FromAddressContainsWords -ne $null -and $TransportRule.FromAddressContainsWords -notmatch ".+@") -or ($TransportRule.FromAddressMatchesPatterns -ne $null -and $TransportRule.FromAddressMatchesPatterns -notmatch ".+@")){ #Look for condition that checks auth results header and its value If(($TransportRule.HeaderContainsMessageHeader -eq 'Authentication-Results' -and $TransportRule.HeaderContainsWords -ne $null) -or ($TransportRule.HeaderMatchesMessageHeader -like '*Authentication-Results*' -and $TransportRule.HeaderMatchesPatterns -ne $null)) { # OK } #Look for exception that checks auth results header and its value elseif(($TransportRule.ExceptIfHeaderContainsMessageHeader -eq 'Authentication-Results' -and $TransportRule.ExceptIfHeaderContainsWords -ne $null) -or ($TransportRule.ExceptIfHeaderMatchesMessageHeader -like '*Authentication-Results*' -and $TransportRule.ExceptIfHeaderMatchesPatterns -ne $null)) { # OK } elseif($TransportRule.SenderIpRanges -ne $null) { # OK } #Look for condition that checks for any other header and its value else { ForEach($RuleDomain in $($TransportRule.SenderDomainIs)) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($TransportRule.Name) ConfigData=$($RuleDomain) Rule="SetSCL -1 action for sender domain but no check for auth results header, sender IP, or other header" Control=$CID } } ForEach($FromAddressContains in $($TransportRule.FromAddressContainsWords)) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($TransportRule.Name) ConfigData="Contains $($FromAddressContains)" Rule="SetSCL -1 action for sender domain but no check for auth results header, sender IP, or other header" Control=$CID } } ForEach($FromAddressMatch in $($TransportRule.FromAddressMatchesPatterns)) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($TransportRule.Name) ConfigData="Matches $($FromAddressMatch)" Rule="SetSCL -1 action for sender domain but no check for auth results header, sender IP, or other header" Control=$CID } } } } #No sender domain restriction, so check for IP restriction elseif($null -ne $TransportRule.SenderIpRanges) { ForEach($SenderIpRange in $TransportRule.SenderIpRanges) { $return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($TransportRule.Name) ConfigData=$SenderIpRange Rule="SetSCL -1 action with IP condition but not limiting sender domain" Control="SOA-114" } } } #No sender restriction, so check for condition that checks auth results header and its value elseif(($TransportRule.HeaderContainsMessageHeader -eq 'Authentication-Results' -and $TransportRule.HeaderContainsWords -ne $null) -or ($TransportRule.HeaderMatchesMessageHeader -like '*Authentication-Results*' -and $TransportRule.HeaderMatchesPatterns -ne $null)) { # OK } #No sender restriction, so check for exception that checks auth results header and its value elseif(($TransportRule.ExceptIfHeaderContainsMessageHeader -eq 'Authentication-Results' -and $TransportRule.ExceptIfHeaderContainsWords -ne $null) -or ($TransportRule.ExceptIfHeaderMatchesMessageHeader -like '*Authentication-Results*' -and $TransportRule.ExceptIfHeaderMatchesPatterns -ne $null)) { # OK } } } # If no rules found with SetSCL -1, then pass. if(!$return) { $return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem="Transport Rules" Rule="No SetSCL -1 actions found" Control=$CID } } return $return } Function Invoke-ORCAConnections { If(!(Get-Command "Connect-EXOPSSession" -ErrorAction:SilentlyContinue)) { Throw "Please load the Exchange Online PowerShell module before running ORCA." } Else { <# The following code is commented out until SCC is required Write-Host "$(Get-Date) Connecting to Security and Compliance Center.." Connect-IPPSSession -PSSessionOption $ProxySetting -WarningAction:SilentlyContinue | Out-Null $sessionSCC = (Get-PSSession | Where-Object {$_.ComputerName -like "*protection.outlook.com"}) #> Write-Host "$(Get-Date) Connecting to Exchange Online.." Connect-EXOPSSession -PSSessionOption $ProxySetting -WarningAction:SilentlyContinue | Out-Null } } Function Get-ORCACheckDefs { <# Check definition The checks defined below allow contextual information to be added in to the report HTML document. - Control : A unique identifier that can be used to index the results back to the check - Area : The area that this check should appear within the report - PassText : The text that should appear in the report when this 'control' passes - FailRecommendation : The text that appears as a title when the 'control' fails. Short, descriptive. E.g "Do this" - Importance : Why this is important - ExpandResults : If we should create a table in the callout which points out which items fail and where - ItemName : When ExpandResults is set to, what does the check return as ConfigItem, for instance, is it a Transport Rule? - DataType : When ExpandResults is set to, what type of data is returned in ConfigData, for instance, is it a Domain? #> $Checks = @() $Checks += New-Object -TypeName PSObject -Property @{ Control=114 Area="Content Filter Policies" Name="IP Allow Lists" PassText="No IP Allow Lists have been configured" FailRecommendation="Remove IP addresses from IP allow list" Importance="IP addresses containted in the IP allow list are able to bypass spam, phishing and spoofing checks, potentially resulting in more spam. Ensure that the IP list is kept to a minimum." ExpandResults=$True ItemName="Content Filter Policy" DataType="Allowed IP" Links= @{ "Configure the connection filter policy"="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-connection-filter-policy" } } $Checks += New-Object -TypeName PSObject -Property @{ Control="118-1" Area="Content Filter Policies" Name="Domain Whitelisting" PassText="Domains are not being whitelisted in an unsafe manner" FailRecommendation="Remove whitelisting on domains" Importance="Emails coming from whitelisted domains bypass several layers of protection within Exchange Online Protection. If domains are whitelisted, they are open to being spoofed from malicious actors." ExpandResults=$True ItemName="Content Filter Policy" DataType="Whitelisted Domain" } $Checks += New-Object -TypeName PSObject -Property @{ Control="118-2" Area="Transport Rules" Name="Domain Whitelisting" PassText="Domains are not being whitelisted in an unsafe manner" FailRecommendation="Remove whitelisting on domains" Importance="Emails coming from whitelisted domains bypass several layers of protection within Exchange Online Protection. If domains are whitelisted, they are open to being spoofed from malicious actors." ExpandResults=$True ItemName="Transport Rule" DataType="Whitelisted Domain" } $Checks += New-Object -TypeName PSObject -Property @{ Control="120-spam" Area="Zero Hour Autopurge" Name="Zero Hour Autopurge Enabled for Spam" PassText="Zero Hour Autopurge is Enabled" FailRecommendation="Enable Zero Hour Autopurge" Importance="Zero Hour Autopurge can assist removing false-negatives post detection from mailboxes. By default, it is enabled." ExpandResults=$True ItemName="Policy" DataType="Setting" } $Checks += New-Object -TypeName PSObject -Property @{ Control="120-malware" Area="Zero Hour Autopurge" Name="Zero Hour Autopurge Enabled for Malware" PassText="Zero Hour Autopurge is Enabled" FailRecommendation="Enable Zero Hour Autopurge" Importance="Zero Hour Autopurge can assist removing false-negatives post detection from mailboxes. By default, it is enabled." ExpandResults=$True ItemName="Policy" DataType="Setting" } $Checks += New-Object -TypeName PSObject -Property @{ Control=121 Area="Zero Hour Autopurge" Name="Supported filter policy action" PassText="Supported filter policy action used" FailRecommendation="Change filter policy action to support Zero Hour Auto Purge" Importance="Zero Hour Autopurge can assist removing false-negatives post detection from mailboxes. It requires a supported action in the spam filter policy." ExpandResults=$True ItemName="Policy" DataType="Setting" } $Checks += New-Object -TypeName PSObject -Property @{ Control=122 Area="Tenant Settings" Name="Unified Audit Log" PassText="Unified Audit Log is enabled" FailRecommendation="Enable the Unified Audit Log" Importance="The Unified Audit Log collects logs from most Office 365 services and provides one central place to correlate and pull logs from Office 365." } $Checks += New-Object -TypeName PSObject -Property @{ Control=125 Area="Malware Filter Policy" Name="External Sender Notificatations" PassText="External Sender notifications are disabled" FailRecommendation="Disable notifying external senders of malware detection" Importance="Placeholder" } $Checks += New-Object -TypeName PSObject -Property @{ Control=139 Area="Content Filter Policies" Name="Spam Action" PassText="Spam action set to JMF or Quarantine" FailRecommendation="Change Spam action to JMF or Quarantine" Importance="Placeholder." ExpandResults=$True ItemName="Spam Policy" DataType="Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=140 Area="Content Filter Policies" Name="High Confidence Spam Action" PassText="High Confidence Spam action set to JMF or Quarantine" FailRecommendation="Change High Confidence Spam action to JMF or Quarantine" Importance="Placeholder." ExpandResults=$True ItemName="Spam Policy" DataType="Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=141 Area="Content Filter Policies" Name="Bulk Action" PassText="Bulk action set to JMF or Quarantine" FailRecommendation="Change Bulk action to JMF or Quarantine" Importance="Placeholder." ExpandResults=$True ItemName="Spam Policy" DataType="Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=142 Area="Content Filter Policies" Name="Phish Action" PassText="Phish action set to Quarantine" FailRecommendation="Change Phish action to Quarantine" Importance="It is recommended that Phish be sent to Quarantine so that these emails are not visible to the end user from within Outlook. As Phishing emails are designed to look legitimate, users may mistakingly think that a phishing email in Junk is false-positive." ExpandResults=$True ItemName="Spam Policy" DataType="Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=143 Area="Content Filter Policies" Name="Safety Tips" PassText="Safety Tips are enabled" FailRecommendation="Safety Tips should be enabled" Importance="By default, safety tips can provide useful security information when reading an email." } $Checks += New-Object -TypeName PSObject -Property @{ Control=156 Area="Advanced Threat Protection Policies" Name="Safe Links Tracking" PassText="Safe Links Policies are tracking clicks" FailRecommendation="Enable link tracking in Safe Links Policies" Importance="Placeholder" } $Checks += New-Object -TypeName PSObject -Property @{ Control=158 Area="Advanced Threat Protection Policies" Name="Safe Attachments SharePoint and Teams" PassText="Safe Attachments is enabled for SharePoint and Teams" FailRecommendation="Enable Safe Attachments for SharePoint and Teams" Importance="Safe Attachments assists scanning for zero day malware by using behavioural analysis and sandboxing, supplimenting signature definitions." } $Checks += New-Object -TypeName PSObject -Property @{ Control=169 Area="Advanced Threat Protection Policies" Name="Office Enablement" PassText="Safe Links is enabled in Office ProPlus" FailRecommendation="Enable Safe Links for Office Clients" Importance="Placeholder" } $Checks += New-Object -TypeName PSObject -Property @{ Control=179 Area="Advanced Threat Protection Policies" Name="Intra-organization Safe Links" PassText="Safe Links is enabled intra-organization" FailRecommendation="Enable Safe Links between internal users" Importance="Phishing attacks are not limited from external users. Commonly, when one user is compromised, that user can be used in a process of laterral movement between different accounts in your organization. Configuring Safe Links so that internal messages are also re-written can assist with latteral movement using phishing." } $Checks += New-Object -TypeName PSObject -Property @{ Control=189 Area="Advanced Threat Protection Policies" Name="Safe Attachment Whitelisting" PassText="Safe Attachments is not bypassed" FailRecommendation="Remove rule to bypass Safe Attachments" Importance="Placeholder" } $Checks += New-Object -TypeName PSObject -Property @{ Control=205 Area="Malware Filter Policy" Name="Common Attachment Type Filter" PassText="Common attachment type filter is enabled" FailRecommendation="Enable common attachment type filter" Importance="The common attachment type filter can block file types that commonly contain malware, including in internal emails." } Return $Checks } Function Get-ORCACollection { $Collection = @{} Write-Host "$(Get-Date) Getting Anti-Spam Settings" $Collection["HostedConnectionFilterPolicy"] = Get-HostedConnectionFilterPolicy $Collection["HostedContentFilterPolicy"] = Get-HostedContentFilterPolicy $Collection["HostedContentFilterRule"] = Get-HostedContentFilterRule $Collection["HostedOutboundSpamFilterPolicy"] = Get-HostedOutboundSpamFilterPolicy Write-Host "$(Get-Date) Getting Tenant Settings" $Collection["AdminAuditLogConfig"] = Get-AdminAuditLogConfig Write-Host "$(Get-Date) Getting Anti Phish Settings" $Collection["AntiPhishPolicy"] = Get-AntiphishPolicy Write-Host "$(Get-Date) Getting Anti-Malware Settings" $Collection["MalwareFilterPolicy"] = Get-MalwareFilterPolicy $Collection["MalwareFilterRule"] = Get-MalwareFilterRule Write-Host "$(Get-Date) Getting Transport Rules" $Collection["TransportRules"] = Get-TransportRule Write-Host "$(Get-Date) Getting ATP Policies" $Collection["SafeAttachmentsPolicy"] = Get-SafeAttachmentPolicy $Collection["SafeAttachmentsRules"] = Get-SafeAttachmentRule $Collection["SafeLinksPolicy"] = Get-SafeLinksPolicy $Collection["SafeLinksRules"] = Get-SafeLinksRule $Collection["AtpPolicy"] = Get-AtpPolicyForO365 Write-Host "$(Get-Date) Getting Accepted Domains" $Collection["AcceptedDomains"] = Get-AcceptedDomain Return $Collection } Function Get-ORCAResults { Param( $Collection ) # Main Analysis Write-Host "$(Get-Date) Analysis starting. Version $($Version)" -ForegroundColor Green $Return = @() Write-Host "$(Get-Date) Analysis - Unified Auditing" $Return += Invoke-CheckUnifiedLogging $Collection.Get_Item("AdminAuditLogConfig") Write-Host "$(Get-Date) Analysis - Exchange - Allow lists" $Return += Invoke-CheckAllowedSenderDomains $Collection.Get_Item("HostedContentFilterPolicy") Write-Host "$(Get-Date) Analysis - Exchange - ZAP Checks" $Return += Invoke-CheckZAP -MalwareFilterPolicies $Collection.Get_Item("MalwareFilterPolicy") -ContentFilterPolicies $Collection.Get_Item("HostedContentFilterPolicy") Write-Host "$(Get-Date) Analysis - Exchange - Content Filter Policy" $Return += Invoke-CheckContentFilterActions -ContentFilterPolicies $Collection.Get_Item("HostedContentFilterPolicy") $Return += Invoke-CheckContentFilterSafetyTips -ContentFilterPolicies $Collection.Get_Item("HostedContentFilterPolicy") Write-Host "$(Get-Date) Analysis - Exchange - Malware Filter Policy" $Return += Invoke-CheckMalwareNotifications $Collection.Get_Item("MalwareFilterPolicy") $Return += Invoke-CheckCommonAttachmentTypeFilter -MalwareFilterPolicies $Collection.Get_Item("MalwareFilterPolicy") $Return += Invoke-CheckIPAllowList $Collection.Get_Item("HostedConnectionFilterPolicy") Write-Host "$(Get-Date) Analysis - Exchange - Transport Rules" $Return += Invoke-CheckTransportRuleSCL $Collection.Get_Item("TransportRules") Write-Host "$(Get-Date) Analysis - ATP - General" $Return += Invoke-CheckATPForSPOTeamsODB -ATPPolicy $Collection.Get_Item("AtpPolicy") Write-Host "$(Get-Date) Analysis - ATP - SafeLinks" $Return += Invoke-CheckATPSafeLinksTracking -ATPPolicy $Collection.Get_Item("SafeLinksPolicy") $Return += Invoke-CheckATPSafeLinksTrackingInternal -ATPPolicy $Collection.Get_Item("SafeLinksPolicy") $Return += Invoke-CheckATPSafeLinksTrackingOfficeApps -ATPPolicy $Collection.Get_Item("AtpPolicy") Write-Host "$(Get-Date) Analysis - ATP - SafeAttachments" $Return += Invoke-CheckATPSafeAttachmentsBypass -TransportRules $Collection.Get_Item("TransportRules") Return $Return } Function Invoke-ORCASummary { <# DETERMINE CHECK RESULTS To determine the final PASS/FAIL result of the check based on the results To add an affected object property which can be used in tables #> Param( $Results, $Checks ) ForEach($Check in $Checks) { $CheckResults = @($Results | Where-Object {$_.Control -eq $Check.Control}) $FailResults = @($CheckResults | Where-Object {$_.Result -eq "Fail"}) $PassResults = @($CheckResults | Where-Object {$_.Result -eq "Pass"}) $Objects = @() ForEach($CheckResult in $CheckResults) { $Objects += New-Object -TypeName PSObject -Property @{ ConfigItem=$($CheckResult.ConfigItem) ConfigData=$($CheckResult.ConfigData) Result=$($CheckResult.Result) } } If($($FailResults.Count) -eq 0) { $Result = "Pass" } else { $Result = "Fail" } $Check | Add-Member -NotePropertyName FailCount -NotePropertyValue $($FailResults.Count) $Check | Add-Member -NotePropertyName PassCount -NotePropertyValue $($PassResults.Count) $Check | Add-Member -NotePropertyName Result -NotePropertyValue $($Result) $Check | Add-Member -NotePropertyName Objects -NotePropertyValue $($Objects) } Return $Checks } Function Get-ORCAHtmlOutput { <# OUTPUT GENERATION / Header #> Param( $Collection, $Checks, $VersionCheck ) Write-Host "$(Get-Date) Generating Output" -ForegroundColor Green # Obtain the tenant domain and date for the report $TenantDomain = ($Collection["AcceptedDomains"] | Where-Object {$_.InitialDomain -eq $True}).DomainName $ReportDate = $(Get-Date -format 'dd-MMM-yyyy HH:mm') # Summary $RecommendationCount = $($Checks | Where-Object {$_.Result -eq "Fail"}).Count $OKCount = $($Checks | Where-Object {$_.Result -eq "Pass"}).Count # Output start $output = "<!doctype html> <html lang='en'> <head> <!-- Required meta tags --> <meta charset='utf-8'> <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css' crossorigin='anonymous'> <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css' integrity='sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T' crossorigin='anonymous'> <script src='https://code.jquery.com/jquery-3.3.1.slim.min.js' integrity='sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo' crossorigin='anonymous'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js' integrity='sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1' crossorigin='anonymous'></script> <script src='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js' integrity='sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM' crossorigin='anonymous'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.js'></script> <style> .bd-callout { padding: 1.25rem; margin-top: 1.25rem; margin-bottom: 1.25rem; border: 1px solid #eee; border-left-width: .25rem; border-radius: .25rem } .bd-callout h4 { margin-top: 0; margin-bottom: .25rem } .bd-callout p:last-child { margin-bottom: 0 } .bd-callout code { border-radius: .25rem } .bd-callout+.bd-callout { margin-top: -.25rem } .bd-callout-info { border-left-color: #5bc0de } .bd-callout-info h4 { color: #5bc0de } .bd-callout-warning { border-left-color: #f0ad4e } .bd-callout-warning h4 { color: #f0ad4e } .bd-callout-danger { border-left-color: #d9534f } .bd-callout-danger h4 { color: #d9534f } .bd-callout-success { border-left-color: #00bd19 } </style> <title>ORCA Report</title> </head> <body class='app header-fixed bg-light'> <nav class='navbar fixed-top navbar-light bg-white p-3 border-bottom'> <div class='container-fluid'> <div class='col-sm' style='text-align:left'> <div class='row'><div><i class='fas fa-binoculars'></i></div><div class='ml-3'><strong>ORCA</strong></div></div> </div> <div class='col-sm' style='text-align:center'> <strong>$($TenantDomain)</strong> </div> <div class='col-sm' style='text-align:right'> $($ReportDate) </div> </div> </nav> <div class='app-body p-3'> <main class='main'> <!-- Main content here --> <div class='container' style='padding-top:50px;'></div> <div class='card'> <div class='card-body'> <h2 class='card-title'>Office ATP Recommended Configuration Analyzer Report</h5> <strong>Version $($VersionCheck.Version.ToString())</strong> <p>This report details any tenant configuration changes recommended within your tenant.</p>" <# OUTPUT GENERATION / Version Warning #> If($VersionCheck.Updated -eq $False) { $Output += " <div class='alert alert-danger pt-2' role='alert'> ORCA is out of date. You're running version $($VersionCheck.Version) but version $($VersionCheck.GalleryVersion) is available! Run Update-Module ORCA to get the latest definitions! </div> " } $Output += "</div> </div>" <# OUTPUT GENERATION / Summary cards #> $Output += " <div class='row p-3'> <div class='col d-flex justify-content-center text-center'> <div class='card text-white bg-warning mb-3' style='width: 18rem;'> <div class='card-header'><h5>Recommendations</h4></div> <div class='card-body'> <h2>$($RecommendationCount)</h3> </div> </div> </div> <div class='col d-flex justify-content-center text-center'> <div class='card text-white bg-success mb-3' style='width: 18rem;'> <div class='card-header'><h5>OK</h4></div> <div class='card-body'> <h2>$($OKCount)</h5> </div> </div> </div> </div> " <# OUTPUT GENERATION / Zones #> ForEach ($Area in ($Checks | Group-Object Area)) { # Write the top of the card $Output += " <div class='card m-3'> <div class='card-header'> $($Area.Name) </div> <div class='card-body'>" # Each check ForEach ($Check in $Area.Group) { $Output += " <h5>$($Check.Name)</h5>" If($Check.Result -eq "Pass") { $CalloutType = "bd-callout-success" $BadgeType = "badge-success" $BadgeName = "OK" $Icon = "fas fa-thumbs-up" $Title = $Check.PassText } Else { $CalloutType = "bd-callout-warning" $BadgeType = "badge-warning" $BadgeName = "Improvement" $Icon = "fas fa-thumbs-down" $Title = $Check.FailRecommendation } $Output += " <div class='bd-callout $($CalloutType) b-t-1 b-r-1 b-b-1 p-3'> <div class='container-fluid'> <div class='row'> <div class='col-1'><i class='$($Icon)'></i></div> <div class='col-8'><h5>$($Title)</h5></div> <div class='col' style='text-align:right'><h5><span class='badge $($BadgeType)'>$($BadgeName)</span></h5></div> </div>" if($Check.Importance) { $Output +=" <div class='row p-3'> <div><p>$($Check.Importance)</p></div> </div>" } If($Check.ExpandResults -eq $True) { # We should expand the results by showing a table of Config Data and Items $Output +="<h6>Effected objects</h6> <div class='row pl-2 pt-3'> <table class='table'> <thead class='border-bottom'> <tr> <th>$($Check.ItemName)</th> <th>$($Check.DataType)</th> <th style='width:50px'></th> </tr> </thead> <tbody> " ForEach($o in $Check.Objects) { if($o.Result -eq "Pass") { $oicon="fas fa-check-circle text-success" } Else{ $oicon="fas fa-times-circle text-danger" } $Output += " <tr> <td>$($o.ConfigItem)</td> <td>$($o.ConfigData)</td> <td><i class='$($oicon)'></i></td> </tr> " } $Output +=" </tbody> </table>" # If any links exist If($Check.Links) { $Output += " <table>" ForEach($Link in $Check.Links.Keys) { $Output += " <tr> <td style='width:40px'><i class='fas fa-external-link-alt'></i></td> <td><a href='$($Check.Links[$Link])'>$Link</a></td> <tr> " } $Output += " </table> " } $Output +=" </div>" } $Output += " </div> </div> " } # End the card $Output+= " </div> </div>" } <# OUTPUT GENERATION / Footer #> $Output += " </main> </div> <footer class='app-footer'> <!-- Footer content here --> </footer> </body> </html>" Return $Output } function Invoke-ORCAVersionCheck { Param( $Terminate ) Write-Host "$(Get-Date) Performing ORCA Version check..." $ORCAVersion = (Get-Module ORCA | Sort-Object Version -Desc)[0].Version $PSGalleryVersion = (Find-Module ORCA -Repository PSGallery -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue).Version If($PSGalleryVersion -gt $ORCAVersion) { $Updated = $False If($Terminate) { Throw "ORCA is out of date. Your version is $ORCAVersion and the published version is $PSGalleryVersion. Run Update-Module ORCA or run Get-ORCAReport with -NoUpdate." } else { Write-Host "$(Get-Date) ORCA is out of date. Your version: $($ORCAVersion) published version is $($PSGalleryVersion)" } } else { $Updated = $True } Return New-Object -TypeName PSObject -Property @{ Updated=$Updated Version=$ORCAVersion GalleryVersion=$PSGalleryVersion } } Function Get-ORCAReport { <# .SYNOPSIS The Office 365 Recommended Configuration Analyzer (ORCA) Report Generator .DESCRIPTION Office 365 Recommended Configuration Analyzer (ORCA) The Get-ORCAReport command generates a HTML report based on recommended practices based on field experiences working with Exchange Online Protection and Advanced Threat Protection. Output report uses open source components for HTML formatting - bootstrap - MIT License - https://getbootstrap.com/docs/4.0/about/license/ - fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free Engine and report generation Cam Murray Field Engineer - Microsoft camurray@microsoft.com .PARAMETER Report Optional. Full path to the report file that you want to generate. If this is not specified, a directory in your current users AppData is created called ORCA. Reports are generated in this directory in the following format: ORCA-tenantname-date.html .PARAMETER NoUpdate Optional. Switch that will tell the script not to exit in the event you are running an outdated rule definition. It's always recommended to be running the latest rule definition/module. .PARAMETER NoConnect Optional. Switch that will instruct ORCA not to connect and to use an already established connection to Exchange Online. .PARAMETER Collection Optional. For passing an already established collection object. Can be used for offline collection analysis. .EXAMPLE Get-ORCAReport .EXAMPLE Get-ORCAReport -Report myreport.html .EXAMPLE Get-ORCAReport -Report myreport.html -NoConnect #> Param( [CmdletBinding()] [Switch]$NoConnect, [Switch]$NoUpdate, $Collection, $Output ) # Version check $VersionCheck = Invoke-ORCAVersionCheck # Unless -NoConnect specified (already connected), connect to Exchange Online If(!$NoConnect) { Invoke-ORCAConnections } # Get the object of ORCA checks $Checks = Get-ORCACheckDefs # Get the collection in to memory. For testing purposes, we support passing the collection as an object If($Null -eq $Collection) { $Collection = Get-ORCACollection } # Get the results $Return = Get-ORCAResults -Collection $Collection # Put data in to Checks from the results $Checks = Invoke-ORCASummary -Results $Return -Checks $Checks # Generate HTML Output $HTMLReport = Get-ORCAHtmlOutput -Collection $Collection -Checks $Checks -VersionCheck $VersionCheck # Write to file If(!$Output) { $OutputDirectory = Get-ORCADirectory $Tenant = $(($Collection["AcceptedDomains"] | Where-Object {$_.InitialDomain -eq $True}).DomainName -split '\.')[0] $ReportFileName = "ORCA-$($tenant)-$(Get-Date -Format 'MMddyy').html" $Output = "$OutputDirectory\$ReportFileName" } $HTMLReport | Out-File -FilePath $Output Write-Host "$(Get-Date) Complete! Output is in $Output" Invoke-Expression $Output } |