lanscape 1.3.5a1__py3-none-any.whl → 1.3.6a1__py3-none-any.whl
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.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- lanscape/__init__.py +9 -1
- lanscape/libraries/app_scope.py +0 -1
- lanscape/libraries/decorators.py +26 -9
- lanscape/libraries/device_alive.py +227 -0
- lanscape/libraries/errors.py +10 -0
- lanscape/libraries/ip_parser.py +73 -1
- lanscape/libraries/logger.py +29 -1
- lanscape/libraries/mac_lookup.py +5 -0
- lanscape/libraries/net_tools.py +156 -188
- lanscape/libraries/port_manager.py +83 -0
- lanscape/libraries/scan_config.py +173 -19
- lanscape/libraries/service_scan.py +3 -3
- lanscape/libraries/subnet_scan.py +111 -26
- lanscape/libraries/version_manager.py +50 -7
- lanscape/libraries/web_browser.py +75 -58
- lanscape/resources/mac_addresses/convert_csv.py +13 -2
- lanscape/resources/ports/convert_csv.py +13 -3
- lanscape/ui/app.py +24 -6
- lanscape/ui/blueprints/__init__.py +4 -1
- lanscape/ui/blueprints/api/__init__.py +2 -0
- lanscape/ui/blueprints/api/port.py +46 -0
- lanscape/ui/blueprints/api/scan.py +57 -5
- lanscape/ui/blueprints/api/tools.py +1 -0
- lanscape/ui/blueprints/web/__init__.py +4 -0
- lanscape/ui/blueprints/web/routes.py +52 -2
- lanscape/ui/main.py +1 -10
- lanscape/ui/shutdown_handler.py +5 -1
- lanscape/ui/static/css/style.css +35 -24
- lanscape/ui/static/js/scan-config.js +76 -2
- lanscape/ui/templates/main.html +0 -7
- lanscape/ui/templates/scan/config.html +71 -10
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/METADATA +1 -1
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/RECORD +36 -35
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/WHEEL +0 -0
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/top_level.txt +0 -0
lanscape/libraries/net_tools.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
"""Network tools for scanning and managing devices on a network."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import ipaddress
|
|
3
5
|
import traceback
|
|
4
6
|
import subprocess
|
|
5
|
-
from time import sleep
|
|
6
7
|
from typing import List, Dict
|
|
7
8
|
import socket
|
|
8
9
|
import struct
|
|
9
10
|
import re
|
|
10
11
|
import psutil
|
|
11
|
-
from icmplib import ping
|
|
12
12
|
|
|
13
13
|
from scapy.sendrecv import srp
|
|
14
14
|
from scapy.layers.l2 import ARP, Ether
|
|
@@ -18,116 +18,13 @@ from lanscape.libraries.service_scan import scan_service
|
|
|
18
18
|
from lanscape.libraries.mac_lookup import MacLookup, get_macs
|
|
19
19
|
from lanscape.libraries.ip_parser import get_address_count, MAX_IPS_ALLOWED
|
|
20
20
|
from lanscape.libraries.errors import DeviceError
|
|
21
|
-
from lanscape.libraries.decorators import job_tracker
|
|
22
|
-
from lanscape.libraries.scan_config import ScanType, PingConfig, ArpConfig
|
|
21
|
+
from lanscape.libraries.decorators import job_tracker
|
|
23
22
|
|
|
24
23
|
log = logging.getLogger('NetTools')
|
|
24
|
+
mac_lookup = MacLookup()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class
|
|
28
|
-
"""Class to check if a device is alive using ARP and/or ping scans."""
|
|
29
|
-
caught_errors: List[DeviceError] = []
|
|
30
|
-
_icmp_alive: bool = False
|
|
31
|
-
_arp_alive: bool = False
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@job_tracker
|
|
35
|
-
def is_alive(
|
|
36
|
-
self,
|
|
37
|
-
ip: str,
|
|
38
|
-
scan_type: ScanType = ScanType.BOTH,
|
|
39
|
-
arp_config: ArpConfig = ArpConfig(),
|
|
40
|
-
ping_config: PingConfig = PingConfig()
|
|
41
|
-
) -> bool:
|
|
42
|
-
"""
|
|
43
|
-
Check if a device is alive by performing ARP and/or ping scans.
|
|
44
|
-
"""
|
|
45
|
-
if scan_type == ScanType.ARP:
|
|
46
|
-
return self._arp_lookup(ip, arp_config)
|
|
47
|
-
if scan_type == ScanType.PING:
|
|
48
|
-
return self._ping_lookup(ip, ping_config)
|
|
49
|
-
return self._ping_lookup(ip, ping_config) or self._arp_lookup(ip, arp_config)
|
|
50
|
-
|
|
51
|
-
@job_tracker
|
|
52
|
-
def _arp_lookup(
|
|
53
|
-
self, ip: str,
|
|
54
|
-
cfg: ArpConfig = ArpConfig()
|
|
55
|
-
) -> bool:
|
|
56
|
-
"""Perform an ARP lookup to check if the device is alive."""
|
|
57
|
-
enforcer_timeout = cfg.timeout * 1.3
|
|
58
|
-
|
|
59
|
-
@timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
|
|
60
|
-
def do_arp_lookup():
|
|
61
|
-
arp_request = ARP(pdst=ip)
|
|
62
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
63
|
-
packet = broadcast / arp_request
|
|
64
|
-
|
|
65
|
-
answered, _ = srp(packet, timeout=cfg.timeout, verbose=False)
|
|
66
|
-
self._arp_alive = any(resp.psrc == ip for _, resp in answered)
|
|
67
|
-
return self._arp_alive
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
for _ in range(cfg.attempts):
|
|
71
|
-
if do_arp_lookup():
|
|
72
|
-
return True
|
|
73
|
-
except Exception as e:
|
|
74
|
-
self.caught_errors.append(DeviceError(e))
|
|
75
|
-
return False
|
|
76
|
-
|
|
77
|
-
@job_tracker
|
|
78
|
-
def _ping_lookup(
|
|
79
|
-
self, ip: str,
|
|
80
|
-
cfg: PingConfig = PingConfig()
|
|
81
|
-
) -> bool:
|
|
82
|
-
"""Perform a ping lookup to check if the device is alive using icmplib."""
|
|
83
|
-
enforcer_timeout = cfg.timeout * cfg.ping_count * 1.3
|
|
84
|
-
|
|
85
|
-
@timeout_enforcer(enforcer_timeout, raise_on_timeout=False)
|
|
86
|
-
def do_icmp_ping():
|
|
87
|
-
try:
|
|
88
|
-
result = ping(
|
|
89
|
-
ip,
|
|
90
|
-
count=cfg.ping_count,
|
|
91
|
-
interval=cfg.retry_delay,
|
|
92
|
-
timeout=cfg.timeout,
|
|
93
|
-
privileged=psutil.WINDOWS # Use privileged mode on Windows
|
|
94
|
-
)
|
|
95
|
-
return result.is_alive
|
|
96
|
-
except Exception as e:
|
|
97
|
-
self.caught_errors.append(DeviceError(e))
|
|
98
|
-
# Fallback to system ping command
|
|
99
|
-
try:
|
|
100
|
-
if psutil.WINDOWS:
|
|
101
|
-
cmd = [
|
|
102
|
-
"ping", "-n", str(cfg.ping_count),
|
|
103
|
-
"-w", str(int(cfg.timeout * 1000)), ip
|
|
104
|
-
]
|
|
105
|
-
else:
|
|
106
|
-
cmd = ["ping", "-c", str(cfg.ping_count), "-W", str(cfg.timeout), ip]
|
|
107
|
-
|
|
108
|
-
result = subprocess.run(
|
|
109
|
-
cmd, stdout=subprocess.PIPE,
|
|
110
|
-
stderr=subprocess.PIPE,
|
|
111
|
-
text=True, check=False
|
|
112
|
-
)
|
|
113
|
-
return result.returncode == 0
|
|
114
|
-
except subprocess.CalledProcessError as fallback_error:
|
|
115
|
-
self.caught_errors.append(DeviceError(fallback_error))
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
for _ in range(cfg.attempts):
|
|
120
|
-
if do_icmp_ping():
|
|
121
|
-
self._icmp_alive = True
|
|
122
|
-
return True
|
|
123
|
-
sleep(cfg.retry_delay)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
self.caught_errors.append(DeviceError(e))
|
|
126
|
-
self._icmp_alive = False
|
|
127
|
-
return False
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class Device(IPAlive):
|
|
27
|
+
class Device:
|
|
131
28
|
"""Represents a network device with metadata and scanning capabilities."""
|
|
132
29
|
|
|
133
30
|
def __init__(self, ip: str):
|
|
@@ -142,13 +39,12 @@ class Device(IPAlive):
|
|
|
142
39
|
self.services: Dict[str, List[int]] = {}
|
|
143
40
|
self.caught_errors: List[DeviceError] = []
|
|
144
41
|
self.log = logging.getLogger('Device')
|
|
145
|
-
self._mac_lookup = MacLookup()
|
|
146
42
|
|
|
147
43
|
def get_metadata(self):
|
|
148
44
|
"""Retrieve metadata such as hostname and MAC addresses."""
|
|
149
45
|
if self.alive:
|
|
150
46
|
self.hostname = self._get_hostname()
|
|
151
|
-
self.
|
|
47
|
+
self._get_mac_addresses()
|
|
152
48
|
|
|
153
49
|
def dict(self) -> dict:
|
|
154
50
|
"""Convert the device object to a dictionary."""
|
|
@@ -189,9 +85,12 @@ class Device(IPAlive):
|
|
|
189
85
|
@job_tracker
|
|
190
86
|
def _get_mac_addresses(self):
|
|
191
87
|
"""Get the possible MAC addresses of a network device given its IP address."""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
88
|
+
# job may already be done depending on
|
|
89
|
+
# the strat from isalive
|
|
90
|
+
if not self.macs:
|
|
91
|
+
self.macs = get_macs(self.ip)
|
|
92
|
+
mac_selector.import_macs(self.macs)
|
|
93
|
+
return self.macs
|
|
195
94
|
|
|
196
95
|
@job_tracker
|
|
197
96
|
def _get_hostname(self):
|
|
@@ -206,7 +105,7 @@ class Device(IPAlive):
|
|
|
206
105
|
@job_tracker
|
|
207
106
|
def _get_manufacturer(self, mac_addr=None):
|
|
208
107
|
"""Get the manufacturer of a network device given its MAC address."""
|
|
209
|
-
return
|
|
108
|
+
return mac_lookup.lookup_vendor(mac_addr) if mac_addr else None
|
|
210
109
|
|
|
211
110
|
|
|
212
111
|
class MacSelector:
|
|
@@ -245,6 +144,7 @@ class MacSelector:
|
|
|
245
144
|
self.macs[mac] = self.macs.get(mac, 0) + 1
|
|
246
145
|
|
|
247
146
|
def clear(self):
|
|
147
|
+
"""Clear the stored MAC addresses."""
|
|
248
148
|
self.macs = {}
|
|
249
149
|
|
|
250
150
|
|
|
@@ -281,15 +181,15 @@ def get_ip_address(interface: str):
|
|
|
281
181
|
# Call the appropriate function based on the platform
|
|
282
182
|
if psutil.WINDOWS:
|
|
283
183
|
return windows()
|
|
284
|
-
|
|
285
|
-
|
|
184
|
+
|
|
185
|
+
# Linux, macOS, and other Unix-like systems
|
|
186
|
+
return unix_like()
|
|
286
187
|
|
|
287
188
|
|
|
288
189
|
def get_netmask(interface: str):
|
|
289
190
|
"""
|
|
290
191
|
Get the netmask of a network interface.
|
|
291
192
|
"""
|
|
292
|
-
|
|
293
193
|
def unix_like(): # Combined Linux and macOS
|
|
294
194
|
try:
|
|
295
195
|
# pylint: disable=import-outside-toplevel, import-error
|
|
@@ -316,8 +216,9 @@ def get_netmask(interface: str):
|
|
|
316
216
|
|
|
317
217
|
if psutil.WINDOWS:
|
|
318
218
|
return windows()
|
|
319
|
-
|
|
320
|
-
|
|
219
|
+
|
|
220
|
+
# Linux, macOS, and other Unix-like systems
|
|
221
|
+
return unix_like()
|
|
321
222
|
|
|
322
223
|
|
|
323
224
|
def get_cidr_from_netmask(netmask: str):
|
|
@@ -329,71 +230,122 @@ def get_cidr_from_netmask(netmask: str):
|
|
|
329
230
|
return str(len(binary_str.rstrip('0')))
|
|
330
231
|
|
|
331
232
|
|
|
233
|
+
def _find_interface_by_default_gateway_windows():
|
|
234
|
+
"""Find the network interface with the default gateway on Windows."""
|
|
235
|
+
try:
|
|
236
|
+
output = subprocess.check_output(
|
|
237
|
+
"route print 0.0.0.0", shell=True, text=True)
|
|
238
|
+
return _parse_windows_route_output(output)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
log.debug(f"Error finding Windows interface by gateway: {e}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_windows_route_output(output):
|
|
245
|
+
"""Parse the output of Windows route command to extract interface index."""
|
|
246
|
+
lines = output.strip().split('\n')
|
|
247
|
+
interface_idx = None
|
|
248
|
+
|
|
249
|
+
# First find the interface index from the routing table
|
|
250
|
+
for line in lines:
|
|
251
|
+
if '0.0.0.0' in line and 'Gateway' not in line: # Skip header
|
|
252
|
+
parts = [p for p in line.split() if p]
|
|
253
|
+
if len(parts) >= 4:
|
|
254
|
+
interface_idx = parts[3]
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# If we found an index, find the corresponding interface name
|
|
258
|
+
if interface_idx:
|
|
259
|
+
for iface_name in psutil.net_if_addrs():
|
|
260
|
+
if str(interface_idx) in iface_name:
|
|
261
|
+
return iface_name
|
|
262
|
+
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _find_interface_by_default_gateway_unix():
|
|
267
|
+
"""Find the network interface with the default gateway on Unix-like systems."""
|
|
268
|
+
try:
|
|
269
|
+
cmd = "ip route show default 2>/dev/null || netstat -rn | grep default"
|
|
270
|
+
output = subprocess.check_output(cmd, shell=True, text=True)
|
|
271
|
+
return _parse_unix_route_output(output)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
log.debug(f"Error finding Unix interface by gateway: {e}")
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_unix_route_output(output):
|
|
278
|
+
"""Parse the output of Unix route commands to extract interface name."""
|
|
279
|
+
for line in output.split('\n'):
|
|
280
|
+
# Parse lines with 'default via ... dev ...'
|
|
281
|
+
if 'default via' in line and 'dev' in line:
|
|
282
|
+
return line.split('dev')[1].split()[0]
|
|
283
|
+
|
|
284
|
+
# Parse simpler 'default ...' lines
|
|
285
|
+
if 'default' in line:
|
|
286
|
+
parts = line.split()
|
|
287
|
+
if len(parts) > 3:
|
|
288
|
+
# Interface is usually the last column
|
|
289
|
+
return parts[-1]
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _get_candidate_interfaces():
|
|
294
|
+
"""Get a list of candidate network interfaces."""
|
|
295
|
+
candidates = []
|
|
296
|
+
for interface, addrs in psutil.net_if_addrs().items():
|
|
297
|
+
stats = psutil.net_if_stats().get(interface)
|
|
298
|
+
if not stats or not stats.isup:
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
ipv4_addrs = [addr for addr in addrs if addr.family == socket.AF_INET]
|
|
302
|
+
if not ipv4_addrs:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# Skip loopback and common virtual interfaces
|
|
306
|
+
is_loopback = any(addr.address.startswith('127.')
|
|
307
|
+
for addr in ipv4_addrs)
|
|
308
|
+
if is_loopback:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
virtual_names = ['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth']
|
|
312
|
+
is_virtual = any(name in interface.lower() for name in virtual_names)
|
|
313
|
+
if is_virtual:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
candidates.append(interface)
|
|
317
|
+
return candidates
|
|
318
|
+
|
|
319
|
+
|
|
332
320
|
def get_primary_interface():
|
|
333
321
|
"""
|
|
334
322
|
Get the primary network interface that is likely handling internet traffic.
|
|
335
323
|
Uses heuristics to identify the most probable interface.
|
|
336
324
|
"""
|
|
337
325
|
# Try to find the interface with the default gateway
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
parts = [p for p in line.split() if p]
|
|
347
|
-
if len(parts) >= 4:
|
|
348
|
-
interface_idx = parts[3]
|
|
349
|
-
# Find interface name in the output
|
|
350
|
-
for iface_name, addrs in psutil.net_if_addrs().items():
|
|
351
|
-
if str(interface_idx) in iface_name:
|
|
352
|
-
return iface_name
|
|
353
|
-
else:
|
|
354
|
-
# Linux/Unix/Mac - use ip route or netstat
|
|
355
|
-
try:
|
|
356
|
-
output = subprocess.check_output(
|
|
357
|
-
"ip route show default 2>/dev/null || netstat -rn | grep default", shell=True, text=True)
|
|
358
|
-
for line in output.split('\n'):
|
|
359
|
-
if 'default via' in line and 'dev' in line:
|
|
360
|
-
return line.split('dev')[1].split()[0]
|
|
361
|
-
elif 'default' in line:
|
|
362
|
-
parts = line.split()
|
|
363
|
-
if len(parts) > 3:
|
|
364
|
-
# Interface is usually the last column
|
|
365
|
-
return parts[-1]
|
|
366
|
-
except (subprocess.SubprocessError, IndexError, FileNotFoundError):
|
|
367
|
-
pass
|
|
368
|
-
except Exception as e:
|
|
369
|
-
log.debug(f"Error determining primary interface: {e}")
|
|
326
|
+
if psutil.WINDOWS:
|
|
327
|
+
interface = _find_interface_by_default_gateway_windows()
|
|
328
|
+
if interface:
|
|
329
|
+
return interface
|
|
330
|
+
else:
|
|
331
|
+
interface = _find_interface_by_default_gateway_unix()
|
|
332
|
+
if interface:
|
|
333
|
+
return interface
|
|
370
334
|
|
|
371
335
|
# Fallback: Identify likely candidates based on heuristics
|
|
372
|
-
candidates =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
stats = psutil.net_if_stats().get(interface)
|
|
376
|
-
if stats and stats.isup:
|
|
377
|
-
ipv4_addrs = [
|
|
378
|
-
addr for addr in addrs if addr.family == socket.AF_INET]
|
|
379
|
-
if ipv4_addrs:
|
|
380
|
-
# Skip loopback and common virtual interfaces
|
|
381
|
-
is_loopback = any(addr.address.startswith('127.')
|
|
382
|
-
for addr in ipv4_addrs)
|
|
383
|
-
is_virtual = any(name in interface.lower() for name in
|
|
384
|
-
['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth'])
|
|
385
|
-
|
|
386
|
-
if not is_loopback and not is_virtual:
|
|
387
|
-
candidates.append(interface)
|
|
336
|
+
candidates = _get_candidate_interfaces()
|
|
337
|
+
if not candidates:
|
|
338
|
+
return None
|
|
388
339
|
|
|
389
340
|
# Prioritize interfaces with names typically used for physical connections
|
|
390
|
-
|
|
341
|
+
physical_prefixes = ['eth', 'en', 'wlan', 'wifi', 'wl', 'wi']
|
|
342
|
+
for prefix in physical_prefixes:
|
|
391
343
|
for interface in candidates:
|
|
392
344
|
if interface.lower().startswith(prefix):
|
|
393
345
|
return interface
|
|
394
346
|
|
|
395
|
-
# Otherwise return the first candidate
|
|
396
|
-
return candidates[0]
|
|
347
|
+
# Otherwise return the first candidate
|
|
348
|
+
return candidates[0]
|
|
397
349
|
|
|
398
350
|
|
|
399
351
|
def get_host_ip_mask(ip_with_cidr: str):
|
|
@@ -456,13 +408,16 @@ def network_from_snicaddr(snicaddr: psutil._common.snicaddr) -> str:
|
|
|
456
408
|
"""
|
|
457
409
|
if not snicaddr.address or not snicaddr.netmask:
|
|
458
410
|
return None
|
|
459
|
-
|
|
411
|
+
|
|
412
|
+
if snicaddr.family == socket.AF_INET:
|
|
460
413
|
addr = f"{snicaddr.address}/{get_cidr_from_netmask(snicaddr.netmask)}"
|
|
461
|
-
|
|
414
|
+
return get_host_ip_mask(addr)
|
|
415
|
+
|
|
416
|
+
if snicaddr.family == socket.AF_INET6:
|
|
462
417
|
addr = f"{snicaddr.address}/{snicaddr.netmask}"
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
418
|
+
return get_host_ip_mask(addr)
|
|
419
|
+
|
|
420
|
+
return f"{snicaddr.address}"
|
|
466
421
|
|
|
467
422
|
|
|
468
423
|
def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|
|
@@ -504,19 +459,32 @@ def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|
|
|
504
459
|
return selected.get("subnet", "")
|
|
505
460
|
|
|
506
461
|
|
|
462
|
+
class ArpSupportChecker:
|
|
463
|
+
"""
|
|
464
|
+
Singleton class to check if ARP requests are supported on the current system.
|
|
465
|
+
The check is only performed once.
|
|
466
|
+
"""
|
|
467
|
+
_supported = None
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def is_supported(cls):
|
|
471
|
+
"""one time check if ARP requests are supported on this system"""
|
|
472
|
+
if cls._supported is not None:
|
|
473
|
+
return cls._supported
|
|
474
|
+
try:
|
|
475
|
+
arp_request = ARP(pdst='0.0.0.0')
|
|
476
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
477
|
+
packet = broadcast / arp_request
|
|
478
|
+
srp(packet, timeout=0, verbose=False)
|
|
479
|
+
cls._supported = True
|
|
480
|
+
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
481
|
+
cls._supported = False
|
|
482
|
+
return cls._supported
|
|
483
|
+
|
|
484
|
+
|
|
507
485
|
def is_arp_supported():
|
|
508
486
|
"""
|
|
509
|
-
Check if ARP requests are supported on the current
|
|
487
|
+
Check if ARP requests are supported on the current system.
|
|
488
|
+
Only runs the check once.
|
|
510
489
|
"""
|
|
511
|
-
|
|
512
|
-
arp_request = ARP(pdst='0.0.0.0')
|
|
513
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
514
|
-
packet = broadcast / arp_request
|
|
515
|
-
|
|
516
|
-
srp(packet, timeout=0, verbose=False)
|
|
517
|
-
return True
|
|
518
|
-
# Scapy_Exception = MacOS
|
|
519
|
-
# PermissionError = Linux
|
|
520
|
-
# RuntimeError = Windows
|
|
521
|
-
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
522
|
-
return False
|
|
490
|
+
return ArpSupportChecker.is_supported()
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Port Manager module for managing port list configurations.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to create, read, update, and delete port lists
|
|
5
|
+
that are stored as JSON files. Each port list contains port numbers and their
|
|
6
|
+
associated services. The module handles validation of port data and provides
|
|
7
|
+
methods for working with port list configurations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
import json
|
|
2
11
|
from typing import List
|
|
3
12
|
from pathlib import Path
|
|
@@ -7,15 +16,46 @@ PORT_DIR = 'ports'
|
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
class PortManager:
|
|
19
|
+
"""
|
|
20
|
+
Manager class for port list operations.
|
|
21
|
+
|
|
22
|
+
Handles the creation, retrieval, updating, and deletion of port lists.
|
|
23
|
+
Port lists are stored as JSON files with port numbers as keys and
|
|
24
|
+
service names as values.
|
|
25
|
+
"""
|
|
26
|
+
|
|
10
27
|
def __init__(self):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the PortManager.
|
|
30
|
+
|
|
31
|
+
Creates the ports directory if it doesn't exist and initializes
|
|
32
|
+
the ResourceManager for file operations.
|
|
33
|
+
"""
|
|
11
34
|
Path(PORT_DIR).mkdir(parents=True, exist_ok=True)
|
|
12
35
|
self.rm = ResourceManager(PORT_DIR)
|
|
13
36
|
|
|
14
37
|
def get_port_lists(self) -> List[str]:
|
|
38
|
+
"""
|
|
39
|
+
Get a list of all available port list names.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List[str]: Names of all available port lists (without .json extension)
|
|
43
|
+
"""
|
|
15
44
|
return [f.replace('.json', '') for f in self.rm.list() if f.endswith('.json')]
|
|
16
45
|
|
|
17
46
|
def get_port_list(self, port_list: str) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Retrieve a port list by name.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
port_list (str): The name of the port list to retrieve
|
|
18
52
|
|
|
53
|
+
Returns:
|
|
54
|
+
dict: A dictionary of port numbers to service names
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If the specified port list does not exist
|
|
58
|
+
"""
|
|
19
59
|
if port_list not in self.get_port_lists():
|
|
20
60
|
msg = f"Port list '{port_list}' does not exist. "
|
|
21
61
|
msg += f"Available port lists: {self.get_port_lists()}"
|
|
@@ -26,6 +66,16 @@ class PortManager:
|
|
|
26
66
|
return data if self.validate_port_data(data) else None
|
|
27
67
|
|
|
28
68
|
def create_port_list(self, port_list: str, data: dict) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Create a new port list.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
port_list (str): Name for the new port list
|
|
74
|
+
data (dict): Dictionary mapping port numbers to service names
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
bool: True if creation was successful, False otherwise
|
|
78
|
+
"""
|
|
29
79
|
if port_list in self.get_port_lists():
|
|
30
80
|
return False
|
|
31
81
|
if not self.validate_port_data(data):
|
|
@@ -36,6 +86,16 @@ class PortManager:
|
|
|
36
86
|
return True
|
|
37
87
|
|
|
38
88
|
def update_port_list(self, port_list: str, data: dict) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Update an existing port list.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
port_list (str): Name of the port list to update
|
|
94
|
+
data (dict): New dictionary mapping port numbers to service names
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
bool: True if update was successful, False otherwise
|
|
98
|
+
"""
|
|
39
99
|
if port_list not in self.get_port_lists():
|
|
40
100
|
return False
|
|
41
101
|
if not self.validate_port_data(data):
|
|
@@ -46,6 +106,15 @@ class PortManager:
|
|
|
46
106
|
return True
|
|
47
107
|
|
|
48
108
|
def delete_port_list(self, port_list: str) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Delete a port list.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
port_list (str): Name of the port list to delete
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
bool: True if deletion was successful, False otherwise
|
|
117
|
+
"""
|
|
49
118
|
if port_list not in self.get_port_lists():
|
|
50
119
|
return False
|
|
51
120
|
|
|
@@ -54,6 +123,20 @@ class PortManager:
|
|
|
54
123
|
return True
|
|
55
124
|
|
|
56
125
|
def validate_port_data(self, port_data: dict) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
Validate port data structure and content.
|
|
128
|
+
|
|
129
|
+
Ensures that:
|
|
130
|
+
- Keys can be converted to integers
|
|
131
|
+
- Values are strings
|
|
132
|
+
- Port numbers are within valid range (0-65535)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
port_data (dict): Dictionary mapping port numbers to service names
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
bool: True if data is valid, False otherwise
|
|
139
|
+
"""
|
|
57
140
|
try:
|
|
58
141
|
for port, service in port_data.items():
|
|
59
142
|
port = int(port) # throws if not int
|