Fix Windows task and certificate collector reliability

This commit is contained in:
2026-10-30 13:06:14 -07:00
parent 57e716b710
commit 7fed3b9ab9
11 changed files with 283 additions and 19 deletions

View File

@@ -4,12 +4,23 @@ function Get-SamsaraCertificates {
Invoke-SamsaraRead $Result 'Local machine certificate store list' {
$collection.stores = @(Get-ChildItem -LiteralPath 'Cert:\LocalMachine' -ErrorAction Stop)
}
# Listing store names is not evidence that any listed store was readable.
if ($collection.stores.Count -gt 0) { $Result.successful_subchecks-- }
foreach ($store in $collection.stores) {
Invoke-SamsaraRead $Result 'Local machine public certificate metadata' {
Get-ChildItem -LiteralPath $store.PSPath -ErrorAction Stop | ForEach-Object {
$Result.data += [pscustomobject]@{ subject = [string]$_.Subject; issuer = [string]$_.Issuer
expires = $_.NotAfter.ToUniversalTime().ToString('o'); thumbprint = [string]$_.Thumbprint
store = [string]$store.PSChildName }
# Certificate provider PSPath may omit LocalMachine (including on 5.1).
# Construct an explicit provider path from the enumerated store name.
$storeName = [string]$store.PSChildName
Invoke-SamsaraRead $Result 'Certificate store query' {
if ($storeName -notmatch '^[A-Za-z0-9_. -]+$') { throw 'Invalid store name' }
$certificates = @(Get-ChildItem -LiteralPath ('Cert:\LocalMachine\' + $storeName) -ErrorAction Stop)
foreach ($certificate in $certificates) {
Invoke-SamsaraRead $Result 'Certificate metadata projection' {
$row = [pscustomobject]@{ subject = [string]$certificate.Subject; issuer = [string]$certificate.Issuer
expires = $certificate.NotAfter.ToUniversalTime().ToString('o'); thumbprint = [string]$certificate.Thumbprint
store = $storeName }
$null = ConvertTo-Json -InputObject $row -Depth 4 -ErrorAction Stop
$Result.data += $row
}
}
}
}

View File

@@ -1,12 +1,17 @@
#requires -Version 5.1
[CmdletBinding()]
param([ValidateSet('all','system','roles','services','network','tasks','software','storage','shares','certificates')][string]$Section = 'all')
param([ValidateSet('all','system','roles','services','network','tasks','software','storage','shares','certificates')][string]$Section = 'all', [switch]$Diagnostic)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$env:PSModulePath = Join-Path $PSHOME 'Modules'
if ($PSVersionTable.PSEdition -eq 'Core') {
# Inbox Windows modules remain under WindowsPowerShell when invoked from PS7.
$env:PSModulePath += ';' + (Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell\v1.0\Modules')
}
$script:SamsaraDiagnostic = $Diagnostic.IsPresent
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
[Console]::Error.WriteLine('This collector requires a local Windows Server execution host.')
@@ -30,7 +35,7 @@ $collectors = [ordered]@{
}
if ($Section -ne 'all') {
$result = Invoke-SamsaraSection $Section $collectors[$Section]
ConvertTo-Json -InputObject $result -Depth 12 -Compress
ConvertTo-SamsaraJson $result 12
exit 0
}
# Direct PowerShell use emits the same complete inventory envelope as the Python backend.
@@ -50,7 +55,7 @@ foreach ($name in $collectors.Keys) {
} elseif ($name -eq 'shares') { $inventory.storage += @($result.data) }
else { $inventory[$name] = @($result.data) }
}
ConvertTo-Json -InputObject ([ordered]@{ schema_version='1.0'; synthetic=$false; collected_at=$collectedAt
server=$server; checks=$checks; inventory=$inventory }) -Depth 14 -Compress
ConvertTo-SamsaraJson ([ordered]@{ schema_version='1.0'; synthetic=$false; collected_at=$collectedAt
server=$server; checks=$checks; inventory=$inventory }) 14
exit 0

View File

@@ -1,8 +1,10 @@
function Get-SamsaraTasks {
param($Result)
Invoke-SamsaraRead $Result 'Scheduled tasks' {
$Result.data = @(Get-ScheduledTask -ErrorAction Stop | ForEach-Object {
$task = $_
$tasks = @(Get-ScheduledTask -ErrorAction Stop)
foreach ($task in $tasks) {
Invoke-SamsaraRead $Result 'Scheduled task metadata projection' {
if ($null -eq $task -or [string]::IsNullOrEmpty([string]$task.TaskName)) { throw 'Missing task identity' }
$triggers = @($task.Triggers | ForEach-Object {
$type = if ($_.CimClass) { [string]$_.CimClass.CimClassName } else { 'Unknown trigger' }
# Select only known scheduler metadata; never serialize entire CIM objects.
@@ -10,11 +12,14 @@ function Get-SamsaraTasks {
'{0}; start={1}; enabled={2}; daysInterval={3}; weeksInterval={4}; daysOfWeek={5}; repeat={6}' -f $type, $_.StartBoundary, $_.Enabled, $_.DaysInterval, $_.WeeksInterval, $_.DaysOfWeek, $repetition
})
$executables = @($task.Actions | ForEach-Object { Get-SamsaraSafeExecutable ([string]$_.Execute) })
[pscustomobject]@{ name = [string]$task.TaskName; path = [string]$task.TaskPath
$row = [pscustomobject]@{ name = [string]$task.TaskName; path = [string]$task.TaskPath
enabled = [bool]$task.Settings.Enabled; state = [string]$task.State
account = [string]$task.Principal.UserId; executables = ($executables -join '; ')
account = if ($task.Principal.UserId) { [string]$task.Principal.UserId } else { [string]$task.Principal.GroupId }; executables = ($executables -join '; ')
triggers = if ($triggers.Count) { $triggers -join ' | ' } else { 'No trigger metadata visible; manual or external invocation remains possible' } }
})
$null = ConvertTo-Json -InputObject $row -Depth 4 -ErrorAction Stop
$Result.data += $row
}
}
}
$Result.limitations += 'Visible Task Scheduler registrations only. Arguments, inline scripts, task XML, event queries and execution history are omitted. Non-executable actions may lack an executable name. Trigger fields are descriptive, not a prediction of execution.'
}

View File

@@ -13,6 +13,13 @@ function Add-SamsaraError {
'command_unavailable' { 'Required local inventory command is unavailable.' }
default { 'Local query could not complete; raw diagnostics are excluded.' }
}
if ($script:SamsaraDiagnostic) {
# Fixed stages only; never include messages, identifiers, targets or stacks.
$type = $Record.Exception.GetType().Name
if ($type -notmatch '^[A-Za-z0-9]+Exception$') { $type = 'Exception' }
$category = [string]$Record.CategoryInfo.Category
$message += ' Exception type: ' + $type + '; category: ' + $category + '.'
}
$Result.errors += [pscustomobject]@{ code = $code; scope = $Scope; message = $message }
if ($code -eq 'access_denied') { $Result.permission_limitations += $Scope + ': read access denied.' }
}
@@ -21,15 +28,28 @@ function Invoke-SamsaraRead {
param($Result, [string]$Scope, [scriptblock]$Query)
$Result.attempted_subchecks++
try {
$ErrorActionPreference = 'Stop'
& $Query
$Result.successful_subchecks++
} catch { Add-SamsaraError $Result $_ $Scope }
}
function ConvertTo-SamsaraJson {
param($Value, [int]$Depth = 14)
try { ConvertTo-Json -InputObject $Value -Depth $Depth -Compress -ErrorAction Stop }
catch {
# A serialization failure must not emit a complete empty inventory.
[Console]::Error.WriteLine('Inventory JSON serialization failed; raw diagnostics are excluded.')
exit 3
}
}
function Get-SamsaraSafeExecutable {
param([string]$Value)
# Task action arguments, scripts and environment values are deliberately omitted.
$name = [IO.Path]::GetFileName(($Value -replace '\\', '/'))
# Scheduler action values need not be valid filesystem paths. Path.GetFileName
# throws on malformed/quoted values in .NET Framework (PowerShell 5.1).
$name = (($Value -replace '\\', '/') -split '/')[-1]
if ($name -match '^[A-Za-z0-9_. -]+\.(exe|com|cmd|bat|ps1|vbs|js|wsf|msc)$') { return $name }
return '[executable name omitted]'
}