sysgraph 0.0.12__tar.gz → 0.0.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sysgraph-0.0.12/src/sysgraph.egg-info → sysgraph-0.0.14}/PKG-INFO +1 -1
- sysgraph-0.0.14/src/sysgraph/__init__.py +1 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/app.py +3 -26
- sysgraph-0.0.14/src/sysgraph/constants.py +36 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/discovery.py +176 -171
- sysgraph-0.0.14/src/sysgraph/dist/assets/index-Ba1ztXgb.css +1 -0
- sysgraph-0.0.14/src/sysgraph/dist/assets/index-DbxwxpDd.js +735 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/dist/index.html +6 -4
- sysgraph-0.0.14/src/sysgraph/graph.py +90 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/main.py +4 -13
- sysgraph-0.0.14/src/sysgraph/model.py +104 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14/src/sysgraph.egg-info}/PKG-INFO +1 -1
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/SOURCES.txt +3 -2
- sysgraph-0.0.12/src/sysgraph/__init__.py +0 -1
- sysgraph-0.0.12/src/sysgraph/dist/assets/index-B0cm-qFh.css +0 -1
- sysgraph-0.0.12/src/sysgraph/dist/assets/index-BaDXONHw.js +0 -735
- sysgraph-0.0.12/src/sysgraph/graph.py +0 -104
- sysgraph-0.0.12/src/sysgraph/model.py +0 -114
- {sysgraph-0.0.12 → sysgraph-0.0.14}/LICENSE +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/MANIFEST.in +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/README.md +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/pyproject.toml +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/setup.cfg +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/setup.py +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/__main__.py +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph/dist/assets/icon-TKtfQOgj.png +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/dependency_links.txt +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/entry_points.txt +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/not-zip-safe +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/requires.txt +0 -0
- {sysgraph-0.0.12 → sysgraph-0.0.14}/src/sysgraph.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.14"
|
|
@@ -71,39 +71,16 @@ def health():
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
@app.get("/api/graph", response_model=GraphSchema)
|
|
74
|
-
def get_graph() ->
|
|
74
|
+
def get_graph() -> dict:
|
|
75
75
|
graph = build_graph()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
LOGGER.debug(graph_dict)
|
|
79
|
-
|
|
80
|
-
return GraphSchema(
|
|
81
|
-
nodes=[
|
|
82
|
-
GraphNodeSchema(
|
|
83
|
-
id=node["id"], type=node["type"], properties=node["properties"]
|
|
84
|
-
)
|
|
85
|
-
for node in graph_dict["nodes"]
|
|
86
|
-
],
|
|
87
|
-
edges=[
|
|
88
|
-
GraphEdgeSchema(
|
|
89
|
-
id=edge["id"],
|
|
90
|
-
source_id=edge["source_id"],
|
|
91
|
-
target_id=edge["target_id"],
|
|
92
|
-
type=edge["type"],
|
|
93
|
-
properties=edge["properties"],
|
|
94
|
-
)
|
|
95
|
-
for edge in graph_dict["edges"]
|
|
96
|
-
],
|
|
97
|
-
)
|
|
76
|
+
return graph.as_dict()
|
|
98
77
|
|
|
99
78
|
|
|
100
79
|
# Serve built assets (JS/CSS bundles, Shoelace icons, etc.) from the same
|
|
101
80
|
# directory that provides index.html. This catch-all mount MUST come after
|
|
102
81
|
# all explicit routes so that /api/* and / are matched first.
|
|
103
82
|
if _dist_dir.is_dir():
|
|
104
|
-
app.mount(
|
|
105
|
-
"/", StaticFiles(directory=str(_dist_dir)), name="static"
|
|
106
|
-
)
|
|
83
|
+
app.mount("/", StaticFiles(directory=str(_dist_dir)), name="static")
|
|
107
84
|
|
|
108
85
|
|
|
109
86
|
def main():
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -- Node types --
|
|
2
|
+
NODE_PROCESS = "process"
|
|
3
|
+
NODE_PIPE = "pipe"
|
|
4
|
+
NODE_SOCKET = "socket"
|
|
5
|
+
NODE_UDS = "uds"
|
|
6
|
+
NODE_EXTERNAL_IP = "external_ip"
|
|
7
|
+
|
|
8
|
+
# -- Edge types --
|
|
9
|
+
EDGE_CHILD_PROCESS = "child_process"
|
|
10
|
+
EDGE_UDS = "uds"
|
|
11
|
+
EDGE_UDS_CONNECTION = "uds_connection"
|
|
12
|
+
EDGE_PIPE = "pipe"
|
|
13
|
+
EDGE_SOCKET = "socket"
|
|
14
|
+
EDGE_SOCKET_CONNECTION = "socket_connection"
|
|
15
|
+
EDGE_EXTERNAL_SOCKET = "external_socket"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# -- Node ID helpers --
|
|
19
|
+
def process_node_id(pid: int) -> str:
|
|
20
|
+
return f"{NODE_PROCESS}::{pid}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def uds_node_id(inode: int) -> str:
|
|
24
|
+
return f"{NODE_UDS}::{inode}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def pipe_node_id(inode: str) -> str:
|
|
28
|
+
return f"{NODE_PIPE}::{inode}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def socket_node_id(address: str, socket_type: str) -> str:
|
|
32
|
+
return f"{NODE_SOCKET}::{address}::{socket_type}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def external_ip_node_id(ip: str) -> str:
|
|
36
|
+
return f"{NODE_EXTERNAL_IP}::{ip}"
|
|
@@ -5,10 +5,28 @@ import subprocess
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from socket import AddressFamily
|
|
9
8
|
|
|
10
9
|
import psutil
|
|
11
10
|
|
|
11
|
+
from sysgraph.constants import (
|
|
12
|
+
EDGE_CHILD_PROCESS,
|
|
13
|
+
EDGE_EXTERNAL_SOCKET,
|
|
14
|
+
EDGE_PIPE,
|
|
15
|
+
EDGE_SOCKET,
|
|
16
|
+
EDGE_SOCKET_CONNECTION,
|
|
17
|
+
EDGE_UDS,
|
|
18
|
+
EDGE_UDS_CONNECTION,
|
|
19
|
+
NODE_EXTERNAL_IP,
|
|
20
|
+
NODE_PIPE,
|
|
21
|
+
NODE_PROCESS,
|
|
22
|
+
NODE_SOCKET,
|
|
23
|
+
NODE_UDS,
|
|
24
|
+
external_ip_node_id,
|
|
25
|
+
pipe_node_id,
|
|
26
|
+
process_node_id,
|
|
27
|
+
socket_node_id,
|
|
28
|
+
uds_node_id,
|
|
29
|
+
)
|
|
12
30
|
from sysgraph.graph import Graph, Node
|
|
13
31
|
from sysgraph.model import (
|
|
14
32
|
NetConnection,
|
|
@@ -37,6 +55,7 @@ def discover_processes() -> list[Process]:
|
|
|
37
55
|
]
|
|
38
56
|
for proc in psutil.process_iter(attrs):
|
|
39
57
|
info = proc.info
|
|
58
|
+
|
|
40
59
|
p = Process(pid=info["pid"])
|
|
41
60
|
p.parent_pid = info["ppid"]
|
|
42
61
|
p.user = info["username"]
|
|
@@ -48,6 +67,12 @@ def discover_processes() -> list[Process]:
|
|
|
48
67
|
p.cpu_user = cpu_times.user
|
|
49
68
|
p.cpu_system = cpu_times.system
|
|
50
69
|
|
|
70
|
+
mem_info = proc.memory_info()
|
|
71
|
+
if mem_info:
|
|
72
|
+
p.memory_rss = mem_info.rss
|
|
73
|
+
p.memory_vms = mem_info.vms
|
|
74
|
+
p.memory_shared = mem_info.shared
|
|
75
|
+
|
|
51
76
|
p.environment = info.get("environ")
|
|
52
77
|
|
|
53
78
|
processes.append(p)
|
|
@@ -200,26 +225,6 @@ def get_processes_open_files() -> dict[int, list[ProcessOpenFile]]:
|
|
|
200
225
|
return result_map
|
|
201
226
|
|
|
202
227
|
|
|
203
|
-
def get_net_connections(pid: int) -> list[NetConnection]:
|
|
204
|
-
proc = psutil.Process(pid)
|
|
205
|
-
connections: list[NetConnection] = []
|
|
206
|
-
for pcon in proc.net_connections(kind="all"):
|
|
207
|
-
if pcon.family not in (AddressFamily.AF_INET, AddressFamily.AF_INET6):
|
|
208
|
-
continue
|
|
209
|
-
connections.append(
|
|
210
|
-
NetConnection(
|
|
211
|
-
pid=pid,
|
|
212
|
-
local_address=SocketAddress(pcon.laddr.ip, pcon.laddr.port),
|
|
213
|
-
remote_address=SocketAddress(pcon.raddr.ip, pcon.raddr.port)
|
|
214
|
-
if pcon.raddr
|
|
215
|
-
else None,
|
|
216
|
-
socket_type=pcon.type.name,
|
|
217
|
-
state=pcon.status,
|
|
218
|
-
)
|
|
219
|
-
)
|
|
220
|
-
return connections
|
|
221
|
-
|
|
222
|
-
|
|
223
228
|
def get_all_net_connections() -> dict[int, list[NetConnection]]:
|
|
224
229
|
"""Fetch all network connections system-wide in a single call."""
|
|
225
230
|
result: dict[int, list[NetConnection]] = defaultdict(list)
|
|
@@ -240,28 +245,17 @@ def get_all_net_connections() -> dict[int, list[NetConnection]]:
|
|
|
240
245
|
return result
|
|
241
246
|
|
|
242
247
|
|
|
243
|
-
def
|
|
244
|
-
graph
|
|
245
|
-
|
|
248
|
+
def _add_process_nodes(
|
|
249
|
+
graph: Graph,
|
|
250
|
+
processes: list[Process],
|
|
251
|
+
) -> dict[int, Node]:
|
|
252
|
+
"""Create process nodes and parent-child edges."""
|
|
246
253
|
pid_to_node: dict[int, Node] = {}
|
|
247
254
|
|
|
248
|
-
# run independent discovery steps in parallel (all I/O-bound)
|
|
249
|
-
with ThreadPoolExecutor() as executor:
|
|
250
|
-
processes_future = executor.submit(discover_processes)
|
|
251
|
-
uds_future = executor.submit(discover_unix_sockets)
|
|
252
|
-
open_files_future = executor.submit(get_processes_open_files)
|
|
253
|
-
net_connections_future = executor.submit(get_all_net_connections)
|
|
254
|
-
|
|
255
|
-
processes = processes_future.result()
|
|
256
|
-
uds = uds_future.result()
|
|
257
|
-
open_files_map = open_files_future.result()
|
|
258
|
-
all_net_connections = net_connections_future.result()
|
|
259
|
-
|
|
260
255
|
for proc in processes:
|
|
261
|
-
proc_node_id = f"process::{proc.pid}"
|
|
262
256
|
node = graph.add_node(
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
process_node_id(proc.pid),
|
|
258
|
+
NODE_PROCESS,
|
|
265
259
|
properties={
|
|
266
260
|
"pid": proc.pid,
|
|
267
261
|
"command": proc.command,
|
|
@@ -269,48 +263,50 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
269
263
|
"name": proc.name,
|
|
270
264
|
"cpu_user": proc.cpu_user,
|
|
271
265
|
"cpu_system": proc.cpu_system,
|
|
266
|
+
"memory_rss": proc.memory_rss,
|
|
267
|
+
"memory_vms": proc.memory_vms,
|
|
268
|
+
"memory_shared": proc.memory_shared,
|
|
272
269
|
"environment": proc.environment,
|
|
273
270
|
},
|
|
274
271
|
)
|
|
275
272
|
pid_to_node[proc.pid] = node
|
|
276
273
|
|
|
277
|
-
# add parent-child relationships
|
|
278
274
|
for proc in processes:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
rel_type="child_process",
|
|
275
|
+
if proc.parent_pid is None:
|
|
276
|
+
continue
|
|
277
|
+
parent_node = pid_to_node.get(proc.parent_pid)
|
|
278
|
+
if parent_node is not None:
|
|
279
|
+
graph.add_edge(
|
|
280
|
+
source_id=parent_node.id,
|
|
281
|
+
target_id=pid_to_node[proc.pid].id,
|
|
282
|
+
rel_type=EDGE_CHILD_PROCESS,
|
|
288
283
|
)
|
|
289
284
|
|
|
285
|
+
return pid_to_node
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _add_uds_nodes(
|
|
289
|
+
graph: Graph,
|
|
290
|
+
uds_sockets: list[UnixDomainSocket],
|
|
291
|
+
pid_to_node: dict[int, Node],
|
|
292
|
+
discover_connectivity: bool,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Create UDS nodes, process→UDS edges, and UDS connection edges."""
|
|
290
295
|
uds_node_map: dict[int, Node] = {}
|
|
291
296
|
|
|
292
297
|
def ensure_uds_node(uds_socket: UnixDomainSocket) -> Node:
|
|
293
298
|
inode = uds_socket.local_inode
|
|
294
|
-
|
|
295
299
|
if inode in uds_node_map:
|
|
296
300
|
return uds_node_map[inode]
|
|
297
301
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if uds_socket.local_path == "*":
|
|
302
|
-
label = f"uds:[{inode}]"
|
|
303
|
-
else:
|
|
304
|
-
label = uds_socket.local_path
|
|
305
|
-
|
|
306
|
-
# strip trailing @ if present (abstract unix socket)
|
|
307
|
-
label = label.rstrip("@")
|
|
308
|
-
else: # no local path
|
|
302
|
+
if uds_socket.local_path and uds_socket.local_path != "*":
|
|
303
|
+
label = uds_socket.local_path.rstrip("@")
|
|
304
|
+
else:
|
|
309
305
|
label = f"uds:[{inode}]"
|
|
310
306
|
|
|
311
307
|
node = graph.add_node(
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
uds_node_id(inode),
|
|
309
|
+
NODE_UDS,
|
|
314
310
|
properties={
|
|
315
311
|
"label": label,
|
|
316
312
|
"local_inode": uds_socket.local_inode,
|
|
@@ -324,12 +320,9 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
324
320
|
uds_node_map[inode] = node
|
|
325
321
|
return node
|
|
326
322
|
|
|
327
|
-
# track which process→uds edges have been added to avoid duplicates
|
|
328
323
|
uds_process_edges: set[tuple[int, int]] = set()
|
|
329
324
|
|
|
330
|
-
|
|
331
|
-
# to their processes
|
|
332
|
-
for uds_socket in uds:
|
|
325
|
+
for uds_socket in uds_sockets:
|
|
333
326
|
uds_node = ensure_uds_node(uds_socket)
|
|
334
327
|
for p_ref in uds_socket.processes:
|
|
335
328
|
edge_key = (p_ref.pid, uds_socket.local_inode)
|
|
@@ -338,125 +331,129 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
338
331
|
graph.add_edge(
|
|
339
332
|
source_id=pid_to_node[p_ref.pid].id,
|
|
340
333
|
target_id=uds_node.id,
|
|
341
|
-
rel_type=
|
|
334
|
+
rel_type=EDGE_UDS,
|
|
342
335
|
properties={
|
|
343
336
|
"label": f"uds (fd={p_ref.fd})",
|
|
344
337
|
"fd": p_ref.fd,
|
|
345
338
|
},
|
|
346
339
|
)
|
|
347
340
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
for con in discover_connected_uds(uds):
|
|
341
|
+
if discover_connectivity:
|
|
342
|
+
for con in discover_connected_uds(uds_sockets):
|
|
351
343
|
uds_node1 = ensure_uds_node(con.socket1)
|
|
352
344
|
uds_node2 = ensure_uds_node(con.socket2)
|
|
353
|
-
|
|
354
345
|
graph.add_edge(
|
|
355
346
|
uds_node1.id,
|
|
356
347
|
uds_node2.id,
|
|
357
|
-
|
|
348
|
+
EDGE_UDS_CONNECTION,
|
|
358
349
|
properties={
|
|
359
350
|
"directional": False,
|
|
360
351
|
"dashed": True,
|
|
361
352
|
},
|
|
362
353
|
)
|
|
363
354
|
|
|
364
|
-
pipe_node_to_node = {}
|
|
365
355
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
356
|
+
def _add_pipe_nodes(
|
|
357
|
+
graph: Graph,
|
|
358
|
+
open_files_map: dict[int, list[ProcessOpenFile]],
|
|
359
|
+
pid_to_node: dict[int, Node],
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Create pipe nodes and directional pipe edges."""
|
|
362
|
+
pipe_nodes: dict[str, Node] = {}
|
|
363
|
+
|
|
364
|
+
def ensure_pipe_node(file_node: str) -> Node:
|
|
365
|
+
if file_node not in pipe_nodes:
|
|
366
|
+
pipe_nodes[file_node] = graph.add_node(
|
|
367
|
+
pipe_node_id(file_node),
|
|
368
|
+
NODE_PIPE,
|
|
369
|
+
properties={"label": f"pipe:[{file_node}]"},
|
|
374
370
|
)
|
|
375
|
-
return
|
|
371
|
+
return pipe_nodes[file_node]
|
|
376
372
|
|
|
377
373
|
for pid, files in open_files_map.items():
|
|
378
374
|
if pid not in pid_to_node:
|
|
379
375
|
continue
|
|
380
376
|
process_node = pid_to_node[pid]
|
|
381
377
|
for file in files:
|
|
382
|
-
# so far just show the pipes
|
|
383
378
|
if file.file_type != "FIFO":
|
|
384
379
|
continue
|
|
385
380
|
node = ensure_pipe_node(file.node)
|
|
386
|
-
|
|
381
|
+
props = {
|
|
382
|
+
"label": f"pipe (fd={file.fd})",
|
|
383
|
+
"fd": file.fd,
|
|
384
|
+
"mode": file.mode,
|
|
385
|
+
}
|
|
387
386
|
if file.mode == "r":
|
|
388
|
-
|
|
387
|
+
graph.add_edge(
|
|
389
388
|
source_id=node.id,
|
|
390
389
|
target_id=process_node.id,
|
|
391
|
-
rel_type=
|
|
392
|
-
properties=
|
|
393
|
-
"label": f"pipe (fd={file.fd})",
|
|
394
|
-
"fd": file.fd,
|
|
395
|
-
"mode": file.mode,
|
|
396
|
-
},
|
|
390
|
+
rel_type=EDGE_PIPE,
|
|
391
|
+
properties=props,
|
|
397
392
|
)
|
|
398
393
|
else:
|
|
399
|
-
|
|
394
|
+
graph.add_edge(
|
|
400
395
|
source_id=process_node.id,
|
|
401
396
|
target_id=node.id,
|
|
402
|
-
rel_type=
|
|
403
|
-
properties=
|
|
404
|
-
"label": f"pipe (fd={file.fd})",
|
|
405
|
-
"fd": file.fd,
|
|
406
|
-
"mode": file.mode,
|
|
407
|
-
},
|
|
397
|
+
rel_type=EDGE_PIPE,
|
|
398
|
+
properties=props,
|
|
408
399
|
)
|
|
409
400
|
|
|
410
|
-
socket_to_pids: dict[tuple[SocketAddress, str], list[int]] = {}
|
|
411
|
-
socket_to_node: dict[tuple[SocketAddress, str], Node] = {}
|
|
412
401
|
|
|
413
|
-
|
|
414
|
-
|
|
402
|
+
_SIMPLE_SOCKET_TYPE = {
|
|
403
|
+
"SOCK_DGRAM": "UDP",
|
|
404
|
+
"SOCK_STREAM": "TCP",
|
|
405
|
+
}
|
|
415
406
|
|
|
416
|
-
def ensure_socket(address: SocketAddress, socket_type, state: str):
|
|
417
|
-
key = (address, socket_type)
|
|
418
|
-
if key in socket_to_node:
|
|
419
|
-
return socket_to_node[key]
|
|
420
|
-
socket_node_id = f"socket::{address}::{socket_type}"
|
|
421
|
-
socket_node = graph.add_node(socket_node_id, "socket")
|
|
422
407
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
408
|
+
def _add_network_nodes(
|
|
409
|
+
graph: Graph,
|
|
410
|
+
processes: list[Process],
|
|
411
|
+
all_net_connections: dict[int, list[NetConnection]],
|
|
412
|
+
pid_to_node: dict[int, Node],
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Create socket/external-IP nodes and their edges."""
|
|
415
|
+
sock_to_pids: dict[tuple[SocketAddress, str], list[int]] = {}
|
|
416
|
+
sock_to_node: dict[tuple[SocketAddress, str], Node] = {}
|
|
417
|
+
connected_sockets: set[tuple[str, ...]] = set()
|
|
427
418
|
|
|
428
|
-
|
|
419
|
+
def is_ipv6(address: str) -> bool:
|
|
420
|
+
return ":" in address
|
|
429
421
|
|
|
422
|
+
def ensure_socket(
|
|
423
|
+
address: SocketAddress, sock_type: str, state: str
|
|
424
|
+
) -> Node:
|
|
425
|
+
key = (address, sock_type)
|
|
426
|
+
if key in sock_to_node:
|
|
427
|
+
return sock_to_node[key]
|
|
428
|
+
|
|
429
|
+
node = graph.add_node(
|
|
430
|
+
socket_node_id(str(address), sock_type),
|
|
431
|
+
NODE_SOCKET,
|
|
432
|
+
)
|
|
433
|
+
simple = _SIMPLE_SOCKET_TYPE.get(sock_type, sock_type)
|
|
430
434
|
if is_ipv6(address.ip):
|
|
431
|
-
|
|
432
|
-
f"[{address.ip}]:{address.port} ({
|
|
435
|
+
node.properties["label"] = (
|
|
436
|
+
f"[{address.ip}]:{address.port} ({simple})"
|
|
433
437
|
)
|
|
434
438
|
else:
|
|
435
|
-
|
|
436
|
-
f"{address.ip}:{address.port} ({
|
|
439
|
+
node.properties["label"] = (
|
|
440
|
+
f"{address.ip}:{address.port} ({simple})"
|
|
437
441
|
)
|
|
442
|
+
node.properties["state"] = state
|
|
443
|
+
node.properties["socket_type"] = sock_type
|
|
444
|
+
sock_to_node[key] = node
|
|
445
|
+
return node
|
|
438
446
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
socket_to_node[key] = socket_node
|
|
443
|
-
return socket_node
|
|
444
|
-
|
|
445
|
-
connected_sockets = set()
|
|
446
|
-
|
|
447
|
-
def ensure_sockets_connected(socket1: Node, socket2: Node):
|
|
448
|
-
key = tuple(sorted([socket1.id, socket2.id]))
|
|
447
|
+
def ensure_sockets_connected(s1: Node, s2: Node) -> None:
|
|
448
|
+
key = tuple(sorted([s1.id, s2.id]))
|
|
449
449
|
if key in connected_sockets:
|
|
450
450
|
return
|
|
451
451
|
connected_sockets.add(key)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
{
|
|
457
|
-
"directional": False,
|
|
458
|
-
"dashed": True,
|
|
459
|
-
},
|
|
452
|
+
graph.add_edge(
|
|
453
|
+
s1.id,
|
|
454
|
+
s2.id,
|
|
455
|
+
EDGE_SOCKET_CONNECTION,
|
|
456
|
+
{"directional": False, "dashed": True},
|
|
460
457
|
)
|
|
461
458
|
|
|
462
459
|
for proc in processes:
|
|
@@ -468,26 +465,25 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
468
465
|
continue
|
|
469
466
|
|
|
470
467
|
for net_con in net_connections:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
468
|
+
sock_key = (
|
|
469
|
+
net_con.local_address,
|
|
470
|
+
net_con.socket_type,
|
|
471
|
+
)
|
|
474
472
|
local_socket = ensure_socket(
|
|
475
473
|
net_con.local_address,
|
|
476
474
|
net_con.socket_type,
|
|
477
475
|
net_con.state,
|
|
478
476
|
)
|
|
479
477
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
) + [proc.pid]
|
|
478
|
+
if proc.pid not in sock_to_pids.get(sock_key, []):
|
|
479
|
+
sock_to_pids[sock_key] = sock_to_pids.get(sock_key, []) + [
|
|
480
|
+
proc.pid
|
|
481
|
+
]
|
|
485
482
|
graph.add_edge(
|
|
486
483
|
source_id=proc_node.id,
|
|
487
484
|
target_id=local_socket.id,
|
|
488
|
-
rel_type=
|
|
485
|
+
rel_type=EDGE_SOCKET,
|
|
489
486
|
)
|
|
490
|
-
|
|
491
487
|
if net_con.remote_address:
|
|
492
488
|
remote_socket = ensure_socket(
|
|
493
489
|
net_con.remote_address,
|
|
@@ -496,35 +492,44 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
496
492
|
)
|
|
497
493
|
ensure_sockets_connected(local_socket, remote_socket)
|
|
498
494
|
|
|
499
|
-
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
},
|
|
495
|
+
# sockets not connected to any process are remote endpoints
|
|
496
|
+
external_ip_nodes: dict[str, Node] = {}
|
|
497
|
+
|
|
498
|
+
for sock_key, sock_node in sock_to_node.items():
|
|
499
|
+
if sock_to_pids.get(sock_key):
|
|
500
|
+
continue
|
|
501
|
+
ip = sock_key[0].ip
|
|
502
|
+
if ip not in external_ip_nodes:
|
|
503
|
+
external_ip_nodes[ip] = graph.add_node(
|
|
504
|
+
external_ip_node_id(ip),
|
|
505
|
+
NODE_EXTERNAL_IP,
|
|
506
|
+
{"label": ip},
|
|
511
507
|
)
|
|
512
|
-
|
|
508
|
+
graph.add_edge(
|
|
509
|
+
external_ip_nodes[ip].id,
|
|
510
|
+
sock_node.id,
|
|
511
|
+
EDGE_EXTERNAL_SOCKET,
|
|
512
|
+
)
|
|
513
513
|
|
|
514
|
-
for socket, socket_node in socket_to_node.items():
|
|
515
|
-
pids = socket_to_pids.get(socket)
|
|
516
514
|
|
|
517
|
-
|
|
518
|
-
|
|
515
|
+
def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
516
|
+
graph = Graph()
|
|
517
|
+
|
|
518
|
+
# run independent discovery steps in parallel (all I/O-bound)
|
|
519
|
+
with ThreadPoolExecutor() as executor:
|
|
520
|
+
processes_future = executor.submit(discover_processes)
|
|
521
|
+
uds_future = executor.submit(discover_unix_sockets)
|
|
522
|
+
open_files_future = executor.submit(get_processes_open_files)
|
|
523
|
+
net_connections_future = executor.submit(get_all_net_connections)
|
|
519
524
|
|
|
520
|
-
|
|
521
|
-
|
|
525
|
+
processes = processes_future.result()
|
|
526
|
+
uds = uds_future.result()
|
|
527
|
+
open_files_map = open_files_future.result()
|
|
528
|
+
all_net_connections = net_connections_future.result()
|
|
522
529
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
"external_socket",
|
|
528
|
-
)
|
|
530
|
+
pid_to_node = _add_process_nodes(graph, processes)
|
|
531
|
+
_add_uds_nodes(graph, uds, pid_to_node, discover_uds_connectivity)
|
|
532
|
+
_add_pipe_nodes(graph, open_files_map, pid_to_node)
|
|
533
|
+
_add_network_nodes(graph, processes, all_net_connections, pid_to_node)
|
|
529
534
|
|
|
530
535
|
return graph
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--z-toolbar: 100;--z-search-help: 150;--z-context-menu: 200;--z-details-panel: 50;--z-selection-overlay: 10;--z-version-badge: 10;--bg-toolbar: rgba(255, 255, 255, .95);--bg-panel: rgba(255, 255, 255, .97);--border-subtle: rgba(0, 0, 0, .08);--border-light: rgba(0, 0, 0, .1);--border-medium: rgba(0, 0, 0, .12);--shadow-xs: rgba(0, 0, 0, .08);--shadow-sm: rgba(0, 0, 0, .14);--shadow-md: rgba(0, 0, 0, .15);--accent-primary: #1a56db;--accent-info: #1a73e8;--accent-danger: #dc2626;--toolbar-height: 48px;--spacing-xs: 4px;--spacing-sm: 6px;--spacing-md: 8px;--spacing-lg: 16px;--font-family: "Ubuntu", "Segoe UI", "Arial", sans-serif;--font-size-xs: 11px;--font-size-sm: 12px;--font-size-md: 13px;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--button-height: 28px;--icon-button-size: 24px;--tp-base-background-color: hsla(230, 5%, 90%, 1);--tp-base-shadow-color: hsla(0, 0%, 0%, .1);--tp-button-background-color: hsla(230, 7%, 75%, 1);--tp-button-background-color-active: hsla(230, 7%, 60%, 1);--tp-button-background-color-focus: hsla(230, 7%, 65%, 1);--tp-button-background-color-hover: hsla(230, 7%, 70%, 1);--tp-button-foreground-color: hsla(230, 10%, 30%, 1);--tp-container-background-color: hsla(230, 15%, 30%, .2);--tp-container-background-color-active: hsla(230, 15%, 30%, .32);--tp-container-background-color-focus: hsla(230, 15%, 30%, .28);--tp-container-background-color-hover: hsla(230, 15%, 30%, .24);--tp-container-foreground-color: hsla(230, 10%, 30%, 1);--tp-groove-foreground-color: hsla(230, 15%, 30%, .1);--tp-input-background-color: hsla(230, 15%, 30%, .1);--tp-input-background-color-active: hsla(230, 15%, 30%, .22);--tp-input-background-color-focus: hsla(230, 15%, 30%, .18);--tp-input-background-color-hover: hsla(230, 15%, 30%, .14);--tp-input-foreground-color: hsla(230, 10%, 30%, 1);--tp-label-foreground-color: hsla(230, 10%, 30%, .7);--tp-monitor-background-color: hsla(230, 15%, 30%, .1);--tp-monitor-foreground-color: hsla(230, 10%, 30%, .5)}html,body{height:100%;margin:0;overflow:hidden;font-family:var(--font-family)}#toolbar{position:fixed;top:0;left:0;right:0;height:var(--toolbar-height);z-index:var(--z-toolbar);background:var(--bg-toolbar);border-bottom:1px solid var(--border-light);display:flex;align-items:center;padding:0 var(--spacing-lg);gap:var(--spacing-sm);box-shadow:0 1px 3px var(--shadow-xs)}#toolbar md-outlined-button{--md-outlined-button-container-height: var(--button-height);--md-outlined-button-label-text-size: var(--font-size-sm);--md-outlined-button-label-text-weight: 500;--md-outlined-button-icon-size: 16px;--md-outlined-button-leading-space: 10px;--md-outlined-button-trailing-space: 12px;--md-outlined-button-icon-offset: -2px;--md-outlined-button-container-shape: var(--radius-sm)}md-outlined-button.active{--md-outlined-button-container-color: #d0e1ff;--md-outlined-button-label-text-color: var(--accent-primary);--md-outlined-button-outline-color: var(--accent-primary);--md-outlined-button-icon-color: var(--accent-primary)}md-outlined-button.danger{--md-outlined-button-label-text-color: var(--accent-danger);--md-outlined-button-outline-color: var(--accent-danger);--md-outlined-button-icon-color: var(--accent-danger)}.toolbar-separator{width:1px;height:var(--icon-button-size);background:var(--border-light)}#searchInput{width:240px;height:var(--button-height);--md-outlined-text-field-container-shape: var(--radius-sm);--md-outlined-text-field-top-space: var(--spacing-xs);--md-outlined-text-field-bottom-space: var(--spacing-xs);--md-outlined-text-field-input-text-size: var(--font-size-sm);--md-outlined-text-field-label-text-size: var(--font-size-sm)}#searchHelpTrigger{--md-icon-button-icon-size: 18px;--md-icon-button-state-layer-height: var(--button-height);--md-icon-button-state-layer-width: var(--button-height);height:var(--button-height);width:var(--button-height)}.search-help-anchor{position:relative;display:inline-flex}#searchHelp{display:none;position:absolute;top:100%;left:0;margin-top:var(--spacing-xs);z-index:var(--z-search-help);border:1px solid var(--border-medium);border-radius:var(--radius-md);padding:10px 14px;box-shadow:0 4px 12px var(--shadow-md);font-size:var(--font-size-sm);line-height:1.5;background:#1e293b;color:#fff}#searchHelp.open{display:block}#searchHelp code{background:#ffffff1f;padding:1px var(--spacing-xs);border-radius:3px}#content{position:fixed;top:var(--toolbar-height);left:0;right:0;bottom:0}#graph{width:100%;height:100%;position:relative}.details-panel{position:absolute;top:var(--spacing-md);left:var(--spacing-md);width:420px;height:350px;min-width:200px;min-height:120px;max-height:calc(100% - var(--spacing-lg));z-index:var(--z-details-panel);background:var(--bg-panel);box-shadow:0 2px 12px var(--shadow-sm);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);display:none;flex-direction:column;overflow:hidden;resize:both}.details-panel.open{display:flex}.details-panel .panel-header{display:flex;align-items:center;height:var(--button-height);padding:0 2px 0 10px;border-bottom:1px solid var(--border-subtle);flex-shrink:0;cursor:grab;-webkit-user-select:none;user-select:none}.details-panel .panel-header:active{cursor:grabbing}.details-panel .panel-title{font-size:var(--font-size-sm);font-weight:500;color:#333;flex:1}.details-panel md-icon-button{--md-icon-button-icon-size: 16px;--md-icon-button-state-layer-height: var(--icon-button-size);--md-icon-button-state-layer-width: var(--icon-button-size);height:var(--icon-button-size);width:var(--icon-button-size)}.details-panel .panel-body{flex:1;overflow-y:auto;padding:var(--spacing-md);font-family:var(--font-family)}#detailsPanel{border-left:3px solid var(--accent-info)}#detailsPanel .panel-header{background:#1a73e80f}#contextMenu{display:none;position:fixed;z-index:var(--z-context-menu);background:var(--bg-panel);border:1px solid var(--border-medium);border-radius:var(--spacing-sm);padding:var(--spacing-xs) 0;box-shadow:0 4px 12px var(--shadow-md);min-width:160px;font-size:var(--font-size-md)}.context-menu-item{padding:var(--spacing-sm) 14px;cursor:pointer;-webkit-user-select:none;user-select:none}.context-menu-item:hover{background:#0000000f}.context-menu-divider{height:1px;margin:var(--spacing-xs) 0;background:var(--border-medium)}.tp-rotv{font-family:var(--font-family)!important;opacity:.9}#settingsPane{max-height:calc(100% - 20px);position:absolute;overflow-y:auto;right:10px;top:10px}#version-badge{position:fixed;bottom:var(--spacing-sm);left:var(--spacing-md);font-size:var(--font-size-xs);color:#0000004d;pointer-events:none;z-index:var(--z-version-badge);font-family:var(--font-family)}.muted{color:#666}hr{border:none;border-top:1px solid var(--border-subtle);margin:var(--spacing-sm) 0}
|