#!/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.")