Public/Get-NSPM365AccessedMessage.ps1

function Get-NSPM365AccessedMessage {
    <#
    .SYNOPSIS
        Expands MailItemsAccessed audit records into one row per message read, with the access type (Bind or Sync), client IP and app.
    .DESCRIPTION
        A Bind record lists the individual messages opened: one row per message, with Subject,
        InternetMessageId and size. A Sync record means a client downloaded whole folders and has
        no per-message detail: one row per folder, Subject empty. Treat a Sync from an attacker's
        IP as every message in that folder read.
 
        InternetMessageId is what to search for (message trace, Graph, eDiscovery) to pull the
        messages themselves.
 
        Reads Purview audit CSV exports (-Path) or parsed records (pipeline); records other than
        MailItemsAccessed are skipped.
    .PARAMETER Path
        One or more Purview audit CSV exports.
    .PARAMETER InputObject
        Parsed audit records (ConvertFrom-NSPM365AuditCsv).
    .EXAMPLE
        Get-NSPM365AccessedMessage -Path .\audit.csv | Where-Object ClientIP -eq 203.0.113.5 | Export-Csv .\read-by-attacker.csv -NoTypeInformation
    .EXAMPLE
        Get-NSPM365AccessedMessage -Path .\audit.csv | Group-Object ClientIP, MailAccessType | Sort-Object Count -Descending
    #>

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Path')]
        [string[]]$Path,
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Record')]
        [psobject[]]$InputObject
    )

    begin {
        Import-NSPLogParse
        $expand = {
            param($Record)
            if ([string]$Record.Operation -ne 'MailItemsAccessed' -or $null -eq $Record.AuditData) { return }
            $audit = $Record.AuditData
            $properties = if ($audit.PSObject.Properties['OperationProperties']) { $audit.OperationProperties } else { @() }
            $value = { param($n) if ($audit.PSObject.Properties[$n]) { [string]$audit.$n } else { '' } }
            $common = [ordered]@{
                TimeUtc          = Get-NSPAuditTime $Record
                UserId           = [string]$Record.UserId
                MailboxOwnerUPN  = & $value 'MailboxOwnerUPN'
                ClientIP         = Get-NSPAuditClientIp $audit
                MailAccessType   = [string](Get-NSPAuditNameValue $properties 'MailAccessType')
                IsThrottled      = [string](Get-NSPAuditNameValue $properties 'IsThrottled')
                AppId            = & $value 'AppId'
                ClientAppId      = & $value 'ClientAppId'
                ClientInfoString = & $value 'ClientInfoString'
            }
            foreach ($row in (Expand-NSPM365AuditRecord -InputObject $Record -ItemSource FolderItems -IncludeEmpty)) {
                $item = $row.Item
                $line = [ordered]@{}
                foreach ($key in $common.Keys) { $line[$key] = $common[$key] }
                $line.FolderPath = if ($row.Folder -and $row.Folder.PSObject.Properties['Path']) { [string]$row.Folder.Path } else { '' }
                $line.Subject = if ($item -and $item.PSObject.Properties['Subject']) { [string]$item.Subject } else { '' }
                $line.InternetMessageId = if ($item -and $item.PSObject.Properties['InternetMessageId']) { [string]$item.InternetMessageId } else { '' }
                $line.SizeBytes = if ($item -and $item.PSObject.Properties['SizeInBytes']) { $item.SizeInBytes } else { $null }
                $line.RecordId = [string]$Record.RecordId
                [pscustomobject]$line
            }
        }
    }
    process { foreach ($record in $InputObject) { if ($null -ne $record) { & $expand $record } } }
    end {
        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            foreach ($record in (Read-NSPAuditCsv -Path $Path)) { & $expand $record }
        }
    }
}