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:
2026-11-04 18:50:24 -08:00
parent 7fed3b9ab9
commit 610a3bb447
51 changed files with 2719 additions and 604 deletions

View File

@@ -1,9 +1,11 @@
"""Actionable assessment report, with full inventory preserved as a separate JSON artifact."""
"""Actionable reports from privacy-minimized evidence, without raw sidecars."""
import hashlib
import html
import json
from pathlib import Path
from urllib.parse import quote
from samsara.core.privacy import minimize_inventory
from samsara.core.storage import write_private
from samsara.analyzers.assessment import assess
from samsara.analyzers.summary import summarize
MANUAL_STEPS = ['Confirm application and business owners and obtain administrator retirement approval.',
@@ -12,21 +14,30 @@ MANUAL_STEPS = ['Confirm application and business owners and obtain administrato
'Verify custom tasks and share consumers; resolve incomplete checks and confirm rollback/retention requirements.']
def private_assessment(data, assessment):
# Never trust a marker or cached finding to authorize raw data disclosure.
source=data.get('server',{}).get('collection_source')
clean=minimize_inventory(data, trusted_source=source)
policy=assessment.get('scoring',{}).get('policy') if isinstance(assessment,dict) else None
if policy is not None:
import copy
policy=copy.deepcopy(policy)
policy['name']='Deployment scoring policy'
return clean, assess(clean, policy=policy)
def export_json(path, data, assessment):
Path(path).write_text(json.dumps({'inventory_result':data,'assessment':assessment},indent=2),encoding='utf-8')
data, assessment=private_assessment(data, assessment)
write_private(path,json.dumps({'inventory_result':data,'assessment':assessment},indent=2))
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
"""Explicit minimized inventory export; never writes a raw sidecar."""
data=minimize_inventory(data)
payload=(json.dumps(data,indent=2,ensure_ascii=True)+'\n').encode('utf-8')
destination=Path(path).with_suffix('.inventory.json')
write_private(destination,payload)
return destination, hashlib.sha256(payload).hexdigest()
def table(headers, rows):
@@ -38,7 +49,7 @@ def table(headers, rows):
def export_html(path, data, assessment):
escape = lambda value: html.escape(str(value))
artifact, digest = inventory_artifact(path,data)
data, assessment = private_assessment(data, assessment)
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':
@@ -78,9 +89,7 @@ def export_html(path, data, assessment):
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 += '<h2>Privacy and omitted evidence</h2><p>Identifying names, accounts, task commands, certificate identities and unapproved paths are omitted. No raw inventory sidecar is generated. JSON export contains the same minimized evidence. Numeric IP addresses remain confidential infrastructure metadata. This report does not establish PHI-free operation or HIPAA compliance.</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')
write_private(path,'<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Samsara</title><style>'+css+'</style></head><body>'+content+'</body></html>')