←
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