48 lines
2.6 KiB
Python
48 lines
2.6 KiB
Python
"""Offline intake shared by Windows and Linux desktops. No code is executed."""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
|
|
MAX_IMPORT_BYTES = 10 * 1024 * 1024
|
|
SCHEMA = Path(__file__).resolve().parents[2] / 'schemas' / 'inventory-v1.schema.json'
|
|
# Inventory fields allowed through the report boundary. Command arguments, environment
|
|
# variables and private key material are deliberately excluded.
|
|
ALLOWED = {
|
|
'roles': {'name', 'state'},
|
|
'services': {'name', 'display_name', 'state', 'startup', 'account'},
|
|
'network': {'state', 'local_address', 'local_port', 'remote_address', 'remote_port', 'pid', 'process', 'direction'},
|
|
'tasks': {'name', 'path', 'enabled', 'account', 'triggers'},
|
|
'software': {'name', 'publisher', 'version', 'installation_date'},
|
|
'storage': {'type', 'name', 'path', 'capacity_gib', 'free_gib'},
|
|
'certificates': {'subject', 'issuer', 'expires', 'thumbprint'},
|
|
}
|
|
SERVER_FIELDS = {'hostname', 'platform', 'operating_system', 'os_build', 'last_boot', 'ip_addresses', 'network_adapters', 'domain'}
|
|
|
|
|
|
def load_inventory(path):
|
|
with Path(path).open('rb') as source:
|
|
raw = source.read(MAX_IMPORT_BYTES + 1)
|
|
if len(raw) > MAX_IMPORT_BYTES:
|
|
raise ValueError('Inventory exceeds the 10 MiB import limit.')
|
|
try:
|
|
data = json.loads(raw.decode('utf-8-sig'))
|
|
except (ValueError, UnicodeError):
|
|
raise ValueError('The selected file is not valid UTF-8 inventory JSON.') from None
|
|
# A report wrapper is accepted, but its precomputed assessment is never trusted.
|
|
if isinstance(data, dict) and 'inventory_result' in data:
|
|
data = data['inventory_result']
|
|
schema = json.loads(SCHEMA.read_text(encoding='utf-8'))
|
|
errors = list(Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(data))
|
|
if errors:
|
|
raise ValueError('Inventory does not match the v1 schema; required fields, types or check statuses are invalid.')
|
|
# Keep diagnostic contents out of logs and exported reports. Diagnostics may
|
|
# contain command lines or credentials from external collection tools.
|
|
data['checks'] = {name: {'status': check['status'], 'message':
|
|
'Imported check status. Consult collector diagnostics separately; details excluded to limit secret exposure.'}
|
|
for name, check in data['checks'].items()}
|
|
data['server'] = {key: value for key, value in data['server'].items() if key in SERVER_FIELDS}
|
|
data['inventory'] = {section: [{key: value for key, value in row.items() if key in ALLOWED[section]}
|
|
for row in rows] for section, rows in data['inventory'].items()}
|
|
return data
|