SSH-Snake/tools/generate-graph.py

116 lines
4.7 KiB
Python
Raw Normal View History

2024-01-04 22:28:05 +01:00
import argparse
import re
from collections import defaultdict
import networkx as nx
from networkx.drawing.nx_agraph import write_dot
def create_graph_from_edges(lookup_table):
graph = nx.DiGraph()
for source, dest in lookup_table:
graph.add_edge(source, dest)
return graph
def build_lookup_table(input_lines, ignore_dest_user):
lookup_table = set()
for line in input_lines:
line = line.strip()
line = re.sub(r"^\[?\d+\]?\s*", "", line)
2024-01-05 14:13:17 +01:00
if ": " in line or "]->" not in line or not line[-2].isdigit() or not line[-1] == ')':
2024-01-04 22:28:05 +01:00
continue
pattern = re.compile(r"(\w+)@\(([\d\.:]+)\)(\[[^\]]+\])->(?=(\w+)@\(([\d\.:]+)\))")
matches = re.finditer(pattern, line)
previous_dest_host = None
for match in matches:
user, host, _, dest_user, dest_host = match.groups()
if host == "(127.0.0.1)" or host == "127.0.0.1":
if prev_dest_host is not None:
host = prev_dest_host
if dest_host == "(127.0.0.1)" or dest_host == "127.0.0.1":
dest_host = host
prev_dest_host = dest_host
else:
prev_dest_host = None
if ignore_dest_user:
target_range = (host, dest_host)
else:
target_range = (f"{user}@{host}", f"{dest_user}@{dest_host}")
lookup_table.add(target_range)
return lookup_table
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Construct a graph file to visualize the relation between servers discovered by SSH-Snake.")
parser.add_argument("--file", help="Path to a file containing the output of SSH-Snake.")
parser.add_argument("--format", help="The format of the graph file to export. The options are gexf or dot.")
parser.add_argument("--with-users", action="store_true", help="Create nodes based on their 'user@host' instead of just 'host'. This setting is optional and not recommended.")
args = parser.parse_args()
if not any(vars(args).values()):
parser.print_help()
exit()
if args.format not in ("gexf", "dot"):
print("Valid options for --format are: gexf or dot")
exit()
with open(args.file, 'r') as file:
input_lines = file.readlines()
ignore_dest_user = True
if args.with_users:
ignore_dest_user = False
lookup_table = build_lookup_table(input_lines, ignore_dest_user)
graph = create_graph_from_edges(lookup_table)
if len(lookup_table) > 500 and args.format == "dot":
print("The list of connections is quite big; YMMV with a .dot file.")
# Set default edge color to green
for edge in graph.edges():
graph.edges[edge]['color'] = '#006400'
# Set default node color to lightgrey.
for node in graph.nodes():
graph.nodes[node]['fillcolor'] = 'lightgrey'
graph.nodes[node]['style'] = 'filled'
# Set any edges that correspond to a dest1 being able to connect to dest2 and backwards (dest1<--->dest2) to red.
for source, dest in graph.edges():
if (dest, source) in graph.edges():
graph.edges[(source, dest)]['dir'] = 'both'
graph.edges[(source, dest)]['color'] = '#CD5C5C'
# Set any node corresponding to a loopback (dest1<--->dest1) to blue
for node in graph.nodes():
if graph.has_edge(node, node) or any((edge[0] == edge[1] == node) for edge in graph.edges()):
graph.nodes[node]['fillcolor'] = '#00BFFF'
output_dot_file_path = "SSHSnake_dot_file.dot"
output_gexf_file_path = "SSHSnake_gexf_file.gexf"
if args.format == "gexf":
nx.write_gexf(graph, output_gexf_file_path)
print("Your gexf file has been created in ./sshsnake_gexf_file.gexf")
print("You can now open the file using Gephi.\n")
print("Or you can use Cytoscape! Take a look at GRAPHICS.md")
else:
nx.drawing.nx_pydot.write_dot(graph, output_dot_file_path)
print("Your dot file has been created in ./sshsnake_dot_file.dot.\n")
print("To convert your dot file to a png or svg, use the following command to sample different algorithms available from graphviz:\n")
print("for alg in sfdp fdp circo twopi neato dot; do\n\t$alg -Tpng -Gsplines=true -Gconcentrate=true -Gnodesep=0.1 -Goverlap=false SSHSnake_dot_file.dot -o $alg.png\ndone\n\n")
print("Alternatively, you can just paste the .dot file into https://dreampuf.github.io/GraphvizOnline/ -- if pasting that type of information into your browser is in your threat model...\n\n")
print("Try placing splines=true; concentrate=true; nodesep=0.1; overlap=false; in the file just after the first line, too!")