Public/Get-TntDefenderEmailThreatReport.ps1
|
function Get-TntDefenderEmailThreatReport { <# .SYNOPSIS Retrieves email threat summary from Microsoft Defender for Office 365. .DESCRIPTION This function generates a detailed report of email security threats including phishing attempts, malware detections, spam statistics, and threat submission data. .PARAMETER TenantId The Azure AD Tenant ID (GUID) to connect to. .PARAMETER ClientId The Application (Client) ID of the app registration created for security reporting. .PARAMETER ClientSecret The client secret for the app registration. Use this for automated scenarios. .PARAMETER CertificateThumbprint The thumbprint of the certificate to use for authentication instead of client secret. .PARAMETER DaysBack Number of days to look back from today for the reporting period. Defaults to 90 days. .EXAMPLE Get-TntDefenderEmailThreatReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret Retrieves email threat summary for the last 90 days. .EXAMPLE Get-TntDefenderEmailThreatReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret -DaysBack 30 Retrieves report for the last 30 days. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject Returns a structured object containing summary statistics of email threats. .NOTES Author: Tom de Leeuw Website: https://systom.dev Module: TenantReports Required Permissions: - SecurityEvents.Read.All (Application) - Reports.Read.All (Application) - SecurityAlert.Read.All (Application) - ThreatSubmission.Read.All (Application) .LINK https://systom.dev #> [CmdletBinding(DefaultParameterSetName = 'ClientSecret')] [OutputType([System.Management.Automation.PSCustomObject])] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')] [ValidateNotNullOrEmpty()] [Alias('Tenant')] [string]$TenantId, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')] [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')] [Alias('ApplicationId')] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Secret', 'ApplicationSecret')] [SecureString]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Thumbprint')] [string]$CertificateThumbprint, # Use interactive authentication (not supported for this function - requires application permissions). [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')] [switch]$Interactive, [Parameter()] [ValidateRange(1, 180)] [int]$DaysBack = 90 ) begin { # Calculate date range from DaysBack $EndDate = Get-Date $StartDate = $EndDate.AddDays(-$DaysBack) # Map DaysBack to nearest valid API period (D7, D30, D90, D180) $ValidPeriods = @(7, 30, 90, 180) $ApiPeriod = $ValidPeriods | Sort-Object { [Math]::Abs($_ - $DaysBack) } | Select-Object -First 1 Write-Verbose "DaysBack $DaysBack mapped to API period D$ApiPeriod" Write-Information "Starting Defender email threat report generation for past $($DaysBack) days..." -InformationAction Continue } process { # Interactive authentication is not supported for this function # Defender/Security APIs require application permissions if ($Interactive) { Write-Warning 'Get-TntDefenderEmailThreatReport requires application permissions and cannot run with interactive authentication.' Write-Warning 'The following application permissions are required: SecurityEvents.Read.All, SecurityAlert.Read.All, ThreatSubmission.Read.All' Write-Warning 'Use -ClientSecret or -CertificateThumbprint authentication instead.' return $null } try { $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters $ConnectionInfo = Connect-TntGraphSession @ConnectionParams $Report = [PSCustomObject]@{ ReportDate = Get-Date StartDate = $StartDate EndDate = $EndDate TotalSecurityAlerts = 0 HighSeverityAlerts = 0 MediumSeverityAlerts = 0 LowSeverityAlerts = 0 ActiveAlerts = 0 ResolvedAlerts = 0 PhishingAlerts = 0 MalwareAlerts = 0 TotalEmailsReceived = 0 TotalEmailsSent = 0 TotalThreatSubmissions = 0 PhishingSubmissions = 0 MalwareSubmissions = 0 } # Email-related security alerts Write-Verbose 'Retrieving email security alerts...' try { # Fetch all alerts within date range, then filter post-retrieval for email-related content $AlertFilter = "createdDateTime ge $($StartDate.ToString('yyyy-MM-ddTHH:mm:ssZ')) and createdDateTime le $($EndDate.ToString('yyyy-MM-ddTHH:mm:ssZ'))" $AllAlerts = Get-MgBetaSecurityAlert -Filter $AlertFilter -All -ErrorAction Stop # Filter for email-related alerts by category or title - ensure array output $SecurityAlerts = @($AllAlerts | Where-Object { $_.Category -in @('phishing', 'malware', 'spam', 'email') -or $_.Title -match 'phish|malware|spam|email|threat' }) Write-Verbose "Retrieved $($AllAlerts.Count) total alerts, $($SecurityAlerts.Count) email-related" if ($SecurityAlerts.Count -gt 0) { $Report.TotalSecurityAlerts = $SecurityAlerts.Count $Report.HighSeverityAlerts = ($SecurityAlerts | Where-Object { $_.Severity -eq 'high' }).Count $Report.MediumSeverityAlerts = ($SecurityAlerts | Where-Object { $_.Severity -eq 'medium' }).Count $Report.LowSeverityAlerts = ($SecurityAlerts | Where-Object { $_.Severity -eq 'low' }).Count $Report.ActiveAlerts = ($SecurityAlerts | Where-Object { $_.Status -ne 'resolved' }).Count $Report.ResolvedAlerts = ($SecurityAlerts | Where-Object { $_.Status -eq 'resolved' }).Count $Report.PhishingAlerts = ($SecurityAlerts | Where-Object { $_.Category -eq 'phishing' }).Count $Report.MalwareAlerts = ($SecurityAlerts | Where-Object { $_.Category -eq 'malware' }).Count } } catch { Write-Warning "Failed to retrieve security alerts: $($_.Exception.Message)" } # 2. Get email threat detection data Write-Verbose 'Retrieving email activity data...' $TempFile = [System.IO.Path]::GetTempFileName() Remove-Item $TempFile -ErrorAction SilentlyContinue # Remove empty file created by GetTempFileName to suppress warning try { # This report shows delivered emails but may not contain threat specifics depending on tenant license # Suppress progress bar due to Microsoft Graph SDK bug with PercentComplete overflow (Int32.MaxValue) # Use script block with isolated scope and ignore the progress-related error $null = & { $ProgressPreference = 'SilentlyContinue' $ErrorActionPreference = 'SilentlyContinue' Get-MgReportEmailActivityUserDetail -Period "D$ApiPeriod" -OutFile $TempFile 2>$null } if (-not (Test-Path $TempFile) -or (Get-Item $TempFile).Length -eq 0) { throw 'Email activity report file was not created or is empty' } $ThreatData = Import-Csv -Path $TempFile if ($ThreatData) { Write-Verbose "Email activity data retrieved for $($ThreatData.Count) users" # Cast to int to avoid decimal output (Measure-Object returns double) $receiveSum = ($ThreatData | Measure-Object -Property 'Receive Count' -Sum -ErrorAction SilentlyContinue).Sum $Report.TotalEmailsReceived = if ($null -ne $receiveSum) { [int]$receiveSum } else { 0 } $sendSum = ($ThreatData | Measure-Object -Property 'Send Count' -Sum -ErrorAction SilentlyContinue).Sum $Report.TotalEmailsSent = if ($null -ne $sendSum) { [int]$sendSum } else { 0 } } else { Write-Verbose 'No email activity data returned from API' } } catch { Write-Warning "Failed to retrieve email activity data: $($_.Exception.Message)" } finally { if (Test-Path $TempFile) { Remove-Item $TempFile -ErrorAction SilentlyContinue } } # 3. Get threat submission data Write-Verbose 'Retrieving threat submission data...' try { $allSubmissions = Get-MgBetaSecurityThreatSubmissionEmailThreat -All -ErrorAction Stop $recentSubmissions = $allSubmissions | Where-Object { $_.CreatedDateTime -ge $StartDate } if ($null -ne $recentSubmissions) { $Report.TotalThreatSubmissions = $recentSubmissions.Count $Report.PhishingSubmissions = ($recentSubmissions | Where-Object { $_.Category -eq 'phishing' }).Count $Report.MalwareSubmissions = ($recentSubmissions | Where-Object { $_.Category -eq 'malware' }).Count } Write-Verbose "Retrieved $($Report.TotalThreatSubmissions) recent threat submissions." } catch { Write-Warning "Failed to retrieve threat submission data: $($_.Exception.Message)" } Write-Information "Defender email threat report completed - $($Report.TotalSecurityAlerts) security alerts found" -InformationAction Continue $Report } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Get-TntDefenderEmailThreatReport failed: $($_.Exception.Message)", $_.Exception), 'GetTntDefenderEmailThreatReportError', [System.Management.Automation.ErrorCategory]::OperationStopped, $TenantId ) $PSCmdlet.ThrowTerminatingError($errorRecord) } finally { if ($ConnectionInfo.ShouldDisconnect) { Disconnect-TntGraphSession -ConnectionState $ConnectionInfo } } } } |