Reporting on Microsoft 365 DLP Overrides with PowerShell

Data Loss Prevention, or DLP, is the part of Microsoft Purview that watches for sensitive information leaving places like Exchange, SharePoint, OneDrive, Teams, and endpoint devices. It is not only a hard block button. A good DLP policy can detect sensitive content, warn the user before the data leaves, block the action when the risk is too high, or allow the user to continue when the business process actually requires it.

Overrides are part of that design. If a policy is configured to warn and allow override, the user can provide a business justification and send the message anyway. In some workflows, that is exactly what the organization intended. A contract may need to go out, a support case may need a controlled attachment, or a user may know something about the business context that the policy cannot understand from pattern matching alone.

The review problem is where this gets messy. These justifications are not sitting in Azure like a simple approval queue. They live in Microsoft Purview activity data, mixed in with the rest of the DLP rule-match activity. Microsoft gives administrators a way to see and export that activity through Activity Explorer and PowerShell, but the portal is built more like an investigation surface than a clean weekly override report.

I wrote a script that streamlines this because I do not want to click through the Purview portal every week, change filters, open individual events, and hope I did not miss the user-entered reason. I want to ask for a date range, pull the Exchange DLP events, filter down to records with override signals, and export the fields that matter: sender, recipients, subject, policy, rule, false-positive flag, and justification.

Reading those justifications is the whole point of allowing overrides in the first place. If users keep entering solid business reasons, the policy may be doing its job. If the reasons are vague, repetitive, or obviously wrong, the organization has a training problem, a workflow problem, or a policy that is allowing too much. Without review, an override feature becomes a bypass with nicer wording.

Build the DLP policy in Purview

The DLP policy is the container for the decision. It defines where Purview should watch, which rule should fire, what the user sees, and whether the user is allowed to override the warning with a business justification. The sensitive information type is different. That is the detection object the policy uses to decide whether the content matches something the organization cares about.

Those two pieces have to be linked. A policy without the right sensitive information type has no useful signal. A sensitive information type that is never used in a policy is just a classifier sitting in Purview.

The path starts in the Microsoft Purview portal under Data Loss Prevention.

Microsoft Purview solutions menu showing Data Loss Prevention

From there, the policy workflow starts under DLP policies. This is where I create the policy that will eventually use the custom CUI banner type.

Microsoft Purview DLP policy entry point

The policy name needs to be plain enough that it still makes sense when it shows up later in Activity Explorer and the exported CSV. A vague name makes the report harder to review because the reviewer has to go back into Purview to understand what fired.

Microsoft Purview DLP policy creation screen with a custom policy name

Administrative units decide whether the policy applies broadly or only inside a delegated administrative boundary. I want that visible because the report later has to be read against the same scope. If the policy only applies to part of the tenant, the override report is not a tenant-wide picture.

Microsoft Purview DLP policy admin units step

The locations step is where I decide which Microsoft 365 workload the policy applies to. Purview can protect multiple locations depending on configuration and licensing, but this report is about email overrides, so Exchange is the workload I care about for the script.

Microsoft Purview DLP policy locations step

At this point the policy shell exists, but it still needs a detection rule. That is where the sensitive information type comes in.

Create the sensitive information type

Microsoft describes sensitive information types as pattern-based classifiers that detect things like financial data, health records, Social Security numbers, and custom patterns. Built-in types are useful, but the local policy I was testing needed a custom sensitive information type for CUI banners.

The custom type is visible under sensitive information types. This is the object the DLP policy will reference later when it decides whether a message matches the rule.

Microsoft Purview sensitive information types list showing a custom CUI banner type

The name is not cosmetic. This is the label administrators will see when they build the rule, investigate matches, and explain why the policy fired. I want the name to describe the data pattern clearly enough that I do not have to reverse-engineer it later.

Microsoft Purview custom sensitive information type name screen

The pattern screen is where the custom type becomes real. For this test, the sensitive information type is looking for CUI banner language, not trying to guess at every possible sensitive document. If the matching logic is too broad, DLP becomes noise instead of a control.

Microsoft Purview custom sensitive information type pattern configuration

The primary element is the signal Purview has to find before the custom type can match. In a CUI banner policy, I want the match anchored to the actual banner text instead of a loose keyword that might appear in normal conversation.

Microsoft Purview primary element configuration for a custom sensitive information type

Supporting elements tighten the match. They add context so the classifier is not only reacting to one string in isolation. This is the difference between matching a real CUI marking pattern and catching random text that happens to look close.

Microsoft Purview supporting elements configuration for a custom sensitive information type

The confidence level controls how Purview treats the match. Microsoft documents confidence levels as a tradeoff between false positives and false negatives. For this kind of banner-style custom type, I would rather be deliberate than noisy because noisy DLP policies are the ones users learn to ignore.

Microsoft Purview custom sensitive information type confidence level selection

The finish screen is the checkpoint before the classifier becomes something I can attach to the DLP policy. If the detection object is vague, misnamed, or built around the wrong signal, the policy and the report will both inherit that problem.

Microsoft Purview custom sensitive information type finish screen

The rule is where the two pieces meet. The DLP policy controls the workflow, but the sensitive information type tells the rule what to look for. Once the CUI banner type is selected in the rule, the policy can warn on matching email instead of acting on a generic condition.

Microsoft Purview DLP policy rule configuration step

The override settings are the reason this script exists. If the policy allows the user to continue with a business justification, that justification should not disappear into a log nobody reads. The feature is only useful if the exception gets reviewed.

Microsoft Purview DLP policy override settings step

The policy mode controls how much impact this has while the rule is being tested. Audit and test modes are useful when the sensitive information type is new. Enforcement is different because users will see the warning and may be allowed to override it.

Microsoft Purview DLP policy mode step

The finish screen is the last chance to confirm the whole workflow: Exchange is in scope, the rule uses the right sensitive information type, overrides behave the way the business expects, and the policy mode matches the rollout plan.

Microsoft Purview DLP policy finish step

This is the point where the article moves from configuration to operations. Purview can ask the user for a justification, but the administrator still needs a practical way to review those justifications after the messages are sent.

The reporting gap

The script here uses Export-ActivityExplorerData, which Microsoft says exports activities from Data classification > Activity Explorer in the Microsoft 365 Purview compliance portal. Activity Explorer reports up to 30 days of data. The cmdlet supports activity filters such as DLPRuleMatch, workload filters such as Exchange, JSON output, page size, and result fields including FalsePositive, Justification, Sender, Receivers, Subject, PolicyName, and RuleName.

The local PowerShell script asks for a number of days, calculates the search window, exports Activity Explorer data, filters events that look like overrides, and writes a CSV to the desktop.

To run it, I use PowerShell with the Exchange Online PowerShell module installed. The account has to be able to connect to Security & Compliance PowerShell and read the Purview activity data. If the module is not installed yet, install it for the current user first:

Install-Module ExchangeOnlineManagement -Scope CurrentUser

Then save the script as something like Get-DLPOverrides.ps1, change admin@example.com to the admin account you actually use, and run it from PowerShell:

cd "$HOME\Desktop"
.\Get-DLPOverrides.ps1

The script imports the module, opens the Security & Compliance PowerShell connection with Connect-IPPSSession, prompts for the number of days to search, and then writes DLPOverrides.csv to the signed-in user’s Desktop. If you enter 0, it searches from local midnight through the current time. If you enter 7, it searches the last seven days.

Here is a sanitized version of the script with comments added:

Import-Module ExchangeOnlineManagement

# Connect to Security & Compliance PowerShell.
# Replace the UPN with an admin account that has the required Purview permissions.
Connect-IPPSSession -UserPrincipalName admin@example.com

# Ask for a whole number of days to search.
do {
    $daysInput = Read-Host "How many days ago do you want to search? Enter 0 for today (midnight through now)"
    $days = 0
    $isValid = [int]::TryParse($daysInput, [ref]$days) -and $days -ge 0

    if (-not $isValid) {
        Write-Host "Please enter a whole number that is 0 or greater." -ForegroundColor Yellow
    }
} until ($isValid)

$end = Get-Date

if ($days -eq 0) {
    # Today means local midnight through now.
    $start = $end.Date
    $rangeDescription = "today from $($start.ToString('yyyy-MM-dd h:mm tt')) through $($end.ToString('yyyy-MM-dd h:mm tt'))"
}
else {
    # For any other value, search a rolling 24-hour window per day.
    $start = $end.AddDays(-$days)
    $hours = $days * 24
    $rangeDescription = "the last $days day(s) ($hours hours), from $($start.ToString('yyyy-MM-dd h:mm tt')) through $($end.ToString('yyyy-MM-dd h:mm tt'))"
}

Write-Host "Searching $rangeDescription..." -ForegroundColor Cyan

# Pull Exchange DLP rule match activity from Activity Explorer.
$result = Export-ActivityExplorerData `
    -StartTime $start `
    -EndTime $end `
    -OutputFormat Json `
    -PageSize 500 `
    -Filter1 @("Activity", "DLPRuleMatch") `
    -Filter2 @("Workload", "Exchange")

$rows = $result.ResultData | ConvertFrom-Json

# Treat a populated justification or false positive flag as an override signal.
$overrides = $rows | Where-Object {
    ($_.Justification -and $_.Justification.Trim() -ne "") -or
    ($_.FalsePositive -eq $true) -or
    ($_.FalsePositive -eq "True")
}

if (-not $overrides -or $overrides.Count -eq 0) {
    Write-Host "No DLP override events found for $rangeDescription."
    exit
}

$report = $overrides |
    Sort-Object Happened -Descending |
    ForEach-Object {
        [PSCustomObject]@{
            Happened      = $_.Happened
            Sender        = $_.EmailInfo.Sender
            Receivers     = ($_.EmailInfo.Receivers -join "; ")
            Subject       = $_.EmailInfo.Subject
            PolicyName    = $_.PolicyMatchInfo.PolicyName
            RuleName      = $_.PolicyMatchInfo.RuleName
            Reason        = $_.Reason
            Attachment    = ($_.AttachmentDetails.Name -join "; ")
            Justification = $_.Justification
        }
    }

$report | Format-Table -Wrap -AutoSize

$outputPath = Join-Path ([Environment]::GetFolderPath('Desktop')) 'DLPOverrides.csv'
$report | Export-Csv $outputPath -NoTypeInformation
Write-Host "CSV saved to $outputPath" -ForegroundColor Green

The local test output showed the behavior I wanted. A short search returned no override events. A wider search returned override rows and wrote DLPOverrides.csv. In the public example, I would sanitize the people and recipients:

Happened              Sender                Receivers
--------              ------                ---------
2026-07-31 5:23 PM    user1@example.com     recipient1@example.com; recipient2@example.com
2026-07-30 5:42 PM    user2@example.com     recipient3@example.com; recipient4@example.com

That is enough for a review queue. If I need more, I can extend the CSV with message subject, attachment names, rule name, policy name, and justification.

Why overrides belong in the process

Overrides are useful when the business process is real and the DLP policy is tuned well.

They are a problem when the policy is noisy, the justifications are vague, or nobody reviews the exceptions. If users are overriding constantly, the sensitive information type may be too broad, the rule may be scoped incorrectly, or the business process may need a better approved channel.

The goal is not to shame users for clicking override. The goal is to know when they did it, why they did it, and whether the policy is still producing the right behavior.

Sources

Learn about data loss prevention

Create and deploy a data loss prevention policy

Learn about sensitive information types

Data loss prevention policy tip reference for new Outlook for Windows

Export-ActivityExplorerData

AI Usage Transparency Report

AI Era · Written during widespread use of AI tools

AI Signal Composition

Rep Tone Struct List Instr
Repetition: 33%
Tone: 52%
Structure: 45%
List: 5%
Instructional: 27%
Emoji: 0%

Score: 0.23 · Moderate AI Influence

Summary

Data Loss Prevention (DLP) is a part of Microsoft Purview that watches for sensitive information leaving places like Exchange, SharePoint, OneDrive, Teams, and endpoint devices.

Related Posts