Files
usmt-dash/Get-Software.ps1
Johan Sandoval 1c531bc2ab Generalize USMT Dash and add a containerized demo mode
Remove all environment-specific data and make the dashboard adoptable
anywhere, then add a demo that runs the real PowerShell services.

- Strip cached device data, internal knowledge-base articles and
  screenshots, server/site names and the old project notes; replace the
  knowledge base with generic sample articles and diagrams
- Move every environment value into Config/usmt-dash.psd1
- Route all infrastructure access in the worker and Get-Software.ps1
  through a provider (Providers/SCCM.ps1), with a config-driven asset
  inventory view instead of a hard-coded custom class
- Add Providers/Demo.ps1: a deterministic simulated fleet whose
  migrations progress in real time, and Demo/New-DemoData.ps1 to seed it
- Server: -Demo makes the API read-only; add get-config and get-logs;
  serve static files only from web/, Documentation/ and an allowlist of
  Data/ files with a path-traversal guard; gzip large responses
- Mirror both services' console output to Data/Logs via a Write-Host
  proxy, and add a terminal panel to the dashboard that streams it
- Demo sandbox (web/demo/demo.js): visitor changes to tags, articles and
  images are kept in the visitor's browser and echoed in the terminals;
  nothing a visitor does reaches the server
- Vendor marked, highlight.js and html2pdf.js instead of loading CDNs
- Dockerfile and compose file for hosting the demo (Coolify-ready)
2026-10-06 15:20:43 +00:00

141 lines
4.6 KiB
PowerShell
Executable File

param(
[string]$ComputerName,
[string]$ConfigPath,
[switch]$Demo
)
if ([string]::IsNullOrWhiteSpace($ComputerName)) { return "[]" }
$scriptDir = $PSScriptRoot
$dataDir = Join-Path $scriptDir "Data"
if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null }
$cacheFile = Join-Path $dataDir "Software-Cache.json"
$blacklistFile = Join-Path $dataDir "Blacklisted-Apps.txt"
if (-not (Test-Path $cacheFile)) { Set-Content -Path $cacheFile -Value "{}" -Encoding UTF8 }
if (-not (Test-Path $blacklistFile)) { Set-Content -Path $blacklistFile -Value "" -Encoding UTF8 }
. (Join-Path $scriptDir "Lib/Common.ps1")
$config = Import-DashConfig -Path $ConfigPath -Demo:$Demo
$cacheHours = $config.Worker.SoftwareCacheHours
# --- 1. CHECK THE CACHE (24-Hour Timer Only) ---
$needsUpdate = $true
if (Test-Path $cacheFile) {
try {
$rawCache = Read-SharedText $cacheFile
if (-not [string]::IsNullOrWhiteSpace($rawCache)) {
$cacheData = $rawCache | ConvertFrom-Json
$propExists = $false
foreach ($p in $cacheData.psobject.properties) {
if ($p.Name -eq $ComputerName) { $propExists = $true; break }
}
if ($propExists) {
$deviceCache = $cacheData.$ComputerName
if ($null -ne $deviceCache.Timestamp -and $null -ne $deviceCache.Data) {
$cacheTime = [datetime]$deviceCache.Timestamp
# Logic Check: Is cache still fresh?
if ($cacheTime -gt (Get-Date).AddHours(-1 * $cacheHours)) {
$needsUpdate = $false
$outData = @($deviceCache.Data)
return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress)
}
}
}
}
} catch {
# If cache is corrupt, proceed to the provider
}
}
if (-not $needsUpdate) { return }
# --- 2. LOAD BLACKLIST ---
$blacklist = @()
if (Test-Path $blacklistFile) {
$blacklist = Get-Content $blacklistFile | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }
}
# --- 3. PROVIDER QUERY (SCCM SQL in Live mode, simulated fleet in Demo mode) ---
$providerName = if ($config.IsDemo) { "Demo" } else { $config.Provider }
$v = $false
. (Join-Path $scriptDir "Providers/$providerName.ps1")
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
foreach ($row in (Get-InstalledSoftware -ComputerName $ComputerName)) {
$appName = "$($row.Name)".Trim()
if ([string]::IsNullOrWhiteSpace($appName) -or $appName -eq "--") { continue }
$isSpecialty = $true
foreach ($badApp in $blacklist) {
if ($appName -like $badApp) {
$isSpecialty = $false
break
}
}
$results.Add([PSCustomObject]@{
Name = $appName
Version = $row.Version
Publisher = $row.Publisher
Source = $row.Source
InstallDate = $row.InstallDate
IsSpecialty = $isSpecialty
})
}
} catch {}
# --- 4. ATOMIC CACHE SAVE ---
try {
$cacheEntry = [PSCustomObject]@{
Timestamp = (Get-Date).ToString("o")
Data = @($results)
}
for ($i = 0; $i -lt 10; $i++) {
try {
$fs = [System.IO.File]::Open($cacheFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
$sr = New-Object System.IO.StreamReader($fs)
$rawCache = $sr.ReadToEnd()
$latestCache = [PSCustomObject]@{}
if (-not [string]::IsNullOrWhiteSpace($rawCache)) {
try { $latestCache = $rawCache | ConvertFrom-Json } catch {}
}
$propExists = $false
foreach ($p in $latestCache.psobject.properties) {
if ($p.Name -eq $ComputerName) { $propExists = $true; break }
}
if ($propExists) {
$latestCache.$ComputerName = $cacheEntry
} else {
$latestCache | Add-Member -MemberType NoteProperty -Name $ComputerName -Value $cacheEntry -Force
}
$fs.SetLength(0)
$sw = New-Object System.IO.StreamWriter($fs, [System.Text.Encoding]::UTF8)
$sw.Write(($latestCache | ConvertTo-Json -Depth 6 -Compress))
$sw.Flush()
$sw.Close()
$sr.Close()
$fs.Close()
break
} catch {
Start-Sleep -Milliseconds 300
}
}
} catch {}
$outData = @($results)
return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress)