# Shared helpers, compatible with Windows PowerShell 5.1. No raw errors are serialized. function Get-SamsaraTimestamp { [DateTime]::UtcNow.ToString('o') } function Add-SamsaraError { param($Result, $Record, [string]$Scope) $code = 'collection_failed' if ($Record.CategoryInfo.Category -in @('PermissionDenied', 'SecurityError') -or $Record.Exception -is [UnauthorizedAccessException] -or $Record.Exception.HResult -eq -2147024891) { $code = 'access_denied' } elseif ($Record.Exception -is [System.Management.Automation.CommandNotFoundException]) { $code = 'command_unavailable' } $message = switch ($code) { 'access_denied' { 'Current account cannot read this scope.' } '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.' } } 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 Invoke-SamsaraSection { param([string]$Name, [scriptblock]$Collector) $result = [ordered]@{ section = $Name; status = 'complete'; message = 'Local read-only query completed within the documented scope.' started_at = Get-SamsaraTimestamp; completed_at = $null errors = @(); permission_limitations = @(); limitations = @() attempted_subchecks = 0; successful_subchecks = 0; data = @() } try { & $Collector $result } catch { Add-SamsaraError $result $_ $Name } if ($result.errors.Count -gt 0) { $result.status = if ($result.successful_subchecks -gt 0) { 'partial' } else { 'failed' } $result.message = 'One or more local queries failed; see sanitized errors and limitations.' } if (-not $script:SamsaraIsElevated -and $Name -in @('roles','services','network','tasks','shares','storage','certificates')) { $result.permission_limitations += 'Not elevated: protected objects may be hidden by the current account permissions.' if ($result.status -eq 'complete') { $result.status = 'partial' } } $result.completed_at = Get-SamsaraTimestamp [pscustomobject]$result } function ConvertTo-SamsaraCheck { param($Section) [ordered]@{ status = $Section.status; message = $Section.message started_at = $Section.started_at; completed_at = $Section.completed_at errors = @($Section.errors); permission_limitations = @($Section.permission_limitations) limitations = @($Section.limitations) } } function Get-SamsaraPrivacyConfiguration { if ($null -eq $script:SamsaraPrivacy) { $assets = Join-Path (Split-Path $PSScriptRoot -Parent) 'samsara\assets' $script:SamsaraPrivacy = Get-Content -LiteralPath (Join-Path $assets 'privacy_policy.json') -Raw -ErrorAction Stop | ConvertFrom-Json $script:SamsaraCatalogue = Get-Content -LiteralPath (Join-Path $assets 'privacy_catalogue.json') -Raw -ErrorAction Stop | ConvertFrom-Json if ($script:SamsaraPrivacy.version -ne '1.0') { throw 'Invalid privacy policy.' } } } function Test-SamsaraRetain { # Mirrors the `retain` block of privacy_policy.json; Python re-validates every value. param([string]$Flag, [string]$Value, [switch]$Path) Get-SamsaraPrivacyConfiguration if ($script:SamsaraPrivacy.retain.$Flag -ne $true -or [string]::IsNullOrEmpty($Value)) { return $false } if ($Value -match '[0-9]{6,}' -or $Value -match '(^|[\\/])\.\.([\\/]|$)') { return $false } if ($Path) { return ($Value.Length -le 260 -and $Value -match "^([A-Za-z]:)?[\\/A-Za-z0-9 ._()+#`$,&'-]*$") } return ($Value.Length -le 120 -and $Value -match "^[A-Za-z0-9][A-Za-z0-9 ._()+#`$:,&'@-]*$") } function Get-SamsaraPublicName { param([string]$Kind, [string]$Value, [int]$Index) Get-SamsaraPrivacyConfiguration $normalized = $Value.ToLowerInvariant() -replace '\.(exe|service)$', '' $flag = switch ($Kind) { 'products' {'software_names'} 'processes' {'process_names'} default {'service_names'} } if (Test-SamsaraRetain $flag $Value) { return $Value } if ($Kind -eq 'services' -and $script:SamsaraPrivacy.approved_service_names -ccontains $Value) { return $Value } if ($Kind -eq 'products') { if ($script:SamsaraPrivacy.approved_software_names -ccontains $Value) { return $Value } foreach ($product in $script:SamsaraCatalogue.products) { if ($Value -match ('^' + [regex]::Escape($product) + '(?![A-Za-z0-9])')) { return $product } } } elseif ($script:SamsaraCatalogue.$Kind -contains $normalized) { return $normalized } if ($Kind -eq 'services') { foreach ($family in $script:SamsaraCatalogue.families.PSObject.Properties) { foreach ($prefix in $family.Value.prefixes) { if ($normalized.StartsWith($prefix, [StringComparison]::Ordinal)) { return $family.Value.representative } } } } return $Kind + '-' + $Index } function Get-SamsaraApprovedRoot { param([string]$Value) Get-SamsaraPrivacyConfiguration if (Test-SamsaraRetain 'storage_names_and_roots' $Value -Path) { return $Value } if ($Value -notmatch '^[A-Za-z]:\\' -or $Value -match '(^|[\\/])\.\.([\\/]|$)' -or $Value -match '[\x00-\x1f]') { return 'Omitted by privacy policy' } foreach ($root in $script:SamsaraPrivacy.approved_share_roots) { if ($root -match '^[A-Za-z]:\\' -and $Value.TrimEnd([char]'\').Equals(([string]$root).TrimEnd([char]'\'), [StringComparison]::OrdinalIgnoreCase)) { return $Value } } return 'Omitted by privacy policy' }