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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user