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

@@ -189,6 +189,8 @@ def classify_dependencies(data):
state = str(row.get('state','')).lower()
proc = normal_name(row.get('process',''))
remote,local = address(row.get('remote_address')), address(row.get('local_address'))
has_remote = remote is not None or row.get('remote_scope') in ('private','public')
peer = row.get('remote_address') or (f"{row['remote_scope']} peer (address withheld)" if row.get('remote_scope') else '')
matching = []
if state in ('established','estab') and local is not None and known_pid(row):
wildcard = ipaddress.ip_address('0.0.0.0' if local.version==4 else '::')
@@ -207,7 +209,7 @@ def classify_dependencies(data):
basis = 'Direction supplied by inventory source; not independently verified.'
elif state in ('established','estab') and matching:
direction,basis = 'Inbound (inferred)','Local address/port and nonzero PID match a listener. Client initiation is inferred, not observed.'
elif state in ('established','estab') and remote and type(row.get('local_port')) is int and row['local_port']>=49152 and 0 < row.get('remote_port',0)<49152:
elif state in ('established','estab') and has_remote and type(row.get('local_port')) is int and row['local_port']>=49152 and 0 < row.get('remote_port',0)<49152:
direction,basis = 'Outbound (inferred)','Ephemeral local port to a non-ephemeral remote port without an attributed listener; heuristic only.'
family = row_family(row)
classification, rationale = UNKNOWN, 'No attributed application workload. No score from the connection alone.'
@@ -219,12 +221,12 @@ def classify_dependencies(data):
if direction == 'Listener':
evidence(family,'attributed_application_listener',f"{row.get('process','unknown')} PID {row.get('pid','unknown')} listening at {row.get('local_address')}:{row.get('local_port')}; a listener alone is potential exposure.",ref)
classification,rationale = POTENTIAL,'Listener attributed to a known application; grouped with service evidence.'
elif state in ('established','estab') and direction.startswith('Inbound') and matching and remote and not remote.is_unspecified:
elif state in ('established','estab') and direction.startswith('Inbound') and matching and has_remote and not (remote and remote.is_unspecified):
if family == 'file-sharing':
evidence(family,'observed_smb_transport',f"File-sharing transport observed on local port {row.get('local_port')}; specific share/export use is unverified without correlated open files.",ref)
classification,rationale = POTENTIAL,'File-sharing transport observed; no named business share attributed.'
else:
evidence(family,'observed_application_clients',f"Observed application TCP activity {row.get('local_address')}:{row.get('local_port')} ↔ {row.get('remote_address')}:{row.get('remote_port')}, PID {row.get('pid')}; matches listener(s) {matching}. {basis} Business ownership remains unverified.",ref,True)
evidence(family,'observed_application_clients',f"Observed application TCP activity {row.get('local_address')}:{row.get('local_port')} ↔ {peer}:{row.get('remote_port')}, PID {row.get('pid')}; matches listener(s) {matching}. {basis} Business ownership remains unverified.",ref,True)
classification,rationale = CONFIRMED,'Observed transport activity on an attributed application listener; direction inference is recorded separately.'
else:
note('Other application traffic',UNKNOWN,f"{direction}: {row.get('process','unknown')}; not scored as an inbound consumer. {basis}",ref)
@@ -237,7 +239,7 @@ def classify_dependencies(data):
classification,rationale=STANDARD,'Baseline OS/management endpoint; no application workload attributed.'
note('OS networking',STANDARD,rationale,ref)
else:
note('Unattributed networking',UNKNOWN,f"{direction}: {row.get('local_address')}:{row.get('local_port')} ↔ {row.get('remote_address')}:{row.get('remote_port')}. {basis}",ref)
note('Unattributed networking',UNKNOWN,f"{direction}: {row.get('local_address')}:{row.get('local_port')} ↔ {peer}:{row.get('remote_port')}. {basis}",ref)
network_observations.append({'inventory_index':index,'direction':direction,'direction_basis':basis,
'classification':classification,'workload':family or '', 'rationale':rationale})
return groups, aggregates, network_observations, task_observations