79 lines
6.2 KiB
Python
79 lines
6.2 KiB
Python
"""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}
|