
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
Task | Feasible? | Method |
---|---|---|
Detect SNMP | ✅ | SNMP query test |
Get Make/Model | ✅ | sysDescr OID |
Pull Routing Table | ✅ | SNMP walk routing MIBs |
Find Clients | ✅ | ARP/Bridge/MAC tables |
Save to File | ✅ | Export to JSON/CSV |
AI Value Add | Optional | Parsing, 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 Equivalent | Used In |
---|---|---|
getCmd | get_cmd | SNMP GET |
nextCmd | next_cmd | SNMP WALK |
setCmd | set_cmd | SNMP SET (not used here) |
bulkCmd | bulk_cmd | SNMP BULK (not used here) |
mpModel=1 (v2c) | Still valid but may switch to named constants in future | SNMP version |
lexicographicMode=False | Still supported | SNMP WALK behavior |
It still insisted that these parts worked even though they didn’t exist in the package.
Component | Status | Notes |
---|---|---|
SnmpEngine | ✅ Still valid | Core SNMP context |
CommunityData | ✅ Still valid | Community string (v1/v2c) |
UdpTransportTarget | ✅ Still valid | Transport to router |
ContextData | ✅ Still valid | Used in SNMPv3 but needed in all |
ObjectType | ✅ Still valid | Wraps the OID |
ObjectIdentity | ✅ Still valid | Used 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
Task | Old (before 2024) | ✅ New (2024+) |
---|---|---|
SNMP GET, WALK, SET, etc. | from pysnmp.hlapi import … | from pysnmplib.hlapi import … |
Installation | pip install pysnmp | pip install pysnmplib |
SNMP engine + transport | Still from pysnmp | Still 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:
- Supports SNMP v2c, and
- 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:
Brand | Compatible? | Notes |
---|---|---|
Cisco | ✅ Yes | Fully supports SNMP v2c and the standard MIBs used here (sysDescr, ipRouteTable, ipNetToMediaTable) |
Juniper | ✅ Yes | Supports standard MIBs, but some SNMP views may be restricted by default |
HP / Aruba | ✅ Yes | Generally SNMP-enabled by default; CLI often needed to enable public read |
Ubiquiti | ✅ Yes (with config) | SNMP must be explicitly enabled in settings |
MikroTik | ✅ Yes | SNMP v2c supported; routing table may need RouterOS access for full results |
TP-Link (business series) | ✅ Yes | SNMP v2c enabled on some models (Omada, JetStream, etc.) |
Netgear ProSAFE / Insight | ✅ Yes | Some enterprise models support SNMP out of the box |
Fortinet (FortiGate) | ✅ Partial | sysDescr works; routing/ARP may require FortiOS SNMP configuration |
Dell Networking / SonicWALL | ✅ Partial | SNMP support varies by firmware and model |
Huawei | ✅ Yes | Supports standard SNMP OIDs in enterprise gear |
Not bad for just playing around.
It also added this without prompting:
⚠️ May Require Extra Configuration
Brand | Reason |
---|---|
Consumer-grade TP-Link / Netgear / Linksys / ASUS | SNMP is often disabled or not supported |
Fortinet | Might need MIB extensions loaded for full routing info |
Nokia / Alcatel | Uses vendor-specific MIBs for many data points |
SD-WAN appliances | May use non-standard OIDs or require SNMP traps instead of polling |
❗ Unsupported or Limited
rand | Reason |
---|---|
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:
- Enable SNMP v2c and set the community string (public or custom).
- Run: snmpget -v2c -c public 1.3.6.1.2.1.1.1.0
- 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 Need | Use This |
---|---|
Real firmware behavior | GNS3 or EVE-NG |
Easy Cisco-like setup | Packet Tracer |
Custom SNMP scenarios | snmpsim |
Commercial-grade emulation | SNMP 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
- Download the EVE-NG ISO:
- 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
- Boot from ISO and install EVE-NG following on-screen instructions.
✅ STEP 2:
Access and Configure EVE-NG
- After boot, find the IP address from the VM console.
- Open your browser and go to: https://<eve-ng-ip>
- Login:
- Username: admin
- Password: eve
- 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:
Vendor | Image Type | Notes |
---|---|---|
Cisco | IOSv (vios) or IOSvL2 | Must be legally obtained |
Juniper | vSRX / vMX | Supports SNMPv2/v3 |
MikroTik | CHR | RouterOS with SNMP |
Arista | vEOS | SNMPv2c enabled by default |
Upload an image (e.g., Cisco IOSv):
- Use scp or WinSCP to upload .qcow2 to: /opt/unetlab/addons/qemu/i86bi-linux-l2-adventerprisek9-15.2/
- Fix permissions:/opt/unetlab/wrappers/unl_wrapper -a fixpermissions
- Restart the EVE-NG node list.
- 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:
- Clone a MIB repo:git clone https://github.com/cisco/mibs-all.git git clone https://github.com/cisco/mibs-all.git
- Place the .mib files in a directory, e.g. ~/snmp-mibs.
- For Linux/macOS, set MIB path: export MIBS=ALL. export MIBDIRS=~/snmp-mibs
- 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
Task | Tool or Step |
---|---|
EVE-NG Setup | VMware + ISO or nested VM |
Router Simulation | IOSv, Juniper vSRX, MikroTik CHR |
Enable SNMP | IOS CLI or Juniper config |
Load MIBs | GitHub clone + MIBDIRS env |
Test Script | Point 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