#!/usr/bin/env python3 """Small stdlib adapter for explicitly scoped device-test evidence collection. No installers, arbitrary arguments, credential persistence, or test verdicts. Run --help for commands. Global options must precede the subcommand. """ from __future__ import annotations import argparse import csv import hashlib import io import ipaddress import json import os import re import shutil import subprocess import sys import tempfile import xml.etree.ElementTree as ET from dataclasses import dataclass, field from pathlib import Path from urllib.parse import parse_qsl, unquote, urlsplit BASE = Path(__file__).resolve().parents[1] CATALOG = BASE / "assets" / "tool-catalog.json" SERIAL_SCRIPT = BASE / "scripts" / "vendor" / "serial_monitor.ps1" SERIAL_SHA256 = "173584db05950b9842f2485eec5b33c0b16062f944216c283a18e038a9138a67" EXECUTABLES = { "ffprobe": ("ffprobe.exe", "ffprobe"), "ffmpeg": ("ffmpeg.exe", "ffmpeg"), "tshark": ("tshark.exe", "tshark"), "dumpcap": ("dumpcap.exe", "dumpcap"), "nmap": ("nmap.exe", "nmap"), "binwalk": ("binwalk.exe", "binwalk"), "powershell": ("powershell.exe", "pwsh.exe", "powershell", "pwsh"), } VERSION_FLAGS = { "ffprobe": "-version", "ffmpeg": "-version", "binwalk": "--help", "powershell": "-Version", } PCAP_FIELDS = ( "frame.number", "frame.time_relative", "frame.len", "ip.src", "ip.dst", "ipv6.src", "ipv6.dst", "tcp.srcport", "tcp.dstport", "udp.srcport", "udp.dstport", "_ws.col.Protocol", ) STREAM_FIELDS = ( "index", "codec_name", "codec_type", "width", "height", "avg_frame_rate", "r_frame_rate", "sample_rate", "channels", "bit_rate", ) FORMAT_FIELDS = ("format_name", "duration", "size", "bit_rate") MEDIAINFO_FIELDS = { "general": ("Format", "Duration", "FileSize", "OverallBitRate"), "video": ( "Format", "CodecID", "Width", "Height", "FrameRate", "FrameCount", "BitRate", "Duration", ), "audio": ("Format", "CodecID", "SamplingRate", "Channels", "BitRate", "Duration"), } SECRET_KEYS = { "password", "passwd", "pwd", "secret", "token", "access_token", "api_key", "apikey", "authorization", "密码", "口令", } class ValidationError(ValueError): """The message is an internal constant, never an untrusted argument.""" class ToolMissing(RuntimeError): pass class SafeParser(argparse.ArgumentParser): def error(self, message): # argparse normally repeats invalid arguments, which can contain secrets. raise ValidationError("invalid_arguments_use_help") def parse_args(argv=None): p = SafeParser(description=__doc__, allow_abbrev=False) p.add_argument( "--profile", help="JSON profile; tools maps tool_id to an existing executable path", ) p.add_argument( "--tool-root", action="append", default=[], help="Trusted extracted-tool directory (repeatable)", ) p.add_argument("--timeout", help="Process timeout, integer 1..3600 seconds") p.add_argument( "--dry-run", action="store_true", help="Validate and print a redacted plan without launching", ) sub = p.add_subparsers(dest="command", required=True, parser_class=SafeParser) sub.add_parser( "list", help="Read the tool catalog and discover local executables", allow_abbrev=False, ) doctor = sub.add_parser( "doctor", help="Check local availability; does not contact devices", allow_abbrev=False, ) doctor.add_argument( "--check-versions", action="store_true", help="Explicitly run bounded version/help commands", ) probe = sub.add_parser( "probe", help="Inspect local media or an RTSP URL from an environment variable", allow_abbrev=False, ) source = probe.add_mutually_exclusive_group(required=True) source.add_argument("--input") source.add_argument("--rtsp-env") probe.add_argument("--transport", choices=("tcp", "udp"), default="tcp") media = sub.add_parser( "media-info", help="Read local media metadata with a configured Windows MediaInfo.dll", allow_abbrev=False, ) media.add_argument("--input", required=True) pcap = sub.add_parser( "analyze-pcap", help="Read a local capture using fixed non-payload fields", allow_abbrev=False, ) pcap.add_argument("--input", required=True) pcap.add_argument("--max-packets", default="1000") cap = sub.add_parser( "capture", help="Capture one host on an explicitly selected interface", allow_abbrev=False, ) cap.add_argument("--interface", required=True) cap.add_argument("--host", required=True) cap.add_argument("--duration", required=True) cap.add_argument("--output", required=True) scan = sub.add_parser( "scan", help="TCP connect scan: one IP and explicit numeric ports only", allow_abbrev=False, ) scan.add_argument("--host", required=True) scan.add_argument("--ports", required=True) firmware = sub.add_parser( "firmware-scan", help="Local Binwalk default signature scan; no extraction", allow_abbrev=False, ) firmware.add_argument("--input", required=True) serial = sub.add_parser( "serial", help="Bounded read-only Windows serial capture", allow_abbrev=False ) serial.add_argument("--port", required=True) serial.add_argument("--baud", required=True) serial.add_argument("--duration", required=True) serial.add_argument("--output", required=True) serial.add_argument( "--dtr-enable", action="store_true", help="Explicitly enable DTR to match the authorized device session; default off", ) serial.add_argument( "--rts-enable", action="store_true", help="Explicitly enable RTS to match the authorized device session; default off", ) return p.parse_args(argv) def validate_profile(profile): if not isinstance(profile, dict) or not isinstance(profile.get("tools", {}), dict): raise ValidationError("profile_must_be_object_with_tools_mapping") def walk(value): if isinstance(value, dict): for key, item in value.items(): normalized = ( re.sub(r"([a-z])([A-Z])", r"\1_\2", str(key)) .lower() .replace("-", "_") ) is_secret = normalized in SECRET_KEYS or any( normalized.endswith("_" + word) for word in SECRET_KEYS ) if ( is_secret and not normalized.endswith("_env") and item not in (None, "") ): raise ValidationError( "plaintext_credentials_forbidden_use_env_reference" ) walk(item) elif isinstance(value, list): for item in value: walk(item) elif isinstance(value, str) and re.search( r"(?i)rtsps?://|[a-z][a-z0-9+.-]*://[^/\s]*@|[?&](?:password|passwd|token|access_token|api_key|secret)=", value, ): raise ValidationError("credential_or_rtsp_url_forbidden_in_profile") walk(profile) for key, value in profile.get("tools", {}).items(): if key not in EXECUTABLES or not isinstance(value, str) or not value.strip(): raise ValidationError("tools_requires_known_tool_id_and_executable_path") for section, keys in ( ("runtimes", {"binwalk_python"}), ("libraries", {"mediainfo"}), ): values = profile.get(section, {}) if not isinstance(values, dict) or any( key not in keys or not isinstance(value, str) or not value.strip() for key, value in values.items() ): raise ValidationError("runtime_and_library_require_known_id_and_path_only") return profile def load_profile(path): if path is None: return {} try: profile = json.loads(Path(path).read_text(encoding="utf-8-sig")) except (OSError, ValueError): raise ValidationError("profile_unreadable_or_invalid_json") from None validate_profile(profile) # Relative tool paths belong to the profile directory, not the caller's cwd. for section in ("tools", "runtimes", "libraries"): for key, value in profile.get(section, {}).items(): if not Path(value).is_absolute(): profile[section][key] = str( (Path(path).resolve().parent / value).resolve() ) return profile def root_candidates(roots): """Only enumerate filenames; never open archives or run their contents.""" for value in roots: root = Path(value).resolve() if not root.is_dir(): raise ValidationError("tool_root_must_be_existing_directory") count = 0 for current, dirs, files in os.walk(root, followlinks=False): depth = len(Path(current).relative_to(root).parts) dirs[:] = ( sorted( d for d in dirs if not d.startswith(".") and not (Path(current) / d).is_symlink() ) if depth < 4 else [] ) for name in sorted(files): count += 1 if count > 20000: return yield Path(current) / name def resolve_tool(tool_id, profile, roots): names = EXECUTABLES[tool_id] configured = profile.get("tools", {}).get(tool_id) if configured: path = Path(configured).resolve() if path.name.lower() not in names: raise ValidationError( "unexpected_executable_name_no_installers_or_wrappers" ) if not path.is_file(): raise ToolMissing(tool_id) return str(path) # Explicit roots precede PATH and require extracted, exact-name executables. for path in root_candidates(roots): if path.name.lower() in names and path.is_file(): return str(path.resolve()) for name in names: found = shutil.which(name) if found and Path(found).name.lower() in names: return str(Path(found).resolve()) raise ToolMissing(tool_id) def configured_local_path(value, tool_id, name_pattern): if not isinstance(value, str) or not value: raise ToolMissing(tool_id) if ( "://" in value or value.startswith(("\\\\", "//")) or re.search(r"[\x00-\x1f]", value) ): raise ValidationError("runtime_and_library_require_local_file") path = Path(value).resolve() if not re.fullmatch(name_pattern, path.name, re.IGNORECASE): raise ValidationError("unexpected_runtime_or_library_name") if not path.is_file(): raise ToolMissing(tool_id) return str(path) def command_prefix(tool_id, profile, roots): if tool_id == "binwalk" and profile.get("runtimes", {}).get("binwalk_python"): python = configured_local_path( profile["runtimes"]["binwalk_python"], "binwalk", r"python(?:3(?:\.\d+)?)?(?:\.exe)?", ) return [python, "-I", "-m", "binwalk"] if tool_id == "mediainfo": library = configured_local_path( profile.get("libraries", {}).get("mediainfo"), "mediainfo", r"MediaInfo\.dll", ) return [ sys.executable, "-I", str(Path(__file__).resolve()), "_media-info-worker", "--library", library, ] return [resolve_tool(tool_id, profile, roots)] def bounded_int(value, low, high, error): if not re.fullmatch(r"[0-9]+", str(value)) or not low <= int(value) <= high: raise ValidationError(error) return int(value) def host_ip(value): try: if "%" in value: raise ValueError address = ipaddress.ip_address(value) if ( address.is_multicast or address.is_unspecified or str(address) == "255.255.255.255" ): raise ValueError return str(address) except ValueError: raise ValidationError( "one_explicit_unicast_ip_required_no_hostname_or_cidr" ) from None def local_input(value): if ( re.search(r"[\x00-\x1f]", value) or "://" in value or value.startswith(("\\\\", "//")) ): raise ValidationError("local_regular_file_required") path = Path(value).resolve() if not path.is_file(): raise ValidationError("local_input_file_missing") return str(path) def new_output(value): if ( re.search(r"[\x00-\x1f]", value) or "://" in value or value.startswith(("\\\\", "//")) ): raise ValidationError("local_output_path_required") path = Path(value).resolve() if path.exists() or not path.parent.is_dir(): raise ValidationError("output_must_be_new_file_in_existing_directory") return str(path) def redact(text, secrets=()): value = str(text) for secret in sorted((str(s) for s in secrets if s), key=len, reverse=True): value = value.replace(secret, "[REDACTED]") value = re.sub(r"(?i)rtsps?://[^\s\"<>]+", "[RTSP_URL_REDACTED]", value) value = re.sub( r"(?i)(password|passwd|pwd|token|secret|authorization)\s*[:=]\s*[^\s,;]+", r"\1=[REDACTED]", value, ) return value @dataclass class Plan: command: str tool_id: str argv: list[str] = field(repr=False) timeout: int metadata: dict = field(default_factory=dict) secrets: tuple[str, ...] = field(default=(), repr=False) sensitive_argument: str | None = field(default=None, repr=False) def public(self): # Only the RTSP argument contains values taken from its credentials/query. # Replacing a short token in fixed flags or executable paths corrupts plans. safe = [ "[RTSP_URL_FROM_ENV]" if a == self.sensitive_argument else redact(a) for a in self.argv ] return { "command": self.command, "tool_id": self.tool_id, "argv": safe, "timeout_seconds": self.timeout, **self.metadata, } def build_plan(args, profile, environ): validate_profile(profile) timeout = ( bounded_int(args.timeout, 1, 3600, "timeout_must_be_1_to_3600_seconds") if args.timeout is not None else 60 ) command = args.command secrets = () sensitive = None meta = {} if command == "probe": tool_id = "ffprobe" entries = ( "stream=" + ",".join(STREAM_FIELDS) + ":format=" + ",".join(FORMAT_FIELDS) ) tail = [ "-v", "quiet", "-show_entries", entries, "-of", "json", "-probesize", "5000000", "-analyzeduration", "5000000", ] if args.rtsp_env: if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", args.rtsp_env): raise ValidationError("invalid_environment_variable_name") url = environ.get(args.rtsp_env, "") try: parsed = urlsplit(url) if ( not url or re.search(r"[\x00-\x20\x7f]", url) or parsed.scheme not in ("rtsp", "rtsps") or not parsed.hostname or parsed.port == 0 or parsed.fragment ): raise ValueError # Accessing .port validates the valid range as well. _ = parsed.port except ValueError: raise ValidationError( "environment_must_contain_complete_rtsp_url" ) from None secrets = tuple( filter( None, ( url, parsed.username, parsed.password, unquote(parsed.password or ""), *(v for _, v in parse_qsl(parsed.query)), ), ) ) sensitive = url meta["rtsp_env"] = args.rtsp_env tail += [ "-rtsp_transport", args.transport, "-rw_timeout", str(timeout * 1000000), "-i", url, ] else: tail += ["-protocol_whitelist", "file", "-i", local_input(args.input)] elif command == "media-info": tool_id = "mediainfo" tail = ["--input", local_input(args.input)] elif command == "analyze-pcap": tool_id = "tshark" limit = bounded_int( args.max_packets, 1, 10000, "max_packets_must_be_1_to_10000" ) tail = [ "-n", "-r", local_input(args.input), "-c", str(limit), "-T", "fields", "-E", "header=y", "-E", "separator=/t", "-E", "quote=d", ] for item in PCAP_FIELDS: tail += ["-e", item] meta["max_packets"] = limit elif command in ("capture", "serial"): duration = bounded_int( args.duration, 1, 600, "duration_must_be_1_to_600_seconds" ) timeout = duration + 10 if args.timeout is None else timeout if timeout < duration + 5: raise ValidationError("timeout_must_exceed_duration_by_at_least_5_seconds") output = new_output(args.output) meta.update( { "output": output, "duration_seconds": duration, "artifact_may_contain_sensitive_data": True, } ) if command == "capture": tool_id = "dumpcap" interface = args.interface if ( not interface or interface.startswith("-") or "://" in interface or re.search(r"[\x00-\x1f]", interface) ): raise ValidationError("explicit_local_interface_required") ip = host_ip(args.host) tail = [ "-q", "-i", interface, "-f", "host " + ip, "-s", "256", "-a", "duration:" + str(duration), "-a", "filesize:102400", "-w", output, ] meta["host"] = ip else: tool_id = "powershell" if not re.fullmatch(r"COM[1-9][0-9]{0,3}", args.port, re.IGNORECASE): raise ValidationError("explicit_windows_com_port_required") baud = bounded_int(args.baud, 1, 4000000, "baud_must_be_1_to_4000000") if Path(output + ".raw.bin").exists(): raise ValidationError("serial_raw_output_already_exists") if ( not SERIAL_SCRIPT.is_file() or hashlib.sha256(SERIAL_SCRIPT.read_bytes()).hexdigest() != SERIAL_SHA256 ): raise ValidationError( "vendor_serial_script_missing_or_checksum_changed" ) tail = [ "-NoLogo", "-NoProfile", "-NonInteractive", "-File", str(SERIAL_SCRIPT), "-Port", args.port.upper(), "-BaudRate", str(baud), "-DurationSeconds", str(duration), "-LogPath", output, ] if args.dtr_enable: tail.append("-DtrEnable") if args.rts_enable: tail.append("-RtsEnable") meta["raw_output"] = output + ".raw.bin" meta.update(dtr_enabled=args.dtr_enable, rts_enabled=args.rts_enable) elif command == "scan": tool_id = "nmap" ip = host_ip(args.host) if not re.fullmatch(r"[0-9]+(?:,[0-9]+)*", args.ports): raise ValidationError("ports_require_explicit_comma_separated_numbers") ports = sorted( { bounded_int(x, 1, 65535, "port_out_of_range") for x in args.ports.split(",") } ) if len(ports) > 128: raise ValidationError("at_most_128_explicit_ports_allowed") tail = [ "-sT", "-n", "-Pn", "--max-retries", "1", "--host-timeout", str(max(1, timeout - 2)) + "s", "-p", ",".join(map(str, ports)), "-oX", "-", ] if ":" in ip: tail += ["-6"] tail += [ip] meta.update({"host": ip, "ports": ports}) elif command == "firmware-scan": tool_id = "binwalk" # Both Binwalk 2.x and 3.x default to signature scanning. No -e/-M. tail = [local_input(args.input)] else: raise ValidationError("unsupported_execution_command") prefix = command_prefix(tool_id, profile, args.tool_root) return Plan(command, tool_id, prefix + tail, timeout, meta, secrets, sensitive) def safe_data(plan, stdout): if plan.command == "probe": source = json.loads(stdout) data = { "streams": [ { k: v for k, v in row.items() if k in STREAM_FIELDS and isinstance(v, (str, int, float)) } for row in source.get("streams", []) if isinstance(row, dict) ], "format": { k: v for k, v in source.get("format", {}).items() if k in FORMAT_FIELDS and isinstance(v, (str, int, float)) }, } # Redact decoded values before JSON escaping (passwords may include quotes). for row in data["streams"] + [data["format"]]: for key, value in row.items(): if isinstance(value, str): # Numeric measurements are structured data, not echoed URL data. # Preserve valid numbers even when a query token is e.g. "1". numeric = key not in ( "codec_name", "codec_type", "format_name", ) and re.fullmatch(r"-?\d+(?:\.\d+)?(?:/\d+)?", value) row[key] = value if numeric else redact(value, plan.secrets) return data if plan.command == "media-info": source = json.loads(stdout) data = {"version": redact(str(source.get("version", "")))} for group, fields in MEDIAINFO_FIELDS.items(): rows = ( [source.get(group, {})] if group == "general" else source.get(group, []) ) filtered = [ { key: redact(str(value)) for key, value in row.items() if key in fields and isinstance(value, (str, int, float)) } for row in rows[:100] if isinstance(row, dict) ] data[group] = ( filtered[0] if group == "general" and filtered else {} if group == "general" else filtered ) return data if plan.command == "analyze-pcap": rows = csv.DictReader(io.StringIO(stdout), delimiter="\t") return { "packets": [ { k: redact(str(v or ""), plan.secrets) for k, v in row.items() if k in PCAP_FIELDS } for _, row in zip(range(plan.metadata["max_packets"]), rows) ] } if plan.command == "scan": root = ET.fromstring(stdout) requested_ports = list(plan.metadata["ports"]) hosts = [] for host in root.findall("host"): state = host.find("status") ports = [] for port in host.findall("ports/port"): status = port.find("state") ports.append( { "port": port.get("portid"), "protocol": port.get("protocol"), "state": status.get("state") if status is not None else None, } ) aggregates = [] for group in host.findall("ports/extraports"): reasons = [ { "reason": reason.get("reason"), "count": int(reason.get("count", "0")), } for reason in group.findall("extrareasons") ] aggregates.append( { "state": group.get("state"), "count": int(group.get("count", "0")), "reasons": reasons, } ) # Aggregated groups do not identify the state of individual ports. # Count only explicit TCP port states when reporting attribution. reported = { int(port["port"]) for port in ports if port["protocol"] == "tcp" and port["state"] } missing = [port for port in requested_ports if port not in reported] coverage = ( "complete" if not missing else "partial" if len(missing) < len(requested_ports) else "none" ) hosts.append( { "state": state.get("state") if state is not None else None, "ports": ports, "aggregate_states": aggregates, "ports_not_individually_reported": missing, "individual_port_coverage": coverage, } ) return { "requested_ports": requested_ports, "host_results_present": bool(hosts), "hosts": hosts, } if plan.command == "firmware-scan": return { "signature_report": redact(stdout[:64000]), "output_truncated": len(stdout) > 64000, } return {} def process_lifecycle(): import importlib.util name = "_artemis_device_test_process_lifecycle" if name not in sys.modules: spec = importlib.util.spec_from_file_location( name, Path(__file__).with_name("process_lifecycle.py") ) module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return sys.modules[name] PROCESS = process_lifecycle() def _run_in_windows_job(argv, **kwargs): return PROCESS.run(argv, **kwargs) def run_process(argv, tool_id, **kwargs): def run(**options): if os.name == "nt": return _run_in_windows_job(argv, **options) # This plugin targets Windows; fail closed rather than claim containment elsewhere. raise PROCESS.ManagedProcessError("process_ownership_requires_windows") if tool_id != "binwalk": return run(**kwargs) env = dict(kwargs.pop("env", os.environ)) env.pop("PYTHONPATH", None) env.pop("PYTHONHOME", None) with tempfile.TemporaryDirectory(prefix="artemis-binwalk-") as config: env["XDG_CONFIG_HOME"] = config return run(env=env, **kwargs) def media_info_worker(argv): """Private fixed DLL worker; parent process owns the hard timeout.""" p = SafeParser(allow_abbrev=False) p.add_argument("--library", required=True) source = p.add_mutually_exclusive_group(required=True) source.add_argument("--input") source.add_argument("--version", action="store_true") try: args = p.parse_args(argv) library = configured_local_path(args.library, "mediainfo", r"MediaInfo\.dll") media = local_input(args.input) if args.input else None import ctypes as C lib = C.WinDLL(library) signatures = { "New": ([], C.c_void_p), "Delete": ([C.c_void_p], None), "Option": ([C.c_void_p, C.c_wchar_p, C.c_wchar_p], C.c_wchar_p), "Open": ([C.c_void_p, C.c_wchar_p], C.c_size_t), "Close": ([C.c_void_p], None), "Get": ( [C.c_void_p, C.c_int, C.c_size_t, C.c_wchar_p, C.c_int, C.c_int], C.c_wchar_p, ), "Count_Get": ([C.c_void_p, C.c_int, C.c_size_t], C.c_size_t), } for name, (argtypes, restype) in signatures.items(): function = getattr(lib, "MediaInfo_" + name) function.argtypes, function.restype = argtypes, restype # MediaInfo 21.09 enables this option for every nonempty value, # including "No". Require its explicit disabled readback before parsing. lib.MediaInfo_Option(None, "Internet", "") if lib.MediaInfo_Option(None, "Internet_Get", "") != "": raise ValueError version = lib.MediaInfo_Option(None, "Info_Version", "") or "" if args.version: print(redact(version)) return 0 handle = lib.MediaInfo_New() if not handle: raise ValueError try: lib.MediaInfo_Option(handle, "Internet", "") if lib.MediaInfo_Option(handle, "Internet_Get", "") != "": raise ValueError if lib.MediaInfo_Open(handle, media) == 0: raise ValueError data = {"version": version} for kind, group in enumerate(("general", "video", "audio")): count = min( 100, lib.MediaInfo_Count_Get(handle, kind, C.c_size_t(-1).value) ) rows = [ { key: lib.MediaInfo_Get(handle, kind, index, key, 1, 0) or "" for key in MEDIAINFO_FIELDS[group] } for index in range(count) ] data[group] = ( rows[0] if group == "general" and rows else {} if group == "general" else rows ) print( json.dumps( safe_data(Plan("media-info", "mediainfo", [], 1), json.dumps(data)), ensure_ascii=True, ) ) return 0 finally: lib.MediaInfo_Close(handle) lib.MediaInfo_Delete(handle) except (ValidationError, ToolMissing, OSError, ValueError, AttributeError): # Never return exception text from native libraries or media metadata. print(json.dumps({"error_code": "mediainfo_library_or_input_unavailable"})) return 1 def execute(plan): result = { "execution_status": "not_started", "test_verdict": "not_evaluated", **plan.public(), } child_env = os.environ.copy() # FFREPORT can silently persist full RTSP command lines even with quiet logs. child_env.pop("FFREPORT", None) if "rtsp_env" in plan.metadata: child_env.pop(plan.metadata["rtsp_env"], None) try: completed = run_process( list(plan.argv), plan.tool_id, shell=False, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL if plan.command in ("capture", "serial") else subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding="utf-8", errors="replace", timeout=plan.timeout, check=False, env=child_env, ) result["returncode"] = completed.returncode result["execution_status"] = ( "completed" if completed.returncode == 0 else "failed" ) result["stderr_suppressed"] = True if completed.returncode == 0: try: result["data"] = safe_data(plan, completed.stdout or "") except (ValueError, TypeError, AttributeError, ET.ParseError): result["execution_status"] = "output_invalid" result["error_code"] = "tool_output_not_expected_format" else: result["error_code"] = "tool_returned_nonzero_sensitive_output_suppressed" except subprocess.TimeoutExpired: result.update( execution_status="timed_out", error_code="process_timeout_partial_output_suppressed", ) except PROCESS.ManagedProcessError as exc: status = ( "cleanup_unconfirmed" if exc.error_code == "process_cleanup_unconfirmed" else "output_limit" if exc.error_code == "process_output_limit" else "unknown" if exc.may_have_started else "launch_failed" ) result.update( execution_status=status, error_code=exc.error_code, may_have_started=exc.may_have_started, ) except OSError: # Includes config-directory cleanup after execution; absence of phase proof is unknown. result.update( execution_status="unknown", error_code="process_io_unconfirmed", may_have_started=True, ) return result def inventory(profile, roots, check_versions=False): validate_profile(profile) catalog = json.loads(CATALOG.read_text(encoding="utf-8")) rows = [] for entry in catalog["tools"]: row = dict(entry) tool_id = entry["tool_id"] if tool_id not in EXECUTABLES and tool_id != "mediainfo": row["availability"] = { "mcp": "mcp-check-required", "orchestrator": "orchestrator-context", }.get(entry.get("interface"), "manual-interface") else: try: prefix = command_prefix(tool_id, profile, roots) if tool_id == "mediainfo": row.update(availability="library-found", library=prefix[-1]) elif tool_id == "binwalk" and len(prefix) > 1: row.update( availability="python-module-configured", executable=prefix[0], module="binwalk", ) else: row.update(availability="executable-found", executable=prefix[0]) if check_versions: # PowerShell -Version requires a version argument; use help only. flag = ( "-?" if tool_id == "powershell" else VERSION_FLAGS.get(tool_id, "--version") ) try: proc = run_process( prefix + [flag], tool_id, shell=False, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=5, text=True, encoding="utf-8", errors="replace", check=False, ) first_line = next( ( line.strip() for line in (proc.stdout or "").splitlines() if line.strip() ), "", ) row["version_check"] = { "execution_status": "completed" if proc.returncode == 0 else "failed", "returncode": proc.returncode, "first_line": redact(first_line[:240]), } except (OSError, subprocess.TimeoutExpired): row["version_check"] = {"execution_status": "unavailable"} except ToolMissing: patterns = entry.get("package_name_patterns", []) found = next( ( p.name for p in root_candidates(roots) if any( fragment.lower() in p.name.lower() for fragment in patterns ) and p.suffix.lower() in (".zip", ".7z") ), None, ) row["availability"] = "package-only" if found else "missing" if found: row["package_filename"] = found rows.append(row) return { "execution_status": "completed", "test_verdict": "not_evaluated", "tools": rows, "note": "Executable discovery is not an installation, driver, privilege, connection or test-pass check.", } def main(argv=None): try: args = parse_args(argv) profile = load_profile(args.profile) if args.command in ("list", "doctor"): result = inventory( profile, args.tool_root, check_versions=bool( getattr(args, "check_versions", False) and not args.dry_run ), ) else: plan = build_plan(args, profile, os.environ) result = ( { "execution_status": "planned", "test_verdict": "not_evaluated", **plan.public(), } if args.dry_run else execute(plan) ) code = 0 if result["execution_status"] in ("planned", "completed") else 1 except ValidationError as error: result = { "execution_status": "invalid_arguments", "test_verdict": "not_evaluated", "error_code": str(error), } code = 2 except ToolMissing as error: result = { "execution_status": "tool_missing", "test_verdict": "not_evaluated", "tool_id": str(error), "error_code": "configure_trusted_executable_via_profile_tools_path_or_tool_root", } code = 3 except (OSError, ValueError): result = { "execution_status": "configuration_error", "test_verdict": "not_evaluated", "error_code": "local_file_unreadable_or_invalid", } code = 2 print(json.dumps(result, ensure_ascii=True, indent=2)) return code if __name__ == "__main__": sys.exit( media_info_worker(sys.argv[2:]) if sys.argv[1:2] == ["_media-info-worker"] else main() )