- 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>
63 lines
2.4 KiB
Python
63 lines
2.4 KiB
Python
"""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()
|