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:
2026-11-04 18:50:24 -08:00
parent 7fed3b9ab9
commit 610a3bb447
51 changed files with 2719 additions and 604 deletions

View File

@@ -1,4 +1,5 @@
import copy
import io
import json
from pathlib import Path
import tempfile
@@ -11,7 +12,7 @@ from samsara.collectors.linux import CollectionCancelled
from samsara.models.inventory import validate_inventory
from samsara.collectors.importer import load_inventory
from samsara.analyzers.assessment import assess
from samsara.reporting.export import export_html, export_json
from report_helpers import export_html, export_json
def fixture(year='2022'):
@@ -49,9 +50,9 @@ class WindowsTests(unittest.TestCase):
self.assertFalse(result['synthetic'])
self.assertEqual(result['server']['collection_source'],'Local Windows read-only collection')
self.assertEqual(len(result['inventory']['storage']),2)
self.assertEqual(set(result['checks']),set(SECTIONS))
self.assertEqual(set(result['checks']),set(SECTIONS)|{'privacy'})
self.assertTrue(all(isinstance(rows,list) for rows in result['inventory'].values()))
self.assertTrue(all('started_at' in c and 'completed_at' in c for c in result['checks'].values()))
self.assertTrue(all('started_at' in c and 'completed_at' in c for k,c in result['checks'].items() if k!='privacy'))
def test_profiles_and_workstation_cannot_masquerade_as_server(self):
for mutation in ['client','build','mismatch']:
@@ -144,6 +145,7 @@ class WindowsTests(unittest.TestCase):
class FakeProcess:
def __init__(self,args,**kwargs):
self.stdout=io.BytesIO()
self.returncode=None
self.args=args
self.kwargs=kwargs
@@ -151,29 +153,26 @@ class FakeProcess:
self.waited=False
def poll(self): return self.returncode
def kill(self): self.killed=True; self.returncode=-1
def wait(self): self.waited=True; return self.returncode
def wait(self,timeout=None): self.waited=True; return self.returncode
class ProcessTests(unittest.TestCase):
def test_serialization_exit_and_diagnostic_switch(self):
def create(args, **kwargs):
from samsara.core.process import ProcessFailure
def fake_capture(args, **kwargs):
self.assertIn('-Diagnostic', args)
process = FakeProcess(args, **kwargs)
process.returncode = 3
kwargs['stdout'].write(b'SECRET')
return process
with patch('samsara.collectors.windows.subprocess.Popen', side_effect=create):
raise ProcessFailure('collection_failed', 3)
with patch('samsara.collectors.windows.capture', side_effect=fake_capture):
with self.assertRaises(WindowsCollectionError) as error:
run_section('tasks', 'powershell.exe', diagnostic=True)
self.assertEqual(error.exception.code, 'serialization_failed')
self.assertNotIn('SECRET', str(error.exception))
def test_timeout_and_cancel_kill_and_reap_without_shell(self):
for cancel in [False,True]:
processes=[]
def create(args,**kwargs):
process=FakeProcess(args,**kwargs);processes.append(process);return process
calls=iter([False,True])
calls=iter([False,False,True])
with self.subTest(cancel=cancel), patch('samsara.collectors.windows.subprocess.Popen',side_effect=create):
with self.assertRaises(CollectionCancelled if cancel else WindowsCollectionError):
run_section('services',Path('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe'), cancelled=(lambda:next(calls)) if cancel else (lambda:False),timeout=0)
@@ -189,7 +188,8 @@ class ProcessTests(unittest.TestCase):
def test_invalid_stdout_and_start_failure_are_sanitized(self):
def create(args,**kwargs):
process=FakeProcess(args,**kwargs);process.returncode=0
kwargs['stdout'].write(b'not JSON SECRET')
process.stdout.write(b'not JSON SECRET')
process.stdout.seek(0)
return process
with patch('samsara.collectors.windows.subprocess.Popen',side_effect=create):
with self.assertRaises(WindowsCollectionError) as error:
@@ -208,8 +208,9 @@ class ProcessTests(unittest.TestCase):
process.returncode=0
record=record_for(fixture(),'services')
record['data']=[]
kwargs['stdout'].write(b'\xef\xbb\xbf'+json.dumps(record).encode('utf-8'))
process.stdout.write(b'\xef\xbb\xbf'+json.dumps(record).encode('utf-8'))
processes.append(process)
process.stdout.seek(0)
return process
with patch('samsara.collectors.windows.subprocess.Popen',side_effect=create):
record=run_section('services','powershell.exe')
@@ -218,18 +219,20 @@ class ProcessTests(unittest.TestCase):
self.assertTrue(processes[0].waited)
def failed(args,**kwargs):
process=FakeProcess(args,**kwargs);process.returncode=1
kwargs['stdout'].write(b'SECRET')
process.stdout.write(b'SECRET')
process.stdout.seek(0)
return process
with patch('samsara.collectors.windows.subprocess.Popen',side_effect=failed):
with self.assertRaises(WindowsCollectionError) as error:
run_section('services','powershell.exe')
self.assertEqual(error.exception.code,'powershell_failed')
self.assertEqual(error.exception.code,'collection_failed')
self.assertNotIn('SECRET',str(error.exception))
def test_completed_process_output_limit(self):
def create(args,**kwargs):
process=FakeProcess(args,**kwargs);process.returncode=0
kwargs['stdout'].write(b'x'*20)
process.stdout.write(b'x'*20)
process.stdout.seek(0)
return process
with patch('samsara.collectors.windows.subprocess.Popen',side_effect=create), patch('samsara.collectors.windows.MAX_OUTPUT',10):
with self.assertRaises(WindowsCollectionError) as error: