Retain infrastructure names and share/mount roots behind policy switches

Add a `retain` block to privacy_policy.json (all on by default) so assessments
show software, services, processes, tasks, shares/mounts and private peer IPs.
Values pass an identifier-pattern filter (digit runs, SSN/phone, e-mail, charset,
length, path depth). Accounts, command lines, child paths, filenames and public
peer IPs stay omitted. Linux collector and Windows PowerShell collectors updated;
tests cover retention, withholding and all-switches-off.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 19:03:49 -08:00
parent 610a3bb447
commit b4c0cf12ca
16 changed files with 189 additions and 29 deletions

View File

@@ -10,6 +10,9 @@ foreach ($file in $files) {
}
foreach ($file in @($files | Where-Object { $_.Name -ne 'Get-ServerInventory.ps1' })) { . $file.FullName }
$script:SamsaraIsElevated = $true
# Synthetic SECRET canaries must never serialize: run these contract tests with every `retain` switch off.
Get-SamsaraPrivacyConfiguration
foreach ($property in @($script:SamsaraPrivacy.retain.PSObject.Properties)) { $property.Value = $false }
$script:SyntheticBuild = '20348'
function Get-CimInstance {
[CmdletBinding()] param($ClassName, $Property, $Filter, $Namespace)

View File

@@ -23,7 +23,7 @@ class ImportTests(unittest.TestCase):
data = self.load(collect_demo(LINUX_DEMO_HOST))
self.assertEqual(data['server']['platform'], 'linux')
result = assess(data)
self.assertTrue(any('Share-' in item['evidence'] for item in result['findings']))
self.assertTrue(any('Configured non-administrative share' in item['evidence'] for item in result['findings']))
self.assertTrue(any('certificates:' in item['evidence'] for item in result['findings']))
def test_report_wrapper_recomputed_and_sensitive_fields_removed(self):

View File

@@ -104,7 +104,10 @@ class RegressionReviewTests(unittest.TestCase):
data = json.loads(Path('samsara/assets/demo_inventory.json').read_text())
data['server']['os_build'] = '١٢٣٤٥'
data['inventory']['software'] = [{'name': 'Microsoft Visual C++ 2015-2022 Redistributable (x64)', 'version': '14.38.33135'}]
clean = minimize_inventory(data)
from samsara.core.privacy import load_privacy_policy
strict = load_privacy_policy()
strict['retain'] = {key: False for key in strict['retain']}
clean = minimize_inventory(data, strict)
self.assertNotEqual(clean['server']['os_build'], '١٢٣٤٥')
self.assertEqual(clean['inventory']['software'][0]['name'], 'Microsoft Visual C++')

View File

@@ -19,6 +19,12 @@ from samsara.analyzers.assessment import assess
CANARY='SYNTHETIC_PERSON_CANARY_9f4'
def strict_policy():
policy=load_privacy_policy()
policy['retain']={key:False for key in policy['retain']}
return policy
def fixture(name='active_sql_server'):
return json.loads(Path('tests/fixtures/scoring',name+'.json').read_text())
@@ -36,14 +42,14 @@ class PrivacyTests(unittest.TestCase):
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)
clean=minimize_inventory(raw,strict_policy())
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):
policy=strict_policy();policy['report_directory']=folder
with patch('samsara.core.storage._verify_windows_acl'), patch('samsara.core.storage.load_privacy_policy',return_value=policy), patch('samsara.core.privacy.load_privacy_policy',return_value=policy):
assessment=assess(original)
assessment['findings'][0]['evidence']=CANARY
export_html(Path(folder)/'test.html',raw,assessment)
@@ -52,7 +58,7 @@ class PrivacyTests(unittest.TestCase):
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']
policy=strict_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')
@@ -128,3 +134,41 @@ class CaptureTests(unittest.TestCase):
self.assertNotIn(CANARY,str(raised.exception))
with self.assertRaises(CollectionCancelled):
capture([sys.executable,'-c','print(1)'],env=env,timeout=3,cancelled=lambda:True)
class RetainedNameTests(unittest.TestCase):
def test_useful_names_and_roots_are_kept_but_identifier_like_values_withheld(self):
raw=fixture('active_file_server')
raw['server']['hostname']='fileserver01'
raw['inventory']['software']=[{'name':'Epic Hyperspace Client 2024','version':'1.2.3'},{'name':'Scanner 123456789','version':'1'}]
raw['inventory']['services'][-1]['name']='CustomImagingService'
raw['inventory']['storage']=[
{'type':'SMB Share','name':'RadiologyArchive','path':'D:\\Shares\\Radiology'},
{'type':'SMB Share','name':'Dept','path':'D:\\Shares\\a\\b\\c\\d.pdf'},
{'type':'Remote Mount','name':'nas01:/export/imaging','path':'/mnt/imaging'},
{'type':'SMB Share','name':'jane.doe@hospital.org','path':'D:\\Shares\\123-45-6789'}]
clean=minimize_inventory(raw)
text=json.dumps(clean)
self.assertEqual(clean['server']['hostname'],'fileserver01')
names=[r['name'] for r in clean['inventory']['software']]
self.assertIn('Epic Hyperspace Client 2024',names); self.assertNotIn('Scanner 123456789',names)
self.assertIn('CustomImagingService',[r['name'] for r in clean['inventory']['services']])
storage=clean['inventory']['storage']
self.assertEqual(storage[0]['name'],'RadiologyArchive'); self.assertEqual(storage[0]['path'],'D:\\Shares\\Radiology')
self.assertEqual(storage[2]['name'],'nas01:/export/imaging'); self.assertEqual(storage[2]['path'],'/mnt/imaging')
for leaked in ('d.pdf','jane.doe','123-45-6789','123456789'): self.assertNotIn(leaked,text)
self.assertEqual(minimize_inventory(clean),clean)
validate_inventory(clean)
def test_every_category_can_be_switched_off(self):
raw=fixture('active_file_server'); raw['server']['hostname']='fileserver01'
raw['inventory']['software']=[{'name':'Epic Hyperspace Client','version':'1'}]
raw['inventory']['storage']=[{'type':'SMB Share','name':'RadiologyArchive','path':'D:\\Shares\\Radiology'}]
text=json.dumps(minimize_inventory(raw,strict_policy()))
for value in ('fileserver01','Epic','RadiologyArchive','Radiology'): self.assertNotIn(value,text)
def test_policy_retain_block_is_validated(self):
with tempfile.TemporaryDirectory() as folder:
bad=load_privacy_policy(); bad['retain']['hostname']='yes'
path=Path(folder)/'policy.json'; path.write_text(json.dumps(bad))
with self.assertRaises(ValueError): load_privacy_policy(path)

View File

@@ -198,7 +198,7 @@ class ScoringRegressionTests(unittest.TestCase):
export_json(output,data,assess(data))
clean=load_inventory(output)
self.assertEqual(len(clean['inventory']['services']),86)
self.assertNotEqual(clean['server']['hostname'], data['server']['hostname'])
self.assertEqual(clean['server']['hostname'], data['server']['hostname']) # infrastructure hostname retained by default policy
def test_custom_jobs_not_misclassified_by_substrings_and_zero_points_maintenance(self):
data=fixture('clean_windows')