tests/Test-Assessment.41035.ps1
|
<#
.SYNOPSIS User reporting for phishing and spam is enabled and routed to a reviewed mailbox. .NOTES Test ID: 41035 Workshop Task: SECOPS-035 Pillar: SecOps Category: Email and collaboration security Required Module: ExchangeOnlineManagement Required permissions: Get-ReportSubmissionPolicy, Get-ReportSubmissionRule #> function Test-Assessment-41035 { [ZtTest( Category = 'Email and collaboration security', CompatibleLicense = ('EXCHANGE_S_STANDARD'), ImplementationCost = 'Low', Pillar = 'SecOps', RiskLevel = 'Medium', Service = ('ExchangeOnline'), SfiPillar = 'Monitor and detect cyberthreats', TenantType = ('Workforce'), TestId = 41035, Title = 'User reporting for phishing and spam is enabled and routed to a reviewed mailbox', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking user report submission policy configuration' Write-ZtProgress -Activity $activity -Status 'Retrieving report submission policy' # Q1a: Retrieve the singleton report submission policy from Exchange Online. $policyList = $null try { $policyList = @(Get-ReportSubmissionPolicy -ErrorAction Stop | Select-Object Identity, EnableReportToMicrosoft, ReportJunkToCustomizedAddress, ReportNotJunkToCustomizedAddress, ReportPhishToCustomizedAddress, ReportJunkAddresses, ReportNotJunkAddresses, ReportPhishAddresses, EnableThirdPartyAddress, ThirdPartyReportAddresses, ReportChatMessageEnabled, ReportChatMessageToCustomizedAddressEnabled, PhishingReviewResultMessage, PostSubmitMessageEnabled) } catch { Write-PSFMessage "Failed to retrieve report submission policy: $_" -Tag Test -Level Warning $params = @{ TestId = '41035' Title = 'User reporting for phishing and spam is enabled and routed to a reviewed mailbox' Status = $false Result = "⚠️ The report submission policy could not be retrieved. Verify that the account has Exchange Online permissions and re-run. Error: $_" CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } # Spec: In an unconfigured tenant the policy object does not exist at all — it is created only when # user-reported settings are explicitly configured. An empty result means the effective defaults # cannot be confirmed from PowerShell. if ($null -eq $policyList -or $policyList.Count -eq 0 -or $null -eq $policyList[0]) { $params = @{ TestId = '41035' Title = 'User reporting for phishing and spam is enabled and routed to a reviewed mailbox' Status = $false Result = '⚠️ **Get-ReportSubmissionPolicy** returned no results. In an unconfigured tenant the policy object is not created until user-reported settings are explicitly saved — the effective defaults cannot be confirmed from PowerShell. Verify the configuration in the [Defender portal > User reported settings](https://security.microsoft.com/securitysettings/userSubmission) and re-run after saving the settings.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } $policy = $policyList[0] # Q1b: Retrieve the report submission rule. The rule may not exist in all tenants (e.g., MS-only route). Write-ZtProgress -Activity $activity -Status 'Retrieving report submission rule' $rule = $null try { $ruleList = @(Get-ReportSubmissionRule -ErrorAction Stop | Select-Object Name, ReportSubmissionPolicy, SentTo, State) if ($ruleList.Count -gt 0) { $rule = $ruleList[0] } } catch { Write-PSFMessage "Failed to retrieve report submission rule (non-fatal — treating as no rule): $_" -Tag Test -Level Warning $rule = $null } #endregion Data Collection #region Assessment Logic $portalUrl = 'https://security.microsoft.com/userSubmissionsReportMessage' # Routing destinations per spec # microsoftRoute: EnableReportToMicrosoft = True; requires no rule. $microsoftRoute = ($policy.EnableReportToMicrosoft -eq $true) # Custom mailbox route flags (set to True by the Defender portal when a reporting mailbox is configured). $junkFlagSet = ($policy.ReportJunkToCustomizedAddress -eq $true) $notJunkFlagSet = ($policy.ReportNotJunkToCustomizedAddress -eq $true) $phishFlagSet = ($policy.ReportPhishToCustomizedAddress -eq $true) $junkCount = ($policy.ReportJunkAddresses | Measure-Object).Count $notJunkCount = ($policy.ReportNotJunkAddresses | Measure-Object).Count $phishCount = ($policy.ReportPhishAddresses | Measure-Object).Count # customMailboxComplete: all three routing flags True AND all three address lists non-empty. $customMailboxComplete = $junkFlagSet -and $notJunkFlagSet -and $phishFlagSet -and ($junkCount -gt 0) -and ($notJunkCount -gt 0) -and ($phishCount -gt 0) $customMailboxPartial = ($junkFlagSet -or $notJunkFlagSet -or $phishFlagSet -or ($junkCount -gt 0) -or ($notJunkCount -gt 0) -or ($phishCount -gt 0)) -and (-not $customMailboxComplete) # ruleActionable: a rule exists with State=Enabled and SentTo populated. # Shared requirement for both the custom-mailbox and non-Microsoft-tool routes. $ruleActionable = ($null -ne $rule) -and ($rule.State -eq 'Enabled') -and (-not [string]::IsNullOrWhiteSpace($rule.SentTo)) # customMailboxRoute: complete flags+addresses AND rule actionable. $customMailboxRoute = $customMailboxComplete -and $ruleActionable # Spec Investigate: flags+addresses complete but rule absent/disabled/no SentTo. $customMailboxRuleProblem = $customMailboxComplete -and (-not $ruleActionable) $thirdPartyCount = ($policy.ThirdPartyReportAddresses | Measure-Object).Count $thirdPartyConfigured = ($policy.EnableThirdPartyAddress -eq $true) -and ($thirdPartyCount -gt 0) # thirdPartyPartial: exactly one of EnableThirdPartyAddress / ThirdPartyReportAddresses is set — an attempted but incomplete route. $thirdPartyPartial = (($policy.EnableThirdPartyAddress -eq $true) -or ($thirdPartyCount -gt 0)) -and (-not $thirdPartyConfigured) # thirdPartyRoute also requires a report submission rule — without it, TP reports are not delivered # to the reporting mailbox or surfaced on the Submissions page (verified against a live tenant). $thirdPartyRoute = $thirdPartyConfigured -and $ruleActionable $thirdPartyRuleProblem = $thirdPartyConfigured -and (-not $ruleActionable) # Spec evaluation order: Pass → Investigate → Fail $anyRoute = $microsoftRoute -or $customMailboxRoute -or $thirdPartyRoute $passed = $false $customStatus = $null if ($anyRoute) { $passed = $true $testResultMarkdown = "✅ User reporting in Outlook is enabled and user-reported messages reach Microsoft, a monitored SOC mailbox, or a configured non-Microsoft reporter.`n`n%TestResult%" } elseif ($customMailboxPartial -or $customMailboxRuleProblem -or $thirdPartyRuleProblem) { $passed = $false $customStatus = 'Investigate' $investigateDetails = [System.Collections.Generic.List[string]]::new() if ($customMailboxPartial) { $investigateDetails.Add('custom reporting mailbox is partially configured — only some of **ReportJunkToCustomizedAddress**/**ReportNotJunkToCustomizedAddress**/**ReportPhishToCustomizedAddress** routing flags or **ReportJunkAddresses**/**ReportNotJunkAddresses**/**ReportPhishAddresses** address lists are set; all six must be configured') } if ($customMailboxRuleProblem) { if ($null -eq $rule) { $investigateDetails.Add('all three custom-mailbox flags and address lists are configured but no report submission rule exists — nothing delivers messages to the SOC mailbox') } elseif ($rule.State -eq 'Disabled') { $investigateDetails.Add("report submission rule '**$($rule.Name)**' is **Disabled** — routing is broken even when policy flags and address lists appear correct") } else { $investigateDetails.Add("report submission rule '**$($rule.Name)**' has no **SentTo** address — nothing delivers messages to the SOC mailbox") } } if ($thirdPartyRuleProblem) { if ($null -eq $rule) { $investigateDetails.Add('**EnableThirdPartyAddress** and **ThirdPartyReportAddresses** are configured but no report submission rule exists — nothing delivers non-Microsoft reporter messages to the reporting mailbox') } elseif ($rule.State -eq 'Disabled') { $investigateDetails.Add("report submission rule '**$($rule.Name)**' is **Disabled** — the non-Microsoft reporter route is broken even though **EnableThirdPartyAddress** and **ThirdPartyReportAddresses** are set") } else { $investigateDetails.Add("report submission rule '**$($rule.Name)**' has no **SentTo** address — nothing delivers non-Microsoft reporter messages to the reporting mailbox") } } $reasonText = ($investigateDetails | ForEach-Object { "- $_" }) -join "`n" $testResultMarkdown = "⚠️ The customized mailbox is partially configured, the report submission rule is absent or disabled, or the non-Microsoft reporter route is incomplete; verify the configuration in the Defender portal.`n`n$reasonText`n`n%TestResult%" } else { $passed = $false $testResultMarkdown = "❌ Reports are sent nowhere actionable; the SOC has no visibility into what end users are reporting and Microsoft re-evaluation is not in the loop.`n`n> **Note:** If your organization uses a non-Microsoft reporting button (such as KnowBe4 PAB or Cofense Reporter) that does not integrate with Exchange Online's report submission policy, this result may be a false positive — verify whether a third-party reporter is in use before remediating.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation #format an address list for display (show up to 3 addresses, then count) $formatAddresses = { param([object]$Addresses) $list = @($Addresses | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($list.Count -eq 0) { return '—' } $display = ($list | Select-Object -First 3 | ForEach-Object { Get-SafeMarkdown -Text $_ }) -join ', ' if ($list.Count -gt 3) { $display += " *(+$($list.Count - 3) more)*" } return $display } #Boolean/null display $formatBool = { param([object]$Value) if ($null -eq $Value) { return '—' } return $Value.ToString() } # Determine per-row result icons # Microsoft route $msResult = if ($microsoftRoute) { '✅ Pass' } else { '❌ Fail' } # Custom mailbox: N/A only when overall Pass and custom mailbox completely unconfigured (not attempted). $customMailboxNA = $anyRoute -and (-not $customMailboxComplete) -and (-not $customMailboxPartial) # Flag rows — route-aware: Pass only when the complete custom route passes, Investigate when # the route was attempted but incomplete, N/A when the route was not attempted, Fail otherwise. $junkFlagResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } $notJunkFlagResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } $phishFlagResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } # Custom mailbox address lists — same route-aware logic as the flag rows: an attempted but # incomplete route reports Investigate; any populated address on an incomplete route is caught by $customMailboxPartial. $junkResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } $notJunkResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } $phishResult = if ($customMailboxRoute) { '✅ Pass' } elseif ($customMailboxPartial -or $customMailboxRuleProblem) { '⚠️ Investigate' } elseif ($customMailboxNA) { 'N/A' } else { '❌ Fail' } # Third-party reporter — N/A only when not attempted at all (neither flag nor addresses set) and another route passes. $tpEnabled = $policy.EnableThirdPartyAddress $tpEnabledDisp = & $formatBool $tpEnabled $thirdPartyAttempted = ($tpEnabled -eq $true) -or ($thirdPartyCount -gt 0) $thirdPartyNA = $anyRoute -and (-not $thirdPartyAttempted) # Route-aware: an attempted but incomplete non-Microsoft route (rule problem or flag/address mismatch) # reports Investigate before any address count is considered; both third-party rows share the same state. $tpResult = if ($thirdPartyRoute) { '✅ Pass' } elseif ($thirdPartyRuleProblem -or $thirdPartyPartial) { '⚠️ Investigate' } elseif ($thirdPartyNA) { 'N/A' } else { '❌ Fail' } $tpAddrResult = if ($thirdPartyRoute) { '✅ Pass' } elseif ($thirdPartyRuleProblem -or $thirdPartyPartial) { '⚠️ Investigate' } elseif ($thirdPartyNA) { 'N/A' } else { '❌ Fail' } # Chat and post-submit — informational only; do not participate in the Pass/Fail/Investigate verdict. $chatResult = 'ℹ️ Informational' $postResult = 'ℹ️ Informational' # Rule state and SentTo — applicable when custom mailbox is fully configured OR non-Microsoft reporter # is configured; without the rule, neither route delivers to the reporting mailbox. $ruleIsRelevant = $customMailboxComplete -or $thirdPartyConfigured $ruleStateName = if (-not $ruleIsRelevant -or $null -eq $rule) { '—' } else { "``$($rule.State)``" } $ruleStateResult = if (-not $ruleIsRelevant) { 'N/A' } elseif (-not $ruleActionable) { '⚠️ Investigate' } else { '✅ Pass' } $ruleSentTo = if (-not $ruleIsRelevant -or $null -eq $rule -or [string]::IsNullOrWhiteSpace($rule.SentTo)) { '—' } else { Get-SafeMarkdown -Text $rule.SentTo } $ruleSentToResult = if (-not $ruleIsRelevant) { 'N/A' } elseif ($ruleActionable) { '✅ Pass' } else { '⚠️ Investigate' } $tableRows = '' $tableRows += "| EnableReportToMicrosoft | $(& $formatBool $policy.EnableReportToMicrosoft) | True (re-evaluation + model training) | $msResult |`n" $tableRows += "| ReportJunkToCustomizedAddress | $(& $formatBool $policy.ReportJunkToCustomizedAddress) | True (custom SOC mailbox route) | $junkFlagResult |`n" $tableRows += "| ReportNotJunkToCustomizedAddress | $(& $formatBool $policy.ReportNotJunkToCustomizedAddress) | True (custom SOC mailbox route) | $notJunkFlagResult |`n" $tableRows += "| ReportPhishToCustomizedAddress | $(& $formatBool $policy.ReportPhishToCustomizedAddress) | True (custom SOC mailbox route) | $phishFlagResult |`n" $tableRows += "| ReportJunkAddresses | $(& $formatAddresses $policy.ReportJunkAddresses) | non-empty (custom SOC mailbox) | $junkResult |`n" $tableRows += "| ReportNotJunkAddresses | $(& $formatAddresses $policy.ReportNotJunkAddresses) | non-empty (custom SOC mailbox) | $notJunkResult |`n" $tableRows += "| ReportPhishAddresses | $(& $formatAddresses $policy.ReportPhishAddresses) | non-empty (custom SOC mailbox) | $phishResult |`n" $tableRows += "| EnableThirdPartyAddress | $tpEnabledDisp | True (if using non-Microsoft reporter) | $tpResult |`n" $tableRows += "| ThirdPartyReportAddresses | $(& $formatAddresses $policy.ThirdPartyReportAddresses) | non-empty (if using non-Microsoft reporter) | $tpAddrResult |`n" $tableRows += "| ReportChatMessageEnabled | $(& $formatBool $policy.ReportChatMessageEnabled) | True (MDO P2 + Teams policy required) | $chatResult |`n" $tableRows += "| PostSubmitMessageEnabled | $(& $formatBool $policy.PostSubmitMessageEnabled) | True (user feedback after submission) | $postResult |`n" $tableRows += "| Report submission rule State | $ruleStateName | Enabled | $ruleStateResult |`n" $tableRows += "| Report submission rule SentTo | $ruleSentTo | (SOC mailbox address) | $ruleSentToResult |`n" $formatTemplate = @' ## [User reported settings]({0}) | Setting | Value | Recommended | Result | | :------ | :---- | :---------- | :----- | {1} '@ $mdInfo = $formatTemplate -f $portalUrl, $tableRows $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '41035' Title = 'User reporting for phishing and spam is enabled and routed to a reviewed mailbox' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |