lanscape 1.3.1a8__py3-none-any.whl → 1.3.2a6__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 +4 -1
- lanscape/__main__.py +4 -1
- lanscape/ui/static/css/style.css +1 -1
- lanscape/ui/templates/core/scripts.html +1 -1
- lanscape/ui/templates/info.html +1 -1
- lanscape/ui/templates/main.html +6 -4
- {lanscape-1.3.1a8.dist-info → lanscape-1.3.2a6.dist-info}/METADATA +4 -3
- lanscape-1.3.2a6.dist-info/RECORD +43 -0
- lanscape/libraries/app_scope.py +0 -70
- lanscape/libraries/decorators.py +0 -75
- lanscape/libraries/errors.py +0 -29
- lanscape/libraries/ip_parser.py +0 -65
- lanscape/libraries/logger.py +0 -42
- lanscape/libraries/mac_lookup.py +0 -69
- lanscape/libraries/net_tools.py +0 -480
- lanscape/libraries/port_manager.py +0 -59
- lanscape/libraries/runtime_args.py +0 -44
- lanscape/libraries/service_scan.py +0 -51
- lanscape/libraries/subnet_scan.py +0 -373
- lanscape/libraries/version_manager.py +0 -54
- lanscape/libraries/web_browser.py +0 -141
- lanscape/resources/mac_addresses/convert_csv.py +0 -27
- lanscape/resources/ports/convert_csv.py +0 -27
- lanscape/tests/__init__.py +0 -3
- lanscape/tests/_helpers.py +0 -15
- lanscape/tests/test_api.py +0 -194
- lanscape/tests/test_env.py +0 -30
- lanscape/tests/test_library.py +0 -53
- lanscape/ui/app.py +0 -122
- lanscape/ui/blueprints/__init__.py +0 -7
- lanscape/ui/blueprints/api/__init__.py +0 -5
- lanscape/ui/blueprints/api/port.py +0 -27
- lanscape/ui/blueprints/api/scan.py +0 -69
- lanscape/ui/blueprints/api/tools.py +0 -30
- lanscape/ui/blueprints/web/__init__.py +0 -5
- lanscape/ui/blueprints/web/routes.py +0 -74
- lanscape/ui/main.py +0 -138
- lanscape-1.3.1a8.dist-info/RECORD +0 -72
- {lanscape-1.3.1a8.dist-info → lanscape-1.3.2a6.dist-info}/WHEEL +0 -0
- {lanscape-1.3.1a8.dist-info → lanscape-1.3.2a6.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.1a8.dist-info → lanscape-1.3.2a6.dist-info}/top_level.txt +0 -0
lanscape/libraries/net_tools.py
DELETED
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import psutil
|
|
3
|
-
import socket
|
|
4
|
-
import struct
|
|
5
|
-
import logging
|
|
6
|
-
import platform
|
|
7
|
-
import ipaddress
|
|
8
|
-
import traceback
|
|
9
|
-
import subprocess
|
|
10
|
-
from time import sleep
|
|
11
|
-
from typing import List, Dict
|
|
12
|
-
from scapy.sendrecv import srp
|
|
13
|
-
from scapy.layers.l2 import ARP, Ether
|
|
14
|
-
from scapy.error import Scapy_Exception
|
|
15
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
-
|
|
17
|
-
from .service_scan import scan_service
|
|
18
|
-
from .mac_lookup import lookup_mac, get_macs
|
|
19
|
-
from .ip_parser import get_address_count, MAX_IPS_ALLOWED
|
|
20
|
-
from .errors import DeviceError
|
|
21
|
-
|
|
22
|
-
log = logging.getLogger('NetTools')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class IPAlive:
|
|
26
|
-
caught_errors: List[DeviceError] = []
|
|
27
|
-
|
|
28
|
-
def is_alive(self, ip: str) -> bool:
|
|
29
|
-
"""
|
|
30
|
-
Run ARP and ping in parallel. As soon as one returns True, we shut
|
|
31
|
-
down the executor (without waiting) and return True. Exceptions
|
|
32
|
-
from either lookup are caught and treated as False.
|
|
33
|
-
"""
|
|
34
|
-
executor = ThreadPoolExecutor(max_workers=2)
|
|
35
|
-
futures = [
|
|
36
|
-
executor.submit(self._arp_lookup, ip),
|
|
37
|
-
executor.submit(self._ping_lookup, ip),
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
for future in as_completed(futures):
|
|
41
|
-
try:
|
|
42
|
-
if future.result():
|
|
43
|
-
# one check succeeded — don't block on the other
|
|
44
|
-
# Cancel remaining futures in a version-compatible way
|
|
45
|
-
for f in futures:
|
|
46
|
-
if not f.done():
|
|
47
|
-
f.cancel()
|
|
48
|
-
|
|
49
|
-
executor.shutdown(wait=False) # Python 3.8 compatible
|
|
50
|
-
return True
|
|
51
|
-
except Exception as e:
|
|
52
|
-
# treat any error as a False response
|
|
53
|
-
log.debug(f'Error while checking {ip}: {e}')
|
|
54
|
-
self.caught_errors.append(DeviceError(e))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# neither check found the host alive
|
|
58
|
-
executor.shutdown()
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
def _arp_lookup(self, ip: str, timeout: int = 3) -> bool:
|
|
62
|
-
arp_request = ARP(pdst=ip)
|
|
63
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
64
|
-
packet = broadcast / arp_request
|
|
65
|
-
|
|
66
|
-
answered, _ = srp(packet, timeout=timeout, verbose=False)
|
|
67
|
-
return any(resp.psrc == ip for _, resp in answered)
|
|
68
|
-
|
|
69
|
-
def _ping_lookup(
|
|
70
|
-
self, host: str,
|
|
71
|
-
retries: int = 2,
|
|
72
|
-
retry_delay: int = .25,
|
|
73
|
-
ping_count: int = 2,
|
|
74
|
-
timeout: int = 2
|
|
75
|
-
) -> bool:
|
|
76
|
-
cmd = []
|
|
77
|
-
os_name = platform.system().lower()
|
|
78
|
-
if os_name == "windows":
|
|
79
|
-
# -n count, -w timeout in ms
|
|
80
|
-
cmd = ['ping', '-n', str(ping_count), '-w', str(timeout*1000)]
|
|
81
|
-
else: # Linux, macOS, and other Unix-like systems
|
|
82
|
-
# -c count, -W timeout in s
|
|
83
|
-
cmd = ['ping', '-c', str(ping_count), '-W', str(timeout)]
|
|
84
|
-
|
|
85
|
-
cmd = cmd + [host]
|
|
86
|
-
|
|
87
|
-
for r in range(retries):
|
|
88
|
-
try:
|
|
89
|
-
proc = subprocess.run(
|
|
90
|
-
cmd,
|
|
91
|
-
text=True,
|
|
92
|
-
stdout=subprocess.PIPE,
|
|
93
|
-
stderr=subprocess.PIPE
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if proc.returncode == 0:
|
|
97
|
-
output = proc.stdout.lower()
|
|
98
|
-
|
|
99
|
-
# Windows/Linux both include “TTL” on a successful reply
|
|
100
|
-
if psutil.WINDOWS or psutil.LINUX:
|
|
101
|
-
if 'ttl' in output:
|
|
102
|
-
return True
|
|
103
|
-
# some distributions of Linux and macOS
|
|
104
|
-
if psutil.MACOS or psutil.LINUX:
|
|
105
|
-
bad = '100.0% packet loss'
|
|
106
|
-
good = 'ping statistics'
|
|
107
|
-
# mac doesnt include TTL, so we check good is there, and bad is not
|
|
108
|
-
if good in output and bad not in output:
|
|
109
|
-
return True
|
|
110
|
-
except subprocess.CalledProcessError as e:
|
|
111
|
-
self.caught_errors.append(DeviceError(e))
|
|
112
|
-
pass
|
|
113
|
-
if r < retries - 1:
|
|
114
|
-
sleep(retry_delay)
|
|
115
|
-
return False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class Device(IPAlive):
|
|
120
|
-
def __init__(self,ip:str):
|
|
121
|
-
self.ip: str = ip
|
|
122
|
-
self.alive: bool = None
|
|
123
|
-
self.hostname: str = None
|
|
124
|
-
self.macs: List[str] = []
|
|
125
|
-
self.manufacturer: str = None
|
|
126
|
-
self.ports: List[int] = []
|
|
127
|
-
self.stage: str = 'found'
|
|
128
|
-
self.services: Dict[str,List[int]] = {}
|
|
129
|
-
self.caught_errors: List[DeviceError] = []
|
|
130
|
-
self.log = logging.getLogger('Device')
|
|
131
|
-
|
|
132
|
-
def get_metadata(self):
|
|
133
|
-
if self.alive:
|
|
134
|
-
self.hostname = self._get_hostname()
|
|
135
|
-
self.macs = self._get_mac_addresses()
|
|
136
|
-
|
|
137
|
-
def dict(self) -> dict:
|
|
138
|
-
obj = vars(self).copy()
|
|
139
|
-
obj.pop('log')
|
|
140
|
-
primary_mac = self.get_mac()
|
|
141
|
-
obj['mac_addr'] = primary_mac
|
|
142
|
-
obj['manufacturer'] = self._get_manufacturer(primary_mac)
|
|
143
|
-
|
|
144
|
-
return obj
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def test_port(self,port:int) -> bool:
|
|
148
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
149
|
-
sock.settimeout(1)
|
|
150
|
-
result = sock.connect_ex((self.ip, port))
|
|
151
|
-
sock.close()
|
|
152
|
-
if result == 0:
|
|
153
|
-
self.ports.append(port)
|
|
154
|
-
return True
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
def scan_service(self,port:int):
|
|
158
|
-
service = scan_service(self.ip,port)
|
|
159
|
-
service_ports = self.services.get(service,[])
|
|
160
|
-
service_ports.append(port)
|
|
161
|
-
self.services[service] = service_ports
|
|
162
|
-
|
|
163
|
-
def get_mac(self):
|
|
164
|
-
if not self.macs:
|
|
165
|
-
self.macs = self._get_mac_addresses()
|
|
166
|
-
return mac_selector.choose_mac(self.macs)
|
|
167
|
-
|
|
168
|
-
def _get_mac_addresses(self):
|
|
169
|
-
"""
|
|
170
|
-
Get the MAC address of a network device given its IP address.
|
|
171
|
-
"""
|
|
172
|
-
macs = get_macs(self.ip)
|
|
173
|
-
mac_selector.import_macs(macs)
|
|
174
|
-
return macs
|
|
175
|
-
|
|
176
|
-
def _get_hostname(self):
|
|
177
|
-
"""
|
|
178
|
-
Get the hostname of a network device given its IP address.
|
|
179
|
-
"""
|
|
180
|
-
try:
|
|
181
|
-
hostname = socket.gethostbyaddr(self.ip)[0]
|
|
182
|
-
return hostname
|
|
183
|
-
except socket.herror as e:
|
|
184
|
-
self.caught_errors.append(DeviceError(e))
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
def _get_manufacturer(self,mac_addr=None):
|
|
188
|
-
"""
|
|
189
|
-
Get the manufacturer of a network device given its MAC address.
|
|
190
|
-
"""
|
|
191
|
-
return lookup_mac(mac_addr) if mac_addr else None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
class MacSelector:
|
|
195
|
-
"""
|
|
196
|
-
Essentially filters out bad mac addresses
|
|
197
|
-
you send in a list of macs,
|
|
198
|
-
it will return the one that has been seen the least
|
|
199
|
-
(ideally meaning it is the most likely to be the correct one)
|
|
200
|
-
this was added because some lookups return multiple macs,
|
|
201
|
-
usually the hwid of a vpn tunnel etc
|
|
202
|
-
"""
|
|
203
|
-
def __init__(self):
|
|
204
|
-
self.macs = {}
|
|
205
|
-
|
|
206
|
-
def choose_mac(self,macs:List[str]) -> str:
|
|
207
|
-
if len(macs) == 1:
|
|
208
|
-
return macs[0]
|
|
209
|
-
lowest = 9999
|
|
210
|
-
lowest_i = -1
|
|
211
|
-
|
|
212
|
-
for mac in macs:
|
|
213
|
-
if self.macs[mac] < lowest:
|
|
214
|
-
lowest = self.macs[mac]
|
|
215
|
-
lowest_i = macs.index(mac)
|
|
216
|
-
return macs[lowest_i] if lowest_i != -1 else None
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def import_macs(self,macs:List[str]):
|
|
220
|
-
for mac in macs:
|
|
221
|
-
self.macs[mac] = self.macs.get(mac,0) + 1
|
|
222
|
-
|
|
223
|
-
def clear(self):
|
|
224
|
-
self.macs = {}
|
|
225
|
-
|
|
226
|
-
mac_selector = MacSelector()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def get_ip_address(interface: str):
|
|
230
|
-
"""
|
|
231
|
-
Get the IP address of a network interface on Windows, Linux, or macOS.
|
|
232
|
-
"""
|
|
233
|
-
def unix_like(): # Combined Linux and macOS
|
|
234
|
-
try:
|
|
235
|
-
import fcntl
|
|
236
|
-
import struct
|
|
237
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
238
|
-
ip_address = socket.inet_ntoa(fcntl.ioctl(
|
|
239
|
-
sock.fileno(),
|
|
240
|
-
0x8915, # SIOCGIFADDR
|
|
241
|
-
struct.pack('256s', interface[:15].encode('utf-8'))
|
|
242
|
-
)[20:24])
|
|
243
|
-
return ip_address
|
|
244
|
-
except IOError:
|
|
245
|
-
return None
|
|
246
|
-
|
|
247
|
-
def windows():
|
|
248
|
-
# Get network interfaces and IP addresses using psutil
|
|
249
|
-
net_if_addrs = psutil.net_if_addrs()
|
|
250
|
-
if interface in net_if_addrs:
|
|
251
|
-
for addr in net_if_addrs[interface]:
|
|
252
|
-
if addr.family == socket.AF_INET: # Check for IPv4
|
|
253
|
-
return addr.address
|
|
254
|
-
return None
|
|
255
|
-
|
|
256
|
-
# Call the appropriate function based on the platform
|
|
257
|
-
if psutil.WINDOWS:
|
|
258
|
-
return windows()
|
|
259
|
-
else: # Linux, macOS, and other Unix-like systems
|
|
260
|
-
return unix_like()
|
|
261
|
-
|
|
262
|
-
def get_netmask(interface: str):
|
|
263
|
-
"""
|
|
264
|
-
Get the netmask of a network interface.
|
|
265
|
-
"""
|
|
266
|
-
|
|
267
|
-
def unix_like(): # Combined Linux and macOS
|
|
268
|
-
try:
|
|
269
|
-
import fcntl
|
|
270
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
271
|
-
netmask = socket.inet_ntoa(fcntl.ioctl(
|
|
272
|
-
sock.fileno(),
|
|
273
|
-
0x891b, # SIOCGIFNETMASK
|
|
274
|
-
struct.pack('256s', interface[:15].encode('utf-8'))
|
|
275
|
-
)[20:24])
|
|
276
|
-
return netmask
|
|
277
|
-
except IOError:
|
|
278
|
-
return None
|
|
279
|
-
|
|
280
|
-
def windows():
|
|
281
|
-
output = subprocess.check_output("ipconfig", shell=True).decode()
|
|
282
|
-
# Use a regular expression to match both interface and subnet mask
|
|
283
|
-
interface_section_pattern = rf"{interface}.*?Subnet Mask.*?:\s+(\d+\.\d+\.\d+\.\d+)"
|
|
284
|
-
match = re.search(interface_section_pattern, output, re.S) # Use re.S to allow dot to match newline
|
|
285
|
-
if match:
|
|
286
|
-
return match.group(1)
|
|
287
|
-
return None
|
|
288
|
-
|
|
289
|
-
if psutil.WINDOWS:
|
|
290
|
-
return windows()
|
|
291
|
-
else: # Linux, macOS, and other Unix-like systems
|
|
292
|
-
return unix_like()
|
|
293
|
-
|
|
294
|
-
def get_cidr_from_netmask(netmask: str):
|
|
295
|
-
"""
|
|
296
|
-
Get the CIDR notation of a netmask.
|
|
297
|
-
"""
|
|
298
|
-
binary_str = ''.join([bin(int(x)).lstrip('0b').zfill(8) for x in netmask.split('.')])
|
|
299
|
-
return str(len(binary_str.rstrip('0')))
|
|
300
|
-
|
|
301
|
-
def get_primary_interface():
|
|
302
|
-
"""
|
|
303
|
-
Get the primary network interface that is likely handling internet traffic.
|
|
304
|
-
Uses heuristics to identify the most probable interface.
|
|
305
|
-
"""
|
|
306
|
-
# Try to find the interface with the default gateway
|
|
307
|
-
try:
|
|
308
|
-
if psutil.WINDOWS:
|
|
309
|
-
# On Windows, parse route print output
|
|
310
|
-
output = subprocess.check_output("route print 0.0.0.0", shell=True, text=True)
|
|
311
|
-
lines = output.strip().split('\n')
|
|
312
|
-
for line in lines:
|
|
313
|
-
if '0.0.0.0' in line and 'Gateway' not in line: # Skip header
|
|
314
|
-
parts = [p for p in line.split() if p]
|
|
315
|
-
if len(parts) >= 4:
|
|
316
|
-
interface_idx = parts[3]
|
|
317
|
-
# Find interface name in the output
|
|
318
|
-
for iface_name, addrs in psutil.net_if_addrs().items():
|
|
319
|
-
if str(interface_idx) in iface_name:
|
|
320
|
-
return iface_name
|
|
321
|
-
else:
|
|
322
|
-
# Linux/Unix/Mac - use ip route or netstat
|
|
323
|
-
try:
|
|
324
|
-
output = subprocess.check_output("ip route show default 2>/dev/null || netstat -rn | grep default",
|
|
325
|
-
shell=True, text=True)
|
|
326
|
-
for line in output.split('\n'):
|
|
327
|
-
if 'default via' in line and 'dev' in line:
|
|
328
|
-
return line.split('dev')[1].split()[0]
|
|
329
|
-
elif 'default' in line:
|
|
330
|
-
parts = line.split()
|
|
331
|
-
if len(parts) > 3:
|
|
332
|
-
return parts[-1] # Interface is usually the last column
|
|
333
|
-
except (subprocess.SubprocessError, IndexError, FileNotFoundError):
|
|
334
|
-
pass
|
|
335
|
-
except Exception as e:
|
|
336
|
-
log.debug(f"Error determining primary interface: {e}")
|
|
337
|
-
|
|
338
|
-
# Fallback: Identify likely candidates based on heuristics
|
|
339
|
-
candidates = []
|
|
340
|
-
|
|
341
|
-
for interface, addrs in psutil.net_if_addrs().items():
|
|
342
|
-
stats = psutil.net_if_stats().get(interface)
|
|
343
|
-
if stats and stats.isup:
|
|
344
|
-
ipv4_addrs = [addr for addr in addrs if addr.family == socket.AF_INET]
|
|
345
|
-
if ipv4_addrs:
|
|
346
|
-
# Skip loopback and common virtual interfaces
|
|
347
|
-
is_loopback = any(addr.address.startswith('127.') for addr in ipv4_addrs)
|
|
348
|
-
is_virtual = any(name in interface.lower() for name in
|
|
349
|
-
['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth'])
|
|
350
|
-
|
|
351
|
-
if not is_loopback and not is_virtual:
|
|
352
|
-
candidates.append(interface)
|
|
353
|
-
|
|
354
|
-
# Prioritize interfaces with names typically used for physical connections
|
|
355
|
-
for prefix in ['eth', 'en', 'wlan', 'wifi', 'wl', 'wi']:
|
|
356
|
-
for interface in candidates:
|
|
357
|
-
if interface.lower().startswith(prefix):
|
|
358
|
-
return interface
|
|
359
|
-
|
|
360
|
-
# Otherwise return the first candidate or None
|
|
361
|
-
return candidates[0] if candidates else None
|
|
362
|
-
|
|
363
|
-
def get_host_ip_mask(ip_with_cidr: str):
|
|
364
|
-
"""
|
|
365
|
-
Get the IP address and netmask of a network interface.
|
|
366
|
-
"""
|
|
367
|
-
cidr = ip_with_cidr.split('/')[1]
|
|
368
|
-
network = ipaddress.ip_network(ip_with_cidr, strict=False)
|
|
369
|
-
return f'{network.network_address}/{cidr}'
|
|
370
|
-
|
|
371
|
-
def get_network_subnet(interface = None):
|
|
372
|
-
"""
|
|
373
|
-
Get the network subnet for a given interface.
|
|
374
|
-
Uses network_from_snicaddr for conversion.
|
|
375
|
-
Default is primary interface.
|
|
376
|
-
"""
|
|
377
|
-
interface = interface or get_primary_interface()
|
|
378
|
-
|
|
379
|
-
try:
|
|
380
|
-
addrs = psutil.net_if_addrs()
|
|
381
|
-
if interface in addrs:
|
|
382
|
-
for snicaddr in addrs[interface]:
|
|
383
|
-
if snicaddr.family == socket.AF_INET and snicaddr.address and snicaddr.netmask:
|
|
384
|
-
subnet = network_from_snicaddr(snicaddr)
|
|
385
|
-
if subnet:
|
|
386
|
-
return subnet
|
|
387
|
-
except Exception:
|
|
388
|
-
log.info(f'Unable to parse subnet for interface: {interface}')
|
|
389
|
-
log.debug(traceback.format_exc())
|
|
390
|
-
return None
|
|
391
|
-
|
|
392
|
-
def get_all_network_subnets():
|
|
393
|
-
"""
|
|
394
|
-
Get the primary network interface.
|
|
395
|
-
"""
|
|
396
|
-
addrs = psutil.net_if_addrs()
|
|
397
|
-
gateways = psutil.net_if_stats()
|
|
398
|
-
subnets = []
|
|
399
|
-
|
|
400
|
-
for interface, snicaddrs in addrs.items():
|
|
401
|
-
for snicaddr in snicaddrs:
|
|
402
|
-
if snicaddr.family == socket.AF_INET and gateways[interface].isup:
|
|
403
|
-
|
|
404
|
-
subnet = network_from_snicaddr(snicaddr)
|
|
405
|
-
|
|
406
|
-
if subnet:
|
|
407
|
-
subnets.append({
|
|
408
|
-
'subnet': subnet,
|
|
409
|
-
'address_cnt': get_address_count(subnet)
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
return subnets
|
|
413
|
-
|
|
414
|
-
def network_from_snicaddr(snicaddr: psutil._common.snicaddr) -> str:
|
|
415
|
-
"""
|
|
416
|
-
Convert a psutil snicaddr object to a human-readable string.
|
|
417
|
-
"""
|
|
418
|
-
if not snicaddr.address or not snicaddr.netmask:
|
|
419
|
-
return None
|
|
420
|
-
elif snicaddr.family == socket.AF_INET:
|
|
421
|
-
addr = f"{snicaddr.address}/{get_cidr_from_netmask(snicaddr.netmask)}"
|
|
422
|
-
elif snicaddr.family == socket.AF_INET6:
|
|
423
|
-
addr = f"{snicaddr.address}/{snicaddr.netmask}"
|
|
424
|
-
else:
|
|
425
|
-
return f"{snicaddr.address}"
|
|
426
|
-
return get_host_ip_mask(addr)
|
|
427
|
-
|
|
428
|
-
def smart_select_primary_subnet(subnets: List[dict] | None = None) -> str:
|
|
429
|
-
"""
|
|
430
|
-
Intelligently select the primary subnet that is most likely handling internet traffic.
|
|
431
|
-
|
|
432
|
-
Selection priority:
|
|
433
|
-
1. Subnet associated with the primary interface (with default gateway)
|
|
434
|
-
2. Largest subnet within maximum allowed IP range
|
|
435
|
-
3. First subnet in the list as fallback
|
|
436
|
-
|
|
437
|
-
Returns an empty string if no subnets are available.
|
|
438
|
-
"""
|
|
439
|
-
subnets = subnets or get_all_network_subnets()
|
|
440
|
-
|
|
441
|
-
if not subnets:
|
|
442
|
-
return ""
|
|
443
|
-
|
|
444
|
-
# First priority: Get subnet for the primary interface
|
|
445
|
-
primary_if = get_primary_interface()
|
|
446
|
-
if primary_if:
|
|
447
|
-
primary_subnet = get_network_subnet(primary_if)
|
|
448
|
-
if primary_subnet:
|
|
449
|
-
# Return this subnet if it's within our list
|
|
450
|
-
for subnet in subnets:
|
|
451
|
-
if subnet["subnet"] == primary_subnet:
|
|
452
|
-
return primary_subnet
|
|
453
|
-
|
|
454
|
-
# Second priority: Find a reasonable sized subnet (existing logic)
|
|
455
|
-
selected = {}
|
|
456
|
-
for subnet in subnets:
|
|
457
|
-
if selected.get("address_cnt", 0) < subnet["address_cnt"] < MAX_IPS_ALLOWED:
|
|
458
|
-
selected = subnet
|
|
459
|
-
|
|
460
|
-
# Third priority: Just take the first subnet if nothing else matched
|
|
461
|
-
if not selected and subnets:
|
|
462
|
-
selected = subnets[0]
|
|
463
|
-
|
|
464
|
-
return selected.get("subnet", "")
|
|
465
|
-
|
|
466
|
-
def is_arp_supported():
|
|
467
|
-
"""
|
|
468
|
-
Check if ARP requests are supported on the current platform.
|
|
469
|
-
"""
|
|
470
|
-
try:
|
|
471
|
-
arp_request = ARP(pdst='0.0.0.0')
|
|
472
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
473
|
-
packet = broadcast / arp_request
|
|
474
|
-
|
|
475
|
-
srp(packet, timeout=0, verbose=False)
|
|
476
|
-
return True
|
|
477
|
-
except Scapy_Exception:
|
|
478
|
-
return False
|
|
479
|
-
|
|
480
|
-
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import List
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from .app_scope import ResourceManager
|
|
5
|
-
|
|
6
|
-
PORT_DIR = 'ports'
|
|
7
|
-
|
|
8
|
-
class PortManager:
|
|
9
|
-
def __init__(self):
|
|
10
|
-
Path(PORT_DIR).mkdir(parents=True, exist_ok=True)
|
|
11
|
-
self.rm = ResourceManager(PORT_DIR)
|
|
12
|
-
|
|
13
|
-
def get_port_lists(self) -> List[str]:
|
|
14
|
-
return [f.replace('.json','') for f in self.rm.list() if f.endswith('.json')]
|
|
15
|
-
|
|
16
|
-
def get_port_list(self, port_list: str) -> dict:
|
|
17
|
-
|
|
18
|
-
if port_list not in self.get_port_lists():
|
|
19
|
-
raise ValueError(f"Port list '{port_list}' does not exist. Available port lists: {self.get_port_lists()}")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
data = json.loads(self.rm.get(f'{port_list}.json'))
|
|
23
|
-
|
|
24
|
-
return data if self.validate_port_data(data) else None
|
|
25
|
-
|
|
26
|
-
def create_port_list(self, port_list: str, data: dict) -> bool:
|
|
27
|
-
if port_list in self.get_port_lists(): return False
|
|
28
|
-
if not self.validate_port_data(data): return False
|
|
29
|
-
|
|
30
|
-
self.rm.create(f'{port_list}.json', json.dumps(data, indent=2))
|
|
31
|
-
|
|
32
|
-
return True
|
|
33
|
-
|
|
34
|
-
def update_port_list(self, port_list: str, data: dict) -> bool:
|
|
35
|
-
if port_list not in self.get_port_lists(): return False
|
|
36
|
-
if not self.validate_port_data(data): return False
|
|
37
|
-
|
|
38
|
-
self.rm.update(f'{port_list}.json', json.dumps(data, indent=2))
|
|
39
|
-
|
|
40
|
-
return True
|
|
41
|
-
|
|
42
|
-
def delete_port_list(self, port_list: str) -> bool:
|
|
43
|
-
if port_list not in self.get_port_lists(): return False
|
|
44
|
-
|
|
45
|
-
self.rm.delete(f'{port_list}.json')
|
|
46
|
-
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
def validate_port_data(self, port_data: dict) -> bool:
|
|
50
|
-
try:
|
|
51
|
-
for port, service in port_data.items():
|
|
52
|
-
port = int(port) # throws if not int
|
|
53
|
-
if not isinstance(service, str): return False
|
|
54
|
-
|
|
55
|
-
if not 0 <= port <= 65535: return False
|
|
56
|
-
return True
|
|
57
|
-
except:
|
|
58
|
-
return False
|
|
59
|
-
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
from dataclasses import dataclass, fields
|
|
3
|
-
import argparse
|
|
4
|
-
from typing import Any, Dict
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class RuntimeArgs:
|
|
8
|
-
reloader: bool = False
|
|
9
|
-
port: int = 5001
|
|
10
|
-
logfile: bool = False
|
|
11
|
-
loglevel: str = 'INFO'
|
|
12
|
-
flask_logging: bool = False
|
|
13
|
-
persistent: bool = False
|
|
14
|
-
|
|
15
|
-
def parse_args() -> RuntimeArgs:
|
|
16
|
-
parser = argparse.ArgumentParser(description='LANscape')
|
|
17
|
-
|
|
18
|
-
parser.add_argument('--reloader', action='store_true', help='Use flask\'s reloader (helpful for local development)')
|
|
19
|
-
parser.add_argument('--port', type=int, default=5001, help='Port to run the webserver on')
|
|
20
|
-
parser.add_argument('--logfile', action='store_true', help='Log output to lanscape.log')
|
|
21
|
-
parser.add_argument('--loglevel', default='INFO', help='Set the log level')
|
|
22
|
-
parser.add_argument('--flask-logging', action='store_true', help='Enable flask logging (disables click output)')
|
|
23
|
-
parser.add_argument('--persistent', action='store_true', help='Don\'t exit after browser is closed')
|
|
24
|
-
|
|
25
|
-
# Parse the arguments
|
|
26
|
-
args = parser.parse_args()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Dynamically map argparse Namespace to the Args dataclass
|
|
30
|
-
args_dict: Dict[str, Any] = vars(args) # Convert the Namespace to a dictionary
|
|
31
|
-
field_names = {field.name for field in fields(RuntimeArgs)} # Get dataclass field names
|
|
32
|
-
|
|
33
|
-
# Only pass arguments that exist in the Args dataclass
|
|
34
|
-
filtered_args = {name: args_dict[name] for name in field_names if name in args_dict}
|
|
35
|
-
|
|
36
|
-
# Deal with loglevel formatting
|
|
37
|
-
filtered_args['loglevel'] = filtered_args['loglevel'].upper()
|
|
38
|
-
|
|
39
|
-
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
40
|
-
if filtered_args['loglevel'] not in valid_levels:
|
|
41
|
-
raise ValueError(f"Invalid log level: {filtered_args['loglevel']}. Must be one of: {valid_levels}")
|
|
42
|
-
|
|
43
|
-
# Return the dataclass instance with the dynamically assigned values
|
|
44
|
-
return RuntimeArgs(**filtered_args)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import logging
|
|
3
|
-
import traceback
|
|
4
|
-
from .app_scope import ResourceManager
|
|
5
|
-
|
|
6
|
-
log = logging.getLogger('ServiceScan')
|
|
7
|
-
SERVICES = ResourceManager('services').get_jsonc('definitions.jsonc')
|
|
8
|
-
|
|
9
|
-
# skip printer ports because they cause blank pages to be printed
|
|
10
|
-
PRINTER_PORTS = [9100, 631]
|
|
11
|
-
|
|
12
|
-
def scan_service(ip: str, port: int, timeout=10) -> str:
|
|
13
|
-
"""
|
|
14
|
-
Synchronous function that attempts to identify the service running on a given port.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
async def _async_scan_service(ip: str, port: int, timeout) -> str:
|
|
18
|
-
if port in PRINTER_PORTS:
|
|
19
|
-
return "Printer"
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
# Add a timeout to prevent hanging
|
|
23
|
-
reader, writer = await asyncio.wait_for(asyncio.open_connection(ip, port), timeout=5)
|
|
24
|
-
|
|
25
|
-
# Send a probe appropriate for common services
|
|
26
|
-
probe = "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(ip).encode("utf-8")
|
|
27
|
-
writer.write(probe)
|
|
28
|
-
await writer.drain()
|
|
29
|
-
|
|
30
|
-
# Receive the response with a timeout
|
|
31
|
-
response = await asyncio.wait_for(reader.read(1024), timeout=timeout)
|
|
32
|
-
writer.close()
|
|
33
|
-
await writer.wait_closed()
|
|
34
|
-
|
|
35
|
-
# Analyze the response to identify the service
|
|
36
|
-
response_str = response.decode("utf-8", errors="ignore")
|
|
37
|
-
for service, hints in SERVICES.items():
|
|
38
|
-
if any(hint.lower() in response_str.lower() for hint in hints):
|
|
39
|
-
return service
|
|
40
|
-
except asyncio.TimeoutError:
|
|
41
|
-
log.warning(f"Timeout scanning {ip}:{port}")
|
|
42
|
-
except Exception as e:
|
|
43
|
-
log.error(f"Error scanning {ip}:{port}: {str(e)}")
|
|
44
|
-
log.debug(traceback.format_exc())
|
|
45
|
-
return "Unknown"
|
|
46
|
-
|
|
47
|
-
# Use asyncio.run to execute the asynchronous logic synchronously
|
|
48
|
-
return asyncio.run(_async_scan_service(ip, port,timeout=timeout))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|