whoson 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- whoson-0.1.0/PKG-INFO +132 -0
- whoson-0.1.0/README.md +109 -0
- whoson-0.1.0/cli.py +155 -0
- whoson-0.1.0/config.py +13 -0
- whoson-0.1.0/discovery/__init__.py +1 -0
- whoson-0.1.0/discovery/nmap_scan.py +114 -0
- whoson-0.1.0/graph/__init__.py +1 -0
- whoson-0.1.0/graph/topology_builder.py +131 -0
- whoson-0.1.0/pyproject.toml +42 -0
- whoson-0.1.0/setup.cfg +4 -0
- whoson-0.1.0/tests/test_cli.py +187 -0
- whoson-0.1.0/tests/test_exporters.py +258 -0
- whoson-0.1.0/tests/test_scanner.py +83 -0
- whoson-0.1.0/tests/test_topology.py +162 -0
- whoson-0.1.0/utils/__init__.py +1 -0
- whoson-0.1.0/utils/exporters.py +367 -0
- whoson-0.1.0/whoson.egg-info/PKG-INFO +132 -0
- whoson-0.1.0/whoson.egg-info/SOURCES.txt +20 -0
- whoson-0.1.0/whoson.egg-info/dependency_links.txt +1 -0
- whoson-0.1.0/whoson.egg-info/entry_points.txt +2 -0
- whoson-0.1.0/whoson.egg-info/requires.txt +5 -0
- whoson-0.1.0/whoson.egg-info/top_level.txt +5 -0
whoson-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whoson
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight subnet audit tool -- see what is alive on your network
|
|
5
|
+
Author-email: Dani Issac <daniissac@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/daniissac/whoson
|
|
8
|
+
Project-URL: Repository, https://github.com/daniissac/whoson
|
|
9
|
+
Project-URL: Issues, https://github.com/daniissac/whoson/issues
|
|
10
|
+
Keywords: network,scanner,nmap,subnet,topology
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: System :: Networking
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: python-nmap
|
|
20
|
+
Requires-Dist: Pillow
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# whoson
|
|
25
|
+
|
|
26
|
+
A lightweight subnet audit tool. Scan a network, see what's alive, and export a topology image -- all from the terminal.
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/whoson/)
|
|
29
|
+
[](https://github.com/daniissac/whoson/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Prerequisites: nmap must be installed
|
|
35
|
+
brew install nmap # macOS
|
|
36
|
+
sudo apt install nmap # Debian/Ubuntu
|
|
37
|
+
sudo dnf install nmap # Fedora/RHEL
|
|
38
|
+
|
|
39
|
+
# Install whoson
|
|
40
|
+
pip install whoson
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Scan a subnet and print a host table
|
|
47
|
+
whoson 192.168.1.0/24
|
|
48
|
+
|
|
49
|
+
# Save a topology diagram as PNG
|
|
50
|
+
whoson 192.168.1.0/24 -i topology.png
|
|
51
|
+
|
|
52
|
+
# Or as JPEG or SVG
|
|
53
|
+
whoson 192.168.1.0/24 -i topology.jpg
|
|
54
|
+
whoson 192.168.1.0/24 -i topology.svg
|
|
55
|
+
|
|
56
|
+
# Export as JSON or CSV
|
|
57
|
+
whoson 192.168.1.0/24 --json results.json
|
|
58
|
+
whoson 192.168.1.0/24 --csv hosts.csv
|
|
59
|
+
|
|
60
|
+
# Use TCP connect scan instead of ping
|
|
61
|
+
whoson 192.168.1.0/24 -t tcp
|
|
62
|
+
|
|
63
|
+
# SYN stealth scan (requires root)
|
|
64
|
+
sudo whoson 10.0.0.0/24 -t syn
|
|
65
|
+
|
|
66
|
+
# Quiet mode -- only write files, no terminal output
|
|
67
|
+
whoson 192.168.1.0/24 -i out.png --json out.json -q
|
|
68
|
+
|
|
69
|
+
# Combine everything
|
|
70
|
+
whoson 192.168.1.0/24 -t tcp -i topology.png --json data.json --csv hosts.csv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Example output
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Scanning 192.168.1.0/24 (254 usable addresses, ping scan)
|
|
77
|
+
Found 5 hosts in 4.2s
|
|
78
|
+
|
|
79
|
+
IP Hostname Type MAC Vendor Ports
|
|
80
|
+
--------------------------------------------------------------------------------
|
|
81
|
+
192.168.1.1 router.local gateway AA:BB:CC:DD:EE:01 Cisco -
|
|
82
|
+
192.168.1.10 web-srv server AA:BB:CC:DD:EE:10 Dell 80,443
|
|
83
|
+
192.168.1.15 db-srv server AA:BB:CC:DD:EE:15 Dell 3306
|
|
84
|
+
192.168.1.50 - workstation AA:BB:CC:DD:EE:50 Apple -
|
|
85
|
+
192.168.1.99 hp-printer printer AA:BB:CC:DD:EE:99 HP 9100
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Host Classification
|
|
89
|
+
|
|
90
|
+
| Type | Criteria | Color |
|
|
91
|
+
|------|----------|-------|
|
|
92
|
+
| Gateway | IP ends in `.1` or `.254` | Red |
|
|
93
|
+
| Server | Open ports: 22, 80, 443, 25, 53, etc. | Teal |
|
|
94
|
+
| Printer | Open ports: 515, 631, 9100 | Green |
|
|
95
|
+
| Workstation | Default | Blue |
|
|
96
|
+
|
|
97
|
+
## Export Formats
|
|
98
|
+
|
|
99
|
+
| Format | Flag | Description |
|
|
100
|
+
|--------|------|-------------|
|
|
101
|
+
| PNG | `-i out.png` | Raster topology image |
|
|
102
|
+
| JPEG | `-i out.jpg` | Raster topology image (compressed) |
|
|
103
|
+
| SVG | `-i out.svg` | Vector topology image (scalable) |
|
|
104
|
+
| JSON | `--json FILE` | Full topology data with metadata |
|
|
105
|
+
| CSV | `--csv FILE` | Host list with IP, hostname, type, MAC, vendor, ports |
|
|
106
|
+
|
|
107
|
+
The `-i` / `--image` flag detects the format from the file extension (`.png`, `.jpg`, `.jpeg`, `.svg`).
|
|
108
|
+
|
|
109
|
+
## Scope and Limitations
|
|
110
|
+
|
|
111
|
+
whoson shows what hosts are alive and classifies them by type. It does **not** discover actual Layer 2/3 topology -- it cannot determine switch port connections or router adjacencies. The image shows a star topology (all hosts connected to the gateway) because that is all that can be honestly inferred from a scan.
|
|
112
|
+
|
|
113
|
+
For real topology discovery using CDP/LLDP/SNMP, see [LibreNMS](https://www.librenms.org/), [Secure Cartography](https://github.com/scottpeterman/secure_cartography), or [NetDisco](https://netdisco.org/).
|
|
114
|
+
|
|
115
|
+
## Dependencies
|
|
116
|
+
|
|
117
|
+
**Runtime:** `python-nmap` + `Pillow` (2 packages).
|
|
118
|
+
|
|
119
|
+
**System:** `nmap` must be available on `PATH`.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/daniissac/whoson.git
|
|
125
|
+
cd whoson
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
whoson-0.1.0/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# whoson
|
|
2
|
+
|
|
3
|
+
A lightweight subnet audit tool. Scan a network, see what's alive, and export a topology image -- all from the terminal.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/whoson/)
|
|
6
|
+
[](https://github.com/daniissac/whoson/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Prerequisites: nmap must be installed
|
|
12
|
+
brew install nmap # macOS
|
|
13
|
+
sudo apt install nmap # Debian/Ubuntu
|
|
14
|
+
sudo dnf install nmap # Fedora/RHEL
|
|
15
|
+
|
|
16
|
+
# Install whoson
|
|
17
|
+
pip install whoson
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Scan a subnet and print a host table
|
|
24
|
+
whoson 192.168.1.0/24
|
|
25
|
+
|
|
26
|
+
# Save a topology diagram as PNG
|
|
27
|
+
whoson 192.168.1.0/24 -i topology.png
|
|
28
|
+
|
|
29
|
+
# Or as JPEG or SVG
|
|
30
|
+
whoson 192.168.1.0/24 -i topology.jpg
|
|
31
|
+
whoson 192.168.1.0/24 -i topology.svg
|
|
32
|
+
|
|
33
|
+
# Export as JSON or CSV
|
|
34
|
+
whoson 192.168.1.0/24 --json results.json
|
|
35
|
+
whoson 192.168.1.0/24 --csv hosts.csv
|
|
36
|
+
|
|
37
|
+
# Use TCP connect scan instead of ping
|
|
38
|
+
whoson 192.168.1.0/24 -t tcp
|
|
39
|
+
|
|
40
|
+
# SYN stealth scan (requires root)
|
|
41
|
+
sudo whoson 10.0.0.0/24 -t syn
|
|
42
|
+
|
|
43
|
+
# Quiet mode -- only write files, no terminal output
|
|
44
|
+
whoson 192.168.1.0/24 -i out.png --json out.json -q
|
|
45
|
+
|
|
46
|
+
# Combine everything
|
|
47
|
+
whoson 192.168.1.0/24 -t tcp -i topology.png --json data.json --csv hosts.csv
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Example output
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Scanning 192.168.1.0/24 (254 usable addresses, ping scan)
|
|
54
|
+
Found 5 hosts in 4.2s
|
|
55
|
+
|
|
56
|
+
IP Hostname Type MAC Vendor Ports
|
|
57
|
+
--------------------------------------------------------------------------------
|
|
58
|
+
192.168.1.1 router.local gateway AA:BB:CC:DD:EE:01 Cisco -
|
|
59
|
+
192.168.1.10 web-srv server AA:BB:CC:DD:EE:10 Dell 80,443
|
|
60
|
+
192.168.1.15 db-srv server AA:BB:CC:DD:EE:15 Dell 3306
|
|
61
|
+
192.168.1.50 - workstation AA:BB:CC:DD:EE:50 Apple -
|
|
62
|
+
192.168.1.99 hp-printer printer AA:BB:CC:DD:EE:99 HP 9100
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Host Classification
|
|
66
|
+
|
|
67
|
+
| Type | Criteria | Color |
|
|
68
|
+
|------|----------|-------|
|
|
69
|
+
| Gateway | IP ends in `.1` or `.254` | Red |
|
|
70
|
+
| Server | Open ports: 22, 80, 443, 25, 53, etc. | Teal |
|
|
71
|
+
| Printer | Open ports: 515, 631, 9100 | Green |
|
|
72
|
+
| Workstation | Default | Blue |
|
|
73
|
+
|
|
74
|
+
## Export Formats
|
|
75
|
+
|
|
76
|
+
| Format | Flag | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| PNG | `-i out.png` | Raster topology image |
|
|
79
|
+
| JPEG | `-i out.jpg` | Raster topology image (compressed) |
|
|
80
|
+
| SVG | `-i out.svg` | Vector topology image (scalable) |
|
|
81
|
+
| JSON | `--json FILE` | Full topology data with metadata |
|
|
82
|
+
| CSV | `--csv FILE` | Host list with IP, hostname, type, MAC, vendor, ports |
|
|
83
|
+
|
|
84
|
+
The `-i` / `--image` flag detects the format from the file extension (`.png`, `.jpg`, `.jpeg`, `.svg`).
|
|
85
|
+
|
|
86
|
+
## Scope and Limitations
|
|
87
|
+
|
|
88
|
+
whoson shows what hosts are alive and classifies them by type. It does **not** discover actual Layer 2/3 topology -- it cannot determine switch port connections or router adjacencies. The image shows a star topology (all hosts connected to the gateway) because that is all that can be honestly inferred from a scan.
|
|
89
|
+
|
|
90
|
+
For real topology discovery using CDP/LLDP/SNMP, see [LibreNMS](https://www.librenms.org/), [Secure Cartography](https://github.com/scottpeterman/secure_cartography), or [NetDisco](https://netdisco.org/).
|
|
91
|
+
|
|
92
|
+
## Dependencies
|
|
93
|
+
|
|
94
|
+
**Runtime:** `python-nmap` + `Pillow` (2 packages).
|
|
95
|
+
|
|
96
|
+
**System:** `nmap` must be available on `PATH`.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/daniissac/whoson.git
|
|
102
|
+
cd whoson
|
|
103
|
+
pip install -e ".[dev]"
|
|
104
|
+
pytest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
whoson-0.1.0/cli.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""whoson -- lightweight subnet audit tool."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from discovery.nmap_scan import NetworkScanner
|
|
8
|
+
from graph.topology_builder import TopologyBuilder
|
|
9
|
+
from utils.exporters import TopologyExporter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _print_table(scan_results):
|
|
13
|
+
"""Print a host table to stdout."""
|
|
14
|
+
hosts = scan_results.get('hosts', [])
|
|
15
|
+
if not hosts:
|
|
16
|
+
print("No hosts found.")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
builder = TopologyBuilder()
|
|
20
|
+
|
|
21
|
+
rows = []
|
|
22
|
+
for h in hosts:
|
|
23
|
+
host_type = builder._classify_host(h)
|
|
24
|
+
ports = ','.join(str(p['port']) for p in h.get('open_ports', []))
|
|
25
|
+
rows.append({
|
|
26
|
+
'ip': h.get('ip', ''),
|
|
27
|
+
'hostname': h.get('hostname', '') or '-',
|
|
28
|
+
'type': host_type,
|
|
29
|
+
'mac': h.get('mac_address', '') or '-',
|
|
30
|
+
'vendor': h.get('vendor', '') or '-',
|
|
31
|
+
'ports': ports or '-',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
col_widths = {
|
|
35
|
+
'ip': max(len('IP'), max(len(r['ip']) for r in rows)),
|
|
36
|
+
'hostname': max(len('Hostname'), max(len(r['hostname']) for r in rows)),
|
|
37
|
+
'type': max(len('Type'), max(len(r['type']) for r in rows)),
|
|
38
|
+
'mac': max(len('MAC'), max(len(r['mac']) for r in rows)),
|
|
39
|
+
'vendor': max(len('Vendor'), max(len(r['vendor']) for r in rows)),
|
|
40
|
+
'ports': max(len('Ports'), max(len(r['ports']) for r in rows)),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
header = (
|
|
44
|
+
f"{'IP':<{col_widths['ip']}} "
|
|
45
|
+
f"{'Hostname':<{col_widths['hostname']}} "
|
|
46
|
+
f"{'Type':<{col_widths['type']}} "
|
|
47
|
+
f"{'MAC':<{col_widths['mac']}} "
|
|
48
|
+
f"{'Vendor':<{col_widths['vendor']}} "
|
|
49
|
+
f"{'Ports':<{col_widths['ports']}}"
|
|
50
|
+
)
|
|
51
|
+
sep = '-' * len(header)
|
|
52
|
+
|
|
53
|
+
print(header)
|
|
54
|
+
print(sep)
|
|
55
|
+
for r in rows:
|
|
56
|
+
print(
|
|
57
|
+
f"{r['ip']:<{col_widths['ip']}} "
|
|
58
|
+
f"{r['hostname']:<{col_widths['hostname']}} "
|
|
59
|
+
f"{r['type']:<{col_widths['type']}} "
|
|
60
|
+
f"{r['mac']:<{col_widths['mac']}} "
|
|
61
|
+
f"{r['vendor']:<{col_widths['vendor']}} "
|
|
62
|
+
f"{r['ports']:<{col_widths['ports']}}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv=None):
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
prog='whoson',
|
|
69
|
+
description='Lightweight subnet audit tool -- see what is alive on your network.',
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
'subnet',
|
|
73
|
+
help='Network in CIDR notation (e.g. 192.168.1.0/24)',
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
'-t', '--type',
|
|
77
|
+
choices=['ping', 'tcp', 'syn'],
|
|
78
|
+
default='ping',
|
|
79
|
+
help='Scan type (default: ping)',
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
'-i', '--image',
|
|
83
|
+
metavar='FILE',
|
|
84
|
+
help='Save topology image (format from extension: .png, .jpg, .svg)',
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
'--json',
|
|
88
|
+
metavar='FILE',
|
|
89
|
+
dest='json_file',
|
|
90
|
+
help='Save topology data as JSON',
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
'--csv',
|
|
94
|
+
metavar='FILE',
|
|
95
|
+
dest='csv_file',
|
|
96
|
+
help='Save host list as CSV',
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
'-q', '--quiet',
|
|
100
|
+
action='store_true',
|
|
101
|
+
help='Suppress table output (only write files)',
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
args = parser.parse_args(argv)
|
|
105
|
+
|
|
106
|
+
scanner = NetworkScanner()
|
|
107
|
+
|
|
108
|
+
if not scanner.validate_subnet(args.subnet):
|
|
109
|
+
print(f"Error: invalid subnet '{args.subnet}'", file=sys.stderr)
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
info = scanner.get_network_info(args.subnet)
|
|
113
|
+
if not args.quiet:
|
|
114
|
+
print(f"Scanning {args.subnet} ({info['usable_addresses']} usable addresses, {args.type} scan)")
|
|
115
|
+
|
|
116
|
+
t0 = time.time()
|
|
117
|
+
scan_results = scanner.scan_subnet(args.subnet, args.type)
|
|
118
|
+
elapsed = time.time() - t0
|
|
119
|
+
|
|
120
|
+
total = scan_results['total_hosts']
|
|
121
|
+
if not args.quiet:
|
|
122
|
+
print(f"Found {total} host{'s' if total != 1 else ''} in {elapsed:.1f}s\n")
|
|
123
|
+
|
|
124
|
+
if total == 0:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if not args.quiet:
|
|
128
|
+
_print_table(scan_results)
|
|
129
|
+
|
|
130
|
+
builder = TopologyBuilder()
|
|
131
|
+
topology_data = builder.build_topology(scan_results)
|
|
132
|
+
exporter = TopologyExporter()
|
|
133
|
+
|
|
134
|
+
if args.image:
|
|
135
|
+
exporter.export_image(topology_data, args.image)
|
|
136
|
+
if not args.quiet:
|
|
137
|
+
print(f"\nImage saved to {args.image}")
|
|
138
|
+
|
|
139
|
+
if args.json_file:
|
|
140
|
+
json_str = exporter.export_json(topology_data)
|
|
141
|
+
with open(args.json_file, 'w', encoding='utf-8') as f:
|
|
142
|
+
f.write(json_str)
|
|
143
|
+
if not args.quiet:
|
|
144
|
+
print(f"JSON saved to {args.json_file}")
|
|
145
|
+
|
|
146
|
+
if args.csv_file:
|
|
147
|
+
csv_str = exporter.export_csv_nodes(topology_data)
|
|
148
|
+
with open(args.csv_file, 'w', encoding='utf-8') as f:
|
|
149
|
+
f.write(csv_str)
|
|
150
|
+
if not args.quiet:
|
|
151
|
+
print(f"CSV saved to {args.csv_file}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
main()
|
whoson-0.1.0/config.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Shared configuration constants for n2g."""
|
|
2
|
+
|
|
3
|
+
NODE_COLORS = {
|
|
4
|
+
'gateway': '#ff6b6b',
|
|
5
|
+
'server': '#4ecdc4',
|
|
6
|
+
'workstation': '#45b7d1',
|
|
7
|
+
'printer': '#96ceb4',
|
|
8
|
+
'unknown': '#999999',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
SERVER_PORTS = {22, 23, 25, 53, 80, 135, 139, 443, 445, 993, 995}
|
|
12
|
+
|
|
13
|
+
PRINTER_PORTS = {515, 631, 9100}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Discovery module for network scanning
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import nmap
|
|
2
|
+
import ipaddress
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
class NetworkScanner:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.nm = nmap.PortScanner()
|
|
10
|
+
self.logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
def validate_subnet(self, subnet: str) -> bool:
|
|
13
|
+
"""Validate if the provided subnet is valid."""
|
|
14
|
+
try:
|
|
15
|
+
ipaddress.ip_network(subnet, strict=False)
|
|
16
|
+
return True
|
|
17
|
+
except ValueError:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def scan_subnet(self, subnet: str, scan_type: str = 'ping') -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Scan a subnet for live hosts using Nmap.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
subnet: Network subnet in CIDR notation (e.g., '192.168.1.0/24')
|
|
26
|
+
scan_type: Type of scan ('ping', 'tcp', 'syn')
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dictionary containing scan results
|
|
30
|
+
"""
|
|
31
|
+
if not self.validate_subnet(subnet):
|
|
32
|
+
raise ValueError(f"Invalid subnet format: {subnet}")
|
|
33
|
+
|
|
34
|
+
scan_results = {
|
|
35
|
+
'subnet': subnet,
|
|
36
|
+
'hosts': [],
|
|
37
|
+
'scan_time': time.time(),
|
|
38
|
+
'total_hosts': 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Configure scan arguments based on type
|
|
43
|
+
if scan_type == 'ping':
|
|
44
|
+
arguments = '-sn' # Ping scan only
|
|
45
|
+
elif scan_type == 'tcp':
|
|
46
|
+
arguments = '-sT -F' # TCP connect scan with fast mode
|
|
47
|
+
elif scan_type == 'syn':
|
|
48
|
+
arguments = '-sS -F' # SYN stealth scan with fast mode
|
|
49
|
+
else:
|
|
50
|
+
arguments = '-sn'
|
|
51
|
+
|
|
52
|
+
self.logger.info(f"Starting {scan_type} scan of {subnet}")
|
|
53
|
+
scan_result = self.nm.scan(hosts=subnet, arguments=arguments)
|
|
54
|
+
|
|
55
|
+
for host in self.nm.all_hosts():
|
|
56
|
+
host_info = {
|
|
57
|
+
'ip': host,
|
|
58
|
+
'hostname': '',
|
|
59
|
+
'state': self.nm[host].state(),
|
|
60
|
+
'protocols': [],
|
|
61
|
+
'open_ports': [],
|
|
62
|
+
'mac_address': '',
|
|
63
|
+
'vendor': ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Get hostname
|
|
67
|
+
if 'hostnames' in self.nm[host] and self.nm[host]['hostnames']:
|
|
68
|
+
host_info['hostname'] = self.nm[host]['hostnames'][0]['name']
|
|
69
|
+
|
|
70
|
+
# Get MAC address and vendor info
|
|
71
|
+
if 'addresses' in self.nm[host]:
|
|
72
|
+
if 'mac' in self.nm[host]['addresses']:
|
|
73
|
+
host_info['mac_address'] = self.nm[host]['addresses']['mac']
|
|
74
|
+
if 'vendor' in self.nm[host]:
|
|
75
|
+
host_info['vendor'] = list(self.nm[host]['vendor'].values())[0] if self.nm[host]['vendor'] else ''
|
|
76
|
+
|
|
77
|
+
# Get port information if available
|
|
78
|
+
for protocol in self.nm[host].all_protocols():
|
|
79
|
+
host_info['protocols'].append(protocol)
|
|
80
|
+
ports = self.nm[host][protocol].keys()
|
|
81
|
+
for port in ports:
|
|
82
|
+
port_state = self.nm[host][protocol][port]['state']
|
|
83
|
+
if port_state == 'open':
|
|
84
|
+
host_info['open_ports'].append({
|
|
85
|
+
'port': port,
|
|
86
|
+
'protocol': protocol,
|
|
87
|
+
'service': self.nm[host][protocol][port].get('name', 'unknown')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
scan_results['hosts'].append(host_info)
|
|
91
|
+
|
|
92
|
+
scan_results['total_hosts'] = len(scan_results['hosts'])
|
|
93
|
+
self.logger.info(f"Scan completed. Found {scan_results['total_hosts']} hosts")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.logger.error(f"Scan failed: {str(e)}")
|
|
97
|
+
raise Exception(f"Network scan failed: {str(e)}")
|
|
98
|
+
|
|
99
|
+
return scan_results
|
|
100
|
+
|
|
101
|
+
def get_network_info(self, subnet: str) -> Dict[str, Any]:
|
|
102
|
+
"""Get basic network information."""
|
|
103
|
+
try:
|
|
104
|
+
network = ipaddress.ip_network(subnet, strict=False)
|
|
105
|
+
return {
|
|
106
|
+
'network_address': str(network.network_address),
|
|
107
|
+
'broadcast_address': str(network.broadcast_address),
|
|
108
|
+
'netmask': str(network.netmask),
|
|
109
|
+
'total_addresses': network.num_addresses,
|
|
110
|
+
'usable_addresses': network.num_addresses - 2 if network.num_addresses > 2 else 0,
|
|
111
|
+
'prefix_length': network.prefixlen
|
|
112
|
+
}
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
raise ValueError(f"Invalid network: {str(e)}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Graph module for topology building
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
|
|
5
|
+
from config import SERVER_PORTS, PRINTER_PORTS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TopologyBuilder:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.nodes: Dict[str, Dict[str, Any]] = {}
|
|
11
|
+
self.edges: List[Dict[str, Any]] = []
|
|
12
|
+
self.logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
def build_topology(self, scan_results: Dict[str, Any]) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Build network topology from scan results.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
scan_results: Results from network scanner
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dict with nodes and edges representing the topology
|
|
23
|
+
"""
|
|
24
|
+
self.nodes.clear()
|
|
25
|
+
self.edges.clear()
|
|
26
|
+
hosts = scan_results.get('hosts', [])
|
|
27
|
+
subnet = scan_results.get('subnet', '')
|
|
28
|
+
|
|
29
|
+
if not hosts:
|
|
30
|
+
return self.get_graph_data()
|
|
31
|
+
|
|
32
|
+
for host in hosts:
|
|
33
|
+
ip = host['ip']
|
|
34
|
+
self.nodes[ip] = {
|
|
35
|
+
'ip': ip,
|
|
36
|
+
'hostname': host.get('hostname', ''),
|
|
37
|
+
'state': host.get('state', 'unknown'),
|
|
38
|
+
'mac_address': host.get('mac_address', ''),
|
|
39
|
+
'vendor': host.get('vendor', ''),
|
|
40
|
+
'open_ports': host.get('open_ports', []),
|
|
41
|
+
'protocols': host.get('protocols', []),
|
|
42
|
+
'node_type': self._classify_host(host),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self._infer_connections(hosts, subnet)
|
|
46
|
+
|
|
47
|
+
return self.get_graph_data()
|
|
48
|
+
|
|
49
|
+
def _classify_host(self, host: Dict[str, Any]) -> str:
|
|
50
|
+
"""Classify host type based on IP address and open ports."""
|
|
51
|
+
ip = host['ip']
|
|
52
|
+
if ip.endswith('.1') or ip.endswith('.254'):
|
|
53
|
+
return 'gateway'
|
|
54
|
+
|
|
55
|
+
host_ports = {port['port'] for port in host.get('open_ports', [])}
|
|
56
|
+
|
|
57
|
+
if host_ports & SERVER_PORTS:
|
|
58
|
+
return 'server'
|
|
59
|
+
|
|
60
|
+
if host_ports & PRINTER_PORTS:
|
|
61
|
+
return 'printer'
|
|
62
|
+
|
|
63
|
+
return 'workstation'
|
|
64
|
+
|
|
65
|
+
def _infer_connections(self, hosts: List[Dict[str, Any]], subnet: str):
|
|
66
|
+
"""
|
|
67
|
+
Build a star topology: every host connects to the gateway.
|
|
68
|
+
|
|
69
|
+
This is the only topology that can be honestly inferred from a
|
|
70
|
+
scan without SNMP/CDP/LLDP neighbor data.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
network = ipaddress.ip_network(subnet, strict=False)
|
|
74
|
+
gateway_ip = str(network.network_address + 1)
|
|
75
|
+
|
|
76
|
+
actual_gateway = None
|
|
77
|
+
for ip, attrs in self.nodes.items():
|
|
78
|
+
if attrs.get('node_type') == 'gateway':
|
|
79
|
+
actual_gateway = ip
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if actual_gateway:
|
|
83
|
+
gateway_ip = actual_gateway
|
|
84
|
+
|
|
85
|
+
if gateway_ip not in self.nodes:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
host_ips = [h['ip'] for h in hosts]
|
|
89
|
+
for ip in host_ips:
|
|
90
|
+
if ip != gateway_ip:
|
|
91
|
+
self.edges.append({
|
|
92
|
+
'source': ip,
|
|
93
|
+
'target': gateway_ip,
|
|
94
|
+
'weight': 1.0,
|
|
95
|
+
'type': 'gateway',
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
except (ValueError, ipaddress.AddressValueError) as exc:
|
|
99
|
+
self.logger.error("Error inferring connections: %s", exc)
|
|
100
|
+
|
|
101
|
+
def get_graph_data(self) -> Dict[str, Any]:
|
|
102
|
+
"""Get graph data in a format suitable for D3.js visualization."""
|
|
103
|
+
nodes = []
|
|
104
|
+
for ip, attrs in self.nodes.items():
|
|
105
|
+
nodes.append({
|
|
106
|
+
'id': ip,
|
|
107
|
+
'label': attrs.get('hostname') or ip,
|
|
108
|
+
'ip': ip,
|
|
109
|
+
'type': attrs.get('node_type', 'workstation'),
|
|
110
|
+
'hostname': attrs.get('hostname', ''),
|
|
111
|
+
'state': attrs.get('state', 'unknown'),
|
|
112
|
+
'mac_address': attrs.get('mac_address', ''),
|
|
113
|
+
'vendor': attrs.get('vendor', ''),
|
|
114
|
+
'open_ports': len(attrs.get('open_ports', [])),
|
|
115
|
+
'protocols': attrs.get('protocols', []),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
type_counts: Dict[str, int] = {}
|
|
119
|
+
for attrs in self.nodes.values():
|
|
120
|
+
nt = attrs.get('node_type', 'unknown')
|
|
121
|
+
type_counts[nt] = type_counts.get(nt, 0) + 1
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
'nodes': nodes,
|
|
125
|
+
'links': list(self.edges),
|
|
126
|
+
'stats': {
|
|
127
|
+
'total_nodes': len(nodes),
|
|
128
|
+
'total_links': len(self.edges),
|
|
129
|
+
'node_types': type_counts,
|
|
130
|
+
},
|
|
131
|
+
}
|