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 If($Policy.HighConfidenceSpamAction -ne "Quarantine") { $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 MoveToJmf If($Policy.SpamAction -ne "MoveToJmf") { $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 MoveToJmf If($Policy.BulkSpamAction -ne "MoveToJmf") { $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 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 { <# 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 = "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="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-CheckATPPhishThreshold { <# Checks to determine if ATP is enabled for SharePoint, Teams, and OD4B as per 'tickbox' in the ATP configuration. #> Param( $AntiPhishPolicy ) $Check = "ATP Phish Threshold" $Cid = "220" $Return = @() ForEach($Policy in $AntiPhishPolicy) { If($Policy.PhishThresholdLevel -eq 1) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishThresholdLevel) Rule="PhishThreshold Level is 1" Control=$Cid } } else { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.PhishThresholdLevel) Rule="PhishThreshold Level 2 or higher" Control=$Cid } } } Return $Return } function Invoke-CheckATPPhishDomainImpAction { Param( $AntiPhishPolicy ) $Check = "ATP Phish Domain Impersonation" $Cid = "222" $Return = @() ForEach($Policy in ($AntiPhishPolicy | Where-Object {$_.Enabled -eq $True})) { If($Policy.EnableTargetedDomainsProtection -eq $False) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="TargetedDomainsProtection Disabled" Rule="Targeted Domains Protection is Off" Control=$Cid } } ElseIf($Policy.EnableTargetedDomainsProtection -eq $True -and $Policy.TargetedDomainProtectionAction -ne "MoveToJMF") { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.TargetedDomainProtectionAction) Rule="TargetedDomainProtectionAction not MoveToJMF" Control=$Cid } } ElseIf($Policy.EnableTargetedDomainsProtection -eq $True -and $Policy.TargetedDomainProtectionAction -eq "MoveToJMF") { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData=$($Policy.TargetedDomainProtectionAction) Rule="EnableTargetedDomainsProtection is True and TargetedDomainProtectionAction is MoveToJMF" Control=$Cid } } } If($Return.Count -eq 0) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check Rule="No Enabled AntiPhish Policy" Control=$Cid } } Return $Return } function Invoke-CheckATPPhishUserImpAction { Param( $AntiPhishPolicy ) $Check = "ATP User Impersonation" $Cid = "223" $Return = @() ForEach($Policy in ($AntiPhishPolicy | Where-Object {$_.Enabled -eq $True})) { If($Policy.EnableTargetedUserProtection -eq $False) { # Policy Targeted UserProtection is off $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="Targetted User Impersonation Disabled" Rule="Targeted User Protection Off" Control=$Cid } } Else { # Check for enabled safety tips If($Policy.EnableSimilarUsersSafetyTips -eq $False) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableSimilarUsersSafetyTips $($Policy.EnableSimilarUsersSafetyTips)" Rule="Safety tips is off" Control=$Cid } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableSimilarUsersSafetyTips $($Policy.EnableSimilarUsersSafetyTips)" Rule="Safety tips is on" Control=$Cid } } # Check for action being MoveToJmf If($Policy.TargetedUserProtectionAction -eq "MoveToJmf") { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="TargetedUserProtectionAction $($Policy.TargetedUserProtectionAction)" Rule="TargetedUserProtectionAction is MoveToJmf" Control=$Cid } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="TargetedUserProtectionAction $($Policy.TargetedUserProtectionAction)" Rule="TargetedUserProtectionAction not MoveToJmf" Control=$Cid } } } } If($Return.Count -eq 0) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check Rule="No Enabled AntiPhish Policy" Control=$Cid } } Return $Return } function Invoke-CheckATPPhishMIAction { <# 221 and 224 - Check ATP Phishing Mailbox Intelligence Action #> Param( $AntiPhishPolicy ) $Check = "ATP Mailbox Intelligence" $Return = @() ForEach($Policy in ($AntiPhishPolicy | Where-Object {$_.Enabled -eq $True})) { # Determine Mailbox Intelligence is ON If($Policy.EnableMailboxIntelligence -eq $false) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableMailboxIntelligence $($Policy.EnableMailboxIntelligence)" Rule="Mailbox Intelligence Off" Control="221" } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableMailboxIntelligence $($Policy.EnableMailboxIntelligence)" Rule="Mailbox Intelligence On" Control="221" } } # Determine if tips for user impersonation is on If($Policy.EnableSimilarUsersSafetyTips -eq $false) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableSimilarUsersSafetyTips $($Policy.EnableSimilarUsersSafetyTips)" Rule="Similar User Safety Tips Off" Control="224" } } else { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" Check=$Check ConfigItem=$($Policy.Name) ConfigData="EnableSimilarUsersSafetyTips $($Policy.EnableSimilarUsersSafetyTips)" Rule="Similar User Safety Tips On" Control="224" } } } If($Return.Count -eq 0) { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" Check=$Check Rule="No Enabled AntiPhish Policy" Control=$Cid } } Return $Return } function Invoke-CheckMarkAsSpamBulkMail { <# ORCA-101 #> Param( $HostedContentFilterPolicy ) $Return = @() $CID = "ORCA-101" ForEach($Policy in $HostedContentFilterPolicy) { If($Policy.MarkAsSpamBulkMail -eq "On") { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" ConfigItem=$($Policy.Name) ConfigData=$Policy.MarkAsSpamBulkMail Control=$CID } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" ConfigItem=$($Policy.Name) ConfigData=$Policy.MarkAsSpamBulkMail Control=$CID } } } Return $Return } function Invoke-CheckBulkThreshold { <# ORCA-100 #> Param( $HostedContentFilterPolicy ) $Return = @() $CID = "ORCA-100" ForEach($Policy in $HostedContentFilterPolicy) { If($Policy.BulkThreshold -le 6) { $Return += New-Object -TypeName psobject -Property @{ Result="Pass" ConfigItem=$($Policy.Name) ConfigData=$Policy.BulkThreshold Control=$CID } } Else { $Return += New-Object -TypeName psobject -Property @{ Result="Fail" ConfigItem=$($Policy.Name) ConfigData=$Policy.BulkThreshold 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="ORCA-100" Area="Content Filter Policies" Name="Bulk Complaint Level" PassText="Bulk Complaint Level threshold is set to 6 or lower" FailRecommendation="Set the Bulk Complaint Level threshold to be 6 or lower" Importance="The differentiation between bulk and spam can sometimes be subjective. The bulk complaint level is based on the number of complaints from the sender. Decreasing the threshold can decrease the amount of perceived spam received." ExpandResults=$True ItemName="Content Filter Policy" DataType="Bulk Complaint Level Threshold" Links= @{ "Bulk Complaint Level values"="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/bulk-complaint-level-values" } } $Checks += New-Object -TypeName PSObject -Property @{ Control="ORCA-101" Area="Content Filter Policies" Name="Mark Bulk as Spam" PassText="Bulk is marked as spam" FailRecommendation="Set the content filter policy to mark bulk mail as spam" Importance="The differentiation between bulk and spam can sometimes be subjective. The bulk complaint level is based on the number of complaints from the sender. Marking bulk as spam can decrease the amount of perceived spam received." ExpandResults=$True ItemName="Content Filter Policy" DataType="Mark as Spam Bulk Mail Setting (MarkAsSpamBulkMail)" } $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= @{ "Use Anti-Spam Policy IP Allow lists"="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365#use-anti-spam-policy-ip-allow-lists" } } $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" Links= @{ "Use Anti-Spam Policy Sender/Domain Allow lists"="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365#use-anti-spam-policy-senderdomain-allow-lists" } } $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" Links= @{ "Using Exchange Transport Rules (ETRs) to allow specific senders"="https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365#using-exchange-transport-rules-etrs-to-allow-specific-senders-recommended" } } $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 Notifications" PassText="External Sender notifications are disabled" FailRecommendation="Disable notifying external senders of malware detection" Importance="Notifying external senders about malware detected in email messages could have negative impact. An adversary may use this information to verify effectiveness of malware detection." } $Checks += New-Object -TypeName PSObject -Property @{ Control=139 Area="Content Filter Policies" Name="Spam Action" PassText="Spam action set to Move message to Junk Email Folder" FailRecommendation="Change Spam action to Move message to Junk Email Folder" Importance="It is recommended to configure Spam detection action to Move messages to Junk Email folder." 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 Quarantine message" FailRecommendation="Change High Confidence Spam action to Quarantine message" Importance="It is recommended to configure High Confidence Spam detection action to Quarantine message." 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 Move message to Junk Email Folder" FailRecommendation="Change Bulk action to Move message to Junk Email Folder" Importance="It is recommended to configure Bulk detection action to Move messages to Junk Email folder." 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 message" FailRecommendation="Change Phish action to Quarantine message" Importance="It is recommended to configure the Phish detection action 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 mistakenly 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 when user clicks on safe links" FailRecommendation="Enable tracking of user clicks in Safe Links Policies" Importance="When these options are configured, click data for URLs in Word, Excel, PowerPoint, Visio documents and in emails is stored by Safe Links. This information can help dealing with phishing, suspicious email messages and URLs." } $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 for Office ProPlus, Office for iOS and Android" FailRecommendation="Enable Safe Links for Office ProPlus, Office for iOS and Android" Importance="Phishing attacks are not limited to email messages. Malicious URLs can be delivered using Office documents as well. Configuring Office 365 ATP Safe Links for Office ProPlus, Office for iOS and Android can help combat against these attacks via providing time-of-click verification of web addresses (URLs) in Office documents." } $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 lateral movement between different accounts in your organization. Configuring Safe Links so that internal messages are also re-written can assist with lateral 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 mail flow rules which bypass Safe Attachments" Importance="Office 365 ATP Safe Attachments assists scanning for zero day malware by using behavioural analysis and sandboxing, supplementing signature definitions. The protection can be bypassed using mail flow rules which set the X-MS-Exchange-Organization-SkipSafeAttachmentProcessing header for email messages." ExpandResults=$True ItemName="Transport Rule" DataType="Details" } $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." } $Checks += New-Object -TypeName PSObject -Property @{ Control=220 Area="Advanced Threat Protection Policies" Name="Phishing Threshold Level" PassText="Phishing theshold level is adequate." FailRecommendation="Increase the antiphishing threshold level" Importance="The higher the antiphishing threshold, the stricter the mechanisms are that detect phishing attempts against your users." ExpandResults=$True ItemName="Antiphishing Policy" DataType="Phishing Threshold Level" } $Checks += New-Object -TypeName PSObject -Property @{ Control=221 Area="Advanced Threat Protection Policies" Name="Mailbox Intelligence Enabled" PassText="Mailbox intelligence is enabled in anti-phishing policies" FailRecommendation="Enable mailbox intelligence in anti-phishing policies" Importance="Mailbox Intelligence checks can provide your users with intelligence on suspicious incomming emails that appear to be from users that they normally communicate with based on their graph." ExpandResults=$True ItemName="Antiphishing Policy" DataType="Setting" } $Checks += New-Object -TypeName PSObject -Property @{ Control=222 Area="Advanced Threat Protection Policies" Name="Domain Impersonation Action" PassText="Domain Impersonation action is set to move to Junk Mail Folder (JMF)" FailRecommendation="Configure domain impersonation action to Junk Mail Filter (JMF)" Importance="Domain Impersonation can detect impersonation attempts against your domains or domains that look very similiar to your domains. Move messages that are caught using this impersonation protection to Junk Mail Folder (JMF)." ExpandResults=$True ItemName="Antiphishing Policy" DataType="Domain Impersonation Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=223 Area="Advanced Threat Protection Policies" Name="Domain Impersonation Action" PassText="User impersonation action is set to Junk Mail Filter (JMF) and Tip" FailRecommendation="Configure user impersonation action to Junk Mail Filter (JMF) and Tip" Importance="User impersonation protection can detect spoofing of your sensitive users. Move messages that are caught using user impersonation detection to Junk and mark them with a safety tip." ExpandResults=$True ItemName="Antiphishing Policy" DataType="User Impersonation Action" } $Checks += New-Object -TypeName PSObject -Property @{ Control=224 Area="Advanced Threat Protection Policies" Name="Mailbox Intelligence Action" PassText="Your policy is configured to notify users with a tip." FailRecommendation="Enable tips so that users can receive visible indication on incomming messages." Importance="Mailbox Intelligence checks can provide your users with intelligence on suspicious incomming emails that appear to be from users that they normally communicate with based on their graph." ExpandResults=$True ItemName="Antiphishing Policy" DataType="Setting" } 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") $Return += Invoke-CheckBulkThreshold -HostedContentFilterPolicy $Collection.Get_Item("HostedContentFilterPolicy") $Return += Invoke-CheckMarkAsSpamBulkMail -HostedContentFilterPolicy $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") Write-Host "$(Get-Date) Analysis - ATP - Antiphishing" $Return += Invoke-CheckATPPhishThreshold -AntiPhishPolicy $Collection.Get_Item("AntiPhishPolicy") $Return += Invoke-CheckATPPhishDomainImpAction -AntiPhishPolicy $Collection.Get_Item("AntiPhishPolicy") $Return += Invoke-CheckATPPhishUserImpAction -AntiPhishPolicy $Collection.Get_Item("AntiPhishPolicy") $Return += Invoke-CheckATPPhishMIAction -AntiPhishPolicy $Collection.Get_Item("EXO-AntiPhishPolicy") 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 } |