Den Überblick über neu registrierte Geräte in deiner Organisation zu behalten, kann eine herausfordernde Aufgabe sein, wenn du dich ausschließlich auf die Intune-Konsole verlässt. Wäre es nicht großartig, automatisch einen umfassenden Bericht per E-Mail zu erhalten? Wie du weißt, liebe ich es, Dinge zu automatisieren. In diesem Blogbeitrag schauen wir uns eine einfache und effiziente Möglichkeit an, mit PowerShell, Azure Automation Runbooks und der Microsoft Graph API einen wöchentlichen Bericht über alle neu registrierten Geräte zu erstellen. Diese automatisierte Lösung spart dir Zeit und Aufwand, sodass du dich auf wichtigere Aufgaben bei der Verwaltung der Geräte deiner Organisation konzentrieren kannst. Also, lass uns loslegen und lernen, wie du diesen wertvollen Bericht erstellst!

Inhaltsverzeichnis
Wie kommst du an das Skript?
Du findest das Skript in meinem GitHub-Repository. Bevor du das Skript ausführst, musst du die Absender- und Empfänger-E-Mail-Adresse anpassen:

Was ist der Zweck des Skripts?
Dieses PowerShell-Skript ruft alle in den letzten 7 Tagen registrierten Geräte ab, erstellt daraus eine CSV-Datei und hängt die CSV an eine E-Mail an.
Wie kann ich den Bericht planen?
Um den Bericht zu planen, kannst du ein Azure Automation Runbook erstellen und dich über eine App-Registrierung authentifizieren. Das Einzige, was du tun musst, ist, den Inhalt des Skripts in das Runbook einzufügen.
Wie erstellst du die Automatisierung?
Eine App-Registrierung erstellen
- Suche nach Microsoft Entra ID

- Wähle App registration

- Wähle +New registration

- Gib einen Namen ein und klicke auf Register

- Klicke auf API permissions und +Add a permission

- Wähle Microsoft Graph

- Wähle Application permissions

- Suche nach DeviceManagementApps.Read.All & Mail.Send

- Klicke auf Grant admin consent for *** und bestätige mit Yes

- Wähle Certificates & secrets und klicke auf +New client secret

- Gib eine Description ein und wähle eine Expires time
- Klicke auf Add

- Kopiere und speichere den Value und die Secret ID

Automation Account erstellen
- Suche nach Automation Accounts

- Klicke auf + Create

- Wähle ein Subscription und eine Resource group
- Gib einen account name ein und wähle eine Region
- Klicke auf Next

- Klicke auf Next

- Klicke auf Next -> Next -> Create

Das Runbook erstellen
- Öffne das Automation Account
- Navigiere zu Variables und klicke auf + Add a variable

- Füge den Secret Value, TenantId und die App ID als Variable hinzu
- Wähle Runbooks
- Klicke auf + Create a runbook

- Gib einen Name ein
- Wähle PowerShell als Runbook-Typ
- Wähle 5.1 als Runtime-Version
- Klicke auf Create

- Füge das Skript aus meinem Github-Repository ein
- Trage im Skript die Absender- und Empfänger-E-Mail ein

- Speichere und teste das Skript


- Klicke auf Publish

- Navigiere zu Schedules und klicke auf + Add a schedule


- Klicke auf Link to schedule und füge den erstellten schedule hinzu


Skript
<#
Version: 1.0
Author: Jannik Reinhard (jannikreinhard.com)
Script: Get-NewEnrolledDevicesReport
Description:
Get Email with new enrolled devices
Release notes:
Version 1.0: Init
#>
function Get-AuthHeader{
param (
[parameter(Mandatory=$true)]$tenantId,
[parameter(Mandatory=$true)]$clientId,
[parameter(Mandatory=$true)]$clientSecret
)
$authBody=@{
client_id=$clientId
client_secret=$clientSecret
scope="https://graph.microsoft.com/.default"
grant_type="client_credentials"
}
$uri="https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$accessToken=Invoke-WebRequest -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $authBody -Method Post -ErrorAction Stop -UseBasicParsing
$accessToken=$accessToken.content | ConvertFrom-Json
$authHeader = @{
'Content-Type'='application/json'
'Authorization'="Bearer " + $accessToken.access_token
'ExpiresOn'=$accessToken.expires_in
}
return $authHeader
}
#################################################################################################
########################################### Start ###############################################
#################################################################################################
# Variables
$MailSender = "mail@abc.onmicrosoft.com"
$MailTo = "mail@abc.onmicrosoft.com"
# Automation Secrets
$tenantId = Get-AutomationVariable -Name 'TenantId'
$clientId = Get-AutomationVariable -Name 'AppId'
$clientSecret = Get-AutomationVariable -Name 'AppSecret'
$global:authToken = Get-AuthHeader -tenantId $tenantId -clientId $clientId -clientSecret $clientSecret
# Define the time range
$endDate = Get-Date
$startDate = $endDate.AddDays(-7)
$filter = "enrolledDateTime gt $($startDate.ToString("yyyy-MM-ddTHH:mm:ssZ"))&enrolledDateTime le $($endDate.ToString("yyyy-MM-ddTHH:mm:ssZ"))"
# Query Intune devices enrolled in the past week
$graphApiUrl = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices"
$graphApiQuery = "?`$filter=$filter&`$select=id,deviceName,operatingSystem,enrolledDateTime,userPrincipalName,model"
$uri = $graphApiUrl + $graphApiQuery
$response = Invoke-RestMethod -Method Get -Uri $uri -Headers $global:authToken
# Generate CSV report
$reportPath = "NewEnrolledDevicesReport.csv"
$response.value | Select-Object id,deviceName,operatingSystem,enrolledDateTime,userPrincipalName,model | Export-Csv -Path $reportPath -NoTypeInformation
$csv = [Convert]::ToBase64String([IO.File]::ReadAllBytes(".\$reportPath"))
#Send Mail
$URLsend = "https://graph.microsoft.com/v1.0/users/$MailSender/sendMail"
$BodyJsonsend = @"
{
"message": {
"subject": "New enrolled devices",
"body": {
"contentType": "Text",
"content": "Dear Admin, this Mail contains the enrolled devices from the previous 7 days"
},
"toRecipients": [
{
"emailAddress": {
"address": "$MailTo"
}
}
],
"attachments": [
{
"@odata.type": "#microsoft.graph.fileAttachment",
"name": "newEnrolledDevicesReport.csv",
"contentType": "text/plain",
"contentBytes": "$csv"
}
]
}
}
"@
Invoke-RestMethod -Method POST -Uri $URLsend -Headers $global:authToken -Body $BodyJsonsend
Häufige Fehler und Tipps
Wenn du den Bericht über alle neu registrierten Geräte zum ersten Mal einrichtest, sorgen meist ein paar Kleinigkeiten für die größten Probleme. Der erste Punkt sind die API-Berechtigungen. Das Skript liest verwaltete Geräte aus, daher braucht die App-Registrierung zusätzlich zu Mail.Send auch DeviceManagementManagedDevices.Read.All, und du musst anschließend auf Grant admin consent klicken. Vergisst du die Zustimmung, wird zwar ein Token ausgestellt, aber jeder Graph-Aufruf endet mit einem 403, was verwirrend sein kann, weil die Authentifizierung selbst erfolgreich aussieht.
Der zweite Stolperstein ist das Client Secret. Secrets laufen ab, wähle also eine realistische Gültigkeitsdauer und setze dir eine Erinnerung, das Secret rechtzeitig vor dem Ablauf zu erneuern. Wenn dein wöchentlicher Bericht plötzlich ausbleibt, ist ein abgelaufenes Secret der wahrscheinlichste Grund. Es hat sich bewährt, das Secret als verschlüsselte Automation-Variable zu speichern, statt es fest in das Runbook zu schreiben, genau wie oben gezeigt, damit du es an einer Stelle aktualisieren kannst, ohne das Skript anzufassen.
Teste das Runbook außerdem einmal manuell, bevor du es mit einem Zeitplan verknüpfst. Nutze dazu den Test-Bereich im Automation Account, um zu prüfen, ob die E-Mail zugestellt wird und sich der CSV-Anhang korrekt öffnen lässt. Sobald das funktioniert, läuft der wöchentliche Zeitplan völlig automatisch. Verwaltest du mehrere Tenants, klonst du das Runbook einfach pro Automation Account und passt die drei Variablen an, und schon hast du einen sauberen, wiederholbaren Bericht über alle neu registrierten Geräte für deine gesamte Umgebung.
Fazit
Zusammenfassend lässt sich sagen, dass die Automatisierung des Prozesses zur Erstellung und zum Versand von Berichten über neu registrierte Geräte Zeit und Aufwand spart und sicherstellt, dass du beim Geräte-Management stets auf dem neuesten Stand bleibst. Indem du die in diesem Beitrag beschriebenen Schritte befolgst, kannst du ganz einfach einen automatisierten Bericht einrichten, der dich über die Geräteregistrierung in deiner Organisation informiert. Probiere es aus und erlebe die Vorteile der Automatisierung aus erster Hand.
Passe das Fazit gerne nach Belieben an oder lass es mich wissen, wenn du weitere Unterstützung benötigst.






Hi Jannik, thank you for this!
I am experiencing this error after testing the Runbook,.
The csv file does exist but it is empty.
Completed
The remote server returned an error: (401) Unauthorized.
Cannot bind argument to parameter ‘InputObject’ because it is null.
Object reference not set to an instance of an object.
System.Management.Automation.MethodInvocationException: Exception calling “ReadAllBytes” with “1” argument(s): “Could not find file ‘C:\Windows\System32\NewEnrolledDevicesReport.csv’.” —> System.IO.FileNotFoundException: Could not find file ‘C:\Windows\System32\NewEnrolledDevicesReport.csv’.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.File.InternalReadAllBytes(String path, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )
— End of inner exception stack trace —
at System.Management.Automation.ExceptionHandlingOps.ConvertToMethodInvocationException(Exception exception, Type typeToThrow, String methodName, Int32 numArgs, MemberInfo memberInfo)
at CallSite.Target(Closure , CallSite , Type , String )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Object reference not set to an instance of an object.
Looks like an bug in the script could it be that there was no enrollments in the last week that no device is in the object and no csv was generated?
Same here: The remote server returned an error: (403) Forbidden.
System.Management.Automation.MethodInvocationException: Exception calling “ReadAllBytes” with “1” argument(s): “Could not find file ‘C:\Windows\System32\NewEnrolledDevicesReport.csv’.” —> System.IO.FileNotFoundException: Could not find file ‘C:\Windows\System32\NewEnrolledDevicesReport.csv’.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.File.InternalReadAllBytes(String path, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )
— End of inner exception stack trace —
at System.Management.Automation.ExceptionHandlingOps.ConvertToMethodInvocationException(Exception exception, Type typeToThrow, String methodName, Int32 numArgs, MemberInfo memberInfo)
at CallSite.Target(Closure , CallSite , Type , String )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
The remote server returned an error: (400) Bad Request.
[…] https://jannikreinhard.com/2023/03/19/how-to-get-an-report-with-all-new-enrolled-devices/ […]
Hi Jannik, for some reason i’m receiving a related error
I know there are multiple entries but the .csv ca’nt be found, any ideas?