#!/usr/bin/env python
# Convert the output of `lsof -F` into PID USER CMD OBJ
import sys
def parse_lsof():
procs = {}
cur = {}
proc = {}
file = {}
for line in sys.stdin:
if line.startswith("COMMAND"):
print("did you run lsof without -F?")
exit(1)
tag = line[0]
val = line[1:].rstrip('\n')
if tag == 'p':
if val not in procs:
proc = {'files': []}
file = None
cur = proc
procs[val] = proc
else:
proc = {}
cur = {}
elif tag == 'f' and proc:
file = {'proc': proc}
cur = file
proc['files'].append(file)
if cur:
cur[tag] = val
# skip kernel threads
if proc:
if file and all(k in file.keys() for k in ("f", "t")):
if file['f'] == "txt" and file['t'] == "unknown":
procs[proc['p']] = {}
proc = {}
file = None
cur = None
return procs
def find_connections(procs):
cs = {
'fifo': {},
'unix': {},
'tcp': {},
'udp': {},
'pipe': {}
}
for pid, proc in procs.items():
if 'files' in proc:
for _, file in enumerate(proc['files']):
if 't' in file and file['t'] == "unix":
if 'i' in file:
i = file['i']
if i in cs['unix']:
cs['unix'][i].append(file)
else:
cs['unix'][i] = []
cs['unix'][i].append(file)
else:
i = file['d']
if i in cs['unix']:
cs['unix'][i].append(file)
else:
cs['unix'][i] = []
cs['unix'][i].append(file)
if 't' in file and file['t'] == "FIFO":
if 'i' in file:
if file['i'] in cs['fifo']:
cs['fifo'][file['i']].append(file)
else:
cs['fifo'][file['i']] = []
cs['fifo'][file['i']].append(file)
if 't' in file and file['t'] == "PIPE":
for n in file['n'].lstrip('->'):
ps = {file['d'], n}
ps = sorted(ps)
if len(ps) == 2:
id = ps[0] + "\\n" + ps[1]
fs = cs['pipe']
if id in fs:
fs[id].append(file)
else:
fs[id] = []
fs[id].append(file)
if 't' in file and (file['t'] == 'IPv4' or file['t'] == 'IPv6'):
if '->' in file['n']:
a, b = file['n'].split("->")
ps = {a, b}
ps = sorted(ps)
if len(ps) == 2:
id = ps[0] + "\\n" + ps[1]
if file['P'] == "TCP":
fs = cs['tcp']
if id in fs:
fs[id].append(file)
else:
fs[id] = []
fs[id].append(file)
else:
fs = cs['udp']
if id in fs:
fs[id].append(file)
else:
fs[id] = []
fs[id].append(file)
return cs
def print_graph(procs, conns):
colors = {
'fifo': "green",
'unix': "purple",
'tcp': "red",
'udp': "orange",
'pipe': "blue"
}
# Generate graph
print("digraph G {")
print(
"\tgraph [ center=true, margin=0.2, nodesep=0.1, ranksep=0.3, rankdir=LR];")
print(
"\tnode [ shape=box, style=\"rounded,filled\" width=0, height=0, fontname=Helvetica, fontsize=10];")
print("\tedge [ fontname=Helvetica, fontsize=10];")
# Parent/child relationships
for pid, proc in procs.items():
if 'R' in proc and proc['R'] == "1":
color = "grey70"
else:
color = "white"
if 'p' in proc and 'n' in proc:
print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" %
(proc['p'], proc['n'], proc['p'], proc['L'], color))
elif 'p' in proc:
if 'L' in proc: # there could be no L flag if process running, but user was removed. lsof: no pwd entry for UID
print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" %
(proc['p'], proc['c'], proc['p'], proc['L'], color))
else:
print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" %
(proc['p'], proc['c'], proc['p'], "no user", color))
if 'R' in proc and proc['R'] in procs:
proc_parent = procs[proc['R']]
if proc_parent:
if proc_parent['p'] != "1":
print(
"\tp%s -> p%s [ penwidth=2 weight=100 color=grey60 dir=\"none\" ];" % (proc['R'], proc['p']))
for type, conn in conns.items():
for id, files in conn.items():
if len(files) == 2:
if files[0]['proc'] != files[1]['proc']:
label = type + ":\\n" + id
dir = "both"
if files[0]['a'] == "w":
dir = "forward"
elif files[0]['a'] == "r":
dir = "backward"
print("\tp%s -> p%s [ color=\"%s\" label=\"%s\" dir=\"%s\"];" % (
files[0]['proc']['p'], files[1]['proc']['p'], colors[type] or "black", label, dir))
print("}")
if __name__ == '__main__':
sys.setrecursionlimit(15000) # Increase the recursion limit before processing starts
try:
procs = parse_lsof()
conns = find_connections(procs)
print_graph(procs, conns)
except RuntimeError as e:
print("Error: Recursion depth exceeded. Consider increasing the recursion limit.")