Files
Samsara/docs/WINDOWS-COLLECTOR-RELIABILITY.md
oceans2alaska 610a3bb447 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>
2026-11-04 18:50:24 -08:00

129 lines
6.8 KiB
Markdown

> **Superseded in part:** the privacy hardening (see `PRIVACY_HARDENING.md`) disables certificate
> enumeration and narrows task collection to name/path/state. The task and certificate
> collector behavior described below no longer applies; the diagnostic switch, exit-code 3
> serialization handling and PowerShell 7 module path remain in effect.
# Windows collector reliability validation — 2026-10-30
## Root causes
The supplied Server 2025 assessment reported tasks failed and 16 certificate
errors, but its generic errors did not retain exception types. Read-only native
reproduction established two defects:
* Tasks: one action executable made .NET Framework `Path.GetFileName` throw a
`MethodInvocationException` wrapping `ArgumentException`. The projection was
inside a single assignment/query catch, so that one value discarded all tasks.
* Certificates: the Windows certificate provider returned store `PSPath` values
such as `Microsoft.PowerShell.Security\Certificate::My`, without LocalMachine.
Using those paths produced `ItemNotFoundException` / `ObjectNotFound` for each
store. Store-list success alone incorrectly gave partial credit when every
store query failed.
The original code already enumerated stores separately; recursion was not the
cause. Missing modules and JSON conversion were checked as failure paths, but
were not the reproduced causes on this host.
## Changes
Executable basenames now use string operations and the existing strict allowlist,
avoiding filesystem path validation. Malformed values become an omission marker;
task records remain visible. Each task is projected and JSON-checked separately.
Principal group accounts are used when UserId is absent. Arguments, XML, inline
scripts, event queries and credentials are never projected.
Certificates use explicit `Cert:\LocalMachine\<store>` paths. Each store query
and certificate projection is isolated; empty successful stores receive credit,
inaccessible stores produce errors, and listing names alone does not give credit
when all listed stores fail. Only subject, issuer, thumbprint, expiration and
store are collected. No private key or certificate export APIs are used.
Read helpers promote nonterminating errors to terminating errors. Per-record JSON
checks reject unrepresentable records. Final JSON failures exit with code 3 and
Python records `serialization_failed`, never a successful empty collection.
Existing subprocess timeout, UTF-8, output limits and schema validation remain.
Optional `-Diagnostic` / Python `--diagnostic` adds exception class and category to
existing sanitized error messages. Fixed command stages appear in error scope.
No raw messages, task names, arguments, store names, targets, stack traces or
error identifiers are included in diagnostics. Inventory remains sensitive and
should be stored privately. Task/certificate UI detail pages display these error
scopes, codes and messages; Collection checks and reports retain their contract.
The production Python backend deliberately continues to use trusted Windows
PowerShell 5.1. Direct PowerShell 7 invocation also includes the inbox Windows
module directory, without opening module lookup to user directories. Commands
autoload from those inbox module paths; missing commands are explicit failures.
No scoring policy, Linux collector or other inventory module was changed.
## Results
On the local Windows Server 2025 host, native 5.1 collection returned:
| Collector | Status | Records | Errors |
| --- | --- | ---: | ---: |
| Tasks | complete | 192 (46 disabled) | 0 |
| Certificates | complete | 49 across 7 populated stores | 0 |
Full Python subprocess collection passed schema validation, returned the same
record counts, and completed all nine checks. Evidence coverage rose from the
supplied assessment's 81% to 100%. Confidence remained moderate under the existing
classification policy; complete collection does not establish retirement safety.
`python -m unittest discover -s tests -v`: **64 tests, 62 passed, 2 skipped**.
Skips: the Linux-only subprocess test on Windows and PowerShell 7 (not installed).
UI navigation/history/export tests, Linux parsing tests, scoring regressions,
Windows subprocess limits and schema tests passed. Native PowerShell AST and
synthetic tests passed under 5.1. Added cases cover normal/disabled/empty tasks,
malformed actions, per-task failures, nonterminating query denial, multiple and
empty stores, partial/all-store denial, redacted diagnostics and serialization
failures. A dedicated regression checks lower coverage/confidence without risk
inflation for failed tasks and partial certificates.
## Exact validation commands
Run from the repository root in an authorized elevated Windows PowerShell console.
These commands only read inventory; they do not change Server configuration or
execution policy. Choose a new output path for each saved inventory.
```powershell
$PSVersionTable.PSVersion
powershell.exe -NoLogo -NoProfile -NonInteractive -File .\tests\powershell\Test-Collectors.ps1
python -m unittest discover -s tests -v
foreach ($section in @('tasks', 'certificates')) {
$raw = & powershell.exe -NoLogo -NoProfile -NonInteractive -File .\powershell\Get-ServerInventory.ps1 -Section $section -Diagnostic
if ($LASTEXITCODE -ne 0) { throw 'Collector process failed' }
$r = $raw | ConvertFrom-Json
[pscustomobject]@{
Section = $section
Status = $r.status
Records = @($r.data).Count
Errors = $r.errors
} | ConvertTo-Json -Depth 5
if ($r.status -ne 'complete') { throw 'Review incomplete collection' }
if (@($r.data).Count -eq 0) { throw 'No records: verify actual host contents independently' }
}
python -m samsara.collectors.windows --diagnostic --output .\windows-validation-new.json
python .\tools\validate_windows_host.py --output-dir .\windows-validation-new
# When PowerShell 7 is already installed:
pwsh.exe -NoLogo -NoProfile -NonInteractive -File .\tests\powershell\Test-Collectors.ps1
pwsh.exe -NoLogo -NoProfile -NonInteractive -File .\powershell\Get-ServerInventory.ps1 -Section tasks -Diagnostic
pwsh.exe -NoLogo -NoProfile -NonInteractive -File .\powershell\Get-ServerInventory.ps1 -Section certificates -Diagnostic
```
The nonempty assertion above is a validation expectation for this host, not a
collector rule: legitimately empty successful queries remain complete.
## Remaining limitations
PowerShell 7 runtime execution and other Server releases were not available for
live validation. Tests run under each runtime when installed; 5.1 syntax and
Server 2016/2019/2022/2025 identity fixtures pass. Access restrictions may hide
objects even when an API reports success. A terminating failure of the initial
task enumeration cannot recover task registrations the API never returned, and
is reported failed. Inventory is a current observation, not historical activity.
No Windows Server configuration was modified.