Private/ResponseHelpers.ps1

<#
    The MDE hand-back payload (Schema 1), carried in an NSP.Toolkit Response hand-off as
    <Company>_MDE_Response.json. The tech brings it back to fill in the EDR exclusion request.
 
      SchemaVersion, Tool, ToolVersion, Generated, Company, ComputerName, Domain, OperatingSystem
      Mde Get-NSPMDEStatus facts (sensor, onboarding, AV mode and versions, exclusions)
      Application Name (as the tech knows it), Candidates (Get-NSPMDECandidateApp)
      Capture ProcMon version, start/end, backing-file name, size and SHA-256, CSV name
      Evidence Violations (the app refused), HeldByEdr, HeldByAv, HeldByOther, HolderNotSeen,
                     DefenderRefused (Defender refused - not evidence, not in the lists), Verdict,
                     ContestedPaths (path, violations, processes, holders),
                     Collisions (the first -MaxCollisions, holders included)
      SuggestedExclusion Processes (the violating executables, full paths where known),
                     Folders (the contested files' folders) - a starting point, not a decision
      TechNotes
#>


$script:NSPMDEResponseSchema = 1

function Select-NSPMDEAppCollision {
    # PURE. The collisions that count as evidence: the app was refused. Defender itself being
    # refused (reading a file the app holds exclusively) is harmless to the app.
    param([AllowNull()][object[]]$Collision)
    @($Collision | Where-Object { $_ -and $_.ProcessKind -notin 'EDR', 'AV' })
}

function Get-NSPMDEEvidenceSummary {
    # PURE. Counts and the one-line verdict for a set of collisions.
    param([AllowNull()][object[]]$Collision)
    $defenderRefused = @($Collision | Where-Object { $_ -and $_.ProcessKind -in 'EDR', 'AV' }).Count
    $all = @(Select-NSPMDEAppCollision -Collision $Collision)
    $edr = @($all | Where-Object HolderKind -eq 'EDR').Count
    $av = @($all | Where-Object HolderKind -eq 'AV').Count
    $other = @($all | Where-Object HolderKind -eq 'Other').Count
    $notSeen = @($all | Where-Object HolderKind -eq 'NotSeen').Count
    $verdict = if (-not $all.Count) {
        'No sharing violations were captured. Reproduce the failure during a capture.'
    } elseif ($edr) {
        "The EDR sensor (MsSense.exe) held the file in $edr of $($all.Count) sharing violation(s). An antivirus exclusion will not stop the sensor; a Defender for Endpoint EDR (Sense) exclusion is required."
    } elseif ($av) {
        "The antivirus engine (MsMpEng.exe) held the file in $av of $($all.Count) sharing violation(s), the sensor in none. A Defender antivirus exclusion should resolve it; this does not support an EDR exclusion request."
    } elseif ($other) {
        "Sharing violations were captured, but Defender held none of the files ($other held by other processes)."
    } else {
        "Sharing violations were captured, but the holder opened the file before the capture began. Start the capture before the app opens the file."
    }
    [pscustomobject][ordered]@{
        Violations      = $all.Count
        HeldByEdr       = $edr
        HeldByAv        = $av
        HeldByOther     = $other
        HolderNotSeen   = $notSeen
        DefenderRefused = $defenderRefused
        Verdict         = $verdict
    }
}

function New-NSPMDEResponsePayload {
    # PURE apart from the clock and host facts. Builds the Schema 1 object.
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Company,
        [string]$ApplicationName,
        $Status,
        [object[]]$Candidate,
        $Capture,
        [object[]]$Collision,
        [string]$TechNotes,
        [int]$MaxCollisions = 25,
        [string]$ToolVersion = '',
        [string]$BackingFileSha256 = '',
        [Nullable[long]]$BackingFileBytes = $null,
        [string]$CsvFile = ''
    )

    $summary = Get-NSPMDEEvidenceSummary -Collision $Collision
    $collisions = @(Select-NSPMDEAppCollision -Collision $Collision)
    $candidates = @($Candidate | Where-Object { $_ })

    $contested = @($collisions | Group-Object Path | Sort-Object Count -Descending | ForEach-Object {
            [pscustomobject][ordered]@{
                Path       = $_.Name
                Violations = $_.Count
                Processes  = @($_.Group.ProcessName | Sort-Object -Unique)
                Holders    = @($_.Group | ForEach-Object { $_.Holders } | ForEach-Object ProcessName | Sort-Object -Unique)
            }
        })

    # Full paths for the violating executables where the crash events supplied them.
    $exePaths = @{}
    foreach ($c in $candidates) { foreach ($p in @($c.ExePaths)) { if ($p) { $exePaths[[string]$c.ExeName] = [string]$p } } }
    $processes = @($collisions | Where-Object { $_.HolderKind -in 'EDR', 'AV' } | ForEach-Object ProcessName | Sort-Object -Unique | ForEach-Object {
            if ($exePaths.ContainsKey($_)) { $exePaths[$_] } else { $_ }
        })
    $folders = @($collisions | Where-Object { $_.HolderKind -in 'EDR', 'AV' } | ForEach-Object {
            $parent = Split-Path -Parent ([string]$_.Path)
            if ($parent) { $parent + '\' }
        } | Sort-Object -Unique)

    $statusFields = 'MsSensePath', 'MsSenseVersion', 'SenseState', 'SenseStartMode', 'Onboarded', 'OnboardingState', 'OrgId',
    'AMRunningMode', 'AMProductVersion', 'AMEngineVersion', 'AntivirusSignatureVersion', 'AntivirusEnabled',
    'RealTimeProtectionEnabled', 'IsTamperProtected', 'ExclusionsVisible', 'ExclusionPath', 'ExclusionProcess', 'ExclusionExtension'
    $mde = [ordered]@{}
    foreach ($f in $statusFields) { $mde[$f] = if ($Status -and $Status.PSObject.Properties[$f]) { $Status.$f } else { $null } }

    $domain = try { [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name } catch { "$env:USERDNSDOMAIN" }
    [pscustomobject][ordered]@{
        SchemaVersion      = $script:NSPMDEResponseSchema
        Tool               = 'NSP.Win.MDE'
        ToolVersion        = $ToolVersion
        Generated          = (Get-Date).ToString('s')
        Company            = $Company
        ComputerName       = "$env:COMPUTERNAME"
        Domain             = $domain
        OperatingSystem    = if ($Status) { [string]$Status.OperatingSystem } else { '' }
        Mde                = [pscustomobject]$mde
        Application        = [pscustomobject][ordered]@{
            Name       = [string]$ApplicationName
            Candidates = @($candidates | ForEach-Object {
                    [pscustomobject][ordered]@{
                        ExeName        = [string]$_.ExeName
                        Hits           = $_.Hits
                        ExePaths       = @($_.ExePaths)
                        FirstSeen      = if ($_.FirstSeen) { ([datetime]$_.FirstSeen).ToString('s') } else { '' }
                        LastSeen       = if ($_.LastSeen) { ([datetime]$_.LastSeen).ToString('s') } else { '' }
                        ExceptionCodes = @($_.ExceptionCodes)
                        EventIds       = @($_.EventIds)
                    }
                })
        }
        Capture            = if ($Capture) {
            [pscustomobject][ordered]@{
                ProcmonVersion    = [string]$Capture.ProcmonVersion
                Started           = if ($Capture.Started) { ([datetime]$Capture.Started).ToString('s') } else { '' }
                Ended             = if ($Capture.Ended) { ([datetime]$Capture.Ended).ToString('s') } else { '' }
                BackingFile       = if ($Capture.BackingFile) { Split-Path -Leaf $Capture.BackingFile } else { '' }
                BackingFileBytes  = $BackingFileBytes
                BackingFileSha256 = $BackingFileSha256
                CsvFile           = $CsvFile
                CoveredSeconds    = if ($Capture.PSObject.Properties['CoveredSeconds']) { $Capture.CoveredSeconds } else { $null }
                Incomplete        = if ($Capture.PSObject.Properties['Incomplete']) { [bool]$Capture.Incomplete } else { $null }
            }
        } else { $null }
        Evidence           = [pscustomobject][ordered]@{
            Violations     = $summary.Violations
            HeldByEdr      = $summary.HeldByEdr
            HeldByAv       = $summary.HeldByAv
            HeldByOther    = $summary.HeldByOther
            HolderNotSeen  = $summary.HolderNotSeen
            DefenderRefused = $summary.DefenderRefused
            Verdict        = $summary.Verdict
            ContestedPaths = $contested
            Collisions     = @($collisions | Select-Object -First $MaxCollisions | ForEach-Object {
                    [pscustomobject][ordered]@{
                        Time          = $_.Time
                        ProcessName   = $_.ProcessName
                        PID           = $_.PID
                        Path          = $_.Path
                        DesiredAccess = $_.DesiredAccess
                        ShareMode     = $_.ShareMode
                        HolderKind    = $_.HolderKind
                        Holders       = @($_.Holders)
                    }
                })
        }
        SuggestedExclusion = [pscustomobject][ordered]@{
            Processes = $processes
            Folders   = $folders
        }
        TechNotes          = [string]$TechNotes
    }
}