Zum Inhalt
Kaffeeundcode
Skriptbibliothek
PowerShell 06.10.2026

PowerShell

Get IntuneEpmElevationAudit

367_Get-IntuneEpmElevationAudit.ps1

<#
.SYNOPSIS
    Exportiert Ereignisse aus Intune Endpoint Privilege Management.
.DESCRIPTION
    <!-- library-status:start -->
    Prüfstatus: Ungeprüft
    Windows- und Tenant-Abnahme ausstehend; keine pauschale Produktionsfreigabe.

    <!-- library-status:end -->

    Liest den offiziellen EPM-Elevation-Event-Report und begrenzt ihn lokal auf den
    gewaehlten Zeitraum. Abgelehnte oder fehlgeschlagene Elevations werden als
    Auffaellig markiert. EPM und die Reportberechtigung EpmPolicy.ViewReports muessen
    im Tenant lizenziert und fuer die angemeldete Rolle freigegeben sein.
.EXAMPLE
    ./17_Intune_Workflows/367_Get-IntuneEpmElevationAudit.ps1 -Days 14 -OnlyFindings -OutputPath './reports/epm-events.csv'
#>
[CmdletBinding()]
param(
    [ValidateRange(1,365)][int]$Days = 30,
    [switch]$OnlyFindings,
    [string]$TenantId,
    [switch]$SkipConnect,
    [string]$OutputPath
)

# kc-bundle:graph:start sha256=168d7f232db5d14cf1be94255b5d535f3739213158e781a4214040a070571864
# Eingebettete Hilfslogik aus Common/IntuneLibrary.psm1; durch tools/bundle-script-dependencies.mjs gepflegt.
New-Module -Name IntuneLibrary -ScriptBlock {
#requires -Version 5.1
Set-StrictMode -Version Latest

function Connect-IlGraph {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string[]]$Scopes, [string]$TenantId, [switch]$SkipConnect)
    if (-not (Get-Command Get-MgContext -ErrorAction SilentlyContinue)) {
        throw 'Microsoft.Graph.Authentication fehlt. Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
    }
    $context = Get-MgContext
    if (-not $SkipConnect -and (-not $context -or ($TenantId -and $context.TenantId -ne $TenantId))) {
        $arguments = @{ Scopes = $Scopes; ErrorAction = 'Stop'; NoWelcome = $true; ContextScope = 'Process' }
        if ($TenantId) { $arguments.TenantId = $TenantId }
        Connect-MgGraph @arguments | Out-Null
        $context = Get-MgContext
    }
    if (-not $context) { throw 'Keine aktive Graph-Sitzung.' }
    if ($TenantId -and $context.TenantId -ne $TenantId) { throw 'Die aktive Graph-Sitzung gehoert zu einem anderen Tenant.' }
    if ($context.AuthType -eq 'Delegated') {
        $missing = @($Scopes | Where-Object { $_ -notin $context.Scopes })
        if ($missing.Count) { throw "Der Sitzung fehlen angeforderte Scopes: $($missing -join ', '). Neu mit diesen Scopes anmelden." }
    }
}

function Assert-IlGraphUri {
    param([Parameter(Mandatory)][string]$Uri)
    $parsed = [uri]$Uri
    if (-not $parsed.IsAbsoluteUri -or $parsed.Scheme -ne 'https' -or $parsed.Host -ne 'graph.microsoft.com' -or $parsed.UserInfo -or $parsed.Port -ne 443) {
        throw 'Nur HTTPS-Anfragen an graph.microsoft.com sind erlaubt (Global Cloud).'
    }
    if ($parsed.AbsolutePath -notmatch '^/(v1.0|beta)/') { throw 'Graph-API-Version fehlt.' }
}

function Invoke-IlGraph {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Uri,
        [ValidateSet('GET','POST','PATCH','DELETE')][string]$Method = 'GET',
        [object]$Body,
        [ValidateRange(0,8)][int]$MaxRetries = 4
    )
    Assert-IlGraphUri $Uri
    for ($attempt = 0; ; $attempt++) {
        try {
            $arguments = @{ Uri = $Uri; Method = $Method; OutputType = 'PSObject'; ErrorAction = 'Stop' }
            if ($PSBoundParameters.ContainsKey('Body')) {
                $arguments.Body = ConvertTo-Json -InputObject $Body -Depth 100 -Compress
                $arguments.ContentType = 'application/json'
            }
            return Invoke-MgGraphRequest @arguments
        } catch {
            $status = 0
            $delay = [math]::Min(60, [math]::Pow(2, $attempt))
            if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response) {
                $response = $_.Exception.Response
                if ($response.PSObject.Properties['StatusCode']) { $status = [int]$response.StatusCode }
                if ($response.PSObject.Properties['Headers'] -and $response.Headers) {
                    try {
                        $retryAfter = $response.Headers.RetryAfter
                        if ($retryAfter.Delta) { $delay = [math]::Ceiling($retryAfter.Delta.TotalSeconds) }
                        elseif ($retryAfter.Date) { $delay = [math]::Ceiling(($retryAfter.Date - [DateTimeOffset]::UtcNow).TotalSeconds) }
                    } catch { Write-Verbose 'Retry-After nicht lesbar; exponentieller Backoff.' }
                }
            }
            # Mutationen niemals automatisch wiederholen: ihre Annahme kann unklar sein.
            if ($Method -ne 'GET' -or $status -notin @(429,503,504) -or $attempt -ge $MaxRetries) { throw }
            if ($delay -gt 300) { throw 'Server fordert mehr als 300 Sekunden Wartezeit; Lauf spaeter erneut starten.' }
            Start-Sleep -Seconds ([math]::Max(1,$delay))
        }
    }
}

function Get-IlGraphCollection {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Uri, [ValidateRange(1,100000)][int]$MaxPages = 10000)
    $seen = @{}
    $rows = New-Object 'System.Collections.Generic.List[object]'
    while ($Uri) {
        if ($seen.ContainsKey($Uri)) { throw 'Wiederholter Graph-nextLink; unvollstaendige Abfrage verworfen.' }
        if ($seen.Count -ge $MaxPages) { throw 'Seitenlimit erreicht; unvollstaendige Abfrage verworfen.' }
        $seen[$Uri] = $true
        $page = Invoke-IlGraph -Uri $Uri
        if (-not $page -or -not $page.PSObject.Properties['value']) { throw "Keine Graph-Collection: $Uri" }
        foreach ($row in @($page.value)) { if ($null -ne $row) { $rows.Add($row) } }
        $Uri = if ($page.PSObject.Properties['@odata.nextLink']) { [string]$page.'@odata.nextLink' } else { $null }
    }
    return $rows.ToArray()
}

function ConvertTo-IlSegment {
    param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Value)
    return [uri]::EscapeDataString($Value)
}

function Resolve-IlDevice {
    [CmdletBinding(DefaultParameterSetName='Name')]
    param(
        [Parameter(Mandatory,ParameterSetName='Id')][string]$DeviceId,
        [Parameter(Mandatory,ParameterSetName='Name')][string]$DeviceName,
        [Parameter(Mandatory,ParameterSetName='Serial')][string]$SerialNumber
    )
    $base = 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices'
    if ($DeviceId) { return Invoke-IlGraph -Uri ($base + '/' + (ConvertTo-IlSegment $DeviceId)) }
    $field = if ($PSCmdlet.ParameterSetName -eq 'Serial') { 'serialNumber' } else { 'deviceName' }
    $value = if ($SerialNumber) { $SerialNumber } else { $DeviceName }
    $filter = [uri]::EscapeDataString("$field eq '$($value.Replace("'","''"))'")
    $devices = @(Get-IlGraphCollection -Uri ($base + '?$filter=' + $filter))
    if ($devices.Count -ne 1) { throw "$($devices.Count) Geraete gefunden. Eine eindeutige DeviceId verwenden." }
    return $devices[0]
}

function Get-IlProperty {
    param([AllowNull()][object]$Object, [Parameter(Mandatory)][string]$Name, [object]$Default = $null)
    if ($null -eq $Object) { return $Default }
    if ($Object -is [System.Collections.IDictionary]) {
        if ($Object.Contains($Name)) { return $Object[$Name] }
    } elseif ($Object.PSObject.Properties[$Name]) { return $Object.$Name }
    return $Default
}

function New-IlFinding {
    param([string]$Check, [ValidateSet('OK','Auffaellig','Nicht anwendbar','Nicht pruefbar')][string]$Status,
          [string]$ObjectId, [string]$Detail, [object]$Data)
    [pscustomobject][ordered]@{ Check=$Check; Status=$Status; ObjectId=$ObjectId; Detail=$Detail; Data=$Data }
}

function Export-IlResult {
    [CmdletBinding()]
    param([AllowNull()][object]$Data, [string]$OutputPath)
    if ($OutputPath) {
        $parent = Split-Path $OutputPath -Parent
        if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
        if ([IO.Path]::GetExtension($OutputPath) -eq '.csv') {
            @($Data) | Export-Csv -LiteralPath $OutputPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop
        } else {
            ConvertTo-Json -InputObject @($Data) -Depth 100 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 -ErrorAction Stop
        }
    }
    return $Data
}

Export-ModuleMember -Function Connect-IlGraph,Invoke-IlGraph,Get-IlGraphCollection,ConvertTo-IlSegment,Resolve-IlDevice,Get-IlProperty,New-IlFinding,Export-IlResult

} | Import-Module -Scope Local -Force
# kc-bundle:graph:end
# kc-bundle:report:start sha256=7528259aa984c181a8e114d1586c9b91eb8d521cfb6233b4b0144570c888f49a
# Eingebettete Hilfslogik aus Common/IntuneReportLibrary.psm1; durch tools/bundle-script-dependencies.mjs gepflegt.
New-Module -Name IntuneReportLibrary -ScriptBlock {
#requires -Version 5.1
Set-StrictMode -Version Latest

function Assert-IlExportDownloadUri {
    param([Parameter(Mandatory)][string]$Uri)
    $parsed = [uri]$Uri
    if (-not $parsed.IsAbsoluteUri) {
        throw 'Ungueltige Export-Downloadadresse. Erwartet wird HTTPS auf Azure Blob Storage.'
    }
    $hostName = $parsed.DnsSafeHost.ToLowerInvariant()
    $allowedHost = $hostName.EndsWith('.blob.core.windows.net') -or $hostName.EndsWith('.blob.storage.azure.net')
    if ($parsed.Scheme -ne 'https' -or $parsed.UserInfo -or $parsed.Port -ne 443 -or -not $allowedHost) {
        throw 'Ungueltige Export-Downloadadresse. Erwartet wird HTTPS auf Azure Blob Storage.'
    }
}

function Invoke-IlReportExport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidatePattern('^[A-Za-z0-9]+$')][string]$ReportName,
        [string]$Filter,
        [string[]]$Select,
        [ValidateSet('v1.0','beta')][string]$ApiVersion = 'beta',
        [ValidateRange(10,1800)][int]$MaxWaitSeconds = 300,
        [ValidateRange(1,30)][int]$PollIntervalSeconds = 3
    )
    $base = "https://graph.microsoft.com/$ApiVersion/deviceManagement/reports/exportJobs"
    $body = [ordered]@{ reportName=$ReportName; format='csv' }
    if ($Filter) { $body.filter = $Filter }
    if ($Select -and $Select.Count) { $body.select = @($Select) }

    $job = IntuneLibraryInvoke-IlGraph -Uri $base -Method POST -Body $body
    $jobId = [string](IntuneLibraryGet-IlProperty $job 'id')
    if (-not $jobId) { throw "Exportjob fuer $ReportName lieferte keine ID." }

    $deadline = [datetime]::UtcNow.AddSeconds($MaxWaitSeconds)
    $jobUri = "$base/$(IntuneLibraryConvertTo-IlSegment $jobId)"
    do {
        $state = IntuneLibraryInvoke-IlGraph -Uri $jobUri
        $status = [string](IntuneLibraryGet-IlProperty $state 'status')
        if ($status -eq 'completed') { break }
        if ($status -in @('failed','unknown')) {
            $reason = [string](IntuneLibraryGet-IlProperty $state 'localizedFailureReason' (IntuneLibraryGet-IlProperty $state 'error'))
            if (-not $reason) { $reason = 'kein Fehlertext gemeldet' }
            throw "Exportjob fuer $ReportName fehlgeschlagen: $reason"
        }
        if ([datetime]::UtcNow -ge $deadline) { throw "Zeitlimit fuer Exportjob $ReportName erreicht (Status: $status)." }
        Start-Sleep -Seconds $PollIntervalSeconds
    } while ($true)

    $downloadUri = [string](IntuneLibraryGet-IlProperty $state 'url')
    if (-not $downloadUri) { throw "Abgeschlossener Exportjob fuer $ReportName lieferte keine Downloadadresse." }
    Assert-IlExportDownloadUri -Uri $downloadUri

    $temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ("kc-intune-export-" + [guid]::NewGuid().ToString('N'))
    $archivePath = Join-Path $temporaryRoot 'report.zip'
    $extractPath = Join-Path $temporaryRoot 'content'
    try {
        New-Item -ItemType Directory -Path $temporaryRoot -Force -ErrorAction Stop | Out-Null
        Invoke-WebRequest -Uri $downloadUri -OutFile $archivePath -UseBasicParsing -ErrorAction Stop | Out-Null
        Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force -ErrorAction Stop
        $csvFiles = @(Get-ChildItem -LiteralPath $extractPath -Filter '*.csv' -File -Recurse -ErrorAction Stop)
        if (-not $csvFiles.Count) { throw "Exportarchiv fuer $ReportName enthaelt keine CSV-Datei." }
        $rows = New-Object 'System.Collections.Generic.List[object]'
        foreach ($csvFile in $csvFiles) {
            foreach ($row in @(Import-Csv -LiteralPath $csvFile.FullName -ErrorAction Stop)) { $rows.Add($row) }
        }
        return $rows.ToArray()
    } finally {
        if (Test-Path -LiteralPath $temporaryRoot) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue }
    }
}

Export-ModuleMember -Function Invoke-IlReportExport

} | Import-Module -Scope Local -Force
# kc-bundle:report:end
Connect-IlGraph -Scopes @('DeviceManagementConfiguration.Read.All','DeviceManagementManagedDevices.Read.All') -TenantId $TenantId -SkipConnect:$SkipConnect
$rows = @(Invoke-IlReportExport -ReportName 'EpmElevationReportElevationEvent')
$cutoff = [datetimeoffset]::UtcNow.AddDays(-$Days)
$result = New-Object 'System.Collections.Generic.List[object]'

foreach ($row in $rows) {
    $rawDate = [string](Get-IlProperty $row 'EventDateTime')
    $eventDate = [datetimeoffset]::MinValue
    $status = 'OK'
    $detail = 'Elevation-Ereignis gelesen.'
    if (-not [datetimeoffset]::TryParse($rawDate,[ref]$eventDate)) {
        $status = 'Nicht pruefbar'; $detail = 'EventDateTime ist nicht auswertbar.'
    } elseif ($eventDate.ToUniversalTime() -lt $cutoff) { continue }
    $reportedResult = [string](Get-IlProperty $row 'Result')
    if ($reportedResult -match '(?i)deny|denied|fail|error|block') { $status='Auffaellig'; $detail="Gemeldetes Ergebnis: $reportedResult" }
    if ($OnlyFindings -and $status -eq 'OK') { continue }
    $result.Add([pscustomobject][ordered]@{
        Status=$status;EventDateTime=$rawDate;Result=$reportedResult;ElevationType=Get-IlProperty $row 'ElevationType';DeviceId=Get-IlProperty $row 'DeviceId';DeviceName=Get-IlProperty $row 'DeviceName'
        UPN=Get-IlProperty $row 'Upn';FilePath=Get-IlProperty $row 'FilePath';ProductName=Get-IlProperty $row 'ProductName';FileVersion=Get-IlProperty $row 'FileVersion';Hash=Get-IlProperty $row 'Hash'
        Publisher=Get-IlProperty $row 'CompanyName';ParentProcessName=Get-IlProperty $row 'ParentProcessName';Justification=Get-IlProperty $row 'Justification';PolicyId=Get-IlProperty $row 'PolicyId';PolicyName=Get-IlProperty $row 'PolicyName';RuleId=Get-IlProperty $row 'RuleId';Detail=$detail
    })
}

if (-not $result.Count) {
    $status = if ($rows.Count) { 'OK' } else { 'Nicht anwendbar' }
    $detail = if ($rows.Count) { "Keine EPM-Auffaelligkeiten innerhalb der letzten $Days Tage." } else { 'Keine EPM-Ereignisse gemeldet; Lizenzierung, Einsatz und Reportberechtigung separat pruefen.' }
    $result.Add((New-IlFinding -Check 'EpmElevationEvents' -Status $status -Detail $detail))
}
Export-IlResult -Data $result.ToArray() -OutputPath $OutputPath