Skip to content

Instantly share code, notes, and snippets.

@s1037989
Last active November 12, 2025 10:46
Show Gist options
  • Select an option

  • Save s1037989/d497676716f6189a9a7d26f673eae89c to your computer and use it in GitHub Desktop.

Select an option

Save s1037989/d497676716f6189a9a7d26f673eae89c to your computer and use it in GitHub Desktop.
windows software inventory generator
<#
.SYNOPSIS
Collect extensive Windows host inventory & security-relevant configuration.
.DESCRIPTION
Produces JSON + HTML report containing:
- system / OS info
- installed apps (registry, winget/choco/Get-Package)
- installed updates/hotfixes
- services, processes, listening ports
- firewall rules
- local users/groups & local admins
- SMB shares & sessions
- scheduled tasks
- drivers (signed)
- BitLocker status
- Windows Defender/AV status
- optional SHA256 hashing of executables from discovered install locations
WARNING: Some cmds require elevation (Admin). This script prefers to relaunch elevated if needed.
.NOTES
- This script intentionally avoids Win32_Product enumeration (it can trigger MSI repairs).
- Designed for defensive inventory; it does not attempt to exploit anything.
#>
# ---- Configuration ----
$ComputeFileHash = $true # Toggle: compute SHA256 for discovered executable files (can be slow)
$MaxHashFiles = 200 # Max number of files to hash (protect against huge scans)
$HashIncludePatterns = @('*.exe','*.dll') # file patterns to hash in discovered install locations
$OutputPrefix = "host_inventory_"
$Now = Get-Date -Format "yyyyMMdd_HHmmss"
$JsonOut = "$OutputPrefix$Now.json"
$HtmlOut = "$OutputPrefix$Now.html"
function Ensure-Elevated {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host "Not running as Administrator. Attempting to re-launch elevated..."
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = (Get-Process -Id $PID).Path
$psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`""
$psi.Verb = "runas"
try {
[System.Diagnostics.Process]::Start($psi) | Out-Null
exit
} catch {
Write-Warning "Elevation declined / failed. Continuing non-elevated; some data may be missing."
}
}
}
# Attempt elevation for full results
Ensure-Elevated
# Utility: safe Get-CimInstance wrapper
function Safe-Cim {
param($Class, $Filter = $null)
try {
if ($Filter) { Get-CimInstance -ClassName $Class -Filter $Filter -ErrorAction Stop }
else { Get-CimInstance -ClassName $Class -ErrorAction Stop }
} catch {
Write-Verbose "CIM $Class query failed: $_"
return $null
}
}
# ---- System basics ----
$sys = @{
ComputerName = $env:COMPUTERNAME
Timestamp = (Get-Date).ToString("o")
OS = @{
Caption = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -First 1 -Property Caption, Version, BuildNumber, OSArchitecture).Caption
Version = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -First 1 -Property Version).Version
Build = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -First 1 -Property BuildNumber).BuildNumber
InstallDate = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -First 1 -Property InstallDate).InstallDate
}
BIOS = (Get-CimInstance -ClassName Win32_BIOS | Select-Object -First 1 * )
ComputerSystem = (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -First 1 * )
Processor = (Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 * )
MemoryGB = [math]::Round((Get-CimInstance -ClassName Win32_ComputerSystem).TotalPhysicalMemory / 1GB,2)
}
# ---- Installed software enumeration ----
# Read Uninstall keys from HKLM (64/32) and HKCU
function Get-InstalledFromRegistry {
$keys = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$out = @()
foreach ($k in $keys) {
try {
Get-ItemProperty -Path $k -ErrorAction SilentlyContinue |
ForEach-Object {
$obj = [PSCustomObject]@{
DisplayName = $_.DisplayName
DisplayVersion = $_.DisplayVersion
Publisher = $_.Publisher
InstallDate = $_.InstallDate
InstallLocation = $_.InstallLocation
UninstallString = $_.UninstallString
RegistryKey = $_.PSPath
}
$out += $obj
}
} catch { Write-Verbose "Registry key $k read failed: $_" }
}
# remove empties and duplicates
$out | Where-Object { $_.DisplayName } | Sort-Object DisplayName -Unique
}
# Also collect from package managers and PowerShell package provider
function Get-PackageManagersInventory {
$res = [ordered]@{}
# winget (if present)
try {
$wingetText = & winget list --source winget 2>$null
if ($LASTEXITCODE -eq 0 -and $wingetText) {
$res.Winget = $wingetText -split "`n"
}
} catch {}
# choco
try {
$choco = & choco list --localonly 2>$null
if ($LASTEXITCODE -eq 0 -and $choco) { $res.Choco = $choco -split "`n" }
} catch {}
# Get-Package (PowerShell package provider)
try {
$gp = Get-Package -ProviderName Programs -ErrorAction SilentlyContinue
if ($gp) { $res.GetPackage = $gp | Select-Object Name, Version, ProviderName, Source }
} catch {}
return $res
}
$installedApps = Get-InstalledFromRegistry
$packageManagers = Get-PackageManagersInventory
# ---- Installed updates / hotfixes ----
# Uses Win32_QuickFixEngineering / Get-HotFix (may not return everything on modern Windows; results vary)
$hotfixes = @()
try {
$hotfixes = Get-CimInstance -ClassName Win32_QuickFixEngineering | Select-Object HotFixID, Description, InstalledOn, InstalledBy
} catch {
Write-Warning "Failed to enumerate hotfixes: $_"
}
# ---- Services, Processes, Listening Ports ----
$services = Get-Service | Select-Object Name, DisplayName, Status, StartType, ServiceType
$processes = Get-Process | Select-Object Id, ProcessName, Path, StartTime -ErrorAction SilentlyContinue
# Listening TCP connections (requires admin for full detail)
$tcp = @()
try {
$tcp = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, OwningProcess
# resolve process names
$tcp = $tcp | ForEach-Object {
$p = $_
$proc = Get-Process -Id $p.OwningProcess -ErrorAction SilentlyContinue
[PSCustomObject]@{
LocalAddress = $p.LocalAddress
LocalPort = $p.LocalPort
OwningProcess = $p.OwningProcess
ProcessName = if ($proc) { $proc.ProcessName } else { $null }
}
}
} catch { Write-Verbose "Get-NetTCPConnection failed: $_" }
# ---- Firewall rules ----
$firewallProfiles = @()
try {
$firewallProfiles = Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction, AllowInboundRules, AllowLocalFirewallRules
} catch { Write-Verbose "Get-NetFirewallProfile failed: $_" }
$firewallRules = @()
try {
$firewallRules = Get-NetFirewallRule -PolicyStore ActiveStore -ErrorAction SilentlyContinue | Select-Object DisplayName, Direction, Enabled, Profile, Action, @{Name='Ports';Expression={(Get-NetFirewallPortFilter -AssociatedNetFirewallRule $_ | Select-Object -ExpandProperty LocalPort -ErrorAction SilentlyContinue) -join ','}}
} catch {}
# ---- Local users / groups and administrators ----
$localUsers = @()
$localGroups = @()
try {
$localUsers = Get-LocalUser | Select-Object Name, Enabled, LastLogon
$localGroups = Get-LocalGroup | Select-Object Name
$localAdmins = Get-LocalGroupMember -Group "Administrators" -ErrorAction SilentlyContinue | Select-Object Name, ObjectClass
} catch { Write-Verbose "Local accounts enumeration failed (module may be unavailable or non-elevated)." }
# ---- SMB shares & sessions ----
$smbShares = @()
$smbSessions = @()
try {
$smbShares = Get-SmbShare | Select-Object Name, Path, Description, ScopeName, ConcurrentUserLimit, EncryptData
$smbSessions = Get-SmbSession | Select-Object ClientComputerName, ClientUserName, NumOpens, SessionId
} catch { Write-Verbose "SMB enumeration failed or not supported on this OS." }
# ---- Scheduled tasks ----
$scheduledTasks = @()
try {
$scheduledTasks = Get-ScheduledTask | Select-Object TaskName, TaskPath, State, Principal
} catch { Write-Verbose "ScheduledTask enumeration failed: $_" }
# ---- Drivers ----
$drivers = @()
try {
$drivers = Get-CimInstance -ClassName Win32_PnPSignedDriver | Select-Object DeviceName, DriverVersion, Manufacturer, DriverDate, ClassName, InfName
} catch { Write-Verbose "Driver enumeration failed: $_" }
# ---- BitLocker ----
$bitlocker = $null
try {
if (Get-Command -Name Get-BitLockerVolume -ErrorAction SilentlyContinue) {
$bitlocker = Get-BitLockerVolume | Select-Object MountPoint, VolumeStatus, LockStatus, KeyProtector
} else {
$bitlocker = "Get-BitLockerVolume cmdlet not present on this platform"
}
} catch { $bitlocker = "BitLocker query failed: $_" }
# ---- Windows Defender / Anti-Malware ----
$defender = $null
try {
if (Get-Command -Name Get-MpComputerStatus -ErrorAction SilentlyContinue) {
$defender = Get-MpComputerStatus | Select-Object AMServiceEnabled, AntivirusEnabled, AntivirusSignatureLastUpdated, AntiSpywareEnabled, ProductUptoDate
} else {
$defender = "Windows Defender cmdlets not available on this system"
}
} catch { $defender = "Get-MpComputerStatus failed: $_" }
# ---- Installed roles/features (server/client) ----
$features = @()
try {
if (Get-Command -Name Get-WindowsFeature -ErrorAction SilentlyContinue) {
$features = Get-WindowsFeature | Where-Object Installed -eq $true | Select-Object DisplayName, Name
} else {
# fallback: DISM /OptionalFeatures
$features = (dism /online /get-features /format:table) -split "`n"
}
} catch { Write-Verbose "Feature query failed: $_" }
# ---- Scheduled updates / Windows Update last check (best-effort) ----
$wu = @{
Hotfixes = $hotfixes
LastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime
}
# Note: Get-HotFix / Win32_QuickFixEngineering may not show all modern update channels; use enterprise tooling (SCCM/WSUS) for authoritative view. :contentReference[oaicite:3]{index=3}
# ---- Scheduled Tasks for discovery / AV / services etc. already captured above ----
# ---- Collect install locations for hashing (from registry entries' InstallLocation) ----
$installLocations = $installedApps | Where-Object { $_.InstallLocation } | Select-Object -ExpandProperty InstallLocation | Sort-Object -Unique
# also include ProgramFiles and ProgramFiles(x86)
$installLocations += (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name ProgramFilesDir -ErrorAction SilentlyContinue).ProgramFilesDir
$installLocations += (Get-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion" -Name ProgramFilesDir -ErrorAction SilentlyContinue).ProgramFilesDir
$installLocations = $installLocations | Where-Object { $_ -and (Test-Path $_) } | Sort-Object -Unique
# ---- Optional file hashing (careful, slow) ----
$hashes = @()
if ($ComputeFileHash -and $installLocations.Count -gt 0) {
$fileCount = 0
foreach ($loc in $installLocations) {
foreach ($pattern in $HashIncludePatterns) {
try {
Get-ChildItem -Path $loc -Include $pattern -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
if ($fileCount -ge $MaxHashFiles) { return }
try {
$h = Get-FileHash -Path $_.FullName -Algorithm SHA256
$hashes += [PSCustomObject]@{
Path = $_.FullName
Length = $_.Length
LastWriteTime = $_.LastWriteTime
SHA256 = $h.Hash
}
$fileCount++
} catch { Write-Verbose "Hash failed for $($_.FullName): $_" }
}
} catch { Write-Verbose "Enumerate $loc failed: $_" }
}
if ($fileCount -ge $MaxHashFiles) { break }
}
}
# ---- Scheduled tasks, services, processes already captured ----
# ---- Build final object ----
$report = [ordered]@{
Metadata = @{
CollectedAt = (Get-Date).ToString("o")
CollectedBy = "$env:USERNAME@$env:COMPUTERNAME"
Notes = "This script gathers inventory and configuration data. It does NOT run active vulnerability plugins."
}
System = $sys
InstalledApplications = $installedApps
PackageManagers = $packageManagers
Hotfixes = $hotfixes
Services = $services
Processes = $processes
ListeningPorts = $tcp
FirewallProfiles = $firewallProfiles
FirewallRules = $firewallRules
LocalUsers = $localUsers
LocalGroups = $localGroups
LocalAdministrators = $localAdmins
SMB = @{
Shares = $smbShares
Sessions = $smbSessions
}
ScheduledTasks = $scheduledTasks
Drivers = $drivers
BitLocker = $bitlocker
Defender = $defender
Features = $features
InstallLocations = $installLocations
FileHashes = $hashes
}
# ---- Write JSON ----
try {
$report | ConvertTo-Json -Depth 6 | Out-File -FilePath $JsonOut -Encoding UTF8
Write-Host "JSON report written to $JsonOut"
} catch {
Write-Warning "Failed to write JSON: $_"
}
# ---- Simple HTML Summary ----
function ToHtmlSafe($s) { if ($null -eq $s) { "" } else { [System.Web.HttpUtility]::HtmlEncode($s.ToString()) } }
$html = @"
<html><head><meta charset='utf-8'><title>Host Inventory Report - $($sys.ComputerName)</title>
<style> body{font-family:Segoe UI, Arial; font-size:13px} h2{color:#2a5d9f} table{border-collapse:collapse; width:100%} th,td{border:1px solid #ddd; padding:6px} th{background:#f2f2f2}</style>
</head><body>
<h1>Host Inventory Report: $(ToHtmlSafe $sys.ComputerName)</h1>
<p>Collected: $(ToHtmlSafe $report.Metadata.CollectedAt)</p>
<h2>OS</h2>
<table><tr><th>Field</th><th>Value</th></tr>
<tr><td>Caption</td><td>$(ToHtmlSafe $sys.OS.Caption)</td></tr>
<tr><td>Version</td><td>$(ToHtmlSafe $sys.OS.Version)</td></tr>
<tr><td>Build</td><td>$(ToHtmlSafe $sys.OS.Build)</td></tr>
<tr><td>Memory (GB)</td><td>$(ToHtmlSafe $sys.MemoryGB)</td></tr>
</table>
<h2>Top Installed Applications (sample)</h2>
<table><tr><th>Name</th><th>Version</th><th>Publisher</th><th>InstallLocation</th></tr>
"@
# include a small sample of installed apps (first 30)
$report.InstalledApplications | Select-Object -First 30 | ForEach-Object {
$html += "<tr><td>$(ToHtmlSafe $_.DisplayName)</td><td>$(ToHtmlSafe $_.DisplayVersion)</td><td>$(ToHtmlSafe $_.Publisher)</td><td>$(ToHtmlSafe $_.InstallLocation)</td></tr>`n"
}
$html += @"
</table>
<h2>Listening Ports</h2>
<table><tr><th>LocalAddress</th><th>Port</th><th>PID</th><th>Process</th></tr>
"@
$report.ListeningPorts | ForEach-Object {
$html += "<tr><td>$(ToHtmlSafe $_.LocalAddress)</td><td>$(ToHtmlSafe $_.LocalPort)</td><td>$(ToHtmlSafe $_.OwningProcess)</td><td>$(ToHtmlSafe $_.ProcessName)</td></tr>`n"
}
$html += @"
</table>
<h2>Local Administrators</h2>
<table><tr><th>Name</th><th>Type</th></tr>
"@
if ($report.LocalAdministrators) {
$report.LocalAdministrators | ForEach-Object { $html += "<tr><td>$(ToHtmlSafe $_.Name)</td><td>$(ToHtmlSafe $_.ObjectClass)</td></tr>`n" }
} else { $html += "<tr><td colspan='2'>No data (non-elevated or module missing)</td></tr>" }
$html += @"
</table>
<h2>Windows Defender Summary</h2>
<pre>$(ToHtmlSafe ($report.Defender | Out-String))</pre>
<h2>Hotfixes (sample)</h2>
<pre>$(ToHtmlSafe (($report.Hotfixes | Select-Object -First 20 | Format-Table -AutoSize | Out-String)))</pre>
<p>Full JSON report available: $JsonOut</p>
</body></html>
"@
try {
$html | Out-File -FilePath $HtmlOut -Encoding UTF8
Write-Host "HTML summary written to $HtmlOut"
} catch {
Write-Warning "Failed to write HTML: $_"
}
# ---- Final message ----
Write-Host "Done. JSON: $JsonOut HTML: $HtmlOut"
Write-Host "Notes:"
Write-Host " - Some commands require Admin. If you ran non-elevated, re-run elevated for fuller data."
Write-Host " - This collection is inventory/configuration only. To map to CVEs you must compare the inventory (packages, versions, hotfixes) against vulnerability feeds (NVD, vendor advisories, Tenable plugin DB)."
#!/usr/bin/env perl
use strict;
use warnings;
use Text::CSV_XS;
use Unicode::Normalize qw(NFKD);
use Getopt::Long;
# Optional: load a regex-synonym table for publishers
# CSV columns: pattern,publisher_norm (pattern is a Perl regex, e.g. (?i)^microsoft( corporation| corp\.)?$
my $publisher_synonyms;
GetOptions("publisher-synonyms=s" => \$publisher_synonyms);
my @COMPANY_SUFFIX = qw(
incorporated inc llc l.l.c. ltd limited corp corporation co ag gmbh s.a. sàrl bv oy ab kg kk pty pte sas srl
);
sub _nfk_clean {
my ($s) = @_;
$s = lc($s // "");
$s = NFKD($s);
$s =~ s/\pM//g; # strip combining marks
$s =~ s/^\s+|\s+$//g; # trim
return $s;
}
my @pub_patterns;
if ($publisher_synonyms) {
open my $L, "<:utf8", $publisher_synonyms or die "$publisher_synonyms: $!";
my $csvL = Text::CSV_XS->new({ binary => 1, auto_diag => 1 });
$csvL->column_names($csvL->getline($L));
while (my $row = $csvL->getline_hr($L)) {
push @pub_patterns, { pattern => $row->{pattern}, norm => $row->{publisher_norm} } if $row->{pattern} && $row->{publisher_norm};
}
close $L;
}
sub normalize_publisher {
my ($p) = @_;
$p = _nfk_clean($p);
# drop company suffixes
my $suffix_rx = join "|", map { quotemeta } @COMPANY_SUFFIX;
$p =~ s/\b($suffix_rx)\b//g;
$p =~ s/[^a-z0-9]+/ /g;
$p =~ s/\s+/ /g;
$p =~ s/^\s+|\s+$//g;
# synonym regexes
for my $r (@pub_patterns) {
if ($p =~ /$r->{pattern}/) {
$p = $r->{norm};
last;
}
}
return $p;
}
# Regex fragments used by both SPL and Perl
my $ARCH = qr/\b(?:x64|x86|amd64|arm64|ia-?32|32-bit|64-bit)\b/i;
my $LOCALE = qr/\b(?:en[-_]us|en[-_]gb|fr[-_]fr|de[-_]de|ja[-_]jp|zh[-_](?:cn|tw))\b/i;
my $NOISE = qr/\b(?:msi|exe|installer|setup|hotfix|security update|cumulative update|rollup|patch|kb\d+|build\s*\d+)\b/i;
my $VER = qr/\b(?:v(?:ersion)?\s*)?(\d+(?:\.\d+)+(?:[-_a-z0-9]+)?)\b/i;
sub extract_version_from_name {
my ($name) = @_;
return $1 if $name =~ $VER;
return undef;
}
sub normalize_product {
my ($prod) = @_;
my $p = _nfk_clean($prod);
# remove arch/locale/noise tails
$p =~ s/$NOISE.*$//i;
$p =~ s/$ARCH//gi;
$p =~ s/$LOCALE//gi;
# remove parenthetical blocks that only contain metadata
$p =~ s/\((?:[^)]*?(?:$ARCH|$LOCALE|kb\d+|build\s*\d+)[^)]*)\)//gi;
# remove apparent version tokens
$p =~ s/$VER//gi;
# collapse and trim
$p =~ s/[^a-z0-9]+/ /g;
$p =~ s/\s+/ /g;
$p =~ s/^\s+|\s+$//g;
return $p;
}
# --- CSV IO ---
my $csv = Text::CSV_XS->new({ binary => 1, auto_diag => 1 });
binmode STDIN, ":utf8";
binmode STDOUT, ":utf8";
# read header
my $hdr = $csv->getline(*STDIN) or die "Expected header line";
$csv->column_names(@$hdr);
my @out_cols = qw(publisher product version publisher_norm product_norm version_norm group_key);
my $out = Text::CSV_XS->new({ binary => 1, eol => "\n" });
$out->print(*STDOUT, \@out_cols);
while (my $row = $csv->getline_hr(*STDIN)) {
my ($publisher, $product, $version) = @{$row}{qw/publisher product version/};
my $publisher_norm = normalize_publisher($publisher);
my $product_norm = normalize_product($product);
my $version_norm = $version // extract_version_from_name($product) // "";
my $group_key = ($publisher_norm || "")."::".($product_norm || "");
$out->print(*STDOUT, [$publisher, $product, ($version // ""), $publisher_norm, $product_norm, $version_norm, $group_key]);
}
<#
.SYNOPSIS
Collect a Splunk-style software inventory on a Windows host (agent-friendly).
.DESCRIPTION
- Enumerates installed applications (registry ARP keys), package managers (winget/choco/Get-Package),
MSI product codes, installed updates, services, scheduled tasks, listening ports, file hashes (optional),
and Splunk Universal Forwarder presence/status.
- Produces JSON and CSV outputs in the current directory.
- Optionally posts the JSON to a Splunk HEC endpoint (set $EnableHEC = $true and supply $HECUrl/$HECToken).
- Avoids Win32_Product because it can trigger Windows Installer repairs; uses registry + package managers instead.
- Designed to be run locally (or executed from a Splunk scripted input).
.NOTES
- Run elevated (Admin) for full data; some items (listening ports, local groups, BitLocker) require elevation.
- This is a local inventory collector only — it does not perform active vulnerability checks.
#>
param(
[switch]$ComputeHashes = $false,
[int]$MaxHashFiles = 200,
[string]$OutputPrefix = "splunk_inventory",
[switch]$EnableHEC = $false,
[string]$HECUrl = "", # e.g. https://splunk-collector:8088/services/collector/event/1.0
[string]$HECToken = "" # Splunk HEC token if posting
)
function Ensure-Elevated {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warning "This script is not running elevated. Some data will be missing."
}
}
# Utility safe wrapper
function Safe-Run {
param($ScriptBlock)
try { & $ScriptBlock } catch { Write-Verbose "Error running block: $_"; return $null }
}
Ensure-Elevated
$now = (Get-Date).ToString("yyyyMMdd_HHmmss")
$jsonFile = "{0}_{1}.json" -f $OutputPrefix, $now
$csvFile = "{0}_{1}.csv" -f $OutputPrefix, $now
# ---- Basic host metadata ----
$hostMeta = [ordered]@{
ComputerName = $env:COMPUTERNAME
Hostname = ([System.Net.Dns]::GetHostName()) -as [string]
FQDN = try { ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName) } catch { $null }
CollectedAt = (Get-Date).ToString("o")
CollectedBy = "$env:USERNAME@$env:COMPUTERNAME"
OS = try { (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -First 1 Caption, Version, BuildNumber, OSArchitecture) } catch { $null }
UFInstalled = $false
UFServiceStatus = $null
}
# ---- Detect Splunk Universal Forwarder presence ----
$ufPaths = @(
"C:\Program Files\SplunkUniversalForwarder",
"C:\Program Files\Splunk\UniversalForwarder",
"C:\Program Files\Splunk\bin",
"C:\Program Files (x86)\SplunkUniversalForwarder"
)
foreach ($p in $ufPaths) {
if (Test-Path $p) { $hostMeta.UFInstalled = $true; break }
}
# check service
try {
$svc = Get-Service -Name "SplunkForwarder","splunkforwarder","SplunkForwarder" -ErrorAction SilentlyContinue | Select-Object -First 1 Name,Status
if ($svc) { $hostMeta.UFInstalled = $true; $hostMeta.UFServiceStatus = $svc }
} catch {}
# ---- Helper: gather installed apps from registry ARP ----
function Get-InstalledAppsFromRegistry {
$keys = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$apps = @()
foreach ($k in $keys) {
try {
Get-ItemProperty -Path $k -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.DisplayName) {
$apps += [PSCustomObject]@{
DisplayName = $_.DisplayName
DisplayVersion = $_.DisplayVersion
Publisher = $_.Publisher
InstallDate = $_.InstallDate
InstallLocation = $_.InstallLocation
UninstallString = $_.UninstallString
RegistryKey = $_.PSPath
EstimatedSource = "ARP/Registry"
MsiProductCode = ($_.PSChildName -match '^{.*}$') ? $_.PSChildName : $null
}
}
}
} catch { Write-Verbose "Registry read failed for $k: $_" }
}
# dedupe by DisplayName+Publisher
$apps | Sort-Object DisplayName, Publisher -Unique
}
# ---- Additional package sources (winget, choco, Get-Package) ----
function Get-PackageManagerInventory {
$pm = [ordered]@{}
# winget
try {
$winget = & winget list --source winget 2>$null
if ($LASTEXITCODE -eq 0 -and $winget) { $pm.winget = ($winget -split "`n" | Where-Object { $_ -and ($_ -notmatch '^Name\s+Id') }) }
} catch {}
# chocolatey
try {
$choco = & choco list --localonly 2>$null
if ($LASTEXITCODE -eq 0 -and $choco) { $pm.choco = ($choco -split "`n" | Where-Object { $_ -and ($_ -notmatch '^\s*' ) }) }
} catch {}
# PowerShell Get-Package
try {
$g = Get-Package -ErrorAction SilentlyContinue
if ($g) { $pm.GetPackage = $g | Select-Object Name, Version, ProviderName, Source }
} catch {}
return $pm
}
# ---- Installed updates/hotfixes ----
function Get-HotFixesSafe {
try { Get-HotFix -ErrorAction Stop | Select-Object -Property Description, HotFixID, InstalledOn } catch { return $null }
}
# ---- MSI product codes (from ARP and registry GUIDs) ----
function Get-MSIProductsViaReg {
$products = @()
# HKLM\Software\Classes\Installer\Products and other places are tricky; ARP PSChildName often is product code
$arp = Get-InstalledAppsFromRegistry
foreach ($a in $arp) {
if ($a.MsiProductCode) {
$products += [PSCustomObject]@{ ProductCode = $a.MsiProductCode; Name = $a.DisplayName; Version = $a.DisplayVersion; Source = "ARP-MSI" }
}
}
return $products
}
# ---- Services, listening ports, processes ----
function Get-HostRuntimeInfo {
$s = Get-Service | Select-Object Name, DisplayName, Status, StartType, ServiceType
$p = Get-Process | Select-Object Id, ProcessName, Path -ErrorAction SilentlyContinue
$tcp = @()
try {
$tcp = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, OwningProcess
$tcp = $tcp | ForEach-Object {
$proc = $null
try { $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue } catch {}
[PSCustomObject]@{
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
OwningProcess = $_.OwningProcess
ProcessName = if ($proc) { $proc.ProcessName } else { $null }
}
}
} catch { Write-Verbose "NetTCP failed: $_" }
return [ordered]@{ Services = $s; Processes = $p; Listening = $tcp }
}
# ---- Scheduled tasks, local admins, SMB shares ----
function Get-ExtraInventory {
$tasks = @(); $localAdmins = @(); $shares = @()
try { $tasks = Get-ScheduledTask | Select-Object TaskName, TaskPath, State, Principal -ErrorAction SilentlyContinue } catch {}
try {
$localAdmins = Get-LocalGroupMember -Group "Administrators" -ErrorAction SilentlyContinue | Select-Object Name, ObjectClass
} catch {}
try { $shares = Get-SmbShare | Select-Object Name, Path, Description -ErrorAction SilentlyContinue } catch {}
return [ordered]@{ Tasks = $tasks; LocalAdmins = $localAdmins; SMB = $shares }
}
# ---- Optional: compute file hashes for discovered install locations ----
function Compute-FileHashes {
param($Paths, $Patterns = @('*.exe','*.dll'), $Max = 200)
$hashes = @(); $count = 0
foreach ($p in $Paths) {
if (-not (Test-Path $p)) { continue }
foreach ($pat in $Patterns) {
try {
Get-ChildItem -Path $p -Filter $pat -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
if ($count -ge $Max) { return $hashes }
try {
$h = Get-FileHash -Path $_.FullName -Algorithm SHA256
$hashes += [PSCustomObject]@{ Path = $_.FullName; Size = $_.Length; LastWrite = $_.LastWriteTime; SHA256 = $h.Hash }
$count++
} catch { Write-Verbose "Hash fail: $_" }
}
} catch { Write-Verbose "Enum fail $p: $_" }
}
}
return $hashes
}
# ---- Main collection ----
Write-Host "Collecting installed applications (registry ARP)..."
$apps = Get-InstalledAppsFromRegistry
Write-Host "Collecting package manager inventories (winget/choco/Get-Package)..."
$pkgManagers = Get-PackageManagerInventory
Write-Host "Collecting installed hotfixes..."
$hotfixes = Get-HotFixesSafe
Write-Host "Collecting MSI product codes..."
$msiProducts = Get-MSIProductsViaReg
Write-Host "Collecting services/processes/listening ports..."
$runtime = Get-HostRuntimeInfo
Write-Host "Collecting scheduled tasks / local admins / shares..."
$extras = Get-ExtraInventory
# Build a list of install locations to consider hashing
$installLocations = $apps | Where-Object { $_.InstallLocation } | Select-Object -ExpandProperty InstallLocation -Unique
# add ProgramFiles
try {
$pf = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name ProgramFilesDir -ErrorAction SilentlyContinue).ProgramFilesDir
if ($pf) { $installLocations += $pf }
$pf86 = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion" -Name ProgramFilesDir -ErrorAction SilentlyContinue).ProgramFilesDir
if ($pf86) { $installLocations += $pf86 }
} catch {}
$installLocations = $installLocations | Where-Object { $_ -and (Test-Path $_) } | Sort-Object -Unique
$hashes = @()
if ($ComputeHashes -and $installLocations.Count -gt 0) {
Write-Host "Computing up to $MaxHashFiles file hashes (this may be slow)..."
$hashes = Compute-FileHashes -Paths $installLocations -Patterns @('*.exe','*.dll') -Max $MaxHashFiles
}
# ---- Prepare final object ----
$report = [ordered]@{
Metadata = $hostMeta
InstalledApplications = $apps
PackageManagerInventory = $pkgManagers
MSIProducts = $msiProducts
HotFixes = $hotfixes
Runtime = $runtime
Extras = $extras
InstallLocations = $installLocations
FileHashes = $hashes
}
# ---- Write outputs ----
try {
$report | ConvertTo-Json -Depth 8 | Out-File -FilePath $jsonFile -Encoding UTF8
Write-Host "JSON written to $jsonFile"
} catch { Write-Warning "Failed to write JSON: $_" }
# For CSV — flatten installed apps to a simple inventory CSV
try {
$apps | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, UninstallString |
Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8
Write-Host "CSV written to $csvFile"
} catch { Write-Warning "Failed to write CSV: $_" }
# ---- Optional: send to Splunk HEC ----
if ($EnableHEC) {
if (-not ($HECUrl) -or -not ($HECToken)) {
Write-Warning "HEC enabled but HECUrl or HECToken is empty. Skipping send."
} else {
Write-Host "Posting JSON to Splunk HEC..."
try {
$body = @{
time = [int][double]::Parse((Get-Date -UFormat %s))
host = $env:COMPUTERNAME
source = "script:collect-splunk-inventory"
sourcetype = "inventory:software:windows"
event = (Get-Content -Path $jsonFile -Raw)
} | ConvertTo-Json -Depth 5
$hdr = @{ "Authorization" = "Splunk $HECToken" }
Invoke-RestMethod -Uri $HECUrl -Method Post -Headers $hdr -Body $body -ContentType "application/json" -ErrorAction Stop
Write-Host "Posted to HEC: $HECUrl"
} catch { Write-Warning "Failed to post to HEC: $_" }
}
}
Write-Host "Done. Files: $jsonFile , $csvFile"
Write-Host "Notes:"
Write-Host " - This script collects the same sort of local inventory Splunk admins forward from UF/scripted inputs (registry, WMI, package managers). To match the depth of a centralized Splunk software inventory, deploy this as a scripted input or have the UF forward the generated JSON to your indexers. Splunk UF supports scripted inputs and arbitrary host-local scripts to gather inventory. :contentReference[oaicite:1]{index=1}"
Write-Host " - Avoid using Win32_Product for inventory (it can cause MSI repairs). Use registry ARP keys + package managers + product codes. :contentReference[oaicite:2]{index=2}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment