Private/Compare.ps1
|
# Curated set of stable evidence sources worth diffing between two capsules of # the same host. Volatile sources (processes, live network endpoints, event # summaries, timeline) are intentionally excluded: their churn would bury the # meaningful persistence-, account-, and inventory-level changes. function Get-ICComparisonSpec { [CmdletBinding()] param() return @( [pscustomobject]@{ Key = 'Services'; Source = 'evidence/services/services.json'; Identity = @('Name'); Label = 'Windows services' } [pscustomobject]@{ Key = 'ScheduledTasks'; Source = 'evidence/scheduled-tasks/tasks.json'; Identity = @('TaskPath', 'TaskName'); Label = 'Scheduled tasks' } [pscustomobject]@{ Key = 'InstalledSoftware'; Source = 'evidence/software/installed-software.json'; Identity = @('Scope', 'KeyName'); Label = 'Installed software' } [pscustomobject]@{ Key = 'RegistryAutoruns'; Source = 'evidence/persistence/registry-autoruns.json'; Identity = @('Key', 'ValueName'); Label = 'Autorun registry values' } [pscustomobject]@{ Key = 'LocalUsers'; Source = 'evidence/identities/local-users.json'; Identity = @('SID'); Label = 'Local users' } [pscustomobject]@{ Key = 'LocalGroups'; Source = 'evidence/identities/local-groups.json'; Identity = @('SID'); Label = 'Local groups' } [pscustomobject]@{ Key = 'Certificates'; Source = 'evidence/certificates/certificate-stores.json'; Identity = @('Store', 'Thumbprint'); Label = 'Certificate trust stores' } [pscustomobject]@{ Key = 'SystemDrivers'; Source = 'evidence/drivers/system-drivers.json'; Identity = @('Name'); Label = 'System drivers' } ) } function Get-ICRecordIdentity { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Record, [Parameter(Mandatory)] [string[]]$IdentityFields ) $parts = foreach ($field in $IdentityFields) { $value = Get-ICPropertyValue -InputObject $Record -Name $field '{0}={1}' -f $field, ([string]$value) } return ($parts -join '|') } function Compare-ICEvidenceRecord { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Baseline, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Current, [Parameter(Mandatory)] [string[]]$IdentityFields ) $baselineIndex = [ordered]@{} foreach ($record in @($Baseline)) { if ($null -eq $record) { continue } $identity = Get-ICRecordIdentity -Record $record -IdentityFields $IdentityFields if (-not $baselineIndex.Contains($identity)) { $baselineIndex[$identity] = $record } } $currentIndex = [ordered]@{} foreach ($record in @($Current)) { if ($null -eq $record) { continue } $identity = Get-ICRecordIdentity -Record $record -IdentityFields $IdentityFields if (-not $currentIndex.Contains($identity)) { $currentIndex[$identity] = $record } } $added = New-Object System.Collections.ArrayList $removed = New-Object System.Collections.ArrayList $changed = New-Object System.Collections.ArrayList foreach ($identity in $currentIndex.Keys) { if (-not $baselineIndex.Contains($identity)) { [void]$added.Add([pscustomobject][ordered]@{ identity = $identity; current = $currentIndex[$identity] }) } } foreach ($identity in $baselineIndex.Keys) { if (-not $currentIndex.Contains($identity)) { [void]$removed.Add([pscustomobject][ordered]@{ identity = $identity; baseline = $baselineIndex[$identity] }) continue } $baselineJson = ConvertTo-Json -InputObject $baselineIndex[$identity] -Depth 20 -Compress $currentJson = ConvertTo-Json -InputObject $currentIndex[$identity] -Depth 20 -Compress if ($baselineJson -cne $currentJson) { $changedFields = New-Object System.Collections.ArrayList $names = @($baselineIndex[$identity].PSObject.Properties.Name) + @($currentIndex[$identity].PSObject.Properties.Name) | Select-Object -Unique foreach ($name in $names) { $before = Get-ICPropertyValue -InputObject $baselineIndex[$identity] -Name $name $after = Get-ICPropertyValue -InputObject $currentIndex[$identity] -Name $name $beforeJson = ConvertTo-Json -InputObject $before -Depth 20 -Compress $afterJson = ConvertTo-Json -InputObject $after -Depth 20 -Compress if ($beforeJson -cne $afterJson) { [void]$changedFields.Add([pscustomobject][ordered]@{ field = $name; baseline = $before; current = $after }) } } [void]$changed.Add([pscustomobject][ordered]@{ identity = $identity changedFields = @($changedFields) }) } } return [pscustomobject][ordered]@{ Added = @($added) Removed = @($removed) Changed = @($changed) } } function Get-ICCapsuleEvidenceData { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$CapsuleRoot, [Parameter(Mandatory)] [string]$Source ) $path = Join-Path $CapsuleRoot ($Source -replace '/', [System.IO.Path]::DirectorySeparatorChar) if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return [pscustomobject]@{ Available = $false; Data = @() } } try { $envelope = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { return [pscustomobject]@{ Available = $false; Data = @() } } if (-not (Test-ICObjectProperty -InputObject $envelope -Name 'data')) { return [pscustomobject]@{ Available = $false; Data = @() } } return [pscustomobject]@{ Available = $true; Data = @($envelope.data) } } |