Installation Guide

Windows 10 Software Inventory Configuration

Configuring the Inventory Collection

Prior to starting this configuration you should have configured the Enterprise App Registration , configured the Log Analytics workspace and enrolled your devices into Endpoint Analytics as mentioned in the previous steps.

Next you will configured a proactive remediation In Endpoint Analytics that runs a PowerShell script on a schedule. This PowerShell script collects the installed software information from the Windows 10 clients and stores it in Log Analytics where it will then be sent to PowerBI.

Script credits to: Jan Ketil Skanke and Sandy Zeng


Copy the following script, or download it from here https://bi.fatstacks.tech/media/Invoke-AppInventory.zip ,edit it entering the Log Analytics Workspace ID and Log Analytics Primary Key that you recorded in the earlier steps of this documentation.

<# Original script credit to Jan Ketil Skanke and Sandy Zeng


https://msendpointmgr.com/2021/04/12/enhance-intune-inventory-data-with-proactive-remediations-and-log-analytics/

https://github.com/MSEndpointMgr/Intune/blob/master/Montoring/CustomInventory/Invoke-CustomInventory.ps1

.DESCRIPTION

This script will collect device app inventory and upload this to a Log Analytics Workspace. This allows you to easily search in device hardware and installed apps inventory.

The script is meant to be ran on a daily schedule via Proactive Remediations in Intune.


The original script has been edited by FatStacks to work seamlessly with PowerBI.


#>


#region initialize

# Enable TLS 1.2 support

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Replace with your Log Analytics Workspace ID

$CustomerId = ""


# Replace with your Primary Key

$SharedKey = ""


#Control if you want to collect App or Device Inventory or both (True = Collect)

$CollectAppInventory = $true


# You can use an optional field to specify the timestamp from the data. If the time field is not specified, Azure Monitor assumes the time is the message ingestion time

# DO NOT DELETE THIS VARIABLE. Recommened keep this blank.

$TimeStampField = ""


#endregion initialize


#region functions


# Function to get all Installed Application

function Get-InstalledApplications() {

param(

[string]$UserSid

)

New-PSDrive -PSProvider Registry -Name "HKU" -Root HKEY_USERS | Out-Null

$regpath = @("HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*")

$regpath += "HKU:\$UserSid\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"

if (-not ([IntPtr]::Size -eq 4)) {

$regpath += "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"

$regpath += "HKU:\$UserSid\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"

}

$propertyNames = 'DisplayName', 'DisplayVersion', 'Publisher', 'UninstallString'

$Apps = Get-ItemProperty $regpath -Name $propertyNames -ErrorAction SilentlyContinue | . { process { if ($_.DisplayName) { $_ } } } | Select-Object DisplayName, DisplayVersion, Publisher, UninstallString, PSPath | Sort-Object DisplayName

Remove-PSDrive -Name "HKU" | Out-Null

Return $Apps

}


# Function to create the authorization signature

Function New-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) {

$xHeaders = "x-ms-date:" + $date

$stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource


$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)

$keyBytes = [Convert]::FromBase64String($sharedKey)


$sha256 = New-Object System.Security.Cryptography.HMACSHA256

$sha256.Key = $keyBytes

$calculatedHash = $sha256.ComputeHash($bytesToHash)

$encodedHash = [Convert]::ToBase64String($calculatedHash)

$authorization = 'SharedKey {0}:{1}' -f $customerId, $encodedHash

return $authorization

}


# Function to create and post the request

Function Send-LogAnalyticsData($customerId, $sharedKey, $body, $logType) {

$method = "POST"

$contentType = "application/json"

$resource = "/api/logs"

$rfc1123date = [DateTime]::UtcNow.ToString("r")

$contentLength = $body.Length

$signature = New-Signature `

-customerId $customerId `

-sharedKey $sharedKey `

-date $rfc1123date `

-contentLength $contentLength `

-method $method `

-contentType $contentType `

-resource $resource

$uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"

#validate that payload data does not exceed limits

if ($body.Length -gt (31.9 *1024*1024))

{

throw("Upload payload is too big and exceed the 32Mb limit for a single upload. Please reduce the payload size. Current payload size is: " + ($body.Length/1024/1024).ToString("#.#") + "Mb")

}


$payloadsize = ("Upload payload size is " + ($body.Length/1024).ToString("#.#") + "Kb ")


$headers = @{

"Authorization" = $signature;

"Log-Type" = $logType;

"x-ms-date" = $rfc1123date;

"time-generated-field" = $TimeStampField;

}


$response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing

$statusmessage = "$($response.StatusCode) : $($payloadsize)"

return $statusmessage

}

function Start-PowerShellSysNative {

param (

[parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the sysnative PowerShell process.")]

[ValidateNotNull()]

[string]$Arguments

)


# Get the sysnative path for powershell.exe

$SysNativePowerShell = Join-Path -Path ($PSHOME.ToLower().Replace("syswow64", "sysnative")) -ChildPath "powershell.exe"


# Construct new ProcessStartInfo object to run scriptblock in fresh process

$ProcessStartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo

$ProcessStartInfo.FileName = $SysNativePowerShell

$ProcessStartInfo.Arguments = $Arguments

$ProcessStartInfo.RedirectStandardOutput = $true

$ProcessStartInfo.RedirectStandardError = $true

$ProcessStartInfo.UseShellExecute = $false

$ProcessStartInfo.WindowStyle = "Hidden"

$ProcessStartInfo.CreateNoWindow = $true


# Instatiate the new 64-bit process

$Process = [System.Diagnostics.Process]::Start($ProcessStartInfo)


# Read standard error output to determine if the 64-bit script process somehow failed

$ErrorOutput = $Process.StandardError.ReadToEnd()

if ($ErrorOutput) {

Write-Error -Message $ErrorOutput

}

}#endfunction

#endregion functions


#region script

#Get Common data for App and Device Inventory:

#Get Intune DeviceID and ManagedDeviceName

if (@(Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' })) {

$MSDMServerInfo = Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' }

$ManagedDeviceInfo = Get-ItemProperty -LiteralPath "Registry::$($MSDMServerInfo)"

}

$ManagedDeviceName = $ManagedDeviceInfo.EntDeviceName

$ManagedDeviceID = $ManagedDeviceInfo.EntDMID

#Get Computer Info

$ComputerInfo = Get-ComputerInfo

$ComputerName = $ComputerInfo.CsName

$ComputerManufacturer = $ComputerInfo.CsManufacturer


#region APPINVENTORY

if ($CollectAppInventory) {

$AppLog = "PowerStacksAppInventory"


#Get SID of current interactive users

$CurrentLoggedOnUser = (Get-WmiObject -Class win32_computersystem).UserName

$AdObj = New-Object System.Security.Principal.NTAccount($CurrentLoggedOnUser)

$strSID = $AdObj.Translate([System.Security.Principal.SecurityIdentifier])

$UserSid = $strSID.Value

#Get Apps for system and current user

$MyApps = Get-InstalledApplications -UserSid $UserSid

$UniqueApps = ($MyApps | Group-Object Displayname | Where-Object { $_.Count -eq 1 } ).Group

$DuplicatedApps = ($MyApps | Group-Object Displayname | Where-Object { $_.Count -gt 1 } ).Group

$NewestDuplicateApp = ($DuplicatedApps | Group-Object DisplayName) | ForEach-Object { $_.Group | Sort-Object [version]DisplayVersion -Descending | Select-Object -First 1 }

$CleanAppList = $UniqueApps + $NewestDuplicateApp | Sort-Object DisplayName


$AppArray = @()

foreach ($App in $CleanAppList) {

$tempapp = New-Object -TypeName PSObject

$tempapp | Add-Member -MemberType NoteProperty -Name "AppName" -Value $App.DisplayName -Force

$tempapp | Add-Member -MemberType NoteProperty -Name "AppVersion" -Value $App.DisplayVersion -Force

$tempapp | Add-Member -MemberType NoteProperty -Name "AppInstallDate" -Value $App.InstallDate -Force -ErrorAction SilentlyContinue

$tempapp | Add-Member -MemberType NoteProperty -Name "AppPublisher" -Value $App.Publisher -Force

$tempapp | Add-Member -MemberType NoteProperty -Name "AppUninstallString" -Value $App.UninstallString -Force

$tempapp | Add-Member -MemberType NoteProperty -Name "AppUninstallRegPath" -Value $app.PSPath.Split("::")[-1]

$AppArray += $tempapp

}

$InstalledAppJson = $AppArray | ConvertTo-Json


$ms = New-Object System.IO.MemoryStream

$cs = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Compress)

$sw = New-Object System.IO.StreamWriter($cs)

$sw.Write($InstalledAppJson)

$sw.Close();

$InstalledAppJson = [System.Convert]::ToBase64String($ms.ToArray())


$MainApp = New-Object -TypeName PSObject

$MainApp | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value "$ComputerName" -Force

$MainApp | Add-Member -MemberType NoteProperty -Name "ManagedDeviceID" -Value "$ManagedDeviceID" -Force

$MainApp | Add-Member -MemberType NoteProperty -Name "InstalledApps" -Value $InstalledAppJson -Force


$AppJson = $MainApp | ConvertTo-Json


# Submit the data to the API endpoint

$ResponseAppInventory = Send-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($AppJson)) -logType $AppLog

}

#endregion APPINVENTORY


#Report back status

$date = Get-Date -Format "dd-MM HH:mm"

$OutputMessage = "InventoryDate:$date "


if ($CollectDeviceInventory) {

if ($ResponseDeviceInventory -match "200 :") {

$OutputMessage = $OutPutMessage + "DeviceInventory:OK " + $ResponseDeviceInventory

}

else {

$OutputMessage = $OutPutMessage + "DeviceInventory:Fail "

}

}

if ($CollectAppInventory) {

if ($ResponseAppInventory -match "200 :") {

$OutputMessage = $OutPutMessage + " AppInventory:OK " + $ResponseAppInventory

}

else {

$OutputMessage = $OutPutMessage + " AppInventory:Fail "

}

}

Write-Output $OutputMessage

Exit 0




#endregion script

Next configure the BI for Intune workspace to report on the software inventory data as described in the Configure the Workspace for Software Inventory documentation.