Snippet Name: IP_Resolver_Utility
Tags:
Created Date: 2025-06-16 13:12:49
Last Modified Date: 2025-05-31 10:32:16
Dependencies:
DBA Notes:
Sample Output:
Title: IP Resolver Utility
Author: user
help me write a utility similar to fastresolver.exe, which quickly interrogates every IP in a given range, say 10.0.0.1 to 10.0.0.255. it tries to resovle the IP address, Hostname,OriginalName,Mac Address And Company name(of the nic card?) I would like it to have an enhancement where if it finds the mac address from a previous scan, it populates any empty values from the previous scan. that way, in my example, where i found 10.0.0.7 had a specific mac address, and I knew what the device was, i could update teh history witht he Hostname, so my scans are more clear in the future
Author: ChatGPT
That???s a great project idea! I can help you write a **Python utility script** that works like a lightweight `FastResolver.exe`.
It will do the following:
??? Scan an IP range (like `10.0.0.1-10.0.0.255`)
??? Resolve **IP ??? Hostname**
??? Get **MAC address** (using ARP or similar methods)
??? Look up **Vendor / Company name** from MAC (via local OUI file or online API)
??? Save results into a **history file** (like JSON or CSV)
??? On future runs, **populate missing fields from history**
Here???s a clear starting point:
---
### ???? Requirements
- Python 3.x
- `scapy` ??? for ARP ping + MAC detection
- `socket` ??? for DNS reverse lookup
- `requests` ??? if we use an external API for MAC vendor lookup
- Local JSON file ??? for history
You can install dependencies:
```bash
pip install scapy requests
```
---
### ???? Script skeleton
```python
import json
import socket
import ipaddress
import subprocess
from scapy.all import ARP, Ether, srp
import requests
HISTORY_FILE = 'scan_history.json'
# Load or initialize history
def load_history():
try:
with open(HISTORY_FILE, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_history(history):
with open(HISTORY_FILE, 'w') as f:
json.dump(history, f, indent=2)
# Get MAC vendor using external API (can be swapped for local DB)
def get_mac_vendor(mac):
try:
resp = requests.get(f'https://api.macvendors.com/{mac}')
if resp.status_code == 200:
return resp.text
except:
pass
return None
# Resolve hostname
def resolve_hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except socket.herror:
return None
# Scan IP range
def scan_ip_range(network):
history = load_history()
arp = ARP(pdst=network)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether/arp
result = srp(packet, timeout=2, verbose=0)[0]
scan_results = {}
for sent, received in result:
ip = received.psrc
mac = received.hwsrc
hostname = resolve_hostname(ip)
vendor = get_mac_vendor(mac)
if mac in history:
# Fill missing from history
previous = history[mac]
hostname = hostname or previous.get('hostname')
vendor = vendor or previous.get('vendor')
scan_results[mac] = {
'ip': ip,
'hostname': hostname,
'vendor': vendor
}
# Update history
history.update(scan_results)
save_history(history)
return scan_results
def main():
# Example range: 10.0.0.1/24
network = '10.0.0.1/24'
results = scan_ip_range(network)
print(f"{'IP':15} {'Hostname':30} {'MAC':20} {'Vendor'}")
print('-' * 80)
for mac, info in results.items():
print(f"{info['ip']:15} {info['hostname'] or '':30} {mac:20} {info['vendor'] or ''}")
if __name__ == "__main__":
main()
```
---
### ??? What it does:
??? ARP sweep on the range
??? Reverse DNS lookup ??? hostname
??? MAC address extraction
??? Vendor lookup using macvendors.com API
??? Local history caching in `scan_history.json`
??? Fills missing info next time
---
### ???? Enhancements you can add later
- Add `--range` argument to specify IP range dynamically
- Add `--update-name` to manually assign friendly names to MACs in the history
- Save results to CSV for easier viewing
- Use a local OUI database (like `oui.txt`) instead of hitting an API
---
### Example command to run:
```bash
python fastresolver.py
```
---
If you like, I can also:
??? Write the local **OUI vendor parser**
??? Add **command-line arguments**
??? Package this into a **single EXE** using `pyinstaller`
Would you like me to prepare any of those next? Let me know! ????
Author: user
codelooks like a minor dependancy is needed, as i get the following error: RuntimeError: Sniffing and sending packets is not available at layer 2: winpcap is not installed. You may use conf.L3socket or conf.L3socket6 to access layer 3
Author: ChatGPT
Great catch ??? you???re absolutely right!
That error happens because **Scapy** on Windows needs **Npcap** or **WinPcap** installed to send and sniff packets at Layer 2 (for ARP scanning). Without it, Scapy can???t send raw Ethernet frames, which is why the script fails on:
```python
result = srp(packet, timeout=2, verbose=0)[0]
```
### ??? How to fix it on Windows
1. **Install Npcap (recommended over WinPcap)**
??? Download Npcap from:
[https://npcap.com/#download](https://npcap.com/#download)
??? During install:
- Check **???Install Npcap in WinPcap API-compatible Mode???** ??? this ensures legacy compatibility.
- You can leave the other options as defaults.
2. **Re-run your script.**
---
### ??? Alternative: Use Layer 3 scanning fallback
If you don???t want to require Npcap, you can switch the scan to a **ping sweep** or **TCP connect scan**, which only needs regular socket access.
Here???s an updated lightweight fallback:
```python
import subprocess
def ping_sweep(ip_range):
live_hosts = []
for ip in ip_range:
result = subprocess.run(['ping', '-n', '1', '-w', '500', str(ip)],
stdout=subprocess.DEVNULL)
if result.returncode == 0:
live_hosts.append(str(ip))
return live_hosts
```
Replace the Scapy ARP section with a loop like:
```python
ip_net = ipaddress.ip_network('10.0.0.1/24')
live_ips = ping_sweep(ip_net.hosts())
for ip in live_ips:
hostname = resolve_hostname(ip)
mac = None # You may need to use arp -a parsing as a fallback
vendor = None
# Populate from history
# ...
```
---
### ???? Summary
- ??? **Recommended** ??? Install [Npcap](https://npcap.com/#download) with WinPcap mode ??? keep using Scapy ARP scans
- ??? **Alternative** ??? Switch to ping or TCP connect sweep + parse `arp -a` for MACs
---
If you want, I can prepare:
??? A full **ping + arp -a fallback script**
??? A **combined script** that tries Scapy first, then falls back to ping if needed
Would you like me to write one of those versions for you? ???? Let me know!
Author: user
i received some excellent output, however, the python file i know was executed from this location, "G:\My Drive\_Data\Python", but the history output file was not created there
Author: ChatGPT
Great observation ??? this happens because the script is currently using:
```python
HISTORY_FILE = 'scan_history.json'
```
This writes the file **to the ???current working directory??? (CWD)** where the Python process is running, which is not always the same as the directory where the script file (`.py`) is located.
In your case, even though you executed:
```bash
python G:\My Drive\_Data\Python\fastresolver.py
```
if you ran it from, say,
```bash
cd C:\Users\Lowell
python G:\My Drive\_Data\Python\fastresolver.py
```
the `scan_history.json` would end up in `C:\Users\Lowell` ??? because that???s the current working directory.
---
### ??? Fix: Make sure the script writes history next to itself
Modify the code like this:
```python
import os
# Get directory where script is located
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HISTORY_FILE = os.path.join(SCRIPT_DIR, 'scan_history.json')
```
Full snippet for context:
```python
import json
import socket
import ipaddress
import subprocess
import os
from scapy.all import ARP, Ether, srp
import requests
# Ensure history saves next to script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HISTORY_FILE = os.path.join(SCRIPT_DIR, 'scan_history.json')
def load_history():
try:
with open(HISTORY_FILE, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_history(history):
with open(HISTORY_FILE, 'w') as f:
json.dump(history, f, indent=2)
```
---
### ???? Result
No matter **where** you run the script from, the history file will always be saved in:
```
G:\My Drive\_Data\Python\scan_history.json
```
if that???s where your `.py` script lives.
---
??? Let me know if you want me to package the **full updated script** for you!
Author: user
that change was effective, thank you. lets change he final output to save an html page named scan_history, but append a timestamp to the name, in the same location, and open the page after execution
Author: ChatGPT
Perfect! Here???s how we can extend your script to:
??? Save the scan results as an **HTML page** with a timestamped filename
??? Save it in the same folder as the script
??? Automatically **open it in the default web browser** after execution
---
### ???? Code changes
We will:
- Add a function to **generate an HTML table**
- Name the file like `scan_history_YYYYMMDD_HHMMSS.html`
- Use the `webbrowser` module to open it
---
### ??? Updated code snippet
```python
import json
import socket
import ipaddress
import subprocess
import os
import datetime
import webbrowser
from scapy.all import ARP, Ether, srp
import requests
# Ensure history saves next to script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HISTORY_FILE = os.path.join(SCRIPT_DIR, 'scan_history.json')
def load_history():
try:
with open(HISTORY_FILE, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_history(history):
with open(HISTORY_FILE, 'w') as f:
json.dump(history, f, indent=2)
def get_mac_vendor(mac):
try:
resp = requests.get(f'https://api.macvendors.com/{mac}')
if resp.status_code == 200:
return resp.text
except:
pass
return None
def resolve_hostname(ip):
try:
return socket.gethostbyaddr(ip)[0]
except socket.herror:
return None
def scan_ip_range(network):
history = load_history()
arp = ARP(pdst=network)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether/arp
result = srp(packet, timeout=2, verbose=0)[0]
scan_results = {}
for sent, received in result:
ip = received.psrc
mac = received.hwsrc
hostname = resolve_hostname(ip)
vendor = get_mac_vendor(mac)
if mac in history:
previous = history[mac]
hostname = hostname or previous.get('hostname')
vendor = vendor or previous.get('vendor')
scan_results[mac] = {
'ip': ip,
'hostname': hostname,
'vendor': vendor
}
history.update(scan_results)
save_history(history)
return scan_results
def save_html_report(results):
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
html_filename = os.path.join(SCRIPT_DIR, f'scan_history_{timestamp}.html')
html = """
<html><head><title>Scan History</title>
<style>
table {border-collapse: collapse; width: 100%;}
th, td {border: 1px solid #ddd; padding: 8px;}
th {background-color: #f2f2f2;}
</style></head><body>
<h2>Network Scan Report - {timestamp}</h2>
<table>
<tr><th>IP</th><th>Hostname</th><th>MAC</th><th>Vendor</th></tr>
""".format(timestamp=timestamp)
for mac, info in results.items():
html += f"<tr><td>{info&#91;'ip']}</td><td>{info&#91;'hostname'] or ''}</td><td>{mac}</td><td>{info&#91;'vendor'] or ''}</td></tr>"
html += "</table></body></html>"
with open(html_filename, 'w') as f:
f.write(html)
webbrowser.open(f'file://{html_filename}')
def main():
network = '10.0.0.1/24'
results = scan_ip_range(network)
print(f"{'IP':15} {'Hostname':30} {'MAC':20} {'Vendor'}")
print('-' * 80)
for mac, info in results.items():
print(f"{info&#91;'ip']:15} {info&#91;'hostname'] or '':30} {mac:20} {info&#91;'vendor'] or ''}")
save_html_report(results)
if __name__ == "__main__":
main()
```
---
### ???? What???s added:
??? **HTML file** like `scan_history_20250508_203000.html`
??? **Styled table** for readable output
??? **Auto-open in browser** using `webbrowser.open()`
---
If you want, I can also help you:
- Add **sorting** or search to the HTML (with JavaScript)
- Save a **master summary page** listing all past HTML reports
Want me to prepare that next? ???? Let me know!
Author: user
i got an error File "g:\My Drive\_Data\Python\FastResolverImproved.py", line 90, in save_html_report
""".format(timestamp=timestamp)
Author: ChatGPT
Good catch!
The error likely happened because of the `.format()` call trying to process **braces `{}`** inside the CSS section:
```html
<style>
table {border-collapse: collapse; width: 100%;}
th, td {border: 1px solid #ddd; padding: 8px;}
th {background-color: #f2f2f2;}
</style>
```
Python interprets those `{}` as placeholders ??? but you want them to stay as **literal curly braces** in the HTML.
---
### ??? How to fix it
You need to **escape the braces** by doubling them:
```python
<style>
table {{border-collapse: collapse; width: 100%;}}
th, td {{border: 1px solid #ddd; padding: 8px;}}
th {{background-color: #f2f2f2;}}
</style>
```
So the corrected section becomes:
```python
html = """
<html><head><title>Scan History</title>
<style>
table {{border-collapse: collapse; width: 100%;}}
th, td {{border: 1px solid #ddd; padding: 8px;}}
th {{background-color: #f2f2f2;}}
</style></head><body>
<h2>Network Scan Report - {timestamp}</h2>
<table>
<tr><th>IP</th><th>Hostname</th><th>MAC</th><th>Vendor</th></tr>
""".format(timestamp=timestamp)
```
---
### ???? Why this happens
In Python, when you write:
```python
"something {placeholder}".format(placeholder='value')
```
the `{placeholder}` is replaced.
If you want a **literal `{` or `}`**, you must **double it**:
```python
"{{ literal brace }}"
```
---
### ??? Summary of fix
??? Update all CSS braces to `{{` and `}}`
??? Keep `.format(timestamp=timestamp)` as it is
---
If you like, I can send you the fully patched version ready to paste in ??? want me to do that? ????
Author: user
ok, how can we order the results by the last octet?
Author: ChatGPT
Great improvement idea ??? ordering by the **last octet** will make the output easier to read!
Here???s a clean way to sort the results by the **last part of the IP** before building the HTML or console output.
---
### ??? Solution
Modify this part of your code **before printing or saving to HTML**:
```python
# Convert scan_results dict to list of (mac, info) tuples
sorted_results = sorted(
results.items(),
key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1])
)
```
Then **loop over `sorted_results` instead of `results.items()`**:
```python
for mac, info in sorted_results:
print(f"{info&#91;'ip']:15} {info&#91;'hostname'] or '':30} {mac:20} {info&#91;'vendor'] or ''}")
```
And in the HTML generator:
```python
for mac, info in sorted_results:
html += f"<tr><td>{info&#91;'ip']}</td><td>{info&#91;'hostname'] or ''}</td><td>{mac}</td><td>{info&#91;'vendor'] or ''}</td></tr>"
```
---
### ??? Code example
Here???s the patch you can drop into your `main()`:
```python
def main():
network = '10.0.0.1/24'
results = scan_ip_range(network)
# Sort by last octet
sorted_results = sorted(
results.items(),
key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1])
)
print(f"{'IP':15} {'Hostname':30} {'MAC':20} {'Vendor'}")
print('-' * 80)
for mac, info in sorted_results:
print(f"{info&#91;'ip']:15} {info&#91;'hostname'] or '':30} {mac:20} {info&#91;'vendor'] or ''}")
save_html_report(dict(sorted_results))
```
And **inside `save_html_report()`**, just use:
```python
for mac, info in sorted(results.items(), key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1])):
html += f"<tr><td>{info&#91;'ip']}</td><td>{info&#91;'hostname'] or ''}</td><td>{mac}</td><td>{info&#91;'vendor'] or ''}</td></tr>"
```
---
### ??? Bonus
If you want to **sort by full IP address**, not just last octet:
```python
key=lambda item: tuple(map(int, item&#91;1]&#91;'ip'].split('.')))
```
Example:
```python
sorted_results = sorted(
results.items(),
key=lambda item: tuple(map(int, item&#91;1]&#91;'ip'].split('.')))
)
```
---
Want me to send you the **fully patched script ready to paste**?
Just say ???yes??? ????
Author: user
in our a=scan_history.json file, previously, i had seen an entry like below. I know it's not currently online, so I would like any entries not found by mac address to be visible in the report with an Asterisk or some other identifier that shows "not currently online" "7c:1c:4e:82:48:5b": {
"ip": "10.0.0.233",
"hostname": "LGTelevision",
"vendor": "LG Innotek"
}
Author: ChatGPT
Excellent enhancement ??? this will make your report even more useful by showing both:
??? **Devices currently online**
??? **Devices known from history but not seen in the current scan (marked clearly, e.g., with an asterisk ??? or ???OFFLINE???)**
---
### ??? How we???ll do it
1. **Load the full history (`scan_history.json`)**
2. **Mark which MAC addresses were found in the current scan**
3. When generating the report (console + HTML):
- If a device was not seen in the current scan, show:
- `ip*` ??? with asterisk
- or extra column like `Status: ONLINE / OFFLINE`
---
### ???? Code changes
Modify your **main()** like this:
```python
def main():
network = '10.0.0.1/24'
current_results = scan_ip_range(network)
history = load_history()
# Mark which MACs are online
full_results = {}
for mac, data in history.items():
online = mac in current_results
full_results&#91;mac] = {
'ip': data&#91;'ip'],
'hostname': data&#91;'hostname'],
'vendor': data&#91;'vendor'],
'online': online
}
if online:
# Use fresh IP, hostname, vendor from scan
full_results&#91;mac].update(current_results&#91;mac])
# Sort by last octet
sorted_results = sorted(
full_results.items(),
key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1])
)
print(f"{'IP':15} {'Hostname':30} {'MAC':20} {'Vendor':30} {'Status'}")
print('-' * 100)
for mac, info in sorted_results:
ip_display = f"{info&#91;'ip']}*" if not info&#91;'online'] else info&#91;'ip']
status = "OFFLINE" if not info&#91;'online'] else "ONLINE"
print(f"{ip_display:15} {info&#91;'hostname'] or '':30} {mac:20} {info&#91;'vendor'] or '':30} {status}")
save_html_report(full_results)
```
---
### ???? Update `save_html_report()`
```python
def save_html_report(results):
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
html_filename = os.path.join(SCRIPT_DIR, f'scan_history_{timestamp}.html')
html = """
<html><head><title>Scan History</title>
<style>
table {{border-collapse: collapse; width: 100%;}}
th, td {{border: 1px solid #ddd; padding: 8px;}}
th {{background-color: #f2f2f2;}}
</style></head><body>
<h2>Network Scan Report - {timestamp}</h2>
<table>
<tr><th>IP</th><th>Hostname</th><th>MAC</th><th>Vendor</th><th>Status</th></tr>
""".format(timestamp=timestamp)
# Sort by last octet
sorted_results = sorted(results.items(), key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1]))
for mac, info in sorted_results:
ip_display = f"{info&#91;'ip']}*" if not info&#91;'online'] else info&#91;'ip']
status = "OFFLINE" if not info&#91;'online'] else "ONLINE"
html += f"<tr><td>{ip_display}</td><td>{info&#91;'hostname'] or ''}</td><td>{mac}</td><td>{info&#91;'vendor'] or ''}</td><td>{status}</td></tr>"
html += "</table></body></html>"
with open(html_filename, 'w') as f:
f.write(html)
webbrowser.open(f'file://{html_filename}')
```
---
### ???? Example output
```
IP Hostname MAC Vendor Status
----------------------------------------------------------------------------------------------------
10.0.0.7 Printer aa:bb:cc:dd:ee:ff HP Inc ONLINE
10.0.0.233* LGTelevision 7c:1c:4e:82:48:5b LG Innotek OFFLINE
```
And the **HTML** will show:
- `10.0.0.233*` ??? with ???OFFLINE??? in the last column
---
### ??? Summary of changes
??? Show all known devices, even if not currently online
??? Add `*` next to IP and `ONLINE` / `OFFLINE` column
??? Keep everything sorted by last octet
---
???? Want me to package the **full final script** for you, ready to paste?
Just say: **???yes, send full script???** ????
Author: user
lets add a "Last Seen" Datetime stamp to the history, so if something was offline,w e have an estimate of the last time we saw it
Author: ChatGPT
Perfect ??? adding a **`LastSeen` timestamp** will give your scan history real forensic value, especially for tracking when devices disappear or return.
---
### ??? Plan
We???ll do three things:
1. ???? **Add a `last_seen` timestamp** (`datetime.now().isoformat()`) to each device when it is seen online
2. ???? Keep the existing `last_seen` timestamp **unchanged** for devices that are offline
3. ???? Display `Last Seen` in both the **console** and **HTML report**
---
### ??? Code Changes
#### ???? Modify `scan_ip_range()` to return a "seen now" set
Update it so it **does not write to history**, just returns current live results.
Then, in `main()`, we merge with history and apply the timestamp logic.
---
### ???? Updated `main()` logic
Replace your current `main()` with this:
```python
def main():
network = '10.0.0.1/24'
current_results = scan_ip_range(network)
history = load_history()
now = datetime.datetime.now().isoformat()
full_results = {}
for mac, data in history.items():
was_seen = mac in current_results
full_results&#91;mac] = {
'ip': current_results&#91;mac]&#91;'ip'] if was_seen else data&#91;'ip'],
'hostname': current_results&#91;mac]&#91;'hostname'] if was_seen else data.get('hostname'),
'vendor': current_results&#91;mac]&#91;'vendor'] if was_seen else data.get('vendor'),
'online': was_seen,
'last_seen': now if was_seen else data.get('last_seen', 'Unknown')
}
# Add any new MACs not previously seen
for mac, data in current_results.items():
if mac not in full_results:
full_results&#91;mac] = {
'ip': data&#91;'ip'],
'hostname': data.get('hostname'),
'vendor': data.get('vendor'),
'online': True,
'last_seen': now
}
# Save full results back to history file
save_history({mac: {
'ip': info&#91;'ip'],
'hostname': info&#91;'hostname'],
'vendor': info&#91;'vendor'],
'last_seen': info&#91;'last_seen']
} for mac, info in full_results.items()})
# Sort and print
sorted_results = sorted(full_results.items(), key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1]))
print(f"{'IP':15} {'Hostname':30} {'MAC':20} {'Vendor':30} {'Status':10} {'Last Seen'}")
print('-' * 130)
for mac, info in sorted_results:
ip_display = f"{info&#91;'ip']}*" if not info&#91;'online'] else info&#91;'ip']
status = "OFFLINE" if not info&#91;'online'] else "ONLINE"
print(f"{ip_display:15} {info&#91;'hostname'] or '':30} {mac:20} {info&#91;'vendor'] or '':30} {status:10} {info&#91;'last_seen']}")
save_html_report(full_results)
```
---
### ???? Update `save_html_report()` to include `Last Seen`
Replace the table header and row loop:
```python
html += "<tr><th>IP</th><th>Hostname</th><th>MAC</th><th>Vendor</th><th>Status</th><th>Last Seen</th></tr>"
for mac, info in sorted(results.items(), key=lambda item: int(item&#91;1]&#91;'ip'].split('.')&#91;-1])):
ip_display = f"{info&#91;'ip']}*" if not info&#91;'online'] else info&#91;'ip']
status = "OFFLINE" if not info&#91;'online'] else "ONLINE"
html += f"<tr><td>{ip_display}</td><td>{info&#91;'hostname'] or ''}</td><td>{mac}</td><td>{info&#91;'vendor'] or ''}</td><td>{status}</td><td>{info&#91;'last_seen']}</td></tr>"
```
---
### ??? Final Result
Your report will now show:
| IP | Hostname | MAC | Vendor | Status | Last Seen |
|--------------|--------------|-------------------|--------------|----------|------------------------|
| 10.0.0.7 | Printer | aa:bb:cc:dd:ee:ff | HP Inc | ONLINE | 2025-05-11T09:33:42 |
| 10.0.0.233\* | LGTelevision | 7c:1c:4e:82:48:5b | LG Innotek | OFFLINE | 2025-04-18T11:27:16 |
---
Would you like me to deliver the **full copy-paste script with all changes rolled in**?