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,3 +1,6 @@
|
||||
from samsara.core.privacy import minimize_inventory, load_privacy_policy
|
||||
from samsara.core.storage import report_directory
|
||||
from uuid import uuid4
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QSortFilterProxyModel
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QLabel,
|
||||
@@ -30,7 +33,7 @@ class CollectionJob(QThread):
|
||||
if self.source == 'local':
|
||||
self.stage.emit('Detecting operating system…')
|
||||
detected = detect_local_os()
|
||||
self.stage.emit('Detected ' + detected['name'] + '. Collecting inventory…')
|
||||
self.stage.emit('Operating system detected. Collecting minimized inventory…')
|
||||
if detected['platform'] == 'windows':
|
||||
data = collect_local_windows(profile=self.profile, stage=self.stage.emit, cancelled=self.isInterruptionRequested)
|
||||
else:
|
||||
@@ -45,6 +48,7 @@ class CollectionJob(QThread):
|
||||
if self.isInterruptionRequested():
|
||||
raise CollectionCancelled()
|
||||
self.stage.emit('Preparing assessment report…')
|
||||
data = minimize_inventory(data, trusted_source=data.get('server',{}).get('collection_source'))
|
||||
self.result.emit(data)
|
||||
except CollectionCancelled:
|
||||
self.failed.emit('Cancelled. Your previous assessment has been kept.')
|
||||
@@ -298,6 +302,9 @@ class MainWindow(QMainWindow):
|
||||
self.history_list.itemActivated.connect(self.open_history)
|
||||
self.history_list.itemClicked.connect(self.open_history)
|
||||
history.addWidget(self.history_list)
|
||||
self.clear_history_button = QPushButton('Clear session assessments')
|
||||
self.clear_history_button.clicked.connect(self.clear_history)
|
||||
history.addWidget(self.clear_history_button)
|
||||
self.history_empty = self.label('No assessments yet.', muted=True)
|
||||
history.addWidget(self.history_empty)
|
||||
reports = self.page('Reports', 'Save the current assessment, including evidence and missing checks.')
|
||||
@@ -315,6 +322,7 @@ class MainWindow(QMainWindow):
|
||||
settings.addWidget(self.label('Windows Server 2016 / 2019 / 2022 / 2025\nRed Hat Enterprise Linux 7 (legacy) / 8 / 9 / 10 and compatible distributions'))
|
||||
settings.addWidget(self.label('Local Windows Server and Linux collectors are available. Windows validation on a supported host is pending. Remote collection is a separate follow-up. Saved inventories and samples work on either desktop.', muted=True))
|
||||
settings.addWidget(self.label('Read-only collection • No credentials stored • Session history only', muted=True))
|
||||
settings.addWidget(self.label('Privacy defaults: identifying names and unapproved paths omitted. Task commands, certificate identities and SMB open-file details are not collected. Exports use a configured private local folder. Review docs/PRIVACY_HARDENING.md before real assessments.', muted=True))
|
||||
settings.addWidget(self.label('Retirement scoring policy'))
|
||||
self.policy_label = self.label('Default policy · ' + policy_id(self.scoring_policy)[:12], muted=True)
|
||||
settings.addWidget(self.policy_label)
|
||||
@@ -376,6 +384,7 @@ class MainWindow(QMainWindow):
|
||||
self.job.start()
|
||||
|
||||
def set_busy(self, busy):
|
||||
self.clear_history_button.setEnabled(not busy)
|
||||
self.policy_button.setEnabled(not busy)
|
||||
self.reset_policy_button.setEnabled(not busy)
|
||||
self.analyze_button.setEnabled(not busy)
|
||||
@@ -399,11 +408,20 @@ class MainWindow(QMainWindow):
|
||||
return data['server'].get('collection_source', 'Imported inventory — source not independently verified')
|
||||
|
||||
def collection_done(self, data):
|
||||
try:
|
||||
data = minimize_inventory(data, trusted_source=data.get('server',{}).get('collection_source'))
|
||||
except ValueError:
|
||||
self.status.setText('Privacy validation failed; assessment withheld.')
|
||||
return
|
||||
assessment = assess(data, policy=self.active_policy)
|
||||
self.history.append({'hostname': data['server']['hostname'], 'risk': assessment['level'],
|
||||
'source': self.source_label(data), 'collected_at': data['collected_at']})
|
||||
self.records.append((data, assessment))
|
||||
self.history_list.addItem(f"{data['server']['hostname']} • {assessment['level'].title()} risk • {self.source_label(data)}")
|
||||
while len(self.records)>load_privacy_policy()['max_session_assessments']:
|
||||
self.records.pop(0)
|
||||
self.history.pop(0)
|
||||
self.history_list.takeItem(0)
|
||||
self.history_list.addItem(f"{data['collected_at']} • {data['server']['hostname']} • {assessment['level'].title()} risk")
|
||||
self.history_empty.hide()
|
||||
self.display_assessment(data, assessment)
|
||||
self.nav.setCurrentRow(0)
|
||||
@@ -428,7 +446,7 @@ class MainWindow(QMainWindow):
|
||||
self.scheduled_jobs.setText('\n'.join(f"• {row['name']} — {row['schedule']}" for row in jobs[:5]) +
|
||||
(f"\n+ {len(jobs) - 5} more; see Tasks." if len(jobs) > 5 else '') +
|
||||
'\nSchedules indicate configured work; execution is not confirmed.' if jobs else
|
||||
'No nonroutine enabled schedules identified. Full schedules and missing scheduler coverage are in Tasks and Collection checks.')
|
||||
'No nonroutine enabled schedules identified. Schedule detail is omitted; review Tasks and Collection checks before concluding no work exists.')
|
||||
self.quick_save.setEnabled(True)
|
||||
self.result_tabs.setCurrentIndex(0)
|
||||
# Discard tables from the previous assessment. Populate detailed tabs on demand.
|
||||
@@ -443,6 +461,26 @@ class MainWindow(QMainWindow):
|
||||
for button in self.report_buttons:
|
||||
button.setEnabled(True)
|
||||
|
||||
def clear_history(self):
|
||||
self.history.clear()
|
||||
self.records.clear()
|
||||
self.history_list.clear()
|
||||
self.history_empty.show()
|
||||
self.data = self.assessment = None
|
||||
self.result_title.setText('No assessment loaded')
|
||||
self.banner.clear()
|
||||
for label in (self.retirement_score,self.coverage_note,self.os_type,self.os_version,self.important_software,self.scheduled_jobs):
|
||||
label.clear()
|
||||
for area in self.detail_areas.values():
|
||||
while area.count():
|
||||
item=area.takeAt(0)
|
||||
if item.widget(): item.widget().deleteLater()
|
||||
area.addWidget(self.label('No assessment loaded.',muted=True))
|
||||
self.report_label.setText('Analyze a computer or open an inventory first.')
|
||||
for button in self.report_buttons: button.setEnabled(False)
|
||||
self.quick_save.setEnabled(False)
|
||||
self.status.setText('Session assessment references cleared. This is not secure memory erasure; exported files remain.')
|
||||
|
||||
def open_history(self, item):
|
||||
index = self.history_list.row(item)
|
||||
if 0 <= index < len(self.records):
|
||||
@@ -488,16 +526,21 @@ class MainWindow(QMainWindow):
|
||||
def save_report(self, kind):
|
||||
if self.data is None:
|
||||
return
|
||||
path, _ = QFileDialog.getSaveFileName(self, 'Save assessment', f'samsara-assessment.{kind}', f'{kind.upper()} (*.{kind})')
|
||||
try:
|
||||
suggested = str(report_directory() / f'samsara-{uuid4().hex[:12]}.{kind}')
|
||||
except (OSError, ValueError):
|
||||
QMessageBox.warning(self, 'Save blocked', 'Provision the private report directory described in docs/PRIVACY_HARDENING.md.')
|
||||
return
|
||||
path, _ = QFileDialog.getSaveFileName(self, 'Save minimized assessment', suggested, f'{kind.upper()} (*.{kind})')
|
||||
if not path:
|
||||
return
|
||||
if not path.lower().endswith('.' + kind):
|
||||
path += '.' + kind
|
||||
try:
|
||||
(export_html if kind == 'html' else export_json)(path, self.data, self.assessment)
|
||||
QMessageBox.information(self, 'Report saved', 'HTML report and separate full inventory JSON saved locally.' if kind == 'html' else 'Assessment and full inventory saved as JSON.')
|
||||
except OSError:
|
||||
QMessageBox.warning(self, 'Save failed', 'Unable to write the report. Check the destination and file permissions.')
|
||||
QMessageBox.information(self, 'Report saved', 'Minimized assessment saved. No raw inventory sidecar was created.')
|
||||
except (OSError, ValueError):
|
||||
QMessageBox.warning(self, 'Save failed', 'Use a new filename directly in the approved private report directory. Check its permissions.')
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.job is not None and self.job.isRunning():
|
||||
|
||||
Reference in New Issue
Block a user