Replace inventory-counting risk with bounded workload scoring and separate confidence
This commit is contained in:
@@ -12,6 +12,7 @@ from samsara.collectors.windows import collect_local_windows
|
||||
from samsara.core.detection import detect_local_os, check_profile, PROFILES
|
||||
from samsara.analyzers.assessment import assess
|
||||
from samsara.analyzers.summary import summarize
|
||||
from samsara.analyzers.policy import load_policy, policy_id
|
||||
from samsara.reporting.export import export_html, export_json
|
||||
|
||||
|
||||
@@ -101,6 +102,8 @@ class MainWindow(QMainWindow):
|
||||
self.assessment = None
|
||||
self.history = []
|
||||
self.records = []
|
||||
self.scoring_policy = load_policy()
|
||||
self.active_policy = self.scoring_policy
|
||||
self.setStyleSheet('''
|
||||
QWidget { background:#131b27; color:#e5ecf5; font-size:14px; }
|
||||
QLabel#title { font-size:27px; font-weight:600; }
|
||||
@@ -254,7 +257,7 @@ class MainWindow(QMainWindow):
|
||||
self.retirement_score = self.label('Retirement risk estimate: —')
|
||||
self.retirement_score.setObjectName('summary')
|
||||
score_layout.addWidget(self.retirement_score)
|
||||
score_layout.addWidget(self.label('Higher scores mean more retirement concerns. Heuristic only; administrator review is required.', muted=True))
|
||||
score_layout.addWidget(self.label('Higher scores mean more workload concerns. Historical usage validation and administrator approval are always required.', muted=True))
|
||||
self.coverage_note = self.label('', muted=True)
|
||||
score_layout.addWidget(self.coverage_note)
|
||||
summary_layout.addWidget(score_card)
|
||||
@@ -312,8 +315,32 @@ 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('Retirement scoring policy'))
|
||||
self.policy_label = self.label('Default policy · ' + policy_id(self.scoring_policy)[:12], muted=True)
|
||||
settings.addWidget(self.policy_label)
|
||||
self.policy_button = QPushButton('Load scoring policy JSON…')
|
||||
self.policy_button.clicked.connect(self.choose_policy)
|
||||
settings.addWidget(self.policy_button)
|
||||
self.reset_policy_button = QPushButton('Restore default scoring policy')
|
||||
self.reset_policy_button.clicked.connect(self.reset_policy)
|
||||
settings.addWidget(self.reset_policy_button)
|
||||
settings.addWidget(self.label('Changes apply to new assessments. History keeps the original score and policy. Scores use the strongest workload, never an inventory count.', muted=True))
|
||||
settings.addStretch()
|
||||
|
||||
def choose_policy(self):
|
||||
path, _ = QFileDialog.getOpenFileName(self, 'Load scoring policy', '', 'Scoring policy (*.json)')
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
self.scoring_policy = load_policy(path)
|
||||
self.policy_label.setText(self.scoring_policy['name'] + ' · ' + policy_id(self.scoring_policy)[:12])
|
||||
except ValueError as exc:
|
||||
QMessageBox.warning(self, 'Invalid scoring policy', str(exc))
|
||||
|
||||
def reset_policy(self):
|
||||
self.scoring_policy = load_policy()
|
||||
self.policy_label.setText('Default policy · ' + policy_id(self.scoring_policy)[:12])
|
||||
|
||||
def source_changed(self):
|
||||
source = self.source.currentData()
|
||||
self.file_row.setVisible(source == 'file')
|
||||
@@ -338,6 +365,7 @@ class MainWindow(QMainWindow):
|
||||
if source == 'file' and not self.inventory_path.text().strip():
|
||||
self.status.setText('Choose an inventory file before analyzing.')
|
||||
return
|
||||
self.active_policy = load_policy(self.scoring_policy)
|
||||
self.set_busy(True)
|
||||
self.status.setText('Starting assessment…')
|
||||
self.job = CollectionJob(source, self.target.currentData(), self.inventory_path.text().strip(), self)
|
||||
@@ -348,6 +376,8 @@ class MainWindow(QMainWindow):
|
||||
self.job.start()
|
||||
|
||||
def set_busy(self, busy):
|
||||
self.policy_button.setEnabled(not busy)
|
||||
self.reset_policy_button.setEnabled(not busy)
|
||||
self.analyze_button.setEnabled(not busy)
|
||||
self.source.setEnabled(not busy)
|
||||
self.target.setEnabled(not busy and self.source.currentData() == 'local')
|
||||
@@ -369,7 +399,7 @@ class MainWindow(QMainWindow):
|
||||
return data['server'].get('collection_source', 'Imported inventory — source not independently verified')
|
||||
|
||||
def collection_done(self, data):
|
||||
assessment = assess(data)
|
||||
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))
|
||||
@@ -377,7 +407,7 @@ class MainWindow(QMainWindow):
|
||||
self.history_empty.hide()
|
||||
self.display_assessment(data, assessment)
|
||||
self.nav.setCurrentRow(0)
|
||||
incomplete = sum(check['status'] != 'complete' for check in data['checks'].values())
|
||||
incomplete = len(assessment['coverage']['incomplete_checks'])
|
||||
self.status.setText(f'Assessment report ready. {incomplete} checks need review. Open the Summary and detail tabs to review the report.')
|
||||
|
||||
def display_assessment(self, data, assessment):
|
||||
@@ -386,7 +416,7 @@ class MainWindow(QMainWindow):
|
||||
self.result_title.setText(data['server']['hostname'])
|
||||
self.banner.setText(self.source_label(data) + ' • ' + data['collected_at'])
|
||||
self.retirement_score.setText(f"Retirement risk estimate: {summary['risk_score']}/100 · {summary['risk_level'].title()}")
|
||||
self.coverage_note.setText(f"{summary['incomplete']} checks incomplete or missing. This estimate does not establish retirement readiness.")
|
||||
self.coverage_note.setText(f"Evidence coverage: {summary['coverage_percent']}% · Confidence: {summary['confidence'].title()} · {summary['incomplete']} checks need review. Coverage gaps do not add risk points.")
|
||||
self.os_type.setText('OS type: ' + summary['os_type'])
|
||||
self.os_version.setText('Version: ' + summary['version'])
|
||||
software = summary['software']
|
||||
@@ -398,7 +428,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 enabled schedules observed in collected evidence. Check Tasks and Collection checks for missing scheduler coverage.')
|
||||
'No nonroutine enabled schedules identified. Full schedules and missing scheduler coverage are in Tasks and Collection checks.')
|
||||
self.quick_save.setEnabled(True)
|
||||
self.result_tabs.setCurrentIndex(0)
|
||||
# Discard tables from the previous assessment. Populate detailed tabs on demand.
|
||||
@@ -432,9 +462,17 @@ class MainWindow(QMainWindow):
|
||||
elif key == 'checks':
|
||||
rows = [{'check': name, **value} for name, value in self.data['checks'].items()]
|
||||
elif key in ('risk', 'dependencies'):
|
||||
rows = self.assessment['findings']
|
||||
findings = self.assessment['findings']
|
||||
if key == 'dependencies':
|
||||
findings = [row for row in findings if row['classification'] in ('Confirmed workload dependency','Potential workload dependency')]
|
||||
rows = [{name: row[name] for name in ('category','classification','severity','points','determines_final_score','evidence','scoring_reason')} for row in findings]
|
||||
else:
|
||||
rows = self.data['inventory'][key]
|
||||
if key == 'risk':
|
||||
area.addWidget(self.label(self.assessment['scoring']['formula'] + '\n' + self.assessment['scoring']['explanation'] + '\nPolicy: ' + self.assessment['scoring']['policy']['name'] + ' · ' + self.assessment['scoring']['policy_id'][:12], muted=True))
|
||||
if key == 'network':
|
||||
observations = self.assessment['network_observations']
|
||||
rows = [{**row, 'assessed_direction': observations[index]['direction'], 'direction_basis': observations[index]['direction_basis'], 'activity_classification': observations[index]['classification']} for index,row in enumerate(rows)]
|
||||
if key in self.data['checks']:
|
||||
check = self.data['checks'][key]
|
||||
area.addWidget(self.label(f"{check['status'].replace('_', ' ').title()}: {check['message']}", muted=True))
|
||||
@@ -455,7 +493,7 @@ class MainWindow(QMainWindow):
|
||||
path += '.' + kind
|
||||
try:
|
||||
(export_html if kind == 'html' else export_json)(path, self.data, self.assessment)
|
||||
QMessageBox.information(self, 'Report saved', 'Assessment saved locally.')
|
||||
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.')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user