diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 992e41e4..317fdaa0 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -113,7 +113,7 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Clear-MtGraphCache', 'Connect-Mae 'Test-MtCisaAntiSpamSafeList', 'Test-MtCisaMailboxAuditing', 'Test-MtCisaSpfRestriction', 'Test-MtCisaSpfDirective', 'Test-MtCisaDkim', 'Test-MtCisaDmarcRecordExist', 'Test-MtCisaDmarcRecordReject', - 'Test-MtCisaDmarcAggregateCisa', + 'Test-MtCisaDmarcAggregateCisa', 'Test-MtCisaDmarcReport', 'Test-MtConditionalAccessWhatIf', 'Test-MtConnection', 'Test-MtEidscaAF01', diff --git a/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.md b/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.md new file mode 100644 index 00000000..299ac89d --- /dev/null +++ b/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.md @@ -0,0 +1,20 @@ +An agency point of contact SHOULD be included for aggregate and failure reports. + +Rationale: Email spoofing attempts are not inherently visible to domain owners. DMARC provides a mechanism to receive reports of spoofing attempts. Including an agency point of contact gives the agency insight into attempts to spoof their domains. + +#### Remediation action: + +See MS.EXO.4.1v1 Instructions for an overview of how to publish and check a DMARC record. Ensure the record published includes: + +* A point of contact specific to your agency in the RUA field. +* reports@dmarc.cyber.dhs.gov as one of the emails in the RUA field. +* One or more agency-defined points of contact in the RUF field. + +#### Related links + +* [Exchange admin center - Accepted domains](https://admin.exchange.microsoft.com/#/accepteddomains) +* [CISA 4 Domain-Based Message Authentication, Reporting, and Conformance (DMARC) - MS.EXO.4.4v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo44v1) +* [CISA ScubaGear Rego Reference](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Rego/EXOConfig.rego#L252) + + +%TestResult% \ No newline at end of file diff --git a/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.ps1 b/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.ps1 new file mode 100644 index 00000000..77daa49e --- /dev/null +++ b/powershell/public/CISA/exchange/Test-MtCisaDmarcReport.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS + Checks state of DMARC records for all exo domains + +.DESCRIPTION + + An agency point of contact SHOULD be included for aggregate and failure reports. + +.EXAMPLE + Test-MtCisaDmarcReport + + Returns true if DMARC record inlcudes report targets within same domain +#> + +Function Test-MtCisaDmarcReport { + [CmdletBinding()] + [OutputType([bool])] + param() + + if(!(Test-MtConnection ExchangeOnline)){ + Add-MtTestResultDetail -SkippedBecause NotConnectedExchange + return $null + } + + $acceptedDomains = Get-AcceptedDomain + <# Parked domains should have DMARC with reject policy + $sendingDomains = $acceptedDomains | Where-Object {` + -not $_.SendingFromDomainDisabled + } + #> + $expandedDomains = @() + foreach($domain in $acceptedDomains){ + #This regex does NOT capture for third level domain scenarios + #e.g., example.co.uk; example.ny.us; + $matchDomain = "(?:^|\.)(?'second'\w+.\w+$)" + $dmarcMatch = $domain.domainname -match $matchDomain + if($dmarcMatch){ + $expandedDomains += $Matches.second + if($domain.domainname -ne $Matches.second){ + $expandedDomains += $domain.domainname + } + }else{ + $expandedDomains += $domain.domainname + } + } + + $dmarcRecords = @() + foreach($domain in $expandedDomains){ + $dmarcRecord = Get-MailAuthenticationRecord -DomainName $domainName -Records DMARC + $dmarcRecord | Add-Member -MemberType NoteProperty -Name "pass" -Value "Failed" + $dmarcRecord | Add-Member -MemberType NoteProperty -Name "reason" -Value "" + + $checkType = $dmarcRecord.dmarcRecord.GetType().Name -eq "DMARCRecord" + $hostsAggregate = $dmarcRecord.dmarcRecord.reportAggregate.mailAddress.Host + $hostsForensic = $dmarcRecord.dmarcRecord.reportForensic.mailAddress.Host + + if($checkType -and $domain -in $hostsAggregate -and $domain -in $hostsForensic){ + $dmarcRecord.pass = "Passed" + }elseif($checkType){ + $dmarcRecord.reason = "No target in domain" + }else{ + $dmarcRecord.reason = $dmarcRecord.dmarcRecord + } + + $dmarcRecords += $dmarcRecord + } + + if("Failed" -in $dmarcRecords.pass){ + $testResult = $false + }else{ + $testResult = $true + } + + if($testResult){ + $testResultMarkdown = "Well done. Your tenant's domains have in domain report targets. Review report targets.`n`n%TestResult%" + }else{ + $testResultMarkdown = "Your tenant's second level domains do not have in domain report targets.`n`n%TestResult%" + } + + $passResult = "✅ Pass" + $failResult = "❌ Fail" + $result = "| Domain | Result | Reason | Targets |`n" + $result += "| --- | --- | --- | --- |`n" + foreach ($item in $dmarcRecords | Sort-Object -Property domain) { + switch($item.pass){ + "Passed" {$itemResult = $passResult} + "Failed" {$itemResult = $failResult} + } + $aggregates = $item.dmarcRecord.reportForensic.mailAddress + $aggregatesCount = ($aggregates|Measure-Object).Count + if($aggregatesCount -ge 3){ + $aggregates = "$($aggregates[0])
$($aggregates[1])
" + $aggregates += "...$aggregatesCount targets" + }elseif(aggregatesCount -gt 1){ + $aggregates = $aggregates -join "
" + } + $forensics = $item.dmarcRecord.reportForensic.mailAddress + $forensicsCount = ($forensics|Measure-Object).Count + if($forensicsCount -ge 3){ + $forensics = "$($forensics[0])
$($forensics[1])
" + $forensics += "...$forensicsCount targets" + }elseif(aggregatesCount -gt 1){ + $forensics = $forensics -join "
" + } + + $result += "| $($item.domain) | $($itemResult) | $($item.reason) | Aggregate Reports: $($aggregates) |`n" + $result += "| $($item.domain) | $($itemResult) | $($item.reason) | Forensic Reports: $($forensics) |`n" + } + + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $result + + Add-MtTestResultDetail -Result $testResultMarkdown + + return $testResult +} \ No newline at end of file diff --git a/tests/CISA/exchange/Test-MtCisaDmarcReport.ps1 b/tests/CISA/exchange/Test-MtCisaDmarcReport.ps1 new file mode 100644 index 00000000..39291d55 --- /dev/null +++ b/tests/CISA/exchange/Test-MtCisaDmarcReport.ps1 @@ -0,0 +1,9 @@ +Describe "CISA SCuBA" -Tag "MS.EXO", "MS.EXO.4.4", "CISA", "Security", "All" { + It "MS.EXO.4.4: An agency point of contact SHOULD be included for aggregate and failure reports." { + $cisaDmarcReport = Test-MtCisaDmarcReport + + if ($null -ne $cisaDmarcReport) { + $cisaDmarcReport | Should -Be $true -Because "DMARC report targets should exist." + } + } +} \ No newline at end of file diff --git a/website/docs/tests/cisa/exo.md b/website/docs/tests/cisa/exo.md index 2907c5b4..2b439d6e 100644 --- a/website/docs/tests/cisa/exo.md +++ b/website/docs/tests/cisa/exo.md @@ -26,6 +26,7 @@ See the [Installation guide](/docs/installation#optional-modules-and-permissions | Test-MtCisaDmarcRecordExist | [MS.EXO.4.1v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo41v1) | | Test-MtCisaDmarcRecordReject | [MS.EXO.4.2v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo42v1) | | Test-MtCisaDmarcAggregateCisa | [MS.EXO.4.3v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo43v1) | +| Test-MtCisaDmarcReport | [MS.EXO.4.4v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo44v1) | | Test-MtCisaSmtpAuthentication | [MS.EXO.5.1v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo51v1) | | Test-MtCisaContactSharing | [MS.EXO.6.1v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo61v1) | | Test-MtCisaCalendarSharing | [MS.EXO.6.2v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo62v1) |