diff --git a/app.py b/app.py index a7a528b..ecc2853 100644 --- a/app.py +++ b/app.py @@ -740,6 +740,16 @@ def security_page_legacy(): """Legacy path redirect to Linux security subsystem page.""" return redirect('/linux-security-subsystem', code=301) +@app.route('/linux-processes-subsystem') +def linux_processes_subsystem_page(): + """SEO-friendly Linux processes subsystem page.""" + return render_template('linux-processes-subsystem.html') + +@app.route('/processes') +def processes_page_legacy(): + """Legacy path redirect to Linux processes subsystem page.""" + return redirect('/linux-processes-subsystem', code=301) + @app.route('/api/syscalls-realtime') def syscalls_realtime(): """API for real-time system calls""" @@ -4448,6 +4458,279 @@ def _read_text(path): } } +def collect_processes_realtime(): + """ + Processes subsystem telemetry focused on: + - syscall interception signals + - network tracing + - security hooks + """ + lsm_raw = "" + try: + with open("/sys/kernel/security/lsm", "r", encoding="utf-8", errors="ignore") as f: + lsm_raw = str(f.read().strip()) + except Exception: + lsm_raw = "" + active_lsms = [x.strip() for x in lsm_raw.split(",") if x.strip()] + + yama_scope = "" + try: + with open("/proc/sys/kernel/yama/ptrace_scope", "r", encoding="utf-8", errors="ignore") as f: + yama_scope = str(f.read().strip()) + except Exception: + yama_scope = "" + + syscall_nodes = [] + seccomp_modes = {"none": 0, "strict": 0, "filter": 0, "unknown": 0} + for proc in psutil.process_iter(["pid", "ppid", "name", "username", "cpu_percent", "memory_percent", "num_threads"]): + try: + pid = int(proc.info.get("pid") or 0) + if pid <= 0: + continue + ppid = int(proc.info.get("ppid") or 0) + name = str(proc.info.get("name") or "unknown") + user = str(proc.info.get("username") or "") + cpu = float(proc.info.get("cpu_percent") or 0.0) + mem = float(proc.info.get("memory_percent") or 0.0) + threads = int(proc.info.get("num_threads") or 0) + fd_count = 0 + try: + fd_count = int(proc.num_fds() or 0) + except Exception: + fd_count = 0 + seccomp_mode = "unknown" + with open(f"/proc/{pid}/status", "r", encoding="utf-8", errors="ignore") as f: + for ln in f: + if ln.startswith("Seccomp:"): + raw = ln.split(":", 1)[1].strip() + if raw == "0": + seccomp_mode = "none" + elif raw == "1": + seccomp_mode = "strict" + elif raw == "2": + seccomp_mode = "filter" + else: + seccomp_mode = "unknown" + break + seccomp_modes[seccomp_mode] = seccomp_modes.get(seccomp_mode, 0) + 1 + syscall_pressure = min(100, int(cpu * 1.5 + threads * 0.35 + mem * 0.8)) + syscall_nodes.append({ + "pid": pid, + "ppid": ppid, + "name": name, + "user": user, + "fd_count": fd_count, + "syscall_pressure": syscall_pressure, + "seccomp_mode": seccomp_mode + }) + except Exception: + continue + syscall_nodes.sort(key=lambda x: x.get("syscall_pressure", 0), reverse=True) + syscall_nodes = syscall_nodes[:14] + + network_nodes = {} + try: + for conn in psutil.net_connections(kind="inet"): + pid = int(getattr(conn, "pid", 0) or 0) + if pid <= 0: + continue + remote_ip = "" + try: + raddr = getattr(conn, "raddr", None) + if raddr and len(raddr) >= 1: + remote_ip = str(raddr[0]) + except Exception: + remote_ip = "" + status = str(getattr(conn, "status", "") or "").upper() + bucket = network_nodes.get(pid) + if not bucket: + proc_name = "unknown" + try: + proc_name = psutil.Process(pid).name() + except Exception: + proc_name = "unknown" + bucket = { + "pid": pid, + "name": proc_name, + "connections": 0, + "remote_ips": set(), + "states": {} + } + network_nodes[pid] = bucket + bucket["connections"] += 1 + if remote_ip: + bucket["remote_ips"].add(remote_ip) + if status: + bucket["states"][status] = bucket["states"].get(status, 0) + 1 + except Exception: + pass + + network_tracing = [] + for _, row in network_nodes.items(): + states_sorted = sorted(row["states"].items(), key=lambda kv: kv[1], reverse=True) + top_state = states_sorted[0][0] if states_sorted else "UNKNOWN" + network_tracing.append({ + "pid": int(row["pid"]), + "name": str(row["name"]), + "connections": int(row["connections"]), + "unique_peers": int(len(row["remote_ips"])), + "peer_sample": sorted(list(row["remote_ips"]))[:4], + "top_state": top_state + }) + network_tracing.sort(key=lambda x: (x.get("connections", 0), x.get("unique_peers", 0)), reverse=True) + network_tracing = network_tracing[:14] + + security_hooks = [ + { + "name": "LSM stack", + "status": "active" if active_lsms else "unknown", + "detail": ",".join(active_lsms[:4]) if active_lsms else "n/a" + }, + { + "name": "SELinux/AppArmor engines", + "status": "active" if any(x in {"selinux", "apparmor"} for x in active_lsms) else "inactive", + "detail": "policy-enforcement-path" + }, + { + "name": "BPF LSM", + "status": "active" if "bpf" in active_lsms else "inactive", + "detail": "dynamic-policy-hook" + }, + { + "name": "seccomp filter gate", + "status": "active" if (seccomp_modes.get("filter", 0) + seccomp_modes.get("strict", 0)) > 0 else "inactive", + "detail": f"filter:{seccomp_modes.get('filter', 0)} strict:{seccomp_modes.get('strict', 0)}" + }, + { + "name": "Yama ptrace scope", + "status": "hardened" if yama_scope in {"2", "3"} else ("relaxed" if yama_scope in {"0", "1"} else "unknown"), + "detail": yama_scope or "n/a" + } + ] + + # Neural graph model: nodes=processes, edges=behavior interactions. + node_pool = {} + for row in syscall_nodes[:16]: + pid = int(row.get("pid") or 0) + if pid <= 0: + continue + node_pool[pid] = { + "pid": pid, + "ppid": int(row.get("ppid") or 0), + "name": str(row.get("name") or "unknown"), + "user": str(row.get("user") or ""), + "syscall_pressure": int(row.get("syscall_pressure") or 0), + "fd_count": int(row.get("fd_count") or 0), + "seccomp_mode": str(row.get("seccomp_mode") or "unknown"), + "connections": 0, + "unique_peers": 0 + } + for row in network_tracing[:16]: + pid = int(row.get("pid") or 0) + if pid <= 0: + continue + if pid not in node_pool: + node_pool[pid] = { + "pid": pid, + "ppid": 0, + "name": str(row.get("name") or "unknown"), + "user": "", + "syscall_pressure": 0, + "fd_count": 0, + "seccomp_mode": "unknown", + "connections": 0, + "unique_peers": 0 + } + node_pool[pid]["connections"] = int(row.get("connections") or 0) + node_pool[pid]["unique_peers"] = int(row.get("unique_peers") or 0) + + edges = [] + edge_keys = set() + network_by_pid = {int(r.get("pid") or 0): r for r in network_tracing} + node_pids = sorted(node_pool.keys()) + + def _add_edge(src_pid, dst_pid, edge_type, weight): + src = int(src_pid or 0) + dst = int(dst_pid or 0) + if src <= 0 or dst <= 0 or src == dst: + return + if src not in node_pool or dst not in node_pool: + return + pair = tuple(sorted((src, dst))) + key = (pair[0], pair[1], edge_type) + if key in edge_keys: + return + edge_keys.add(key) + edges.append({ + "source": src, + "target": dst, + "type": edge_type, + "weight": float(max(0.1, min(1.0, weight))) + }) + + # IPC edges: parent-child links inside the sampled set. + for pid, node in node_pool.items(): + ppid = int(node.get("ppid") or 0) + if ppid in node_pool: + _add_edge(pid, ppid, "ipc", 0.72) + + # Syscalls edges: close-pressure processes likely competing on kernel hooks. + sorted_by_pressure = sorted(node_pool.values(), key=lambda n: n.get("syscall_pressure", 0), reverse=True) + for i in range(len(sorted_by_pressure) - 1): + a = sorted_by_pressure[i] + b = sorted_by_pressure[i + 1] + diff = abs(int(a.get("syscall_pressure", 0)) - int(b.get("syscall_pressure", 0))) + weight = 1.0 - min(0.8, diff / 100.0) + _add_edge(int(a.get("pid")), int(b.get("pid")), "syscalls", weight) + + # Network edges: connect nodes that share at least one peer sample. + for i in range(len(node_pids)): + for j in range(i + 1, len(node_pids)): + pa = node_pids[i] + pb = node_pids[j] + ra = network_by_pid.get(pa) or {} + rb = network_by_pid.get(pb) or {} + sa = set(ra.get("peer_sample") or []) + sb = set(rb.get("peer_sample") or []) + if sa and sb and (sa & sb): + _add_edge(pa, pb, "network", 0.88) + + # File access edges: processes with high FD count and same user. + for i in range(len(node_pids)): + for j in range(i + 1, len(node_pids)): + na = node_pool[node_pids[i]] + nb = node_pool[node_pids[j]] + if not na.get("user") or na.get("user") != nb.get("user"): + continue + fa = int(na.get("fd_count") or 0) + fb = int(nb.get("fd_count") or 0) + if fa >= 16 and fb >= 16: + _add_edge(int(na.get("pid")), int(nb.get("pid")), "file_access", 0.64) + + nodes = list(node_pool.values())[:18] + edges = edges[:64] + + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "syscalls_interception": syscall_nodes, + "network_tracing": network_tracing, + "security_hooks": security_hooks, + "neural_graph": { + "nodes": nodes, + "edges": edges + }, + "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" + } + } + @app.route('/api/kernel-dna') def kernel_dna(): """API endpoint for Kernel DNA visualization data""" @@ -4473,6 +4756,14 @@ def security_realtime(): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/api/processes-realtime') +def processes_realtime(): + """Realtime-ish processes interaction feed for processes visualization.""" + try: + return jsonify(collect_processes_realtime()) + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/api/frontend-logs', methods=['POST', 'OPTIONS']) def ingest_frontend_logs(): """Receive frontend logs in ECS-like JSON and append to local JSONL file.""" diff --git a/index.html b/index.html index d99be4a..a000bb4 100755 --- a/index.html +++ b/index.html @@ -96,7 +96,7 @@