├── .gitattributes ├── LICENSE ├── README.md ├── example.jpg └── lsofgraph.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 akme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python version of [zevv/lsofgraph](https://github.com/zevv/lsofgraph) 2 | 3 | A small utility to convert Unix `lsof` output to a graph showing FIFO and UNIX interprocess communication. 4 | 5 | Generate graph: 6 | 7 | ````shell 8 | sudo lsof -n -F | python lsofgraph.py | dot -Tjpg > /tmp/a.jpg 9 | OR 10 | sudo lsof -n -F | python lsofgraph.py | dot -T svg > /tmp/a.svg 11 | ```` 12 | or add `unflatten` to the chain for a better layout: 13 | 14 | ````shell 15 | sudo lsof -n -F | python lsofgraph.py | unflatten -l 1 -c 6 | dot -T jpg > /tmp/a.jpg 16 | OR 17 | sudo lsof -n -F | python lsofgraph.py | unflatten -l 1 -c 6 | dot -T svg > /tmp/a.svg 18 | ```` 19 | Note: In cases of handling large datasets, you might encounter a "maximum recursion depth exceeded" error. To work around this, you can increase the recursion limit in your Python environment by adding `sys.setrecursionlimit(15000)` in the `lsofgraph.py` script. 20 | 21 | ![example output](/example.jpg) 22 | 23 | # Install and use on MacOS 24 | 25 | Graphviz contains utilities dot and unflatten 26 | ````shell 27 | brew install Graphviz 28 | git clone https://github.com/akme/lsofgraph-python.git 29 | cd lsofgraph-python 30 | lsof -n -F | python lsofgraph.py | unflatten -l 1 -c 6 | dot -T jpg > /tmp/a.jpg && open /tmp/a.jpg 31 | lsof -n -F | python lsofgraph.py | unflatten -l 1 -c 6 | dot -T svg > /tmp/a.jpg && open -a Safari.app '/tmp/a.svg' 32 | ```` 33 | -------------------------------------------------------------------------------- /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akme/lsofgraph-python/990ef961d9b3843b41bfedaa11993f52700d0dbd/example.jpg -------------------------------------------------------------------------------- /lsofgraph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Convert the output of `lsof -F` into PID USER CMD OBJ 3 | import sys 4 | 5 | 6 | def parse_lsof(): 7 | procs = {} 8 | cur = {} 9 | proc = {} 10 | file = {} 11 | 12 | for line in sys.stdin: 13 | 14 | if line.startswith("COMMAND"): 15 | print("did you run lsof without -F?") 16 | exit(1) 17 | 18 | tag = line[0] 19 | val = line[1:].rstrip('\n') 20 | if tag == 'p': 21 | if val not in procs: 22 | proc = {'files': []} 23 | file = None 24 | cur = proc 25 | procs[val] = proc 26 | else: 27 | proc = {} 28 | cur = {} 29 | elif tag == 'f' and proc: 30 | file = {'proc': proc} 31 | cur = file 32 | proc['files'].append(file) 33 | 34 | if cur: 35 | cur[tag] = val 36 | 37 | # skip kernel threads 38 | 39 | if proc: 40 | if file and all(k in file.keys() for k in ("f", "t")): 41 | if file['f'] == "txt" and file['t'] == "unknown": 42 | procs[proc['p']] = {} 43 | proc = {} 44 | file = None 45 | cur = None 46 | return procs 47 | 48 | 49 | def find_connections(procs): 50 | cs = { 51 | 'fifo': {}, 52 | 'unix': {}, 53 | 'tcp': {}, 54 | 'udp': {}, 55 | 'pipe': {} 56 | } 57 | for pid, proc in procs.items(): 58 | if 'files' in proc: 59 | for _, file in enumerate(proc['files']): 60 | 61 | if 't' in file and file['t'] == "unix": 62 | if 'i' in file: 63 | i = file['i'] 64 | if i in cs['unix']: 65 | cs['unix'][i].append(file) 66 | else: 67 | cs['unix'][i] = [] 68 | cs['unix'][i].append(file) 69 | else: 70 | i = file['d'] 71 | if i in cs['unix']: 72 | cs['unix'][i].append(file) 73 | else: 74 | cs['unix'][i] = [] 75 | cs['unix'][i].append(file) 76 | 77 | if 't' in file and file['t'] == "FIFO": 78 | 79 | if 'i' in file: 80 | if file['i'] in cs['fifo']: 81 | cs['fifo'][file['i']].append(file) 82 | else: 83 | cs['fifo'][file['i']] = [] 84 | cs['fifo'][file['i']].append(file) 85 | 86 | if 't' in file and file['t'] == "PIPE": 87 | for n in file['n'].lstrip('->'): 88 | ps = {file['d'], n} 89 | ps = sorted(ps) 90 | if len(ps) == 2: 91 | id = ps[0] + "\\n" + ps[1] 92 | fs = cs['pipe'] 93 | if id in fs: 94 | fs[id].append(file) 95 | else: 96 | fs[id] = [] 97 | fs[id].append(file) 98 | 99 | if 't' in file and (file['t'] == 'IPv4' or file['t'] == 'IPv6'): 100 | if '->' in file['n']: 101 | a, b = file['n'].split("->") 102 | ps = {a, b} 103 | ps = sorted(ps) 104 | if len(ps) == 2: 105 | id = ps[0] + "\\n" + ps[1] 106 | if file['P'] == "TCP": 107 | fs = cs['tcp'] 108 | if id in fs: 109 | fs[id].append(file) 110 | else: 111 | fs[id] = [] 112 | fs[id].append(file) 113 | else: 114 | fs = cs['udp'] 115 | if id in fs: 116 | fs[id].append(file) 117 | else: 118 | fs[id] = [] 119 | fs[id].append(file) 120 | return cs 121 | 122 | 123 | def print_graph(procs, conns): 124 | colors = { 125 | 'fifo': "green", 126 | 'unix': "purple", 127 | 'tcp': "red", 128 | 'udp': "orange", 129 | 'pipe': "blue" 130 | } 131 | 132 | # Generate graph 133 | 134 | print("digraph G {") 135 | print( 136 | "\tgraph [ center=true, margin=0.2, nodesep=0.1, ranksep=0.3, rankdir=LR];") 137 | print( 138 | "\tnode [ shape=box, style=\"rounded,filled\" width=0, height=0, fontname=Helvetica, fontsize=10];") 139 | print("\tedge [ fontname=Helvetica, fontsize=10];") 140 | 141 | # Parent/child relationships 142 | 143 | for pid, proc in procs.items(): 144 | if 'R' in proc and proc['R'] == "1": 145 | color = "grey70" 146 | else: 147 | color = "white" 148 | if 'p' in proc and 'n' in proc: 149 | print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" % 150 | (proc['p'], proc['n'], proc['p'], proc['L'], color)) 151 | elif 'p' in proc: 152 | if 'L' in proc: # there could be no L flag if process running, but user was removed. lsof: no pwd entry for UID 153 | print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" % 154 | (proc['p'], proc['c'], proc['p'], proc['L'], color)) 155 | else: 156 | print("\tp%s [ label = \"%s\\n%s %s\" fillcolor=%s ];" % 157 | (proc['p'], proc['c'], proc['p'], "no user", color)) 158 | if 'R' in proc and proc['R'] in procs: 159 | proc_parent = procs[proc['R']] 160 | if proc_parent: 161 | if proc_parent['p'] != "1": 162 | print( 163 | "\tp%s -> p%s [ penwidth=2 weight=100 color=grey60 dir=\"none\" ];" % (proc['R'], proc['p'])) 164 | 165 | for type, conn in conns.items(): 166 | for id, files in conn.items(): 167 | if len(files) == 2: 168 | if files[0]['proc'] != files[1]['proc']: 169 | label = type + ":\\n" + id 170 | dir = "both" 171 | if files[0]['a'] == "w": 172 | dir = "forward" 173 | elif files[0]['a'] == "r": 174 | dir = "backward" 175 | print("\tp%s -> p%s [ color=\"%s\" label=\"%s\" dir=\"%s\"];" % ( 176 | files[0]['proc']['p'], files[1]['proc']['p'], colors[type] or "black", label, dir)) 177 | print("}") 178 | 179 | 180 | if __name__ == '__main__': 181 | sys.setrecursionlimit(15000) # Increase the recursion limit before processing starts 182 | try: 183 | procs = parse_lsof() 184 | conns = find_connections(procs) 185 | print_graph(procs, conns) 186 | except RuntimeError as e: 187 | print("Error: Recursion depth exceeded. Consider increasing the recursion limit.") 188 | --------------------------------------------------------------------------------