Building a Network Discovery Tool using AI

This project was done using the paid for version of ChatGPT. I started with a simple task of wanting to create a simple discovery client that could use SNMP to automatically identify and discover the contents of the routing table for multiple routers. The task proved simple enough, but the AI that generated the code needed some work. For the most part, the code was pretty good. Where AI falls short is when it runs into outdated data in the database. For example, it tends to pull from outdated libraries. In this case, it tried to use the pysnmp.hlapi library, but didn’t realize that much of the discovery modules were broken out of the that library and moved to the pysnmplib library. After multiple attempts, I just looked up where the functions existed. What is interesting is that when I inform the learning engine that the modules had moved, it recognized the action. It just hadn’t built the connection between the two. I have found this to be common when asking AI to write code.

I used Pycharm to load the python code and used pip install to pull in the required libraries. This was the agent’s first attempt at interpreting the problem.

✅ 

Feasibility Breakdown

1. Detect SNMP on a Router

  • How: Try SNMP queries (GET, WALK) on known OIDs (Object Identifiers).
  • Tools/Protocols: snmpwalk, snmpget (Net-SNMP), Python’s pysnmp library.
  • Detection Logic:
    • Attempt SNMP queries using common community strings (public, private) and SNMP versions (v1, v2c, v3).
    • If the response is valid, SNMP is active.

2. Determine Make and Model

  • How: Query the sysDescr.0 OID: .1.3.6.1.2.1.1.1.0.
  • Example Result: “Cisco IOS Software, C3750 Software (C3750-IPBASEK9-M), Version 12.2(55)SE7, RELEASE SOFTWARE (fc1)”.
  • AI Enhancement: Use a small model or regex classifier to normalize and extract make/model/version info across different vendors.

3. Pull the Routing Table

  • How: Walk the routing-related MIBs:
    • For IPv4: .1.3.6.1.2.1.4.21 (ipRouteTable)
    • For IPv6: .1.3.6.1.2.1.4.24.4 (ipCidrRouteTable)
  • Result: Destination networks, next hops, interface IDs, metrics.

4. Discover All Clients in the Table

  • How:
    • Use the ARP table MIB (ipNetToMediaTable – .1.3.6.1.2.1.4.22) or bridge MIBs for Layer 2 devices.
    • Optionally use:
      • .1.3.6.1.2.1.17.4.3.1 (MAC address table)
      • .1.3.6.1.2.1.2.2.1 (interface indexes)
  • Output: IP → MAC mappings, associated interface, potentially hostname.

5. Feed That Data to a File

  • How: Simple JSON, CSV, or database export using Python or Go.
  • Example Output:
  • {
  • “router”: {
  • “ip”: “192.168.1.1”,
  • “model”: “Cisco C3750”,
  • “routing_table”: […],
  • “clients”: […]
  • }
  • }

Part of the problem was asking the AI how it could be used to improve this process. Here was it’s response.

🤖 Where AI Comes In

While the SNMP discovery process is rule-based, AI could assist in:

  • Vendor normalization: Parsing sysDescr or OID trees to reliably infer make/model even across unknown or customized routers.
  • Anomaly detection: Training a model to detect suspicious routes or duplicate clients.
  • Client fingerprinting: Enriching client MAC/IP data using AI models trained to classify device types based on patterns in ARP/hostname/OUI.

🛠️ Tech Stack Suggestion

  • Language: Python (using pysnmp), or Go (for performance + concurrency)
  • Optional AI: Small language model or classifier for vendor parsing
  • Output Format: JSON or CSV
  • Deployment: Docker container for portable execution across environments

✅ Summary

TaskFeasible?Method
Detect SNMPSNMP query test
Get Make/ModelsysDescr OID
Pull Routing TableSNMP walk routing MIBs
Find ClientsARP/Bridge/MAC tables
Save to FileExport to JSON/CSV
AI Value AddOptionalParsing, enrichment, anomaly detection

The above image was not the first image, but I had added requirements to allow input of multiple routers at the same time and process them in parallel (as much as that can be done with python)

Note, the AI assumed only one type of discovery for this application. It used SNMP v2c. I figured I could update it for the others once it finished. This was a test to see how quickly it could be done.

This was the original code it produced. I emboldened the area that failed initially due to using an outdated library.

from pysnmp.hlapi import (

    SnmpEngine,

    CommunityData,

    UdpTransportTarget,

    ContextData,

    ObjectType,

    ObjectIdentity,

    get_cmd,

    next_cmd

)

import json

router_ip = ‘192.168.1.1’

community_string = ‘public’

snmp_port = 161

SYS_DESCR_OID = ‘1.3.6.1.2.1.1.1.0’

IP_ROUTE_TABLE_OID = ‘1.3.6.1.2.1.4.21’

ARP_TABLE_OID = ‘1.3.6.1.2.1.4.22.1.2’

def snmp_get(oid):

    iterator = GetCmd(

        SnmpEngine(),

        CommunityData(community_string, mpModel=1),

        UdpTransportTarget((router_ip, snmp_port)),

        ContextData(),

        ObjectType(ObjectIdentity(oid))

    )

    try:

        errorIndication, errorStatus, errorIndex, varBinds = next(iterator)

        if errorIndication:

            print(f”SNMP GET error: {errorIndication}”)

            return None

        elif errorStatus:

            print(f”SNMP GET error: {errorStatus.prettyPrint()}”)

            return None

        else:

            for varBind in varBinds:

                return str(varBind[1])

    except StopIteration:

        return None

def snmp_walk(base_oid):

    results = []

    for (errorIndication, errorStatus, errorIndex, varBinds) in NextCmd(

        SnmpEngine(),

        CommunityData(community_string, mpModel=1),

        UdpTransportTarget((router_ip, snmp_port)),

        ContextData(),

        ObjectType(ObjectIdentity(base_oid)),

        lexicographicMode=False

    ):

        if errorIndication:

            print(f”SNMP WALK error: {errorIndication}”)

            break

        elif errorStatus:

            print(f”SNMP WALK error: {errorStatus.prettyPrint()}”)

            break

        for varBind in varBinds:

            results.append((str(varBind[0]), str(varBind[1])))

    return results

# Run Discovery

make_model = snmp_get(SYS_DESCR_OID)

routing_table = snmp_walk(IP_ROUTE_TABLE_OID)

arp_table = snmp_walk(ARP_TABLE_OID)

output = {

    ‘router_ip’: router_ip,

    ‘make_model’: make_model,

    ‘routing_table’: routing_table,

    ‘clients’: arp_table

}

with open(‘snmp_discovery_output.json’, ‘w’, encoding=’utf-8′) as f:

    json.dump(output, f, indent=2)

print(“Discovery complete. Output written to snmp_discovery_output.json”)

One of the nice things about Pycharm is that it does have it’s own AI helper that can usually help identify and correct the problem. But in this case, it only identified that the package did not contain these components. I asked it which components in the package had been deprecated after I had discovered that the GetCmd and NextCmd commands had been deprecated and replaced. Here was its response.

🧾 

Deprecated vs Updated Functions and Patterns

Deprecated in pysnmp 5.x+✅ Updated EquivalentUsed In
getCmdget_cmdSNMP GET
nextCmdnext_cmdSNMP WALK
setCmdset_cmdSNMP SET (not used here)
bulkCmdbulk_cmdSNMP BULK (not used here)
mpModel=1 (v2c)Still valid but may switch to named constants in futureSNMP version
lexicographicMode=FalseStill supportedSNMP WALK behavior

It still insisted that these parts worked even though they didn’t exist in the package.

ComponentStatusNotes
SnmpEngine✅ Still validCore SNMP context
CommunityData✅ Still validCommunity string (v1/v2c)
UdpTransportTarget✅ Still validTransport to router
ContextData✅ Still validUsed in SNMPv3 but needed in all
ObjectType✅ Still validWraps the OID
ObjectIdentity✅ Still validUsed to define MIB OIDs

After telling it that the components till didn’t exist, it went though multiple troubleshooting suggestions that didn’t work because it assumed user error in importing the components. It did have me update to the latest version of the libraries to no avail.

After some research myself, I told it the following :
the modules are now contained in pysnmplib which must be installed 

This was it’s response

You’re absolutely correct — as of recent updates, pysnmp was split, and the SNMP high-level API (hlapi) components are now contained in a separate package called pysnmplib.



✅ Summary of the Change

TaskOld (before 2024)✅ New (2024+)
SNMP GET, WALK, SET, etc.from pysnmp.hlapi import …from pysnmplib.hlapi import …
Installationpip install pysnmppip install pysnmplib
SNMP engine + transportStill from pysnmpStill from pysnmp

I then proceeded to install the pssnmplib package using pip install. Afterward, the errors disappeared.

However, I did do an analysis of the file it produced and told the JetBrains AI to fix the code to make it better. It actually produced the following code:

JetBrains refactored the code to this.  # SNMP Discovery Tool – Python 3.13 & pysnmp 5.x Compatible

# Requires: pip install pysnmp>=5.0.0

from pysnmp.hlapi import (

    SnmpEngine,

    CommunityData,

    UdpTransportTarget,

    ContextData,

    ObjectType,

    ObjectIdentity,

    getCmd as get_cmd,

    nextCmd as next_cmd

)

import json

# Configuration

SNMP_CONFIG = {

    ‘router_ip’: ‘192.168.1.1’,

    ‘community_string’: ‘public’,

    ‘port’: 161

}

# Common OIDs

SYS_DESCR_OID = ‘1.3.6.1.2.1.1.1.0’  # sysDescr

IP_ROUTE_TABLE_OID = ‘1.3.6.1.2.1.4.21’  # ipRouteTable

ARP_TABLE_OID = ‘1.3.6.1.2.1.4.22.1.2’  # ipNetToMediaPhysAddress

def perform_snmp_get(oid):

    “””

    Perform an SNMP GET operation for a specific OID.

    “””

    result = None

    iterator = get_cmd(

        SnmpEngine(),

        CommunityData(SNMP_CONFIG[‘community_string’], mpModel=1),

        UdpTransportTarget((SNMP_CONFIG[‘router_ip’], SNMP_CONFIG[‘port’])),

        ContextData(),

        ObjectType(ObjectIdentity(oid))

    )

    try:

        error_indication, error_status, _, var_binds = next(iterator)

        if error_indication:

            print(f”SNMP GET error: {error_indication}”)

        elif error_status:

            print(f”SNMP GET error: {error_status.prettyPrint()}”)

        else:

            result = str(var_binds[0][1]) if var_binds else None

    except StopIteration:

        print(“SNMP GET iterator stopped unexpectedly.”)

    return result

def perform_snmp_walk(base_oid):

    “””

    Perform an SNMP WALK operation starting from a base OID.

    “””

    results = []

    for (error_indication, error_status, _, var_binds) in next_cmd(

            SnmpEngine(),

            CommunityData(SNMP_CONFIG[‘community_string’], mpModel=1),

            UdpTransportTarget((SNMP_CONFIG[‘router_ip’], SNMP_CONFIG[‘port’])),

            ContextData(),

            ObjectType(ObjectIdentity(base_oid)),

            lexicographicMode=False

    ):

        if error_indication:

            print(f”SNMP WALK error: {error_indication}”)

            break

        elif error_status:

            print(f”SNMP WALK error: {error_status.prettyPrint()}”)

            break

        results.extend((str(var_bind[0]), str(var_bind[1])) for var_bind in var_binds)

    return results

def save_to_json(data, filename):

    “””

    Save dictionary data to a JSON file.

    “””

    try:

        with open(filename, ‘w’, encoding=’utf-8′) as file:

            json.dump(data, file, indent=2)

        print(f”Discovery complete. Output written to {filename}”)

    except IOError as e:

        print(f”Error writing to {filename}: {e}”)

# Step 1: Get Router Make and Model

make_and_model = perform_snmp_get(SYS_DESCR_OID)

# Step 2: Get Routing Table

routing_table = perform_snmp_walk(IP_ROUTE_TABLE_OID)

# Step 3: Get ARP Table (Connected Clients)

arp_table = perform_snmp_walk(ARP_TABLE_OID)

# Step 4: Format and Save Output

discovery_data = {

    ‘router_ip’: SNMP_CONFIG[‘router_ip’],

    ‘make_model’: make_and_model,

    ‘routing_table’: routing_table,

    ‘clients’: arp_table

}

save_to_json(discovery_data, ‘snmp_discovery_output.json’)

I fed this back into the chatGPT bot and it refactored its own code with the changes.

It was at that point that it asked me if I wanted to be able to batch scan multiple routers, which I did. And it asked if I wanted to run them in parallel and produce separate JSON output files, which I also did.

It did ask if I wanted to add additional logic for timeouts, but I skipped that for now.

I did ask which routers would this script be compatible with and it provided the following response.

Yes — this script uses SNMP v2c with standard MIBs, meaning it will work on any router or switch that:

  1. Supports SNMP v2c, and
  2. Implements standard MIBs (particularly for system description, routing table, and ARP table)

✅ Confirmed Compatible Router Brands

These vendors typically support the SNMP MIBs used in this script:

BrandCompatible?Notes
Cisco✅ YesFully supports SNMP v2c and the standard MIBs used here (sysDescr, ipRouteTable, ipNetToMediaTable)
Juniper✅ YesSupports standard MIBs, but some SNMP views may be restricted by default
HP / Aruba✅ YesGenerally SNMP-enabled by default; CLI often needed to enable public read
Ubiquiti✅ Yes (with config)SNMP must be explicitly enabled in settings
MikroTik✅ YesSNMP v2c supported; routing table may need RouterOS access for full results
TP-Link (business series)✅ YesSNMP v2c enabled on some models (Omada, JetStream, etc.)
Netgear ProSAFE / Insight✅ YesSome enterprise models support SNMP out of the box
Fortinet (FortiGate)✅ PartialsysDescr works; routing/ARP may require FortiOS SNMP configuration
Dell Networking / SonicWALL✅ PartialSNMP support varies by firmware and model
Huawei✅ YesSupports standard SNMP OIDs in enterprise gear

Not bad for just playing around.

It also added this without prompting:



⚠️ May Require Extra Configuration

BrandReason
Consumer-grade TP-Link / Netgear / Linksys / ASUSSNMP is often disabled or not supported
FortinetMight need MIB extensions loaded for full routing info
Nokia / AlcatelUses vendor-specific MIBs for many data points
SD-WAN appliancesMay use non-standard OIDs or require SNMP traps instead of polling



❗ Unsupported or Limited

randReason
Google Nest Routers❌ No SNMP support
Eero / Mesh Routers❌ No SNMP support
Apple Airport Extreme❌ SNMP not supported

It then proceeded to tell me how it works.

✅ How to Verify If It Works

To test compatibility on any brand:

  1. Enable SNMP v2c and set the community string (public or custom).
  2. Run: snmpget -v2c -c public 1.3.6.1.2.1.1.1.0
  3. If you get a response (e.g., Cisco IOS…), the script should work.

It then asked the following:


Would you like me to generate a detection module that maps the sysDescr string to a normalized brand name (e.g., Cisco, Juniper)?

Yes

✅ Brand detection has been added to the script!

Now, for each router scanned, the output JSON includes a “brand” field, automatically inferred from the sysDescr string using known vendor keywords.

Example snippet from output:json

{
“router_ip”: “192.168.1.1”,
“make_model”: “Cisco IOS Software, …”,
“brand”: “Cisco”,
“routing_table”: […],
“clients”: […]
}

I then asked it to output a summary table of all scanned brands at the end

and extend this detection to include firmware versions or model numbers.

✅ Your script now extracts and reports:

  • Brand (based on sysDescr)
  • Model (using regex for model/hardware/platform fields)
  • Firmware version (based on version/v tokens)

🔚 At the end of the scan, it prints a summary table like this:

Summary of Scanned Routers:

IP Address Brand Model Firmware

192.168.1.1 Cisco C3750 12.2(55)SE7
192.168.1.2 MikroTik RB2011 6.49.2

Now, the important thing when creating a script like this and probably the most difficult is testing. Asking the AI which emulators (I’ve played with several) it recommends to test it, it came up with this answer.

Summary of Scanned Routers:

IP Address Brand Model Firmware

192.168.1.1 Cisco C3750 12.2(55)SE7
192.168.1.2 MikroTik RB2011 6.49.2

pip install snmpsim
snmpsim-command-responder –data-dir=./data –agent-udpv4-endpoint=127.0.0.1:161

Good for scripting SNMP test scenarios.



✅ Recommendation

Your NeedUse This
Real firmware behaviorGNS3 or EVE-NG
Easy Cisco-like setupPacket Tracer
Custom SNMP scenariossnmpsim
Commercial-grade emulationSNMP Research Agent Simulator

I then asked it for directions setting up EV-NG (I’ve done this before, but wanted to see the accuracy)

Here’s a step-by-step guide to install and set up EVE-NG (Community Edition) on a local machine or VM, configure SNMP-enabled routers inside it, and load custom MIBs from GitHub to test your SNMP discovery script.


✅ STEP 1: 

Install EVE-NG Community Edition

🔧 Option A: Use VMware Workstation or ESXi

  1. Download the EVE-NG ISO:
  2. Create a new VM with these specs:
    • 4+ vCPUs
    • 8+ GB RAM
    • 50+ GB disk
    • OS: Ubuntu 20.04/22.04 or Debian 10+
    • Network: Bridged mode or NAT
  3. Boot from ISO and install EVE-NG following on-screen instructions.

✅ STEP 2: 

Access and Configure EVE-NG

  1. After boot, find the IP address from the VM console.
  2. Open your browser and go to: https://<eve-ng-ip>
  3. Login:
    • Username: admin
    • Password: eve
  4. Set up a lab environment:
    • Click + Add New Lab
    • Name it SNMP Test Lab
    • Open the lab and click Add an Object → Node

✅ STEP 3: 

Add a Router with SNMP Support

To test SNMP fully, you need real router images.

✅ Supported SNMP-enabled OS images for EVE-NG:

VendorImage TypeNotes
CiscoIOSv (vios) or IOSvL2Must be legally obtained
JunipervSRX / vMXSupports SNMPv2/v3
MikroTikCHRRouterOS with SNMP
AristavEOSSNMPv2c enabled by default

Upload an image (e.g., Cisco IOSv):

  1. Use scp or WinSCP to upload .qcow2 to: /opt/unetlab/addons/qemu/i86bi-linux-l2-adventerprisek9-15.2/
  2. Fix permissions:/opt/unetlab/wrappers/unl_wrapper -a fixpermissions
  3. Restart the EVE-NG node list.
  4. Add the router node to your lab.

    ✅ STEP 4: Enable SNMP on the Router Node


Start the router, open console (telnet or web), and run:



For Cisco IOS:
conf t
snmp-server community public RO
snmp-server enable traps
end
write memory

✅ STEP 5: 

Install MIBs on Your Local Machine

Now your EVE router is SNMP-enabled.

Note, due to the proprietary nature of MIB files, you must be careful using them for anything beyond personal use.

If you’re using MIB files from GitHub, such as Cisco’s MIBs:

  1. Clone a MIB repo:git clone https://github.com/cisco/mibs-all.git git clone https://github.com/cisco/mibs-all.git
  2. Place the .mib files in a directory, e.g. ~/snmp-mibs.
  3. For Linux/macOS, set MIB path: export MIBS=ALL. export MIBDIRS=~/snmp-mibs
  4. Optional: Use a viewer to browse MIBs: sudo apt install snmp-mibs-downloader

✅ STEP 6: 

Run Your Script Against the EVE-NG Router


On your Python host (outside of EVE-NG):

python snmp_discovery.py
Make sure your script points to the EVE-NG router IP and uses SNMP community public.

✅ STEP 7: (Optional) Use snmpwalk to Validate MIBs

snmpwalk -v2c -c public 1.3.6.1.2.1.1.1.0
snmpwalk -v2c -c public -m +CISCO-SMI

✅ Summary

TaskTool or Step
EVE-NG SetupVMware + ISO or nested VM
Router SimulationIOSv, Juniper vSRX, MikroTik CHR
Enable SNMPIOS CLI or Juniper config
Load MIBsGitHub clone + MIBDIRS env
Test ScriptPoint to EVE-NG SNMP IP

At this point, I’ve asked the AI bot to enhance the code to include clients and the following data.

{
“ip”: “192.168.1.10”,
“mac”: “00:11:22:33:44:55”,
“interface”: “GigabitEthernet1/0/1”,
“hostname”: “printer-office”,
“mac_vendor”: “HP Inc.”,
“client_type”: “Printer”,
“ipNetToMediaType”: “dynamic”
}

To get the vendor, we need the mac-vendor-lookup library which we have included using the pip install mac-vendor-lookup command

The following is the code it produced.

#SNMP Discovery Tool – Enriched with Client Metadata

#Requires: pip install pysnmp>=5.0.0

#Optional: pip install mac-vendor-lookup

from pysnmp.hlapi import (
SnmpEngine,
CommunityData,
UdpTransportTarget,
ContextData,
ObjectType,
ObjectIdentity,
getCmd as get_cmd,
nextCmd as next_cmd
)
import json
import os
import re
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
from mac_vendor_lookup import MacLookup

#Configuration: List of routers to scan

ROUTERS = [
{‘router_ip’: ‘192.168.1.1’, ‘community_string’: ‘public’, ‘port’: 161},
{‘router_ip’: ‘192.168.1.2’, ‘community_string’: ‘public’, ‘port’: 161},
{‘router_ip’: ‘192.168.1.3’, ‘community_string’: ‘public’, ‘port’: 161},
]

#$Common OIDs

SYS_DESCR_OID = ‘1.3.6.1.2.1.1.1.0’
IP_ROUTE_TABLE_OID = ‘1.3.6.1.2.1.4.21.1.1’
IP_ROUTE_MASK_OID = ‘1.3.6.1.2.1.4.21.1.11’
IP_ROUTE_IFINDEX_OID = ‘1.3.6.1.2.1.4.21.1.2’
IF_NAME_OID = ‘1.3.6.1.2.1.31.1.1.1.1’
ARP_MAC_OID = ‘1.3.6.1.2.1.4.22.1.2’
ARP_IFINDEX_OID = ‘1.3.6.1.2.1.4.22.1.1’
ARP_TYPE_OID = ‘1.3.6.1.2.1.4.22.1.4’

BRAND_KEYWORDS = {
‘Cisco’: [‘cisco’, ‘ios’],
‘Juniper’: [‘juniper’, ‘junos’],
‘MikroTik’: [‘mikrotik’, ‘routeros’],
‘Ubiquiti’: [‘ubiquiti’, ‘edgeos’, ‘unifi’],
‘HP’: [‘hp’, ‘hewlett-packard’, ‘aruba’],
‘Netgear’: [‘netgear’],
‘TP-Link’: [‘tp-link’],
‘Fortinet’: [‘fortinet’, ‘fortigate’],
‘Huawei’: [‘huawei’],
‘Dell’: [‘dell’, ‘sonicwall’],
‘Alcatel’: [‘alcatel’, ‘nokia’]
}

summary_table = []
mac_lookup = MacLookup()

def detect_brand_info(description):
if not description:
return ‘Unknown’, ‘Unknown’, ‘Unknown’
desc_lower = description.lower()
brand = ‘Unknown’
for b, keywords in BRAND_KEYWORDS.items():
if any(keyword in desc_lower for keyword in keywords):
brand = b
break
model_match = re.search(r'(model|platform|hardware)[^,\n:][:\s]([\w-]+)’, description, re.IGNORECASE)
version_match = re.search(r'(version|v)[\s]*([\w.()-]+)’, description, re.IGNORECASE)
model = model_match.group(2) if model_match else ‘Unknown’
firmware = version_match.group(2) if version_match else ‘Unknown’
return brand, model, firmware

def perform_snmp_walk(base_oid, config):
results = []
for (error_indication, error_status, _, var_binds) in next_cmd(
SnmpEngine(),
CommunityData(config[‘community_string’], mpModel=1),
UdpTransportTarget((config[‘router_ip’], config[‘port’])),
ContextData(),
ObjectType(ObjectIdentity(base_oid)),
lexicographicMode=False
):
if error_indication:
print(f”SNMP WALK error for {config[‘router_ip’]}: {error_indication}”)
break
elif error_status:
print(f”SNMP WALK error for {config[‘router_ip’]}: {error_status.prettyPrint()}”)
break
results.extend((str(var_bind[0]), str(var_bind[1])) for var_bind in var_binds)
return results

def resolve_hostname(ip_address):
try:
return socket.gethostbyaddr(ip_address)[0]
except Exception:
return ‘Unknown’

def save_to_json(data, filename):
try:
with open(filename, ‘w’, encoding=’utf-8′) as file:
json.dump(data, file, indent=2)
print(f”Output written to {filename}”)
except IOError as e:
print(f”Error writing to {filename}: {e}”)

def scan_router(router):
print(f”Scanning {router[‘router_ip’]}…”)
sys_descr = perform_snmp_walk(SYS_DESCR_OID, router)
ip_routes = perform_snmp_walk(IP_ROUTE_TABLE_OID, router)
masks = dict(perform_snmp_walk(IP_ROUTE_MASK_OID, router))
interfaces = dict(perform_snmp_walk(IP_ROUTE_IFINDEX_OID, router))
ifnames = dict(perform_snmp_walk(IF_NAME_OID, router))
arp_mac = dict(perform_snmp_walk(ARP_MAC_OID, router))
arp_ifindex = dict(perform_snmp_walk(ARP_IFINDEX_OID, router))
arp_type = dict(perform_snmp_walk(ARP_TYPE_OID, router))

brand, model, firmware = detect_brand_info(sys_descr[0][1] if sys_descr else ”)
enriched_routes = []

for oid, ip in ip_routes:
ip_suffix = oid.split(‘.’)[-4:]
ip_key = ‘.’.join(ip_suffix)
mask = masks.get(f”1.3.6.1.2.1.4.21.1.11.{ip_key}”, ‘Unknown’)
if_index = interfaces.get(f”1.3.6.1.2.1.4.21.1.2.{ip_key}”, ‘Unknown’)
if_name = ifnames.get(f”1.3.6.1.2.1.31.1.1.1.1.{if_index}”, ‘Unknown’)
hostname = resolve_hostname(ip)
enriched_routes.append({
‘ip’: ip,
‘subnet_mask’: mask,
‘interface’: if_name,
‘hostname’: hostname
})

enriched_clients = []
for oid, mac in arp_mac.items():
ip_parts = oid.split(‘.’)[-4:]
ip = ‘.’.join(ip_parts)
if_index_oid = f”1.3.6.1.2.1.4.22.1.1.1.{ip}”
type_oid = f”1.3.6.1.2.1.4.22.1.4.1.{ip}”
if_index = arp_ifindex.get(if_index_oid, ‘Unknown’)
interface = ifnames.get(f”1.3.6.1.2.1.31.1.1.1.1.{if_index}”, ‘Unknown’)
hostname = resolve_hostname(ip)
mac_vendor = mac_lookup.lookup(mac) if mac else ‘Unknown’
client_type = ‘dynamic’ if arp_type.get(type_oid) == ‘3’ else ‘static’

enriched_clients.append({
‘ip’: ip,
‘mac’: mac,
‘interface’: interface,
‘hostname’: hostname,
‘mac_vendor’: mac_vendor,
‘client_type’: client_type
})

discovery_data = {
‘router_ip’: router[‘router_ip’],
‘make_model’: sys_descr[0][1] if sys_descr else ”,
‘brand’: brand,
‘model’: model,
‘firmware’: firmware,
‘routing_table’: enriched_routes,
‘clients’: enriched_clients
}

summary_table.append({
‘router_ip’: router[‘router_ip’],
‘brand’: brand,
‘model’: model,
‘firmware’: firmware
})

filename = f”snmp_discovery_{router[‘router_ip’].replace(‘.’, ‘_’)}.json”
save_to_json(discovery_data, filename)
return router[‘router_ip’]

#Parallel execution

with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(scan_router, router): router for router in ROUTERS}
for future in as_completed(futures):
ip = futures[future][‘router_ip’]
try:
result = future.result()
print(f”Scan completed for {result}”)
except Exception as e:
print(f”Error scanning {ip}: {e}”)

#Summary Table

print(“\nSummary of Scanned Routers:”)
print(f”{‘IP Address’:<16} {‘Brand’:<12} {‘Model’:<20} {‘Firmware’:<20}”)
print(“-” * 70)
for entry in summary_table:
print(f”{entry[‘router_ip’]:<16} {entry[‘brand’]:<12} {entry[‘model’]:<20} {entry[‘firmware’]:<20}”)

A Sample JSON file of the output is as follows:

{
“router_ip”: “192.168.1.1”,
“make_model”: “Cisco IOS Software, C3750 Software (C3750-IPBASEK9-M), Version 12.2(55)SE7”,
“brand”: “Cisco”,
“model”: “C3750”,
“firmware”: “12.2(55)SE7”,
“routing_table”: [
{
“ip”: “192.168.1.0”,
“subnet_mask”: “255.255.255.0”,
“interface”: “GigabitEthernet1/0/1”,
“hostname”: “local-network”
}
],
“clients”: [
{
“ip”: “192.168.1.10”,
“mac”: “00:11:22:33:44:55”,
“interface”: “GigabitEthernet1/0/1”,
“hostname”: “printer-office”,
“mac_vendor”: “HP Inc.”,
“client_type”: “dynamic”
},
{
“ip”: “192.168.1.11”,
“mac”: “66:77:88:99:AA:BB”,
“interface”: “GigabitEthernet1/0/2”,
“hostname”: “workstation-jdoe”,
“mac_vendor”: “Dell Inc.”,
“client_type”: “static”
}
]
}

Updated the code for additional logging and retries

#SNMP Discovery Tool – With Retries, Error Handling, and Logging

#Requires: pip install pysnmp>=5.0.0, mac-vendor-lookup

from pysnmp.hlapi import (
SnmpEngine,
CommunityData,
UdpTransportTarget,
ContextData,
ObjectType,
ObjectIdentity,
getCmd as get_cmd,
nextCmd as next_cmd
)
import json
import os
import re
import socket
import csv
import time
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from mac_vendor_lookup import MacLookup

#Logging setup

logging.basicConfig(filename=’snmp_discovery.log’, level=logging.INFO,
format=’%(asctime)s [%(levelname)s] %(message)s’)

ROUTERS = [
{‘router_ip’: ‘192.168.1.1’, ‘community_string’: ‘public’, ‘port’: 161},
{‘router_ip’: ‘192.168.1.2’, ‘community_string’: ‘public’, ‘port’: 161},
{‘router_ip’: ‘192.168.1.3’, ‘community_string’: ‘public’, ‘port’: 161},
]

#SNMP OIDs

SYS_DESCR_OID = ‘1.3.6.1.2.1.1.1.0’
SYS_UPTIME_OID = ‘1.3.6.1.2.1.1.3.0’
IP_ROUTE_TABLE_OID = ‘1.3.6.1.2.1.4.21.1.1’
IP_ROUTE_MASK_OID = ‘1.3.6.1.2.1.4.21.1.11’
IP_ROUTE_IFINDEX_OID = ‘1.3.6.1.2.1.4.21.1.2’
IF_NAME_OID = ‘1.3.6.1.2.1.31.1.1.1.1’
IF_DESC_OID = ‘1.3.6.1.2.1.31.1.1.1.18’
IF_STATUS_OID = ‘1.3.6.1.2.1.2.2.1.8’
IF_SPEED_OID = ‘1.3.6.1.2.1.2.2.1.5’
ARP_MAC_OID = ‘1.3.6.1.2.1.4.22.1.2’
ARP_IFINDEX_OID = ‘1.3.6.1.2.1.4.22.1.1’
ARP_TYPE_OID = ‘1.3.6.1.2.1.4.22.1.4’

BRAND_KEYWORDS = {
‘Cisco’: [‘cisco’, ‘ios’],
‘Juniper’: [‘juniper’, ‘junos’],
‘MikroTik’: [‘mikrotik’, ‘routeros’],
‘Ubiquiti’: [‘ubiquiti’, ‘edgeos’, ‘unifi’],
‘HP’: [‘hp’, ‘hewlett-packard’, ‘aruba’],
‘Netgear’: [‘netgear’],
‘TP-Link’: [‘tp-link’],
‘Fortinet’: [‘fortinet’, ‘fortigate’],
‘Huawei’: [‘huawei’],
‘Dell’: [‘dell’, ‘sonicwall’],
‘Alcatel’: [‘alcatel’, ‘nokia’]
}

summary_table = []
mac_lookup = MacLookup()

def detect_brand_info(description):
if not description:
return ‘Unknown’, ‘Unknown’, ‘Unknown’
desc_lower = description.lower()
brand = ‘Unknown’
for b, keywords in BRAND_KEYWORDS.items():
if any(keyword in desc_lower for keyword in keywords):
brand = b
break
model_match = re.search(r'(model|platform|hardware)[^,\n:][:\s]([\w-]+)’, description, re.IGNORECASE)
version_match = re.search(r'(version|v)[\s]*([\w.()-]+)’, description, re.IGNORECASE)
model = model_match.group(2) if model_match else ‘Unknown’
firmware = version_match.group(2) if version_match else ‘Unknown’
return brand, model, firmware

def perform_snmp_walk(base_oid, config, retries=3):
results = []
for attempt in range(retries):
try:
for (error_indication, error_status, _, var_binds) in next_cmd(
SnmpEngine(),
CommunityData(config[‘community_string’], mpModel=1),
UdpTransportTarget((config[‘router_ip’], config[‘port’]), timeout=2, retries=1),
ContextData(),
ObjectType(ObjectIdentity(base_oid)),
lexicographicMode=False
):
if error_indication:
raise Exception(error_indication)
elif error_status:
raise Exception(error_status.prettyPrint())
results.extend((str(var_bind[0]), str(var_bind[1])) for var_bind in var_binds)
return results
except Exception as e:
logging.warning(f”Retry {attempt+1}/{retries} for {base_oid} on {config[‘router_ip’]}: {e}”)
time.sleep(1)
logging.error(f”Failed SNMP walk for OID {base_oid} on {config[‘router_ip’]} after {retries} retries.”)
return results

def perform_snmp_get(oid, config, retries=3):
for attempt in range(retries):
try:
iterator = get_cmd(
SnmpEngine(),
CommunityData(config[‘community_string’], mpModel=1),
UdpTransportTarget((config[‘router_ip’], config[‘port’]), timeout=2, retries=1),
ContextData(),
ObjectType(ObjectIdentity(oid))
)
error_indication, error_status, _, var_binds = next(iterator)
if error_indication or error_status:
raise Exception(error_indication or error_status.prettyPrint())
return str(var_binds[0][1])
except Exception as e:
logging.warning(f”Retry {attempt+1}/{retries} for {oid} on {config[‘router_ip’]}: {e}”)
time.sleep(1)
logging.error(f”Failed SNMP get for OID {oid} on {config[‘router_ip’]} after {retries} retries.”)
return ‘Unknown’

def resolve_hostname(ip_address):
try:
return socket.gethostbyaddr(ip_address)[0]
except Exception:
return ‘Unknown’

def save_to_json(data, filename):
with open(filename, ‘w’, encoding=’utf-8′) as file:
json.dump(data, file, indent=2)

def export_clients_to_csv(clients, filename):
if not clients:
return
with open(filename, ‘w’, newline=”) as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=clients[0].keys())
writer.writeheader()
for row in clients:
writer.writerow(row)

def scan_router(router):
logging.info(f”Starting scan of {router[‘router_ip’]}”)
try:
sys_descr = perform_snmp_walk(SYS_DESCR_OID, router)
uptime = perform_snmp_get(SYS_UPTIME_OID, router)
ip_routes = perform_snmp_walk(IP_ROUTE_TABLE_OID, router)
masks = dict(perform_snmp_walk(IP_ROUTE_MASK_OID, router))
interfaces = dict(perform_snmp_walk(IP_ROUTE_IFINDEX_OID, router))
ifnames = dict(perform_snmp_walk(IF_NAME_OID, router))
ifdescs = dict(perform_snmp_walk(IF_DESC_OID, router))
ifstatus = dict(perform_snmp_walk(IF_STATUS_OID, router))
ifspeeds = dict(perform_snmp_walk(IF_SPEED_OID, router))
arp_mac = dict(perform_snmp_walk(ARP_MAC_OID, router))
arp_ifindex = dict(perform_snmp_walk(ARP_IFINDEX_OID, router))
arp_type = dict(perform_snmp_walk(ARP_TYPE_OID, router))

brand, model, firmware = detect_brand_info(sys_descr[0][1] if sys_descr else ”)
enriched_routes = []

for oid, ip in ip_routes:
ip_suffix = oid.split(‘.’)[-4:]
ip_key = ‘.’.join(ip_suffix)
mask = masks.get(f”1.3.6.1.2.1.4.21.1.11.{ip_key}”, ‘Unknown’)
if_index = interfaces.get(f”1.3.6.1.2.1.4.21.1.2.{ip_key}”, ‘Unknown’)
if_name = ifnames.get(f”1.3.6.1.2.1.31.1.1.1.1.{if_index}”, ‘Unknown’)
hostname = resolve_hostname(ip)
enriched_routes.append({
‘ip’: ip,
‘subnet_mask’: mask,
‘interface’: if_name,
‘hostname’: hostname
})

enriched_clients = []
for oid, mac in arp_mac.items():
ip_parts = oid.split(‘.’)[-4:]
ip = ‘.’.join(ip_parts)
if_index_oid = f”1.3.6.1.2.1.4.22.1.1.1.{ip}”
type_oid = f”1.3.6.1.2.1.4.22.1.4.1.{ip}”
if_index = arp_ifindex.get(if_index_oid, ‘Unknown’)
interface = ifnames.get(f”1.3.6.1.2.1.31.1.1.1.1.{if_index}”, ‘Unknown’)
hostname = resolve_hostname(ip)
mac_vendor = mac_lookup.lookup(mac) if mac else ‘Unknown’
client_type = ‘dynamic’ if arp_type.get(type_oid) == ‘3’ else ‘static’
desc = ifdescs.get(f”1.3.6.1.2.1.31.1.1.1.18.{if_index}”, ‘Unknown’)
speed = ifspeeds.get(f”1.3.6.1.2.1.2.2.1.5.{if_index}”, ‘Unknown’)
status = ifstatus.get(f”1.3.6.1.2.1.2.2.1.8.{if_index}”, ‘Unknown’)

enriched_clients.append({
‘ip’: ip,
‘mac’: mac,
‘interface’: interface,
‘hostname’: hostname,
‘mac_vendor’: mac_vendor,
‘client_type’: client_type,
‘port_description’: desc,
‘port_speed’: speed,
‘port_status’: ‘up’ if status == ‘1’ else ‘down’
})

discovery_data = {
‘router_ip’: router[‘router_ip’],
‘make_model’: sys_descr[0][1] if sys_descr else ”,
‘brand’: brand,
‘model’: model,
‘firmware’: firmware,
‘uptime’: uptime,
‘routing_table’: enriched_routes,
‘clients’: enriched_clients
}

summary_table.append({
‘router_ip’: router[‘router_ip’],
‘brand’: brand,
‘model’: model,
‘firmware’: firmware
})

base_name = router[‘router_ip’].replace(‘.’, ‘_’)
save_to_json(discovery_data, f”snmp_discovery_{base_name}.json”)
export_clients_to_csv(enriched_clients, f”clients_{base_name}.csv”)
logging.info(f”Scan completed for {router[‘router_ip’]}”)
return router[‘router_ip’]

except Exception as e:
logging.error(f”Failed to scan {router[‘router_ip’]}: {e}”)
return None

with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(scan_router, router): router for router in ROUTERS}
for future in as_completed(futures):
ip = futures[future][‘router_ip’]
try:
result = future.result()
if result:
print(f”Scan completed for {result}”)
else:
print(f”Scan failed for {ip}”)
except Exception as e:
print(f”Unhandled error scanning {ip}: {e}”)

print(“\nSummary of Scanned Routers:”)
print(f”{‘IP Address’:<16} {‘Brand’:<12} {‘Model’:<20} {‘Firmware’:<20}”)
print(“-” * 70)
for entry in summary_table:
print(f”{entry[‘router_ip’]:<16} {entry[‘brand’]:<12} {entry[‘model’]:<20} {entry[‘firmware’]:<20}”)

Will continue to update this as I test the code

Let me know if you have any comments