Replace inventory-counting risk with bounded workload scoring and separate confidence

This commit is contained in:
2026-10-30 12:34:39 -07:00
parent f65ade9fff
commit 57e716b710
26 changed files with 10351 additions and 87 deletions

View File

@@ -1,33 +1,86 @@
"""Actionable assessment report, with full inventory preserved as a separate JSON artifact."""
import hashlib
import html
import json
from pathlib import Path
from urllib.parse import quote
from samsara.analyzers.summary import summarize
MANUAL_STEPS = ["Confirm application and business owners and obtain retirement approval.",
"Review historical monitoring, DNS, load balancers, backups and firewall logs.",
"Validate service accounts, scheduled workloads and SMB/NFS/Samba consumers with owners.",
"Resolve failed or missing checks and verify rollback and retention requirements."]
MANUAL_STEPS = ['Confirm application and business owners and obtain administrator retirement approval.',
'Validate historical usage across a representative business cycle, including infrequent and offline jobs.',
'Review DNS, load balancers, monitoring, backups, firewall logs and service-account consumers.',
'Verify custom tasks and share consumers; resolve incomplete checks and confirm rollback/retention requirements.']
def export_json(path, data, assessment):
Path(path).write_text(json.dumps({"inventory_result": data, "assessment": assessment}, indent=2), encoding="utf-8")
Path(path).write_text(json.dumps({'inventory_result':data,'assessment':assessment},indent=2),encoding='utf-8')
def inventory_artifact(path, data):
payload = (json.dumps(data,indent=2,ensure_ascii=True)+'\n').encode('utf-8')
digest = hashlib.sha256(payload).hexdigest()
destination = Path(path).with_name(Path(path).stem + '.inventory-' + digest[:12] + '.json')
try:
with destination.open('xb') as output:
output.write(payload)
except FileExistsError:
if destination.read_bytes() != payload:
raise FileExistsError('Existing inventory artifact differs; choose a new report filename.') from None
return destination, digest
def table(headers, rows):
escape = lambda value: html.escape(str(value))
if not rows:
return '<p class="muted">None identified in collected evidence. Review missing evidence before drawing conclusions.</p>'
return '<div class="table"><table><thead><tr>' + ''.join('<th>'+escape(h)+'</th>' for h in headers) + '</tr></thead><tbody>' + ''.join('<tr>'+''.join('<td>'+escape(v)+'</td>' for v in row)+'</tr>' for row in rows) + '</tbody></table></div>'
def export_html(path, data, assessment):
escape = lambda value: html.escape(str(value))
content = "<h1>Samsara assessment</h1><p>SYNTHETIC DEMO — no real collection performed.</p>" if data["synthetic"] else "<h1>Samsara assessment</h1><p>IMPORTED INVENTORY — source assertions not independently verified.</p>"
if not data['synthetic'] and data['server'].get('collection_source') == 'Local Linux read-only collection':
content = '<h1>Samsara assessment</h1><p>LOCAL LINUX INVENTORY — read-only snapshot of the execution host.</p>'
if not data['synthetic'] and data['server'].get('collection_source') == 'Local Windows read-only collection':
content = '<h1>Samsara assessment</h1><p>LOCAL WINDOWS INVENTORY — read-only snapshot of the execution host.</p>'
content += "<p>Heuristic requiring administrator review. This report does not authorize retirement.</p>"
summary = summarize(data, assessment)
content += f"<h2>Summary</h2><p>Retirement risk estimate: {escape(summary['risk_level'])} ({summary['risk_score']}/100). Higher scores mean more retirement concerns.</p>"
content += f"<p>OS type: {escape(summary['os_type'])}<br>Version: {escape(summary['version'])}<br>Incomplete or missing checks: {summary['incomplete']}</p>"
content += '<h3>Important software</h3><p>Workload name hints and application services; use unverified.</p><ul>'
content += ''.join(f"<li>{escape(row['name'])} — {escape(row['version'])}</li>" for row in summary['software']) or '<li>No workload name matches observed; review full inventory and missing checks.</li>'
content += '</ul><h3>Scheduled jobs and scripts</h3><p>Configured schedules do not confirm execution.</p><ul>'
content += ''.join(f"<li>{escape(row['name'])} — {escape(row['schedule'])}</li>" for row in summary['jobs']) or '<li>No enabled schedules observed; scheduler coverage may be incomplete.</li>'
content += '</ul>'
for title, value in [("Server overview", data["server"]), ("Collection timestamp", data["collected_at"]), ("Inventory", data["inventory"]), ("Dependencies and risk findings", assessment["findings"]), ("Collection checks / missing evidence", data["checks"])]:
content += f"<h2>{title}</h2><pre>{escape(json.dumps(value, indent=2))}</pre>"
content += "<h2>Recommended manual verification</h2><ul>" + "".join(f"<li>{escape(s)}</li>" for s in MANUAL_STEPS) + "</ul>"
Path(path).write_text('<!doctype html><html lang="en"><meta charset="utf-8"><title>Samsara</title><style>body{font:16px system-ui;max-width:1100px;margin:40px auto;padding:20px}pre{white-space:pre-wrap;background:#eee;padding:16px}</style>' + content + '</html>', encoding="utf-8")
artifact, digest = inventory_artifact(path,data)
source = 'SYNTHETIC DEMO — no real collection performed.' if data['synthetic'] else 'IMPORTED INVENTORY — source assertions not independently verified.'
for platform in ('Linux','Windows'):
if not data['synthetic'] and data['server'].get('collection_source') == f'Local {platform} read-only collection':
source = f'LOCAL {platform.upper()} INVENTORY — read-only snapshot of the execution host.'
summary = summarize(data,assessment)
scoring = assessment['scoring']
content = '<h1>Samsara assessment</h1><p class="source">'+escape(source)+'</p>'
content += '<h2>Summary</h2>'
content += '<p><strong>'+escape(data['server']['hostname'])+'</strong> · '+escape(data['collected_at'])+'</p>'
content += f"<div class='card'><strong>Retirement risk estimate: {escape(assessment['level']).title()} · {assessment['score']}/100</strong><p>{escape(scoring['explanation'])}</p></div>"
content += f"<p>Evidence coverage: <strong>{summary['coverage_percent']}%</strong> · Evidence confidence: <strong>{escape(summary['confidence'])}</strong> · {summary['incomplete']} checks need review.</p>"
content += '<p>Coverage and uncertainty are separate from risk. A zero or low score is not retirement clearance. Historical usage validation and administrator approval are always required.</p>'
content += f"<p>OS type: {escape(summary['os_type'])}<br>Version: {escape(summary['version'])}</p>"
actionable = [row for row in assessment['findings'] if row['points']>0]
content += '<h2>Actionable workload findings</h2>'
content += table(['Workload','Classification','Workload score','Evidence','Why this affects scoring'],[(row['category'],row['classification'],row['points'],row['evidence'],row['scoring_reason']) for row in actionable])
content += '<h2>How the score was calculated</h2><p>'+escape(scoring['formula'])+'</p>'
content += table(['Workload','Selected rule','Score','Determines final score','Evidence observations'],[(row['title'],row['selected_rule'],row['score'],'Yes (co-leading)' if row['determines_final_score'] else 'No; below the maximum',row['evidence_count']) for row in scoring['breakdown']])
content += '<details><summary>Policy weights, thresholds and audit identity</summary><p>Policy SHA-256: '+escape(scoring['policy_id'])+'</p><pre>'+escape(json.dumps(scoring['policy'],indent=2))+'</pre></details>'
content += '<h2>Important software</h2><p>Inventory name hints only; installed software does not establish active use.</p>'
content += table(['Software','Version','Evidence'],[(r['name'],r['version'],r['evidence']) for r in summary['software'][:12]])
content += '<h2>Scheduled jobs and scripts</h2><p>Nonroutine enabled schedules; configuration does not confirm execution. All tasks remain in the inventory.</p>'
content += table(['Job','Schedule','State'],[(r['name'],r['schedule'],r['status']) for r in summary['jobs'][:12]])
context = [r for r in assessment['findings'] if r['points']==0 and r['id'] not in ('coverage','observation-limits')]
content += '<h2>Unscored context and review items</h2><p>Routine services, administrative shares, client traffic and unknown observations do not receive workload points.</p>'
content += table(['Category','Classification','Context'],[(r['category'],r['classification'],r['evidence']) for r in context])
directions = {}
for row in assessment['network_observations']:
directions[row['direction']] = directions.get(row['direction'],0)+1
content += '<h2>Network interpretation</h2><p>Direction inferred from listener/PID or ephemeral-port patterns is a heuristic, not a captured initiating handshake. Confirmed workload findings describe attributed observed transport activity, not verified business ownership.</p>'
content += table(['Direction / activity','Observations'],list(sorted(directions.items())))
content += '<h2>Collection checks / missing evidence</h2><p>Failed and partial checks reduce confidence; they never add workload risk points.</p>'
checks = []
for name in sorted(set(data['checks']) | set(assessment['scoring']['policy']['coverage_weights'])):
row = data['checks'].get(name,{'status':'not_collected','message':'No status supplied.'})
errors = '; '.join(f"{e['code']}: {e['message']}" for e in row.get('errors',[]))
checks.append((name,row['status'],row.get('started_at','Not supplied'),row.get('completed_at','Not supplied'),row['message'],errors,'; '.join(row.get('permission_limitations',[])),'; '.join(row.get('limitations',[]))))
content += table(['Check','Status','started_at (UTC)','completed_at (UTC)','Result','Errors','permission_limitations','Scope limitations'],checks)
content += '<h2>Recommended manual verification</h2><ul>'+''.join('<li>'+escape(step)+'</li>' for step in MANUAL_STEPS)+'</ul>'
content += '<h2>Complete raw inventory</h2><p>Every collected inventory row is preserved separately, including routine services and administrative shares. The analyzer has not changed the collected evidence.</p>'
content += '<p><a href="'+escape(quote(artifact.name))+'">Download / open complete inventory JSON</a></p><p>Inventory SHA-256: '+digest+'</p>'
content += table(['Inventory section','Rows'],[(name,len(rows)) for name,rows in data['inventory'].items()])
content += '<p>JSON assessment export also preserves all raw inventory, classifications, evidence references and policy. Keep the inventory file alongside this HTML when sharing or archiving.</p>'
css = 'body{font:15px system-ui;max-width:1180px;margin:32px auto;padding:20px;color:#233044;line-height:1.55}h1,h2{color:#173c60}.muted{color:#667}.source,.card,details{background:#eef3f8;padding:16px;border-radius:7px}.table{overflow-x:auto}table{border-collapse:collapse;width:100%;margin:12px 0}th,td{border:1px solid #d9e0e8;text-align:left;vertical-align:top;padding:9px;white-space:pre-wrap;overflow-wrap:anywhere}th{background:#eef3f8}pre{white-space:pre-wrap;overflow-wrap:anywhere}a{color:#1263a3}'
Path(path).write_text('<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Samsara</title><style>'+css+'</style></head><body>'+content+'</body></html>',encoding='utf-8')