Files
Samsara/tests/test_privacy.py
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

131 lines
7.9 KiB
Python

"""Synthetic disclosure regressions. No real inventory or patient data is used."""
import copy
import json
import os
from pathlib import Path
import stat
import sys
import tempfile
import unittest
from unittest.mock import patch
from samsara.core.privacy import minimize_inventory, load_privacy_policy, canonical_share_root
from samsara.core.storage import write_private, require_local_path
from samsara.core.process import capture, ProcessFailure, CollectionCancelled
from samsara.models.inventory import validate_inventory
from samsara.reporting.export import export_html, export_json
from samsara.analyzers.assessment import assess
CANARY='SYNTHETIC_PERSON_CANARY_9f4'
def fixture(name='active_sql_server'):
return json.loads(Path('tests/fixtures/scoring',name+'.json').read_text())
class PrivacyTests(unittest.TestCase):
def test_all_free_text_and_stale_findings_are_removed(self):
raw=fixture();original=copy.deepcopy(raw)
raw['server'].update(hostname=CANARY,domain=CANARY,network_adapters=[CANARY])
raw['inventory']['services'][-1].update(name='MSSQL$'+CANARY,account=CANARY,display_name=CANARY)
raw['inventory']['tasks']=[{'name':CANARY,'path':'\\'+CANARY+'\\','enabled':True,'account':CANARY,'triggers':CANARY,'arguments':CANARY}]
raw['inventory']['software']=[{'name':'Microsoft SQL Server '+CANARY,'publisher':CANARY,'version':'16.0.1 '+CANARY}]
raw['inventory']['storage']=[{'type':'SMB Share','name':CANARY,'path':'C:\\'+CANARY}]
raw['inventory']['certificates']=[{'subject':CANARY,'issuer':CANARY,'thumbprint':CANARY,'expires':'2027-01-01T00:00:00Z'}]
raw['checks']['services'].update(message=CANARY,errors=[{'code':CANARY,'scope':CANARY,'message':CANARY}])
raw['checks'][CANARY]={'status':'failed','message':CANARY}
raw['privacy']={'profile':'minimized-metadata-v1'} # A marker cannot bypass projection.
snapshot=copy.deepcopy(raw)
clean=minimize_inventory(raw)
self.assertEqual(raw,snapshot)
self.assertNotIn(CANARY,json.dumps(clean))
validate_inventory(clean)
self.assertEqual(assess(clean)['score'],70)
with tempfile.TemporaryDirectory() as folder:
policy=load_privacy_policy();policy['report_directory']=folder
with patch('samsara.core.storage._verify_windows_acl'), patch('samsara.core.storage.load_privacy_policy',return_value=policy):
assessment=assess(original)
assessment['findings'][0]['evidence']=CANARY
export_html(Path(folder)/'test.html',raw,assessment)
export_json(Path(folder)/'test.json',raw,assessment)
for path in Path(folder).iterdir(): self.assertNotIn(CANARY,path.read_text())
self.assertEqual(len(list(Path(folder).iterdir())),2)
def test_exact_roots_only_no_descendant_or_network_approval(self):
policy=load_privacy_policy();policy['approved_share_roots']=['D:\\Approved']
raw=fixture();raw['inventory']['storage']=[{'type':'SMB Share','name':'private','path':p} for p in ['D:\\Approved','D:\\Approved\\child','D:\\Other']]
clean=minimize_inventory(raw,policy)
self.assertEqual(clean['inventory']['storage'][0]['path'],'D:\\Approved')
self.assertNotIn('child',json.dumps(clean))
for bad in ['\\\\server\\share','//server/share','D:\\Approved\\..\\Other']:
with self.assertRaises(ValueError): canonical_share_root(bad)
def test_workload_scores_survive_minimization(self):
for name,score in [('clean_windows',0),('inactive_game_server',0),('active_sql_server',70),('active_iis_server',70),('domain_controller',95),('active_file_server',80),('incomplete_collection',0)]:
with self.subTest(name=name):
clean=minimize_inventory(fixture(name))
self.assertEqual(assess(clean)['score'],score)
self.assertTrue(assess(clean)['review_required'])
self.assertEqual(minimize_inventory(clean),clean)
def test_dates_closed_schema_and_oversized_input_fail(self):
raw=fixture();raw['collected_at']='not a date'
with self.assertRaises(ValueError):validate_inventory(raw)
raw=fixture();raw['inventory']['services'][0]['unexpected']={'sensitive':CANARY}
with self.assertRaises(ValueError):validate_inventory(raw)
raw=fixture();raw['inventory']['tasks']=[{}]*20001
with self.assertRaises(ValueError):minimize_inventory(raw)
def test_default_collectors_do_not_request_high_risk_scopes(self):
for name,forbidden in [('Get-ServerStorage.ps1','Get-SmbOpenFile'),('Get-ServerCertificates.ps1','Get-ChildItem'),('Get-ServerTasks.ps1','Get-ScheduledTask'),('Get-ServerServices.ps1','StartName')]:
self.assertNotIn(forbidden,Path('powershell',name).read_text())
self.assertNotIn('TemporaryFile',Path('samsara/collectors/windows.py').read_text())
self.assertNotIn('TemporaryFile',Path('samsara/collectors/linux.py').read_text())
class StorageTests(unittest.TestCase):
@unittest.skipIf(os.name=='nt','POSIX permission test; native Windows ACL acceptance requires a provisioned lab root.')
def test_private_root_exclusive_files_and_destination_boundary(self):
with tempfile.TemporaryDirectory() as base:
root=Path(base)/'reports';policy=load_privacy_policy();policy['report_directory']=str(root)
with patch('samsara.core.storage.load_privacy_policy',return_value=policy):
output=root/'report.json';write_private(output,'{}')
if os.name!='nt':
self.assertEqual(stat.S_IMODE(root.stat().st_mode),0o700)
self.assertEqual(stat.S_IMODE(output.stat().st_mode),0o600)
with self.assertRaises(FileExistsError):write_private(output,'overwrite')
with self.assertRaises(ValueError):write_private(Path(base)/'outside.json','{}')
self.assertEqual(output.read_text(),'{}')
if os.name!='nt':
root.chmod(0o755)
with self.assertRaises(ValueError):write_private(root/'unsafe.json','{}')
def test_network_uri_symlink_paths_rejected(self):
for value in ['\\\\server\\share\\data.json','//server/share/data.json','https://host/path']:
with self.assertRaises(ValueError):require_local_path(value)
if os.name!='nt':
with tempfile.TemporaryDirectory() as base:
link=Path(base)/'link';link.symlink_to(Path(base),target_is_directory=True)
with self.assertRaises(ValueError):require_local_path(link/'data.json')
@unittest.skipIf(os.name=='nt','POSIX FIFO regression')
def test_import_rejects_fifo_without_waiting_for_a_writer(self):
from samsara.collectors.importer import load_inventory
with tempfile.TemporaryDirectory() as base:
pipe=Path(base)/'inventory.json'
os.mkfifo(pipe)
with self.assertRaises(ValueError): load_inventory(pipe)
class CaptureTests(unittest.TestCase):
def test_bounded_memory_capture_timeout_cancel_and_sanitized_errors(self):
env={'PATH':os.defpath, **{key:os.environ[key] for key in ('SystemRoot','WINDIR') if key in os.environ}}
self.assertEqual(capture([sys.executable,'-c','print("synthetic")'],env=env,timeout=3),b'synthetic\n')
for code,script,options in [('output_limit','print("x"*200000)',{'limit':100}),('timeout','import time; time.sleep(10)',{'timeout':.05}),('collection_failed','raise RuntimeError("'+CANARY+'")',{})]:
with self.subTest(code=code), self.assertRaises(ProcessFailure) as raised:
capture([sys.executable,'-c',script],env=env,**dict({'timeout':3},**options))
self.assertEqual(raised.exception.code,code)
self.assertNotIn(CANARY,str(raised.exception))
with self.assertRaises(CollectionCancelled):
capture([sys.executable,'-c','print(1)'],env=env,timeout=3,cancelled=lambda:True)