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:
62
samsara/core/process.py
Normal file
62
samsara/core/process.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Bounded child output in memory; never spool inventory to temporary files."""
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class CollectionCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProcessFailure(RuntimeError):
|
||||
def __init__(self, code, returncode=None):
|
||||
self.code = code
|
||||
self.returncode = returncode
|
||||
super().__init__('Inventory subprocess failed (' + code + '); diagnostics omitted.')
|
||||
|
||||
|
||||
def capture(arguments, *, env, timeout, cancelled=lambda: False, limit=10*1024*1024, creationflags=0):
|
||||
if cancelled():
|
||||
raise CollectionCancelled()
|
||||
try:
|
||||
child = subprocess.Popen(arguments, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, shell=False, env=env, creationflags=creationflags)
|
||||
except OSError:
|
||||
raise ProcessFailure('command_unavailable') from None
|
||||
buffer = bytearray()
|
||||
exceeded = threading.Event()
|
||||
read_failed = threading.Event()
|
||||
def drain():
|
||||
try:
|
||||
while chunk := child.stdout.read(65536):
|
||||
if len(buffer)+len(chunk)>limit:
|
||||
exceeded.set()
|
||||
break
|
||||
buffer.extend(chunk)
|
||||
except (OSError, ValueError):
|
||||
read_failed.set()
|
||||
reader = threading.Thread(target=drain, daemon=True)
|
||||
reader.start()
|
||||
deadline = time.monotonic()+timeout
|
||||
try:
|
||||
while child.poll() is None:
|
||||
if cancelled(): raise CollectionCancelled()
|
||||
if exceeded.is_set(): raise ProcessFailure('output_limit')
|
||||
if time.monotonic()>=deadline: raise ProcessFailure('timeout')
|
||||
time.sleep(.02)
|
||||
reader.join(timeout=1)
|
||||
if reader.is_alive(): raise ProcessFailure('cleanup_failed')
|
||||
if cancelled(): raise CollectionCancelled()
|
||||
if exceeded.is_set(): raise ProcessFailure('output_limit')
|
||||
if read_failed.is_set() or child.returncode: raise ProcessFailure('collection_failed', child.returncode)
|
||||
return bytes(buffer)
|
||||
finally:
|
||||
if child.poll() is None: child.kill()
|
||||
try:
|
||||
child.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise ProcessFailure('cleanup_failed') from None
|
||||
# Fixed collectors must not spawn descendants holding stdout open.
|
||||
# Do not block on closing a buffered stream owned by a stalled reader.
|
||||
reader.join(timeout=.2)
|
||||
if not reader.is_alive(): child.stdout.close()
|
||||
Reference in New Issue
Block a user