Streamlining Network Visualization: An In-Depth Guide to Interactive HTML Maps with Minimal Dependencies

In the intricate landscape of modern networking, understanding the intricacies of data transmission paths is pivotal for maintaining robust security and optimal performance. Traceroute mapping, a technique employed by network administrators, security professionals, and auditors, plays a crucial role in unraveling the complex web of network topology. This article delves into the capabilities of traceroute mappers, shedding light on how these tools serve as indispensable aids in visualizing, analyzing, and fortifying network structures.

The initial segment explores the essence of traceroute mapping, emphasizing its ability to chart the course of data packets as they traverse through various nodes, such as routers and switches, en route to a predetermined destination. This mapping process not only unravels the fundamental architecture of a network but also empowers professionals to identify potential issues and enhance overall performance.

Beyond mere visualization, the article delves into the transformative power of traceroute mappers in uncovering the network’s internal intricacies. By mapping paths to destination IP hosts and meticulously scanning each node along the way, these tools facilitate the creation of comprehensive network maps. This, in turn, enhances the readability of reachable hosts, delineates available ports and services, and delineates the complete network path leading to the intended destination.

As a focal point, the article introduces a developed tool that elevates traceroute mapping to new heights. This tool not only conducts the traceroute operations but also presents the results in an easily interpretable graphical format. This visual representation simplifies the analysis for users, facilitating a more intuitive understanding of the intricacies of the network path.


Nmap scan

Nmap scanner will be used for all networks host and ports discovery tasks. The current scripts presented on this post will only check for IP, protocol and ports, and the traceroute results but can be changed to add more properties to the objects.

An example scan with nmap to perform the traceroute feature would be:

nmap -sS -p22,80,443,445,8080 -oX network_one.xml --traceroute

Mandatory options are:

  • -oX (or -oA)
    • to save the output file in XML format
  • –traceroute
    • to scan for the IP path of the packets until the destination


Setup Neo4J (for this howto)

The next example assumes disabled authentication in the neo4j database. To do this configure in the neo4j.conf file the following property:

This is optional but if you do not disabled it you will have to change the authentication in the scripts presented.


Nmap XML to Neo4J

To import the Nmap results XML to a Neo4j database the following script can be used.

This script will create a database with two object types: Host and Port. The relation between those two is has_port, meaning if a port is returned in the results for a host it will be presented as this relationship. The relationship between Host objects is connects_to, meaning the host A passed the network traffic to host B.

Host object types have and address property. Port have several properties: number, proto, service, state.

import sys, json
from xml.dom.minidom import parse
from neo4j import GraphDatabase

NEO4J_URL = "bolt://"

created = []

driver = GraphDatabase.driver(NEO4J_URL)
session = driver.session(database="neo4j")

def add_node(tx, data):
    result ="CREATE (h:Host) SET h.address = $address RETURN id(h)", address=data['data']['id'])
    return result.single()[0]

def add_nmap_results(tx, results):
    address, proto, nr, state, service = results"MATCH (h:Host {address: $address}) MERGE (h)-[:has_port]->(:Port {number: $nr, proto: $proto, service: $service, state: $state})", address=address, nr=nr, state=state, proto=proto, service=service)

def relate_nodes(tx, nodes):
    first, last = nodes"MATCH (src:Host {address: $first}), (dst:Host {address: $last}) MERGE (src)-[:connects_to]->(dst)", first=first, last=last)

src = "SRC"
data = {'data': {'id': src}}
id = session.execute_write(add_node, data)

def parse_elements(dom):
    hosts = dom.getElementsByTagName('host')
    for host in hosts:
        address = host.getElementsByTagName('address')[0].getAttribute('addr')
        print("Adding " + address)
        data = {'data': {'id': address}}
        id = session.execute_write(add_node, data)

        ports = host.getElementsByTagName('ports')[0]
        for port in ports.getElementsByTagName('port'):
            proto, nr = port.getAttribute('protocol'), port.getAttribute('portid')
            state = port.getElementsByTagName('state')[0].getAttribute('state')
            try: service = port.getElementsByTagName('service')[0].getAttribute('name')
            except: service = ""
            session.execute_write(add_nmap_results, (address, proto, nr, state, service))

            trace = host.getElementsByTagName('trace')[0]
            hops = trace.getElementsByTagName('hop')
            last = src
            for hop in hops:
                ip = hop.getAttribute('ipaddr')
                if ip not in created:
                    data = {'data': {'id': ip}}
                    session.execute_write(add_node, data)
                session.execute_write(relate_nodes, (last, ip))
                last = ip

for arg in sys.argv[1:]:
    dom = parse(arg)

This may take a long time depending on the number of objects.


Neo4J queries

The major advantage of having the network map in Neo4J is then the search possibility and graph retrieval. Following are some sugestions on queries than can be usefull for network discovery.


Find routers

MATCH (n1:Host)-[:connects_to*2]->(n2:Host) RETURN n1

Matches all Host that have connections to 2 or more Host.


Find routers with open ports

MATCH (n1:Host)-[:has_port]->(p1:Port {state: 'open'}), (n1)-[:connects_to*2]->(n2:Host) RETURN n1

Matches all Host with a Port with state “open” that connects to 2 or more Host


Walk to all number 22 open ports

MATCH (n1:Host {address: 'SRC'})-[:connects_to*]->(n2:Host)-[:has_port]->(p:Port {state: 'open', number: '22'}) RETURN n1

Matches all paths to the different Host with Port number 22 in state “open”.


All paths to host

MATCH p1=((n1:Host {address: 'SRC'})-[:connects_to*]->(n2:Host {address: ''})) RETURN p1

Matches all paths to Host from SRC node.


Converting to a interactive HTML

In the end, if you want an interactive HTML map, without dependencies beyond the static HTML/JS/CSS files, you may use the following script.

In this script the Neo4J host is at URL and the statement used is for retrieving the full map of Neo4J database:

match p=((n1:Host)-[:connects_to]->(n2:Host)-[:has_port*]->(port:Port)) return p

If for instance the Port object is not needed in the end HTML it may be excluded from the query, becoming:

match p=((n1:Host)-[:connects_to]->(n2:Host)) return p


This is the query that will render the map and you can choose whatever you want to represent.

import sys, json, requests

db = {'nodes': [], 'edges': []}

response ="", json={
  "statements": [
      "statement": "match p=((n1:Host)-[:connects_to]->(n2:Host)-[:has_port*]->(port:Port)) return p",
      "resultDataContents": ["graph"]

if response.status_code == 201:
    results = response.json()
    for result in results['results']:
        for data in result['data']:
            for node in data['graph']['nodes']:
                if node['labels'][0] == "Host":
                    label = node['properties']['address']
                elif node['labels'][0] == "Port":
                    label = node['properties']['number']
                    raise Exception("Cannot handle it!")

                n = {'id': node['id'], 'classes': node['labels'], 'data': node['properties'], 'label': label}
                db['nodes'].append({'data': n})
            for node in data['graph']['relationships']:
                n = {'id': node['id'], 'source': node['startNode'], 'target': node['endNode'], 'type': node['type']}
                db['edges'].append({'data': n})

elements = {'elements': db,
            'layout': {'name': 'euler', 'randomize': True, 'animate': True},
            'style': [
                {'selector': 'node', 'style': {'label': 'data(label)'}},
                {'selector': 'edge', 'style': {'label': 'data(type)'}}

<div id='cy' class='mw-100 mh-100' style="width: 1000px; height: 1000px"></div>
<a href="javascript:redraw()">Reorganize</a>
<script src="js/cytoscape.min.js"></script>
<script src="js/cytoscape-euler.js"></script>
<script src="js/popper.min.js"></script>
<script src="js/cytoscape-popper.js"></script>
<script src="js/tippy-bundle.umd.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
function redraw(){
  newlayout = cy.layout({name: "euler"});;

var cy = cytoscape({
    container: document.getElementById('cy'),
    elements: %s,
    layout: %s,
    style: %s
""" % (json.dumps(elements['elements']), json.dumps(elements['layout']), json.dumps(elements['style'])))


The final result will look something like the following image. The HTML is pannable and zoomable and you can select nodes.


Final Thoughts

In conclusion, this article serves as a comprehensive guide to the multifaceted world of traceroute mapping tools. By examining their role in network visualization, issue identification, and defense planning, it aims to provide readers with a deeper understanding of these tools’ significance in contemporary network management and security.

Hope you like this post and keep up coming for more. If you need more information about network mapping and what ArtResilia can do for you feel free to reach out. You are always welcome!



Hugo Trovão