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>
130 lines
6.9 KiB
Python
130 lines
6.9 KiB
Python
import json
|
||
from pathlib import Path
|
||
import tempfile
|
||
import unittest
|
||
from unittest.mock import patch
|
||
|
||
from samsara.collectors.linux import (collect_local_linux, parse_connections,
|
||
parse_units, run_command, CollectionCancelled)
|
||
from samsara.collectors.demo import collect_demo, LINUX_DEMO_HOST
|
||
from samsara.analyzers.assessment import assess
|
||
from report_helpers import export_html
|
||
|
||
|
||
class LinuxTests(unittest.TestCase):
|
||
def test_service_states_and_startup(self):
|
||
rows = parse_units('nginx.service loaded active running Web server\nexample.service loaded inactive dead Example\n',
|
||
'nginx.service enabled enabled\nexample.service disabled disabled\n')
|
||
self.assertEqual(rows[0]['state'], 'Running')
|
||
self.assertEqual(rows[0]['startup'], 'enabled')
|
||
self.assertEqual(rows[1]['state'], 'inactive/dead')
|
||
|
||
def test_ipv4_ipv6_and_unprivileged_connections(self):
|
||
rows = parse_connections('LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\nESTAB 0 0 [::1]:443 [::1]:51234 users:(("nginx",pid=123,fd=7))\n')
|
||
self.assertEqual(rows[0]['remote_port'], 0)
|
||
self.assertIsNone(rows[0]['pid'])
|
||
self.assertEqual(rows[1]['local_address'], '::1')
|
||
self.assertEqual(rows[1]['state'], 'Established')
|
||
self.assertEqual(rows[1]['process'], 'nginx')
|
||
self.assertEqual(rows[1]['direction'], 'Unknown')
|
||
|
||
@patch('samsara.collectors.linux.platform.system', return_value='Windows')
|
||
def test_wrong_execution_platform(self, system):
|
||
with self.assertRaises(ValueError):
|
||
collect_local_linux()
|
||
|
||
@patch('samsara.collectors.linux.platform.system', return_value='Linux')
|
||
def test_partial_failures_never_use_demo(self, system):
|
||
def unavailable(args, **kwargs):
|
||
raise RuntimeError('SECRET diagnostics must not be reported')
|
||
data = collect_local_linux(runner=unavailable)
|
||
self.assertFalse(data['synthetic'])
|
||
self.assertEqual(data['checks']['services']['status'], 'failed')
|
||
self.assertEqual(data['checks']['network']['status'], 'failed')
|
||
self.assertEqual(data['checks']['software']['status'], 'failed')
|
||
self.assertEqual(data['inventory']['services'], [])
|
||
self.assertNotIn('SECRET', json.dumps(data))
|
||
self.assertEqual(data['checks']['certificates']['status'], 'not_collected')
|
||
|
||
@patch('samsara.collectors.linux.platform.system', return_value='Linux')
|
||
def test_cancel(self, system):
|
||
with self.assertRaises(CollectionCancelled):
|
||
collect_local_linux(cancelled=lambda: True)
|
||
|
||
def test_mint_and_rhel_package_parsing(self):
|
||
for rpm in [False, True]:
|
||
def command(args, **kwargs):
|
||
if args[0] == 'ip':
|
||
return '[]'
|
||
if args[0] == 'rpm':
|
||
return 'nginx\t1.2-1\tExample vendor\n'
|
||
if args[0] == 'dpkg-query':
|
||
return 'installed\tnginx:amd64\t1.2\nconfig-files\tremoved\t1.0\n'
|
||
return ''
|
||
with self.subTest(rpm=rpm), patch('samsara.collectors.linux.platform.system', return_value='Linux'), patch('samsara.collectors.linux.Path.is_file', return_value=rpm):
|
||
data = collect_local_linux(runner=command)
|
||
self.assertEqual(len(data['inventory']['software']), 1)
|
||
self.assertIn('nginx', data['inventory']['software'][0]['name'])
|
||
self.assertEqual(data['checks']['software']['status'], 'partial')
|
||
|
||
def test_local_report_source_and_unclassified_services(self):
|
||
data = collect_demo(LINUX_DEMO_HOST)
|
||
data['synthetic'] = False
|
||
data['server']['collection_source'] = 'Local Linux read-only collection'
|
||
data['inventory']['services'] = [{'name': 'desktop-example.service', 'state': 'Running'}]
|
||
result = assess(data)
|
||
self.assertFalse(any(f['category'] == 'Active service' for f in result['findings']))
|
||
self.assertTrue(any(f['category'] == 'Unclassified services' for f in result['findings']))
|
||
with tempfile.TemporaryDirectory() as folder:
|
||
path = Path(folder) / 'report.html'
|
||
export_html(path, data, result)
|
||
self.assertIn('LOCAL LINUX INVENTORY', path.read_text())
|
||
self.assertNotIn('IMPORTED INVENTORY', path.read_text())
|
||
|
||
@unittest.skipUnless(__import__('sys').platform.startswith('linux'), 'Linux command test')
|
||
def test_command_timeout_and_cancellation(self):
|
||
with self.assertRaisesRegex(RuntimeError, 'timeout'):
|
||
run_command(['sleep', '1'], timeout=.05)
|
||
with self.assertRaises(CollectionCancelled):
|
||
run_command(['sleep', '1'], cancelled=lambda: True)
|
||
|
||
|
||
class RegressionReviewTests(unittest.TestCase):
|
||
def test_ss_states_map_to_canonical_names(self):
|
||
from samsara.collectors.linux import parse_connections
|
||
rows = parse_connections('TIME-WAIT 0 0 10.0.0.5:44321 10.0.0.9:443\nCLOSE-WAIT 0 0 10.0.0.5:1 10.0.0.9:2\nFIN-WAIT-1 0 0 10.0.0.5:3 10.0.0.9:4\n')
|
||
self.assertEqual([r['state'] for r in rows], ['TimeWait', 'CloseWait', 'FinWait1'])
|
||
|
||
def test_non_ascii_digits_and_cpp_product_label(self):
|
||
import json
|
||
from pathlib import Path
|
||
from samsara.core.privacy import minimize_inventory, numeric_version, safe_time
|
||
self.assertEqual(numeric_version('١٢.٣٤'), 'Not collected')
|
||
self.assertIsNone(safe_time('٢٠٢٦-٠١-٠١T٠٠:٠٠:٠٠Z'))
|
||
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'}]
|
||
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++')
|
||
|
||
def test_remote_peer_addresses_are_masked_but_scoring_survives(self):
|
||
import copy
|
||
from samsara.core.privacy import minimize_inventory
|
||
from samsara.models.inventory import validate_inventory
|
||
data = json.loads(Path('tests/fixtures/scoring/active_sql_server.json').read_text())
|
||
before = assess(minimize_inventory(copy.deepcopy(data)))['score']
|
||
for row in data['inventory']['network']:
|
||
if row.get('remote_address') not in ('', '0.0.0.0', '::'):
|
||
row['remote_address'] = '93.184.216.34'
|
||
clean = minimize_inventory(data)
|
||
validate_inventory(clean)
|
||
self.assertNotIn('93.184.216.34', json.dumps(clean))
|
||
self.assertEqual(minimize_inventory(clean), clean)
|
||
self.assertEqual(assess(clean)['score'], before)
|
||
self.assertNotIn('93.184.216.34', json.dumps(assess(clean)))
|
||
self.assertIn('public', {r['remote_scope'] for r in clean['inventory']['network']})
|