Files
Samsara/samsara/analyzers/assessment.py

79 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bounded workload risk, distinct from inventory coverage and uncertainty."""
from collections import Counter
from samsara.analyzers.dependencies import classify_dependencies, CONFIRMED, POTENTIAL, STANDARD, UNKNOWN
from samsara.analyzers.policy import load_policy, policy_id, risk_level
REVIEW = 'Historical usage validation, application-owner verification and administrator approval are required. No active connections does not prove a server is unused.'
def assess(data, policy=None):
policy = load_policy(policy)
groups, aggregates, network, tasks = classify_dependencies(data)
findings = []
breakdown = []
for group_id, group in sorted(groups.items()):
rule = max(sorted(group['rules']), key=lambda name: policy['scores'][name])
score = policy['scores'][rule]
references = sorted(group['evidence'])
selected_refs = [ref for ref in references if group['evidence_rules'][ref] == rule]
visible_refs = (selected_refs[:3] + [ref for ref in references if ref not in selected_refs])[:6]
examples = [group['evidence'][ref] for ref in visible_refs]
if len(references)>6:
examples.append(f'{len(references)-6} additional supporting inventory observations; see evidence_refs in JSON.')
classification = CONFIRMED if group['confirmed'] else POTENTIAL
reason = f"Rule {rule} assigns {score}/100. Strongest rule within workload {group_id}; corroborating services/ports/connections are not added."
findings.append({'id':group_id, 'category':group['title'], 'classification':classification,
'evidence':'\n'.join(examples), 'evidence_refs':references,
'severity':risk_level(score,policy), 'points':score, 'scoring_reason':reason})
breakdown.append({'workload':group_id,'title':group['title'],'classification':classification,
'selected_rule':rule,'candidate_rules':sorted(group['rules']),'selected_evidence_refs':selected_refs,
'score':score,'evidence_count':len(references),'reason':reason})
for (category,classification), observations in sorted(aggregates.items()):
examples = [text for _,text in observations[:3]]
findings.append({'id':'context:'+category,'category':category,'classification':classification,
'evidence':f'{len(observations)} observations. ' + ' '.join(examples),
'evidence_refs':[ref for ref,_ in observations], 'severity':'informational','points':0,
'scoring_reason':'Context/uncertainty only; no evidence-based workload score.'})
checks = data.get('checks',{})
incomplete = []
earned, possible = 0.0, sum(policy['coverage_weights'].values())
for name,weight in policy['coverage_weights'].items():
check = checks.get(name,{})
status = check.get('status','not_collected')
if status == 'complete' and (check.get('errors') or check.get('permission_limitations')):
status = 'partial'
earned += weight * (1 if status=='complete' else policy['partial_check_credit'] if status=='partial' else 0)
for name in sorted(set(checks) | set(policy['coverage_weights'])):
check = checks.get(name, {'status':'not_collected','message':'No status supplied for this assessment scope.'})
if check['status'] != 'complete' or check.get('permission_limitations') or check.get('errors'):
incomplete.append({'check':name,'status':check['status'],'message':check['message']})
if incomplete:
findings.append({'id':'coverage','category':'Missing evidence','classification':UNKNOWN,
'evidence':'\n'.join(f"{row['check']}: {row['status']} — {row['message']}" for row in incomplete),
'evidence_refs':['checks.'+row['check'] for row in incomplete],
'severity':'informational','points':0,
'scoring_reason':'Collection gaps reduce confidence and trigger review; they do not establish an active workload.'})
findings.append({'id':'observation-limits','category':'Observation limits','classification':UNKNOWN,
'evidence':REVIEW,'evidence_refs':[], 'severity':'informational','points':0,
'scoring_reason':'General observation limits never add retirement risk points.'})
score = max((row['score'] for row in breakdown), default=0)
for row in breakdown:
row['determines_final_score'] = row['score']==score and score>0
for finding in findings:
finding['determines_final_score'] = finding['points']==score and score>0
coverage_percent = round(100*earned/possible)
unknown_services = len(aggregates.get(('Unclassified services',UNKNOWN),[]))
unknown_directions = sum(row['direction']=='Unknown' for row in network)
confidence = 'low' if coverage_percent<60 else 'moderate' if coverage_percent<90 or incomplete or unknown_services or unknown_directions else 'high'
return {'model_version':'2.0','score':score,'level':risk_level(score,policy),'findings':findings,
'scoring':{'formula':'Maximum grouped workload score (0–100); never a sum of inventory observations.',
'policy_id':policy_id(policy),'policy':policy,'breakdown':breakdown,
'explanation':'No scored workload evidence was identified. This is not retirement clearance.' if not score else f"Final {score}/100 is the highest workload score; other workloads and supporting observations are not added."},
'coverage':{'percent':coverage_percent,'incomplete_checks':incomplete,
'method':'Weighted complete checks; partial checks receive policy-defined credit. Missing checks receive zero. Scope limitations still apply.'},
'confidence':{'level':confidence,'unclassified_services':unknown_services,'unknown_connection_directions':unknown_directions,
'scope':'Confidence concerns collected/classified evidence only, not historical inactivity or retirement readiness.'},
'network_observations':network,'task_observations':tasks,
'classification_counts':dict(Counter(row['classification'] for row in findings)),
'review_required':True,'manual_verification':REVIEW}