From d219d757a3ca68f596b6f59917b3b5b40dda20ee Mon Sep 17 00:00:00 2001 From: Ignacio Serrano <103440830+iserrano76@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:17:33 +0200 Subject: [PATCH 1/7] COntinuation of #2097 --- .build/cspell-words.txt | 2 + M365/MDO/MDOThreatPolicyChecker.ps1 | 926 ++++++++++++++++++++++++ docs/M365/MDO/MDOThreatPolicyChecker.md | 94 +++ mkdocs.yml | 2 + 4 files changed, 1024 insertions(+) create mode 100644 M365/MDO/MDOThreatPolicyChecker.ps1 create mode 100644 docs/M365/MDO/MDOThreatPolicyChecker.md diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index 8bb4f8b2dc..2e5e09aabf 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -19,6 +19,7 @@ contoso CTMM Datacenter dcom +DMARC Dsamain DTLS dumptidset @@ -29,6 +30,7 @@ EICAR eicar Emotet emsmdb +Entra EOMT Eseback Eventlog diff --git a/M365/MDO/MDOThreatPolicyChecker.ps1 b/M365/MDO/MDOThreatPolicyChecker.ps1 new file mode 100644 index 0000000000..4c6998fc3a --- /dev/null +++ b/M365/MDO/MDOThreatPolicyChecker.ps1 @@ -0,0 +1,926 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +#Requires -Modules Microsoft.Graph.Authentication +#Requires -Modules Microsoft.Graph.Users +#Requires -Modules Microsoft.Graph.Groups +#Requires -Modules ExchangeOnlineManagement -Version 3.0.0 + +<# +.SYNOPSIS +Evaluates user coverage and potential redundancies in Microsoft Defender for Office 365 and Exchange Online Protection threat policies, including anti-malware, anti-phishing, and anti-spam policies, as well as Safe Attachments and Safe Links policies if licensed. + +.DESCRIPTION +This script checks which Microsoft Defender for Office 365 and Exchange Online Protection threat policies cover a particular user, including anti-malware, anti-phishing, inbound and outbound anti-spam, as well as Safe Attachments and Safe Links policies in case these are licensed for your tenant. In addition, the script can check for threat policies that have inclusion and/or exclusion settings that may be redundant or confusing and lead to missed coverage of users or coverage by an unexpected threat policy. + +.PARAMETER CsvFilePath + Allows you to specify a CSV file with a list of email addresses to check. +.PARAMETER EmailAddress + Allows you to specify email address or multiple addresses separated by commas. +.PARAMETER IncludeMDOPolicies + Checks both EOP and MDO (Safe Attachment and Safe Links) policies for user(s) specified in the CSV file or EmailAddress parameter. +.PARAMETER OnlyMDOPolicies + Checks only MDO (Safe Attachment and Safe Links) policies for user(s) specified in the CSV file or EmailAddress parameter. +.PARAMETER ShowDetailedPolicies + In addition to the policy applied, show any policy details that are set to True, On, or not blank. +.PARAMETER SkipConnectionCheck + Skips connection check for Graph and Exchange Online. +.PARAMETER SkipVersionCheck + Skips the version check of the script. +.PARAMETER ScriptUpdateOnly + Just updates script version to latest one. + +.EXAMPLE + .\MDOThreatPolicyChecker.ps1 + To check all threat policies for potentially confusing user inclusion and/or exclusion conditions and print them out for review. + +.EXAMPLE + .\MDOThreatPolicyChecker.ps1 -CsvFilePath [Path\filename.csv] + To provide a CSV input file with email addresses and see only EOP policies. + +.EXAMPLE + .\MDOThreatPolicyChecker.ps1 -EmailAddress user1@contoso.com,user2@fabrikam.com + To provide multiple email addresses by command line and see only EOP policies. + +.EXAMPLE + .\MDOThreatPolicyChecker.ps1 -CsvFilePath [Path\filename.csv] -IncludeMDOPolicies + To provide a CSV input file with email addresses and see both EOP and MDO policies. + +.EXAMPLE + .\MDOThreatPolicyChecker.ps1 -EmailAddress user1@contoso.com -OnlyMDOPolicies + To provide an email address and see only MDO (Safe Attachment and Safe Links) policies. +#> + +[CmdletBinding(DefaultParameterSetName = 'AppliedTenant')] +param( + [ValidateScript({ Test-Path $_ -PathType Leaf })] + [Parameter(Mandatory = $true, ParameterSetName = 'AppliedCsv')] + [Parameter(Mandatory = $true, ParameterSetName = 'AppliedMDOCsv')] + [string]$CsvFilePath, + + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'AppliedEmail')] + [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'AppliedMDOEmail')] + [string[]]$EmailAddress, + + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedCsv')] + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedEmail')] + [switch]$IncludeMDOPolicies, + + [Parameter(Mandatory = $true, ParameterSetName = 'AppliedMDOCsv')] + [Parameter(Mandatory = $true, ParameterSetName = 'AppliedMDOEmail')] + [switch]$OnlyMDOPolicies, + + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedCsv')] + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedEmail')] + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedMDOCsv')] + [Parameter(Mandatory = $false, ParameterSetName = 'AppliedMDOEmail')] + [switch]$ShowDetailedPolicies, + + [Parameter(Mandatory = $false)] + [switch]$SkipConnectionCheck, + + [Parameter(Mandatory = $false)] + [switch]$SkipVersionCheck, + + [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] + [switch]$ScriptUpdateOnly +) + +begin { + + . $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 + . $PSScriptRoot\..\..\Shared\LoggerFunctions.ps1 + . $PSScriptRoot\..\..\Shared\OutputOverrides\Write-Verbose.ps1 + . $PSScriptRoot\..\..\Shared\OutputOverrides\Write-Warning.ps1 + . $PSScriptRoot\..\..\Shared\OutputOverrides\Write-Host.ps1 + + # Cache to reduce calls to Get-MgGroup + $groupCache = @{} + # Cache of members to reduce number of calls to Get-MgGroupMember + $memberCache = @{} + + function Get-GroupObjectId { + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [MailAddress]$GroupEmail + ) + + $stGroupEmail = $GroupEmail.ToString() + # Check the cache first + Write-Verbose "Looking Group $stGroupEmail in cache" + if ($groupCache.ContainsKey($stGroupEmail)) { + Write-Verbose "Group $stGroupEmail found in cache" + return $groupCache[$stGroupEmail] + } + + # Get the group + $group = $null + Write-Verbose "Getting $stGroupEmail" + try { + $group = Get-MgGroup -Filter "mail eq '$stGroupEmail'" -ErrorAction Stop + } catch { + Write-Host "Error getting group $stGroupEmail`: $_" -ForegroundColor Red + return $null + } + + if ($group -and $group.id) { + if ($group.Id.GetType() -eq [string]) { + # Cache the result + Write-Verbose "Added to cache Group $stGroupEmail with Id $($group.Id)" + $groupCache[$stGroupEmail] = $group.Id + + # Return the Object ID of the group + return $group.Id + } else { + Write-Host "Wrong type for $($group.ToString()): $group.Id.GetType().Name" -ForegroundColor Red + return $null + } + } else { + Write-Host "The EmailAddress of group $stGroupEmail was not found" -ForegroundColor Red + return $null + } + } + + # Function to check if an email is in a group + function Test-IsInGroup { + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [MailAddress]$Email, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$GroupObjectId + ) + + # Check the cache first + $stEmail = $Email.ToString() + $cacheKey = "$stEmail|$GroupObjectId" + Write-Verbose "Looking for $stEmail|$GroupObjectId in cache" + if ($memberCache.ContainsKey($cacheKey)) { + Write-Verbose "Found $stEmail|$GroupObjectId in cache" + return $memberCache[$cacheKey] + } + + # Get the group members + $groupMembers = $null + Write-Verbose "Getting $GroupObjectId" + try { + $groupMembers = Get-MgGroupMember -GroupId $GroupObjectId -ErrorAction Stop + } catch { + Write-Host "Error getting group members for $GroupObjectId`: $_" -ForegroundColor Red + return $null + } + + # Check if the email address is in the group + if ($null -ne $groupMembers) { + foreach ($member in $groupMembers) { + # Check if the member is a user + if ($member['@odata.type'] -eq '#microsoft.graph.user') { + if ($member.Id) { + # Get the user object by Id + Write-Verbose "Getting user with Id $($member.Id)" + try { + $user = Get-MgUser -UserId $member.Id -ErrorAction Stop + } catch { + Write-Host "Error getting user with Id $($member.Id): $_" -ForegroundColor Red + return $null + } + # Compare the user's email address with the $email parameter + if ($user.Mail -eq $Email.ToString()) { + # Cache the result + $memberCache[$cacheKey] = $true + return $true + } + } else { + Write-Host "The user with Id $($member.Id) does not have an email address." -ForegroundColor Red + } + } + # Check if the member is a group + elseif ($member['@odata.type'] -eq '#microsoft.graph.group') { + Write-Verbose "Nested group $($member.Id)" + # Recursive call to check nested groups + $isInNestedGroup = Test-IsInGroup -Email $Email -GroupObjectId $member.Id + if ($isInNestedGroup) { + # Cache the result + Write-Verbose "Cache group $cacheKey" + $memberCache[$cacheKey] = $true + return $true + } + } + } + } else { + Write-Verbose "The group with Object ID $GroupObjectId does not have any members." + } + + # Cache the result + $memberCache[$cacheKey] = $false + return $false + } + + function Test-EmailAddress { + [OutputType([MailAddress])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$EmailAddress, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$AcceptedDomains + ) + + try { + $tempAddress = $null + Write-Verbose "Casting $EmailAddress" + $tempAddress = [MailAddress]$EmailAddress + } catch { + Write-Host "The EmailAddress $EmailAddress cannot be validated. Please provide a valid email address." -ForegroundColor Red + return $null + } + $recipient = $null + Write-Verbose "Getting $EmailAddress" + try { + $recipient = Get-EXORecipient $EmailAddress -ErrorAction Stop + } catch { + Write-Host "Error getting recipient $EmailAddress`: $_" -ForegroundColor Red + return $null + } + + if ($null -eq $recipient) { + Write-Host "$EmailAddress is not a recipient in this tenant" -ForegroundColor Red + return $null + } else { + $domain = $tempAddress.Host + Write-Verbose "Checking domain $domain" + if ($AcceptedDomains -contains $domain) { + Write-Verbose "Verified domain $domain for $tempAddress" + return $tempAddress + } else { + Write-Host "The domain $domain is not an accepted domain in your organization. Please provide a valid email address: $tempAddress " -ForegroundColor Red + return $null + } + } + } + + # Function to check rules + function Test-Rules { + param( + [Parameter(Mandatory = $true)] + $Rules, + [Parameter(Mandatory = $true)] + [MailAddress]$Email, + [Parameter(Mandatory = $false)] + [switch]$Outbound + ) + + foreach ($rule in $Rules) { + $senderOrReceiver = $exceptSenderOrReceiver = $memberOf = $exceptMemberOf = $domainsIs = $exceptIfDomainsIs = $null + $emailInRule = $emailExceptionInRule = $groupInRule = $groupExceptionInRule = $domainInRule = $domainExceptionInRule = $false + + if ($Outbound) { + Write-Verbose "Checking outbound rule $($rule.Name)" + $requestedProperties = 'From', 'ExceptIfFrom', 'FromMemberOf', 'ExceptIfFromMemberOf', 'SenderDomainIs', 'ExceptIfSenderDomainIs' + $senderOrReceiver = $rule.From + $exceptSenderOrReceiver = $rule.ExceptIfFrom + $memberOf = $rule.FromMemberOf + $exceptMemberOf = $rule.ExceptIfFromMemberOf + $domainsIs = $rule.SenderDomainIs + $exceptIfDomainsIs = $rule.ExceptIfSenderDomainIs + } else { + Write-Verbose "Checking inbound rule $($rule.Name)" + $requestedProperties = 'SentTo', 'ExceptIfSentTo', 'SentToMemberOf', 'ExceptIfSentToMemberOf', 'RecipientDomainIs', 'ExceptIfRecipientDomainIs' + $senderOrReceiver = $rule.SentTo + $exceptSenderOrReceiver = $rule.ExceptIfSentTo + $memberOf = $rule.SentToMemberOf + $exceptMemberOf = $rule.ExceptIfSentToMemberOf + $domainsIs = $rule.RecipientDomainIs + $exceptIfDomainsIs = $rule.ExceptIfRecipientDomainIs + } + + $Policy.PSObject.Properties | ForEach-Object { + if ($requestedProperties -contains $_.Name) { + Write-Host "`t`t$($_.Name): $($_.Value)" + } + } + Write-Verbose " " + + if ($senderOrReceiver -and $Email -in $senderOrReceiver) { + Write-Verbose "emailInRule" + $emailInRule = $true + } + if ($exceptSenderOrReceiver -and $Email -in $exceptSenderOrReceiver) { + Write-Verbose "emailExceptionInRule" + $emailExceptionInRule = $true + } + + if ($memberOf) { + foreach ($groupEmail in $memberOf) { + Write-Verbose "Checking member in $groupEmail" + $groupObjectId = Get-GroupObjectId -GroupEmail $groupEmail + if ([string]::IsNullOrEmpty($groupObjectId)) { + Write-Host "The group in $($rule.Name) with email address $groupEmail does not exist." -ForegroundColor Yellow + } else { + $groupInRule = Test-IsInGroup -Email $Email -GroupObjectId $groupObjectId + if ($groupInRule) { + Write-Verbose "groupInRule $($Email.ToString()) - $($groupObjectId)" + break + } + } + } + } + + if ($exceptMemberOf) { + foreach ($groupEmail in $exceptMemberOf) { + Write-Verbose "Checking member in exception $groupEmail" + $groupObjectId = Get-GroupObjectId -GroupEmail $groupEmail + if ([string]::IsNullOrEmpty($groupObjectId)) { + Write-Host "The group in $($rule.Name) with email address $groupEmail does not exist." -ForegroundColor Yellow + } else { + $groupExceptionInRule = Test-IsInGroup -Email $Email -GroupObjectId $groupObjectId + if ($groupExceptionInRule) { + Write-Verbose "groupExceptionInRule $($Email.ToString()) - $($groupObjectId)" + break + } + } + } + } + + $temp = $Email.Host + + while ($temp.IndexOf(".") -gt 0) { + if ($temp -in $domainsIs) { + Write-Verbose "domainInRule: $temp" + $domainInRule = $true + } + if ($temp -in $exceptIfDomainsIs) { + Write-Verbose "domainExceptionInRule: $temp" + $domainExceptionInRule = $true + } + $temp = $temp.Substring($temp.IndexOf(".") + 1) + } + + # Check for explicit inclusion in any user, group, or domain that are not empty, and account for 3 empty inclusions + # Also check for any exclusions as user, group, or domain. Nulls don't need to be accounted for and this is an OR condition for exclusions + if ((($emailInRule -or (-not $senderOrReceiver)) -and + ($domainInRule -or (-not $domainsIs)) -and + ($groupInRule -or (-not $memberOf))) -and + ($emailInRule -or $domainInRule -or $groupInRule)) { + if ((-not $emailExceptionInRule) -and + (-not $groupExceptionInRule) -and + (-not $domainExceptionInRule)) { + Write-Verbose "Return Rule $($rule.Name)" + Write-Verbose "emailInRule: $emailInRule domainInRule: $domainInRule groupInRule: $groupInRule " + Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " + return $rule + } + } + + if (-not $Outbound) { + # Check for implicit inclusion (no mailboxes included at all), which is possible for Presets and SA/SL. They are included if not explicitly excluded. + if ((-not $senderOrReceiver) -and (-not $domainsIs) -and (-not $memberOf)) { + if ((-not $emailExceptionInRule) -and + (-not $groupExceptionInRule) -and + (-not $domainExceptionInRule)) { + Write-Verbose "Return Rule $($rule.Name)" + Write-Verbose "senderOrReceiver: $senderOrReceiver domainsIs: $domainsIs memberOf: $memberOf " + Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " + return $rule + } + } + } + } + return $null + } + + function Show-DetailedPolicy { + param ( + [Parameter(Mandatory = $true)] + $Policy + ) + Write-Host "`n`tProperties of the policy that are True, On, or not blank:" + $excludedProperties = 'Identity', 'Id', 'Name', 'ExchangeVersion', 'DistinguishedName', 'ObjectCategory', 'ObjectClass', 'WhenChanged', 'WhenCreated', ` + 'WhenChangedUTC', 'WhenCreatedUTC', 'ExchangeObjectId', 'OrganizationalUnitRoot', 'OrganizationId', 'OriginatingServer', 'ObjectState', 'Priority', 'ImmutableId', ` + 'Description', 'HostedContentFilterPolicy', 'AntiPhishPolicy', 'MalwareFilterPolicy', 'SafeAttachmentPolicy', 'SafeLinksPolicy', 'HostedOutboundSpamFilterPolicy' + + $Policy.PSObject.Properties | ForEach-Object { + if ($null -ne $_.Value -and ` + (($_.Value.GetType() -eq [Boolean] -and $_.Value -eq $true) ` + -or ($_.Value -ne '{}' -and $_.Value -ne 'Off' -and $_.Value -ne $true -and $_.Value -ne '' -and $excludedProperties -notcontains $_.Name))) { + Write-Host "`t`t$($_.Name): $($_.Value)" + } else { + Write-Verbose "`t`tExcluded property:$($_.Name): $($_.Value)" + } + } + Write-Host " " + } + + function Get-Policy { + param( + $Rule = $null, + $PolicyType = $null + ) + + if ($null -eq $Rule) { + if ($PolicyType -eq "Anti-phish") { + $policyDetails = "`n$PolicyType (Impersonation, Mailbox/Spoof Intelligence, Honor DMARC):`n`tThe Default policy." + } elseif ($PolicyType -eq "Anti-spam") { + $policyDetails = "`n$PolicyType (includes phish & bulk actions):`n`tThe Default policy." + } else { + $policyDetails = "`n${PolicyType}:`n`tThe Default policy." + } + } else { + if ($PolicyType -eq "Anti-phish") { + $policyDetails = "`n$PolicyType (Impersonation, Mailbox/Spoof Intelligence, Honor DMARC):`n`tName: {0}`n`tPriority: {1}" -f $Rule.Name, $Rule.Priority + } elseif ($PolicyType -eq "Anti-spam") { + $policyDetails = "`n$PolicyType (includes phish & bulk actions):`n`tName: {0}`n`tPriority: {1}" -f $Rule.Name, $Rule.Priority + } else { + $policyDetails = "`n${PolicyType}:`n`tName: {0}`n`tPriority: {1}" -f $Rule.Name, $Rule.Priority + } + } + return $policyDetails + } + + function Test-GraphContext { + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string[]]$Scopes, + [Parameter(Mandatory = $true)] + [string[]]$ExpectedScopes + ) + + $validScope = $true + foreach ($expectedScope in $ExpectedScopes) { + if ($Scopes -contains $expectedScope) { + Write-Verbose "Scopes $expectedScope is present." + } else { + Write-Host "The following scope is missing: $expectedScope" -ForegroundColor Red + $validScope = $false + } + } + return $validScope + } + + function Write-DebugLog ($message) { + if (![string]::IsNullOrEmpty($message)) { + $Script:DebugLogger = $Script:DebugLogger | Write-LoggerInstance $message + } + } + + function Write-HostLog ($message) { + if (![string]::IsNullOrEmpty($message)) { + $Script:HostLogger = $Script:HostLogger | Write-LoggerInstance $message + } + # all write-host should be logged in the debug log as well. + Write-DebugLog $message + } + + Import-Module Microsoft.Graph.Authentication + Import-Module ExchangeOnlineManagement + + $LogFileName = "MDOThreatPolicyChecker" + $StartDate = Get-Date + $StartDateFormatted = ($StartDate).ToString("yyyyMMddhhmmss") + $Script:DebugLogger = Get-NewLoggerInstance -LogName "$LogFileName-Debug-$StartDateFormatted" -LogDirectory $PSScriptRoot -AppendDateTimeToFileName $false -ErrorAction SilentlyContinue + $Script:HostLogger = Get-NewLoggerInstance -LogName "$LogFileName-Results-$StartDateFormatted" -LogDirectory $PSScriptRoot -AppendDateTimeToFileName $false -ErrorAction SilentlyContinue + SetWriteHostAction ${Function:Write-HostLog} + SetWriteVerboseAction ${Function:Write-DebugLog} + SetWriteWarningAction ${Function:Write-HostLog} + + $BuildVersion = "" + + Write-Host ("MDOThreatPolicyChecker.ps1 script version $($BuildVersion)") -ForegroundColor Green + + if ($ScriptUpdateOnly) { + switch (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/MDOThreatPolicyChecker-VersionsURL" -Confirm:$false) { + ($true) { Write-Host ("Script was successfully updated") -ForegroundColor Green } + ($false) { Write-Host ("No update of the script performed") -ForegroundColor Yellow } + default { Write-Host ("Unable to perform ScriptUpdateOnly operation") -ForegroundColor Red } + } + return + } + + if ((-not($SkipVersionCheck)) -and (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/MDOThreatPolicyChecker-VersionsURL" -Confirm:$false)) { + Write-Host ("Script was updated. Please re-run the command") -ForegroundColor Yellow + return + } +} + +process { + if (-not $SkipConnectionCheck) { + #Validate EXO PS Connection + $exoConnection = $null + try { + $exoConnection = Get-ConnectionInformation -ErrorAction Stop + } catch { + Write-Host "Error checking EXO connection: $_" -ForegroundColor Red + Write-Host "Verify that you have ExchangeOnlineManagement module installed" -ForegroundColor Yellow + Write-Host "You need a connection To Exchange Online, you can use:" -ForegroundColor Yellow + Write-Host "Connect-ExchangeOnline" -ForegroundColor Yellow + Write-Host "Exchange Online Powershell Module is required" -ForegroundColor Red + exit + } + if ($null -eq $exoConnection) { + Write-Host "Not connected to EXO" -ForegroundColor Red + Write-Host "You need a connection To Exchange Online, you can use:" -ForegroundColor Yellow + Write-Host "Connect-ExchangeOnline" -ForegroundColor Yellow + Write-Host "Exchange Online Powershell Module is required" -ForegroundColor Red + exit + } elseif ($exoConnection.count -eq 1) { + Write-Host " " + Write-Host "Connected to EXO" + Write-Host "Session details" + Write-Host "Tenant Id: $($exoConnection.TenantId)" + Write-Host "User: $($exoConnection.UserPrincipalName)" + } else { + Write-Host "You have more than one EXO sessions. Please use just one session" -ForegroundColor Red + exit + } + + if ($PSCmdlet.ParameterSetName -ne "AppliedTenant") { + #Validate Graph is connected + $graphConnection = $null + Write-Host " " + try { + $graphConnection = Get-MgContext -ErrorAction Stop + } catch { + Write-Host "Error checking Graph connection: $_" -ForegroundColor Red + Write-Host "Verify that you have Microsoft.Graph.Users and Microsoft.Graph.Groups modules installed and loaded" -ForegroundColor Yellow + Write-Host "You could use:" -ForegroundColor Yellow + Write-Host "Connect-MgGraph -Scopes 'Group.Read.All','User.Read.All'" -ForegroundColor Yellow + exit + } + if ($null -eq $graphConnection) { + Write-Host "Not connected to Graph" -ForegroundColor Red + Write-Host "Verify that you have Microsoft.Graph.Users and Microsoft.Graph.Groups modules installed and loaded" -ForegroundColor Yellow + Write-Host "You could use:" -ForegroundColor Yellow + Write-Host "Connect-MgGraph -Scopes 'Group.Read.All','User.Read.All'" -ForegroundColor Yellow + exit + } elseif ($graphConnection.count -eq 1) { + $expectedScopes = "GroupMember.Read.All", 'User.Read.All' + if (Test-GraphContext -Scopes $graphConnection.Scopes -ExpectedScopes $expectedScopes) { + Write-Host "Connected to Graph" + Write-Host "Session details" + Write-Host "TenantID: $(($graphConnection).TenantId)" + Write-Host "Account: $(($graphConnection).Account)" + } else { + Write-Host "We cannot continue without Graph Powershell session without Expected Scopes" -ForegroundColor Red + Write-Host "Verify that you have Microsoft.Graph.Users and Microsoft.Graph.Groups modules installed and loaded" -ForegroundColor Yellow + Write-Host "You could use:" -ForegroundColor Yellow + Write-Host "Connect-MgGraph -Scopes 'Group.Read.All','User.Read.All'" -ForegroundColor Yellow + exit + } + } else { + Write-Host "You have more than one Graph sessions. Please use just one session" -ForegroundColor Red + exit + } + if (($graphConnection.TenantId) -ne ($exoConnection.TenantId) ) { + Write-Host "`nThe Tenant Id from Graph and EXO are different. Please use the same tenant" -ForegroundColor Red + exit + } + } + } + + if ($PSCmdlet.ParameterSetName -eq "AppliedTenant") { + # Define the cmdlets to retrieve policies from and their corresponding policy types + $cmdlets = @{ + "Get-HostedContentFilterRule" = "Anti-spam Policy" + "Get-HostedOutboundSpamFilterRule" = "Outbound Spam Policy" + "Get-MalwareFilterRule" = "Malware Policy" + "Get-AntiPhishRule" = "Anti-phishing Policy" + "Get-SafeLinksRule" = "Safe Links Policy" + "Get-SafeAttachmentRule" = "Safe Attachment Policy" + "Get-ATPBuiltInProtectionRule" = "Built-in protection preset security Policy" + { Get-EOPProtectionPolicyRule -Identity 'Strict Preset Security Policy' } = "EOP" + { Get-EOPProtectionPolicyRule -Identity 'Standard Preset Security Policy' } = "EOP" + { Get-ATPProtectionPolicyRule -Identity 'Strict Preset Security Policy' } = "MDO (Safe Links / Safe Attachments)" + { Get-ATPProtectionPolicyRule -Identity 'Standard Preset Security Policy' } = "MDO (Safe Links / Safe Attachments)" + } + + $foundIssues = $false + + Write-Host " " + # Loop through each cmdlet + foreach ($cmdlet in $cmdlets.Keys) { + # Retrieve the policies + $policies = & $cmdlet + + # Loop through each policy + foreach ($policy in $policies) { + # Initialize an empty list to store issues + $issues = New-Object System.Collections.Generic.List[string] + + # Check the logic of the policy and add issues to the list + if ($policy.SentTo -and $policy.ExceptIfSentTo) { + $issues.Add("`t`t-> User inclusions and exclusions. `n`t`t`tExcluding and including Users individually is redundant and confusing as only the included Users could possibly be included.`n") + } + if ($policy.RecipientDomainIs -and $policy.ExceptIfRecipientDomainIs) { + $issues.Add("`t`t-> Domain inclusions and exclusions. `n`t`t`tExcluding and including Domains is redundant and confusing as only the included Domains could possibly be included.`n") + } + if ($policy.SentTo -and $policy.SentToMemberOf) { + $issues.Add("`t`t-> Illogical inclusions of Users and Groups. `n`t`t`tThe policy will only apply to Users who are also members of any Groups you have specified. `n`t`t`tThis makes the Group inclusion redundant and confusing.`n`t`t`tSuggestion: use one or the other type of inclusion.`n") + } + if ($policy.SentTo -and $policy.RecipientDomainIs) { + $issues.Add("`t`t-> Illogical inclusions of Users and Domains. `n`t`t`tThe policy will only apply to Users whose email domains also match any Domains you have specified. `n`t`t`tThis makes the Domain inclusion redundant and confusing.`n`t`t`tSuggestion: use one or the other type of inclusion.`n") + } + + # If there are any issues, print the policy details once and then list all the issues + if ($issues.Count -gt 0) { + if ($policy.State -eq "Enabled") { + $color = [console]::ForegroundColor + } else { + $color = "Yellow" + } + Write-Host ("Policy $($policy.Name):") + Write-Host ("`tType: $($cmdlets[$cmdlet]).") + Write-Host ("`tState: $($policy.State).") -ForegroundColor $color + Write-Host ("`tIssues: ") -ForegroundColor Red + foreach ($issue in $issues) { + Write-Host $issue + } + $foundIssues = $true + } + } + } + if (-not $foundIssues) { + Write-Host ("No logical inconsistencies found!") -ForegroundColor DarkGreen + } + } else { + if ($CsvFilePath) { + try { + # Import CSV file + $csvFile = Import-Csv -Path $CsvFilePath + # checking 'email' header + if ($csvFile[0].PSObject.Properties.Name -contains 'Email') { + $EmailAddress = $csvFile | Select-Object -ExpandProperty Email + } else { + Write-Host "CSV does not contain 'Email' header." -ForegroundColor Red + exit + } + } catch { + Write-Host "Error importing CSV file: $_" -ForegroundColor Red + exit + } + } + + $acceptedDomains = $null + try { + $acceptedDomains = Get-AcceptedDomain -ErrorAction Stop + } catch { + Write-Host "Error getting Accepted Domains: $_" -ForegroundColor Red + exit + } + + if ($null -eq $acceptedDomains) { + Write-Host "We do not get accepted domains." -ForegroundColor Red + exit + } + + if ($acceptedDomains.count -eq 0) { + Write-Host "No accepted domains found." -ForegroundColor Red + exit + } else { + $acceptedDomainList = New-Object System.Collections.Generic.List[string] + $acceptedDomains | ForEach-Object { $acceptedDomainList.Add($_.DomainName.ToString()) } + } + + $foundError = $false + $validEmailAddress = New-Object System.Collections.Generic.List[MailAddress] + foreach ($email in $EmailAddress) { + $tempAddress = $null + $tempAddress = Test-EmailAddress -EmailAddress $email -AcceptedDomains $acceptedDomainList + if ($null -eq $tempAddress) { + $foundError = $true + } else { + $validEmailAddress.Add($tempAddress) + } + } + if ($foundError) { + exit + } + + $malwareFilterRules = $null + $antiPhishRules = $null + $hostedContentFilterRules = $null + $hostedOutboundSpamFilterRules = $null + $eopStrictPresetRules = $null + $eopStandardPresetRules = $null + + if ( -not $OnlyMDOPolicies) { + $malwareFilterRules = Get-MalwareFilterRule | Where-Object { $_.State -ne 'Disabled' } + $antiPhishRules = Get-AntiPhishRule | Where-Object { $_.State -ne 'Disabled' } + $hostedContentFilterRules = Get-HostedContentFilterRule | Where-Object { $_.State -ne 'Disabled' } + $hostedOutboundSpamFilterRules = Get-HostedOutboundSpamFilterRule | Where-Object { $_.State -ne 'Disabled' } + $eopStrictPresetRules = Get-EOPProtectionPolicyRule -Identity 'Strict Preset Security Policy' | Where-Object { $_.State -ne 'Disabled' } + $eopStandardPresetRules = Get-EOPProtectionPolicyRule -Identity 'Standard Preset Security Policy' | Where-Object { $_.State -ne 'Disabled' } + } + + $safeAttachmentRules = $null + $safeLinksRules = $null + $mdoStrictPresetRules = $null + $mdoStandardPresetRules = $null + + if ($IncludeMDOPolicies -or $OnlyMDOPolicies) { + # Get the custom and preset rules for Safe Attachments/Links + $safeAttachmentRules = Get-SafeAttachmentRule | Where-Object { $_.State -ne 'Disabled' } + $safeLinksRules = Get-SafeLinksRule | Where-Object { $_.State -ne 'Disabled' } + $mdoStrictPresetRules = Get-ATPProtectionPolicyRule -Identity 'Strict Preset Security Policy' | Where-Object { $_.State -ne 'Disabled' } + $mdoStandardPresetRules = Get-ATPProtectionPolicyRule -Identity 'Standard Preset Security Policy' | Where-Object { $_.State -ne 'Disabled' } + } + + foreach ($email in $validEmailAddress) { + $stEmailAddress = $email.ToString() + # Initialize a variable to capture all policy details + $allPolicyDetails = "" + Write-Host "`n`nPolicies applied to $stEmailAddress..." + + if ( -not $OnlyMDOPolicies) { + # Check the Strict EOP rules first as they have higher precedence + $matchedRule = $null + if ($eopStrictPresetRules) { + $matchedRule = Test-Rules -Rules $eopStrictPresetRules -email $stEmailAddress + } + if ($eopStrictPresetRules -contains $matchedRule) { + $allPolicyDetails += "`nFor malware, spam, and phishing:`n`tName: {0}`n`tPriority: {1}`n`tThe policy actions are not configurable." -f $matchedRule.Name, $matchedRule.Priority + Write-Host $allPolicyDetails -ForegroundColor Green + $outboundSpamMatchedRule = $null + if ($hostedOutboundSpamFilterRules) { + $outboundSpamMatchedRule = Test-Rules -Rules $hostedOutboundSpamFilterRules -email $stEmailAddress -Outbound + $allPolicyDetails = Get-Policy $outboundSpamMatchedRule "Outbound Spam" + Write-Host $allPolicyDetails -ForegroundColor Yellow + } + } else { + # Check the Standard EOP rules secondly + $matchedRule = $null + if ($eopStandardPresetRules) { + $matchedRule = Test-Rules -Rules $eopStandardPresetRules -email $stEmailAddress + } + if ($eopStandardPresetRules -contains $matchedRule) { + $allPolicyDetails += "`nFor malware, spam, and phishing:`n`tName: {0}`n`tPriority: {1}`n`tThe policy actions are not configurable." -f $matchedRule.Name, $matchedRule.Priority + Write-Host $allPolicyDetails -ForegroundColor Green + $outboundSpamMatchedRule = $allPolicyDetails = $null + if ($hostedOutboundSpamFilterRules) { + $outboundSpamMatchedRule = Test-Rules -Rules $hostedOutboundSpamFilterRules -Email $stEmailAddress -Outbound + $allPolicyDetails = Get-Policy $outboundSpamMatchedRule "Outbound Spam" + Write-Host $allPolicyDetails -ForegroundColor Yellow + } + } else { + # If no match in EOPProtectionPolicyRules, check MalwareFilterRules, AntiPhishRules, outboundSpam, and HostedContentFilterRules + $allPolicyDetails = " " + $malwareMatchedRule = $malwareFilterPolicy = $null + if ($malwareFilterRules) { + $malwareMatchedRule = Test-Rules -Rules $malwareFilterRules -Email $stEmailAddress + if ($null -eq $malwareMatchedRule) { + Write-Host "`nMalware:`n`tDefault policy" -ForegroundColor Yellow + } else { + $malwareFilterPolicy = Get-MalwareFilterPolicy $malwareMatchedRule.Name + Write-Host "`nMalware:`n`tName: $($malwareMatchedRule.Name)`n`tPriority: $($malwareMatchedRule.Priority)" -ForegroundColor Yellow + if ($malwareFilterPolicy -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $malwareFilterPolicy + } + } + } + $antiPhishMatchedRule = $antiPhishPolicy = $null + if ($antiPhishRules) { + $antiPhishMatchedRule = Test-Rules -Rules $antiPhishRules -Email $stEmailAddress + if ($null -eq $antiPhishMatchedRule) { + Write-Host "`nAnti-phish:`n`tDefault policy" -ForegroundColor Yellow + } else { + $antiPhishPolicy = Get-AntiPhishPolicy $antiPhishMatchedRule.Name + Write-Host "`nAnti-phish:`n`tName: $($antiPhishMatchedRule.Name)`n`tPriority: $($antiPhishMatchedRule.Priority)" -ForegroundColor Yellow + if ($antiPhishPolicy -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $antiPhishPolicy + } + } + } + $spamMatchedRule = $hostedContentFilterPolicy = $null + if ($hostedContentFilterRules) { + $spamMatchedRule = Test-Rules -Rules $hostedContentFilterRules -Email $stEmailAddress + if ($null -eq $spamMatchedRule) { + Write-Host "`nAnti-spam::`n`tDefault policy" -ForegroundColor Yellow + } else { + $hostedContentFilterPolicy = Get-HostedContentFilterPolicy $spamMatchedRule.Name + Write-Host "`nAnti-spam:`n`tName: $($spamMatchedRule.Name)`n`tPriority: $($spamMatchedRule.Priority)" -ForegroundColor Yellow + if ($hostedContentFilterPolicy -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $hostedContentFilterPolicy + } + } + } + $outboundSpamMatchedRule = $hostedOutboundSpamFilterPolicy = $null + if ($hostedOutboundSpamFilterRules) { + $outboundSpamMatchedRule = Test-Rules -Rules $hostedOutboundSpamFilterRules -email $stEmailAddress -Outbound + if ($null -eq $outboundSpamMatchedRule) { + Write-Host "`nOutbound Spam:`n`tDefault policy" -ForegroundColor Yellow + } else { + $hostedOutboundSpamFilterPolicy = Get-HostedOutboundSpamFilterPolicy $outboundSpamMatchedRule.Name + Write-Host "`nOutbound Spam:`n`tName: $($outboundSpamMatchedRule.Name)`n`tPriority: $($outboundSpamMatchedRule.Priority)" -ForegroundColor Yellow + if ($hostedOutboundSpamFilterPolicy -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $hostedOutboundSpamFilterPolicy + } + } + } + $allPolicyDetails = $userDetails + "`n" + $allPolicyDetails + Write-Host $allPolicyDetails -ForegroundColor Yellow + } + } + } + + if ($IncludeMDOPolicies -or $OnlyMDOPolicies) { + $domain = $email.Host + $matchedRule = $null + + # Check the MDO Strict Preset rules first as they have higher precedence + if ($mdoStrictPresetRules) { + $matchedRule = Test-Rules -Rules $mdoStrictPresetRules -Email $stEmailAddress + } + if ($mdoStrictPresetRules -contains $matchedRule) { + Write-Host ("`nFor both Safe Attachments and Safe Links:`n`tName: {0}`n`tPriority: {1}" -f $matchedRule.Name, $matchedRule.Priority) -ForegroundColor Green + } else { + # Check the Standard MDO rules secondly + $matchedRule = $null + if ($mdoStandardPresetRules) { + $matchedRule = Test-Rules -Rules $mdoStandardPresetRules -Email $stEmailAddress + } + if ($mdoStandardPresetRules -contains $matchedRule) { + Write-Host ("`nFor both Safe Attachments and Safe Links:`n`tName: {0}`n`tPriority: {1}" -f $matchedRule.Name, $matchedRule.Priority) -ForegroundColor Green + } else { + # No match in preset ATPProtectionPolicyRules, check custom SA/SL rules + $SAmatchedRule = $null + if ($safeAttachmentRules) { + $SAmatchedRule = Test-Rules -Rules $safeAttachmentRules -Email $stEmailAddress + } + $SLmatchedRule = $null + if ($safeLinksRules) { + $SLmatchedRule = Test-Rules -Rules $safeLinksRules -Email $stEmailAddress + } + if ($null -eq $SAmatchedRule) { + # Get the Built-in Protection Rule + $builtInProtectionRule = Get-ATPBuiltInProtectionRule + # Initialize a variable to track if the user is a member of any excluded group + $isInExcludedGroup = $false + # Check if the user is a member of any group in ExceptIfSentToMemberOf + foreach ($groupEmail in $builtInProtectionRule.ExceptIfSentToMemberOf) { + $groupObjectId = Get-GroupObjectId -GroupEmail $groupEmail + if ((-not [string]::IsNullOrEmpty($groupObjectId)) -and (Test-IsInGroup -Email $stEmailAddress -GroupObjectId $groupObjectId)) { + $isInExcludedGroup = $true + break + } + } + # Check if the user is returned by ExceptIfSentTo, isInExcludedGroup, or ExceptIfRecipientDomainIs in the Built-in Protection Rule + if ($stEmailAddress -in $builtInProtectionRule.ExceptIfSentTo -or + $isInExcludedGroup -or + $domain -in $builtInProtectionRule.ExceptIfRecipientDomainIs) { + Write-Host "`nSafe Attachments:`n`tThe user is excluded from all Safe Attachment protection because they are excluded from Built-in Protection, and they are not explicitly included in any other policy." -ForegroundColor Red + } else { + Write-Host "`nSafe Attachments:`n`tIf your organization has at least one A5/E5, or MDO license, the user is included in the Built-in policy." -ForegroundColor Yellow + } + $policy = $null + } else { + $safeAttachmentPolicy = Get-SafeAttachmentPolicy -Identity $SAmatchedRule.Name + Write-Host "`nSafe Attachments:`n`tName: $($SAmatchedRule.Name)`n`tPriority: $($SAmatchedRule.Priority)" -ForegroundColor Yellow + if ($SAmatchedRule -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $safeAttachmentPolicy + } + } + + if ($null -eq $SLmatchedRule) { + # Get the Built-in Protection Rule + $builtInProtectionRule = Get-ATPBuiltInProtectionRule + + # Initialize a variable to track if the user is a member of any excluded group + $isInExcludedGroup = $false + + # Check if the user is a member of any group in ExceptIfSentToMemberOf + foreach ($groupEmail in $builtInProtectionRule.ExceptIfSentToMemberOf) { + $groupObjectId = Get-GroupObjectId -GroupEmail $groupEmail + if ((-not [string]::IsNullOrEmpty($groupObjectId)) -and (Test-IsInGroup -Email $stEmailAddress -GroupObjectId $groupObjectId)) { + $isInExcludedGroup = $true + break + } + } + + # Check if the user is returned by ExceptIfSentTo, isInExcludedGroup, or ExceptIfRecipientDomainIs in the Built-in Protection Rule + if ($stEmailAddress -in $builtInProtectionRule.ExceptIfSentTo -or + $isInExcludedGroup -or + $domain -in $builtInProtectionRule.ExceptIfRecipientDomainIs) { + Write-Host "`nSafe Links:`n`tThe user is excluded from all Safe Links protection because they are excluded from Built-in Protection, and they are not explicitly included in any other policy." -ForegroundColor Red + } else { + Write-Host "`nSafe Links:`n`tIf your organization has at least one A5/E5, or MDO license, the user is included in the Built-in policy." -ForegroundColor Yellow + } + $policy = $null + } else { + $safeLinkPolicy = Get-SafeLinksPolicy -Identity $SLmatchedRule.Name + Write-Host "`nSafe Links:`n`tName: $($SLmatchedRule.Name)`n`tPriority: $($SLmatchedRule.Priority)" -ForegroundColor Yellow + if ($SLmatchedRule -and $ShowDetailedPolicies) { + Show-DetailedPolicy -Policy $safeLinkPolicy + } + } + } + } + } + } + } + Write-Host " " +} diff --git a/docs/M365/MDO/MDOThreatPolicyChecker.md b/docs/M365/MDO/MDOThreatPolicyChecker.md new file mode 100644 index 0000000000..3a38eb5c94 --- /dev/null +++ b/docs/M365/MDO/MDOThreatPolicyChecker.md @@ -0,0 +1,94 @@ +# MDOThreatPolicyChecker + +Download the latest release: [MDOThreatPolicyChecker.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/MDOThreatPolicyChecker.ps1) + +This script checks which Microsoft Defender for Office 365 and Exchange Online Protection threat policies cover a particular user, including anti-malware, anti-phishing, inbound and outbound anti-spam, as well as Safe Attachments and Safe Links policies in case these are licensed for your tenant. In addition, the script can check for threat policies that have inclusion and/or exclusion settings that may be redundant or confusing and lead to missed coverage of users or coverage by an unexpected threat policy. + +## Common Usage +The script uses Exchange Online cmdlets from Exchange Online module and Microsoft.Graph cmdLets from Microsoft.Graph.Authentication, Microsoft.Graph.Groups and Microsoft.Graph.Users modules. + +To run the PowerShell Graph cmdlets used in this script, you need only the following modules from the Microsoft.Graph PowerShell SDK: +- Microsoft.Graph.Groups: Contains cmdlets for managing groups, including `Get-MgGroup` and `Get-MgGroupMember`. +- Microsoft.Graph.Users: Includes cmdlets for managing users, such as `Get-MgUser`. +- Microsoft.Graph.Authentication: Required for authentication purposes and to run any cmdlet that interacts with Microsoft Graph. + +You can find the Microsoft Graph modules in the following link:
+    https://www.powershellgallery.com/packages/Microsoft.Graph/
+    https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation?view=graph-powershell-1.0#installation + +Here's how you can install the required submodules for the PowerShell Graph SDK cmdlets: + +```powershell +Install-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser +Install-Module -Name Microsoft.Graph.Groups -Scope CurrentUser +Install-Module -Name Microsoft.Graph.Users -Scope CurrentUser +``` + +!!! warning "NOTE" + + Remember to run these commands in a PowerShell session with the appropriate permissions. The -Scope CurrentUser parameter installs the modules for the current user only, which doesn't require administrative privileges. + + +In the Graph connection you will need the following scopes 'Group.Read.All','User.Read.All'
+```powershell +Connect-MgGraph -Scopes 'Group.Read.All','User.Read.All' +``` +

+You need as well an Exchange Online session.
+```powershell +Connect-ExchangeOnline +``` + +You can find the Exchange module and information in the following links:
+    https://learn.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps
+    https://www.powershellgallery.com/packages/ExchangeOnlineManagement + + +## Examples: +To check all threat policies for potentially confusing user inclusion and/or exclusion conditions and print them out for review, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 +``` + +To provide a CSV input file with email addresses and see only EOP policies, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -CsvFilePath [Path\filename.csv] +``` + +To provide multiple email addresses by command line and see only EOP policies, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -EmailAddress user1@contoso.com,user2@fabrikam.com +``` + +To provide a CSV input file with email addresses and see both EOP and MDO policies, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -CsvFilePath [Path\filename.csv] -IncludeMDOPolicies +``` + +To provide an email address and see only MDO (Safe Attachment and Safe Links) policies, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -EmailAddress user1@contoso.com -OnlyMDOPolicies +``` + +To see the details of the policies applied to mailbox in a CSV file for both EOP and MDO, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -CsvFilePath [Path\filename.csv] -IncludeMDOPolicies -ShowDetailedPolicies +``` + +To get all mailboxes in your tenant and print out their EOP and MDO policies, run the following:
+```powershell +.\MDOThreatPolicyChecker.ps1 -IncludeMDOPolicies -EmailAddress @(Get-ExOMailbox -ResultSize unlimited | Select-Object -ExpandProperty PrimarySmtpAddress) +``` + +## Parameters + +Parameter | Description | +----------|-------------| +CsvFilePath | Allows you to specify a CSV file with a list of email addresses to check. Csv file must include a first line with header Email. +EmailAddress | Allows you to specify email address or multiple addresses separated by commas. +IncludeMDOPolicies | Checks both EOP and MDO (Safe Attachment and Safe Links) policies for user(s) specified in the CSV file or EmailAddress parameter. +OnlyMDOPolicies | Checks only MDO (Safe Attachment and Safe Links) policies for user(s) specified in the CSV file or EmailAddress parameter. +ShowDetailedPolicies | In addition to the policy applied, show any policy details that are set to True, On, or not blank. +SkipConnectionCheck | Skips connection check for Graph and Exchange Online. +SkipVersionCheck | Skips the version check of the script. +ScriptUpdateOnly | Just updates script version to latest one. diff --git a/mkdocs.yml b/mkdocs.yml index 4c1c58431e..dce6bec2f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,8 @@ nav: - Hybrid: - Test-HMAEAS: Hybrid/Test-HMAEAS.md - M365: + - MDO: + - MDOThreatPolicyChecker: M365/MDO/MDOThreatPolicyChecker.md - DLT365Groupsupgrade: M365/DLT365Groupsupgrade.md - Performance: - ExPerfWiz: Performance/ExPerfWiz.md From b7c1587cb0a72daa689a91df5502f14370e37d27 Mon Sep 17 00:00:00 2001 From: Ignacio Serrano <103440830+iserrano76@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:39:24 +0200 Subject: [PATCH 2/7] Update with David Requested Changes --- M365/MDO/MDOThreatPolicyChecker.ps1 | 107 +++++++++++++--------------- 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/M365/MDO/MDOThreatPolicyChecker.ps1 b/M365/MDO/MDOThreatPolicyChecker.ps1 index 4c6998fc3a..e239c3afdf 100644 --- a/M365/MDO/MDOThreatPolicyChecker.ps1 +++ b/M365/MDO/MDOThreatPolicyChecker.ps1 @@ -121,7 +121,7 @@ begin { try { $group = Get-MgGroup -Filter "mail eq '$stGroupEmail'" -ErrorAction Stop } catch { - Write-Host "Error getting group $stGroupEmail`: $_" -ForegroundColor Red + Write-Host "Error getting group $stGroupEmail`:`n$_" -ForegroundColor Red return $null } @@ -134,7 +134,7 @@ begin { # Return the Object ID of the group return $group.Id } else { - Write-Host "Wrong type for $($group.ToString()): $group.Id.GetType().Name" -ForegroundColor Red + Write-Host "Wrong type for $($group.ToString()): $($group.Id.GetType().Name)" -ForegroundColor Red return $null } } else { @@ -170,7 +170,7 @@ begin { try { $groupMembers = Get-MgGroupMember -GroupId $GroupObjectId -ErrorAction Stop } catch { - Write-Host "Error getting group members for $GroupObjectId`: $_" -ForegroundColor Red + Write-Host "Error getting group members for $GroupObjectId`:`n$_" -ForegroundColor Red return $null } @@ -185,7 +185,7 @@ begin { try { $user = Get-MgUser -UserId $member.Id -ErrorAction Stop } catch { - Write-Host "Error getting user with Id $($member.Id): $_" -ForegroundColor Red + Write-Host "Error getting user with Id $($member.Id):`n$_" -ForegroundColor Red return $null } # Compare the user's email address with the $email parameter @@ -237,31 +237,30 @@ begin { $tempAddress = [MailAddress]$EmailAddress } catch { Write-Host "The EmailAddress $EmailAddress cannot be validated. Please provide a valid email address." -ForegroundColor Red - return $null - } - $recipient = $null - Write-Verbose "Getting $EmailAddress" - try { - $recipient = Get-EXORecipient $EmailAddress -ErrorAction Stop - } catch { - Write-Host "Error getting recipient $EmailAddress`: $_" -ForegroundColor Red + Write-Host "Error details:`n$_" -ForegroundColor Red return $null } - if ($null -eq $recipient) { - Write-Host "$EmailAddress is not a recipient in this tenant" -ForegroundColor Red - return $null - } else { - $domain = $tempAddress.Host - Write-Verbose "Checking domain $domain" - if ($AcceptedDomains -contains $domain) { - Write-Verbose "Verified domain $domain for $tempAddress" - return $tempAddress - } else { - Write-Host "The domain $domain is not an accepted domain in your organization. Please provide a valid email address: $tempAddress " -ForegroundColor Red - return $null + $domain = $tempAddress.Host + Write-Verbose "Checking domain $domain" + if ($AcceptedDomains -contains $domain) { + Write-Verbose "Verified domain $domain for $tempAddress" + $recipient = $null + Write-Verbose "Getting $EmailAddress" + try { + $recipient = Get-EXORecipient $EmailAddress -ErrorAction Stop + if ($null -eq $recipient) { + Write-Host "$EmailAddress is not a recipient in this tenant" -ForegroundColor Red + } else { + return $tempAddress + } + } catch { + Write-Host "Error getting recipient $EmailAddress`:`n$_" -ForegroundColor Red } + } else { + Write-Host "The domain $domain is not an accepted domain in your organization. Please provide a valid email address: $tempAddress " -ForegroundColor Red } + return $null } # Function to check rules @@ -348,7 +347,6 @@ begin { } $temp = $Email.Host - while ($temp.IndexOf(".") -gt 0) { if ($temp -in $domainsIs) { Write-Verbose "domainInRule: $temp" @@ -363,32 +361,23 @@ begin { # Check for explicit inclusion in any user, group, or domain that are not empty, and account for 3 empty inclusions # Also check for any exclusions as user, group, or domain. Nulls don't need to be accounted for and this is an OR condition for exclusions - if ((($emailInRule -or (-not $senderOrReceiver)) -and - ($domainInRule -or (-not $domainsIs)) -and - ($groupInRule -or (-not $memberOf))) -and - ($emailInRule -or $domainInRule -or $groupInRule)) { - if ((-not $emailExceptionInRule) -and - (-not $groupExceptionInRule) -and - (-not $domainExceptionInRule)) { - Write-Verbose "Return Rule $($rule.Name)" - Write-Verbose "emailInRule: $emailInRule domainInRule: $domainInRule groupInRule: $groupInRule " - Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " - return $rule - } + if (((($emailInRule -or (-not $senderOrReceiver)) -and ($domainInRule -or (-not $domainsIs)) -and ($groupInRule -or (-not $memberOf))) -and + ($emailInRule -or $domainInRule -or $groupInRule)) -and + ((-not $emailExceptionInRule) -and (-not $groupExceptionInRule) -and (-not $domainExceptionInRule))) { + Write-Verbose "Return Rule $($rule.Name)" + Write-Verbose "emailInRule: $emailInRule domainInRule: $domainInRule groupInRule: $groupInRule " + Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " + return $rule } - if (-not $Outbound) { - # Check for implicit inclusion (no mailboxes included at all), which is possible for Presets and SA/SL. They are included if not explicitly excluded. - if ((-not $senderOrReceiver) -and (-not $domainsIs) -and (-not $memberOf)) { - if ((-not $emailExceptionInRule) -and - (-not $groupExceptionInRule) -and - (-not $domainExceptionInRule)) { - Write-Verbose "Return Rule $($rule.Name)" - Write-Verbose "senderOrReceiver: $senderOrReceiver domainsIs: $domainsIs memberOf: $memberOf " - Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " - return $rule - } - } + # Check for implicit inclusion (no mailboxes included at all), which is possible for Presets and SA/SL. They are included if not explicitly excluded. Only inbound + if ((-not $Outbound) -and + (((-not $senderOrReceiver) -and (-not $domainsIs) -and (-not $memberOf)) -and + ((-not $emailExceptionInRule) -and (-not $groupExceptionInRule) -and (-not $domainExceptionInRule)))) { + Write-Verbose "Return Rule $($rule.Name)" + Write-Verbose "senderOrReceiver: $senderOrReceiver domainsIs: $domainsIs memberOf: $memberOf " + Write-Verbose "emailExceptionInRule: $emailExceptionInRule groupExceptionInRule: $groupExceptionInRule domainExceptionInRule: $domainExceptionInRule " + return $rule } } return $null @@ -400,14 +389,14 @@ begin { $Policy ) Write-Host "`n`tProperties of the policy that are True, On, or not blank:" - $excludedProperties = 'Identity', 'Id', 'Name', 'ExchangeVersion', 'DistinguishedName', 'ObjectCategory', 'ObjectClass', 'WhenChanged', 'WhenCreated', ` - 'WhenChangedUTC', 'WhenCreatedUTC', 'ExchangeObjectId', 'OrganizationalUnitRoot', 'OrganizationId', 'OriginatingServer', 'ObjectState', 'Priority', 'ImmutableId', ` - 'Description', 'HostedContentFilterPolicy', 'AntiPhishPolicy', 'MalwareFilterPolicy', 'SafeAttachmentPolicy', 'SafeLinksPolicy', 'HostedOutboundSpamFilterPolicy' + $excludedProperties = 'Identity', 'Id', 'Name', 'ExchangeVersion', 'DistinguishedName', 'ObjectCategory', 'ObjectClass', 'WhenChanged', 'WhenCreated', + 'WhenChangedUTC', 'WhenCreatedUTC', 'ExchangeObjectId', 'OrganizationalUnitRoot', 'OrganizationId', 'OriginatingServer', 'ObjectState', 'Priority', 'ImmutableId', + 'Description', 'HostedContentFilterPolicy', 'AntiPhishPolicy', 'MalwareFilterPolicy', 'SafeAttachmentPolicy', 'SafeLinksPolicy', 'HostedOutboundSpamFilterPolicy' $Policy.PSObject.Properties | ForEach-Object { - if ($null -ne $_.Value -and ` - (($_.Value.GetType() -eq [Boolean] -and $_.Value -eq $true) ` - -or ($_.Value -ne '{}' -and $_.Value -ne 'Off' -and $_.Value -ne $true -and $_.Value -ne '' -and $excludedProperties -notcontains $_.Name))) { + if ($null -ne $_.Value -and + (($_.Value.GetType() -eq [Boolean] -and $_.Value -eq $true) -or + ($_.Value -ne '{}' -and $_.Value -ne 'Off' -and $_.Value -ne $true -and $_.Value -ne '' -and $excludedProperties -notcontains $_.Name))) { Write-Host "`t`t$($_.Name): $($_.Value)" } else { Write-Verbose "`t`tExcluded property:$($_.Name): $($_.Value)" @@ -515,7 +504,7 @@ process { try { $exoConnection = Get-ConnectionInformation -ErrorAction Stop } catch { - Write-Host "Error checking EXO connection: $_" -ForegroundColor Red + Write-Host "Error checking EXO connection:`n$_" -ForegroundColor Red Write-Host "Verify that you have ExchangeOnlineManagement module installed" -ForegroundColor Yellow Write-Host "You need a connection To Exchange Online, you can use:" -ForegroundColor Yellow Write-Host "Connect-ExchangeOnline" -ForegroundColor Yellow @@ -546,7 +535,7 @@ process { try { $graphConnection = Get-MgContext -ErrorAction Stop } catch { - Write-Host "Error checking Graph connection: $_" -ForegroundColor Red + Write-Host "Error checking Graph connection:`n$_" -ForegroundColor Red Write-Host "Verify that you have Microsoft.Graph.Users and Microsoft.Graph.Groups modules installed and loaded" -ForegroundColor Yellow Write-Host "You could use:" -ForegroundColor Yellow Write-Host "Connect-MgGraph -Scopes 'Group.Read.All','User.Read.All'" -ForegroundColor Yellow @@ -660,7 +649,7 @@ process { exit } } catch { - Write-Host "Error importing CSV file: $_" -ForegroundColor Red + Write-Host "Error importing CSV file:`n$_" -ForegroundColor Red exit } } @@ -669,7 +658,7 @@ process { try { $acceptedDomains = Get-AcceptedDomain -ErrorAction Stop } catch { - Write-Host "Error getting Accepted Domains: $_" -ForegroundColor Red + Write-Host "Error getting Accepted Domains:`n$_" -ForegroundColor Red exit } From d941ce168ac1a941c74c2fbc37b46710f09698cb Mon Sep 17 00:00:00 2001 From: Ignacio Serrano <103440830+iserrano76@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:34:49 +0200 Subject: [PATCH 3/7] Removed Imports --- M365/MDO/MDOThreatPolicyChecker.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/M365/MDO/MDOThreatPolicyChecker.ps1 b/M365/MDO/MDOThreatPolicyChecker.ps1 index e239c3afdf..8bab8d90f5 100644 --- a/M365/MDO/MDOThreatPolicyChecker.ps1 +++ b/M365/MDO/MDOThreatPolicyChecker.ps1 @@ -466,9 +466,6 @@ begin { Write-DebugLog $message } - Import-Module Microsoft.Graph.Authentication - Import-Module ExchangeOnlineManagement - $LogFileName = "MDOThreatPolicyChecker" $StartDate = Get-Date $StartDateFormatted = ($StartDate).ToString("yyyyMMddhhmmss") From dba1218240619c4280a6a8afa661e5337f18689c Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Fri, 12 Jul 2024 11:57:37 -0700 Subject: [PATCH 4/7] Change Get-MB to Get-Recipient --- Calendar/Get-RBASummary.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index 82884e23d5..ce26bea7b0 100644 --- a/Calendar/Get-RBASummary.ps1 +++ b/Calendar/Get-RBASummary.ps1 @@ -283,10 +283,10 @@ function OutputMBList { $Org = $Identity.Split('@')[1] if ($null -ne $Org) { - $User = Get-Mailbox -Identity $User -organization $Org + $User = Get-Recipient -Identity $User -organization $Org Write-Host " `t `t [$($User.DisplayName)] -- $($User.PrimarySmtpAddress)" } else { - $User = Get-Mailbox -Identity $User + $User = Get-Recipient -Identity $User Write-Host " `t `t [$($User.DisplayName)] -- $($User.PrimarySmtpAddress)" } } From 91d43a97883b4afb04ec397c951799cf29a105bb Mon Sep 17 00:00:00 2001 From: DKhrebin <43005759+DKhrebin@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:55:54 +0100 Subject: [PATCH 5/7] Update Test-ExchAVExclusions.ps1 Nice to have file version to recognize outdated 3rd party software. --- Diagnostics/AVTester/Test-ExchAVExclusions.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 index dcd66cf7f7..262a46fc7f 100644 --- a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 +++ b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 @@ -466,7 +466,7 @@ while ($currentDiff -gt 0) { if ($ProcessModules.count -gt 0) { foreach ($module in $ProcessModules) { - $OutString = ("PROCESS: $($process.ProcessName) PID($($process.Id)) UNEXPECTED MODULE: $($module.ModuleName) COMPANY: $($module.Company)`n`tPATH: $($module.FileName)") + $OutString = ("PROCESS: $($process.ProcessName) PID($($process.Id)) UNEXPECTED MODULE: $($module.ModuleName) COMPANY: $($module.Company)`n`tPATH: $($module.FileName)`n`tFileVersion: $($module.FileVersion)") Write-Host "[FAIL] - $OutString" -ForegroundColor Red if ($process.MainModule.ModuleName -eq "W3wp.exe") { $SuspiciousW3wpProcessList += $OutString From 48420007f8ded82146c7bf8146673be2be949126 Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Thu, 11 Jul 2024 15:22:21 -0700 Subject: [PATCH 6/7] Fix PS errors caused by ImportExcel not be installed, minor tweaks fix client names License Add Sensitivity Use Get-Recipient where we can instead of Get-MB Fix Room MB detection when no PII Formatting --- Calendar/CalLogHelpers/CalLogCSVFunctions.ps1 | 1 + .../CalLogHelpers/CalLogExportFunctions.ps1 | 207 ----------------- .../CalLogHelpers/CalLogInfoFunctions.ps1 | 14 +- .../CalLogHelpers/ExcelModuleInstaller.ps1 | 3 +- .../CalLogHelpers/ExportToExcelFunctions.ps1 | 212 ++++++++++++++++++ Calendar/CalLogHelpers/Invoke-GetCalLogs.ps1 | 1 + Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 | 81 ++++--- .../ShortClientNameFunctions.ps1 | 5 + Calendar/CalLogHelpers/TimelineFunctions.ps1 | 5 + Calendar/Check-SharingStatus.ps1 | 4 +- .../Get-CalendarDiagnosticObjectsSummary.ps1 | 10 +- 11 files changed, 292 insertions(+), 251 deletions(-) create mode 100644 Calendar/CalLogHelpers/ExportToExcelFunctions.ps1 diff --git a/Calendar/CalLogHelpers/CalLogCSVFunctions.ps1 b/Calendar/CalLogHelpers/CalLogCSVFunctions.ps1 index b5bbd0282d..10b10d8e6b 100644 --- a/Calendar/CalLogHelpers/CalLogCSVFunctions.ps1 +++ b/Calendar/CalLogHelpers/CalLogCSVFunctions.ps1 @@ -19,6 +19,7 @@ $script:CalendarItemTypes = @{ 'IPM.Schedule.Meeting.Resp.Neg' = "Resp.Neg" 'IPM.Schedule.Meeting.Resp.Tent' = "Resp.Tent" 'IPM.Schedule.Meeting.Resp.Pos' = "Resp.Pos" + '(Occurrence Deleted)' = "Exception.Deleted" } # =================================================================================================== diff --git a/Calendar/CalLogHelpers/CalLogExportFunctions.ps1 b/Calendar/CalLogHelpers/CalLogExportFunctions.ps1 index bcf74bc913..469d451527 100644 --- a/Calendar/CalLogHelpers/CalLogExportFunctions.ps1 +++ b/Calendar/CalLogHelpers/CalLogExportFunctions.ps1 @@ -59,22 +59,6 @@ function Export-CalLogCSV { $script:GCDO | Export-Csv -Path $FilenameRaw -NoTypeInformation -Encoding UTF8 } -# Export to Excel -function Export-CalLogExcel { - Write-Host -ForegroundColor Cyan "Exporting Enhanced CalLogs to Excel Tab [$ShortId]..." - $ExcelParamsArray = GetExcelParams -path $FileName -tabName $ShortId - - $excel = $GCDOResults | Export-Excel @ExcelParamsArray -PassThru - - FormatHeader ($excel) - - Export-Excel -ExcelPackage $excel -WorksheetName $ShortId -MoveToStart - - # Export Raw Logs for Developer Analysis - Write-Host -ForegroundColor Cyan "Exporting Raw CalLogs to Excel Tab [$($ShortId + "_Raw")]..." - $script:GCDO | Export-Excel -Path $FileName -WorksheetName $($ShortId + "_Raw") -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -} - function Export-Timeline { Write-Verbose "Export to Excel is : $ExportToExcel" @@ -88,194 +72,3 @@ function Export-Timeline { $script:TimeLineOutput | Export-Csv -Path $script:TimeLineFilename -NoTypeInformation -Encoding UTF8 -Append } } - -function Export-TimelineExcel { - Write-Host -ForegroundColor Cyan "Exporting Timeline to Excel..." - $script:TimeLineOutput | Export-Excel -Path $FileName -WorksheetName $($ShortId + "_TimeLine") -Title "Timeline for $Identity" -AutoSize -FreezeTopRow -BoldTopRow -} - -function GetExcelParams($path, $tabName) { - if ($script:IsOrganizer) { - $TableStyle = "Light10" # Orange for Organizer - $TitleExtra = ", Organizer" - } elseif ($script:IsRoomMB) { - Write-Host -ForegroundColor green "Room Mailbox Detected" - $TableStyle = "Light11" # Green for Room Mailbox - $TitleExtra = ", Resource" - } else { - $TableStyle = "Light12" # Light Blue for normal - # Dark Blue for Delegates (once we can determine this) - } - - return @{ - Path = $path - FreezeTopRow = $true - # BoldTopRow = $true - Verbose = $false - TableStyle = $TableStyle - WorksheetName = $tabName - TableName = $tabName - FreezeTopRowFirstColumn = $true - AutoFilter = $true - AutoNameRange = $true - Append = $true - Title = "Enhanced Calendar Logs for $Identity" + $TitleExtra + " for MeetingID [$($script:GCDO[0].CleanGlobalObjectId)]." - TitleSize = 14 - ConditionalText = $ConditionalFormatting - } -} - -# Need better way of tagging cells than the Range. Every time one is updated, you need to update all the ones after it. -$ConditionalFormatting = $( - # Client, ShortClientInfoString and LogClientInfoString - New-ConditionalText "Outlook" -ConditionalTextColor Green -BackgroundColor $null - New-ConditionalText "OWA" -ConditionalTextColor DarkGreen -BackgroundColor $null - New-ConditionalText "Transport" -ConditionalTextColor Blue -BackgroundColor $null - New-ConditionalText "Repair" -ConditionalTextColor DarkRed -BackgroundColor LightPink - New-ConditionalText "Other ?BA" -ConditionalTextColor Orange -BackgroundColor $null - New-ConditionalText "Other REST" -ConditionalTextColor DarkRed -BackgroundColor $null - New-ConditionalText "ResourceBookingAssistant" -ConditionalTextColor Blue -BackgroundColor $null - - #LogType - New-ConditionalText -Range "C3:C9999" -ConditionalType ContainsText -Text "Ignorable" -ConditionalTextColor DarkRed -BackgroundColor $null - New-ConditionalText -Range "C:C" -ConditionalType ContainsText -Text "Cleanup" -ConditionalTextColor DarkRed -BackgroundColor $null - New-ConditionalText -Range "C:C" -ConditionalType ContainsText -Text "Sharing" -ConditionalTextColor Blue -BackgroundColor $null - - # TriggerAction - New-ConditionalText -Range "G:G" -ConditionalType ContainsText -Text "Create" -ConditionalTextColor Green -BackgroundColor $null - New-ConditionalText -Range "G:G" -ConditionalType ContainsText -Text "Delete" -ConditionalTextColor Red -BackgroundColor $null - # ItemClass - New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "IPM.Appointment" -ConditionalTextColor Blue -BackgroundColor $null - New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "Canceled" -ConditionalTextColor Black -BackgroundColor Orange - New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text ".Request" -ConditionalTextColor DarkGreen -BackgroundColor $null - New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text ".Resp." -ConditionalTextColor Orange -BackgroundColor $null - New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "IPM.OLE.CLASS" -ConditionalTextColor Plum -BackgroundColor $null - - #FreeBusyStatus - New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Free" -ConditionalTextColor Red -BackgroundColor $null - New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Tentative" -ConditionalTextColor Orange -BackgroundColor $null - New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Busy" -ConditionalTextColor Green -BackgroundColor $null - - #Shared Calendar information - New-ConditionalText -Range "Q3:Q9999" -ConditionalType NotEqual -Text "Not Shared" -ConditionalTextColor Blue -BackgroundColor $null - New-ConditionalText -Range "R:R" -ConditionalType ContainsText -Text "TRUE" -ConditionalTextColor Blue -BackgroundColor $null - New-ConditionalText -Range "S:S" -ConditionalType NotEqual -Text "NotFound" -ConditionalTextColor Blue -BackgroundColor $null - - #MeetingRequestType - New-ConditionalText -Range "V:V" -ConditionalType ContainsText -Text "Outdated" -ConditionalTextColor DarkRed -BackgroundColor LightPink - - #AppointmentAuxiliaryFlags - New-ConditionalText -Range "AE3:AE9999" -ConditionalType ContainsText -Text "Copy" -ConditionalTextColor DarkRed -BackgroundColor LightPink - - #ResponseType - New-ConditionalText -Range "AI3:AI9999" -ConditionalType ContainsText -Text "Organizer" -ConditionalTextColor Orange -BackgroundColor $null - -) - -function FormatHeader { - param( - [object] $excel - ) - $sheet = $excel.Workbook.Worksheets[$ShortId] - $HeaderRow = 2 - $n = 0 - - # Static List of Columns for now... - $sheet.Column(++$n) | Set-ExcelRange -Width 6 -HorizontalAlignment center # LogRow - Set-CellComment -Text "This is the Enhanced Calendar Logs for [$Identity] for MeetingID `n [$($script:GCDO[0].CleanGlobalObjectId)]." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center #LogTimestamp - Set-CellComment -Text "LogTimestamp: Time when the change was recorded in the CalLogs. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 11 -HorizontalAlignment center # LogType - Set-CellComment -Text "LogType: Can this Log be safely ignored?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # SubjectProperty - Set-CellComment -Text "SubjectProperty: The Subject of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Client - Set-CellComment -Text "Client: The 'friendly' Client name of the client that made the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 5 -HorizontalAlignment Left # LogClientInfoString - Set-CellComment -Text "LogClientInfoString: Full Client Info String of client that made the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 12 -HorizontalAlignment Center # TriggerAction - Set-CellComment -Text "TriggerAction: The action that caused the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 18 -HorizontalAlignment Left # ItemClass - Set-CellComment -Text "ItemClass: The Class of the Calendar Item" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # Seq:Exp:ItemVersion - Set-CellComment -Text "Seq:Exp:ItemVersion: The Sequence Version, the Exception Version, and the Item Version. Each type of item has its own count." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Organizer - Set-CellComment -Text "Organizer: The Organizer of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # From - Set-CellComment -Text "From: The SMTP address of the Organizer of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 12 -HorizontalAlignment center # FreeBusyStatus - Set-CellComment -Text "FreeBusyStatus: The FreeBusy Status of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # ResponsibleUser - Set-CellComment -Text "ResponsibleUser: The Responsible User of the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Sender - Set-CellComment -Text "Sender: The Sender of the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 16 -HorizontalAlignment Left # LogFolder - Set-CellComment -Text "LogFolder: The Log Folder that the CalLog was in." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 16 -HorizontalAlignment Left # OriginalLogFolder - Set-CellComment -Text "OriginalLogFolder: The Original Log Folder that the item was in / delivered to." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 15 -HorizontalAlignment Right # SharedFolderName - Set-CellComment -Text "SharedFolderName: Was this from a Modern Sharing, and if so what Folder." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsFromSharedCalendar - Set-CellComment -Text "IsFromSharedCalendar: Is this CalLog from a Modern Sharing relationship?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # ExternalSharingMasterId - Set-CellComment -Text "ExternalSharingMasterId: If this is not [NotFound], then it is from a Modern Sharing relationship." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # ReceivedBy - Set-CellComment -Text "ReceivedBy: The Receiver of the Calendar Item. Should always be the owner of the Mailbox." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # ReceivedRepresenting - Set-CellComment -Text "ReceivedRepresenting: Who the item was Received for, of then the Delegate." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # MeetingRequestType - Set-CellComment -Text "MeetingRequestType: The Meeting Request Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center # StartTime - Set-CellComment -Text "StartTime: The Start Time of the Meeting. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center # EndTime - Set-CellComment -Text "EndTime: The End Time of the Meeting. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 17 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment Left # OriginalStartDate - Set-CellComment -Text "OriginalStartDate: The Original Start Date of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # TimeZone - Set-CellComment -Text "TimeZone: The Time Zone of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # Location - Set-CellComment -Text "Location: The Location of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # CalendarItemType - Set-CellComment -Text "CalendarItemType: The Calendar Item Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsException - Set-CellComment -Text "IsException: Is this an Exception?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # RecurrencePattern - Set-CellComment -Text "RecurrencePattern: The Recurrence Pattern of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Center # AppointmentAuxiliaryFlags - Set-CellComment -Text "AppointmentAuxiliaryFlags: The Appointment Auxiliary Flags of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Left # DisplayAttendeesAll - Set-CellComment -Text "DisplayAttendeesAll: List of the Attendees of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # AttendeeCount - Set-CellComment -Text "AttendeeCount: The Attendee Count." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # AppointmentState - Set-CellComment -Text "AppointmentState: The Appointment State of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # ResponseType - Set-CellComment -Text "ResponseType: The Response Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment center # ClientIntent - Set-CellComment -Text "ClientIntent: The Client Intent of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # AppointmentRecurring - Set-CellComment -Text "AppointmentRecurring: Is this a Recurring Meeting?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # HasAttachment - Set-CellComment -Text "HasAttachment: Does this Meeting have an Attachment?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsCancelled - Set-CellComment -Text "IsCancelled: Is this Meeting Cancelled?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsAllDayEvent - Set-CellComment -Text "IsAllDayEvent: Is this an All Day Event?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsSeriesCancelled - Set-CellComment -Text "IsSeriesCancelled: Is this a Series Cancelled Meeting?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Left # SendMeetingMessagesDiagnostics - Set-CellComment -Text "SendMeetingMessagesDiagnostics: Compound Property to describe why meeting was or was not sent to everyone." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 50 -HorizontalAlignment Left # AttendeeCollection - Set-CellComment -Text "AttendeeCollection: The Attendee Collection of the Meeting, use -TrackingLogs to get values." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 40 -HorizontalAlignment Center # CalendarLogRequestId - Set-CellComment -Text "CalendarLogRequestId: The Calendar Log Request ID of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - $sheet.Column(++$n) | Set-ExcelRange -Width 120 -HorizontalAlignment Left # CleanGlobalObjectId - Set-CellComment -Text "CleanGlobalObjectId: The Clean Global Object ID of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet - - # Update header rows after all the others have been set. - # Title Row - $sheet.Row(1) | Set-ExcelRange -HorizontalAlignment Left - - # Set the Header row to be bold and left aligned - $sheet.Row($HeaderRow) | Set-ExcelRange -Bold -HorizontalAlignment Left -} diff --git a/Calendar/CalLogHelpers/CalLogInfoFunctions.ps1 b/Calendar/CalLogHelpers/CalLogInfoFunctions.ps1 index 90d36ce7ef..fccfcd6897 100644 --- a/Calendar/CalLogHelpers/CalLogInfoFunctions.ps1 +++ b/Calendar/CalLogHelpers/CalLogInfoFunctions.ps1 @@ -32,24 +32,22 @@ function SetIsRoom { param( $CalLogs ) - [bool] $IsRoom = $false + # See if we have already determined this is a Room MB. if ($script:Rooms -contains $Identity) { - $IsRoom = $true - return $IsRoom + return $true } # Simple logic is if RBA is running on the MB, it is a Room MB, otherwise it is not. foreach ($CalLog in $CalLogs) { - Write-Verbose "Checking if this is a Room Mailbox. [$($CalLog.ItemType)] [$($CalLog.ExternalSharingMasterId)] [$($CalLog.LogClientInfoString)]" - if ($CalLog.ItemType -eq "IPM.Appointment" -and + Write-Verbose "Checking if this is a Room Mailbox. [$($CalLog.ItemClass)] [$($CalLog.ExternalSharingMasterId)] [$($CalLog.LogClientInfoString)]" + if ($CalLog.ItemClass -eq "IPM.Appointment" -and $CalLog.ExternalSharingMasterId -eq "NotFound" -and $CalLog.LogClientInfoString -like "*ResourceBookingAssistant*" ) { - $IsRoom = $true - return $IsRoom + return $true } } - return $IsRoom + return $false } <# diff --git a/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 b/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 index bac893f8af..3f542ee0c6 100644 --- a/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 +++ b/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 @@ -2,7 +2,8 @@ # Licensed under the MIT License. # =================================================================================================== -# Excel Functions +# ImportExcel Functions +# see https://github.com/dfinke/ImportExcel for information on the module. # =================================================================================================== function CheckExcelModuleInstalled { [CmdletBinding(SupportsShouldProcess=$true)] diff --git a/Calendar/CalLogHelpers/ExportToExcelFunctions.ps1 b/Calendar/CalLogHelpers/ExportToExcelFunctions.ps1 new file mode 100644 index 0000000000..b7153135a1 --- /dev/null +++ b/Calendar/CalLogHelpers/ExportToExcelFunctions.ps1 @@ -0,0 +1,212 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Export to Excel +function Export-CalLogExcel { + Write-Host -ForegroundColor Cyan "Exporting Enhanced CalLogs to Excel Tab [$ShortId]..." + $ExcelParamsArray = GetExcelParams -path $FileName -tabName $ShortId + + $excel = $GCDOResults | Export-Excel @ExcelParamsArray -PassThru + + FormatHeader ($excel) + + Export-Excel -ExcelPackage $excel -WorksheetName $ShortId -MoveToStart + + # Export Raw Logs for Developer Analysis + Write-Host -ForegroundColor Cyan "Exporting Raw CalLogs to Excel Tab [$($ShortId + "_Raw")]..." + $script:GCDO | Export-Excel -Path $FileName -WorksheetName $($ShortId + "_Raw") -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd +} + +function Export-TimelineExcel { + Write-Host -ForegroundColor Cyan "Exporting Timeline to Excel..." + $script:TimeLineOutput | Export-Excel -Path $FileName -WorksheetName $($ShortId + "_TimeLine") -Title "Timeline for $Identity" -AutoSize -FreezeTopRow -BoldTopRow +} + +function GetExcelParams($path, $tabName) { + if ($script:IsOrganizer) { + $TableStyle = "Light10" # Orange for Organizer + $TitleExtra = ", Organizer" + } elseif ($script:IsRoomMB) { + Write-Host -ForegroundColor green "Room Mailbox Detected" + $TableStyle = "Light11" # Green for Room Mailbox + $TitleExtra = ", Resource" + } else { + $TableStyle = "Light12" # Light Blue for normal + # Dark Blue for Delegates (once we can determine this) + } + + if ($script:CalLogsDisabled) { + $TitleExtra += ", WARNING: CalLogs are Turned Off for $Identity! This will be a incomplete story" + } + + return @{ + Path = $path + FreezeTopRow = $true + # BoldTopRow = $true + Verbose = $false + TableStyle = $TableStyle + WorksheetName = $tabName + TableName = $tabName + FreezeTopRowFirstColumn = $true + AutoFilter = $true + AutoNameRange = $true + Append = $true + Title = "Enhanced Calendar Logs for $Identity" + $TitleExtra + " for MeetingID [$($script:GCDO[0].CleanGlobalObjectId)]." + TitleSize = 14 + ConditionalText = $ConditionalFormatting + } +} + +# Need better way of tagging cells than the Range. Every time one is updated, you need to update all the ones after it. +$ConditionalFormatting = $( + # Client, ShortClientInfoString and LogClientInfoString + New-ConditionalText "Outlook" -ConditionalTextColor Green -BackgroundColor $null + New-ConditionalText "OWA" -ConditionalTextColor DarkGreen -BackgroundColor $null + New-ConditionalText "Transport" -ConditionalTextColor Blue -BackgroundColor $null + New-ConditionalText "Repair" -ConditionalTextColor DarkRed -BackgroundColor LightPink + New-ConditionalText "Other ?BA" -ConditionalTextColor Orange -BackgroundColor $null + New-ConditionalText "Other REST" -ConditionalTextColor DarkRed -BackgroundColor $null + New-ConditionalText "ResourceBookingAssistant" -ConditionalTextColor Blue -BackgroundColor $null + + #LogType + New-ConditionalText -Range "C3:C9999" -ConditionalType ContainsText -Text "Ignorable" -ConditionalTextColor DarkRed -BackgroundColor $null + New-ConditionalText -Range "C:C" -ConditionalType ContainsText -Text "Cleanup" -ConditionalTextColor DarkRed -BackgroundColor $null + New-ConditionalText -Range "C:C" -ConditionalType ContainsText -Text "Sharing" -ConditionalTextColor Blue -BackgroundColor $null + + # TriggerAction + New-ConditionalText -Range "G:G" -ConditionalType ContainsText -Text "Create" -ConditionalTextColor Green -BackgroundColor $null + New-ConditionalText -Range "G:G" -ConditionalType ContainsText -Text "Delete" -ConditionalTextColor Red -BackgroundColor $null + # ItemClass + New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "IPM.Appointment" -ConditionalTextColor Blue -BackgroundColor $null + New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "Cancellation" -ConditionalTextColor Black -BackgroundColor Orange + New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text ".Request" -ConditionalTextColor DarkGreen -BackgroundColor $null + New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text ".Resp." -ConditionalTextColor Orange -BackgroundColor $null + New-ConditionalText -Range "H:H" -ConditionalType ContainsText -Text "IPM.OLE.CLASS" -ConditionalTextColor Plum -BackgroundColor $null + + #FreeBusyStatus + New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Free" -ConditionalTextColor Red -BackgroundColor $null + New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Tentative" -ConditionalTextColor Orange -BackgroundColor $null + New-ConditionalText -Range "L3:L9999" -ConditionalType ContainsText -Text "Busy" -ConditionalTextColor Green -BackgroundColor $null + + #Shared Calendar information + New-ConditionalText -Range "Q3:Q9999" -ConditionalType NotEqual -Text "Not Shared" -ConditionalTextColor Blue -BackgroundColor $null + New-ConditionalText -Range "R:R" -ConditionalType ContainsText -Text "TRUE" -ConditionalTextColor Blue -BackgroundColor $null + New-ConditionalText -Range "S:S" -ConditionalType NotEqual -Text "NotFound" -ConditionalTextColor Blue -BackgroundColor $null + + #MeetingRequestType + New-ConditionalText -Range "V:V" -ConditionalType ContainsText -Text "Outdated" -ConditionalTextColor DarkRed -BackgroundColor LightPink + + #AppointmentAuxiliaryFlags + New-ConditionalText -Range "AE3:AE9999" -ConditionalType ContainsText -Text "Copied" -ConditionalTextColor DarkRed -BackgroundColor LightPink + + #ResponseType + New-ConditionalText -Range "AI3:AI9999" -ConditionalType ContainsText -Text "Organizer" -ConditionalTextColor Orange -BackgroundColor $null +) + +function FormatHeader { + param( + [object] $excel + ) + $sheet = $excel.Workbook.Worksheets[$ShortId] + $HeaderRow = 2 + $n = 0 + + # Static List of Columns for now... + $sheet.Column(++$n) | Set-ExcelRange -Width 6 -HorizontalAlignment center # LogRow + Set-CellComment -Text "This is the Enhanced Calendar Logs for [$Identity] for MeetingID `n [$($script:GCDO[0].CleanGlobalObjectId)]." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center #LogTimestamp + Set-CellComment -Text "LogTimestamp: Time when the change was recorded in the CalLogs. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 11 -HorizontalAlignment center # LogType + Set-CellComment -Text "LogType: Grouping of logs so ignorable ones can be quickly filtered?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # SubjectProperty + Set-CellComment -Text "SubjectProperty: The Subject of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Client + Set-CellComment -Text "Client (ShortClientInfoString): The 'friendly' Client name of the client that made the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 5 -HorizontalAlignment Left # LogClientInfoString + Set-CellComment -Text "LogClientInfoString: Full Client Info String of client that made the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 12 -HorizontalAlignment Center # TriggerAction + Set-CellComment -Text "TriggerAction (CalendarLogTriggerAction): The type of action that caused the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 18 -HorizontalAlignment Left # ItemClass + Set-CellComment -Text "ItemClass: The Class of the Calendar Item" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # Seq:Exp:ItemVersion + Set-CellComment -Text "Seq:Exp:ItemVersion (AppointmentLastSequenceNumber:AppointmentSequenceNumber:ItemVersion): The Sequence Version, the Exception Version, and the Item Version. Each type of item has its own count." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Organizer + Set-CellComment -Text "Organizer (From.FriendlyDisplayName): The Organizer of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # From + Set-CellComment -Text "From: The SMTP address of the Organizer of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 12 -HorizontalAlignment center # FreeBusyStatus + Set-CellComment -Text "FreeBusy (FreeBusyStatus): The FreeBusy Status of the Calendar Item." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # ResponsibleUser + Set-CellComment -Text "ResponsibleUser(ResponsibleUserName): The Responsible User of the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # Sender + Set-CellComment -Text "Sender (SenderEmailAddress): The Sender of the change." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 16 -HorizontalAlignment Left # LogFolder + Set-CellComment -Text "LogFolder (ParentDisplayName): The Log Folder that the CalLog was in." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 16 -HorizontalAlignment Left # OriginalLogFolder + Set-CellComment -Text "OriginalLogFolder (OriginalParentDisplayName): The Original Log Folder that the item was in / delivered to." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 15 -HorizontalAlignment Right # SharedFolderName + Set-CellComment -Text "SharedFolderName: Was this from a Modern Sharing, and if so what Folder." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsFromSharedCalendar + Set-CellComment -Text "IsFromSharedCalendar: Is this CalLog from a Modern Sharing relationship?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # ExternalSharingMasterId + Set-CellComment -Text "ExternalSharingMasterId: If this is not [NotFound], then it is from a Modern Sharing relationship." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # ReceivedBy + Set-CellComment -Text "ReceivedBy: The Receiver of the Calendar Item. Should always be the owner of the Mailbox." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # ReceivedRepresenting + Set-CellComment -Text "ReceivedRepresenting: Who the item was Received for, of then the Delegate." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # MeetingRequestType + Set-CellComment -Text "MeetingRequestType: The Meeting Request Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center # StartTime + Set-CellComment -Text "StartTime: The Start Time of the Meeting. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment center # EndTime + Set-CellComment -Text "EndTime: The End Time of the Meeting. This and all Times are in UTC." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 17 -NumberFormat "m/d/yyyy h:mm:ss" -HorizontalAlignment Left # OriginalStartDate + Set-CellComment -Text "OriginalStartDate: The Original Start Date of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # TimeZone + Set-CellComment -Text "TimeZone: The Time Zone of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment Left # Location + Set-CellComment -Text "Location: The Location of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # CalendarItemType + Set-CellComment -Text "CalendarItemType: The Calendar Item Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsException + Set-CellComment -Text "IsException: Is this an Exception?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # RecurrencePattern + Set-CellComment -Text "RecurrencePattern: The Recurrence Pattern of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Center # AppointmentAuxiliaryFlags + Set-CellComment -Text "AppointmentAuxiliaryFlags: The Appointment Auxiliary Flags of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Left # DisplayAttendeesAll + Set-CellComment -Text "DisplayAttendeesAll: List of the Attendees of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # AttendeeCount + Set-CellComment -Text "AttendeeCount: The Attendee Count." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment Left # AppointmentState + Set-CellComment -Text "AppointmentState: The Appointment State of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # ResponseType + Set-CellComment -Text "ResponseType: The Response Type of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 20 -HorizontalAlignment center # ClientIntent + Set-CellComment -Text "ClientIntent: The Client Intent of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # AppointmentRecurring + Set-CellComment -Text "AppointmentRecurring: Is this a Recurring Meeting?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # HasAttachment + Set-CellComment -Text "HasAttachment: Does this Meeting have an Attachment?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsCancelled + Set-CellComment -Text "IsCancelled: Is this Meeting Cancelled?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsAllDayEvent + Set-CellComment -Text "IsAllDayEvent: Is this an All Day Event?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 10 -HorizontalAlignment center # IsSeriesCancelled + Set-CellComment -Text "IsSeriesCancelled: Is this a Series Cancelled Meeting?" -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 30 -HorizontalAlignment Left # SendMeetingMessagesDiagnostics + Set-CellComment -Text "SendMeetingMessagesDiagnostics: Compound Property to describe why meeting was or was not sent to everyone." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 50 -HorizontalAlignment Left # AttendeeCollection + Set-CellComment -Text "AttendeeCollection: The Attendee Collection of the Meeting, use -TrackingLogs to get values." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 40 -HorizontalAlignment Center # CalendarLogRequestId + Set-CellComment -Text "CalendarLogRequestId: The Calendar Log Request ID of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + $sheet.Column(++$n) | Set-ExcelRange -Width 120 -HorizontalAlignment Left # CleanGlobalObjectId + Set-CellComment -Text "CleanGlobalObjectId: The Clean Global Object ID of the Meeting." -Row $HeaderRow -ColumnNumber $n -Worksheet $sheet + + # Update header rows after all the others have been set. + # Title Row + $sheet.Row(1) | Set-ExcelRange -HorizontalAlignment Left + + # Set the Header row to be bold and left aligned + $sheet.Row($HeaderRow) | Set-ExcelRange -Bold -HorizontalAlignment Left +} diff --git a/Calendar/CalLogHelpers/Invoke-GetCalLogs.ps1 b/Calendar/CalLogHelpers/Invoke-GetCalLogs.ps1 index b3df2d1ab4..fdc4843d12 100644 --- a/Calendar/CalLogHelpers/Invoke-GetCalLogs.ps1 +++ b/Calendar/CalLogHelpers/Invoke-GetCalLogs.ps1 @@ -30,6 +30,7 @@ $script:CustomPropertyNameList = "SendMeetingMessagesDiagnostics", "SentRepresentingDisplayName", "SentRepresentingEmailAddress", +"Sensitivity", "LogTimestamp", "LogClientInfoString", "OriginalStartDate", diff --git a/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 b/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 index 801e435f11..616429cb06 100644 --- a/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 +++ b/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 @@ -5,7 +5,6 @@ $WellKnownCN_CA = "MICROSOFT SYSTEM ATTENDANT" $CalAttendant = "Calendar Assistant" $WellKnownCN_Trans = "MicrosoftExchange" $Transport = "Transport Service" - <# .SYNOPSIS Get the Mailbox for the Passed in Identity. @@ -18,46 +17,59 @@ Might want to extend to do 'Get-MailUser' as well. function GetMailbox { param( [string]$Identity, - [string]$Organization + [string]$Organization, + [bool]$UseGetMailbox ) - try { - Write-Verbose "Searching Get-Mailbox $(if (-not ([string]::IsNullOrEmpty($Organization))) {"with Org: $Organization"}) for $Identity." + if ($UseGetMailbox) { + $Cmdlet = "Get-Mailbox" + } else { + $Cmdlet = "Get-Recipient" + } + $params = @{Identity = $Identity + ErrorAction = "SilentlyContinue" + } - if ($Identity -and $Organization) { - if ($script:MSSupport) { - Write-Verbose "Using Organization parameter" - $GetMailboxOutput = Get-Mailbox -Identity $Identity -Organization $Organization -ErrorAction SilentlyContinue - } else { - Write-Verbose "Using -OrganizationalUnit parameter" - $GetMailboxOutput = Get-Mailbox -Identity $Identity -OrganizationalUnit $Organization -ErrorAction SilentlyContinue - } - } else { - $GetMailboxOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue + try { + Write-Verbose "Searching $Cmdlet $(if (-not ([string]::IsNullOrEmpty($Organization))) {"with Org: $Organization"}) for $Identity." + + if (-not ([string]::IsNullOrEmpty($Organization)) -and $script:MSSupport) { + Write-Verbose "Using Organization parameter" + $params.Add("Organization", $Organization) + } elseif (-not ([string]::IsNullOrEmpty($Organization))) { + Write-Verbose "Using -OrganizationalUnit parameter with $Organization." + $params.Add("Organization", $Organization) } - if (!$GetMailboxOutput) { + Write-Verbose "Running $Cmdlet with params: $($params.Values)" + $RecipientOutput = & $Cmdlet @params + Write-Verbose "RecipientOutput: $RecipientOutput" + + if (!$RecipientOutput) { Write-Host "Unable to find [$Identity]$(if ($Organization -ne `"`" ) {" in Organization:[$Organization]"})." Write-Host "Trying to find a Group Mailbox for [$Identity]..." - $GetMailboxOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue -GroupMailbox - if (!$GetMailboxOutput) { + $RecipientOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue -GroupMailbox + if (!$RecipientOutput) { Write-Host "Unable to find a Group Mailbox for [$Identity] either." return $null } else { - Write-Verbose "Found GroupMailbox [$($GetMailboxOutput.DisplayName)]" + Write-Verbose "Found GroupMailbox [$($RecipientOutput.DisplayName)]" } - } else { - Write-Verbose "Found [$($GetMailboxOutput.DisplayName)]" } - if (CheckForNoPIIAccess($script:GetMailboxOutput.DisplayName)) { - Write-Host -ForegroundColor Magenta "No PII Access for [$Identity]" + if ($null -eq $script:PIIAccess) { + [bool]$script:PIIAccess = CheckForPIIAccess($RecipientOutput.DisplayName) + } + + if ($script:PIIAccess) { + Write-Verbose "Found [$($RecipientOutput.DisplayName)]" } else { - Write-Verbose "Found [$($GetMailboxOutput.DisplayName)]" + Write-Host -ForegroundColor Magenta "No PII Access for [$Identity]" } - return $GetMailboxOutput + + return $RecipientOutput } catch { - Write-Error "An error occurred while running Get-Mailbox: [$_]" + Write-Error "An error occurred while running ${Cmdlet}: [$_]" } } @@ -84,13 +96,13 @@ function CheckIdentities { Write-Host "Preparing to check $($Identity.count) Mailbox(es)..." foreach ($Id in $Identity) { - $Account = GetMailbox -Identity $Id + $Account = GetMailbox -Identity $Id -UseGetMailbox $true if ($null -eq $Account) { # -or $script:MB.GetType().FullName -ne "Microsoft.Exchange.Data.Directory.Management.Mailbox") { Write-DashLineBoxColor "`n Error: Mailbox [$Id] not found on Exchange Online. Please validate the mailbox name and try again.`n" -Color Red continue } - if (CheckForNoPIIAccess $Account.DisplayName) { + if (-not (CheckForPIIAccess($Account.DisplayName))) { Write-Host -ForegroundColor DarkRed "No PII access for Mailbox [$Id]. Falling back to SMTP Address." $IdentityList += $ID if ($null -eq $script:MB) { @@ -104,11 +116,16 @@ function CheckIdentities { } } if ($Account.CalendarVersionStoreDisabled -eq $true) { + [bool]$script:CalLogsDisabled = $true Write-Host -ForegroundColor DarkRed "Mailbox [$Id] has CalendarVersionStoreDisabled set to True. This mailbox will not have Calendar Logs." Write-Host -ForegroundColor DarkRed "Some logs will be available for Mailbox [$Id] but they will not be complete." } if ($Account.RecipientTypeDetails -eq "RoomMailbox" -or $Account.RecipientTypeDetails -eq "EquipmentMailbox") { - $script:Rooms += $Account.PrimarySmtpAddress.ToString() + if ($script:PIIAccess -eq $true) { + $script:Rooms += $Account.PrimarySmtpAddress.ToString() + } else { + $script:Rooms += $Id + } Write-Host -ForegroundColor Green "[$Id] is a Room / Equipment Mailbox." } } @@ -262,14 +279,14 @@ function BetterThanNothingCNConversion { .SYNOPSIS Checks if an entries is Redacted to protect PII. #> -function CheckForNoPIIAccess { +function CheckForPIIAccess { param( $PassedString ) if ($PassedString -match "REDACTED-") { - return $true - } else { return $false + } else { + return $true } } @@ -314,7 +331,7 @@ function GetMailboxProp { } Write-Verbose "`t GetMailboxProp:[$Prop] :Found::[$ReturnValue]" - if (CheckForNoPIIAccess($ReturnValue)) { + if (-not (CheckForPIIAccess($ReturnValue))) { Write-Verbose "No PII Access for [$ReturnValue]" return BetterThanNothingCNConversion($PassedCN) } diff --git a/Calendar/CalLogHelpers/ShortClientNameFunctions.ps1 b/Calendar/CalLogHelpers/ShortClientNameFunctions.ps1 index 914ef5eb4b..2953c3d590 100644 --- a/Calendar/CalLogHelpers/ShortClientNameFunctions.ps1 +++ b/Calendar/CalLogHelpers/ShortClientNameFunctions.ps1 @@ -58,6 +58,11 @@ function CreateShortClientName { return $ShortClientName } + if ($LogClientInfoString -like "*EDiscoverySearch*") { + $ShortClientName = "EDiscoverySearch" + return $ShortClientName + } + if ($LogClientInfoString -like "Client=EBA*" -or $LogClientInfoString -like "Client=TBA*") { if ($LogClientInfoString -like "*ResourceBookingAssistant*") { $ShortClientName = "ResourceBookingAssistant" diff --git a/Calendar/CalLogHelpers/TimelineFunctions.ps1 b/Calendar/CalLogHelpers/TimelineFunctions.ps1 index eb22ce5cbb..aa40f1f67a 100644 --- a/Calendar/CalLogHelpers/TimelineFunctions.ps1 +++ b/Calendar/CalLogHelpers/TimelineFunctions.ps1 @@ -75,6 +75,11 @@ function BuildTimeline { Write-Host "Found $($script:EnhancedCalLogs.count) Log entries, only the $($InterestingCalLogs.count) Non-Ignorable entries will be analyzed in the TimeLine. `n" } + if ($script:CalLogsDisabled) { + Write-Host -ForegroundColor Red "Warning: CalLogs are disabled for this user, Timeline / CalLogs will be incomplete." + return + } + Write-DashLineBoxColor " TimeLine for: [$Identity]", " Subject: $($script:GCDO[0].NormalizedSubject)", " Organizer: $Script:Organizer", diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index c11b57880b..f16aedd58c 100644 --- a/Calendar/Check-SharingStatus.ps1 +++ b/Calendar/Check-SharingStatus.ps1 @@ -429,9 +429,9 @@ function GetReceiverInformation { # need to check if Get-CalendarValidationResult in the PS Workspace if ((Get-Command -Name Get-CalendarValidationResult -ErrorAction SilentlyContinue) -and $null -ne $ReceiverCalEntries) { - Write-Host "Running cmdlet: Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $($ReceiverCalEntries[0].LocalFolderId) -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1" $ewsId_del= $ReceiverCalEntries[0].LocalFolderId - Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $ewsId_del -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1 + Write-Host "Running cmdlet: Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $ewsId_del -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1 | FT -a GlobalObjectId, EventValidationResult " + Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $ewsId_del -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1 | Format-List UserPrimarySMTPAddress, Subject, GlobalObjectId, EventValidationResult, EventComparisonResult } } diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index 8c4acac385..8684ad28f6 100644 --- a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 +++ b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 @@ -26,6 +26,9 @@ # .PARAMETER CaseNumber # Case Number to include in the Filename of the output. # +# .PARAMETER ShortLogs +# Limit Logs to 500 instead of the default 2000, in case the server has trouble responding with the full logs. +# # .EXAMPLE # Get-CalendarDiagnosticObjectsSummary.ps1 -Identity someuser@microsoft.com -MeetingID 040000008200E00074C5B7101A82E008000000008063B5677577D9010000000000000000100000002FCDF04279AF6940A5BFB94F9B9F73CD # @@ -45,6 +48,7 @@ param ( [string[]]$Identity, [switch]$ExportToExcel, [string]$CaseNumber, + [switch]$ShortLogs, [Parameter(Mandatory, ParameterSetName = 'MeetingID', Position = 1)] [string]$MeetingID, @@ -79,11 +83,15 @@ Write-Verbose "Script Versions: $BuildVersion" . $PSScriptRoot\CalLogHelpers\ShortClientNameFunctions.ps1 . $PSScriptRoot\CalLogHelpers\CalLogInfoFunctions.ps1 . $PSScriptRoot\CalLogHelpers\CalLogExportFunctions.ps1 -. $PSScriptRoot\CalLogHelpers\ExcelModuleInstaller.ps1 . $PSScriptRoot\CalLogHelpers\CreateTimelineRow.ps1 . $PSScriptRoot\CalLogHelpers\FindChangedPropFunctions.ps1 . $PSScriptRoot\CalLogHelpers\Write-DashLineBoxColor.ps1 +if ($ExportToExcel.IsPresent) { + . $PSScriptRoot\CalLogHelpers\ExcelModuleInstaller.ps1 + . $PSScriptRoot\CalLogHelpers\ExportToExcelFunctions.ps1 +} + # =================================================================================================== # Main # =================================================================================================== From 1d9e7e6bbd1e5994d82fe9e397b67b7d4fae36e1 Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 16 Jul 2024 08:20:34 -0700 Subject: [PATCH 7/7] Minor change to rerun validation --- Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 b/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 index 616429cb06..ea894111bd 100644 --- a/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 +++ b/Calendar/CalLogHelpers/Invoke-GetMailbox.ps1 @@ -64,7 +64,7 @@ function GetMailbox { if ($script:PIIAccess) { Write-Verbose "Found [$($RecipientOutput.DisplayName)]" } else { - Write-Host -ForegroundColor Magenta "No PII Access for [$Identity]" + Write-Verbose "No PII Access for [$Identity]" } return $RecipientOutput