From feb292b492f30ad3fdc24a371a2852e5c02b4181 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Sat, 23 May 2026 14:51:21 +0000 Subject: [PATCH] linux memory, processes subsystem --- kernel_ai/contracts/api_contracts.py | 2 + kernel_ai/http/api_handlers/__init__.py | 2 + kernel_ai/http/api_handlers/kernel.py | 83 ++ kernel_ai/http/api_handlers/network_system.py | 60 + kernel_ai/http/api_handlers/processes.py | 84 ++ kernel_ai/http/api_handlers/security_logs.py | 46 + kernel_ai/logging_helpers.py | 74 ++ kernel_ai/sentry_helpers.py | 27 + kernel_ai/sentry_setup.py | 36 + kernel_ai/services/crypto/__init__.py | 6 + kernel_ai/services/crypto/crypto_pipeline.py | 234 ++++ kernel_ai/services/crypto/entropy.py | 105 ++ kernel_ai/services/crypto/helpers.py | 281 ++++ .../services/crypto/security_pipeline.py | 356 ++++++ kernel_ai/services/processes_runtime.py | 225 +++- static/js/memory-belt.js | 106 +- static/js/processes-belt.js | 1135 +++++++++++++++-- templates/linux-memory-subsystem.html | 2 +- templates/linux-processes-subsystem.html | 2 +- 19 files changed, 2750 insertions(+), 116 deletions(-) create mode 100644 kernel_ai/http/api_handlers/__init__.py create mode 100644 kernel_ai/http/api_handlers/kernel.py create mode 100644 kernel_ai/http/api_handlers/network_system.py create mode 100644 kernel_ai/http/api_handlers/processes.py create mode 100644 kernel_ai/http/api_handlers/security_logs.py create mode 100644 kernel_ai/logging_helpers.py create mode 100644 kernel_ai/sentry_helpers.py create mode 100644 kernel_ai/sentry_setup.py create mode 100644 kernel_ai/services/crypto/__init__.py create mode 100644 kernel_ai/services/crypto/crypto_pipeline.py create mode 100644 kernel_ai/services/crypto/entropy.py create mode 100644 kernel_ai/services/crypto/helpers.py create mode 100644 kernel_ai/services/crypto/security_pipeline.py diff --git a/kernel_ai/contracts/api_contracts.py b/kernel_ai/contracts/api_contracts.py index d994d37..155e21f 100644 --- a/kernel_ai/contracts/api_contracts.py +++ b/kernel_ai/contracts/api_contracts.py @@ -52,6 +52,7 @@ class ProcessesRealtimeResponse(TypedDict): syscalls_interception: list network_tracing: list security_hooks: list + semantic_ops: list neural_graph: dict memory_visual: dict meta: dict @@ -161,6 +162,7 @@ def validate_processes_realtime_response(payload: Any) -> None: _expect_key(data, "syscalls_interception", list, "processes_realtime") _expect_key(data, "network_tracing", list, "processes_realtime") _expect_key(data, "security_hooks", list, "processes_realtime") + _expect_key(data, "semantic_ops", list, "processes_realtime") _expect_key(data, "neural_graph", dict, "processes_realtime") _expect_key(data, "memory_visual", dict, "processes_realtime") _expect_key(data, "meta", dict, "processes_realtime") diff --git a/kernel_ai/http/api_handlers/__init__.py b/kernel_ai/http/api_handlers/__init__.py new file mode 100644 index 0000000..4800172 --- /dev/null +++ b/kernel_ai/http/api_handlers/__init__.py @@ -0,0 +1,2 @@ +"""Domain-oriented API handler modules.""" + diff --git a/kernel_ai/http/api_handlers/kernel.py b/kernel_ai/http/api_handlers/kernel.py new file mode 100644 index 0000000..54aec50 --- /dev/null +++ b/kernel_ai/http/api_handlers/kernel.py @@ -0,0 +1,83 @@ +"""Kernel/observability API handlers.""" + +from datetime import datetime + +import psutil +from flask import current_app, request +from flask import jsonify + +from kernel_ai.http.common import api_json, build_error_payload +from kernel_ai.sentry_helpers import capture_exception +from kernel_ai.services import core_observability as _core_observability_service +from kernel_ai.services import telemetry_orchestration as _telemetry +from kernel_ai.state import get_state_container + + +def syscalls_realtime(): + return api_json( + lambda: { + "timestamp": datetime.now().isoformat(), + "syscalls": _telemetry.get_real_system_calls(), + "cpu_usage": psutil.cpu_percent(interval=1), + "memory_usage": psutil.virtual_memory().percent, + "system_info": _core_observability_service.get_system_info(), + } + ) + + +def kernel_data(): + return api_json( + lambda: { + "timestamp": datetime.now().isoformat(), + "syscalls": _telemetry.get_real_system_calls(), + "subsystems": _telemetry.get_kernel_subsystem_status(), + "processes": len(psutil.pids()), + "system_stats": { + "cpu_count": psutil.cpu_count(), + "memory_total": psutil.virtual_memory().total, + "disk_usage": psutil.disk_usage("/").percent, + }, + } + ) + + +def process_kernel_map(): + return api_json(_telemetry.get_process_kernel_map) + + +def nginx_files(): + return api_json(lambda: {"files": _telemetry.get_nginx_open_files()}) + + +def get_execution_context(): + return api_json( + lambda: _telemetry.get_execution_context_data( + exec_context_prev=get_state_container(current_app).exec_context_prev + ) + ) + + +def kernel_dna(): + return api_json(_telemetry.get_kernel_dna_data) + + +def sentry_test(): + """Temporary endpoint for manual Sentry verification.""" + if not current_app.config.get("SENTRY_TEST_ENDPOINT_ENABLED", False): + return jsonify(build_error_payload("Not found", "not_found")), 404 + + expected_token = current_app.config.get("SENTRY_TEST_ENDPOINT_TOKEN", "") + incoming_token = (request.headers.get("X-Sentry-Test-Token") or "").strip() + if expected_token and incoming_token != expected_token: + return jsonify(build_error_payload("Forbidden", "forbidden")), 403 + + mode = (request.args.get("mode") or "capture").strip().lower() + if mode == "raise": + raise RuntimeError("Manual sentry raise test") + + try: + raise RuntimeError("Manual sentry capture test") + except RuntimeError as exc: + capture_exception(exc, where="api.sentry_test", extra={"mode": mode}) + + return jsonify({"ok": True, "mode": mode, "message": "Sentry event captured"}) diff --git a/kernel_ai/http/api_handlers/network_system.py b/kernel_ai/http/api_handlers/network_system.py new file mode 100644 index 0000000..fd71b2a --- /dev/null +++ b/kernel_ai/http/api_handlers/network_system.py @@ -0,0 +1,60 @@ +"""Network/system API handlers.""" + +from flask import current_app, request + +from kernel_ai.collectors import proc_fs as _proc_fs +from kernel_ai.http.common import api_json +from kernel_ai.services import devices as _devices_service +from kernel_ai.services import network as _network_service +from kernel_ai.services import system_view as _system_view_service +from kernel_ai.state import get_state_container + + +def active_connections(): + return api_json(lambda: {"connections": _network_service.get_active_connections()}) + + +def traceroute_info(): + def _payload(): + state = get_state_container(current_app) + remote_ip = request.args.get("ip", "").strip() + if not remote_ip: + raise ValueError("Missing 'ip' query parameter") + return _network_service.get_traceroute_info( + remote_ip, + traceroute_cache=state.traceroute_cache, + cache_ttl_seconds=state.traceroute_cache_ttl_seconds, + ) + + return api_json(_payload, exception_statuses=[(ValueError, 400)]) + + +def network_stack_realtime(): + return api_json( + lambda: _network_service.get_network_stack_realtime( + network_stack_prev=get_state_container(current_app).network_stack_prev + ) + ) + + +def devices_realtime(): + return api_json( + lambda: _devices_service.get_devices_realtime( + devices_prev=get_state_container(current_app).devices_prev, + read_diskstats_fn=_proc_fs.read_diskstats, + read_interrupt_lines_fn=_proc_fs.read_interrupt_lines, + read_tty_irq_total_fn=_proc_fs.read_tty_irq_total, + ) + ) + + +def filesystem_blocks(): + return api_json( + lambda: _system_view_service.get_filesystem_blocks( + filesystem_prev=get_state_container(current_app).filesystem_prev + ) + ) + + +def isolation_context(): + return api_json(_system_view_service.get_isolation_context) diff --git a/kernel_ai/http/api_handlers/processes.py b/kernel_ai/http/api_handlers/processes.py new file mode 100644 index 0000000..4586546 --- /dev/null +++ b/kernel_ai/http/api_handlers/processes.py @@ -0,0 +1,84 @@ +"""Process-centric API handlers.""" + +from datetime import datetime +from flask import request + +from kernel_ai.http.common import api_json +from kernel_ai.services import process_inspect as _process_inspect_service +from kernel_ai.services import process_timeline as _process_timeline_service +from kernel_ai.services import processes as _processes_service + + +def get_processes(): + return api_json(lambda: {"processes": _processes_service.get_processes_basic_data()}) + + +def get_process_threads(pid): + return api_json(lambda: _process_inspect_service.get_process_threads_info(pid)) + + +def get_process_cpu(pid): + return api_json(lambda: _process_inspect_service.get_process_cpu_info(pid)) + + +def get_process_fds(pid): + return api_json(lambda: _process_inspect_service.get_process_fds_info(pid)) + + +def get_processes_detailed(): + return api_json(lambda: {"processes": _processes_service.get_processes_detailed_data()}) + + +def get_ipc_links(): + def _payload(): + max_pairs = request.args.get("max_pairs", default=120, type=int) + max_nodes = request.args.get("max_nodes", default=24, type=int) + max_pairs = max(20, min(300, max_pairs)) + max_nodes = max(8, min(64, max_nodes)) + return _process_inspect_service.get_ipc_links_summary(max_pairs=max_pairs, max_nodes=max_nodes) + + return api_json(_payload) + + +def get_proc_matrix(): + def _payload(): + matrix = _processes_service.get_proc_matrix_data() + return {"matrix": matrix, "timestamp": datetime.now().isoformat()} + + return api_json(_payload) + + +def get_proc_timeline(): + return api_json( + lambda: _process_timeline_service.get_proc_timeline_data( + request.args.get("pid", type=int), + window_s=request.args.get("window_s", default=30, type=int), + ), + exception_statuses=[(ValueError, 400), (ProcessLookupError, 404)], + ) + + +def get_proc_timeline_branches(): + def _payload(): + limit = request.args.get("limit", default=6, type=int) + events = request.args.get("events", default=10, type=int) + window_s = request.args.get("window_s", default=30, type=int) + return _process_timeline_service.get_proc_timeline_branches_data( + limit=limit, + events=events, + window_s=window_s, + ) + + return api_json(_payload) + + +def processes_realtime(): + return api_json(_processes_service.collect_processes_realtime) + + +def get_proc_graph(): + return api_json(_processes_service.get_proc_graph_data, error_extra={"nodes": [], "edges": []}) + + +def get_process_files(): + return api_json(lambda: {"curves": [], "timestamp": datetime.now().isoformat()}, error_extra={"curves": []}) diff --git a/kernel_ai/http/api_handlers/security_logs.py b/kernel_ai/http/api_handlers/security_logs.py new file mode 100644 index 0000000..0fcdbe5 --- /dev/null +++ b/kernel_ai/http/api_handlers/security_logs.py @@ -0,0 +1,46 @@ +"""Security/crypto/frontend-log API handlers.""" + +from flask import current_app, jsonify, request + +from kernel_ai.http.common import api_json +from kernel_ai.services import crypto_security as _crypto_security_service +from kernel_ai.services import frontend_logs as _frontend_logs_service +from kernel_ai.state import get_state_container + + +def _write_frontend_event(event_payload): + state = get_state_container(current_app) + _frontend_logs_service.write_frontend_event( + event_payload=event_payload, + frontend_log_file=state.frontend_log_file, + write_lock=state.frontend_log_write_lock, + ) + + +def crypto_realtime(): + state = get_state_container(current_app) + return api_json( + lambda: _crypto_security_service.collect_crypto_realtime( + crypto_prev=state.crypto_prev, + entropy_prev=state.entropy_prev, + ) + ) + + +def security_realtime(): + state = get_state_container(current_app) + return api_json( + lambda: _crypto_security_service.collect_security_realtime( + security_prev=state.security_prev + ) + ) + + +def ingest_frontend_logs(): + if request.method == "OPTIONS": + return ("", 204) + response_payload, status_code = _frontend_logs_service.ingest_frontend_logs_payload( + payload=request.get_json(silent=True), + write_event_fn=_write_frontend_event, + ) + return jsonify(response_payload), status_code diff --git a/kernel_ai/logging_helpers.py b/kernel_ai/logging_helpers.py new file mode 100644 index 0000000..a0e41dc --- /dev/null +++ b/kernel_ai/logging_helpers.py @@ -0,0 +1,74 @@ +"""Shared backend logging helpers.""" + +from __future__ import annotations + +import logging +from collections.abc import Mapping, Sequence + +try: + from flask import g, has_request_context +except ImportError: # pragma: no cover + g = None + + def has_request_context() -> bool: # type: ignore[override] + return False + + +_SENSITIVE_KEYS = {"authorization", "cookie", "set-cookie", "password", "passwd", "secret", "token", "api_key"} +_MASK = "***" +_MAX_STR_LEN = 512 + + +def _truncate_string(value: str, max_len: int = _MAX_STR_LEN) -> str: + if len(value) <= max_len: + return value + return value[:max_len] + "..." + + +def sanitize_payload(value): + """Recursively mask sensitive fields and cap string lengths.""" + if isinstance(value, str): + return _truncate_string(value) + if isinstance(value, Mapping): + out = {} + for key, val in value.items(): + key_str = str(key) + if key_str.lower() in _SENSITIVE_KEYS: + out[key_str] = _MASK + else: + out[key_str] = sanitize_payload(val) + return out + if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)): + return [sanitize_payload(x) for x in value] + return value + + +def log_event( + logger: logging.Logger, + level: int | str, + message: str, + *, + event_dataset: str | None = None, + component: str | None = None, + operation: str | None = None, + event_data: dict | None = None, +) -> None: + """Emit structured event with sanitized context.""" + if isinstance(level, str): + level_int = getattr(logging, level.upper(), logging.INFO) + else: + level_int = int(level) + + payload = sanitize_payload(event_data or {}) + if event_dataset: + payload.setdefault("event.dataset", event_dataset) + if component: + payload.setdefault("component", component) + if operation: + payload.setdefault("operation", operation) + if has_request_context() and g is not None: + request_id = getattr(g, "request_id", None) + if request_id: + payload.setdefault("request.id", request_id) + + logger.log(level_int, message, extra={"event_data": payload}) diff --git a/kernel_ai/sentry_helpers.py b/kernel_ai/sentry_helpers.py new file mode 100644 index 0000000..0dfa588 --- /dev/null +++ b/kernel_ai/sentry_helpers.py @@ -0,0 +1,27 @@ +"""Lightweight helpers for optional Sentry reporting.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +def capture_exception(exc: BaseException, *, where: str | None = None, extra: Mapping | None = None) -> None: + """Send an exception to Sentry if SDK is available/initialized. + + This helper never raises to keep exception paths safe. + """ + try: + import sentry_sdk + except Exception: + return + + try: + with sentry_sdk.push_scope() as scope: + if where: + scope.set_tag("where", str(where)) + if extra: + for key, value in extra.items(): + scope.set_extra(str(key), value) + sentry_sdk.capture_exception(exc) + except Exception: + return diff --git a/kernel_ai/sentry_setup.py b/kernel_ai/sentry_setup.py new file mode 100644 index 0000000..9fef07c --- /dev/null +++ b/kernel_ai/sentry_setup.py @@ -0,0 +1,36 @@ +"""Sentry initialization helpers for Flask app.""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +def init_sentry(app) -> bool: + """Initialize Sentry from app config. + + Returns True when initialization succeeds and False otherwise. + """ + dsn = str(app.config.get("SENTRY_DSN", "") or "").strip() + if not dsn: + logger.info("Sentry disabled: SENTRY_DSN is empty") + return False + + try: + import sentry_sdk + from sentry_sdk.integrations.flask import FlaskIntegration + except Exception as exc: # pragma: no cover - import failure is deployment issue + logger.warning("Sentry SDK not available: %s", exc) + return False + + sentry_sdk.init( + dsn=dsn, + integrations=[FlaskIntegration()], + environment=app.config.get("SENTRY_ENVIRONMENT"), + release=app.config.get("SENTRY_RELEASE"), + send_default_pii=bool(app.config.get("SENTRY_SEND_DEFAULT_PII", False)), + traces_sample_rate=float(app.config.get("SENTRY_TRACES_SAMPLE_RATE", 0.0)), + ) + logger.info("Sentry initialized") + return True diff --git a/kernel_ai/services/crypto/__init__.py b/kernel_ai/services/crypto/__init__.py new file mode 100644 index 0000000..5f2c752 --- /dev/null +++ b/kernel_ai/services/crypto/__init__.py @@ -0,0 +1,6 @@ +"""Crypto/security service submodules.""" + +from kernel_ai.services.crypto import crypto_pipeline, entropy, helpers, security_pipeline + +__all__ = ["helpers", "entropy", "crypto_pipeline", "security_pipeline"] + diff --git a/kernel_ai/services/crypto/crypto_pipeline.py b/kernel_ai/services/crypto/crypto_pipeline.py new file mode 100644 index 0000000..0895662 --- /dev/null +++ b/kernel_ai/services/crypto/crypto_pipeline.py @@ -0,0 +1,234 @@ +"""Crypto telemetry pipeline extracted from legacy service.""" + +from __future__ import annotations + +import time +from datetime import datetime + +from kernel_ai.logging_helpers import log_event + + +def collect_crypto_realtime(crypto_prev, callbacks=None, psutil_module=None, logger=None): + """ + Build a near-realtime list of processes likely interacting with kernel crypto. + This is heuristic-based and derived from active network/process context. + """ + callbacks = callbacks or {} + psutil_module = psutil_module or __import__("psutil") + infer_crypto_protocol_fn = callbacks.get("infer_crypto_protocol", lambda _lp, _rp, _name: ("Crypto API", "AES/SHA")) + is_likely_crypto_actor_fn = callbacks.get("is_likely_crypto_actor", lambda *_args: True) + infer_tls_terminator_fn = callbacks.get("infer_tls_terminator", lambda _name, _port, _proto, _listeners: "n/a") + collect_algorithm_competition_fn = callbacks.get("collect_algorithm_competition", lambda _algo: {"request": "AES", "implementations": [], "selected": None}) + parse_proc_crypto_entries_fn = callbacks.get("parse_proc_crypto_entries", lambda: []) + collect_kernel_crypto_clients_fn = callbacks.get("collect_kernel_crypto_clients", lambda _items: []) + collect_hw_offload_status_fn = callbacks.get("collect_hw_offload_status", lambda _entries, _comps: []) + collect_sync_async_queue_fn = callbacks.get("collect_sync_async_queue", lambda _items: {}) + collect_algorithm_requesters_fn = callbacks.get("collect_algorithm_requesters", lambda _items, _clients: {"aes": [], "sha": [], "chacha20": []}) + build_crypto_decision_pipelines_fn = callbacks.get("build_crypto_decision_pipelines", lambda **_kwargs: {}) + collect_entropy_cloud_status_fn = callbacks.get("collect_entropy_cloud_status", lambda: {}) + + items = [] + tls_listener_by_port = {} + tls_listener_names = set() + unknown_pid_flows = 0 + + try: + connections = psutil_module.net_connections(kind="inet") + except psutil_module.Error as exc: + if logger: + log_event( + logger, + "DEBUG", + "Failed to read net connections for crypto realtime", + event_dataset="kernel_ai.app", + component="services.crypto.crypto_pipeline", + operation="collect_crypto_realtime", + event_data={"error": str(exc)}, + ) + connections = [] + + tls_ports = {443, 8443, 9443, 6443} + for conn in connections: + status = str(getattr(conn, "status", "") or "") + if status != "LISTEN": + continue + laddr = getattr(conn, "laddr", None) + local_port = getattr(laddr, "port", 0) if laddr else 0 + if int(local_port or 0) not in tls_ports: + continue + pid = getattr(conn, "pid", None) + pid_i = int(pid or 0) + process_name = "unknown" + if pid_i: + try: + process_name = psutil_module.Process(pid_i).name().lower() + except psutil_module.Error: + process_name = f"pid-{pid_i}" + tls_listener_by_port[int(local_port)] = {"pid": pid_i, "process": process_name} + tls_listener_names.add(process_name) + items.append( + { + "process": process_name, + "pid": pid_i, + "protocol": "TLS", + "algorithm": "AES-GCM/SHA256", + "endpoint": f"0.0.0.0:{int(local_port)}", + "local_port": int(local_port), + "remote_port": 0, + "status": "LISTEN", + "tls_terminator": process_name, + "source_kind": "listener", + } + ) + + for conn in connections: + pid = getattr(conn, "pid", None) + status = str(getattr(conn, "status", "") or "") + if status not in ("ESTABLISHED", "SYN_SENT", "SYN_RECV"): + continue + + laddr = getattr(conn, "laddr", None) + raddr = getattr(conn, "raddr", None) + local_ip = getattr(laddr, "ip", "") if laddr else "" + local_port = getattr(laddr, "port", 0) if laddr else 0 + remote_ip = getattr(raddr, "ip", "") if raddr else "" + remote_port = getattr(raddr, "port", 0) if raddr else 0 + + pid_i = int(pid or 0) + process_name = "unknown" + if pid_i: + try: + proc = psutil_module.Process(pid_i) + process_name = proc.name() + except psutil_module.Error: + process_name = f"pid-{pid_i}" + else: + unknown_pid_flows += 1 + listener_meta = tls_listener_by_port.get(int(local_port or 0)) + if listener_meta: + process_name = listener_meta.get("process") or "unknown" + + protocol, algorithm = infer_crypto_protocol_fn(local_port, remote_port, process_name) + if not is_likely_crypto_actor_fn(process_name, local_port, remote_port, protocol): + continue + + tls_terminator = infer_tls_terminator_fn(process_name, local_port, protocol, tls_listener_names) + endpoint = f"{remote_ip}:{remote_port}" if remote_ip else f"{local_ip}:{local_port}" + + items.append( + { + "process": process_name.lower(), + "pid": pid_i, + "protocol": protocol, + "algorithm": algorithm, + "endpoint": endpoint, + "local_port": int(local_port or 0), + "remote_port": int(remote_port or 0), + "status": status, + "tls_terminator": tls_terminator, + "source_kind": "connection", + } + ) + + if not items: + for proc in psutil_module.process_iter(attrs=["pid", "name"]): + try: + name = str(proc.info.get("name", "")).lower() + except (psutil_module.Error, KeyError, TypeError): + continue + if any(token in name for token in ["nginx", "sshd", "curl", "openssl", "kube", "vpn", "python"]): + protocol, algorithm = infer_crypto_protocol_fn(0, 0, name) + items.append( + { + "process": name, + "pid": int(proc.info.get("pid") or 0), + "protocol": protocol, + "algorithm": algorithm, + "endpoint": "-", + "local_port": 0, + "remote_port": 0, + "status": "RUNNING", + "tls_terminator": "n/a", + "source_kind": "process", + } + ) + if len(items) >= 12: + break + + deduped = {} + for item in items: + key = ( + item.get("process"), + int(item.get("pid") or 0), + item.get("protocol"), + item.get("algorithm"), + item.get("endpoint"), + item.get("status"), + item.get("source_kind"), + ) + if key not in deduped: + deduped[key] = item + items = list(deduped.values()) + + now = time.time() + prev_ts = crypto_prev["timestamp"] + prev_flows = crypto_prev["active_flows"] + active_flows = len(items) + crypto_prev["timestamp"] = now + crypto_prev["active_flows"] = active_flows + + if prev_ts: + dt = max(now - prev_ts, 0.001) + flow_delta = abs(active_flows - prev_flows) + ops_per_sec = round((active_flows * 90) + (flow_delta / dt) * 60, 2) + else: + ops_per_sec = round(active_flows * 90, 2) + + unique_processes = [] + for item in items: + p = item["process"] + if p not in unique_processes: + unique_processes.append(p) + + algorithm_competitions = { + "aes": collect_algorithm_competition_fn("aes"), + "sha": collect_algorithm_competition_fn("sha"), + "chacha20": collect_algorithm_competition_fn("chacha20"), + } + proc_crypto_entries = parse_proc_crypto_entries_fn() + kernel_clients = collect_kernel_crypto_clients_fn(items) + hw_offload = collect_hw_offload_status_fn(proc_crypto_entries, algorithm_competitions) + crypto_stage1 = { + "kernel_clients": kernel_clients, + "sync_async": collect_sync_async_queue_fn(items), + "hw_offload": hw_offload, + } + algorithm_requesters = collect_algorithm_requesters_fn(items, kernel_clients) + crypto_decision_pipelines = build_crypto_decision_pipelines_fn( + algorithm_competitions=algorithm_competitions, + kernel_clients=kernel_clients, + hw_offload=hw_offload, + algorithm_requesters=algorithm_requesters, + ) + entropy_cloud = collect_entropy_cloud_status_fn() + + return { + "items": items[:24], + "processes": unique_processes[:16], + "meta": { + "ops_per_sec": ops_per_sec, + "tls_sessions": sum(1 for i in items if i.get("protocol") == "TLS"), + "active_flows": active_flows, + "unknown_pid_flows": int(unknown_pid_flows), + "tls_terminators": sorted(list(tls_listener_names))[:8], + "algorithm_competition": algorithm_competitions["aes"], + "algorithm_competitions": algorithm_competitions, + "algorithm_requesters": algorithm_requesters, + "crypto_stage1": crypto_stage1, + "entropy_cloud": entropy_cloud, + "crypto_decision_pipeline": crypto_decision_pipelines.get("aes", {}), + "crypto_decision_pipelines": crypto_decision_pipelines, + "source": "live-heuristic-v2", + "timestamp": datetime.utcnow().isoformat() + "Z", + }, + } diff --git a/kernel_ai/services/crypto/entropy.py b/kernel_ai/services/crypto/entropy.py new file mode 100644 index 0000000..8856a8e --- /dev/null +++ b/kernel_ai/services/crypto/entropy.py @@ -0,0 +1,105 @@ +"""Entropy-related helpers for crypto subsystem.""" + +from __future__ import annotations + +import logging +import time + +import psutil + +logger = logging.getLogger(__name__) + + +def read_sysctl_int(path, default=0): + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + raw = f.read().strip() + return int(raw or default) + except (OSError, ValueError, TypeError): + return int(default) + + +def read_proc_interrupt_total(): + try: + with open("/proc/stat", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + if line.startswith("intr "): + parts = line.strip().split() + if len(parts) >= 2: + return int(parts[1]) + except (OSError, ValueError): + return 0 + return 0 + + +def collect_entropy_cloud_status(entropy_prev): + """Collect Linux random subsystem entropy status and source activity.""" + now = time.time() + entropy_bits = read_sysctl_int("/proc/sys/kernel/random/entropy_avail", 0) + pool_size_bits = read_sysctl_int("/proc/sys/kernel/random/poolsize", 256) + read_threshold = read_sysctl_int("/proc/sys/kernel/random/read_wakeup_threshold", 128) + write_threshold = read_sysctl_int("/proc/sys/kernel/random/write_wakeup_threshold", 64) + try: + disk = psutil.disk_io_counters() + except psutil.Error: + disk = None + try: + net = psutil.net_io_counters() + except psutil.Error: + net = None + intr_total = read_proc_interrupt_total() + prev_ts = entropy_prev.get("timestamp") + dt = max(now - prev_ts, 0.001) if prev_ts else None + disk_read_now = int(getattr(disk, "read_bytes", 0) or 0) + disk_write_now = int(getattr(disk, "write_bytes", 0) or 0) + net_sent_now = int(getattr(net, "bytes_sent", 0) or 0) + net_recv_now = int(getattr(net, "bytes_recv", 0) or 0) + if dt: + disk_delta = max((disk_read_now - int(entropy_prev.get("disk_read_bytes") or disk_read_now)) + (disk_write_now - int(entropy_prev.get("disk_write_bytes") or disk_write_now)), 0) + net_delta = max((net_sent_now - int(entropy_prev.get("net_sent_bytes") or net_sent_now)) + (net_recv_now - int(entropy_prev.get("net_recv_bytes") or net_recv_now)), 0) + intr_delta = max(intr_total - int(entropy_prev.get("interrupt_total") or intr_total), 0) + else: + disk_delta = 0 + net_delta = 0 + intr_delta = 0 + entropy_prev["timestamp"] = now + entropy_prev["disk_read_bytes"] = disk_read_now + entropy_prev["disk_write_bytes"] = disk_write_now + entropy_prev["net_sent_bytes"] = net_sent_now + entropy_prev["net_recv_bytes"] = net_recv_now + entropy_prev["interrupt_total"] = intr_total + + def scale_intensity(rate_value, scale): + return int(max(0, min(100, (float(rate_value) / float(scale)) * 100.0))) + + disk_rate = (disk_delta / dt) if dt else 0 + net_rate = (net_delta / dt) if dt else 0 + intr_rate = (intr_delta / dt) if dt else 0 + irq_intensity = scale_intensity(intr_rate, 25000) + disk_intensity = scale_intensity(disk_rate, 80 * 1024 * 1024) + net_intensity = scale_intensity(net_rate, 120 * 1024 * 1024) + hwrng_intensity = 68 if entropy_bits > max(read_threshold, 128) else 34 + sources = [ + {"source": "interrupt timing", "intensity": irq_intensity, "status": "active" if irq_intensity >= 25 else "low"}, + {"source": "disk IO", "intensity": disk_intensity, "status": "active" if disk_intensity >= 18 else "low"}, + {"source": "network timing", "intensity": net_intensity, "status": "active" if net_intensity >= 18 else "low"}, + {"source": "hardware RNG", "intensity": hwrng_intensity, "status": "active" if hwrng_intensity >= 50 else "limited"}, + ] + source_avg = int(sum(s["intensity"] for s in sources) / max(len(sources), 1)) + entropy_pct = max(0.0, min(1.0, float(entropy_bits) / max(float(pool_size_bits), 1.0))) + particle_density = max(16, min(84, int(18 + entropy_pct * 42 + source_avg * 0.35))) + key_birth_rate = round(0.6 + entropy_pct * 9.4 + source_avg * 0.06, 2) + crng_state = "ready" if entropy_bits >= max(read_threshold, 128) else "warming" + random_state = "stable" if entropy_bits >= max(write_threshold, 64) else "refilling" + return { + "entropy_pool_bits": int(entropy_bits), + "entropy_pool_size_bits": int(pool_size_bits), + "crng_state": crng_state, + "random_subsystem_state": random_state, + "particle_density": int(particle_density), + "key_birth_rate_est": float(key_birth_rate), + "sources": sources, + "read_wakeup_threshold": int(read_threshold), + "write_wakeup_threshold": int(write_threshold), + "mode": "live-heuristic", + } diff --git a/kernel_ai/services/crypto/helpers.py b/kernel_ai/services/crypto/helpers.py new file mode 100644 index 0000000..93de235 --- /dev/null +++ b/kernel_ai/services/crypto/helpers.py @@ -0,0 +1,281 @@ +"""Helper functions for crypto telemetry pipelines.""" + +from __future__ import annotations + + +def infer_crypto_protocol(local_port, remote_port, process_name): + """Infer protocol/algorithm pair from ports and process hints.""" + ports = {int(local_port or 0), int(remote_port or 0)} + p_name = (process_name or "").lower() + if 22 in ports or "ssh" in p_name: + return "SSH", "Curve25519/ChaCha20-Poly1305" + if 51820 in ports or "wireguard" in p_name or p_name.startswith("wg"): + return "WireGuard", "ChaCha20-Poly1305" + tls_ports = {443, 465, 636, 853, 993, 995, 8443, 9443, 6443, 2376} + if ports & tls_ports or any(x in p_name for x in ["nginx", "haproxy", "curl", "wget", "openssl", "stunnel", "traefik"]): + return "TLS", "AES-GCM/SHA256" + return "Crypto API", "AES/SHA" + + +def is_likely_crypto_actor(process_name, local_port, remote_port, protocol): + """Heuristic gate to avoid flooding with unrelated sockets.""" + p_name = (process_name or "").lower() + interesting_ports = {22, 443, 465, 636, 853, 993, 995, 2376, 6443, 8443, 9443, 51820} + process_tokens = [ + "nginx", + "haproxy", + "envoy", + "caddy", + "apache", + "httpd", + "traefik", + "sshd", + "ssh", + "wg", + "wireguard", + "openssl", + "stunnel", + "curl", + "wget", + "python", + "gunicorn", + "uvicorn", + ] + if protocol in ("TLS", "SSH", "WireGuard"): + return True + if int(local_port or 0) in interesting_ports or int(remote_port or 0) in interesting_ports: + return True + return any(token in p_name for token in process_tokens) + + +def infer_tls_terminator(process_name, local_port, protocol, tls_listener_names): + """Guess where TLS termination happens.""" + if protocol != "TLS": + return "n/a" + p_name = (process_name or "").lower() + if p_name and p_name != "unknown": + return p_name + if int(local_port or 0) in {443, 8443, 9443, 6443} and tls_listener_names: + top = next(iter(tls_listener_names)) + return f"listener:{top}" + if int(local_port or 0) in {443, 8443, 9443, 6443}: + return "unknown" + return "upstream-or-external-lb" + + +def parse_proc_crypto_entries(): + """Parse /proc/crypto into a list of dict entries.""" + entries = [] + try: + with open("/proc/crypto", "r", encoding="utf-8", errors="ignore") as f: + raw = f.read() + except OSError: + return entries + + blocks = [block.strip() for block in raw.split("\n\n") if block.strip()] + for block in blocks: + item = {} + for line in block.splitlines(): + if ":" not in line: + continue + key, value = line.split(":", 1) + item[key.strip().lower()] = value.strip() + if item: + entries.append(item) + return entries + + +def collect_algorithm_competition(requested_algorithm="aes"): + """Build algorithm competition using kernel crypto registry.""" + entries = parse_proc_crypto_entries() + requested = (requested_algorithm or "aes").lower() + req_type_allow = {"aes": {"skcipher", "aead", "cipher"}, "sha": {"shash", "ahash", "hash"}, "chacha20": {"skcipher", "aead", "cipher"}} + req_tokens = {"aes": ["aes"], "sha": ["sha"], "chacha20": ["chacha20", "xchacha20", "chacha"]} + allowed_types = req_type_allow.get(requested, {"skcipher", "aead", "cipher", "shash", "ahash", "hash"}) + tokens = req_tokens.get(requested, [requested]) + candidates = [] + + for entry in entries: + name = str(entry.get("name", "")).lower() + driver = str(entry.get("driver", "")).lower() + alg_type = str(entry.get("type", "")).lower() + if not any(token in name or token in driver for token in tokens): + continue + if alg_type and alg_type not in allowed_types: + continue + try: + priority = int(entry.get("priority", "0") or 0) + except ValueError: + priority = 0 + impl_name = driver or name or "unknown-impl" + candidates.append({"name": impl_name, "priority": priority, "type": alg_type or "unknown", "source": "kernel"}) + + dedup = {} + for item in candidates: + existing = dedup.get(item["name"]) + if existing is None or item["priority"] > existing["priority"]: + dedup[item["name"]] = item + candidates = list(dedup.values()) + candidates.sort(key=lambda x: x["priority"], reverse=True) + + if not candidates: + fallback_map = { + "aes": [{"name": "aesni-intel", "priority": 300, "type": "skcipher", "source": "mock"}, {"name": "aes-avx", "priority": 200, "type": "skcipher", "source": "mock"}, {"name": "aes-generic", "priority": 100, "type": "skcipher", "source": "mock"}], + "sha": [{"name": "sha256-avx2", "priority": 240, "type": "shash", "source": "mock"}, {"name": "sha256-ssse3", "priority": 180, "type": "shash", "source": "mock"}, {"name": "sha256-generic", "priority": 100, "type": "shash", "source": "mock"}], + "chacha20": [{"name": "chacha20-neon", "priority": 260, "type": "skcipher", "source": "mock"}, {"name": "chacha20-simd", "priority": 220, "type": "skcipher", "source": "mock"}, {"name": "chacha20-generic", "priority": 100, "type": "skcipher", "source": "mock"}], + } + candidates = fallback_map.get(requested, fallback_map["aes"]) + + selected = candidates[0] if candidates else None + return {"request": requested.upper(), "implementations": candidates[:8], "selected": selected, "selection_policy": "max-priority"} + + +def collect_kernel_crypto_clients(items): + """Infer major kernel crypto clients from active context.""" + client_rules = [ + ("kTLS", ["nginx", "haproxy", "envoy", "caddy", "apache", "httpd", "traefik"], "TLS"), + ("WireGuard", ["wg", "wireguard"], "WireGuard"), + ("IPsec/XFRM", ["charon", "strongswan", "ipsec", "racoon"], "TLS"), + ("dm-crypt", ["cryptsetup", "dmcrypt", "luks"], "CRYPTO API"), + ("fscrypt", ["fscrypt"], "CRYPTO API"), + ("AF_ALG", ["openssl", "python", "curl", "wget"], "CRYPTO API"), + ] + results = [] + lowered_items = [{"process": str(item.get("process", "")).lower(), "protocol": str(item.get("protocol", ""))} for item in items] + for name, tokens, proto_hint in client_rules: + flows = 0 + for item in lowered_items: + proc = item["process"] + proto = item["protocol"] + if any(token in proc for token in tokens): + flows += 1 + elif proto_hint and proto == proto_hint: + flows += 1 + status = "active" if flows > 0 else "idle" + results.append({"name": name, "status": status, "active_flows": int(flows)}) + return results + + +def collect_sync_async_queue(items): + """Estimate sync/async crypto execution pressure from active flows.""" + active_items = [i for i in items if str(i.get("status", "")).upper() != "LISTEN"] + async_items = [i for i in active_items if str(i.get("source_kind", "")) == "connection" or str(i.get("protocol", "")).upper() in {"TLS", "WIREGUARD", "SSH"}] + sync_items = max(len(active_items) - len(async_items), 0) + sum(1 for i in items if str(i.get("source_kind", "")) == "process") + queue_depth = max(len(async_items) - 1, 0) + queue_latency_ms = round(0.35 + min(5.5, queue_depth * 0.42 + len(active_items) * 0.08), 2) + return {"sync_ops_est": int(sync_items), "async_ops_est": int(len(async_items)), "queue_depth_est": int(queue_depth), "queue_latency_ms_est": queue_latency_ms, "mode": "heuristic"} + + +def collect_hw_offload_status(entries, algorithm_competitions): + """Estimate hardware acceleration availability from /proc/crypto drivers.""" + names = [] + for entry in entries: + n = str(entry.get("name", "")).lower() + d = str(entry.get("driver", "")).lower() + if n: + names.append(n) + if d: + names.append(d) + + def has_token(tokens): + return any(any(token in item for token in tokens) for item in names) + + selected_impls = {key: str(value.get("selected", {}).get("name", "")).lower() for key, value in (algorithm_competitions or {}).items()} + selected_joined = " ".join(selected_impls.values()) + engines = [ + {"engine": "AES-NI / CPU INSTR", "available": has_token(["aesni", "vaes"]), "active": ("aesni" in selected_joined or "vaes" in selected_joined)}, + {"engine": "SIMD (AVX/NEON)", "available": has_token(["avx", "sse", "simd", "neon"]), "active": any(token in selected_joined for token in ["avx", "simd", "neon", "sse"])}, + {"engine": "ARM CRYPTO EXT", "available": has_token(["arm64", "ce", "neon"]), "active": "arm64" in selected_joined}, + {"engine": "QAT OFFLOAD", "available": has_token(["qat"]), "active": "qat" in selected_joined}, + {"engine": "VIRTIO-CRYPTO", "available": has_token(["virtio"]), "active": "virtio" in selected_joined}, + ] + result = [] + for item in engines: + if item["active"]: + status = "active" + elif item["available"]: + status = "available" + else: + status = "unavailable" + result.append({"engine": item["engine"], "status": status}) + return result + + +def collect_algorithm_requesters(items, kernel_clients): + """Infer likely requestor objects that trigger algorithm competition.""" + algo_map = {"aes": {}, "sha": {}, "chacha20": {}} + client_boost_rules = {"aes": {"kTLS", "dm-crypt", "AF_ALG", "IPsec/XFRM"}, "sha": {"kTLS", "AF_ALG", "IPsec/XFRM"}, "chacha20": {"WireGuard", "AF_ALG"}} + for item in items or []: + process_name = str(item.get("process", "unknown")).lower() or "unknown" + protocol = str(item.get("protocol", "")).upper() + algorithm = str(item.get("algorithm", "")).upper() + status = str(item.get("status", "")).upper() + if status == "LISTEN": + continue + matched_algorithms = set() + if "AES" in algorithm or protocol == "TLS": + matched_algorithms.add("aes") + if "SHA" in algorithm or protocol == "TLS": + matched_algorithms.add("sha") + if "CHACHA" in algorithm or protocol in {"WIREGUARD", "SSH"}: + matched_algorithms.add("chacha20") + if not matched_algorithms and protocol == "CRYPTO API": + matched_algorithms.update(["aes", "sha"]) + for algo_key in matched_algorithms: + key = f"process:{process_name}" + bucket = algo_map[algo_key].setdefault(key, {"name": process_name, "kind": "process", "score": 0}) + bucket["score"] += 1 + for client in kernel_clients or []: + name = str(client.get("name", "")).strip() + flows = int(client.get("active_flows", 0) or 0) + if not name or flows <= 0: + continue + for algo_key, allowed_clients in client_boost_rules.items(): + if name not in allowed_clients: + continue + key = f"client:{name}" + bucket = algo_map[algo_key].setdefault(key, {"name": name, "kind": "kernel-client", "score": 0}) + bucket["score"] += max(2, flows) + result = {} + for algo_key, raw in algo_map.items(): + ranked = sorted(raw.values(), key=lambda x: x.get("score", 0), reverse=True) + if not ranked: + ranked = [{"name": "user/kernel request", "kind": "generic", "score": 1}] + result[algo_key] = ranked[:4] + return result + + +def build_crypto_decision_pipelines(algorithm_competitions, kernel_clients, hw_offload, algorithm_requesters): + """Build visual decision pipeline metadata for each algorithm family.""" + hw_active = [h.get("engine") for h in (hw_offload or []) if h.get("status") == "active"] + hw_available = [h.get("engine") for h in (hw_offload or []) if h.get("status") == "available"] + capability_hint = ", ".join(hw_active[:2] or hw_available[:2]) if (hw_active or hw_available) else "generic-cpu-only" + tfm_lookup_map = {"AES": "crypto_alloc_skcipher(aes)", "SHA": "crypto_alloc_shash(sha*)", "CHACHA20": "crypto_alloc_skcipher(chacha20)"} + pipelines = {} + for key, comp in (algorithm_competitions or {}).items(): + request = str(comp.get("request", key)).upper() + impls = comp.get("implementations", []) or [] + shortlist = [str(x.get("name", "unknown")) for x in impls[:3]] + requesters = list((algorithm_requesters or {}).get(key, [])) + top_requester = requesters[0] if requesters else {"name": "user/kernel request", "kind": "generic"} + request_origin = f"{top_requester.get('kind', 'generic')}: {top_requester.get('name', 'unknown')}" + selected_driver = str((comp.get("selected") or {}).get("name", "unknown")) + fallback_driver = next((name for name in shortlist if "generic" in name.lower()), shortlist[-1] if shortlist else "none") + selected_is_generic = "generic" in selected_driver.lower() + selected_source = str((comp.get("selected") or {}).get("source", "kernel")).lower() + fallback_active = selected_is_generic and len(shortlist) > 1 + pipelines[key] = { + "request": request, + "request_origin": request_origin, + "requesters": requesters, + "tfm_lookup": tfm_lookup_map.get(request, f"crypto_lookup({request.lower()})"), + "impl_shortlist": shortlist, + "priority_check": "max priority wins", + "capability_check": capability_hint, + "selected_driver": selected_driver, + "fallback_driver": fallback_driver, + "fallback_active": bool(fallback_active), + "fallback_reason": "higher-priority impl unavailable or unsupported" if fallback_active else "not-triggered", + "source": selected_source, + } + return pipelines diff --git a/kernel_ai/services/crypto/security_pipeline.py b/kernel_ai/services/crypto/security_pipeline.py new file mode 100644 index 0000000..ca6ba5d --- /dev/null +++ b/kernel_ai/services/crypto/security_pipeline.py @@ -0,0 +1,356 @@ +"""Security telemetry pipeline for crypto/security service.""" + +from __future__ import annotations + +from datetime import datetime + + +def collect_security_realtime(security_prev, psutil_module, subprocess_module, random_module, os_module): + """ + Stage-1 security subsystem telemetry: + - Threat decision pipeline + - Process trust graph + - Attack surface map + """ + now = __import__("time").time() + process_rows = [] + suspicious_tokens = { + "nmap", + "masscan", + "hydra", + "sqlmap", + "metasploit", + "msfconsole", + "netcat", + "nc", + "ncat", + "socat", + "john", + "hashcat", + "strace", + "gdb", + } + trusted_tokens = {"systemd", "sshd", "nginx", "python", "containerd", "dockerd", "kubelet", "cron", "rsyslogd", "dbus-daemon"} + ptrace_like = {"strace", "gdb", "ltrace"} + + def classify_trust(score): + if score >= 70: + return "blocked" + if score >= 48: + return "suspicious" + if score >= 28: + return "observe" + return "trusted" + + for proc in psutil_module.process_iter(["pid", "name", "username", "memory_percent", "status", "num_threads"]): + try: + pid = int(proc.info.get("pid") or 0) + name = str(proc.info.get("name") or "unknown").lower() + mem = float(proc.info.get("memory_percent") or 0.0) + threads = int(proc.info.get("num_threads") or 0) + status = str(proc.info.get("status") or "unknown") + user = str(proc.info.get("username") or "") + + score = 12 + if any(tok in name for tok in suspicious_tokens): + score += 38 + if any(tok in name for tok in trusted_tokens): + score -= 10 + if user == "root": + score += 14 + if threads > 120: + score += 8 + if mem > 8.0: + score += 8 + if status in {"zombie", "stopped"}: + score += 10 + score = max(0, min(100, score)) + trust = classify_trust(score) + + process_rows.append( + { + "pid": pid, + "name": name, + "trust": trust, + "risk_score": score, + "threads": threads, + "mem_percent": round(mem, 2), + "status": status, + "user": user, + } + ) + except (psutil_module.NoSuchProcess, psutil_module.AccessDenied, psutil_module.ZombieProcess): + continue + except (psutil_module.Error, KeyError, TypeError, ValueError): + continue + + process_rows.sort(key=lambda p: (p["risk_score"], p["mem_percent"], p["threads"]), reverse=True) + trust_graph = process_rows[:12] + + request_candidates = ["open /etc/shadow", "connect tcp:443", "exec /usr/bin/sudo", "ptrace attach", "bpf program load", "write /usr/lib/systemd/*"] + hook_candidates = ["security_file_open", "security_socket_connect", "security_bprm_check", "seccomp-bpf", "cgroup device policy", "audit hook"] + lanes = [] + for idx, row in enumerate(trust_graph[:10]): + req = request_candidates[idx % len(request_candidates)] + hook = hook_candidates[idx % len(hook_candidates)] + score = int(row.get("risk_score") or 0) + if score >= 70: + verdict = "deny" + elif score >= 45: + verdict = "audit" + else: + verdict = "allow" + lanes.append( + { + "process": row.get("name", "unknown"), + "pid": int(row.get("pid", 0)), + "request": req, + "hook": hook, + "verdict": verdict, + "reason": "risk-score-policy", + "risk_score": score, + } + ) + + try: + listen_ports = len([c for c in psutil_module.net_connections(kind="inet") if str(getattr(c, "status", "") or "") == "LISTEN"]) + except psutil_module.Error: + listen_ports = 0 + + try: + with open("/proc/modules", "r", encoding="utf-8", errors="ignore") as f: + loaded_modules = sum(1 for _ in f) + except OSError: + loaded_modules = 0 + + ptrace_processes = sum(1 for p in process_rows if any(tok in p.get("name", "") for tok in ptrace_like)) + root_processes = sum(1 for p in process_rows if p.get("user") == "root") + suspicious_processes = sum(1 for p in process_rows if p.get("trust") in {"suspicious", "blocked"}) + + setuid_bins = 0 + try: + out = subprocess_module.check_output( + "find /usr/bin /usr/sbin -xdev -perm -4000 -type f 2>/dev/null | wc -l", + shell=True, + text=True, + timeout=1.8, + ).strip() + setuid_bins = int(out or 0) + except (subprocess_module.SubprocessError, OSError, ValueError): + setuid_bins = 0 + + attack_surface = [ + {"name": "open-listen-ports", "value": int(listen_ports), "severity": "high" if listen_ports > 40 else "medium"}, + {"name": "setuid-binaries", "value": int(setuid_bins), "severity": "high" if setuid_bins > 70 else "medium"}, + {"name": "loaded-kernel-modules", "value": int(loaded_modules), "severity": "medium" if loaded_modules > 180 else "low"}, + {"name": "ptrace-capable-processes", "value": int(ptrace_processes), "severity": "high" if ptrace_processes > 0 else "low"}, + {"name": "root-processes", "value": int(root_processes), "severity": "medium" if root_processes > 120 else "low"}, + {"name": "suspicious-processes", "value": int(suspicious_processes), "severity": "high" if suspicious_processes > 6 else "medium"}, + ] + + def _read_text(path): + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + return str(f.read().strip()) + except OSError: + return "" + + apparmor_raw = _read_text("/sys/module/apparmor/parameters/enabled") + selinux_enforce = _read_text("/sys/fs/selinux/enforce") + selinux_policy = _read_text("/sys/fs/selinux/policyvers") + yama_scope = _read_text("/proc/sys/kernel/yama/ptrace_scope") + bpf_unpriv = _read_text("/proc/sys/kernel/unprivileged_bpf_disabled") + landlock_present = os_module.path.exists("/sys/kernel/security/landlock") + ima_present = os_module.path.exists("/sys/kernel/security/ima") + bpf_lsm_present = os_module.path.exists("/sys/kernel/security/bpf") + try: + lsm_list_raw = _read_text("/sys/kernel/security/lsm") + active_lsms = [x.strip() for x in lsm_list_raw.split(",")] if lsm_list_raw else [] + stacking_enabled = len([x for x in active_lsms if x in {"selinux", "apparmor", "bpf"}]) > 1 + except (OSError, AttributeError, TypeError): + active_lsms = [] + stacking_enabled = False + + lsm_status = [ + {"name": "AppArmor", "status": "enforcing" if apparmor_raw.lower().startswith("y") else ("disabled" if apparmor_raw else "unknown"), "detail": apparmor_raw or "n/a", "type": "policy_engine"}, + {"name": "SELinux", "status": "enforcing" if selinux_enforce == "1" else ("disabled" if selinux_enforce == "0" else "unknown"), "detail": selinux_enforce or "n/a", "type": "policy_engine", "policy_version": selinux_policy or "n/a"}, + {"name": "BPF LSM", "status": "present" if bpf_lsm_present else "absent", "detail": "eBPF-based LSM" if bpf_lsm_present else "n/a", "type": "policy_engine"}, + {"name": "LSM Stacking", "status": "enabled" if stacking_enabled else "disabled", "detail": ",".join(active_lsms[:3]) if active_lsms else "n/a", "type": "stacking"}, + {"name": "Yama ptrace", "status": "hardened" if yama_scope in {"2", "3"} else ("relaxed" if yama_scope in {"0", "1"} else "unknown"), "detail": yama_scope or "n/a", "type": "restriction"}, + {"name": "unprivileged bpf", "status": "blocked" if bpf_unpriv == "1" else ("allowed" if bpf_unpriv == "0" else "unknown"), "detail": bpf_unpriv or "n/a", "type": "restriction"}, + {"name": "Landlock", "status": "present" if landlock_present else "absent", "detail": "sysfs" if landlock_present else "n/a", "type": "restriction"}, + {"name": "IMA/EVM", "status": "present" if ima_present else "absent", "detail": "sysfs" if ima_present else "n/a", "type": "integrity"}, + ] + + lsm_engines = [] + if apparmor_raw.lower().startswith("y"): + lsm_engines.append({"name": "AppArmor", "type": "policy_engine", "status": "enforcing", "hooks": ["file_open", "bprm_check", "socket_connect"], "decisions_per_sec": random_module.randint(8, 45)}) + if selinux_enforce == "1": + lsm_engines.append({"name": "SELinux", "type": "policy_engine", "status": "enforcing", "hooks": ["file_open", "bprm_check", "socket_connect", "inode_create"], "decisions_per_sec": random_module.randint(12, 52)}) + if bpf_lsm_present: + lsm_engines.append({"name": "BPF LSM", "type": "policy_engine", "status": "enforcing", "hooks": ["file_open", "bprm_check", "socket_connect"], "decisions_per_sec": random_module.randint(5, 28)}) + + all_capabilities_map = { + 0: "CAP_CHOWN", 1: "CAP_DAC_OVERRIDE", 2: "CAP_DAC_READ_SEARCH", 3: "CAP_FOWNER", 4: "CAP_FSETID", 5: "CAP_KILL", + 6: "CAP_SETGID", 7: "CAP_SETUID", 8: "CAP_SETPCAP", 9: "CAP_LINUX_IMMUTABLE", 10: "CAP_NET_BIND_SERVICE", + 11: "CAP_NET_BROADCAST", 12: "CAP_NET_ADMIN", 13: "CAP_NET_RAW", 14: "CAP_IPC_LOCK", 15: "CAP_IPC_OWNER", + 16: "CAP_SYS_MODULE", 17: "CAP_SYS_RAWIO", 18: "CAP_SYS_CHROOT", 19: "CAP_SYS_PTRACE", 20: "CAP_SYS_PACCT", + 21: "CAP_SYS_ADMIN", 22: "CAP_SYS_BOOT", 23: "CAP_SYS_NICE", 24: "CAP_SYS_RESOURCE", 25: "CAP_SYS_TIME", + 26: "CAP_SYS_TTY_CONFIG", 27: "CAP_MKNOD", 28: "CAP_LEASE", 29: "CAP_AUDIT_WRITE", 30: "CAP_AUDIT_CONTROL", + 31: "CAP_SETFCAP", 32: "CAP_MAC_OVERRIDE", 33: "CAP_MAC_ADMIN", 34: "CAP_SYSLOG", 35: "CAP_WAKE_ALARM", + 36: "CAP_BLOCK_SUSPEND", 37: "CAP_AUDIT_READ", 38: "CAP_PERFMON", 39: "CAP_BPF", 40: "CAP_CHECKPOINT_RESTORE", + } + dangerous_caps = {12: "CAP_NET_ADMIN", 16: "CAP_SYS_MODULE", 17: "CAP_SYS_RAWIO", 19: "CAP_SYS_PTRACE", 21: "CAP_SYS_ADMIN", 39: "CAP_BPF"} + capabilities_rows = [] + seccomp_counts = {"none": 0, "strict": 0, "filter": 0, "unknown": 0} + seccomp_processes = [] + capabilities_processes = [] + + common_syscalls = [ + "read", "write", "open", "close", "stat", "fstat", "lstat", "poll", "lseek", "mmap", "mprotect", "munmap", "brk", + "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "ioctl", "pread64", "pwrite64", "readv", "writev", "access", "pipe", + "select", "sched_yield", "mremap", "msync", "mincore", "madvise", "shmget", "shmat", "shmctl", "dup", "dup2", + "pause", "nanosleep", "getitimer", "alarm", "setitimer", "getpid", + ] + + for row in process_rows[:180]: + pid = int(row.get("pid") or 0) + if pid <= 0: + continue + cap_eff_hex = "" + cap_prm_hex = "" + seccomp_mode = "unknown" + try: + with open(f"/proc/{pid}/status", "r", encoding="utf-8", errors="ignore") as f: + for ln in f: + if ln.startswith("CapEff:"): + cap_eff_hex = ln.split(":", 1)[1].strip() + elif ln.startswith("CapPrm:"): + cap_prm_hex = ln.split(":", 1)[1].strip() + elif ln.startswith("Seccomp:"): + seccomp_raw = ln.split(":", 1)[1].strip() + if seccomp_raw == "0": + seccomp_mode = "none" + elif seccomp_raw == "1": + seccomp_mode = "strict" + elif seccomp_raw == "2": + seccomp_mode = "filter" + else: + seccomp_mode = "unknown" + except OSError: + pass + + seccomp_counts[seccomp_mode] = seccomp_counts.get(seccomp_mode, 0) + 1 + if seccomp_mode in {"filter", "strict"}: + allowed_syscalls = [] + blocked_syscalls = [] + proc_name_lower = str(row.get("name", "")).lower() + if "nginx" in proc_name_lower or "apache" in proc_name_lower: + allowed_syscalls = ["read", "write", "open", "close", "socket", "accept", "send", "recv", "epoll_wait", "fstat"] + blocked_syscalls = ["ptrace", "mount", "umount", "sys_module", "bpf", "keyctl"] + elif "sshd" in proc_name_lower: + allowed_syscalls = ["read", "write", "open", "close", "socket", "accept", "send", "recv", "fork", "execve"] + blocked_syscalls = ["mount", "umount", "sys_module", "bpf"] + elif "docker" in proc_name_lower or "containerd" in proc_name_lower: + allowed_syscalls = ["read", "write", "open", "close", "socket", "clone", "unshare", "mount", "umount"] + blocked_syscalls = ["sys_module", "bpf"] + else: + allowed_syscalls = common_syscalls[:40] + blocked_syscalls = ["ptrace", "mount", "umount", "sys_module", "bpf", "keyctl", "kexec_load"] + seccomp_processes.append( + { + "pid": pid, + "name": row.get("name", "unknown"), + "mode": seccomp_mode, + "allowed_syscalls": allowed_syscalls[:20], + "blocked_syscalls": blocked_syscalls, + "sandbox_level": "strict" if seccomp_mode == "strict" else "filter", + } + ) + + if not cap_eff_hex: + continue + try: + cap_eff_val = int(cap_eff_hex, 16) + except ValueError: + continue + + all_caps = [all_capabilities_map.get(bit, f"CAP_{bit}") for bit in range(41) if (cap_eff_val & (1 << bit))] + matched = [name for bit, name in dangerous_caps.items() if (cap_eff_val & (1 << bit))] + capabilities_processes.append( + { + "pid": pid, + "name": row.get("name", "unknown"), + "user": row.get("user", ""), + "capabilities": all_caps[:15], + "dangerous_caps": matched, + "cap_eff_hex": cap_eff_hex, + "has_keys": len(all_caps) > 0, + } + ) + if not matched: + continue + risk = min(100, 20 + len(matched) * 16 + (10 if row.get("user") == "root" else 0)) + capabilities_rows.append( + { + "pid": pid, + "name": row.get("name", "unknown"), + "user": row.get("user", ""), + "seccomp": seccomp_mode, + "cap_eff": cap_eff_hex, + "cap_prm": cap_prm_hex or "0", + "dangerous": matched[:4], + "risk_score": int(risk), + } + ) + + capabilities_rows.sort(key=lambda x: (x.get("risk_score", 0), len(x.get("dangerous", []))), reverse=True) + capabilities_drift = capabilities_rows[:8] + + total_seccomp_sample = max(1, sum(seccomp_counts.values())) + unsandboxed = [r for r in capabilities_rows if r.get("seccomp") == "none"] + unsandboxed.sort(key=lambda x: x.get("risk_score", 0), reverse=True) + seccomp_coverage = { + "none": int(seccomp_counts.get("none", 0)), + "strict": int(seccomp_counts.get("strict", 0)), + "filter": int(seccomp_counts.get("filter", 0)), + "unknown": int(seccomp_counts.get("unknown", 0)), + "coverage_percent": round((seccomp_counts.get("filter", 0) + seccomp_counts.get("strict", 0)) * 100.0 / total_seccomp_sample, 2), + "high_risk_unsandboxed": [{"pid": int(r.get("pid", 0)), "name": str(r.get("name", "unknown")), "risk_score": int(r.get("risk_score", 0))} for r in unsandboxed[:6]], + } + + prev_ts = security_prev["timestamp"] + prev_events = int(security_prev["events"] or 0) + current_events = len(lanes) + security_prev["timestamp"] = now + security_prev["events"] = current_events + if prev_ts: + dt = max(0.001, now - prev_ts) + decisions_per_sec = round((current_events / dt) + abs(current_events - prev_events) * 0.6, 2) + else: + decisions_per_sec = float(current_events) + + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "pipeline": {"stages": ["request event", "LSM/seccomp hook", "policy verdict"], "lanes": lanes}, + "trust_graph": trust_graph, + "attack_surface": attack_surface, + "security_tools": {"lsm_status": lsm_status, "capabilities_drift": capabilities_drift, "seccomp_coverage": seccomp_coverage}, + "security_core": { + "lsm_engines": lsm_engines, + "seccomp_processes": seccomp_processes[:12], + "capabilities_processes": capabilities_processes[:12], + "stacking_enabled": stacking_enabled, + "active_lsms": active_lsms, + }, + "meta": { + "decisions_per_sec": decisions_per_sec, + "events": current_events, + "trusted": sum(1 for p in trust_graph if p.get("trust") == "trusted"), + "observe": sum(1 for p in trust_graph if p.get("trust") == "observe"), + "suspicious": sum(1 for p in trust_graph if p.get("trust") == "suspicious"), + "blocked": sum(1 for p in trust_graph if p.get("trust") == "blocked"), + "seccomp_coverage_percent": seccomp_coverage.get("coverage_percent", 0.0), + "mode": "live-heuristic-v2", + }, + } diff --git a/kernel_ai/services/processes_runtime.py b/kernel_ai/services/processes_runtime.py index e457516..afe0d9f 100644 --- a/kernel_ai/services/processes_runtime.py +++ b/kernel_ai/services/processes_runtime.py @@ -247,6 +247,220 @@ def _read_proc_faults(pid: int): return {"minflt": 0, "majflt": 0} +def _read_proc_fd_semantics(pid: int, max_entries: int = 96) -> dict: + """Summarize /proc fd targets into kernel-relevant categories.""" + summary = { + "socket": 0, + "pipe": 0, + "eventpoll": 0, + "eventfd": 0, + "timerfd": 0, + "regular": 0, + "anon": 0, + "deleted": 0, + "sampled": 0, + } + try: + entries = [name for name in os.listdir(f"/proc/{int(pid)}/fd") if name.isdigit()] + except (OSError, ValueError): + return summary + + for name in entries[: max(0, int(max_entries))]: + try: + target = os.readlink(f"/proc/{int(pid)}/fd/{name}") + except OSError: + continue + summary["sampled"] += 1 + target_l = target.lower() + if target_l.startswith("socket:"): + summary["socket"] += 1 + elif target_l.startswith("pipe:"): + summary["pipe"] += 1 + elif "anon_inode:[eventpoll]" in target_l: + summary["eventpoll"] += 1 + summary["anon"] += 1 + elif "anon_inode:[eventfd]" in target_l: + summary["eventfd"] += 1 + summary["anon"] += 1 + elif "anon_inode:[timerfd]" in target_l: + summary["timerfd"] += 1 + summary["anon"] += 1 + elif target_l.startswith("anon_inode:"): + summary["anon"] += 1 + else: + summary["regular"] += 1 + if " (deleted)" in target_l: + summary["deleted"] += 1 + return summary + + +def _parse_tcp_snmp_counters() -> dict: + """Read selected global TCP counters from /proc/net/snmp.""" + try: + with open("/proc/net/snmp", "r", encoding="utf-8", errors="ignore") as f: + lines = [line.strip() for line in f if line.strip().startswith("Tcp:")] + except OSError: + return {} + if len(lines) < 2: + return {} + headers = lines[-2].split()[1:] + values = lines[-1].split()[1:] + out = {} + for key, raw in zip(headers, values): + if key not in {"ActiveOpens", "PassiveOpens", "RetransSegs", "InSegs", "OutSegs"}: + continue + try: + out[key] = int(raw) + except ValueError: + continue + return out + + +def _semantic_op(label: str, op_type: str, active: bool, weight: float, source: str, evidence: dict | None = None) -> dict: + return { + "label": label, + "type": op_type, + "active": bool(active), + "weight": round(float(max(0.0, weight)), 2), + "source": source, + "evidence": evidence or {}, + } + + +def _build_semantic_ops(node_pool: dict, network_tracing: list[dict], security_hooks: list[dict], tcp_counters: dict) -> list[dict]: + """Build semantic kernel-operation chains from procfs/psutil-derived evidence.""" + network_by_pid = {int(row.get("pid") or 0): row for row in network_tracing} + active_security = any(str(hook.get("status") or "") in {"active", "hardened"} for hook in security_hooks) + rows = [] + ranked_nodes = sorted( + node_pool.values(), + key=lambda row: (int(row.get("syscall_pressure") or 0), int(row.get("connections") or 0), int(row.get("fd_count") or 0)), + reverse=True, + ) + for node in ranked_nodes[:18]: + pid = int(node.get("pid") or 0) + if pid <= 0: + continue + name = str(node.get("name") or "process") + name_l = name.lower() + fd_sem = dict(node.get("fd_semantics") or {}) + network = network_by_pid.get(pid) or {} + connections = int(network.get("connections") or node.get("connections") or 0) + unique_peers = int(network.get("unique_peers") or node.get("unique_peers") or 0) + fd_count = int(node.get("fd_count") or 0) + threads = int(node.get("num_threads") or 0) + pressure = int(node.get("syscall_pressure") or 0) + socket_fds = int(fd_sem.get("socket") or 0) + regular_fds = int(fd_sem.get("regular") or 0) + eventpoll_fds = int(fd_sem.get("eventpoll") or 0) + pipe_fds = int(fd_sem.get("pipe") or 0) + has_web_name = any(token in name_l for token in ("nginx", "http", "gunicorn", "node", "curl", "chrome", "firefox")) + has_tcp_retrans = int(tcp_counters.get("RetransSegs") or 0) > 0 + has_tls_hint = has_web_name and (connections > 0 or socket_fds > 0) + + ops = [ + _semantic_op( + "socket()", + "network", + socket_fds > 0 or connections > 0, + max(socket_fds, connections), + "/proc//fd socket + psutil.net_connections", + {"socket_fds": socket_fds, "connections": connections}, + ), + _semantic_op( + "connect()/accept()", + "network", + connections > 0, + connections, + "psutil.net_connections", + {"connections": connections, "top_state": str(network.get("top_state") or "UNKNOWN")}, + ), + _semantic_op( + "nf_conntrack", + "netfilter", + connections > 0 and unique_peers > 0, + unique_peers, + "psutil.net_connections peer sample", + {"unique_peers": unique_peers, "peer_sample": list(network.get("peer_sample") or [])[:4]}, + ), + _semantic_op( + "tcp retransmission", + "tcp", + connections > 0 and has_tcp_retrans, + int(tcp_counters.get("RetransSegs") or 0), + "/proc/net/snmp Tcp.RetransSegs", + {"retrans_segs": int(tcp_counters.get("RetransSegs") or 0)}, + ), + _semantic_op( + "TLS handshake", + "crypto", + has_tls_hint, + max(1, connections), + "process-name/socket heuristic", + {"name": name, "connections": connections}, + ), + _semantic_op( + "epoll_wait()", + "event", + eventpoll_fds > 0, + eventpoll_fds, + "/proc//fd anon_inode:[eventpoll]", + {"eventpoll_fds": eventpoll_fds, "fd_sampled": int(fd_sem.get("sampled") or 0)}, + ), + _semantic_op( + "sendfile()/splice()", + "vfs", + (socket_fds > 0 or connections > 0) and regular_fds > 0, + regular_fds, + "/proc//fd regular + socket mix", + {"regular_fds": regular_fds, "socket_fds": socket_fds}, + ), + _semantic_op( + "seccomp/LSM check", + "security", + active_security or str(node.get("seccomp_mode") or "") in {"filter", "strict"}, + 2 if active_security else 1, + "/sys/kernel/security/lsm + /proc//status", + {"seccomp_mode": str(node.get("seccomp_mode") or "unknown")}, + ), + _semantic_op( + "sched_pick_next()", + "sched", + True, + max(1, threads), + "psutil.num_threads", + {"threads": threads, "pressure": pressure}, + ), + _semantic_op( + "clone/fork lineage", + "task", + int(node.get("ppid") or 0) > 0, + 1, + "/proc//stat", + {"ppid": int(node.get("ppid") or 0)}, + ), + _semantic_op( + "pipe()/eventfd", + "ipc", + pipe_fds > 0 or int(fd_sem.get("eventfd") or 0) > 0, + pipe_fds + int(fd_sem.get("eventfd") or 0), + "/proc//fd pipe/eventfd", + {"pipe_fds": pipe_fds, "eventfd": int(fd_sem.get("eventfd") or 0)}, + ), + ] + active_ops = [op for op in ops if op["active"]] + rows.append( + { + "pid": pid, + "name": name, + "ops": active_ops[:8], + "fd_semantics": fd_sem, + "source": "procfs+psutil-derived", + } + ) + return rows + + def _build_memory_process_pressure(syscall_nodes, meminfo_kb): mt_kb = max(1, int((meminfo_kb or {}).get("MemTotal") or 0)) psi = _parse_memory_psi() @@ -487,6 +701,7 @@ def collect_processes_realtime(): fd_count = int(proc.num_fds() or 0) except (psutil.Error, OSError, TypeError, ValueError): fd_count = 0 + fd_semantics = _read_proc_fd_semantics(pid) seccomp_mode = "unknown" with open(f"/proc/{pid}/status", "r", encoding="utf-8", errors="ignore") as f: for ln in f: @@ -510,6 +725,7 @@ def collect_processes_realtime(): "name": name, "user": user, "fd_count": fd_count, + "fd_semantics": fd_semantics, "num_threads": threads, "syscall_pressure": syscall_pressure, "seccomp_mode": seccomp_mode, @@ -607,6 +823,7 @@ def collect_processes_realtime(): "user": str(row.get("user") or ""), "syscall_pressure": int(row.get("syscall_pressure") or 0), "fd_count": int(row.get("fd_count") or 0), + "fd_semantics": dict(row.get("fd_semantics") or {}), "num_threads": int(row.get("num_threads") or 0), "seccomp_mode": str(row.get("seccomp_mode") or "unknown"), "connections": 0, @@ -626,6 +843,7 @@ def collect_processes_realtime(): "user": "", "syscall_pressure": 0, "fd_count": 0, + "fd_semantics": {}, "num_threads": 0, "seccomp_mode": "unknown", "connections": 0, @@ -692,6 +910,8 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): nodes = list(node_pool.values())[:18] edges = edges[:64] + tcp_counters = _parse_tcp_snmp_counters() + semantic_ops = _build_semantic_ops(node_pool, network_tracing, security_hooks, tcp_counters) try: vm = psutil.virtual_memory() @@ -746,12 +966,15 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): "syscalls_interception": syscall_nodes, "network_tracing": network_tracing, "security_hooks": security_hooks, + "semantic_ops": semantic_ops, "neural_graph": {"nodes": nodes, "edges": edges}, "memory_visual": memory_visual, "meta": { "processes_sampled": len(syscall_nodes), "network_processes": len(network_tracing), "seccomp_filter_percent": round((seccomp_modes.get("filter", 0) + seccomp_modes.get("strict", 0)) * 100.0 / max(1, sum(seccomp_modes.values())), 2), - "mode": "live-heuristic-v1", + "semantic_ops_source": "procfs+psutil-derived", + "tcp_retrans_segs": int(tcp_counters.get("RetransSegs") or 0), + "mode": "live-semantic-v2", }, } diff --git a/static/js/memory-belt.js b/static/js/memory-belt.js index 2c82995..02a86fb 100644 --- a/static/js/memory-belt.js +++ b/static/js/memory-belt.js @@ -1,7 +1,7 @@ // Linux memory subsystem — strip map (same API payload as processes-realtime memory_visual) -// Version: 2 — deeper meminfo: slab split, dirty/writeback, THP, vmalloc, LRU +// Version: 3 — memory path now: lightweight kernel path hints from vmstat/PSI -debugLog('💾 memory-belt.js v1: Script loading...'); +debugLog('💾 memory-belt.js v3: Script loading...'); class MemorySubsystemVisualization { constructor() { @@ -26,6 +26,8 @@ class MemorySubsystemVisualization { this.clickHandler = null; this.viewMode = 'fabric'; this.viewModeButton = null; + this.prevMemoryVmstat = null; + this.memoryVmstatDelta = {}; } init(containerId = 'memory-belt-container') { @@ -143,6 +145,7 @@ class MemorySubsystemVisualization { .then((data) => { if (!data || data.error) throw new Error(data?.error || 'No data'); this.telemetry = data; + this.updateMemoryKernelDeltas(); }) .catch(() => { this.telemetry = { @@ -175,6 +178,22 @@ class MemorySubsystemVisualization { }); } + updateMemoryKernelDeltas() { + const vmstat = this.telemetry?.memory_visual?.kernel_memory_state?.vmstat || {}; + const keys = [ + 'pgfault', 'pgmajfault', 'pgscan_kswapd', 'pgscan_direct', + 'pgsteal_kswapd', 'pgsteal_direct', 'compact_stall', 'oom_kill' + ]; + const next = {}; + keys.forEach((key) => { + const cur = Number(vmstat[key] || 0); + const prev = this.prevMemoryVmstat ? Number(this.prevMemoryVmstat[key] || 0) : cur; + next[key] = Math.max(0, cur - prev); + }); + this.memoryVmstatDelta = next; + this.prevMemoryVmstat = { ...vmstat }; + } + drawPanel(x, y, w, h, title, opts = {}) { const alpha = typeof opts.alpha === 'number' ? opts.alpha : 0.88; const showTitle = opts.showTitle !== false; @@ -211,6 +230,87 @@ class MemorySubsystemVisualization { this.ctx.textAlign = 'start'; } + buildKernelMemoryPathItems(sum, psiMem) { + const delta = this.memoryVmstatDelta || {}; + const dirtyWb = Number(sum.dirty_writeback_mb || 0); + const swapPercent = Number(sum.swap_percent || 0); + const psiSome = Number(psiMem.some_avg10 || 0); + const psiFull = Number(psiMem.full_avg10 || 0); + return [ + { + label: 'page fault', + value: Number(delta.pgfault || 0), + active: Number(delta.pgfault || 0) > 0, + color: 'rgba(120, 220, 255, 0.92)' + }, + { + label: 'reclaim', + value: Number(delta.pgscan_kswapd || 0) + Number(delta.pgscan_direct || 0), + active: Number(delta.pgscan_kswapd || 0) + Number(delta.pgscan_direct || 0) > 0 || psiSome > 0.01, + color: 'rgba(126, 242, 210, 0.92)' + }, + { + label: 'kswapd', + value: Number(delta.pgsteal_kswapd || 0), + active: Number(delta.pgsteal_kswapd || 0) > 0, + color: 'rgba(118, 230, 170, 0.9)' + }, + { + label: 'compaction', + value: Number(delta.compact_stall || 0), + active: Number(delta.compact_stall || 0) > 0, + color: 'rgba(190, 150, 255, 0.9)' + }, + { + label: 'writeback', + value: dirtyWb, + active: dirtyWb > 0.01, + color: 'rgba(255, 190, 110, 0.92)' + }, + { + label: 'swap', + value: swapPercent, + active: swapPercent > 0.1 || psiFull > 0.01, + color: 'rgba(255, 120, 150, 0.9)' + } + ]; + } + + drawKernelMemoryPath(x, y, w, h, sum, psiMem) { + const items = this.buildKernelMemoryPathItems(sum, psiMem); + this.drawPanel(x, y, w, h, '', { alpha: 0.62, showTitle: false }); + this.ctx.fillStyle = 'rgba(214, 238, 255, 0.95)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('KERNEL MEMORY PATH', x + 10, y + 14); + this.ctx.fillStyle = 'rgba(145, 198, 224, 0.82)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText('vmstat delta / poll · psi pressure hints', x + 10, y + 26); + + const startY = y + 42; + const stepX = Math.max(48, Math.floor((w - 26) / items.length)); + items.forEach((item, idx) => { + const cx = x + 14 + idx * stepX; + const activePulse = item.active ? (0.65 + 0.25 * Math.sin(this.tick * 0.08 + idx)) : 0.28; + if (idx > 0) { + this.ctx.beginPath(); + this.ctx.moveTo(cx - stepX + 18, startY); + this.ctx.lineTo(cx - 8, startY); + this.ctx.strokeStyle = item.active ? 'rgba(130, 230, 255, 0.34)' : 'rgba(100, 140, 165, 0.18)'; + this.ctx.lineWidth = 1; + this.ctx.stroke(); + } + this.ctx.beginPath(); + this.ctx.arc(cx, startY, item.active ? 4.6 : 3.4, 0, Math.PI * 2); + this.ctx.fillStyle = item.active ? item.color : `rgba(110, 140, 160, ${activePulse})`; + this.ctx.fill(); + this.ctx.strokeStyle = item.active ? 'rgba(236, 252, 255, 0.82)' : 'rgba(150, 178, 198, 0.38)'; + this.ctx.stroke(); + this.ctx.fillStyle = item.active ? 'rgba(226, 246, 255, 0.94)' : 'rgba(142, 166, 184, 0.72)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText(item.label.slice(0, 11), cx - 8, startY + 14); + }); + } + drawMemoryStats(x, y, w, h) { const sum = this.telemetry?.memory_visual?.summary || {}; const procPressure = Array.isArray(this.telemetry?.memory_visual?.process_pressure) @@ -218,6 +318,8 @@ class MemorySubsystemVisualization { : []; const psiMem = this.telemetry?.memory_visual?.kernel_memory_state?.psi_memory || {}; this.drawPanel(x, y, w, h, 'physical memory · /proc/meminfo + psutil', { alpha: 0.88 }); + const pathW = Math.min(430, Math.max(330, w * 0.34)); + this.drawKernelMemoryPath(x + w - pathW - 14, y + 32, pathW, 68, sum, psiMem); this.ctx.fillStyle = '#c4f8ff'; this.ctx.font = '11px "Share Tech Mono", monospace'; this.ctx.fillText(`total ${Number(sum.total_mb || 0).toFixed(0)} MiB`, x + 16, y + 46); diff --git a/static/js/processes-belt.js b/static/js/processes-belt.js index 60625c1..8630e34 100644 --- a/static/js/processes-belt.js +++ b/static/js/processes-belt.js @@ -1,7 +1,7 @@ // Processes Subsystem Visualization -// Version: 13 +// Version: 20 -debugLog('🧠 processes-belt.js v13: Script loading...'); +debugLog('🧠 processes-belt.js v20: Script loading...'); class ProcessesSubsystemVisualization { constructor() { @@ -15,7 +15,7 @@ class ProcessesSubsystemVisualization { this.telemetry = null; this.tick = 0; this.nodeLayout = new Map(); - this.layoutMode = 'temporal'; + this.layoutMode = 'microscope'; this.modeButtons = new Map(); this.edgeFilter = 'all'; this.filterButtons = new Map(); @@ -83,9 +83,9 @@ class ProcessesSubsystemVisualization { position:absolute;top:18px;left:18px;display:flex;gap:8px;z-index:1001; `; const modes = [ + { key: 'microscope', label: 'MICROSCOPE' }, { key: 'temporal', label: '3-LAYER GRAPH' }, - { key: 'radial', label: 'RADIAL GRAPH' }, - { key: 'microscope', label: 'MICROSCOPE' } + { key: 'radial', label: 'RADIAL GRAPH' } ]; modes.forEach((m) => { const btn = document.createElement('button'); @@ -728,20 +728,47 @@ class ProcessesSubsystemVisualization { drawRadialNeuralGraph(nodes, edges, pos, x, y, w, h) { const cx = x + w * 0.5; const cy = y + h * 0.52; - const ring = Math.min(w, h) * 0.38; + const outer = Math.min(w, h) * 0.34; + const inner = outer * 0.46; this.nodeHitAreas = []; + + [outer, outer * 0.78, outer * 0.58, inner].forEach((r, idx) => { + this.ctx.beginPath(); + this.ctx.arc(cx, cy, r, 0, Math.PI * 2); + this.ctx.strokeStyle = idx === 0 ? 'rgba(140,212,255,0.22)' : 'rgba(118,190,232,0.16)'; + this.ctx.lineWidth = idx === 0 ? 1.2 : 1; + this.ctx.stroke(); + }); + + this.ctx.strokeStyle = 'rgba(120,195,235,0.14)'; + this.ctx.lineWidth = 1; + for (let i = 0; i < 8; i += 1) { + const a = (Math.PI * 2 * i) / 8; + this.ctx.beginPath(); + this.ctx.moveTo(cx + Math.cos(a) * inner, cy + Math.sin(a) * inner); + this.ctx.lineTo(cx + Math.cos(a) * outer, cy + Math.sin(a) * outer); + this.ctx.stroke(); + } + + const sweep = (this.tick * 0.0025) % (Math.PI * 2); + this.ctx.beginPath(); + this.ctx.moveTo(cx, cy); + this.ctx.arc(cx, cy, outer, sweep - 0.14, sweep + 0.14); + this.ctx.closePath(); + this.ctx.fillStyle = 'rgba(120, 210, 245, 0.08)'; + this.ctx.fill(); + nodes.forEach((node, idx) => { const pid = Number(node.pid || 0); const a = ((Math.PI * 2) / Math.max(1, nodes.length)) * idx - Math.PI / 2; - const drift = Math.sin(this.tick * 0.0035 + idx * 0.4) * 0.018; - const nx = cx + Math.cos(a + drift) * (ring * (0.75 + (idx % 4) * 0.07)); - const ny = cy + Math.sin(a + drift) * (ring * (0.75 + (idx % 4) * 0.07)); - pos.set(pid, { x: nx, y: ny }); + const noise = Math.sin(this.tick * 0.004 + idx * 0.31) * 0.02; + const radius = outer * (0.62 + (idx % 4) * 0.08); + pos.set(pid, { + x: cx + Math.cos(a + noise) * radius, + y: cy + Math.sin(a + noise) * radius + }); }); - this.positionHistory.push(pos); - if (this.positionHistory.length > 3) this.positionHistory.shift(); - const neighborPids = new Set(); if (this.hoveredNodePid) { edges.forEach((edge) => { @@ -752,33 +779,18 @@ class ProcessesSubsystemVisualization { }); } - // Core glow - const coreGrad = this.ctx.createRadialGradient(cx, cy, 8, cx, cy, 120); - coreGrad.addColorStop(0, 'rgba(124,178,255,0.45)'); - coreGrad.addColorStop(1, 'rgba(124,178,255,0)'); - this.ctx.beginPath(); - this.ctx.arc(cx, cy, 120, 0, Math.PI * 2); - this.ctx.fillStyle = coreGrad; - this.ctx.fill(); - edges.forEach((edge, idx) => { const s = pos.get(Number(edge.source || 0)); const t = pos.get(Number(edge.target || 0)); if (!s || !t) return; - const wv = Number(edge.weight || 0.4); + const weight = Number(edge.weight || 0.4); const color = this.edgeColor(String(edge.type || '')); const isHoverEdge = this.hoveredNodePid && (Number(edge.source || 0) === this.hoveredNodePid || Number(edge.target || 0) === this.hoveredNodePid); - this.ctx.globalAlpha = isHoverEdge ? 0.95 : (0.2 + wv * 0.38); + this.ctx.globalAlpha = isHoverEdge ? 0.94 : (0.18 + weight * 0.34); const salt = idx * 97 + Number(edge.source || 0) + Number(edge.target || 0); const ctrl = this.radialCurveControl(s.x, s.y, t.x, t.y, cx, cy, salt); - this.drawCurvedStroke(s.x, s.y, t.x, t.y, color, 0.65 + wv, salt, true, ctrl); + this.drawCurvedStroke(s.x, s.y, t.x, t.y, color, isHoverEdge ? 1.2 : 0.8, salt, false, ctrl); this.ctx.globalAlpha = 1; - const tp = (this.tick * 0.006 + idx * 0.08) % 1; - const p = this.quadBezierPoint(s.x, s.y, ctrl.cx, ctrl.cy, t.x, t.y, tp); - this.ctx.beginPath(); - this.ctx.arc(p.x, p.y, 1.7, 0, Math.PI * 2); - this.ctx.fillStyle = 'rgba(240, 248, 255, 0.95)'; - this.ctx.fill(); }); nodes.forEach((node) => { @@ -786,43 +798,29 @@ class ProcessesSubsystemVisualization { const p = pos.get(pid); if (!p) return; const pressure = Number(node.syscall_pressure || 0); - const nodeR = 7 + Math.min(8, pressure / 18); - const danger = pressure >= 70; + const nodeR = 4.2 + Math.min(4.8, pressure / 30); const mode = String(node.seccomp_mode || 'unknown'); - const fill = danger ? '#eb7e7e' : (mode === 'filter' || mode === 'strict' ? '#60d69d' : '#8a9cea'); + const danger = pressure >= 70; + const fill = danger ? '#ef8f8f' : (mode === 'filter' || mode === 'strict' ? '#75dfb4' : '#9cb8df'); const isHovered = this.hoveredNodePid && pid === this.hoveredNodePid; const isNeighbor = this.hoveredNodePid && neighborPids.has(pid); - - for (let hi = 0; hi < this.positionHistory.length - 1; hi++) { - const histPos = this.positionHistory[hi].get(pid); - if (!histPos) continue; - const ghostAlpha = 0.07 + hi * 0.08; - this.ctx.beginPath(); - this.ctx.arc(histPos.x, histPos.y, Math.max(2, nodeR * 0.6), 0, Math.PI * 2); - this.ctx.fillStyle = fill; - this.ctx.globalAlpha = ghostAlpha; - this.ctx.fill(); - } - this.ctx.globalAlpha = 1; + const alpha = isHovered ? 1 : (isNeighbor ? 0.94 : 0.82); this.ctx.beginPath(); this.ctx.arc(p.x, p.y, nodeR, 0, Math.PI * 2); this.ctx.fillStyle = fill; + this.ctx.globalAlpha = alpha; this.ctx.fill(); - this.ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.98)' : 'rgba(227,241,255,0.95)'; - this.ctx.lineWidth = isHovered ? 2 : 1; - this.ctx.stroke(); - this.ctx.fillStyle = '#d8e5f7'; - this.ctx.globalAlpha = isHovered ? 1 : (isNeighbor ? 0.95 : 0.82); - this.ctx.font = '9px "Share Tech Mono", monospace'; - this.ctx.fillText(`${String(node.name || 'proc').slice(0, 8)}`, p.x - 20, p.y + nodeR + 12); this.ctx.globalAlpha = 1; - this.nodeHitAreas.push({ x: p.x, y: p.y, r: nodeR, pid }); + this.ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.98)' : 'rgba(227,241,255,0.82)'; + this.ctx.lineWidth = isHovered ? 1.8 : 0.9; + this.ctx.stroke(); + this.nodeHitAreas.push({ x: p.x, y: p.y, r: nodeR + 1, pid }); }); this.ctx.fillStyle = '#a7b6cb'; this.ctx.font = '10px "Share Tech Mono", monospace'; - this.ctx.fillText('radial: task ring · curved edges · hover neighborhood', x + 14, y + h - 14); + this.ctx.fillText('radial graph: process pressure and edge topology', x + 14, y + h - 14); } drawMicroscopeTimeline(focusNode, x, y, w, h) { @@ -860,10 +858,9 @@ class ProcessesSubsystemVisualization { drawSeries((pt) => Math.min(100, pt.fd_count * 2), 'rgba(255, 205, 128, 0.82)', 0.8); } - drawMicroscopeGraph(nodes, edges, x, y, w, h) { - this.drawPanel(x, y, w, 36, 'process microscope · parent/child · threads · interactions', { alpha: 0.9 }); - const innerY = y + 42; - const innerH = h - 50; + drawProcessControlRoom(nodes, edges, x, y, w, h) { + const innerY = y + 4; + const innerH = h - 8; this.nodeHitAreas = []; if (!nodes.length) { this.ctx.fillStyle = 'rgba(196,207,224,0.72)'; @@ -871,12 +868,19 @@ class ProcessesSubsystemVisualization { this.ctx.fillText('NO PROCESS GRAPH DATA', x + 20, innerY + 28); return; } + const byPid = new Map(nodes.map((n) => [Number(n.pid || 0), n])); const focus = this.getFocusProcess(nodes); if (!focus) return; const focusPid = Number(focus.pid || 0); - const centerX = x + w * 0.5; - const centerY = innerY + innerH * 0.42; + const panelGap = 14; + const sideW = Math.max(210, Math.min(300, w * 0.24)); + const leftX = x + 18; + const rightX = x + w - sideW - 18; + const centerX0 = leftX + sideW + panelGap; + const centerW = Math.max(360, rightX - centerX0 - panelGap); + const centerX = centerX0 + centerW * 0.5; + const centerY = innerY + innerH * 0.47; const parent = byPid.get(Number(focus.ppid || 0)) || null; const children = nodes.filter((n) => Number(n.ppid || 0) === focusPid).slice(0, 6); const neighbors = []; @@ -894,19 +898,59 @@ class ProcessesSubsystemVisualization { seen.add(pid); uniqNeighbors.push(it); }); + + const syscalls = Array.isArray(this.telemetry?.syscalls_interception) ? this.telemetry.syscalls_interception : []; + const networkRows = Array.isArray(this.telemetry?.network_tracing) ? this.telemetry.network_tracing : []; + const securityHooks = Array.isArray(this.telemetry?.security_hooks) ? this.telemetry.security_hooks : []; const threadsCount = Math.max(1, Number(focus.num_threads || 1)); - const ringA = Math.min(w, innerH) * 0.18; - const ringB = Math.min(w, innerH) * 0.29; - const ringC = Math.min(w, innerH) * 0.4; + const pressure = Math.max(0, Math.min(100, Number(focus.syscall_pressure || 0))); + const graphRadius = Math.min(centerW, innerH) * 0.34; + const familyR = graphRadius * 0.42; + const threadR = graphRadius * 0.66; + const ioR = graphRadius * 0.95; + const hotProcesses = nodes + .slice() + .sort((a, b) => Number(b.syscall_pressure || 0) - Number(a.syscall_pressure || 0)) + .slice(0, 7); + const edgeCounts = edges.reduce((acc, edge) => { + const type = String(edge.type || 'link'); + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + + this.drawPanel(leftX, innerY + 34, sideW, innerH - 54, '', { alpha: 0.72, showTitle: false }); + this.drawPanel(centerX0, innerY + 34, centerW, innerH - 54, '', { alpha: 0.52, showTitle: false }); + this.drawPanel(rightX, innerY + 34, sideW, innerH - 54, '', { alpha: 0.72, showTitle: false }); + + this.ctx.strokeStyle = 'rgba(140, 220, 255, 0.25)'; + this.ctx.lineWidth = 1; + this.ctx.beginPath(); + this.ctx.moveTo(x + 12, innerY + 18); + this.ctx.lineTo(x + w - 12, innerY + 18); + this.ctx.stroke(); + this.ctx.fillStyle = 'rgba(198, 230, 255, 0.86)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('PROCESS CONTROL ROOM', x + 18, innerY + 13); + this.ctx.textAlign = 'center'; + this.ctx.fillText(`${this.autoFocusEnabled ? 'AUTO' : 'PINNED'} FOCUS · PID ${focusPid}`, centerX, innerY + 13); + this.ctx.textAlign = 'start'; - // semantic rings - [ringA, ringB, ringC].forEach((r, idx) => { + [familyR, threadR, ioR].forEach((r, idx) => { this.ctx.beginPath(); this.ctx.arc(centerX, centerY, r, 0, Math.PI * 2); - this.ctx.strokeStyle = idx === 2 ? 'rgba(120,170,230,0.18)' : 'rgba(120,170,230,0.24)'; - this.ctx.lineWidth = idx === 0 ? 1.1 : 0.9; + this.ctx.strokeStyle = idx === 0 ? 'rgba(130, 210, 255, 0.24)' : 'rgba(120,200,240,0.14)'; + this.ctx.lineWidth = idx === 0 ? 1.2 : 0.9; this.ctx.stroke(); }); + [ + { label: 'FAMILY', r: familyR, color: 'rgba(170, 205, 255, 0.78)' }, + { label: 'THREADS', r: threadR, color: 'rgba(118, 230, 170, 0.78)' }, + { label: 'IO / IPC / NET', r: ioR, color: 'rgba(255, 204, 132, 0.78)' } + ].forEach((ring, idx) => { + this.ctx.fillStyle = ring.color; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(ring.label, centerX + ring.r + 8, centerY - 6 + idx * 12); + }); const nodePos = new Map(); const placeNode = (node, px, py, radius, fill, label) => { @@ -921,82 +965,937 @@ class ProcessesSubsystemVisualization { this.ctx.font = '9px "Share Tech Mono", monospace'; this.ctx.fillText(label, px + radius + 6, py + 3); const pid = Number(node?.pid || 0); - if (pid > 0) this.nodeHitAreas.push({ x: px, y: py, r: radius, pid }); + if (pid > 0) this.nodeHitAreas.push({ x: px, y: py, r: radius + 2, pid }); if (pid > 0) nodePos.set(pid, { x: px, y: py }); }; - // center focus - placeNode( - focus, - centerX, - centerY, - 11, - 'rgba(163, 220, 255, 0.9)', - `${String(focus.name || 'proc').slice(0, 14)}:${focusPid}` - ); - this.selectedNodePid = focusPid; - - // parent/children ring + const focusGlow = this.ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 62); + focusGlow.addColorStop(0, pressure >= 70 ? 'rgba(255, 120, 120, 0.34)' : 'rgba(126, 205, 255, 0.34)'); + focusGlow.addColorStop(1, 'rgba(126, 205, 255, 0)'); + this.ctx.beginPath(); + this.ctx.arc(centerX, centerY, 62, 0, Math.PI * 2); + this.ctx.fillStyle = focusGlow; + this.ctx.fill(); + placeNode(focus, centerX, centerY, 13, pressure >= 70 ? 'rgba(255, 138, 138, 0.92)' : 'rgba(163, 220, 255, 0.94)', `${String(focus.name || 'proc').slice(0, 14)}:${focusPid}`); + if (parent) { - const px = centerX; - const py = centerY - ringA; - placeNode(parent, px, py, 7, 'rgba(192, 210, 238, 0.85)', `parent ${String(parent.name || '').slice(0, 10)}:${Number(parent.pid || 0)}`); + placeNode(parent, centerX, centerY - familyR, 7, 'rgba(192, 210, 238, 0.85)', `parent ${String(parent.name || '').slice(0, 10)}:${Number(parent.pid || 0)}`); } children.forEach((ch, idx) => { const a = (Math.PI * 2 * idx / Math.max(1, children.length)) - Math.PI / 2; - const px = centerX + Math.cos(a) * ringA; - const py = centerY + Math.sin(a) * ringA; - placeNode(ch, px, py, 6.2, 'rgba(152, 204, 255, 0.82)', `child ${String(ch.name || '').slice(0, 8)}:${Number(ch.pid || 0)}`); + placeNode(ch, centerX + Math.cos(a) * familyR, centerY + Math.sin(a) * familyR, 6.2, 'rgba(152, 204, 255, 0.82)', `child ${String(ch.name || '').slice(0, 8)}:${Number(ch.pid || 0)}`); }); - - // threads ring (synthetic thread beads) const threadsToDraw = Math.min(16, Math.max(3, threadsCount)); for (let i = 0; i < threadsToDraw; i++) { const a = (Math.PI * 2 * i / threadsToDraw) + this.tick * 0.005; - const px = centerX + Math.cos(a) * ringB; - const py = centerY + Math.sin(a) * ringB; this.ctx.beginPath(); - this.ctx.arc(px, py, 2.1, 0, Math.PI * 2); + this.ctx.arc(centerX + Math.cos(a) * threadR, centerY + Math.sin(a) * threadR, 2.1, 0, Math.PI * 2); this.ctx.fillStyle = 'rgba(120, 230, 170, 0.85)'; this.ctx.fill(); } - - // interactions ring uniqNeighbors.slice(0, 10).forEach((item, idx) => { const n = item.node; const a = (Math.PI * 2 * idx / Math.max(1, Math.min(10, uniqNeighbors.length))) + Math.PI * 0.06; - const px = centerX + Math.cos(a) * ringC; - const py = centerY + Math.sin(a) * ringC; const edgeType = String(item.type || ''); const color = edgeType === 'network' ? 'rgba(244,201,119,0.88)' : (edgeType === 'ipc' ? 'rgba(96,214,157,0.88)' : (edgeType === 'file_access' ? 'rgba(235,126,126,0.88)' : 'rgba(160,180,210,0.85)')); - placeNode(n, px, py, 5.2, color, `${edgeType || 'link'} ${String(n.name || '').slice(0, 8)}:${Number(n.pid || 0)}`); + placeNode(n, centerX + Math.cos(a) * ioR, centerY + Math.sin(a) * ioR, 5.2, color, `${edgeType || 'link'} ${String(n.name || '').slice(0, 8)}:${Number(n.pid || 0)}`); }); - - // links from focus to related for (const [pid, p] of nodePos.entries()) { if (pid === focusPid) continue; this.drawCurvedStroke(centerX, centerY, p.x, p.y, 'rgba(204, 224, 248, 0.38)', 0.9, focusPid * 17 + pid, false); } - const leftX = x + 16; - const detailY = innerY + innerH - 110; - this.drawPanel(leftX, detailY, Math.min(390, w * 0.42), 96, '', { alpha: 0.84, showTitle: false }); this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; this.ctx.font = '10px "Share Tech Mono", monospace'; - this.ctx.fillText(`focus ${String(focus.name || 'proc').slice(0, 18)}:${focusPid}`, leftX + 10, detailY + 18); + this.ctx.fillText('HOT PROCESSES', leftX + 12, innerY + 56); + hotProcesses.forEach((node, idx) => { + const pid = Number(node.pid || 0); + const rowY = innerY + 78 + idx * 32; + const rowActive = pid === focusPid; + if (rowActive) { + this.ctx.fillStyle = 'rgba(124, 178, 255, 0.14)'; + this.ctx.fillRect(leftX + 8, rowY - 15, sideW - 16, 26); + } + const rowPressure = Math.max(0, Math.min(100, Number(node.syscall_pressure || 0))); + this.ctx.fillStyle = rowActive ? '#eaf5ff' : 'rgba(205, 222, 245, 0.92)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`${idx + 1}. ${String(node.name || 'proc').slice(0, 15)}:${pid}`, leftX + 12, rowY - 2); + this.ctx.fillStyle = 'rgba(80, 108, 140, 0.55)'; + this.ctx.fillRect(leftX + 12, rowY + 6, sideW - 72, 4); + this.ctx.fillStyle = rowPressure >= 70 ? 'rgba(255, 130, 130, 0.88)' : 'rgba(116, 205, 255, 0.82)'; + this.ctx.fillRect(leftX + 12, rowY + 6, (sideW - 72) * (rowPressure / 100), 4); + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.88)'; + this.ctx.fillText(`${rowPressure.toFixed(0)}%`, leftX + sideW - 44, rowY + 10); + this.nodeHitAreas.push({ x: leftX + sideW * 0.5, y: rowY - 4, r: sideW * 0.45, pid }); + }); + + const detailY = innerY + innerH - 126; + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('FOCUS DETAIL', leftX + 12, detailY); this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; this.ctx.font = '9px "Share Tech Mono", monospace'; - this.ctx.fillText(`pressure ${Number(focus.syscall_pressure || 0).toFixed(0)} · rss ${(Number(focus.rss_bytes || 0) / (1024 * 1024)).toFixed(1)}MB`, leftX + 10, detailY + 36); - this.ctx.fillText(`threads ${Number(focus.num_threads || 0)} · fd ${Number(focus.fd_count || 0)} · seccomp ${String(focus.seccomp_mode || 'unknown')}`, leftX + 10, detailY + 52); - this.ctx.fillText(`parent ${parent ? `${String(parent.name || '').slice(0, 10)}:${Number(parent.pid || 0)}` : 'none'} · children ${children.length}`, leftX + 10, detailY + 68); - this.ctx.fillText(`interactions ${uniqNeighbors.length} (ipc/network/file_access)`, leftX + 10, detailY + 84); + this.ctx.fillText(`${String(focus.name || 'proc').slice(0, 18)}:${focusPid}`, leftX + 12, detailY + 20); + this.ctx.fillText(`rss ${(Number(focus.rss_bytes || 0) / (1024 * 1024)).toFixed(1)}MB · fd ${Number(focus.fd_count || 0)}`, leftX + 12, detailY + 36); + this.ctx.fillText(`threads ${threadsCount} · seccomp ${String(focus.seccomp_mode || 'unknown')}`, leftX + 12, detailY + 52); + this.ctx.fillText(`parent ${parent ? `${String(parent.name || '').slice(0, 9)}:${Number(parent.pid || 0)}` : 'none'}`, leftX + 12, detailY + 68); + this.ctx.fillText(`children ${children.length} · links ${uniqNeighbors.length}`, leftX + 12, detailY + 84); + + const barRow = (label, value, max, y0, color) => { + const pct = Math.max(0, Math.min(1, Number(value || 0) / Math.max(1, max))); + this.ctx.fillStyle = 'rgba(205, 232, 255, 0.9)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(label, rightX + 12, y0); + this.ctx.fillStyle = 'rgba(72, 98, 128, 0.48)'; + this.ctx.fillRect(rightX + 94, y0 - 8, sideW - 118, 5); + this.ctx.fillStyle = color; + this.ctx.fillRect(rightX + 94, y0 - 8, (sideW - 118) * pct, 5); + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; + this.ctx.fillText(String(value), rightX + sideW - 28, y0); + }; + + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('LIVE CHANNELS', rightX + 12, innerY + 56); + barRow('syscalls', edgeCounts.syscalls || 0, Math.max(1, edges.length), innerY + 82, 'rgba(136,201,255,0.82)'); + barRow('network', edgeCounts.network || 0, Math.max(1, edges.length), innerY + 106, 'rgba(181,240,255,0.82)'); + barRow('ipc', edgeCounts.ipc || 0, Math.max(1, edges.length), innerY + 130, 'rgba(126,242,210,0.82)'); + barRow('file', edgeCounts.file_access || 0, Math.max(1, edges.length), innerY + 154, 'rgba(255,184,168,0.82)'); + + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('NETWORK TRACE', rightX + 12, innerY + 194); + networkRows.slice(0, 3).forEach((row, idx) => { + const y0 = innerY + 214 + idx * 18; + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(row.name || 'proc').slice(0, 12)}:${Number(row.pid || 0)} c${Number(row.connections || 0)}`, rightX + 12, y0); + this.ctx.fillText(String(row.top_state || 'STATE').slice(0, 12), rightX + sideW - 76, y0); + }); + + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('SECURITY HOOKS', rightX + 12, innerY + 286); + securityHooks.slice(0, 4).forEach((hook, idx) => { + const y0 = innerY + 306 + idx * 18; + const active = String(hook.status || '') === 'active'; + this.ctx.fillStyle = active ? 'rgba(118, 230, 170, 0.9)' : 'rgba(255, 184, 128, 0.86)'; + this.ctx.fillText(active ? 'o' : '-', rightX + 12, y0); + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(hook.name || 'hook').slice(0, 22)}`, rightX + 26, y0); + }); + + const syscallFocus = syscalls.find((row) => Number(row.pid || 0) === focusPid) || syscalls[0]; + if (syscallFocus) { + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('SYSCALL SAMPLE', rightX + 12, innerY + innerH - 108); + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(syscallFocus.name || 'proc').slice(0, 16)}:${Number(syscallFocus.pid || 0)}`, rightX + 12, innerY + innerH - 88); + this.ctx.fillText(`threads ${Number(syscallFocus.num_threads || 0)} · fd ${Number(syscallFocus.fd_count || 0)}`, rightX + 12, innerY + innerH - 72); + this.ctx.fillText(`pressure ${Number(syscallFocus.syscall_pressure || 0).toFixed(0)}%`, rightX + 12, innerY + innerH - 56); + } + + this.drawMicroscopeTimeline(focus, centerX0 + 18, innerY + innerH - 106, centerW - 36, 86); + this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('click process: pin focus · center rings: family, threads, io/ipc/network · right panels: live telemetry', x + 12, y + h - 10); + } + + drawProcessOrbitalMap(nodes, edges, x, y, w, h) { + const innerY = y + 4; + const innerH = h - 8; + this.nodeHitAreas = []; + if (!nodes.length) { + this.ctx.fillStyle = 'rgba(196,207,224,0.72)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('NO PROCESS ORBIT DATA', x + 20, innerY + 28); + return; + } + + const focus = this.getFocusProcess(nodes); + const focusPid = Number(focus?.pid || 0); + const hotProcesses = nodes + .slice() + .sort((a, b) => Number(b.syscall_pressure || 0) - Number(a.syscall_pressure || 0)) + .slice(0, 9); + const networkRows = Array.isArray(this.telemetry?.network_tracing) ? this.telemetry.network_tracing : []; + const securityHooks = Array.isArray(this.telemetry?.security_hooks) ? this.telemetry.security_hooks : []; + const edgeCounts = edges.reduce((acc, edge) => { + const type = String(edge.type || 'link'); + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + const totalEdges = Math.max(1, edges.length); + const kernelX = x + w * 0.18; + const kernelY = innerY + innerH * 0.52; + const kernelR = Math.min(w, innerH) * 0.19; + + this.ctx.fillStyle = 'rgba(126, 190, 230, 0.08)'; + for (let i = 0; i < 44; i += 1) { + const px = x + 36 + this.stableUnit(i * 137) * (w - 72); + const py = innerY + 44 + this.stableUnit(i * 271) * (innerH - 120); + const blink = 0.22 + 0.18 * Math.sin(this.tick * 0.018 + i); + this.ctx.globalAlpha = blink; + this.ctx.fillRect(px, py, 2, 2); + } + this.ctx.globalAlpha = 1; + + this.ctx.strokeStyle = 'rgba(120, 215, 255, 0.26)'; + this.ctx.lineWidth = 1; + this.ctx.strokeRect(x + 0.5, innerY + 0.5, w - 1, innerH - 1); + this.ctx.fillStyle = 'rgba(198, 230, 255, 0.86)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + const tabs = ['AUTONOMOUS', 'PROC CONTROL', 'IPC CHANNEL', 'VFS ROUTES', 'NET TRACE', 'SECURITY MAP', 'SCHEDULER', 'SETTINGS']; + tabs.forEach((tab, idx) => { + const tx = x + 52 + idx * Math.min(118, w / 9); + this.ctx.strokeStyle = 'rgba(70, 156, 200, 0.42)'; + this.ctx.strokeRect(tx, innerY + 8, Math.min(104, w / 10), 12); + this.ctx.fillText(tab, tx + 5, innerY + 17); + }); + + const planetGradient = this.ctx.createRadialGradient(kernelX - kernelR * 0.32, kernelY - kernelR * 0.36, kernelR * 0.08, kernelX, kernelY, kernelR); + planetGradient.addColorStop(0, 'rgba(220, 255, 250, 0.98)'); + planetGradient.addColorStop(0.36, 'rgba(116, 224, 224, 0.9)'); + planetGradient.addColorStop(0.78, 'rgba(40, 118, 148, 0.72)'); + planetGradient.addColorStop(1, 'rgba(16, 43, 66, 0.94)'); + this.ctx.beginPath(); + this.ctx.arc(kernelX, kernelY, kernelR, 0, Math.PI * 2); + this.ctx.fillStyle = planetGradient; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(196, 255, 244, 0.72)'; + this.ctx.lineWidth = 1.2; + this.ctx.stroke(); + for (let i = -4; i <= 4; i += 1) { + const yy = kernelY + i * kernelR * 0.18; + const half = Math.sqrt(Math.max(0, kernelR * kernelR - (yy - kernelY) * (yy - kernelY))); + this.ctx.beginPath(); + this.ctx.moveTo(kernelX - half * 0.85, yy); + this.ctx.quadraticCurveTo(kernelX, yy + Math.sin(i + this.tick * 0.01) * 7, kernelX + half * 0.85, yy); + this.ctx.strokeStyle = 'rgba(220, 255, 248, 0.15)'; + this.ctx.stroke(); + } + this.ctx.beginPath(); + this.ctx.arc(kernelX, kernelY, kernelR * 1.17, -0.28, Math.PI * 1.18); + this.ctx.strokeStyle = 'rgba(120, 230, 255, 0.28)'; + this.ctx.stroke(); + + this.ctx.fillStyle = 'rgba(220, 255, 248, 0.96)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('KERNEL CORE', kernelX - kernelR * 0.18, kernelY - kernelR - 26); + this.ctx.fillStyle = 'rgba(166, 230, 244, 0.9)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`STATUS: ONLINE`, kernelX - kernelR * 0.18, kernelY - kernelR - 12); + this.ctx.fillText(`TASKS ${nodes.length} · EDGES ${edges.length}`, kernelX - kernelR * 0.18, kernelY + kernelR + 22); + + const stationLayout = [ + [0.43, 0.26], [0.56, 0.18], [0.68, 0.35], [0.81, 0.22], [0.9, 0.42], + [0.47, 0.62], [0.62, 0.72], [0.76, 0.62], [0.9, 0.72] + ]; + const stationPos = new Map(); + hotProcesses.forEach((node, idx) => { + const [fx, fy] = stationLayout[idx] || [0.45 + idx * 0.05, 0.5]; + const px = x + w * fx; + const py = innerY + innerH * fy; + const pid = Number(node.pid || 0); + const pressure = Math.max(0, Math.min(100, Number(node.syscall_pressure || 0))); + const active = pid === focusPid; + stationPos.set(pid, { x: px, y: py, node }); + + this.ctx.setLineDash([3, 4]); + this.ctx.beginPath(); + this.ctx.moveTo(kernelX + kernelR * 0.72, kernelY - kernelR * 0.16 + idx * 5); + const mx = kernelX + (px - kernelX) * 0.52; + this.ctx.lineTo(mx, py); + this.ctx.lineTo(px - 34, py); + this.ctx.strokeStyle = active ? 'rgba(255, 225, 130, 0.72)' : 'rgba(120, 215, 255, 0.24)'; + this.ctx.lineWidth = active ? 1.4 : 0.8; + this.ctx.stroke(); + this.ctx.setLineDash([]); + + const nodeR = active ? 10 : 7; + const nodeColor = pressure >= 70 ? 'rgba(255, 136, 118, 0.96)' : (pressure >= 38 ? 'rgba(255, 204, 118, 0.94)' : 'rgba(105, 232, 198, 0.94)'); + const halo = this.ctx.createRadialGradient(px, py, 0, px, py, 42); + halo.addColorStop(0, active ? 'rgba(255, 230, 130, 0.26)' : 'rgba(120, 230, 255, 0.16)'); + halo.addColorStop(1, 'rgba(120, 230, 255, 0)'); + this.ctx.beginPath(); + this.ctx.arc(px, py, 42, 0, Math.PI * 2); + this.ctx.fillStyle = halo; + this.ctx.fill(); + this.ctx.beginPath(); + this.ctx.arc(px, py, nodeR, 0, Math.PI * 2); + this.ctx.fillStyle = nodeColor; + this.ctx.fill(); + this.ctx.strokeStyle = active ? 'rgba(255,255,230,0.98)' : 'rgba(220,246,255,0.82)'; + this.ctx.stroke(); + this.nodeHitAreas.push({ x: px, y: py, r: 16, pid }); + + this.drawPanel(px + 14, py - 28, 190, 54, '', { alpha: active ? 0.82 : 0.62, showTitle: false }); + this.ctx.fillStyle = active ? '#fff7bf' : 'rgba(212, 238, 255, 0.94)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`PROC: ${String(node.name || 'proc').slice(0, 14)}`, px + 24, py - 10); + this.ctx.fillStyle = 'rgba(150, 220, 238, 0.9)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`PID ${pid} · STATUS: ${pressure >= 70 ? 'HOT' : 'ONLINE'}`, px + 24, py + 5); + this.ctx.fillText(`pressure ${pressure.toFixed(0)} · fd ${Number(node.fd_count || 0)} · th ${Number(node.num_threads || 0)}`, px + 24, py + 18); + }); + + edges.slice(0, 70).forEach((edge, idx) => { + const s = stationPos.get(Number(edge.source || 0)); + const t = stationPos.get(Number(edge.target || 0)); + if (!s || !t) return; + this.ctx.globalAlpha = 0.24 + Math.min(0.32, Number(edge.weight || 0) * 0.22); + this.drawCurvedStroke(s.x, s.y, t.x, t.y, this.edgeColor(String(edge.type || '')), 0.8, idx * 31, false); + this.ctx.globalAlpha = 1; + }); + + const legendX = x + w * 0.48; + const legendY = innerY + innerH - 54; + this.drawPanel(legendX, legendY, Math.min(420, w * 0.34), 36, '', { alpha: 0.66, showTitle: false }); + [ + ['SYSCALL', edgeCounts.syscalls || 0, 'rgba(136,201,255,0.9)'], + ['NETWORK', edgeCounts.network || 0, 'rgba(181,240,255,0.9)'], + ['IPC', edgeCounts.ipc || 0, 'rgba(126,242,210,0.9)'], + ['FILE', edgeCounts.file_access || 0, 'rgba(255,184,168,0.9)'] + ].forEach((item, idx) => { + const lx = legendX + 14 + idx * 96; + this.ctx.fillStyle = item[2]; + this.ctx.fillRect(lx, legendY + 13, 8, 8); + this.ctx.fillStyle = 'rgba(207, 229, 246, 0.92)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${item[0]} ${item[1]}`, lx + 14, legendY + 20); + }); + + const rightPanelX = x + w - 172; + this.drawPanel(rightPanelX, innerY + innerH - 202, 148, 136, '', { alpha: 0.62, showTitle: false }); + this.ctx.fillStyle = 'rgba(212, 238, 255, 0.94)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('LIVE TELEMETRY', rightPanelX + 10, innerY + innerH - 184); + const net = networkRows[0]; + this.ctx.fillStyle = 'rgba(160, 205, 228, 0.88)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`routes ${totalEdges}`, rightPanelX + 10, innerY + innerH - 164); + this.ctx.fillText(`net ${net ? `${String(net.name || 'proc').slice(0, 9)} c${Number(net.connections || 0)}` : 'none'}`, rightPanelX + 10, innerY + innerH - 148); + securityHooks.slice(0, 3).forEach((hook, idx) => { + const active = String(hook.status || '') === 'active'; + this.ctx.fillStyle = active ? 'rgba(105, 230, 170, 0.9)' : 'rgba(255, 190, 120, 0.86)'; + this.ctx.fillText(`${active ? 'o' : '-'} ${String(hook.name || 'hook').slice(0, 15)}`, rightPanelX + 10, innerY + innerH - 126 + idx * 16); + }); + + this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('orbital process map: kernel core -> process stations -> ipc/network/file routes · click station to pin focus', x + 12, y + h - 10); + } + + buildSemanticOpsForProcess(node, networkRows, securityHooks) { + const pid = Number(node?.pid || 0); + const semanticRows = Array.isArray(this.telemetry?.semantic_ops) ? this.telemetry.semantic_ops : []; + const backendRow = semanticRows.find((row) => Number(row?.pid || 0) === pid); + if (backendRow && Array.isArray(backendRow.ops) && backendRow.ops.length) { + return backendRow.ops.slice(0, 8).map((op) => ({ + label: String(op?.label || 'kernel op'), + type: String(op?.type || 'task'), + active: op?.active !== false, + weight: Number(op?.weight || 1), + source: String(op?.source || backendRow.source || 'backend'), + evidence: op?.evidence || {} + })); + } + const name = String(node?.name || 'process').toLowerCase(); + const fdCount = Number(node?.fd_count || 0); + const threads = Number(node?.num_threads || 0); + const pressure = Number(node?.syscall_pressure || 0); + const netRow = networkRows.find((row) => Number(row.pid || 0) === pid); + const connections = Number(netRow?.connections || node?.connections || 0); + const hasNetworkName = /nginx|node|python|gunicorn|curl|ssh|agent|chrome|firefox|http|tcp/.test(name); + const activeSecurity = securityHooks.some((hook) => String(hook.status || '') === 'active'); + const ops = [ + { label: 'sched_pick_next()', type: 'sched', active: true, weight: Math.max(threads, 1) }, + { label: 'clone/fork lineage', type: 'task', active: Number(node?.ppid || 0) > 0, weight: 1 }, + { label: 'seccomp/LSM check', type: 'security', active: activeSecurity || String(node?.seccomp_mode || '') !== 'unknown', weight: activeSecurity ? 2 : 1 }, + { label: 'epoll_wait()', type: 'event', active: fdCount > 8 || connections > 0 || /nginx|node|gunicorn/.test(name), weight: fdCount }, + { label: 'socket()', type: 'network', active: connections > 0 || hasNetworkName, weight: connections }, + { label: 'connect()/accept()', type: 'network', active: connections > 0, weight: connections }, + { label: 'nf_conntrack', type: 'netfilter', active: connections > 0, weight: Number(netRow?.unique_peers || 0) }, + { label: 'tcp retransmission', type: 'tcp', active: connections > 0 && pressure >= 35, weight: pressure }, + { label: 'TLS handshake', type: 'crypto', active: /nginx|curl|chrome|firefox|http|ssl|tls/.test(name), weight: pressure }, + { label: 'sendfile()/splice()', type: 'vfs', active: fdCount >= 12 || /nginx|http/.test(name), weight: fdCount }, + ]; + return ops.filter((op) => op.active).slice(0, 8); + } + + semanticOpColor(type, active = true) { + const alpha = active ? 0.94 : 0.42; + if (type === 'sched') return `rgba(132, 205, 255, ${alpha})`; + if (type === 'task') return `rgba(186, 210, 242, ${alpha})`; + if (type === 'security') return `rgba(125, 235, 174, ${alpha})`; + if (type === 'event') return `rgba(255, 216, 128, ${alpha})`; + if (type === 'network') return `rgba(124, 232, 255, ${alpha})`; + if (type === 'netfilter') return `rgba(168, 146, 255, ${alpha})`; + if (type === 'tcp') return `rgba(255, 146, 118, ${alpha})`; + if (type === 'crypto') return `rgba(238, 186, 255, ${alpha})`; + if (type === 'vfs') return `rgba(255, 188, 132, ${alpha})`; + return `rgba(190, 220, 245, ${alpha})`; + } + + drawSemanticKernelGraphEngine(nodes, edges, x, y, w, h) { + const innerY = y + 4; + const innerH = h - 8; + this.nodeHitAreas = []; + if (!nodes.length) { + this.ctx.fillStyle = 'rgba(196,207,224,0.72)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('NO SEMANTIC KERNEL GRAPH DATA', x + 20, innerY + 28); + return; + } + + const focus = this.getFocusProcess(nodes); + const focusPid = Number(focus?.pid || 0); + const networkRows = Array.isArray(this.telemetry?.network_tracing) ? this.telemetry.network_tracing : []; + const securityHooks = Array.isArray(this.telemetry?.security_hooks) ? this.telemetry.security_hooks : []; + const hotProcesses = nodes + .slice() + .sort((a, b) => Number(b.syscall_pressure || 0) - Number(a.syscall_pressure || 0)) + .slice(0, 7); + const edgeCounts = edges.reduce((acc, edge) => { + const type = String(edge.type || 'link'); + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + + const kernelX = x + w * 0.17; + const kernelY = innerY + innerH * 0.54; + const kernelR = Math.min(w, innerH) * 0.17; + const graphX0 = x + w * 0.36; + const graphW = w * 0.58; + const rowTop = innerY + 86; + const rowStep = Math.min(74, Math.max(56, (innerH - 178) / Math.max(1, hotProcesses.length))); + + this.ctx.fillStyle = 'rgba(2, 6, 12, 0.72)'; + this.ctx.fillRect(x, innerY, w, innerH); + this.ctx.strokeStyle = 'rgba(76, 164, 205, 0.16)'; + this.ctx.lineWidth = 1; + for (let gx = x + 28; gx < x + w; gx += 44) { + this.ctx.beginPath(); + this.ctx.moveTo(gx + 0.5, innerY); + this.ctx.lineTo(gx + 0.5, innerY + innerH); + this.ctx.stroke(); + } + for (let gy = innerY + 28; gy < innerY + innerH; gy += 38) { + this.ctx.beginPath(); + this.ctx.moveTo(x, gy + 0.5); + this.ctx.lineTo(x + w, gy + 0.5); + this.ctx.stroke(); + } + + this.ctx.strokeStyle = 'rgba(110, 220, 255, 0.32)'; + this.ctx.strokeRect(x + 0.5, innerY + 0.5, w - 1, innerH - 1); + this.ctx.fillStyle = 'rgba(210, 242, 255, 0.96)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('SEMANTIC KERNEL GRAPH ENGINE', x + 18, innerY + 20); + this.ctx.fillStyle = 'rgba(126, 210, 235, 0.78)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`REALTIME LINUX GRAPH: process -> syscall -> subsystem -> effect · focus pid ${focusPid || '-'}`, x + 18, innerY + 36); + ['PROCESS', 'SYSCALL', 'SECURITY', 'NETFILTER', 'TCP/TLS', 'EVENT LOOP', 'VFS'].forEach((label, idx) => { + const tx = graphX0 + idx * (graphW / 7); + this.ctx.strokeStyle = 'rgba(70, 156, 200, 0.42)'; + this.ctx.strokeRect(tx, innerY + 14, Math.min(92, graphW / 8), 14); + this.ctx.fillStyle = 'rgba(165, 225, 245, 0.82)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(label, tx + 5, innerY + 24); + }); + + const coreGradient = this.ctx.createRadialGradient(kernelX - kernelR * 0.28, kernelY - kernelR * 0.32, kernelR * 0.05, kernelX, kernelY, kernelR); + coreGradient.addColorStop(0, 'rgba(230,255,250,0.98)'); + coreGradient.addColorStop(0.34, 'rgba(112,231,222,0.9)'); + coreGradient.addColorStop(0.72, 'rgba(36,118,148,0.72)'); + coreGradient.addColorStop(1, 'rgba(10,30,55,0.96)'); + this.ctx.beginPath(); + this.ctx.arc(kernelX, kernelY, kernelR, 0, Math.PI * 2); + this.ctx.fillStyle = coreGradient; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(196, 255, 244, 0.72)'; + this.ctx.lineWidth = 1.2; + this.ctx.stroke(); + for (let i = -4; i <= 4; i += 1) { + const yy = kernelY + i * kernelR * 0.18; + const half = Math.sqrt(Math.max(0, kernelR * kernelR - (yy - kernelY) * (yy - kernelY))); + this.ctx.beginPath(); + this.ctx.moveTo(kernelX - half * 0.85, yy); + this.ctx.quadraticCurveTo(kernelX, yy + Math.sin(i + this.tick * 0.01) * 7, kernelX + half * 0.85, yy); + this.ctx.strokeStyle = 'rgba(220, 255, 248, 0.15)'; + this.ctx.stroke(); + } + this.ctx.fillStyle = 'rgba(220, 255, 248, 0.96)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('LINUX KERNEL CORE', kernelX - kernelR * 0.34, kernelY - kernelR - 22); + this.ctx.fillStyle = 'rgba(166, 230, 244, 0.9)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`TASKS ${nodes.length} · RELATIONS ${edges.length}`, kernelX - kernelR * 0.22, kernelY + kernelR + 22); + + const opPositions = []; + hotProcesses.forEach((node, rowIdx) => { + const pid = Number(node.pid || 0); + const rowY = rowTop + rowIdx * rowStep; + const pressure = Math.max(0, Math.min(100, Number(node.syscall_pressure || 0))); + const active = pid === focusPid; + const ops = this.buildSemanticOpsForProcess(node, networkRows, securityHooks); + const processX = graphX0; + const processColor = pressure >= 70 ? 'rgba(255, 132, 112, 0.96)' : (active ? 'rgba(255, 230, 132, 0.96)' : 'rgba(112, 232, 204, 0.92)'); + + this.ctx.setLineDash([3, 5]); + this.ctx.beginPath(); + this.ctx.moveTo(kernelX + kernelR * 0.85, kernelY - kernelR * 0.2 + rowIdx * 9); + this.ctx.lineTo(processX - 40, rowY); + this.ctx.strokeStyle = active ? 'rgba(255, 232, 135, 0.64)' : 'rgba(120, 215, 255, 0.22)'; + this.ctx.lineWidth = active ? 1.4 : 0.8; + this.ctx.stroke(); + this.ctx.setLineDash([]); + + this.ctx.beginPath(); + this.ctx.arc(processX, rowY, active ? 9 : 7, 0, Math.PI * 2); + this.ctx.fillStyle = processColor; + this.ctx.fill(); + this.ctx.strokeStyle = active ? 'rgba(255,255,230,0.98)' : 'rgba(220,246,255,0.82)'; + this.ctx.stroke(); + this.nodeHitAreas.push({ x: processX, y: rowY, r: 16, pid }); + this.ctx.fillStyle = active ? '#fff7bf' : 'rgba(212, 238, 255, 0.94)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(node.name || 'process').slice(0, 15)}:${pid}`, processX + 14, rowY - 6); + this.ctx.fillStyle = 'rgba(150, 220, 238, 0.88)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`pressure ${pressure.toFixed(0)} · fd ${Number(node.fd_count || 0)} · th ${Number(node.num_threads || 0)}`, processX + 14, rowY + 8); + + let prev = { x: processX, y: rowY }; + ops.forEach((op, opIdx) => { + const px = graphX0 + 110 + opIdx * Math.min(112, (graphW - 130) / Math.max(1, ops.length)); + const py = rowY + Math.sin(this.tick * 0.015 + rowIdx + opIdx) * 3; + this.ctx.beginPath(); + this.ctx.moveTo(prev.x + 8, prev.y); + this.ctx.lineTo(px - 10, py); + this.ctx.strokeStyle = this.semanticOpColor(op.type, op.active).replace('0.94', active ? '0.72' : '0.34'); + this.ctx.lineWidth = active ? 1.4 : 0.9; + this.ctx.stroke(); + + const r = active ? 5.2 : 4.2; + this.ctx.beginPath(); + this.ctx.arc(px, py, r, 0, Math.PI * 2); + this.ctx.fillStyle = this.semanticOpColor(op.type, true); + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(235,248,255,0.74)'; + this.ctx.stroke(); + this.ctx.fillStyle = active ? 'rgba(245, 252, 255, 0.96)' : 'rgba(190, 215, 235, 0.82)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(op.label, px + 8, py + 3); + opPositions.push({ x: px, y: py, type: op.type, active }); + prev = { x: px, y: py }; + }); + }); + + opPositions.forEach((a, idx) => { + const b = opPositions.slice(idx + 1).find((candidate) => candidate.type === a.type && Math.abs(candidate.y - a.y) > 20); + if (!b) return; + this.ctx.globalAlpha = a.active || b.active ? 0.18 : 0.08; + this.drawCurvedStroke(a.x, a.y, b.x, b.y, this.semanticOpColor(a.type, true), 0.55, idx * 19, false); + this.ctx.globalAlpha = 1; + }); + + const legendX = x + w * 0.36; + const legendY = innerY + innerH - 48; + this.drawPanel(legendX, legendY, Math.min(560, w * 0.44), 34, '', { alpha: 0.66, showTitle: false }); + [ + ['sched', 'scheduler', this.semanticOpColor('sched')], + ['sec', 'LSM/seccomp', this.semanticOpColor('security')], + ['net', 'socket/tcp', this.semanticOpColor('network')], + ['tls', 'crypto path', this.semanticOpColor('crypto')], + ['vfs', 'sendfile/vfs', this.semanticOpColor('vfs')] + ].forEach((item, idx) => { + const lx = legendX + 14 + idx * 104; + this.ctx.fillStyle = item[2]; + this.ctx.fillRect(lx, legendY + 12, 8, 8); + this.ctx.fillStyle = 'rgba(207, 229, 246, 0.92)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(item[1], lx + 14, legendY + 19); + }); + + const statusX = x + w - 190; + this.drawPanel(statusX, innerY + innerH - 158, 166, 96, '', { alpha: 0.62, showTitle: false }); + this.ctx.fillStyle = 'rgba(212, 238, 255, 0.94)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('ENGINE SIGNALS', statusX + 10, innerY + innerH - 140); + this.ctx.fillStyle = 'rgba(160, 205, 228, 0.88)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`syscalls ${edgeCounts.syscalls || 0}`, statusX + 10, innerY + innerH - 120); + this.ctx.fillText(`network ${edgeCounts.network || 0}`, statusX + 10, innerY + innerH - 104); + this.ctx.fillText(`ipc ${edgeCounts.ipc || 0}`, statusX + 10, innerY + innerH - 88); + this.ctx.fillText(`file ${edgeCounts.file_access || 0}`, statusX + 10, innerY + innerH - 72); - this.drawMicroscopeTimeline(focus, x + w * 0.56, innerY + innerH - 96, w * 0.4, 84); this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; this.ctx.font = '9px "Share Tech Mono", monospace'; - this.ctx.fillText('click node: pin focus · rings: parent/child, threads, interactions', x + 12, y + h - 10); + this.ctx.fillText('semantic kernel graph: process -> syscall -> LSM/netfilter/TCP/TLS/event-loop/VFS · click process to pin focus', x + 12, y + h - 10); + } + + drawEbpfKernelModule(coreX, coreY, coreR, x, innerY, w, innerH, securityHooks) { + const activeBpf = securityHooks.some((hook) => /bpf/i.test(String(hook.name || '')) && String(hook.status || '') === 'active'); + const pulse = 0.55 + 0.25 * Math.sin(this.tick * 0.06); + const start = -0.55; + const end = 0.2; + const r0 = coreR * 0.34; + const r1 = coreR * 0.96; + + this.ctx.beginPath(); + this.ctx.arc(coreX, coreY, r1, start, end); + this.ctx.arc(coreX, coreY, r0, end, start, true); + this.ctx.closePath(); + this.ctx.fillStyle = activeBpf ? `rgba(90, 255, 190, ${0.22 + pulse * 0.18})` : 'rgba(90, 185, 220, 0.18)'; + this.ctx.fill(); + this.ctx.strokeStyle = activeBpf ? 'rgba(135, 255, 205, 0.78)' : 'rgba(120, 220, 255, 0.42)'; + this.ctx.lineWidth = activeBpf ? 1.6 : 1; + this.ctx.stroke(); + + for (let i = 0; i < 5; i += 1) { + const a = start + (end - start) * (i / 4); + const px = coreX + Math.cos(a) * (r0 + (r1 - r0) * 0.58); + const py = coreY + Math.sin(a) * (r0 + (r1 - r0) * 0.58); + this.ctx.beginPath(); + this.ctx.arc(px, py, i === 2 ? 3.2 : 2.2, 0, Math.PI * 2); + this.ctx.fillStyle = activeBpf ? 'rgba(190, 255, 220, 0.95)' : 'rgba(130, 210, 235, 0.72)'; + this.ctx.fill(); + } + + const panelX = x + Math.min(w * 0.31, coreX + coreR * 0.96); + const panelY = coreY - coreR * 0.08; + this.ctx.setLineDash([3, 4]); + this.ctx.beginPath(); + this.ctx.moveTo(coreX + Math.cos(-0.12) * r1, coreY + Math.sin(-0.12) * r1); + this.ctx.lineTo(panelX - 12, panelY + 22); + this.ctx.strokeStyle = activeBpf ? 'rgba(135, 255, 205, 0.58)' : 'rgba(120, 220, 255, 0.28)'; + this.ctx.stroke(); + this.ctx.setLineDash([]); + + this.drawPanel(panelX, panelY, 178, 82, '', { alpha: activeBpf ? 0.72 : 0.56, showTitle: false }); + this.ctx.fillStyle = activeBpf ? 'rgba(175, 255, 214, 0.96)' : 'rgba(190, 230, 245, 0.9)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('eBPF EMBEDDED MODULE', panelX + 10, panelY + 15); + this.ctx.fillStyle = activeBpf ? 'rgba(120, 255, 202, 0.94)' : 'rgba(150, 205, 224, 0.82)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + const rows = [ + ['loader', 'userspace -> bpf()'], + ['verifier', 'safety proof'], + ['maps', 'shared state'], + ['JIT', 'native kernel code'], + ['hooks', 'kprobe/tc/tracepoint'] + ]; + rows.forEach((row, idx) => { + const yy = panelY + 30 + idx * 10; + this.ctx.fillStyle = idx === 1 ? 'rgba(255, 232, 145, 0.92)' : (activeBpf ? 'rgba(170, 245, 220, 0.86)' : 'rgba(150, 205, 224, 0.72)'); + this.ctx.fillText(`${row[0]}: ${row[1]}`.slice(0, 34), panelX + 10, yy); + }); + + this.ctx.fillStyle = 'rgba(132, 235, 255, 0.18)'; + for (let i = 0; i < 4; i += 1) { + const mx = x + w * (0.36 + i * 0.1); + const my = innerY + innerH * (0.21 + i * 0.12); + this.ctx.beginPath(); + this.ctx.arc(mx, my, 5 + i, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(120, 235, 255, 0.18)'; + this.ctx.stroke(); + } + } + + drawReferenceSemanticKernelGraph(nodes, edges, x, y, w, h) { + const innerY = y + 4; + const innerH = h - 8; + this.nodeHitAreas = []; + if (!nodes.length) { + this.ctx.fillStyle = 'rgba(196,207,224,0.72)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('NO SEMANTIC KERNEL GRAPH DATA', x + 20, innerY + 28); + return; + } + + const focus = this.getFocusProcess(nodes); + const focusPid = Number(focus?.pid || 0); + const networkRows = Array.isArray(this.telemetry?.network_tracing) ? this.telemetry.network_tracing : []; + const securityHooks = Array.isArray(this.telemetry?.security_hooks) ? this.telemetry.security_hooks : []; + const edgeCounts = edges.reduce((acc, edge) => { + const type = String(edge.type || 'link'); + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + const rankedProcesses = nodes + .slice() + .sort((a, b) => Number(b.syscall_pressure || 0) - Number(a.syscall_pressure || 0)) + .slice(0, 8); + const hotProcesses = rankedProcesses.some((node) => Number(node.pid || 0) === focusPid) || !focus + ? rankedProcesses + : [focus, ...rankedProcesses.filter((node) => Number(node.pid || 0) !== focusPid)].slice(0, 8); + + const coreX = x + w * 0.04; + const coreY = innerY + innerH * 0.56; + const coreR = Math.min(w, innerH) * 0.28; + const stationLayout = [ + [0.38, 0.28], [0.55, 0.20], [0.68, 0.37], [0.82, 0.24], + [0.43, 0.62], [0.58, 0.74], [0.75, 0.64], [0.90, 0.46] + ]; + + this.ctx.fillStyle = 'rgba(2, 6, 12, 0.88)'; + this.ctx.fillRect(x, innerY, w, innerH); + const bgGlow = this.ctx.createRadialGradient(x + w * 0.4, innerY + innerH * 0.42, 0, x + w * 0.4, innerY + innerH * 0.42, w * 0.72); + bgGlow.addColorStop(0, 'rgba(36, 84, 104, 0.24)'); + bgGlow.addColorStop(0.45, 'rgba(8, 18, 28, 0.24)'); + bgGlow.addColorStop(1, 'rgba(0, 0, 0, 0)'); + this.ctx.fillStyle = bgGlow; + this.ctx.fillRect(x, innerY, w, innerH); + + this.ctx.strokeStyle = 'rgba(52, 118, 150, 0.16)'; + this.ctx.lineWidth = 1; + for (let gx = x + 18; gx < x + w; gx += 34) { + this.ctx.beginPath(); + this.ctx.moveTo(gx + 0.5, innerY); + this.ctx.lineTo(gx + 0.5, innerY + innerH); + this.ctx.stroke(); + } + for (let gy = innerY + 18; gy < innerY + innerH; gy += 30) { + this.ctx.beginPath(); + this.ctx.moveTo(x, gy + 0.5); + this.ctx.lineTo(x + w, gy + 0.5); + this.ctx.stroke(); + } + + this.ctx.strokeStyle = 'rgba(66, 178, 220, 0.42)'; + this.ctx.strokeRect(x + 0.5, innerY + 0.5, w - 1, innerH - 1); + const topTabs = ['AUTONOMIC', 'PROC GRAPH', 'EBPF VM', 'IPC CHAIN', 'NET FILTER', 'TLS MAP', 'VFS ROUTES', 'SECURITY']; + topTabs.forEach((tab, idx) => { + const tw = Math.min(104, (w - 120) / topTabs.length); + const tx = x + 52 + idx * (tw + 6); + this.ctx.strokeStyle = 'rgba(67, 165, 215, 0.52)'; + this.ctx.strokeRect(tx, innerY + 8, tw, 13); + this.ctx.fillStyle = idx === 1 ? 'rgba(205, 244, 255, 0.95)' : 'rgba(125, 201, 228, 0.82)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText(tab, tx + 5, innerY + 18); + }); + + for (let i = 0; i < 8; i += 1) { + const by = innerY + 42 + i * 24; + this.ctx.strokeStyle = 'rgba(0, 228, 185, 0.38)'; + this.ctx.strokeRect(x + 4, by, 26, 16); + this.ctx.fillStyle = i % 3 === 0 ? 'rgba(120, 235, 190, 0.42)' : 'rgba(22, 82, 96, 0.62)'; + this.ctx.fillRect(x + 7, by + 3, 20, 10); + } + + for (let i = 0; i < 54; i += 1) { + const px = x + 40 + this.stableUnit(i * 977) * (w - 80); + const py = innerY + 44 + this.stableUnit(i * 431) * (innerH - 120); + this.ctx.globalAlpha = 0.08 + 0.14 * this.stableUnit(i * 71); + this.ctx.fillStyle = i % 4 === 0 ? 'rgba(255, 126, 110, 0.9)' : 'rgba(85, 230, 205, 0.9)'; + this.ctx.fillRect(px, py, 2, 2); + } + this.ctx.globalAlpha = 1; + + const coreGradient = this.ctx.createRadialGradient(coreX + coreR * 0.18, coreY - coreR * 0.32, coreR * 0.04, coreX, coreY, coreR); + coreGradient.addColorStop(0, 'rgba(230, 255, 248, 0.98)'); + coreGradient.addColorStop(0.36, 'rgba(110, 232, 224, 0.9)'); + coreGradient.addColorStop(0.72, 'rgba(39, 133, 154, 0.74)'); + coreGradient.addColorStop(1, 'rgba(8, 32, 54, 0.98)'); + this.ctx.beginPath(); + this.ctx.arc(coreX, coreY, coreR, 0, Math.PI * 2); + this.ctx.fillStyle = coreGradient; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(210, 255, 246, 0.72)'; + this.ctx.lineWidth = 1.4; + this.ctx.stroke(); + for (let i = -6; i <= 6; i += 1) { + const yy = coreY + i * coreR * 0.12; + const half = Math.sqrt(Math.max(0, coreR * coreR - (yy - coreY) * (yy - coreY))); + this.ctx.beginPath(); + this.ctx.moveTo(coreX - half * 0.88, yy); + this.ctx.quadraticCurveTo(coreX, yy + Math.sin(i + this.tick * 0.01) * 8, coreX + half * 0.88, yy); + this.ctx.strokeStyle = 'rgba(220, 255, 248, 0.14)'; + this.ctx.stroke(); + } + this.ctx.beginPath(); + this.ctx.arc(coreX, coreY, coreR * 1.18, -0.36, Math.PI * 1.12); + this.ctx.strokeStyle = 'rgba(120, 230, 255, 0.32)'; + this.ctx.stroke(); + this.drawEbpfKernelModule(coreX, coreY, coreR, x, innerY, w, innerH, securityHooks); + + this.ctx.fillStyle = 'rgba(220, 255, 248, 0.98)'; + this.ctx.font = '13px "Share Tech Mono", monospace'; + this.ctx.fillText('ESD: LINUX KERNEL', x + 72, coreY - coreR * 0.72); + this.ctx.fillStyle = 'rgba(120, 255, 202, 0.96)'; + this.ctx.font = '13px "Share Tech Mono", monospace'; + this.ctx.fillText('STATUS: ONLINE', x + 72, coreY - coreR * 0.72 + 17); + this.ctx.fillStyle = 'rgba(151, 210, 232, 0.86)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`SEMANTIC GRAPH ACTIVE · TASKS ${nodes.length} · EDGES ${edges.length}`, x + 74, coreY - coreR * 0.72 + 30); + if (focus) { + this.drawPanel(x + 78, coreY + coreR * 0.32, 184, 54, '', { alpha: 0.58, showTitle: false }); + this.ctx.fillStyle = 'rgba(255, 238, 160, 0.96)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('FOCUS TRACE', x + 90, coreY + coreR * 0.32 + 16); + this.ctx.fillStyle = 'rgba(176, 226, 240, 0.9)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(focus.name || 'process').slice(0, 18)}:${focusPid}`, x + 90, coreY + coreR * 0.32 + 31); + this.ctx.fillText(`pressure ${Number(focus.syscall_pressure || 0).toFixed(0)} · fd ${Number(focus.fd_count || 0)}`, x + 90, coreY + coreR * 0.32 + 44); + } + + const stationPoints = []; + hotProcesses.forEach((node, idx) => { + const [fx, fy] = stationLayout[idx] || [0.5, 0.5]; + const px = x + w * fx; + const py = innerY + innerH * fy; + const pid = Number(node.pid || 0); + const pressure = Math.max(0, Math.min(100, Number(node.syscall_pressure || 0))); + const active = pid === focusPid; + const ops = this.buildSemanticOpsForProcess(node, networkRows, securityHooks).slice(0, 5); + stationPoints.push({ x: px, y: py, pid, node }); + + const startX = coreX + coreR * 0.86; + const startY = coreY - coreR * 0.12 + idx * 8; + const bendX = x + w * (0.29 + (idx % 3) * 0.08); + const routePulse = (Math.sin(this.tick * 0.04 + idx) + 1) * 0.5; + this.ctx.setLineDash([2, 3]); + this.ctx.beginPath(); + this.ctx.moveTo(startX, startY); + this.ctx.lineTo(bendX, startY); + this.ctx.lineTo(bendX, py); + this.ctx.lineTo(px - 24, py); + this.ctx.strokeStyle = active ? 'rgba(255, 226, 128, 0.78)' : 'rgba(120, 215, 255, 0.28)'; + this.ctx.lineWidth = active ? 1.5 : 0.8; + this.ctx.stroke(); + this.ctx.setLineDash([]); + if (active) { + this.ctx.shadowColor = 'rgba(255, 226, 128, 0.75)'; + this.ctx.shadowBlur = 12; + this.ctx.beginPath(); + const particleX = bendX + (px - 24 - bendX) * routePulse; + this.ctx.arc(particleX, py, 3.2, 0, Math.PI * 2); + this.ctx.fillStyle = 'rgba(255, 244, 178, 0.95)'; + this.ctx.fill(); + this.ctx.shadowBlur = 0; + } + + const orbitR = active ? 43 : 32; + this.ctx.beginPath(); + this.ctx.ellipse(px, py, orbitR, orbitR * 0.42, this.tick * 0.002 + idx, 0, Math.PI * 2); + this.ctx.strokeStyle = active ? 'rgba(255, 226, 128, 0.36)' : 'rgba(84, 208, 232, 0.22)'; + this.ctx.stroke(); + this.ctx.beginPath(); + this.ctx.arc(px, py, active ? 10 : 7, 0, Math.PI * 2); + this.ctx.fillStyle = pressure >= 70 ? 'rgba(255, 126, 102, 0.96)' : (active ? 'rgba(255, 226, 132, 0.96)' : 'rgba(100, 230, 204, 0.96)'); + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(230, 252, 255, 0.86)'; + this.ctx.stroke(); + this.nodeHitAreas.push({ x: px, y: py, r: 16, pid }); + + const labelW = active ? 204 : 178; + const labelH = 45 + Math.min(3, ops.length) * 10; + const labelX = px > x + w * 0.78 ? px - labelW - 16 : px + 14; + const labelY = py - 28; + this.drawPanel(labelX, labelY, labelW, labelH, '', { alpha: active ? 0.78 : 0.58, showTitle: false }); + if (active) { + this.ctx.strokeStyle = 'rgba(255, 226, 128, 0.7)'; + this.ctx.strokeRect(labelX + 0.5, labelY + 0.5, labelW - 1, labelH - 1); + } + this.ctx.fillStyle = active ? '#fff7bf' : 'rgba(212, 238, 255, 0.94)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`ESD: ${String(node.name || 'process').slice(0, 13).toUpperCase()}`, labelX + 10, labelY + 15); + this.ctx.fillStyle = pressure >= 70 ? 'rgba(255, 160, 126, 0.94)' : 'rgba(120, 255, 202, 0.94)'; + this.ctx.fillText(`STATUS: ${pressure >= 70 ? 'HOT' : 'ONLINE'}`, labelX + 10, labelY + 29); + this.ctx.fillStyle = 'rgba(151, 210, 232, 0.86)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText(`PID ${pid} · P ${pressure.toFixed(0)} · FD ${Number(node.fd_count || 0)}`, labelX + 10, labelY + 40); + ops.slice(0, 3).forEach((op, opIdx) => { + this.ctx.fillStyle = this.semanticOpColor(op.type, true); + this.ctx.fillText(`├ ${op.label}`, labelX + 10, labelY + 52 + opIdx * 10); + }); + }); + + edges.slice(0, 54).forEach((edge, idx) => { + const s = stationPoints.find((p) => p.pid === Number(edge.source || 0)); + const t = stationPoints.find((p) => p.pid === Number(edge.target || 0)); + if (!s || !t) return; + this.ctx.globalAlpha = 0.14 + Math.min(0.28, Number(edge.weight || 0) * 0.18); + this.drawCurvedStroke(s.x, s.y, t.x, t.y, this.edgeColor(String(edge.type || '')), 0.65, idx * 37, false); + this.ctx.globalAlpha = 1; + }); + + const tileY = innerY + innerH - 76; + [ + ['socket()', networkRows.length], + ['epoll_wait()', hotProcesses.filter((node) => Number(node.fd_count || 0) > 8).length], + ['nf_conntrack', edgeCounts.network || 0], + ['sendfile()', edgeCounts.file_access || 0], + ['eBPF', securityHooks.some((h) => /bpf/i.test(String(h.name || '')) && String(h.status || '') === 'active') ? 'ON' : 'VM'], + ['LSM hooks', securityHooks.length] + ].forEach((tile, idx) => { + const tx = x + w * 0.46 + idx * 74; + this.ctx.fillStyle = 'rgba(7, 42, 52, 0.72)'; + this.ctx.fillRect(tx, tileY, 62, 24); + this.ctx.strokeStyle = 'rgba(70, 210, 190, 0.42)'; + this.ctx.strokeRect(tx + 0.5, tileY + 0.5, 61, 23); + this.ctx.fillStyle = 'rgba(142, 244, 218, 0.88)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText(String(tile[0]).toUpperCase(), tx + 5, tileY + 9); + this.ctx.fillStyle = 'rgba(224, 246, 255, 0.9)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(String(tile[1]), tx + 5, tileY + 20); + }); + + const dockY = innerY + innerH - 36; + const dockFill = this.ctx.createLinearGradient(x + 6, dockY, x + w - 6, dockY); + dockFill.addColorStop(0, 'rgba(4, 18, 28, 0.3)'); + dockFill.addColorStop(0.5, 'rgba(22, 78, 88, 0.36)'); + dockFill.addColorStop(1, 'rgba(4, 18, 28, 0.3)'); + this.ctx.fillStyle = dockFill; + this.ctx.fillRect(x + 6, dockY, w - 12, 26); + this.ctx.strokeStyle = 'rgba(74, 184, 224, 0.46)'; + this.ctx.strokeRect(x + 6, dockY, w - 12, 26); + const dockItems = [ + ['SYSCALL', edgeCounts.syscalls || 0, 'rgba(136,201,255,0.9)'], + ['NETWORK', edgeCounts.network || 0, 'rgba(181,240,255,0.9)'], + ['IPC', edgeCounts.ipc || 0, 'rgba(126,242,210,0.9)'], + ['FILE', edgeCounts.file_access || 0, 'rgba(255,184,168,0.9)'], + ['SECURITY', securityHooks.filter((h) => String(h.status || '') === 'active').length, 'rgba(120,235,170,0.9)'] + ]; + dockItems.forEach((item, idx) => { + const bx = x + w * 0.47 + idx * 86; + this.ctx.fillStyle = item[2]; + this.ctx.fillRect(bx, dockY + 8, 9, 9); + this.ctx.fillStyle = 'rgba(207, 229, 246, 0.92)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`${item[0]} ${item[1]}`, bx + 14, dockY + 16); + }); + + const sideX = x + w - 62; + for (let i = 0; i < 4; i += 1) { + const py = innerY + innerH * 0.34 + i * 52; + this.ctx.strokeStyle = 'rgba(74, 184, 224, 0.46)'; + this.ctx.strokeRect(sideX, py, 42, 28); + this.ctx.beginPath(); + this.ctx.moveTo(sideX + 7, py + 19); + for (let sx = 0; sx < 24; sx += 1) { + const vx = sideX + 7 + sx; + const vy = py + 15 + Math.sin(this.tick * 0.06 + sx * 0.7 + i) * 5; + if (sx === 0) this.ctx.moveTo(vx, vy); + else this.ctx.lineTo(vx, vy); + } + this.ctx.strokeStyle = 'rgba(96, 226, 255, 0.62)'; + this.ctx.stroke(); + } + + this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('semantic kernel graph engine: linux core -> process stations -> syscall/netfilter/tcp/tls/vfs chains · click station to pin', x + 12, y + h - 10); + } + + drawMicroscopeGraph(nodes, edges, x, y, w, h) { + this.drawReferenceSemanticKernelGraph(nodes, edges, x, y, w, h); } drawTopStats(x, y, w, h) { @@ -1058,7 +1957,7 @@ class ProcessesSubsystemVisualization { const gap = 16; const top = 58; - const statsH = 98; + const statsH = this.layoutMode === 'microscope' ? 74 : 98; const graphY = top + statsH + gap; const graphH = Math.max(260, h - graphY - 36); @@ -1088,7 +1987,21 @@ class ProcessesSubsystemVisualization { this.drawHudChrome(gap, graphY, w - gap * 2, graphH); this.drawKernelHeader(); - this.drawTopStats(gap, top, w - gap * 2, statsH); + if (this.layoutMode === 'microscope') { + this.drawPanel(gap, top, w - gap * 2, statsH, '', { alpha: 0.68, showTitle: false }); + this.ctx.fillStyle = 'rgba(206, 232, 255, 0.92)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + const graph = this.telemetry?.neural_graph || {}; + const n = Array.isArray(graph.nodes) ? graph.nodes.length : 0; + const e = Array.isArray(graph.edges) ? graph.edges.length : 0; + const seccompPct = Number(this.telemetry?.meta?.seccomp_filter_percent || 0).toFixed(1); + this.ctx.fillText(`scan: tasks ${n} · relations ${e} · seccomp ${seccompPct}%`, gap + 14, top + 24); + this.ctx.fillStyle = 'rgba(150, 194, 225, 0.82)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`mode ${String(this.layoutMode).toUpperCase()} · edge ${String(this.edgeFilter).toUpperCase()} · ${this.autoFocusEnabled ? 'AUTO' : 'MANUAL'} FOCUS`, gap + 14, top + 44); + } else { + this.drawTopStats(gap, top, w - gap * 2, statsH); + } this.drawNeuralGraph(gap, graphY, w - gap * 2, graphH); } diff --git a/templates/linux-memory-subsystem.html b/templates/linux-memory-subsystem.html index 1cad269..28c6895 100644 --- a/templates/linux-memory-subsystem.html +++ b/templates/linux-memory-subsystem.html @@ -87,7 +87,7 @@

Linux kernel memory management

- + - +