psIntune.psm1
<#
.COPYRIGHT Portions of this are derived from Microsoft content on GitHub at the following URL: https://github.com/microsoftgraph/powershell-intune-samples/blob/master/ManagedDevices/ManagedDevices_Apps_Get.ps1 Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. See LICENSE in the project root for license information. #> #region Microsoft GitHub sample code #$graphApiVersion = "beta" function get-AuthToken { <# .SYNOPSIS This function is used to authenticate with the Graph API REST interface .DESCRIPTION The function authenticate with the Graph API Interface with the tenant name .PARAMETER User UserName for cloud services access .EXAMPLE Get-AuthToken -User "john.doe@contoso.com" Authenticates user with the Graph API interface .NOTES NAME: Get-AuthToken #> [cmdletbinding()] param ( [parameter(Mandatory)] $User ) $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User $tenant = $userUpn.Host Write-Host "Checking for AzureAD module..." $AadModule = Get-Module -Name "AzureAD" -ListAvailable if ($null -eq $AadModule) { Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview" $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable } if ($null -eq $AadModule) { Write-Host Write-Host "AzureAD Powershell module not installed..." -f Red Write-Host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow Write-Host "Script can't continue..." -f Red Write-Host exit } # Getting path to ActiveDirectory Assemblies # If the module count is greater than 1 find the latest version if ($AadModule.count -gt 1){ $Latest_Version = ($AadModule | Select-Object version | Sort-Object)[-1] $aadModule = $AadModule | Where-Object { $_.version -eq $Latest_Version.version } # Checking if there are multiple versions of the same module found if($AadModule.count -gt 1){ $aadModule = $AadModule | Select-Object -Unique } $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" } else { $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" } [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null $clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547" $redirectUri = "urn:ietf:wg:oauth:2.0:oob" $resourceAppIdURI = "https://graph.microsoft.com" $authority = "https://login.microsoftonline.com/$Tenant" try { $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId") $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result # If the accesstoken is valid then create the authentication header if ($authResult.AccessToken){ # Creating header for Authorization token $authHeader = @{ 'Content-Type'='application/json' 'Authorization'="Bearer " + $authResult.AccessToken 'ExpiresOn'=$authResult.ExpiresOn } return $authHeader } else { Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red break } } catch { Write-Error $_.Exception.Message Write-Error $_.Exception.ItemName break } } function get-psIntuneAuth { <# .SYNOPSIS Returns authentication token object .PARAMETER UserName UserName for cloud services access .EXAMPLE Get-psIntuneAuth -UserName "john.doe@contoso.com" .NOTES Name: Get-psIntuneAuth #> [CmdletBinding()] param ( [parameter(Mandatory)][string] $UserName ) # Checking if authToken exists before running authentication if ($global:authToken) { # Setting DateTime to Universal time to work in all timezones $DateTime = (Get-Date).ToUniversalTime() # If the authToken exists checking when it expires $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes if ($TokenExpires -le 0){ Write-Host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow # Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name) $global:authToken = Get-AuthToken -User $UserName } } else { # Authentication doesn't exist, calling Get-AuthToken function $global:authToken = Get-AuthToken -User $UserName } } function get-MsGraphData($Path) { <# .SYNOPSIS Returns MS Graph data from (beta) REST API query .PARAMETER Path REST API URI path suffix .NOTES This function was derived from https://www.dowst.dev/search-intune-for-devices-with-application-installed/ (Thanks to Matt Dowst) #> $FullUri = "https://graph.microsoft.com/$graphApiVersion/$Path" [System.Collections.Generic.List[PSObject]]$Collection = @() $NextLink = $FullUri do { $Result = Invoke-RestMethod -Method Get -Uri $NextLink -Headers $AuthHeader if ($Result.'@odata.count') { $Result.value | ForEach-Object{$Collection.Add($_)} } else { $Collection.Add($Result) } $NextLink = $Result.'@odata.nextLink' } while ($NextLink) return $Collection } function Get-ManagedDevices(){ <# .SYNOPSIS This function is used to get Intune Managed Devices from the Graph API REST interface .DESCRIPTION The function connects to the Graph API Interface and gets any Intune Managed Device .PARAMETER IncludeEAS Switch to include EAS devices (not included by default) .PARAMETER ExcludeMDM Switch to exclude MDM devices (not excluded by default) .EXAMPLE Get-ManagedDevices Returns all managed devices but excludes EAS devices registered within the Intune Service .EXAMPLE Get-ManagedDevices -IncludeEAS Returns all managed devices including EAS devices registered within the Intune Service .NOTES NAME: Get-ManagedDevices .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-ManagedDevices.md #> [cmdletbinding()] param ( [parameter(Mandatory)][string] $UserName, [parameter()][string] $DeviceName = "", [parameter()][switch] $IncludeEAS, [parameter()][switch] $ExcludeMDM ) #$graphApiVersion = "beta" $Resource = "deviceManagement/managedDevices" try { Get-psIntuneAuth -UserName $UserName $Count_Params = 0 if ($IncludeEAS.IsPresent){ $Count_Params++ } if ($ExcludeMDM.IsPresent){ $Count_Params++ } if ($Count_Params -gt 1) { Write-Warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function" #Write-Host break } elseif ($IncludeEAS) { Write-Verbose "IncludeEAS = true" $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" } elseif ($ExcludeMDM) { Write-Verbose "ExcludeMDM = true" $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'" } else { if (![string]::IsNullOrEmpty($DeviceName)) { Write-Verbose "DeviceName = $DeviceName" $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=deviceName eq '$DeviceName' and managementAgent eq 'mdm' and managementAgent eq 'easmdm'" } else { Write-Verbose "Default = True" $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'" } Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices" #Write-Host } $response = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get) $Devices = $response.Value $DevicesNextLink = $response."@odata.nextLink" while ($DevicesNextLink) { $response = (Invoke-RestMethod -Uri $DevicesNextLink -Headers $authToken -Method Get) $DevicesNextLink = $response."@odata.nextLink" $Devices += $response.value } $Devices } catch { $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $responseBody = $reader.ReadToEnd(); Write-Warning "Response content:`n$responseBody" Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" Write-Host break } } #endregion function Get-psIntuneAzureADUser() { <# .SYNOPSIS This function is used to get AAD Users from the Graph API REST interface .DESCRIPTION The function connects to the Graph API Interface and gets any users registered with AAD .EXAMPLE Get-psIntuneAzureADUser Returns all users registered with Azure AD .EXAMPLE Get-psIntuneAzureADUser -userPrincipleName user@domain.com Returns specific user by UserPrincipalName registered with Azure AD .NOTES NAME: Get-psIntuneAzureADUser .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-psIntuneAzureADUser.md #> [cmdletbinding()] param ( [parameter()][string] $userPrincipalName, [parameter()][string] $Property ) $User_resource = "users" try { if ([string]::IsNullOrEmpty($userPrincipalName)) { $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } else { if ([string]::IsNullOrEmpty($Property)) { $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" Write-Verbose $uri Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get } else { $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" Write-Verbose $uri (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } } } catch { $ex = $_.Exception $errorResponse = $ex.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($errorResponse) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $responseBody = $reader.ReadToEnd(); Write-Host "Response content:`n$responseBody" -f Red Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" #Write-Host break } } function Get-psIntuneAzureADDevices { <# .SYNOPSIS Return AzureAD device accounts .DESCRIPTION Return all AzureAD tenant device accounts .EXAMPLE Get-psIntuneAzureADDevices .NOTES Name: Get-psIntuneAzureADDevices .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-psIntuneAzureADDevices.md #> [CmdletBinding()] param() try { if (!$AADCred) { Write-Host "Connecting to AzureAD, you may be required to confirm MFA" -ForegroundColor Yellow $Global:AADCred = Connect-AzureAD } if (!$AADCred) { throw "AzureAD authentication was not completed" } Write-Host "Requesting devices from Azure AD tenant" -ForegroundColor Cyan $aadcomps = Get-AzureADDevice -All $True Write-Host "Returned $($aadcomps.Count) devices from Azure AD" -ForegroundColor Cyan $aadcomps | Foreach-Object { $devname = $_.DisplayName Write-Verbose "reading properties for: $devname" $llogin = $_.ApproximateLastLogonTimeStamp if (![string]::IsNullOrEmpty($llogin)) { $xdaysOld = (New-TimeSpan -Start $([datetime]$llogin) -End (Get-Date)).Days } else { $xdaysOld = $null } if (![string]::IsNullOrEmpty($_.LastDirSyncTime)) { $xSyncDays = (New-TimeSpan -Start $([datetime]$_.LastDirSyncTime) -End (Get-Date)).Days } else { $xSyncDays = $null } [pscustomobject]@{ Name = $devname DeviceId = $_.DeviceId ObjectId = $_.ObjectId Enabled = $_.AccountEnabled OSName = $_.DeviceOSType OSVersion = $_.DeviceOSVersion TrustType = $_.DeviceTrustType LastLogon = $_.ApproximateLastLogonTimeStamp LastLogonDays = $xdaysOld IsCompliant = $($_.IsCompliant -eq $True) IsManaged = $($_.IsManaged -eq $True) DirSyncEnabled = $($_.DirSyncEnabled -eq $True) LastDirSync = $_.LastDirSyncTime LastSyncDays = $xSyncDays ProfileType = $_.ProfileType } } } catch { Write-Error $_.Exception.Message } } function Get-psIntuneDevice { <# .SYNOPSIS Returns dataset of Intune-managed devices with inventoried apps .DESCRIPTION Returns dataset of Intune-managed devices with inventoried apps .PARAMETER UserName UserPrincipalName for authentication request .PARAMETER DeviceName Filter query to just one specified device by name. Default is query all devices .PARAMETER ShowProgress Display progress as data is exported (default is silent / no progress shown) .PARAMETER Detail Controls the level of granularity of the results: * Summary - returns basic device information only * Detailed - returns detailed device information * Full - returns detailed device information with installed applications * Raw - returns raw Graph API results only .EXAMPLE $devices = Get-psIntuneDevice -UserName "john.doe@contoso.com" -DeviceName "Desktop123" -Detail Detailed Returns detailed data for one device without installed applications .EXAMPLE $devices = Get-psIntuneDevice -UserName "john.doe@contoso.com" Returns summary data without applications .EXAMPLE $devices = Get-psIntuneDevice -UserName "john.doe@contoso.com" -ShowProgress Returns summary data without applications and shows progress during processing .EXAMPLE $devices = Get-psIntuneDevice -UserName "john.doe@contoso.com" -Detail -Full -ShowProgress Returns detailed data with applications for each device and shows progress during processing .NOTES NAME: Get-psIntuneDevice .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-psIntuneDevice.md #> [CmdletBinding()] [OutputType([hashtable])] param ( [parameter(Mandatory)][string] $UserName, [parameter()][string] $DeviceName = "", [parameter()][ValidateSet('Full','Detailed','Summary','Raw')][string] $Detail = 'Summary', [parameter()][switch] $ShowProgress, [parameter()][string] $graphApiVersion = "beta" ) try { if (![string]::IsNullOrEmpty($DeviceName)) { $devices = Get-ManagedDevices -Username $UserName -DeviceName $DeviceName } else { $devices = Get-ManagedDevices -UserName $UserName } $dcount = $Devices.Count $dx = 1 Write-Verbose "returned $dcount devices" if ($Detail -eq 'Full') { Write-Warning "Full option takes the longest to process. This may take a few minutes." } foreach ($Device in $Devices){ if ($ShowProgress) { Write-Progress -Activity "Found $dcount devices" -Status "Reading device $dx of $dcount" -PercentComplete $(($dx/$dcount)*100) -id 1 } $DeviceID = $Device.id $LastSync = $Device.lastSyncDateTime $SyncDays = (New-TimeSpan -Start $LastSync -End (Get-Date)).Days switch ($Detail) { 'Summary' { [pscustomobject]@{ DeviceName = $Device.DeviceName DeviceID = $DeviceID UserName = $Device.userDisplayName OSName = $Device.operatingSystem OSVersion = $Device.osVersion LastSyncTime = $LastSync LastSyncDays = $SyncDays } } 'Detailed' { $compliant = $($Device.complianceState -eq $True) $disksize = [math]::Round(($Device.totalStorageSpaceInBytes / 1GB),2) $freespace = [math]::Round(($Device.freeStorageSpaceInBytes / 1GB),2) $mem = [math]::Round(($Device.physicalMemoryInBytes / 1GB),2) [pscustomobject]@{ DeviceName = $Device.DeviceName DeviceID = $DeviceID Manufacturer = $Device.manufacturer Model = $Device.model UserName = $Device.userDisplayName EthernetMAC = $Device.ethernetMacAddress WiFiMAC = $Device.WiFiMacAddress MemoryGB = $mem DiskSizeGB = $disksize FreeSpaceGB = $freespace SerialNumber = $Device.serialNumber OSName = $Device.operatingSystem OSVersion = $Device.osVersion Ownership = $Device.ownerType Category = $Device.deviceCategoryDisplayName EnrollDate = $Device.enrolledDateTime LastSyncTime = $LastSync LastSyncDays = $SyncDays Compliant = $compliant AutoPilot = $Device.autopilotEnrolled } } 'Full' { $uriApps = "https://graph.microsoft.com/$graphApiVersion/deviceManagement/manageddevices('$DeviceID')?`$expand=detectedApps" $DetectedApps = (Invoke-RestMethod -Uri $uriApps -Headers $authToken -Method Get).detectedApps $compliant = $($Device.complianceState -eq $True) $disksize = [math]::Round(($Device.totalStorageSpaceInBytes / 1GB),2) $freespace = [math]::Round(($Device.freeStorageSpaceInBytes / 1GB),2) $mem = [math]::Round(($Device.physicalMemoryInBytes / 1GB),2) [pscustomobject]@{ DeviceName = $Device.DeviceName DeviceID = $DeviceID Manufacturer = $Device.manufacturer Model = $Device.model UserName = $Device.userDisplayName EthernetMAC = $Device.ethernetMacAddress WiFiMAC = $Device.WiFiMacAddress MemoryGB = $mem DiskSizeGB = $disksize FreeSpaceGB = $freespace SerialNumber = $Device.serialNumber OSName = $Device.operatingSystem OSVersion = $Device.osVersion Ownership = $Device.ownerType Category = $Device.deviceCategoryDisplayName EnrollDate = $Device.enrolledDateTime LastSyncTime = $LastSync LastSyncDays = $SyncDays Compliant = $compliant AutoPilot = $Device.autopilotEnrolled Apps = $DetectedApps } } 'Raw' { $Device } } # switch $dx++ } # foreach } catch { Write-Error $_.Exception.Message } } function Get-psIntuneInstalledApps { <# .SYNOPSIS Returns App inventory data from Intune Device data set .DESCRIPTION Returns App inventory data from Intune Device data set .PARAMETER DataSet Data returned from Get-psIntuneDevice .EXAMPLE $devices = Get-psIntuneDevice -UserName "john.doe@contoso.com" $applist = Get-psIntuneInstalledApps -DataSet $devices .NOTES NAME: Get-psIntuneInstalledApps .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-psIntuneInstalledApps.md #> [CmdletBinding()] param ( [parameter(Mandatory=$True)] [ValidateNotNull()] $DataSet, [parameter()][switch] $GroupByName ) $badnames = ('. .','. . .','..','...') Write-Verbose "reading $($DataSet.Count) objects" $appcount = 0 $result = $DataSet | Foreach-Object { $devicename = $_.DeviceName $apps = $_.Apps if ($null -ne $Apps) { foreach ($app in $apps) { $displayName = $($app.displayName).ToString().Trim() if (![string]::IsNullOrEmpty($displayName)) { if ($displayName -notin $badnames) { if ($($app.Id).Length -gt 36) { $ptype = 'WindowsStore' } elseif ($($app.Id).Length -eq 36) { $ptype = 'Win32' } else { $ptype = 'Other' } [pscustomobject]@{ ProductName = $displayName ProductVersion = $($app.version).ToString().Trim() ProductCode = $app.Id ProductType = $ptype DeviceName = $devicename } } } } $appcount++ } else { Write-Verbose "$devicename - has no apps" } } if ($appcount -eq 0) { Write-Warning "DataSet objects have no applications linked. Use [-Detail Full] option with Get-psIntuneDevice" } if ($GroupByName) { $result | Group-Object -Property ProductName | Select-Object Count,Name | Sort-Object Name -Unique } else { $result } } function Get-psIntuneDevicesWithApp { <# .SYNOPSIS Returns Intune managed devices having a specified App installed .DESCRIPTION Returns Intune managed devices having a specified App installed .PARAMETER AppDataSet Applications dataset returned from Get-DsIntuneDeviceApps(). If not provided, Devices are queried automatically, which will incur additional time. .PARAMETER Application Name, or wildcard name, of App to search for .PARAMETER UserName UserPrincipalName for authentication request .PARAMETER ShowProgress Display progress during execution (default is silent / no progress shown) .EXAMPLE Get-psIntuneDevicesWithApp -Application "*Putty*" -UserName "john.doe@contoso.com" Returns list of Intune-managed devices which have any app name containing "Putty" installed .EXAMPLE Get-psIntuneDevicesWithApp -Application "*Putty*" -UserName "john.doe@contoso.com" -ShowProgress Returns list of Intune-managed devices which have any apps name containing "Putty" installed, and displays progress during execution .NOTES NAME: Get-psIntuneDevicesWithApp This function was derived almost entirely from https://www.dowst.dev/search-intune-for-devices-with-application-installed/ (Thanks to Matt Dowst) .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Get-psIntuneDevicesWithApp.md #> [CmdletBinding()] param ( [parameter()] $AppDataSet, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $Application, [parameter()][string] $Version, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $Username, [parameter()][switch] $ShowProgress ) Write-Verbose "Getting authentication token" $AuthHeader = Get-AuthToken -User $Username Write-Verbose "getting all devices in Intune" $AllDevices = Get-MsGraphData "deviceManagement/managedDevices" # Get detected app for each device and check for app name [System.Collections.Generic.List[PSObject]]$FoundApp = @() $wp = 1 Write-Verbose "querying devices for $Application $Version" foreach ($Device in $AllDevices) { if ($ShowProgress) { Write-Progress -Activity "Found $($FoundApp.count)" -Status "$wp of $($AllDevices.count)" -PercentComplete $(($wp/$($AllDevices.count))*100) -id 1 } $AppData = Get-MsGraphData "deviceManagement/managedDevices/$($Device.id)?`$expand=detectedApps" $DetectedApp = $AppData.detectedApps | Where-Object {$_.displayname -like $Application} if (![string]::IsNullOrEmpty($Version)) { $DetectedApp = $DetectedApp | Where-Object { $_.ProductVersion -eq $Version } } if ($DetectedApp) { $DetectedApp | Select-Object @{l='DeviceName';e={$Device.DeviceName}}, @{l='Application';e={$_.displayname}}, Version, SizeInByte, @{l='LastSyncDateTime';e={$Device.lastSyncDateTime}}, @{l='DeviceId';e={$Device.id}} | Foreach-Object { $FoundApp.Add($_) } } $wp++ } if ($ShowProgress) { Write-Progress -Activity "Done" -Id 1 -Completed } $FoundApp } function Write-psIntuneDeviceReport { <# .SYNOPSIS Export Inventory Data to Excel Workbook .DESCRIPTION Export Intune Device inventory data to Excel Workbook .PARAMETER DataSet Device Data queried from Intune using Get-psIntuneDevice -Detail Full If DataSet is not provided, data will be queried from Intune. .PARAMETER OutputFolder Path for output file. Default is current user Documents path .PARAMETER Title Title to use for output filename .PARAMETER DeviceOS Filter devices by operating system. Options: Android, iOS, Windows, All Default is All .PARAMETER StaleLimit Number of days since last Intune synchronization to consider as a stale account Default is 180 .PARAMETER LowDiskGB Free disk space GB to indicate "low disk space". Default is 20 .PARAMETER AzureAD Includes AzureAD device accounts with report .PARAMETER Overwrite If output file exists, with same name, it will be overwritten. Default is to abort if idential filename exists. .PARAMETER Show Display workbook when export is complete. Default is to not show #> [CmdletBinding()] param ( [parameter()] $DataSet, [parameter()][string] $OutputFolder = "$($env:USERPROFILE)\Documents", [parameter()][string] $Title = "", [parameter()][string][ValidateSet('All','Windows','Android','iOS')] $DeviceOS = 'All', [parameter()][ValidateRange(1,1000)][int] $StaleLimit = 180, [parameter()][ValidateRange(0,100)][int] $LowDiskGB = 20, [parameter()][switch] $AzureAD, [parameter()][switch] $Overwrite, [parameter()][switch] $Show ) $time1 = Get-Date Write-Host "Gathering data to generate report" $xlFile = "$OutputFolder\IntuneDevices`_$Title`_$(Get-Date -f 'yyyy-MM-dd').xlsx" Write-Verbose "output file = $xlFile" if ((Test-Path $xlFile) -and (!$Overwrite)) { Write-Warning "Output file exists [$xlFile]. Use -Overwrite to replace." break } if ($null -eq $DataSet) { Write-Host "Requesting new query results..." $devs = Get-psIntuneDevice -Detail Full -ShowProgress } else { $devs = $DataSet } Write-Host "Returned $($devs.Count) devices" if ($DeviceOS -ne 'All') { Write-Verbose "filtering devices on OSName = $DeviceOS" $devs = $devs | Where-Object {$_.OSName -match $DeviceOS} Write-Verbose "filtered to $($devs.Count) devices with $DeviceOS" } Write-Host "Applying filter rule: Base Devices" $intdevs = $devs | Select-Object * -ExcludeProperty Apps Write-Host "Applying filter rule: Device Models" $models = $devs | Select-Object Manufacturer,Model | Group-Object -Property Manufacturer,Model | Select-Object Count,Name | Sort-Object Count -Descending Write-Host "Applying filter rule: Orphaned Devices" $deldevs = $devs | Where-Object {$_.DeviceName -eq 'User deleted for this device'} | Select-Object * -ExcludeProperty Apps Write-Host "Applying filter rule: Duplicate Devices" $dupedevs = $devs | Select-Object DeviceName,DeviceID | Group-Object -Property DeviceName | Select-Object Count,Name Write-Host "Applying filter rule: Stale Devices" $staledevs = $devs | Where-Object {$_.LastSyncDays -ge $StaleLimit} | Select-Object DeviceName,DeviceID,Category,Ownership,Manufacturer,Model,UserName,SerialNumber,LastSyncTime,LastSyncDays,EnrollDate Write-Host "Applying filter rule: Devices with Low Disk Space" $lowdisk = $devs | Where-Object {$_.FreeSpaceGB -lt $LowDiskGB} | Select-Object * -ExcludeProperty Apps Write-Host "Applying filter rule: Software" $apps = Get-psIntuneInstalledApps -DataSet $devs Write-Host "Applying filter rule: Software Install Counts" $appcounts = $apps | Group-Object -Property ProductName | Select-Object Count,Name | Sort-Object Name -Unique Write-Host "Applying filter rule: Distinct Software Installs" $distapps = $apps | Select-Object ProductName,ProductType,DeviceName | Sort-Object ProductName -Unique if ($AzureAD) { $aadevs = Get-psIntuneAzureADDevices if ($DeviceOS -ne 'All') { $aadevs = $aadevs | Where-Object { $_.OSName -match $DeviceOS } Write-Verbose "returned $($aadevs.Count) AzureAD devices running $DeviceOS" } } Write-Host "Crunching statistics and stuff..." $stats = @() $stats += $intdevs | Group-Object -Property OSName,OSVersion | Sort-Object Count -Descending | Select-Object Name,Count | ForEach-Object {[pscustomobject]@{Name = 'OperatingSystem'; Property = $_.Name; Value = $_.Count}} $stats += $intdevs | Group-Object -Property Manufacturer,Model | Select-Object Name,Count | Sort-Object Count -Descending | ForEach-Object {[pscustomobject]@{Name = 'Models'; Property = $_.Name; Value = $_.Count}} $stats += $intdevs | Group-Object -Property Ownership | Select-Object Name,Count | Sort-Object Count -Descending | ForEach-Object {[pscustomobject]@{Name = 'Ownership'; Property = $_.Name; Value = $_.Count}} $stats += $intdevs | Group-Object -Property Category | Select-Object Name,Count | Sort-Object Count -Descending | ForEach-Object {[pscustomobject]@{Name = 'Category'; Property = $_.Name; Value = $_.Count}} $stats += $intdevs | Group-Object -Property UserName | Select-Object Name,Count | Where-Object {$_.Count -gt 1 -and $_.Name -ne ''} | Sort-Object Count -Descending | Select-Object -First 25 | ForEach-Object {[pscustomobject]@{Name = 'UserName'; Property = $_.Name; Value = $_.Count}} if ($AzureAD) { $stats += $aadevs | Group-Object -Property IsCompliant | Select-Object Name,Count | ForEach-Object {[pscustomobject]@{Name = 'Compliant'; Property = $_.Name; Value = $_.Count}} $stats += $aadevs | Group-Object -Property DirSyncEnabled | Select-Object Name,Count | ForEach-Object {[pscustomobject]@{Name = 'DirSyncEnabled'; Property = $_.Name; Value = $_.Count}} $stats += $aadevs | Group-Object -Property TrustType | Select-Object Name,Count | ForEach-Object {[pscustomobject]@{Name = 'TrustType'; Property = $_.Name; Value = $_.Count}} $stats += $aadevs | Group-Object -Property OSVersion | Select-Object Name,Count | ForEach-Object {$_.Name -ne '' -and $_.Count -gt 5} | Sort-Object Count -Descending | ForEach-Object {[pscustomobject]@{Name = 'OSVersion'; Property = $_.Name; Value = $_.Count}} $stats += $aadevs | Group-Object -Property LastLogonDays | ForEach-Object {$_.Name -gt 180 -and $_.Count -gt 10} | ForEach-Object {[pscustomobject]@{Name = 'DaysSinceLogon'; Count = $_.Count; Property = [int]$_.Name}} | Sort-Object Property -Descending $iMissing = $aadevs | Where-Object {$_.Name -notin $intdevs.DeviceName} $aMissing = $intdevs | Where-Object {$_.DeviceName -notin $aadevs.Name} } Write-Host "Exporting datasets: Summary" $stats | Export-Excel -Path $xlFile -WorksheetName "Summary" -ClearSheet -AutoSize -AutoFilter -FreezeTopRow Write-Host "Exporting datasets: Intune Devices" $intdevs | Export-Excel -Path $xlFile -WorksheetName "IntuneDevices" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn if ($AzureAD) { Write-Host "Exporting datasets: AzureAD devices" $aadevs | Export-Excel -Path $xlFile -WorksheetName "AzureADDevices" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn } Write-Host "Exporting datasets: Intune Device Models" $models | Export-Excel -Path $xlFile -WorksheetName "IntuneModels" -ClearSheet -AutoSize -AutoFilter -FreezeTopRow Write-Host "Exporting datasets: Intune Stale Devices" $staledevs | Export-Excel -Path $xlFile -WorksheetName "IntuneStaleDevices" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn Write-Host "Exporting datasets: Intune Duplicate Devices" $dupedevs | Export-Excel -Path $xlFile -WorksheetName "IntuneDuplicates" -ClearSheet -AutoSize -AutoFilter -FreezeTopRow Write-Host "Exporting datasets: Intune Orphaned Devices" $deldevs | Export-Excel -Path $xlFile -WorksheetName "IntuneOrphaned" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn Write-Host "Exporting datasets: Intune Devices with Low Disk Space" $lowdisk | Export-Excel -Path $xlFile -WorksheetName "IntuneLowDisk" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn Write-Host "Exporting datasets: Intune Installed Software" $apps | Export-Excel -Path $xlFile -WorksheetName "IntuneSoftware" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn Write-Host "Exporting datasets: Intune Software Install Counts" $appcounts | Export-Excel -Path $xlFile -WorksheetName "IntuneInstallCounts" -ClearSheet -AutoSize -AutoFilter -FreezeTopRow Write-Host "Exporting datasets: Intune Software Unique Instances" $distapps | Export-Excel -Path $xlFile -WorksheetName "IntuneSoftwareUnique" -ClearSheet -AutoSize -AutoFilter -FreezeTopRow if ($AzureAD) { Write-Host "Exporting datasets: Devices missing from Intune" $iMissing | Export-Excel -Path $xlFile -WorksheetName "IntuneMissing" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn Write-Host "Exporting datasets: Devices missing from Azure AD" $aMissing | Export-Excel -Path $xlFile -WorksheetName "AADMissing" -ClearSheet -AutoSize -AutoFilter -FreezeTopRowFirstColumn } if ($Show) { Start-Process -FilePath "$xlFile" } Write-Host "Export saved to $xlFile" -ForegroundColor Green $time2 = Get-Date $rt = New-TimeSpan -Start $time1 -End $time2 Write-Host "Total runtime: $($rt.Hours)`:$($rt.Minutes)`:$($rt.Seconds) (hh`:mm`:ss)" -ForegroundColor Cyan } function Invoke-psIntuneAppQuery { <# .SYNOPSIS Query DataSet for unique App installation counts .DESCRIPTION Filters instances of application installations by Name/Title only to determine unique installations by device. Some devices will report multiple instances of the same application, with different ProductVersion numbers. This function excludes duplicates to show one-per-device only. .PARAMETER AppDataSet Device data returned from Get-DsIntuneDeviceData(). If not provided, Get-DsIntuneDeviceData() is invoked automatically. Passing Device data to -DeviceData can save significant processing time. .PARAMETER ProductName Application Product name .EXAMPLE $devices = Get-DsIntuneDeviceData -UserName "john.doe@contoso.com" $applist = Get-DsIntuneDeviceApps -DataSet $devices $rows = Invoke-psIntuneAppQuery -AppDataSet $applist -ProductName "Acme Crapware 19.20 64-bit" .NOTES NAME: Invoke-psIntuneAppQuery .LINK https://github.com/Skatterbrainz/ds-intune/blob/master/docs/Invoke-psIntuneAppQuery.md #> [CmdletBinding()] param ( [parameter(Mandatory)][ValidateNotNullOrEmpty()] $AppDataSet, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $ProductName ) try { $result = ($AppDataSet | Select-Object ProductName,DeviceName | Where-Object {$_.ProductName -eq $ProductName} | Sort-Object ProductName,DeviceName -Unique) } catch { Write-Error $_.Exception.Message } finally { $result } } |