A host-discovery and local-inventory tool that sweeps an authorized subnet for live hosts (ICMP/TCP) and enumerates the local machine's interfaces, addresses, and open connections with psutil.
Asset inventory is the foundation of both offense and defense: you cannot secure what you cannot see. This project does two things — it sweeps a CIDR subnet you own to find responsive hosts (via a ping helper and a TCP fallback), and it inventories the local machine (interfaces, IPs, MACs, and listening sockets) using psutil. The subnet sweep is threaded for speed and uses ipaddress to expand the range safely.
[!warning] Authorized use only Sweep only networks you own or are authorized to inventory. Host discovery is active reconnaissance. Examples default to your own local subnet and localhost.
- Enumerate local network interfaces, addresses, and routes.
- List listening sockets and map them to owning processes.
- Discover live hosts on an authorized local subnet.
- Produce a repeatable inventory suitable for change detection.
- Distinguish local introspection from active network probing.
| Item | Detail |
|---|---|
| Python | 3.9 or newer |
| Dependencies | psutil (python3 -m pip install psutil); socket, ipaddress from the standard library |
| Privileges | None for local inventory; root to see process names for other users' sockets |
| Target | The local host, and a lab subnet you own for discovery |
| Note | Local inventory is passive; host discovery is active |
- CLI —
argparsesubcommands:sweep <cidr>andlocal. - Range expander —
ipaddress.ip_network()yields host addresses. - Host prober — a TCP connect to a common port (fallback when ICMP needs root), threaded via
ThreadPoolExecutor. - Local inventory —
psutil.net_if_addrs()for interfaces andpsutil.net_connections()for sockets. - Reporter — prints live hosts and a local summary table.
sweep: CIDR ─▶ hosts ─▶ ThreadPool(tcp_alive) ─▶ live host list
local: psutil ─▶ interfaces + addresses + listening sockets ─▶ table
network-inventory/
├── netinv.py # CLI (sweep / local)
└── requirements.txt # psutil
- List interfaces. Use
psutil.net_if_addrs()andnet_if_stats()for addresses, netmasks, and link state. - List listening sockets.
psutil.net_connections(kind="inet"), filtering toLISTEN. - Map to processes. Resolve each socket's PID to a process name and command line, handling processes that vanish mid-scan.
- Flag exposure. Highlight anything bound to
0.0.0.0or::rather than loopback — that is the finding that matters. - Derive the subnet. Combine address and netmask with
ipaddress.ip_network()to get the local network. - Discover hosts. Optionally sweep the subnet with a bounded TCP connect check on common ports.
- Emit a stable inventory. Sort deterministically and write JSON so two runs can be diffed.
- Diff mode. Compare against a previous inventory and report what appeared or disappeared.
#!/usr/bin/env python3
"""netinv.py - Subnet host discovery + local network inventory (educational).
Sweep only networks you own or are authorized to inventory.
"""
import argparse
import ipaddress
import socket
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import psutil
def tcp_alive(ip: str, ports: tuple[int, ...], timeout: float) -> str | None:
"""Return ip if any probe port accepts a TCP connection, else None."""
for port in ports:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(timeout)
if sock.connect_ex((ip, port)) == 0:
return ip
return None
def sweep(cidr: str, ports: tuple[int, ...], threads: int, timeout: float) -> list[str]:
"""Threaded TCP host discovery across a CIDR range."""
net = ipaddress.ip_network(cidr, strict=False)
hosts = [str(ip) for ip in net.hosts()]
print(f"[*] Sweeping {cidr} ({len(hosts)} host(s)) on ports {ports}")
live: list[str] = []
with ThreadPoolExecutor(max_workers=threads) as pool:
futures = [pool.submit(tcp_alive, ip, ports, timeout) for ip in hosts]
for future in as_completed(futures):
result = future.result()
if result:
live.append(result)
print(f"[+] up: {result}")
return sorted(live, key=lambda ip: ipaddress.ip_address(ip))
def local_inventory() -> None:
"""Print local interfaces, addresses, and listening sockets via psutil."""
print("[*] Interfaces and addresses:")
for name, addrs in psutil.net_if_addrs().items():
for addr in addrs:
fam = addr.family.name if hasattr(addr.family, "name") else addr.family
print(f" {name:<12} {str(fam):<10} {addr.address}")
print("\n[*] Listening sockets:")
for conn in psutil.net_connections(kind="inet"):
if conn.status == psutil.CONN_LISTEN and conn.laddr:
print(f" {conn.laddr.ip}:{conn.laddr.port:<6} pid={conn.pid}")
def main() -> int:
parser = argparse.ArgumentParser(description="Network inventory tool.")
sub = parser.add_subparsers(dest="command", required=True)
sweep_p = sub.add_parser("sweep", help="discover live hosts on a subnet")
sweep_p.add_argument("cidr", help="target subnet, e.g. 192.168.1.0/24")
sweep_p.add_argument("-t", "--threads", type=int, default=100)
sweep_p.add_argument("--timeout", type=float, default=0.5)
sweep_p.add_argument("--ports", default="22,80,443,445,3389",
help="probe ports (default: 22,80,443,445,3389)")
sub.add_parser("local", help="inventory the local machine")
args = parser.parse_args()
if args.command == "sweep":
ports = tuple(int(p) for p in args.ports.split(",") if p.strip())
live = sweep(args.cidr, ports, args.threads, args.timeout)
print(f"\n[*] {len(live)} live host(s).")
elif args.command == "local":
local_inventory()
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except ValueError as exc:
print(f"[!] {exc}", file=sys.stderr)
raise SystemExit(2)pip install psutil
python netinv.py local
python netinv.py sweep 192.168.1.0/24 -t 200 --timeout 0.3[*] Interfaces and addresses:
lo AF_INET 127.0.0.1
eth0 AF_INET 192.168.1.42
eth0 AF_PACKET 02:42:ac:11:00:02
[*] Listening sockets:
127.0.0.1:631 pid=812
0.0.0.0:22 pid=749
# Local inventory only (passive)
python3 netinv.py --local
# Include listening sockets with process names (root shows all)
sudo python3 netinv.py --local --sockets
# Active discovery on an authorized lab subnet
python3 netinv.py --discover 192.168.56.0/24 --workers 32
# Save and diff
python3 netinv.py --local --json > today.json
python3 netinv.py --diff yesterday.json today.json$ python3 netinv.py --local --sockets
INTERFACES
lo 127.0.0.1/8 up loopback
eth0 192.168.56.10/24 up 1000 Mbps
LISTENING SOCKETS
PROTO LOCAL ADDRESS PID PROCESS EXPOSURE
tcp 127.0.0.1:631 1204 cupsd loopback only
tcp 0.0.0.0:22 980 sshd ALL INTERFACES
tcp 0.0.0.0:8000 18342 python3 ALL INTERFACES
udp 0.0.0.0:68 742 dhclient ALL INTERFACES
[!] 3 services bound to all interfaces - confirm each is intended
$ python3 netinv.py --diff yesterday.json today.json
[+] NEW LISTENER 0.0.0.0:8000 python3
[-] GONE 127.0.0.1:3306 mysqld
| Condition | Exception | Response |
|---|---|---|
| Insufficient privileges for socket details | psutil.AccessDenied |
Show the socket without the process name and note that root reveals more |
| Process exits mid-enumeration | psutil.NoSuchProcess |
Skip that entry; the process table is live |
psutil not installed |
ModuleNotFoundError |
Explain the install command; exit 2 |
| Interface has no address | KeyError |
Skip the address family rather than failing |
| Invalid CIDR supplied | ValueError |
Report the malformed network and exit 2 |
| Discovery target unreachable | socket.timeout |
Treat as "no response", continue the sweep |
| Previous inventory missing for diff | FileNotFoundError |
Report the path and exit 2 |
[!warning] Local inventory is passive; discovery is not Enumerating your own host is safe. Sweeping a subnet is active scanning — only do it on a network you own or are explicitly authorized to test.
- Services bound to
0.0.0.0are the finding. A development server on all interfaces is reachable by everyone on the network; that is the most common real issue this tool surfaces. - Never run subnet discovery on a network you do not control — including shared office, hotel, or cloud networks where other tenants are present.
- The inventory is highly sensitive. It lists every listening service, its version-bearing process, and the local topology. Treat it as confidential and store it accordingly.
- Root reveals more, so run it deliberately. Elevated enumeration exposes other users' processes; only do it where you are authorized to see that.
- Diffing is the real value. A new listener appearing between two runs is a strong signal — of a legitimate deployment or of something you did not expect.
- Do not chain automatically into port scanning. Discovery and probing are separate authorization decisions.
from unittest.mock import MagicMock, patch
import netinv
def test_flags_all_interface_binding():
assert netinv.exposure("0.0.0.0") == "ALL INTERFACES"
assert netinv.exposure("127.0.0.1") == "loopback only"
assert netinv.exposure("::") == "ALL INTERFACES"
def test_handles_access_denied():
import psutil
with patch("psutil.Process", side_effect=psutil.AccessDenied(pid=1)):
assert netinv.process_name(1) == "unknown"
def test_handles_vanished_process():
import psutil
with patch("psutil.Process", side_effect=psutil.NoSuchProcess(pid=1)):
assert netinv.process_name(1) == "unknown"
def test_subnet_derived_from_address_and_mask():
assert str(netinv.subnet_of("192.168.56.10", "255.255.255.0")) == "192.168.56.0/24"
def test_diff_reports_new_and_gone():
before = {"listeners": ["127.0.0.1:3306"]}
after = {"listeners": ["0.0.0.0:8000"]}
result = netinv.diff(before, after)
assert result["new"] == ["0.0.0.0:8000"]
assert result["gone"] == ["127.0.0.1:3306"]Mock psutil throughout — tests must not depend on the host's actual network state.
- True ICMP ping — use raw sockets (root) or shell out to
pingfor hosts that block all TCP. - ARP sweep — on the local L2 segment, use
scapyARP requests for faster, stealthier discovery. - MAC vendor lookup — resolve OUIs to vendor names for asset classification.
- Port/service follow-up — feed discovered hosts into [[Port-Scanner]] and [[Banner-Grabber]].
- Export — write inventory to CSV/JSON for a CMDB or diffing over time.
| Symptom | Likely cause | Fix |
|---|---|---|
Process names show as unknown |
Sockets owned by other users | Run with sudo to see the full picture |
ModuleNotFoundError: psutil |
Not installed in the active venv | python3 -m pip install psutil |
| No interfaces listed | Running in a restricted container | Expected — container namespaces limit visibility |
| Discovery finds nothing | Wrong subnet, or host firewalls dropping probes | Verify the CIDR; test one host manually |
| Inventory differs between identical runs | Ephemeral ports and short-lived processes | Filter to listening sockets and sort deterministically |
AccessDenied floods the output |
Enumerating without privileges | Handle it per socket and summarise the count |
- [[Port-Scanner]]
- [[Banner-Grabber]]
- [[Log-Analyzer]]
- [[Socket-Programming-for-Networking]]
- [[Mini-Projects/Readme|Mini-Projects]] — module index
- [[Readme|Python for Security Professionals]] — course home