Tip #19: Abuse Intune's discovered apps to inventory data from Windows devices
Abstract
Please read the conclusion at the end, before jumping the gun here!
Intune’s feature discovered apps, as its name suggests, discovers apps that are installed on your devices. It does so, by scanning {HKLM|HKCU}:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
with the Intune Management Extension (IME). Along with DisplayName
and DisplayVersion
, it sends a few other values back to Intune, which we see under Home > Devices > Your Device > Discovered apps
(Screenshot above).
By creating fake entries under the uninstall
reg keys, you can send any string to Intune / GraphAPI. Well, It’s Intune together with IME that will do that for you. This can be used to inventory data from your devices. Through trial and error, I found out that:
There is a 64-character limit for the
DisplayName
andDisplayVersion
values.
This means, unfortunately, you can’t send the full value of for instance the HardwareHash
to Intune. But you can send a substring of it.
How to
New-IntuneInvEntry PowerShell function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
Function New-IntuneInvEntry {
<#
.SYNOPSIS
Creates fake software inventory entries (Add Remove Programs / Installed Apps)
used to create custom inventory items in Intune
.DESCRIPTION
Creates fake software inventory entries in HKLM|HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
with the DisplayName, DisplayVersion, Publisher, InstallDate, NoModify, NoRepair, NoRemove. If it should
be hidden in Add/Remove Programs and Installed Apps it will also set the SystemComponent and WindowsInstaller values to 1.
The Intune Management Extension will read these entries and report them as software inventory.
This can be used to create custom inventory items in Intune
.PARAMETER Prefix
The prefix of the registry key. Default is IntuneInv
.PARAMETER Separator
The separator between the prefix and the name. Default is :
.PARAMETER Publisher
The publisher of the registry key. Default is the prefix
.PARAMETER Name
The name of the registry key. This will be used as the DisplayName
.PARAMETER Value
The value of the registry key. This will be used as the DisplayVersion
.PARAMETER ShowInAddRemovePrograms
If this switch is present the registry key will be visible in Add/Remove Programs
.PARAMETER RunAsUser
If this switch is present the registry key will be created
in HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
.EXAMPLE
New-IntuneInvEntry -Name "Visible" -Value "ShowInAddRemove" -ShowInAddRemovePrograms
.EXAMPLE
New-IntuneInvEntry -Name "Hidden" -Value "HideInAddRemove"
.EXAMPLE
New-IntuneInvEntry -Name "SomeUserValue" -Value "SomeValue" -RunAsUser
.EXAMPLE
New-IntuneInvEntry -Name "SomeUserValueVisible" -Value "SomeValue" -RunAsUser -ShowInAddRemovePrograms -Verbose
.NOTES
Author: Marius Wyss
Date: 2023-10-5
Website: https://ittips.ch
Twitter: @MrWyss-MSFT
GitHub: https://github.com/MrWyss-MSFT
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)] [ValidateLength(1,15)] [String] $Prefix = "IntuneInv",
[Parameter(Mandatory = $false)] [ValidateLength(1,1)] [String] $Separator = ":",
[Parameter(Mandatory = $false)] [ValidateLength(1,64)] [String] $Publisher = $Prefix,
[Parameter(Mandatory = $true)] [ValidateLength(1,48)] [String] $Name,
[Parameter(Mandatory = $true)] [ValidateLength(1,64)] [String] $Value,
[Parameter(Mandatory = $false)] [Switch] $ShowInAddRemovePrograms,
[Parameter(Mandatory = $false)] [switch] $RunAsUser
)
# Set the registry path, if the RunAsUser switch is present use HKCU, else HKLM
$RegPath = "{0}:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -f $(if ($RunAsUser.IsPresent) {"HKCU"} else {"HKLM"})
# Prepare Vars
$KeyName = "$Prefix$Separator$Name"
$RegDate = (Get-Date).ToString("yyyyMMdd")
Write-Verbose -Message "----------------- Creating Registry Key -----------------"
# Create a new Registry key
$RegKey = New-Item -Path $RegPath -Name $KeyName -Force
Write-Verbose -Message "Created Registry Key: $KeyName in $RegPath"
# Set the value of the registry key
# DisplayName as Variable Name
$RegKey | Set-ItemProperty -Name "DisplayName" -Value $KeyName -Force
Write-Verbose -Message "Set DisplayName to $KeyName"
# DisplayVersion as Variable Value
$RegKey | Set-ItemProperty -Name "DisplayVersion" -Value $Value -Force
Write-Verbose -Message "Set DisplayVersion to $Value"
# Publisher to the Prefix
$RegKey | Set-ItemProperty -Name "Publisher" -Value $Publisher -Force
Write-Verbose -Message "Set Publisher to $Publisher"
# Set InstallDate in the yyyymmdd format
$RegKey | Set-ItemProperty -Name "InstallDate" -Value $RegDate -Force
Write-Verbose -Message "Set InstallDate to $RegDate"
# Set NoModify, NoRepair, NoRemove for Add/Remove Programs
$RegKey | Set-ItemProperty -Name "NoModify" -Value 1 -Force
$RegKey | Set-ItemProperty -Name "NoRepair" -Value 1 -Force
$RegKey | Set-ItemProperty -Name "NoRemove" -Value 1 -Force
Write-Verbose -Message "Set NoModify, NoRepair, NoRemove to 1"
# Hide the entry in Add/Remove Programs and Installed Apps
if (-not $ShowInAddRemovePrograms.IsPresent) {
$RegKey | Set-ItemProperty -Name "SystemComponent " -Value 1 -Force
$Regkey | Set-ItemProperty -Name "WindowsInstaller" -Value 1 -Force
Write-Verbose -Message "Set SystemComponent and WindowsInstaller to 1"
}
Write-Verbose -Message "---------------------------------------------------------"
Write-Verbose -Message ""
}
Create a few keys
Here is an example script that uses the New-IntuneInvEntry
function to create a few keys, that I think, cannot be found anywhere in Intune.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Gather Info
$InvItems = @{
"LastRun" = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"PublicIP" = (Invoke-WebRequest -Uri "http://ipinfo.io/json").Content | ConvertFrom-Json | Select-Object -ExpandProperty ip
"DisplayResolution" = (Get-CimInstance CIM_VideoController | where Name -NotLike "Microsoft Remote*" | foreach-object { "$($_.CurrentHorizontalResolution)x$($_.CurrentVerticalResolution) @ $($_.CurrentRefreshRate) Hz ($([Math]::Log($_.CurrentNumberOfColors, 2)) Bit)" }) -join ", "
"LocalAdmins" = (((net localgroup $((Get-CimInstance -namespace root/CIMV2 -ClassName Win32_Group | where SID -Like "S-1-5-32-544*").Name)).where({$_ -match '-{79}'},'skipuntil') -notmatch '-{79}|The command completed' | Select-object -SkipLast 1) -join ", ")
"LastReboot" = Get-Date -Date $((Get-WinEvent -ProviderName 'Microsoft-Windows-Kernel-Boot'| where {$_.ID -eq $(if (([System.Environment]::OSVersion.Version).ToString() -match "10.0.22") {18} else {27}) -and $_.message -like "*0x1*"} -ea silentlycontinue)[0]).TimeCreated -Format "yyyy-MM-dd HH:mm:ss"
"HardwareHashSubString" = ((Get-CimInstance -Namespace root/cimv2/mdm/dmmap -Class MDM_DevDetail_Ext01 -Filter "InstanceID='Ext' AND ParentID='./DevDetail'").DeviceHardwareData).SubString(0,64)
"TimeZone" = (Get-TimeZone).Id
"WindowsFeatureExperiencePackVersion" = (Get-AppxPackage 'MicrosoftWindows.Client.CBS').Version
}
ForEach ($InvItem in $InvItems.GetEnumerator()) {
New-IntuneInvEntry -Name $InvItem.Name -Value $InvItem.Value
}
Once these keys are created, the Intune Management Extension will pick them up and send them to Intune. You might want to restart the IME service Restart-Service IntuneManagementExtension
to speed up the process. After a while in the IntuneManagementExtension.log
you will see entries like this:
1
2
[Win32AppInventory] Collected app inventory details: 000012a53c30f24ecaa1963e136e6d830ccb0000ffff, IntuneInv:TimeZone, W. Europe Standard Time, IntuneInv, 65535, 01/01/0001 00:00:00
[Win32AppInventory] Sending Win32 application inventory report to service. Inventory report type: 0, Inventory payload: {JSON see below}
Json sent to Intune
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"ApplicationInventory": [
{
"Application": {
"ApplicationId": "000012a53c30f24ecaa1963e136e6d830ccb0000ffff",
"ApplicationName": "IntuneInv:TimeZone",
"ApplicationVersion": "W. Europe Standard Time",
"ApplicationPublisher": "IntuneInv",
"ApplicationLanguage": "65535",
"ApplicationInstallDate": "\/Date(-62135596800000)\/"
},
"Status": 0
}
],
"InventoryReportType": 0
}
To trigger this script, you can use Scripts in Intune for single shot or Remediation Scripts for recurring runs.
Retrieve the data
Intune Console
It might take a few hours after that for it to show in the Intune console und Home > Devices > Your Device > Discovered apps
and even longer for it to show up in the Monitor Reports
Using Graph API
There is a Graph API endpoint that allows you to query discovered apps per device. You can use the Graph Explorer to test it out.
Per Device Report
1
https://graph.microsoft.com/beta/deviceManagement/managedDevices('YOURDEVICEID')?$select=deviceName,detectedApps&$expand=detectedApps
Unfortunately, the extended property detectedApps cannot be filtered further with the GraphAPI odata implementation. So, you have to filter the response yourself.
Discovered Report
I have another script to query the GraphAPI for the all discovered apps report. You can find it here.
Conclusion
This is a very hacky way to get inventory data from your devices and it has some limitations. I am not a big fan of using features for something they are not intended for. But I hope some day we have a ConfigMgr Hardware Inventory equivalent in Intune. Until then, this might be one of many ways to get some inventory data from your devices.