lanscape 1.3.0a6__tar.gz → 1.3.1__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.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- {lanscape-1.3.0a6/src/lanscape.egg-info → lanscape-1.3.1}/PKG-INFO +12 -3
- {lanscape-1.3.0a6 → lanscape-1.3.1}/README.md +10 -1
- {lanscape-1.3.0a6 → lanscape-1.3.1}/pyproject.toml +2 -2
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/net_tools.py +179 -56
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/runtime_args.py +2 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/subnet_scan.py +3 -1
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/version_manager.py +1 -1
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/web_browser.py +21 -4
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/tests/test_api.py +0 -1
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/tests/test_env.py +6 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/app.py +17 -4
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/main.py +9 -6
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/css/style.css +35 -14
- lanscape-1.3.1/src/lanscape/ui/static/js/on-tab-close.js +42 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/core/scripts.html +4 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/info.html +9 -7
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/main.html +7 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1/src/lanscape.egg-info}/PKG-INFO +12 -3
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape.egg-info/SOURCES.txt +1 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape.egg-info/requires.txt +1 -1
- {lanscape-1.3.0a6 → lanscape-1.3.1}/LICENSE +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/MANIFEST.in +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/setup.cfg +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/__init__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/__main__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/app_scope.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/decorators.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/errors.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/ip_parser.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/logger.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/mac_lookup.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/port_manager.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/libraries/service_scan.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/tests/__init__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/tests/_helpers.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/tests/test_library.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/api/port.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/api/scan.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/api/tools.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/blueprints/web/routes.py +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/core.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/main.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/base.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/mdennis281/py-lanscape
|
|
@@ -14,7 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: Flask<5.0,>=3.0
|
|
15
15
|
Requires-Dist: psutil<7.0,>=6.0
|
|
16
16
|
Requires-Dist: requests<3.0,>=2.32
|
|
17
|
-
Requires-Dist:
|
|
17
|
+
Requires-Dist: setuptools
|
|
18
18
|
Requires-Dist: scapy<3.0,>=2.3.2
|
|
19
19
|
Requires-Dist: tabulate==0.9.0
|
|
20
20
|
Requires-Dist: pytest
|
|
@@ -32,7 +32,8 @@ python -m lanscape
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Flags
|
|
35
|
-
- `--port <port number>` port of the flask app (default:
|
|
35
|
+
- `--port <port number>` port of the flask app (default: automagic)
|
|
36
|
+
- `--persistent` dont shutdown server when browser tab is closed (default: false)
|
|
36
37
|
- `--reloader` essentially flask debug mode- good for local development (default: false)
|
|
37
38
|
- `--logfile` save log output to lanscape.log
|
|
38
39
|
- `--loglevel <level>` set the logger's log level (default: INFO)
|
|
@@ -55,6 +56,14 @@ can sometimes require admin-level permissions to retrieve accurate results.
|
|
|
55
56
|
### Message "WARNING: No libpcap provider available ! pcap won't be used"
|
|
56
57
|
This is a missing dependency related to the ARP lookup. This is handled in the code, but you would get marginally faster/better results with this installed: [npcap download](https://npcap.com/#download)
|
|
57
58
|
|
|
59
|
+
### The accuracy of the devices found is low
|
|
60
|
+
I use a combination of ARP and Ping to determine if a device is online. This method drops in stability when used in many threads.
|
|
61
|
+
Recommendations:
|
|
62
|
+
|
|
63
|
+
- Drop parallelism value (advanced dropdown)
|
|
64
|
+
- Use python > 3.10 im noticing threadpool improvements after this version
|
|
65
|
+
- Create a bug - I'm curious
|
|
66
|
+
|
|
58
67
|
|
|
59
68
|
### Something else
|
|
60
69
|
Feel free to submit a github issue detailing your experience.
|
|
@@ -10,7 +10,8 @@ python -m lanscape
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Flags
|
|
13
|
-
- `--port <port number>` port of the flask app (default:
|
|
13
|
+
- `--port <port number>` port of the flask app (default: automagic)
|
|
14
|
+
- `--persistent` dont shutdown server when browser tab is closed (default: false)
|
|
14
15
|
- `--reloader` essentially flask debug mode- good for local development (default: false)
|
|
15
16
|
- `--logfile` save log output to lanscape.log
|
|
16
17
|
- `--loglevel <level>` set the logger's log level (default: INFO)
|
|
@@ -33,6 +34,14 @@ can sometimes require admin-level permissions to retrieve accurate results.
|
|
|
33
34
|
### Message "WARNING: No libpcap provider available ! pcap won't be used"
|
|
34
35
|
This is a missing dependency related to the ARP lookup. This is handled in the code, but you would get marginally faster/better results with this installed: [npcap download](https://npcap.com/#download)
|
|
35
36
|
|
|
37
|
+
### The accuracy of the devices found is low
|
|
38
|
+
I use a combination of ARP and Ping to determine if a device is online. This method drops in stability when used in many threads.
|
|
39
|
+
Recommendations:
|
|
40
|
+
|
|
41
|
+
- Drop parallelism value (advanced dropdown)
|
|
42
|
+
- Use python > 3.10 im noticing threadpool improvements after this version
|
|
43
|
+
- Create a bug - I'm curious
|
|
44
|
+
|
|
36
45
|
|
|
37
46
|
### Something else
|
|
38
47
|
Feel free to submit a github issue detailing your experience.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lanscape"
|
|
3
|
-
version = "1.3.
|
|
3
|
+
version = "1.3.1"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Michael Dennis", email="michael@dipduo.com" },
|
|
6
6
|
]
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
"Flask>=3.0,<5.0",
|
|
17
17
|
"psutil>=6.0,<7.0",
|
|
18
18
|
"requests>=2.32,<3.0",
|
|
19
|
-
"
|
|
19
|
+
"setuptools",
|
|
20
20
|
"scapy>=2.3.2,<3.0",
|
|
21
21
|
"tabulate==0.9.0",
|
|
22
22
|
"pytest"
|
|
@@ -9,7 +9,9 @@ import traceback
|
|
|
9
9
|
import subprocess
|
|
10
10
|
from time import sleep
|
|
11
11
|
from typing import List, Dict
|
|
12
|
-
from scapy.
|
|
12
|
+
from scapy.sendrecv import srp
|
|
13
|
+
from scapy.layers.l2 import ARP, Ether
|
|
14
|
+
from scapy.error import Scapy_Exception
|
|
13
15
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
16
|
|
|
15
17
|
from .service_scan import scan_service
|
|
@@ -38,13 +40,19 @@ class IPAlive:
|
|
|
38
40
|
for future in as_completed(futures):
|
|
39
41
|
try:
|
|
40
42
|
if future.result():
|
|
41
|
-
# one check succeeded — don
|
|
42
|
-
|
|
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
|
|
43
50
|
return True
|
|
44
51
|
except Exception as e:
|
|
45
52
|
# treat any error as a False response
|
|
53
|
+
log.debug(f'Error while checking {ip}: {e}')
|
|
46
54
|
self.caught_errors.append(DeviceError(e))
|
|
47
|
-
|
|
55
|
+
|
|
48
56
|
|
|
49
57
|
# neither check found the host alive
|
|
50
58
|
executor.shutdown()
|
|
@@ -60,7 +68,7 @@ class IPAlive:
|
|
|
60
68
|
|
|
61
69
|
def _ping_lookup(
|
|
62
70
|
self, host: str,
|
|
63
|
-
retries: int =
|
|
71
|
+
retries: int = 2,
|
|
64
72
|
retry_delay: int = .25,
|
|
65
73
|
ping_count: int = 2,
|
|
66
74
|
timeout: int = 2
|
|
@@ -70,20 +78,35 @@ class IPAlive:
|
|
|
70
78
|
if os_name == "windows":
|
|
71
79
|
# -n count, -w timeout in ms
|
|
72
80
|
cmd = ['ping', '-n', str(ping_count), '-w', str(timeout*1000)]
|
|
73
|
-
else:
|
|
81
|
+
else: # Linux, macOS, and other Unix-like systems
|
|
74
82
|
# -c count, -W timeout in s
|
|
75
83
|
cmd = ['ping', '-c', str(ping_count), '-W', str(timeout)]
|
|
76
84
|
|
|
85
|
+
cmd = cmd + [host]
|
|
86
|
+
|
|
77
87
|
for r in range(retries):
|
|
78
88
|
try:
|
|
79
|
-
|
|
80
|
-
cmd
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
proc = subprocess.run(
|
|
90
|
+
cmd,
|
|
91
|
+
text=True,
|
|
92
|
+
stdout=subprocess.PIPE,
|
|
93
|
+
stderr=subprocess.PIPE
|
|
83
94
|
)
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
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
|
|
87
110
|
except subprocess.CalledProcessError as e:
|
|
88
111
|
self.caught_errors.append(DeviceError(e))
|
|
89
112
|
pass
|
|
@@ -205,9 +228,9 @@ mac_selector = MacSelector()
|
|
|
205
228
|
|
|
206
229
|
def get_ip_address(interface: str):
|
|
207
230
|
"""
|
|
208
|
-
Get the IP address of a network interface on Windows or
|
|
231
|
+
Get the IP address of a network interface on Windows, Linux, or macOS.
|
|
209
232
|
"""
|
|
210
|
-
def
|
|
233
|
+
def unix_like(): # Combined Linux and macOS
|
|
211
234
|
try:
|
|
212
235
|
import fcntl
|
|
213
236
|
import struct
|
|
@@ -233,17 +256,15 @@ def get_ip_address(interface: str):
|
|
|
233
256
|
# Call the appropriate function based on the platform
|
|
234
257
|
if psutil.WINDOWS:
|
|
235
258
|
return windows()
|
|
236
|
-
|
|
237
|
-
return
|
|
238
|
-
else:
|
|
239
|
-
return None
|
|
259
|
+
else: # Linux, macOS, and other Unix-like systems
|
|
260
|
+
return unix_like()
|
|
240
261
|
|
|
241
262
|
def get_netmask(interface: str):
|
|
242
263
|
"""
|
|
243
264
|
Get the netmask of a network interface.
|
|
244
265
|
"""
|
|
245
266
|
|
|
246
|
-
def
|
|
267
|
+
def unix_like(): # Combined Linux and macOS
|
|
247
268
|
try:
|
|
248
269
|
import fcntl
|
|
249
270
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
@@ -267,7 +288,8 @@ def get_netmask(interface: str):
|
|
|
267
288
|
|
|
268
289
|
if psutil.WINDOWS:
|
|
269
290
|
return windows()
|
|
270
|
-
|
|
291
|
+
else: # Linux, macOS, and other Unix-like systems
|
|
292
|
+
return unix_like()
|
|
271
293
|
|
|
272
294
|
def get_cidr_from_netmask(netmask: str):
|
|
273
295
|
"""
|
|
@@ -278,20 +300,65 @@ def get_cidr_from_netmask(netmask: str):
|
|
|
278
300
|
|
|
279
301
|
def get_primary_interface():
|
|
280
302
|
"""
|
|
281
|
-
Get the primary network interface
|
|
303
|
+
Get the primary network interface that is likely handling internet traffic.
|
|
304
|
+
Uses heuristics to identify the most probable interface.
|
|
282
305
|
"""
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
295
362
|
|
|
296
363
|
def get_host_ip_mask(ip_with_cidr: str):
|
|
297
364
|
"""
|
|
@@ -301,25 +368,26 @@ def get_host_ip_mask(ip_with_cidr: str):
|
|
|
301
368
|
network = ipaddress.ip_network(ip_with_cidr, strict=False)
|
|
302
369
|
return f'{network.network_address}/{cidr}'
|
|
303
370
|
|
|
304
|
-
def get_network_subnet(interface =
|
|
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.
|
|
305
376
|
"""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"""
|
|
377
|
+
interface = interface or get_primary_interface()
|
|
378
|
+
|
|
309
379
|
try:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return get_host_ip_mask(ip_mask)
|
|
319
|
-
except:
|
|
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:
|
|
320
388
|
log.info(f'Unable to parse subnet for interface: {interface}')
|
|
321
389
|
log.debug(traceback.format_exc())
|
|
322
|
-
return
|
|
390
|
+
return None
|
|
323
391
|
|
|
324
392
|
def get_all_network_subnets():
|
|
325
393
|
"""
|
|
@@ -332,7 +400,9 @@ def get_all_network_subnets():
|
|
|
332
400
|
for interface, snicaddrs in addrs.items():
|
|
333
401
|
for snicaddr in snicaddrs:
|
|
334
402
|
if snicaddr.family == socket.AF_INET and gateways[interface].isup:
|
|
335
|
-
|
|
403
|
+
|
|
404
|
+
subnet = network_from_snicaddr(snicaddr)
|
|
405
|
+
|
|
336
406
|
if subnet:
|
|
337
407
|
subnets.append({
|
|
338
408
|
'subnet': subnet,
|
|
@@ -341,17 +411,70 @@ def get_all_network_subnets():
|
|
|
341
411
|
|
|
342
412
|
return subnets
|
|
343
413
|
|
|
344
|
-
def
|
|
414
|
+
def network_from_snicaddr(snicaddr: psutil._common.snicaddr) -> str:
|
|
345
415
|
"""
|
|
346
|
-
|
|
347
|
-
not perfect, but works better than subnets[0]
|
|
416
|
+
Convert a psutil snicaddr object to a human-readable string.
|
|
348
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)
|
|
349
455
|
selected = {}
|
|
350
456
|
for subnet in subnets:
|
|
351
|
-
if selected.get(
|
|
457
|
+
if selected.get("address_cnt", 0) < subnet["address_cnt"] < MAX_IPS_ALLOWED:
|
|
352
458
|
selected = subnet
|
|
353
|
-
|
|
459
|
+
|
|
460
|
+
# Third priority: Just take the first subnet if nothing else matched
|
|
461
|
+
if not selected and subnets:
|
|
354
462
|
selected = subnets[0]
|
|
355
|
-
|
|
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
|
|
356
479
|
|
|
357
480
|
|
|
@@ -10,6 +10,7 @@ class RuntimeArgs:
|
|
|
10
10
|
logfile: bool = False
|
|
11
11
|
loglevel: str = 'INFO'
|
|
12
12
|
flask_logging: bool = False
|
|
13
|
+
persistent: bool = False
|
|
13
14
|
|
|
14
15
|
def parse_args() -> RuntimeArgs:
|
|
15
16
|
parser = argparse.ArgumentParser(description='LANscape')
|
|
@@ -19,6 +20,7 @@ def parse_args() -> RuntimeArgs:
|
|
|
19
20
|
parser.add_argument('--logfile', action='store_true', help='Log output to lanscape.log')
|
|
20
21
|
parser.add_argument('--loglevel', default='INFO', help='Set the log level')
|
|
21
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')
|
|
22
24
|
|
|
23
25
|
# Parse the arguments
|
|
24
26
|
args = parser.parse_args()
|
|
@@ -12,7 +12,7 @@ from tabulate import tabulate
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
14
|
|
|
15
|
-
from .net_tools import Device
|
|
15
|
+
from .net_tools import Device, is_arp_supported
|
|
16
16
|
from .ip_parser import parse_ip_input
|
|
17
17
|
from .port_manager import PortManager
|
|
18
18
|
from.errors import SubnetScanTerminationFailure
|
|
@@ -80,6 +80,8 @@ class SubnetScanner:
|
|
|
80
80
|
self.uid = str(uuid.uuid4())
|
|
81
81
|
self.results = ScannerResults(self)
|
|
82
82
|
self.log: logging.Logger = logging.getLogger('SubnetScanner')
|
|
83
|
+
if not is_arp_supported():
|
|
84
|
+
self.log.warning('ARP is not supported with the active runtime context. Device discovery will be limited to ping responses.')
|
|
83
85
|
self.log.debug(f'Instantiated with uid: {self.uid}')
|
|
84
86
|
self.log.debug(f'Port Count: {len(self.ports)} | Device Count: {len(self.subnet)}')
|
|
85
87
|
|
|
@@ -33,7 +33,7 @@ def lookup_latest_version(package=PACKAGE):
|
|
|
33
33
|
no_cache = f'?cachebust={randint(0,6969)}'
|
|
34
34
|
url = f"https://pypi.org/pypi/{package}/json{no_cache}"
|
|
35
35
|
try:
|
|
36
|
-
response = requests.get(url)
|
|
36
|
+
response = requests.get(url,timeout=5)
|
|
37
37
|
response.raise_for_status() # Raise an exception for HTTP errors
|
|
38
38
|
latest = response.json()['info']['version']
|
|
39
39
|
log.debug(f'Latest pypi version: {latest}')
|
|
@@ -14,7 +14,7 @@ import webbrowser
|
|
|
14
14
|
import logging
|
|
15
15
|
import re
|
|
16
16
|
import time
|
|
17
|
-
import
|
|
17
|
+
from typing import Optional
|
|
18
18
|
from ..ui.app import app
|
|
19
19
|
|
|
20
20
|
log = logging.getLogger('WebBrowser')
|
|
@@ -34,7 +34,8 @@ def open_webapp(url: str) -> bool:
|
|
|
34
34
|
raise RuntimeError('Unable to find browser binary')
|
|
35
35
|
log.debug(f'Opening {url} with {exe}')
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
cmd = f'"{exe}" --app="{url}"'
|
|
38
|
+
subprocess.run(cmd, check=True, shell=True)
|
|
38
39
|
|
|
39
40
|
if time.time() - start < 2:
|
|
40
41
|
log.debug(f'Unable to hook into closure of UI, listening for flask shutdown')
|
|
@@ -56,7 +57,7 @@ def open_webapp(url: str) -> bool:
|
|
|
56
57
|
return False
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def get_default_browser_executable() -> str
|
|
60
|
+
def get_default_browser_executable() -> Optional[str]:
|
|
60
61
|
if sys.platform.startswith("win"):
|
|
61
62
|
try:
|
|
62
63
|
import winreg
|
|
@@ -117,8 +118,24 @@ def get_default_browser_executable() -> str | None:
|
|
|
117
118
|
# strip arguments like “%u”, “--flag”, etc.
|
|
118
119
|
exec_cmd = exec_cmd.split()[0]
|
|
119
120
|
exec_cmd = exec_cmd.split("%")[0]
|
|
120
|
-
|
|
121
|
+
return exec_cmd
|
|
121
122
|
return None
|
|
122
123
|
|
|
124
|
+
elif sys.platform.startswith("darwin"):
|
|
125
|
+
# macOS: try to find Chrome first for app mode support, fallback to default
|
|
126
|
+
try:
|
|
127
|
+
p = subprocess.run(
|
|
128
|
+
["mdfind", "kMDItemCFBundleIdentifier == 'com.google.Chrome'"],
|
|
129
|
+
capture_output=True, text=True, check=True
|
|
130
|
+
)
|
|
131
|
+
chrome_paths = p.stdout.strip().split('\n')
|
|
132
|
+
if chrome_paths and chrome_paths[0]:
|
|
133
|
+
return f"{chrome_paths[0]}/Contents/MacOS/Google Chrome"
|
|
134
|
+
except subprocess.CalledProcessError:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Fallback to system default
|
|
138
|
+
return "/usr/bin/open"
|
|
139
|
+
|
|
123
140
|
else:
|
|
124
141
|
raise NotImplementedError(f"Unsupported platform: {sys.platform!r}")
|
|
@@ -2,6 +2,7 @@ import unittest
|
|
|
2
2
|
|
|
3
3
|
from ..libraries.version_manager import lookup_latest_version
|
|
4
4
|
from ..libraries.app_scope import ResourceManager, is_local_run
|
|
5
|
+
from ..libraries.net_tools import is_arp_supported
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
@@ -20,5 +21,10 @@ class EnvTestCase(unittest.TestCase):
|
|
|
20
21
|
def test_local_version(self):
|
|
21
22
|
self.assertTrue(is_local_run())
|
|
22
23
|
|
|
24
|
+
def test_arp_support(self):
|
|
25
|
+
arp_supported = is_arp_supported()
|
|
26
|
+
self.assertIn(arp_supported, [True, False],
|
|
27
|
+
f"ARP support should be either True or False, not {arp_supported}"
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from flask import Flask, render_template
|
|
1
|
+
from flask import Flask, render_template, request
|
|
2
2
|
from time import sleep
|
|
3
3
|
import multiprocessing
|
|
4
4
|
import traceback
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
from ..libraries.runtime_args import RuntimeArgs, parse_args
|
|
10
10
|
from ..libraries.version_manager import is_update_available, get_installed_version, lookup_latest_version
|
|
11
11
|
from ..libraries.app_scope import is_local_run
|
|
12
|
+
from ..libraries.net_tools import is_arp_supported
|
|
12
13
|
|
|
13
14
|
app = Flask(
|
|
14
15
|
__name__
|
|
@@ -57,16 +58,28 @@ set_global_safe('update_available', is_update_available)
|
|
|
57
58
|
set_global_safe('latest_version',lookup_latest_version)
|
|
58
59
|
set_global_safe('runtime_args', vars(parse_args()))
|
|
59
60
|
set_global_safe('is_local',is_local_run)
|
|
61
|
+
set_global_safe('is_arp_supported', is_arp_supported)
|
|
60
62
|
|
|
61
63
|
## External hook to kill flask server
|
|
62
64
|
################################
|
|
63
65
|
|
|
64
66
|
exiting = False
|
|
65
|
-
@app.route("/shutdown")
|
|
67
|
+
@app.route("/shutdown", methods=['GET', 'POST'])
|
|
66
68
|
def exit_app():
|
|
69
|
+
|
|
70
|
+
req_type = request.args.get('type')
|
|
71
|
+
if req_type == 'browser-close':
|
|
72
|
+
args = parse_args()
|
|
73
|
+
if args.persistent:
|
|
74
|
+
log.info('Dectected browser close, not exiting flask.')
|
|
75
|
+
return "Ignored"
|
|
76
|
+
log.info('Web browser closed, terminating flask. (disable with --peristent)')
|
|
77
|
+
elif req_type == 'core':
|
|
78
|
+
log.info('Core requested exit, terminating flask.')
|
|
79
|
+
else:
|
|
80
|
+
log.info('Received external exit request. Terminating flask.')
|
|
67
81
|
global exiting
|
|
68
82
|
exiting = True
|
|
69
|
-
log.info('Received external exit request. Terminating flask.')
|
|
70
83
|
return "Done"
|
|
71
84
|
|
|
72
85
|
@app.teardown_request
|
|
@@ -89,7 +102,7 @@ def internal_error(e):
|
|
|
89
102
|
## Webserver creation functions
|
|
90
103
|
################################
|
|
91
104
|
|
|
92
|
-
def
|
|
105
|
+
def start_webserver_daemon(args: RuntimeArgs) -> threading.Thread:
|
|
93
106
|
proc = threading.Thread(target=start_webserver, args=(args,))
|
|
94
107
|
proc.daemon = True # Kill thread when main thread exits
|
|
95
108
|
proc.start()
|
|
@@ -7,12 +7,13 @@ import os
|
|
|
7
7
|
from ..libraries.logger import configure_logging
|
|
8
8
|
from ..libraries.runtime_args import parse_args, RuntimeArgs
|
|
9
9
|
from ..libraries.web_browser import open_webapp
|
|
10
|
+
from ..libraries.net_tools import is_arp_supported
|
|
10
11
|
# do this so any logs generated on import are displayed
|
|
11
12
|
args = parse_args()
|
|
12
13
|
configure_logging(args.loglevel, args.logfile, args.flask_logging)
|
|
13
14
|
|
|
14
15
|
from ..libraries.version_manager import get_installed_version, is_update_available
|
|
15
|
-
from .app import
|
|
16
|
+
from .app import start_webserver_daemon, start_webserver
|
|
16
17
|
import socket
|
|
17
18
|
|
|
18
19
|
|
|
@@ -43,8 +44,10 @@ def _main():
|
|
|
43
44
|
log.info('Flask reloaded app.')
|
|
44
45
|
|
|
45
46
|
args.port = get_valid_port(args.port)
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
if not is_arp_supported():
|
|
49
|
+
log.warning('ARP is not supported, device discovery is degraded. For more information, see the help guide: https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md')
|
|
50
|
+
|
|
48
51
|
try:
|
|
49
52
|
start_webserver_ui(args)
|
|
50
53
|
log.info('Exiting...')
|
|
@@ -100,13 +103,13 @@ def start_webserver_ui(args: RuntimeArgs):
|
|
|
100
103
|
).start()
|
|
101
104
|
start_webserver(args)
|
|
102
105
|
else:
|
|
103
|
-
flask_thread =
|
|
106
|
+
flask_thread = start_webserver_daemon(args)
|
|
104
107
|
app_closed = open_browser(uri)
|
|
105
108
|
|
|
106
109
|
# depending on env, open_browser may or
|
|
107
110
|
# may not be coupled with the closure of UI
|
|
108
111
|
# (if in browser tab, it's uncoupled)
|
|
109
|
-
if not app_closed:
|
|
112
|
+
if not app_closed or args.persistent:
|
|
110
113
|
# not doing a direct join so i can still
|
|
111
114
|
# terminate the app with ctrl+c
|
|
112
115
|
while flask_thread.is_alive():
|
|
@@ -126,7 +129,7 @@ def get_valid_port(port: int):
|
|
|
126
129
|
def terminate():
|
|
127
130
|
import requests
|
|
128
131
|
log.info('Attempting flask shutdown')
|
|
129
|
-
requests.get(f'http://127.0.0.1:{args.port}/shutdown')
|
|
132
|
+
requests.get(f'http://127.0.0.1:{args.port}/shutdown?type=core')
|
|
130
133
|
|
|
131
134
|
|
|
132
135
|
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
--danger-accent: #ff5252; /* Bright red for warnings */
|
|
12
12
|
--danger-accent-hover: #e64545;
|
|
13
13
|
|
|
14
|
+
--danger-accent-transparent: rgba(255, 82, 82, 0.3);/* Light red for subtle danger indication */
|
|
15
|
+
|
|
14
16
|
--warning-accent: #cc8801; /* Vibrant amber for warnings */
|
|
15
17
|
--warning-accent-hover: #d08802;
|
|
16
18
|
|
|
@@ -54,8 +56,9 @@ body:has(.submodule) footer {
|
|
|
54
56
|
background-color: var(--primary-bg);
|
|
55
57
|
border-radius: 8px;
|
|
56
58
|
box-shadow: 0 0 10px var(--box-shadow);
|
|
57
|
-
width:
|
|
59
|
+
width: 95%;
|
|
58
60
|
margin-top: 10px;
|
|
61
|
+
overflow: hidden;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
#header {
|
|
@@ -255,18 +258,7 @@ details {
|
|
|
255
258
|
|
|
256
259
|
|
|
257
260
|
|
|
258
|
-
|
|
259
|
-
#power-button {
|
|
260
|
-
left: auto;
|
|
261
|
-
right: 0;
|
|
262
|
-
border-width: 0 0 1px 1px;
|
|
263
|
-
border-radius: 0 0 0 5px;
|
|
264
|
-
}
|
|
265
|
-
.container-fluid {
|
|
266
|
-
width:98%;
|
|
267
|
-
padding: 8px;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
261
|
+
|
|
270
262
|
|
|
271
263
|
/* Card Styles */
|
|
272
264
|
.card {
|
|
@@ -692,7 +684,36 @@ html {
|
|
|
692
684
|
background-color: var(--warning-accent);
|
|
693
685
|
}
|
|
694
686
|
|
|
695
|
-
|
|
687
|
+
#arp-error {
|
|
688
|
+
width: calc(100% + 40px);
|
|
689
|
+
position: relative;
|
|
690
|
+
display: flex;
|
|
691
|
+
justify-content: center;
|
|
692
|
+
background-color: var(--danger-accent-transparent);
|
|
693
|
+
color: var(--text-color);
|
|
694
|
+
transform: translate3d(-20px, -20px, 0);
|
|
695
|
+
font-size: small;
|
|
696
|
+
}
|
|
697
|
+
#arp-error span {
|
|
698
|
+
text-align: center;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
@media screen and (max-width: 681px) {
|
|
702
|
+
#power-button {
|
|
703
|
+
left: auto;
|
|
704
|
+
right: 0;
|
|
705
|
+
border-width: 0 0 1px 1px;
|
|
706
|
+
border-radius: 0 0 0 5px;
|
|
707
|
+
}
|
|
708
|
+
.container-fluid {
|
|
709
|
+
width:98%;
|
|
710
|
+
padding: 8px;
|
|
711
|
+
}
|
|
712
|
+
#arp-error {
|
|
713
|
+
width: calc(100% + 16px);
|
|
714
|
+
transform: translate3d(-8px, -8px, 0);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
696
717
|
|
|
697
718
|
@media screen and (max-width: 885px) {
|
|
698
719
|
#overview-container .col-4 {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// helps flask server know when the browser tab is closed
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function sendOnUnload(event = null) {
|
|
5
|
+
const url = '/shutdown?type=browser-close';
|
|
6
|
+
const data = JSON.stringify({ event });
|
|
7
|
+
console.log('sendOnUnload called:', data);
|
|
8
|
+
// (1) Using navigator.sendBeacon
|
|
9
|
+
if (navigator.sendBeacon) {
|
|
10
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
11
|
+
navigator.sendBeacon(url, blob);
|
|
12
|
+
}
|
|
13
|
+
// (2) Or—you can use fetch with keepalive (supported in modern browsers)
|
|
14
|
+
else {
|
|
15
|
+
fetch(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
body: data,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
keepalive: true
|
|
20
|
+
})
|
|
21
|
+
.catch((err) => {
|
|
22
|
+
// If it fails, there's not much you can do here.
|
|
23
|
+
console.warn('sendOnUnload fetch failed:', err);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let hasBeenCalled = false;
|
|
29
|
+
|
|
30
|
+
// When pagehide is called w/ persist=false we want to send our payload.
|
|
31
|
+
// Wont work on all browsers, but should work on most modern ones.
|
|
32
|
+
window.addEventListener('pagehide', (event) => {
|
|
33
|
+
if (!hasBeenCalled && !event.persisted) {
|
|
34
|
+
// persisted = false means page is being discarded, not cached
|
|
35
|
+
const clonedEvent = {
|
|
36
|
+
type: 'pagehide',
|
|
37
|
+
persisted: event.persisted
|
|
38
|
+
};
|
|
39
|
+
sendOnUnload(clonedEvent);
|
|
40
|
+
hasBeenCalled = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
|
4
4
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
|
|
5
5
|
<script src="{{ url_for('static', filename='js/core.js') }}"></script>
|
|
6
|
+
|
|
7
|
+
{% if section is not defined %}
|
|
8
|
+
<script src="{{ url_for('static', filename='js/on-tab-close.js') }}"></script>
|
|
9
|
+
{% endif %}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<div id="header">
|
|
5
5
|
<!-- Header and Scan Submission Inline -->
|
|
6
6
|
<div class="d-flex justify-content-start align-items-center">
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
7
|
+
<a href="/" class="text-decoration-none" aria-label="Go to homepage">
|
|
8
|
+
<h1 class="title">
|
|
9
|
+
<span>LAN</span>scape
|
|
10
|
+
</h1>
|
|
11
|
+
</a>
|
|
10
12
|
</div>
|
|
11
13
|
</div>
|
|
12
14
|
<div class="scroll-container" id="content">
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
({{app_version}} -> {{latest_version}})
|
|
19
21
|
</p>
|
|
20
22
|
<input type="text" readonly class="form-control mb-3 mt-3" value="pip install --upgrade lanscape --no-cache"/>
|
|
21
|
-
<a
|
|
23
|
+
<a class="text-decoration-none" href="https://pypi.org/project/lanscape/" target="_blank">
|
|
22
24
|
<button class="btn btn-primary m-2">PyPi - Lanscape</button>
|
|
23
25
|
</a>
|
|
24
26
|
</div>
|
|
@@ -33,10 +35,10 @@
|
|
|
33
35
|
This project has been a learning journey, & I hope it helps you
|
|
34
36
|
discover more about your network as well. Enjoy!
|
|
35
37
|
</p>
|
|
36
|
-
<a href="https://github.com/mdennis281/" target="_blank"
|
|
37
|
-
<button class="btn btn-primary m-2">
|
|
38
|
+
<a href="https://github.com/mdennis281/" class="text-decoration-none" target="_blank">
|
|
39
|
+
<button class="btn btn-primary m-2">GitHub</button>
|
|
38
40
|
</a>
|
|
39
|
-
<a href="https://github.com/mdennis281/LANscape" target="_blank">
|
|
41
|
+
<a href="https://github.com/mdennis281/LANscape" class="text-decoration-none" target="_blank">
|
|
40
42
|
<button class="btn btn-secondary m-2">Project Repo</button>
|
|
41
43
|
</a>
|
|
42
44
|
</div>
|
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
</div>
|
|
50
50
|
<div id="content">
|
|
51
51
|
<div class="container-fluid my-4">
|
|
52
|
+
<!-- ARP Error -->
|
|
53
|
+
<div id="arp-error" class="{{ 'div-hide' if is_arp_supported else '' }}">
|
|
54
|
+
<span>
|
|
55
|
+
Unable to use ARP lookup. Device discovery is degraded.
|
|
56
|
+
<a target="_blank" href="https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md">Steps to fix</a>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
52
59
|
<!-- Scan Results -->
|
|
53
60
|
<div id="scan-results" class="div-hide">
|
|
54
61
|
<div class="d-flex justify-content-between">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/mdennis281/py-lanscape
|
|
@@ -14,7 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: Flask<5.0,>=3.0
|
|
15
15
|
Requires-Dist: psutil<7.0,>=6.0
|
|
16
16
|
Requires-Dist: requests<3.0,>=2.32
|
|
17
|
-
Requires-Dist:
|
|
17
|
+
Requires-Dist: setuptools
|
|
18
18
|
Requires-Dist: scapy<3.0,>=2.3.2
|
|
19
19
|
Requires-Dist: tabulate==0.9.0
|
|
20
20
|
Requires-Dist: pytest
|
|
@@ -32,7 +32,8 @@ python -m lanscape
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Flags
|
|
35
|
-
- `--port <port number>` port of the flask app (default:
|
|
35
|
+
- `--port <port number>` port of the flask app (default: automagic)
|
|
36
|
+
- `--persistent` dont shutdown server when browser tab is closed (default: false)
|
|
36
37
|
- `--reloader` essentially flask debug mode- good for local development (default: false)
|
|
37
38
|
- `--logfile` save log output to lanscape.log
|
|
38
39
|
- `--loglevel <level>` set the logger's log level (default: INFO)
|
|
@@ -55,6 +56,14 @@ can sometimes require admin-level permissions to retrieve accurate results.
|
|
|
55
56
|
### Message "WARNING: No libpcap provider available ! pcap won't be used"
|
|
56
57
|
This is a missing dependency related to the ARP lookup. This is handled in the code, but you would get marginally faster/better results with this installed: [npcap download](https://npcap.com/#download)
|
|
57
58
|
|
|
59
|
+
### The accuracy of the devices found is low
|
|
60
|
+
I use a combination of ARP and Ping to determine if a device is online. This method drops in stability when used in many threads.
|
|
61
|
+
Recommendations:
|
|
62
|
+
|
|
63
|
+
- Drop parallelism value (advanced dropdown)
|
|
64
|
+
- Use python > 3.10 im noticing threadpool improvements after this version
|
|
65
|
+
- Create a bug - I'm curious
|
|
66
|
+
|
|
58
67
|
|
|
59
68
|
### Something else
|
|
60
69
|
Feel free to submit a github issue detailing your experience.
|
|
@@ -56,6 +56,7 @@ src/lanscape/ui/static/img/ico/site.webmanifest
|
|
|
56
56
|
src/lanscape/ui/static/js/core.js
|
|
57
57
|
src/lanscape/ui/static/js/layout-sizing.js
|
|
58
58
|
src/lanscape/ui/static/js/main.js
|
|
59
|
+
src/lanscape/ui/static/js/on-tab-close.js
|
|
59
60
|
src/lanscape/ui/static/js/quietReload.js
|
|
60
61
|
src/lanscape/ui/static/js/shutdown-server.js
|
|
61
62
|
src/lanscape/ui/static/js/subnet-info.js
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/android-chrome-192x192.png
RENAMED
|
File without changes
|
{lanscape-1.3.0a6 → lanscape-1.3.1}/src/lanscape/ui/static/img/ico/android-chrome-512x512.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|