Privacy hardening, remote-peer IP masking and review fixes
- Add privacy projection (core/privacy.py), private report storage, bounded
process capture; omit identities, free text and unapproved paths
- Mask remote peer IPs: only loopback/host/wildcard stay numeric, others are
reduced to remote_scope private/public (schema + scoring updated)
- Map ss TCP states to canonical names; ASCII-only numeric fields; fix
"Microsoft Visual C++" product matching (Python and PowerShell)
- Merge 7fed3b9: -Diagnostic switch, serialization exit code 3, PS7 module
path, UI error detail; keep hardened task/certificate collectors
- Add audit docs, privacy docs and regression tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,37 +12,25 @@ foreach ($file in @($files | Where-Object { $_.Name -ne 'Get-ServerInventory.ps1
|
||||
$script:SamsaraIsElevated = $true
|
||||
$script:SyntheticBuild = '20348'
|
||||
function Get-CimInstance {
|
||||
[CmdletBinding()] param($ClassName, $Property, $Filter)
|
||||
[CmdletBinding()] param($ClassName, $Property, $Filter, $Namespace)
|
||||
switch ($ClassName) {
|
||||
'Win32_OperatingSystem' { [pscustomobject]@{ Caption='Synthetic Windows Server'; Version=('10.0.' + $script:SyntheticBuild); BuildNumber=$script:SyntheticBuild; ProductType=3; LastBootUpTime=[DateTime]::UtcNow.AddDays(-2) } }
|
||||
'Win32_ComputerSystem' { [pscustomobject]@{ Domain='example.test'; PartOfDomain=$true } }
|
||||
'Win32_NetworkAdapterConfiguration' { [pscustomobject]@{ Description='Synthetic NIC'; IPAddress=@('192.0.2.20') } }
|
||||
'Win32_Service' { [pscustomobject]@{ Name='W3SVC'; DisplayName='Synthetic Web'; State='Running'; StartMode='Auto'; StartName='EXAMPLE\svc_web'; ProcessId=123; PathName='SECRET command arguments' } }
|
||||
'MSFT_ScheduledTask' { [pscustomobject]@{ TaskName='SECRET'; TaskPath='\Custom\SECRET\'; State=3 } }
|
||||
'Win32_LogicalDisk' { [pscustomobject]@{ DeviceID='C:'; Size=100GB; FreeSpace=50GB; FileSystem='NTFS' } }
|
||||
default { throw 'Unexpected synthetic CIM query' }
|
||||
}
|
||||
}
|
||||
function Get-WindowsFeature { [CmdletBinding()] param() [pscustomobject]@{ Name='Web-Server'; DisplayName='Synthetic Web Server'; Installed=$true; InstallState='Installed'; FeatureType='Role' } }
|
||||
function Get-Process { [CmdletBinding()] param() [pscustomobject]@{ Id=123; ProcessName='SyntheticWorker'; CommandLine='SECRET' } }
|
||||
function Get-NetTCPConnection { [CmdletBinding()] param() [pscustomobject]@{ State='Established'; LocalAddress='192.0.2.20'; LocalPort=443; RemoteAddress='192.0.2.21'; RemotePort=50000; OwningProcess=123 } }
|
||||
function Get-ScheduledTask {
|
||||
[CmdletBinding()] param()
|
||||
[pscustomobject]@{ TaskName='Synthetic report'; TaskPath='\Example\'; State='Ready'
|
||||
Settings=[pscustomobject]@{ Enabled=$true }; Principal=[pscustomobject]@{ UserId='EXAMPLE\svc_report' }
|
||||
Actions=@([pscustomobject]@{ Execute='C:\Tools\report.exe'; Arguments='--password=SECRET' })
|
||||
Triggers=@([pscustomobject]@{ CimClass=[pscustomobject]@{ CimClassName='MSFT_TaskDailyTrigger' }; StartBoundary='2026-10-01T02:00:00'; Enabled=$true; DaysInterval=1; Repetition=$null }) }
|
||||
}
|
||||
function Get-Volume { [CmdletBinding()] param() [pscustomobject]@{ FileSystemLabel='Synthetic OS'; DriveLetter='C'; Path='C:\'; FileSystem='NTFS'; Size=100GB; SizeRemaining=50GB } }
|
||||
function Get-SmbShare { [CmdletBinding()] param() [pscustomobject]@{ Name='SyntheticReports'; Path='C:\Reports'; Special=$false } }
|
||||
function Get-SmbOpenFile {
|
||||
[CmdletBinding()] param()
|
||||
[pscustomobject]@{ Path='C:\Reports\SECRET-file.txt'; ShareRelativePath='SECRET-file.txt'; ClientComputerName='192.0.2.21'; ClientUserName='SECRET' }
|
||||
[pscustomobject]@{ Path='C:\Reports\SECRET-admin.txt'; ShareRelativePath='Reports\SECRET-admin.txt'; ClientComputerName='192.0.2.21'; ClientUserName='SECRET' }
|
||||
}
|
||||
function Get-ChildItem {
|
||||
[CmdletBinding()] param($LiteralPath)
|
||||
if ($LiteralPath -eq 'Cert:\LocalMachine') { [pscustomobject]@{ PSPath='Cert:\LocalMachine\My'; PSChildName='My' } }
|
||||
else { [pscustomobject]@{ Subject='CN=synthetic.example.test'; Issuer='CN=Synthetic CA'; NotAfter=[DateTime]::UtcNow.AddDays(30); Thumbprint='0000000000000000000000000000000000000000'; PrivateKey='SECRET' } }
|
||||
}
|
||||
function Get-ScheduledTask { throw 'Forbidden task object enumeration' }
|
||||
function Get-Volume { throw 'Forbidden volume label enumeration' }
|
||||
function Get-SmbShare { [CmdletBinding()] param() [pscustomobject]@{ Name='SECRET'; Path='C:\SECRET'; Special=$false } }
|
||||
function Get-SmbOpenFile { throw 'Forbidden open-file enumeration' }
|
||||
function Get-ChildItem { throw 'Forbidden certificate enumeration' }
|
||||
function Assert-True { param($Value, [string]$Message) if (-not $Value) { throw $Message } }
|
||||
$collectors = [ordered]@{
|
||||
system={param($r) Get-SamsaraSystem $r}; roles={param($r) Get-SamsaraRoles $r}
|
||||
@@ -52,12 +40,13 @@ $collectors = [ordered]@{
|
||||
}
|
||||
foreach ($name in $collectors.Keys) {
|
||||
$section = Invoke-SamsaraSection $name $collectors[$name]
|
||||
Assert-True ($section.status -eq 'complete') ('Synthetic section failed: ' + $name)
|
||||
$expected = if ($name -eq 'certificates') { 'not_collected' } elseif ($name -in @('tasks','storage','shares')) { 'partial' } else { 'complete' }
|
||||
Assert-True ($section.status -eq $expected) ('Synthetic section failed: ' + $name)
|
||||
Assert-True ($section.started_at -and $section.completed_at) 'Missing timestamps'
|
||||
Assert-True (($section | ConvertTo-Json -Depth 12) -notmatch 'SECRET') 'Secret field serialized'
|
||||
if ($name -eq 'network') { Assert-True ($section.data[0].process -eq 'SyntheticWorker') 'Process correlation failed' }
|
||||
if ($name -eq 'shares') { Assert-True ($section.data[0].observed_open_files -eq 1) 'Share activity correlation missing' }
|
||||
if ($name -eq 'tasks') { Assert-True ($section.data[0].executables -eq 'report.exe') 'Executable basename missing' }
|
||||
if ($name -eq 'network') { Assert-True ($section.data[0].process -eq 'processes-123') 'Process correlation failed' }
|
||||
if ($name -eq 'shares') { Assert-True ($null -eq $section.data[0].observed_open_files) 'Unexpected open-file evidence' }
|
||||
if ($name -eq 'tasks') { Assert-True ($null -eq $section.data[0].executables) 'Task action leaked' }
|
||||
}
|
||||
foreach ($build in @('14393','17763','20348','26100')) {
|
||||
$script:SyntheticBuild=$build
|
||||
@@ -66,13 +55,13 @@ foreach ($build in @('14393','17763','20348','26100')) {
|
||||
}
|
||||
# Test registry projection with in-memory objects; never open a real registry hive.
|
||||
$fakeEntry = New-Object PSObject
|
||||
$fakeEntry | Add-Member ScriptMethod GetValue { param($key) switch ($key) { 'DisplayName' {'Synthetic application'}; 'DisplayVersion' {'1.0'}; 'Publisher' {'Example'}; 'InstallDate' {'20260101'}; default {throw 'Unexpected registry value requested'} } }
|
||||
$fakeEntry | Add-Member ScriptMethod GetValue { param($key,$default,$options) switch ($key) { 'DisplayName' {'Synthetic application'}; 'DisplayVersion' {'1.0'}; 'Publisher' {'Example'}; 'InstallDate' {'20260101'}; default {throw 'Unexpected registry value requested'} } }
|
||||
$fakeEntry | Add-Member ScriptMethod Dispose { }
|
||||
$fakeUninstall = New-Object PSObject
|
||||
$fakeUninstall | Add-Member ScriptMethod GetSubKeyNames { @('Example') }
|
||||
$fakeUninstall | Add-Member ScriptMethod OpenSubKey { param($name,$writable) if ($writable) {throw 'Writable registry access'}; return $fakeEntry }
|
||||
$section=Invoke-SamsaraSection 'software' {param($r) Invoke-SamsaraRead $r 'Synthetic registry view' { Read-SamsaraSoftwareEntries $r $fakeUninstall 'Registry64' } }
|
||||
Assert-True ($section.status -eq 'complete' -and $section.data[0].name -eq 'Synthetic application') 'Registry metadata projection failed'
|
||||
Assert-True ($section.status -eq 'complete' -and $section.data[0].name -eq 'products-1') 'Registry metadata projection failed'
|
||||
function Get-SmbShare { [CmdletBinding()] param() throw [UnauthorizedAccessException]::new('SECRET protected details') }
|
||||
$section=Invoke-SamsaraSection 'shares' {param($r) Get-SamsaraShares $r}
|
||||
Assert-True ($section.status -eq 'partial' -and $section.data.Count -eq 0) 'Denied share enumeration was not partial/empty'
|
||||
@@ -82,63 +71,3 @@ $script:SamsaraIsElevated=$false
|
||||
$section=Invoke-SamsaraSection 'services' {param($r) Get-SamsaraServices $r}
|
||||
Assert-True ($section.status -eq 'partial' -and $section.permission_limitations.Count -gt 0) 'Non-elevated scope not marked partial'
|
||||
Write-Output 'PowerShell AST and synthetic collector tests passed. No real Windows inventory was collected.'
|
||||
# Reliability cases: mocks never access real tasks, certificates or private keys.
|
||||
$script:SamsaraIsElevated=$true
|
||||
$script:SamsaraDiagnostic=$true
|
||||
function New-TestTask {
|
||||
param([bool]$Enabled=$true)
|
||||
[pscustomobject]@{TaskName='Example';TaskPath='\';State='Ready';Settings=[pscustomobject]@{Enabled=$Enabled};Principal=[pscustomobject]@{UserId='SYSTEM'};Actions=@([pscustomobject]@{Execute='"C:\Tools\bad.exe"';Arguments='SECRET'});Triggers=@()}
|
||||
}
|
||||
function Get-ScheduledTask { [CmdletBinding()]param() New-TestTask $true; New-TestTask $false }
|
||||
$r=Invoke-SamsaraSection tasks {param($r) Get-SamsaraTasks $r}
|
||||
Assert-True ($r.status -eq 'complete' -and $r.data.Count -eq 2 -and -not $r.data[1].enabled) 'Normal/disabled tasks lost'
|
||||
Assert-True ($r.data[0].executables -eq '[executable name omitted]') 'Malformed executable not sanitized'
|
||||
function Get-ScheduledTask { [CmdletBinding()]param() }
|
||||
$r=Invoke-SamsaraSection tasks {param($r) Get-SamsaraTasks $r}
|
||||
Assert-True ($r.status -eq 'complete' -and $r.data.Count -eq 0) 'Successful empty task query failed'
|
||||
function Get-ScheduledTask { [CmdletBinding()]param() Write-Error 'SECRET' -Category PermissionDenied }
|
||||
$r=Invoke-SamsaraSection tasks {param($r) Get-SamsaraTasks $r}
|
||||
Assert-True ($r.status -eq 'failed' -and $r.data.Count -eq 0) 'Failed task query treated as empty success'
|
||||
function Get-ScheduledTask {
|
||||
[CmdletBinding()]param()
|
||||
New-TestTask
|
||||
$bad=New-TestTask
|
||||
$bad | Add-Member ScriptProperty TaskName { throw [UnauthorizedAccessException]::new('SECRET') } -Force
|
||||
# Throw from ToString to reliably exercise projection failures on both runtimes.
|
||||
$value=New-Object PSObject
|
||||
$value | Add-Member ScriptMethod ToString { throw [UnauthorizedAccessException]::new('SECRET') } -Force
|
||||
$bad.TaskPath=$value
|
||||
$bad
|
||||
}
|
||||
$r=Invoke-SamsaraSection tasks {param($r) Get-SamsaraTasks $r}
|
||||
Assert-True ($r.status -eq 'partial' -and $r.data.Count -eq 1 -and $r.errors.Count -gt 0) 'Individual task failure discarded healthy records'
|
||||
Assert-True (($r | ConvertTo-Json -Depth 12) -notmatch 'SECRET') 'Diagnostic leaked secret'
|
||||
$script:StoreMode='normal'
|
||||
function Get-ChildItem {
|
||||
[CmdletBinding()]param($LiteralPath)
|
||||
if ($LiteralPath -eq 'Cert:\LocalMachine') {
|
||||
foreach ($n in @('My','Root','Empty')) { [pscustomobject]@{PSChildName=$n;PSPath=('Microsoft.PowerShell.Security\Certificate::'+$n)} }
|
||||
} elseif ($LiteralPath -eq 'Cert:\LocalMachine\Empty') { return }
|
||||
elseif ($LiteralPath -in @('Cert:\LocalMachine\My','Cert:\LocalMachine\Root')) {
|
||||
if ($script:StoreMode -eq 'denied' -or ($script:StoreMode -eq 'partial' -and $LiteralPath -like '*Root')) { throw [UnauthorizedAccessException]::new('SECRET') }
|
||||
[pscustomobject]@{Subject='CN=Example';Issuer='CN=CA';Thumbprint='00';NotAfter=[datetime]::UtcNow;PrivateKey='SECRET'}
|
||||
} else { throw 'Incorrect certificate provider path' }
|
||||
}
|
||||
$r=Invoke-SamsaraSection certificates {param($r) Get-SamsaraCertificates $r}
|
||||
Assert-True ($r.status -eq 'complete' -and $r.data.Count -eq 2 -and @($r.data.store | Select-Object -Unique).Count -eq 2) 'Multiple stores/empty store failed'
|
||||
$script:StoreMode='partial'
|
||||
$r=Invoke-SamsaraSection certificates {param($r) Get-SamsaraCertificates $r}
|
||||
Assert-True ($r.status -eq 'partial' -and $r.data.Count -eq 1 -and $r.errors[0].code -eq 'access_denied') 'Partial store failure lost records'
|
||||
Assert-True (($r | ConvertTo-Json -Depth 12) -notmatch 'SECRET') 'Certificate diagnostics leaked secret'
|
||||
function Get-ChildItem { [CmdletBinding()]param($LiteralPath) if ($LiteralPath -eq 'Cert:\LocalMachine') { [pscustomobject]@{PSChildName='My'} } else { throw [UnauthorizedAccessException]::new('SECRET') } }
|
||||
$r=Invoke-SamsaraSection certificates {param($r) Get-SamsaraCertificates $r}
|
||||
Assert-True ($r.status -eq 'failed' -and $r.data.Count -eq 0) 'Inaccessible stores treated as empty success'
|
||||
function Get-ChildItem { [CmdletBinding()]param($LiteralPath) }
|
||||
$r=Invoke-SamsaraSection certificates {param($r) Get-SamsaraCertificates $r}
|
||||
Assert-True ($r.status -eq 'complete' -and $r.data.Count -eq 0) 'Empty store list failed'
|
||||
function Get-ScheduledTask { [CmdletBinding()]param() New-TestTask }
|
||||
function ConvertTo-Json { [CmdletBinding()]param($InputObject,$Depth,[switch]$Compress) throw [InvalidOperationException]::new('SECRET serialization') }
|
||||
$r=Invoke-SamsaraSection tasks {param($r) Get-SamsaraTasks $r}
|
||||
Assert-True ($r.status -ne 'complete' -and $r.data.Count -eq 0 -and $r.errors.Count -eq 1) 'Serialization failure accepted as evidence'
|
||||
Remove-Item Function:\ConvertTo-Json
|
||||
Write-Output 'Reliability collector tests passed.'
|
||||
|
||||
Reference in New Issue
Block a user