lanscape 1.2.10a1__py3-none-any.whl → 1.3.0a1__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.
- lanscape/libraries/errors.py +17 -0
- lanscape/libraries/logger.py +2 -2
- lanscape/libraries/net_tools.py +70 -40
- lanscape/libraries/runtime_args.py +2 -0
- lanscape/libraries/subnet_scan.py +25 -14
- lanscape/libraries/version_manager.py +7 -3
- lanscape/libraries/web_browser.py +115 -0
- lanscape/tests/test_api.py +43 -1
- lanscape/ui/app.py +3 -6
- lanscape/ui/blueprints/api/scan.py +3 -1
- lanscape/ui/blueprints/web/routes.py +33 -26
- lanscape/ui/main.py +66 -30
- lanscape/ui/static/css/style.css +3 -1
- lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- lanscape/ui/static/img/ico/favicon.ico +0 -0
- lanscape/ui/static/js/main.js +38 -4
- lanscape/ui/templates/scan/overview.html +3 -2
- {lanscape-1.2.10a1.dist-info → lanscape-1.3.0a1.dist-info}/METADATA +2 -2
- {lanscape-1.2.10a1.dist-info → lanscape-1.3.0a1.dist-info}/RECORD +26 -25
- {lanscape-1.2.10a1.dist-info → lanscape-1.3.0a1.dist-info}/WHEEL +1 -1
- {lanscape-1.2.10a1.dist-info → lanscape-1.3.0a1.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.2.10a1.dist-info → lanscape-1.3.0a1.dist-info}/top_level.txt +0 -0
lanscape/libraries/errors.py
CHANGED
|
@@ -10,3 +10,20 @@ class SubnetTooLargeError(Exception):
|
|
|
10
10
|
class SubnetScanTerminationFailure(Exception):
|
|
11
11
|
def __init__(self,running_threads):
|
|
12
12
|
super().__init__(f'Unable to terminate active threads: {running_threads}')
|
|
13
|
+
|
|
14
|
+
class DeviceError(Exception):
|
|
15
|
+
def __init__(self, e:Exception):
|
|
16
|
+
self.base: Exception = e
|
|
17
|
+
self.method = self._attempt_extract_method()
|
|
18
|
+
|
|
19
|
+
def _attempt_extract_method(self):
|
|
20
|
+
try:
|
|
21
|
+
tb = self.base.__traceback__
|
|
22
|
+
frame = tb.tb_frame
|
|
23
|
+
return frame.f_code.co_name
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(e)
|
|
26
|
+
return 'unknown'
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return f'Error(source={self.method}, msg={self.base})'
|
lanscape/libraries/logger.py
CHANGED
|
@@ -3,7 +3,7 @@ from logging.handlers import RotatingFileHandler
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def configure_logging(loglevel:str, logfile:bool):
|
|
6
|
+
def configure_logging(loglevel:str, logfile:bool, flask_logging:bool=False) -> None:
|
|
7
7
|
numeric_level = getattr(logging, loglevel.upper(), None)
|
|
8
8
|
if not isinstance(numeric_level, int):
|
|
9
9
|
raise ValueError(f'Invalid log level: {loglevel}')
|
|
@@ -12,7 +12,7 @@ def configure_logging(loglevel:str, logfile:bool):
|
|
|
12
12
|
logging.basicConfig(level=numeric_level, format='[%(name)s] %(levelname)s - %(message)s')
|
|
13
13
|
|
|
14
14
|
# flask spams too much on info
|
|
15
|
-
if
|
|
15
|
+
if not flask_logging:
|
|
16
16
|
disable_flask_logging()
|
|
17
17
|
|
|
18
18
|
if logfile:
|
lanscape/libraries/net_tools.py
CHANGED
|
@@ -10,58 +10,86 @@ import subprocess
|
|
|
10
10
|
from time import sleep
|
|
11
11
|
from typing import List, Dict
|
|
12
12
|
from scapy.all import ARP, Ether, srp
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
13
14
|
|
|
14
15
|
from .service_scan import scan_service
|
|
15
16
|
from .mac_lookup import lookup_mac, get_macs
|
|
16
17
|
from .ip_parser import get_address_count, MAX_IPS_ALLOWED
|
|
18
|
+
from .errors import DeviceError
|
|
17
19
|
|
|
18
20
|
log = logging.getLogger('NetTools')
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class IPAlive:
|
|
22
|
-
|
|
23
|
-
def is_alive(self,ip:str) -> bool:
|
|
24
|
-
try:
|
|
25
|
-
self.alive = self._arp_lookup(ip)
|
|
26
|
-
except:
|
|
27
|
-
self.log.debug('failed ARP, falling back to ping')
|
|
28
|
-
self.alive = self._ping_lookup(ip)
|
|
29
|
-
|
|
30
|
-
return self.alive
|
|
31
|
-
|
|
32
|
-
def _arp_lookup(self,ip,timeout=4):
|
|
33
|
-
arp_request = ARP(pdst=ip)
|
|
34
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
35
|
-
arp_request_broadcast = broadcast / arp_request
|
|
24
|
+
caught_errors: List[DeviceError] = []
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
def is_alive(self, ip: str) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Run ARP and ping in parallel. As soon as one returns True, we shut
|
|
29
|
+
down the executor (without waiting) and return True. Exceptions
|
|
30
|
+
from either lookup are caught and treated as False.
|
|
31
|
+
"""
|
|
32
|
+
executor = ThreadPoolExecutor(max_workers=2)
|
|
33
|
+
futures = [
|
|
34
|
+
executor.submit(self._arp_lookup, ip),
|
|
35
|
+
executor.submit(self._ping_lookup, ip),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for future in as_completed(futures):
|
|
39
|
+
try:
|
|
40
|
+
if future.result():
|
|
41
|
+
# one check succeeded — don’t block on the other
|
|
42
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
43
|
+
return True
|
|
44
|
+
except Exception as e:
|
|
45
|
+
# treat any error as a False response
|
|
46
|
+
self.caught_errors.append(DeviceError(e))
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# neither check found the host alive
|
|
50
|
+
executor.shutdown()
|
|
43
51
|
return False
|
|
44
52
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
def _arp_lookup(self, ip: str, timeout: int = 3) -> bool:
|
|
54
|
+
arp_request = ARP(pdst=ip)
|
|
55
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
56
|
+
packet = broadcast / arp_request
|
|
57
|
+
|
|
58
|
+
answered, _ = srp(packet, timeout=timeout, verbose=False)
|
|
59
|
+
return any(resp.psrc == ip for _, resp in answered)
|
|
60
|
+
|
|
61
|
+
def _ping_lookup(
|
|
62
|
+
self, host: str,
|
|
63
|
+
retries: int = 1,
|
|
64
|
+
retry_delay: int = .25,
|
|
65
|
+
ping_count: int = 2,
|
|
66
|
+
timeout: int = 2
|
|
67
|
+
) -> bool:
|
|
68
|
+
cmd = []
|
|
69
|
+
os_name = platform.system().lower()
|
|
70
|
+
if os_name == "windows":
|
|
71
|
+
# -n count, -w timeout in ms
|
|
72
|
+
cmd = ['ping', '-n', str(ping_count), '-w', str(timeout*1000)]
|
|
73
|
+
else:
|
|
74
|
+
# -c count, -W timeout in s
|
|
75
|
+
cmd = ['ping', '-c', str(ping_count), '-W', str(timeout)]
|
|
76
|
+
|
|
77
|
+
for r in range(retries):
|
|
78
|
+
try:
|
|
79
|
+
output = subprocess.check_output(
|
|
80
|
+
cmd + [host],
|
|
81
|
+
stderr=subprocess.STDOUT,
|
|
82
|
+
universal_newlines=True
|
|
83
|
+
)
|
|
84
|
+
# Windows/Linux both include “TTL” on a successful reply
|
|
85
|
+
if 'TTL' in output.upper():
|
|
86
|
+
return True
|
|
87
|
+
except subprocess.CalledProcessError as e:
|
|
88
|
+
self.caught_errors.append(DeviceError(e))
|
|
89
|
+
pass
|
|
90
|
+
if r < retries - 1:
|
|
63
91
|
sleep(retry_delay)
|
|
64
|
-
|
|
92
|
+
return False
|
|
65
93
|
|
|
66
94
|
|
|
67
95
|
|
|
@@ -75,6 +103,7 @@ class Device(IPAlive):
|
|
|
75
103
|
self.ports: List[int] = []
|
|
76
104
|
self.stage: str = 'found'
|
|
77
105
|
self.services: Dict[str,List[int]] = {}
|
|
106
|
+
self.caught_errors: List[DeviceError] = []
|
|
78
107
|
self.log = logging.getLogger('Device')
|
|
79
108
|
|
|
80
109
|
def get_metadata(self):
|
|
@@ -128,7 +157,8 @@ class Device(IPAlive):
|
|
|
128
157
|
try:
|
|
129
158
|
hostname = socket.gethostbyaddr(self.ip)[0]
|
|
130
159
|
return hostname
|
|
131
|
-
except socket.herror:
|
|
160
|
+
except socket.herror as e:
|
|
161
|
+
self.caught_errors.append(DeviceError(e))
|
|
132
162
|
return None
|
|
133
163
|
|
|
134
164
|
def _get_manufacturer(self,mac_addr=None):
|
|
@@ -9,6 +9,7 @@ class RuntimeArgs:
|
|
|
9
9
|
port: int = 5001
|
|
10
10
|
logfile: bool = False
|
|
11
11
|
loglevel: str = 'INFO'
|
|
12
|
+
flask_logging: bool = False
|
|
12
13
|
|
|
13
14
|
def parse_args() -> RuntimeArgs:
|
|
14
15
|
parser = argparse.ArgumentParser(description='LANscape')
|
|
@@ -17,6 +18,7 @@ def parse_args() -> RuntimeArgs:
|
|
|
17
18
|
parser.add_argument('--port', type=int, default=5001, help='Port to run the webserver on')
|
|
18
19
|
parser.add_argument('--logfile', action='store_true', help='Log output to lanscape.log')
|
|
19
20
|
parser.add_argument('--loglevel', default='INFO', help='Set the log level')
|
|
21
|
+
parser.add_argument('--flask-logging', action='store_true', help='Enable flask logging (disables click output)')
|
|
20
22
|
|
|
21
23
|
# Parse the arguments
|
|
22
24
|
args = parser.parse_args()
|
|
@@ -10,7 +10,7 @@ from time import sleep
|
|
|
10
10
|
from typing import List, Union
|
|
11
11
|
from tabulate import tabulate
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
14
|
|
|
15
15
|
from .net_tools import Device
|
|
16
16
|
from .ip_parser import parse_ip_input
|
|
@@ -57,6 +57,9 @@ class ScanConfig:
|
|
|
57
57
|
|
|
58
58
|
def parse_subnet(self) -> List[ipaddress.IPv4Network]:
|
|
59
59
|
return parse_ip_input(self.subnet)
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return f'ScanCfg(subnet={self.subnet}, ports={self.port_list}, multiplier={self.t_multiplier})'
|
|
60
63
|
|
|
61
64
|
|
|
62
65
|
|
|
@@ -91,7 +94,7 @@ class SubnetScanner:
|
|
|
91
94
|
self.running = True
|
|
92
95
|
with ThreadPoolExecutor(max_workers=self.cfg.t_cnt('isalive')) as executor:
|
|
93
96
|
futures = {executor.submit(self._get_host_details, str(ip)): str(ip) for ip in self.subnet}
|
|
94
|
-
for future in futures:
|
|
97
|
+
for future in as_completed(futures):
|
|
95
98
|
ip = futures[future]
|
|
96
99
|
try:
|
|
97
100
|
future.result()
|
|
@@ -183,12 +186,11 @@ class SubnetScanner:
|
|
|
183
186
|
Get the MAC address and open ports of the given host.
|
|
184
187
|
"""
|
|
185
188
|
device = Device(host)
|
|
186
|
-
|
|
189
|
+
device.alive = self._ping(device)
|
|
187
190
|
self.results.scanned()
|
|
188
|
-
if not
|
|
191
|
+
if not device.alive:
|
|
189
192
|
return None
|
|
190
193
|
self.log.debug(f'[{host}] is alive, getting metadata')
|
|
191
|
-
|
|
192
194
|
device.get_metadata()
|
|
193
195
|
self.results.devices.append(device)
|
|
194
196
|
return True
|
|
@@ -290,7 +292,7 @@ class ScannerResults:
|
|
|
290
292
|
out['devices'] = [device.dict() for device in sortedDevices]
|
|
291
293
|
|
|
292
294
|
if out_type == str:
|
|
293
|
-
return json.dumps(out, indent=2)
|
|
295
|
+
return json.dumps(out,default=str, indent=2)
|
|
294
296
|
# otherwise return dict
|
|
295
297
|
return out
|
|
296
298
|
|
|
@@ -317,44 +319,53 @@ class ScannerResults:
|
|
|
317
319
|
class ScanManager:
|
|
318
320
|
"""
|
|
319
321
|
Maintain active and completed scans in memory for
|
|
320
|
-
future reference
|
|
322
|
+
future reference. Singleton implementation.
|
|
321
323
|
"""
|
|
324
|
+
_instance = None
|
|
325
|
+
|
|
326
|
+
def __new__(cls, *args, **kwargs):
|
|
327
|
+
if not cls._instance:
|
|
328
|
+
cls._instance = super(ScanManager, cls).__new__(cls, *args, **kwargs)
|
|
329
|
+
return cls._instance
|
|
330
|
+
|
|
322
331
|
def __init__(self):
|
|
323
|
-
self
|
|
332
|
+
if not hasattr(self, 'scans'): # Prevent reinitialization
|
|
333
|
+
self.scans: List[SubnetScanner] = []
|
|
334
|
+
self.log = logging.getLogger('ScanManager')
|
|
324
335
|
|
|
325
336
|
def new_scan(self, config: ScanConfig) -> SubnetScanner:
|
|
326
337
|
scan = SubnetScanner(config)
|
|
327
338
|
self._start(scan)
|
|
339
|
+
self.log.info(f'Scan started - {config}')
|
|
328
340
|
self.scans.append(scan)
|
|
329
341
|
return scan
|
|
330
342
|
|
|
331
|
-
def get_scan(self,scan_id:str) -> SubnetScanner:
|
|
343
|
+
def get_scan(self, scan_id: str) -> SubnetScanner:
|
|
332
344
|
"""
|
|
333
345
|
Get scan by scan.uid
|
|
334
346
|
"""
|
|
335
347
|
for scan in self.scans:
|
|
336
348
|
if scan.uid == scan_id:
|
|
337
349
|
return scan
|
|
338
|
-
|
|
350
|
+
|
|
339
351
|
def terminate_scans(self):
|
|
340
352
|
"""
|
|
341
|
-
|
|
353
|
+
Terminate all active scans
|
|
342
354
|
"""
|
|
343
355
|
for scan in self.scans:
|
|
344
356
|
if scan.running:
|
|
345
357
|
scan.terminate()
|
|
346
358
|
|
|
347
|
-
def wait_until_complete(self,scan_id:str) -> SubnetScanner:
|
|
359
|
+
def wait_until_complete(self, scan_id: str) -> SubnetScanner:
|
|
348
360
|
scan = self.get_scan(scan_id)
|
|
349
361
|
while scan.running:
|
|
350
362
|
sleep(.5)
|
|
351
363
|
return scan
|
|
352
364
|
|
|
353
|
-
def _start(self,scan:SubnetScanner):
|
|
365
|
+
def _start(self, scan: SubnetScanner):
|
|
354
366
|
t = threading.Thread(target=scan.start)
|
|
355
367
|
t.start()
|
|
356
368
|
return t
|
|
357
|
-
|
|
358
369
|
|
|
359
370
|
|
|
360
371
|
|
|
@@ -16,10 +16,14 @@ latest = None # used to 'remember' pypi version each runtime
|
|
|
16
16
|
def is_update_available(package=PACKAGE) -> bool:
|
|
17
17
|
installed = get_installed_version(package)
|
|
18
18
|
available = lookup_latest_version(package)
|
|
19
|
-
if installed == LOCAL_VERSION: return False # local
|
|
20
|
-
if 'a' in installed: return False # alpha
|
|
21
|
-
if 'b' in installed: return False # beta
|
|
22
19
|
|
|
20
|
+
is_update_exempt = (
|
|
21
|
+
'a' in installed, 'b' in installed, # pre-release
|
|
22
|
+
installed == LOCAL_VERSION
|
|
23
|
+
)
|
|
24
|
+
if any(is_update_exempt): return False
|
|
25
|
+
|
|
26
|
+
log.debug(f'Installed: {installed} | Available: {available}')
|
|
23
27
|
return installed != available
|
|
24
28
|
|
|
25
29
|
def lookup_latest_version(package=PACKAGE):
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Get the executable path of the system’s default web browser.
|
|
4
|
+
|
|
5
|
+
Supports:
|
|
6
|
+
- Windows (reads from the registry)
|
|
7
|
+
- Linux (uses xdg-mime / xdg-settings + .desktop file parsing)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import webbrowser
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
from ..ui.app import app
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger('WebBrowser')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def open_webapp(url: str) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
will try to open the web page as an app
|
|
26
|
+
on failure, will open as a tab in default browser
|
|
27
|
+
|
|
28
|
+
returns:
|
|
29
|
+
"""
|
|
30
|
+
start = time.time()
|
|
31
|
+
try:
|
|
32
|
+
exe = get_default_browser_executable()
|
|
33
|
+
if not exe:
|
|
34
|
+
raise RuntimeError('Unable to find browser binary')
|
|
35
|
+
|
|
36
|
+
#this is blocking until closed
|
|
37
|
+
subprocess.run(f'{exe} --app="{url}"')
|
|
38
|
+
if time.time() - start < 2:
|
|
39
|
+
log.debug(f'Unable to hook into closure of UI, listening for flask shutdown')
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
log.warning('Failed to open webpage as app, falling back to browser tab')
|
|
45
|
+
log.debug(e)
|
|
46
|
+
webbrowser.open(url, new=2)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_default_browser_executable() -> str | None:
|
|
51
|
+
if sys.platform.startswith("win"):
|
|
52
|
+
try:
|
|
53
|
+
import winreg
|
|
54
|
+
# On Windows the HKEY_CLASSES_ROOT\http\shell\open\command key
|
|
55
|
+
# holds the command for opening HTTP URLs.
|
|
56
|
+
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"http\shell\open\command") as key:
|
|
57
|
+
cmd, _ = winreg.QueryValueEx(key, None)
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# cmd usually looks like: '"C:\\Program Files\\Foo\\foo.exe" %1'
|
|
62
|
+
m = re.match(r'\"?(.+?\.exe)\"?', cmd)
|
|
63
|
+
return m.group(1) if m else None
|
|
64
|
+
|
|
65
|
+
elif sys.platform.startswith("linux"):
|
|
66
|
+
# First, find the .desktop file name
|
|
67
|
+
desktop_file = None
|
|
68
|
+
try:
|
|
69
|
+
# Try xdg-mime
|
|
70
|
+
p = subprocess.run(
|
|
71
|
+
["xdg-mime", "query", "default", "x-scheme-handler/http"],
|
|
72
|
+
capture_output=True, text=True,
|
|
73
|
+
check=True
|
|
74
|
+
)
|
|
75
|
+
desktop_file = p.stdout.strip()
|
|
76
|
+
except subprocess.CalledProcessError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
if not desktop_file:
|
|
80
|
+
# Fallback to xdg-settings
|
|
81
|
+
try:
|
|
82
|
+
p = subprocess.run(
|
|
83
|
+
["xdg-settings", "get", "default-web-browser"],
|
|
84
|
+
capture_output=True, text=True,
|
|
85
|
+
check=True
|
|
86
|
+
)
|
|
87
|
+
desktop_file = p.stdout.strip()
|
|
88
|
+
except subprocess.CalledProcessError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Final fallback: BROWSER environment variable
|
|
92
|
+
if not desktop_file:
|
|
93
|
+
return os.environ.get("BROWSER")
|
|
94
|
+
|
|
95
|
+
# Look for that .desktop file in standard locations
|
|
96
|
+
search_paths = [
|
|
97
|
+
os.path.expanduser("~/.local/share/applications"),
|
|
98
|
+
"/usr/local/share/applications",
|
|
99
|
+
"/usr/share/applications",
|
|
100
|
+
]
|
|
101
|
+
for path in search_paths:
|
|
102
|
+
full_path = os.path.join(path, desktop_file)
|
|
103
|
+
if os.path.isfile(full_path):
|
|
104
|
+
with open(full_path, encoding="utf-8", errors="ignore") as f:
|
|
105
|
+
for line in f:
|
|
106
|
+
if line.startswith("Exec="):
|
|
107
|
+
exec_cmd = line[len("Exec="):].strip()
|
|
108
|
+
# strip arguments like “%u”, “--flag”, etc.
|
|
109
|
+
exec_cmd = exec_cmd.split()[0]
|
|
110
|
+
exec_cmd = exec_cmd.split("%")[0]
|
|
111
|
+
return exec_cmd
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
raise NotImplementedError(f"Unsupported platform: {sys.platform!r}")
|
lanscape/tests/test_api.py
CHANGED
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
from ..ui.app import app
|
|
4
4
|
from ..libraries.net_tools import get_network_subnet
|
|
5
5
|
from ._helpers import right_size_subnet
|
|
6
|
+
import time
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
@@ -89,6 +90,7 @@ class ApiTestCase(unittest.TestCase):
|
|
|
89
90
|
]
|
|
90
91
|
for uri in uris:
|
|
91
92
|
response = self.app.get(uri)
|
|
93
|
+
print(uri, response.status_code)
|
|
92
94
|
self.assertEqual(response.status_code,200)
|
|
93
95
|
|
|
94
96
|
|
|
@@ -143,7 +145,47 @@ class ApiTestCase(unittest.TestCase):
|
|
|
143
145
|
self.assertIsNotNone(data.get('msg'))
|
|
144
146
|
if count == -1:
|
|
145
147
|
self.assertFalse(data.get('valid'))
|
|
146
|
-
|
|
148
|
+
|
|
149
|
+
def test_scan_api(self):
|
|
150
|
+
"""
|
|
151
|
+
Test the scan API endpoints
|
|
152
|
+
"""
|
|
153
|
+
# Create a new scan
|
|
154
|
+
new_scan = {
|
|
155
|
+
'subnet': right_size_subnet(get_network_subnet()),
|
|
156
|
+
'port_list': 'small',
|
|
157
|
+
'parallelism': 1
|
|
158
|
+
}
|
|
159
|
+
response = self.app.post('/api/scan', json=new_scan)
|
|
160
|
+
self.assertEqual(response.status_code, 200)
|
|
161
|
+
scan_info = json.loads(response.data)
|
|
162
|
+
self.assertEqual(scan_info['status'], 'running')
|
|
163
|
+
scan_id = scan_info['scan_id']
|
|
164
|
+
self.assertIsNotNone(scan_id)
|
|
165
|
+
|
|
166
|
+
percent_complete = 0
|
|
167
|
+
while percent_complete < 100:
|
|
168
|
+
# Get scan summary
|
|
169
|
+
response = self.app.get(f'/api/scan/{scan_id}/summary')
|
|
170
|
+
self.assertEqual(response.status_code, 200)
|
|
171
|
+
summary = json.loads(response.data)
|
|
172
|
+
self.assertTrue(summary['running'] or summary['stage'] == 'complete')
|
|
173
|
+
percent_complete = summary['percent_complete']
|
|
174
|
+
self.assertGreaterEqual(percent_complete, 0)
|
|
175
|
+
self.assertLessEqual(percent_complete, 100)
|
|
176
|
+
# Wait for a bit before checking again
|
|
177
|
+
time.sleep(2)
|
|
178
|
+
|
|
179
|
+
self.assertEqual(summary['running'], False)
|
|
180
|
+
self.assertEqual(summary['stage'], 'complete')
|
|
181
|
+
self.assertGreater(summary['runtime'], 0)
|
|
182
|
+
|
|
183
|
+
devices_alive = summary['devices']['alive']
|
|
184
|
+
devices_scanned = summary['devices']['scanned']
|
|
185
|
+
devices_total = summary['devices']['total']
|
|
186
|
+
|
|
187
|
+
self.assertEqual(devices_scanned, devices_total)
|
|
188
|
+
self.assertGreater(devices_alive, 0)
|
|
147
189
|
|
|
148
190
|
|
|
149
191
|
|
lanscape/ui/app.py
CHANGED
|
@@ -13,7 +13,7 @@ from ..libraries.app_scope import is_local_run
|
|
|
13
13
|
app = Flask(
|
|
14
14
|
__name__
|
|
15
15
|
)
|
|
16
|
-
log = logging.getLogger('
|
|
16
|
+
log = logging.getLogger('flask')
|
|
17
17
|
|
|
18
18
|
## Import and register BPs
|
|
19
19
|
################################
|
|
@@ -89,12 +89,12 @@ def internal_error(e):
|
|
|
89
89
|
## Webserver creation functions
|
|
90
90
|
################################
|
|
91
91
|
|
|
92
|
-
def start_webserver_dameon(args: RuntimeArgs) ->
|
|
92
|
+
def start_webserver_dameon(args: RuntimeArgs) -> threading.Thread:
|
|
93
93
|
proc = threading.Thread(target=start_webserver, args=(args,))
|
|
94
94
|
proc.daemon = True # Kill thread when main thread exits
|
|
95
95
|
proc.start()
|
|
96
96
|
log.info('Flask server initializing as dameon')
|
|
97
|
-
|
|
97
|
+
return proc
|
|
98
98
|
|
|
99
99
|
def start_webserver(args: RuntimeArgs) -> int:
|
|
100
100
|
run_args = {
|
|
@@ -103,10 +103,7 @@ def start_webserver(args: RuntimeArgs) -> int:
|
|
|
103
103
|
'debug':args.reloader,
|
|
104
104
|
'use_reloader':args.reloader
|
|
105
105
|
}
|
|
106
|
-
|
|
107
106
|
app.run(**run_args)
|
|
108
107
|
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
start_webserver(True)
|
|
111
108
|
|
|
112
109
|
|
|
@@ -3,6 +3,7 @@ from ....libraries.subnet_scan import ScanConfig
|
|
|
3
3
|
from .. import scan_manager
|
|
4
4
|
|
|
5
5
|
from flask import request, jsonify
|
|
6
|
+
import json
|
|
6
7
|
import traceback
|
|
7
8
|
|
|
8
9
|
# Subnet Scanner API
|
|
@@ -30,7 +31,8 @@ def scan_subnet_async():
|
|
|
30
31
|
@api_bp.route('/api/scan/<scan_id>', methods=['GET'])
|
|
31
32
|
def get_scan(scan_id):
|
|
32
33
|
scan = scan_manager.get_scan(scan_id)
|
|
33
|
-
|
|
34
|
+
# cast to str and back to handle custom JSON serialization
|
|
35
|
+
return jsonify(json.loads(scan.results.export(str)))
|
|
34
36
|
|
|
35
37
|
@api_bp.route('/api/scan/<scan_id>/summary',methods=['GET'])
|
|
36
38
|
def get_scan_summary(scan_id):
|
|
@@ -16,47 +16,54 @@ def index():
|
|
|
16
16
|
subnet = smart_select_primary_subnet(subnets)
|
|
17
17
|
|
|
18
18
|
port_list = 'medium'
|
|
19
|
-
parallelism =
|
|
19
|
+
parallelism = 1
|
|
20
20
|
if scan_id := request.args.get('scan_id'):
|
|
21
|
-
if
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
if scan := scan_manager.get_scan(scan_id):
|
|
22
|
+
subnet = scan.cfg.subnet
|
|
23
|
+
port_list = scan.cfg.port_list
|
|
24
|
+
parallelism = scan.cfg.t_multiplier
|
|
25
|
+
|
|
26
26
|
else:
|
|
27
27
|
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
28
28
|
return redirect('/')
|
|
29
29
|
return render_template(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
'main.html',
|
|
31
|
+
subnet=subnet,
|
|
32
|
+
port_list=port_list,
|
|
33
|
+
parallelism=parallelism,
|
|
34
|
+
alternate_subnets=subnets
|
|
35
|
+
)
|
|
36
|
+
|
|
36
37
|
|
|
37
38
|
@web_bp.route('/scan/<scan_id>', methods=['GET'])
|
|
38
39
|
@web_bp.route('/scan/<scan_id>/<section>', methods=['GET'])
|
|
39
40
|
def render_scan(scan_id, section='all'):
|
|
40
|
-
scanner
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if scanner := scan_manager.get_scan(scan_id):
|
|
42
|
+
data = scanner.results.export()
|
|
43
|
+
filter = request.args.get('filter')
|
|
44
|
+
return render_template('scan.html', data=data, section=section, filter=filter)
|
|
45
|
+
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
46
|
+
return redirect('/')
|
|
44
47
|
|
|
45
48
|
@web_bp.route('/errors/<scan_id>')
|
|
46
49
|
def view_errors(scan_id):
|
|
47
|
-
scanner
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
if scanner := scan_manager.get_scan(scan_id):
|
|
51
|
+
data = scanner.results.export()
|
|
52
|
+
return render_template('scan/scan-error.html',data=data)
|
|
53
|
+
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
54
|
+
return redirect('/')
|
|
50
55
|
|
|
51
56
|
@web_bp.route('/export/<scan_id>')
|
|
52
57
|
def export_scan(scan_id):
|
|
53
|
-
scanner
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if scanner := scan_manager.get_scan(scan_id):
|
|
59
|
+
export_json = scanner.results.export(str)
|
|
60
|
+
return render_template(
|
|
61
|
+
'scan/export.html',
|
|
62
|
+
scan=scanner,
|
|
63
|
+
export_json=export_json
|
|
64
|
+
)
|
|
65
|
+
log.debug(f'Redirecting, scan {scan_id} doesnt exist in memory')
|
|
66
|
+
return redirect('/')
|
|
60
67
|
|
|
61
68
|
@web_bp.route('/shutdown-ui')
|
|
62
69
|
def shutdown_ui():
|
lanscape/ui/main.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
import threading
|
|
3
|
-
import webbrowser
|
|
4
3
|
import time
|
|
5
4
|
import logging
|
|
6
5
|
import traceback
|
|
7
6
|
import os
|
|
8
7
|
from ..libraries.logger import configure_logging
|
|
9
8
|
from ..libraries.runtime_args import parse_args, RuntimeArgs
|
|
9
|
+
from ..libraries.web_browser import open_webapp
|
|
10
10
|
# do this so any logs generated on import are displayed
|
|
11
11
|
args = parse_args()
|
|
12
|
-
configure_logging(args.loglevel, args.logfile)
|
|
12
|
+
configure_logging(args.loglevel, args.logfile, args.flask_logging)
|
|
13
13
|
|
|
14
14
|
from ..libraries.version_manager import get_installed_version, is_update_available
|
|
15
|
-
from .app import start_webserver
|
|
15
|
+
from .app import start_webserver_dameon, start_webserver
|
|
16
16
|
import socket
|
|
17
17
|
|
|
18
18
|
|
|
@@ -22,9 +22,19 @@ log = logging.getLogger('core')
|
|
|
22
22
|
IS_FLASK_RELOAD = os.environ.get("WERKZEUG_RUN_MAIN")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def main():
|
|
26
|
+
try:
|
|
27
|
+
_main()
|
|
28
|
+
except KeyboardInterrupt:
|
|
29
|
+
log.info('Keyboard interrupt received, terminating...')
|
|
30
|
+
terminate()
|
|
31
|
+
except Exception as e:
|
|
32
|
+
log.critical(f'Unexpected error: {e}')
|
|
33
|
+
log.debug(traceback.format_exc())
|
|
34
|
+
terminate()
|
|
25
35
|
|
|
26
36
|
|
|
27
|
-
def
|
|
37
|
+
def _main():
|
|
28
38
|
if not IS_FLASK_RELOAD:
|
|
29
39
|
log.info(f'LANscape v{get_installed_version()}')
|
|
30
40
|
try_check_update()
|
|
@@ -36,12 +46,11 @@ def main():
|
|
|
36
46
|
|
|
37
47
|
|
|
38
48
|
try:
|
|
39
|
-
|
|
40
|
-
no_gui(args)
|
|
41
|
-
|
|
49
|
+
start_webserver_ui(args)
|
|
42
50
|
log.info('Exiting...')
|
|
43
|
-
except Exception:
|
|
51
|
+
except Exception as e:
|
|
44
52
|
# showing error in debug only because this is handled gracefully
|
|
53
|
+
log.critical(f'Failed to start app. Error: {e}')
|
|
45
54
|
log.debug('Failed to start. Traceback below')
|
|
46
55
|
log.debug(traceback.format_exc())
|
|
47
56
|
|
|
@@ -57,31 +66,52 @@ def try_check_update():
|
|
|
57
66
|
log.warning('Unable to check for updates.')
|
|
58
67
|
|
|
59
68
|
|
|
60
|
-
def open_browser(url: str,wait=2):
|
|
69
|
+
def open_browser(url: str, wait=2) -> bool:
|
|
61
70
|
"""
|
|
62
71
|
Open a browser window to the specified
|
|
63
72
|
url after waiting for the server to start
|
|
64
73
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
try:
|
|
75
|
+
time.sleep(wait)
|
|
76
|
+
log.info(f'Starting UI - http://127.0.0.1:{args.port}')
|
|
77
|
+
return open_webapp(url)
|
|
78
|
+
|
|
79
|
+
except:
|
|
80
|
+
log.debug(traceback.format_exc())
|
|
81
|
+
log.info(f'Unable to open web browser, server running on {url}')
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def start_webserver_ui(args: RuntimeArgs):
|
|
87
|
+
uri = f'http://127.0.0.1:{args.port}'
|
|
88
|
+
|
|
89
|
+
# running reloader requires flask to run in main thread
|
|
90
|
+
# this decouples UI from main process
|
|
91
|
+
if args.reloader:
|
|
92
|
+
# determine if it was reloaded by flask debug reloader
|
|
93
|
+
# if it was, dont open the browser again
|
|
94
|
+
log.info('Opening UI as daemon')
|
|
95
|
+
if not IS_FLASK_RELOAD:
|
|
96
|
+
threading.Thread(
|
|
97
|
+
target=open_browser,
|
|
98
|
+
args=(uri,),
|
|
99
|
+
daemon=True
|
|
100
|
+
).start()
|
|
101
|
+
start_webserver(args)
|
|
102
|
+
else:
|
|
103
|
+
flask_thread = start_webserver_dameon(args)
|
|
104
|
+
app_closed = open_browser(uri)
|
|
105
|
+
|
|
106
|
+
# depending on env, open_browser may or
|
|
107
|
+
# may not be coupled with the closure of UI
|
|
108
|
+
# (if in browser tab, it's uncoupled)
|
|
109
|
+
if not app_closed:
|
|
110
|
+
# not doing a direct join so i can still
|
|
111
|
+
# terminate the app with ctrl+c
|
|
112
|
+
while flask_thread.is_alive():
|
|
113
|
+
time.sleep(1)
|
|
81
114
|
|
|
82
|
-
start_webserver(
|
|
83
|
-
args
|
|
84
|
-
)
|
|
85
115
|
|
|
86
116
|
def get_valid_port(port: int):
|
|
87
117
|
"""
|
|
@@ -93,6 +123,12 @@ def get_valid_port(port: int):
|
|
|
93
123
|
return port
|
|
94
124
|
port += 1
|
|
95
125
|
|
|
126
|
+
def terminate():
|
|
127
|
+
import requests
|
|
128
|
+
log.info('Attempting flask shutdown')
|
|
129
|
+
requests.get(f'http://127.0.0.1:{args.port}/shutdown')
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
96
133
|
if __name__ == "__main__":
|
|
97
|
-
main()
|
|
98
|
-
|
|
134
|
+
main()
|
lanscape/ui/static/css/style.css
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
lanscape/ui/static/js/main.js
CHANGED
|
@@ -187,10 +187,44 @@ function pollScanSummary(id) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function updateOverviewUI(summary) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
// helper to turn a number of seconds into "MM:SS"
|
|
191
|
+
function formatMMSS(totalSeconds) {
|
|
192
|
+
const secs = Math.floor(totalSeconds);
|
|
193
|
+
const m = Math.floor(secs / 60);
|
|
194
|
+
const s = secs % 60;
|
|
195
|
+
// pad minutes and seconds to 2 digits
|
|
196
|
+
const mm = String(m).padStart(2, '0');
|
|
197
|
+
const ss = String(s).padStart(2, '0');
|
|
198
|
+
return `${mm}:${ss}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const alive = summary.devices.alive;
|
|
202
|
+
const scanned = summary.devices.scanned;
|
|
203
|
+
const total = summary.devices.total;
|
|
204
|
+
|
|
205
|
+
// ensure we have a number of elapsed seconds
|
|
206
|
+
const runtimeSec = parseFloat(summary.runtime) || 0;
|
|
207
|
+
const pctComplete = Number(summary.percent_complete) || 0;
|
|
208
|
+
|
|
209
|
+
// compute remaining seconds correctly
|
|
210
|
+
const remainingSec = pctComplete > 0
|
|
211
|
+
? (runtimeSec * (100 - pctComplete)) / pctComplete
|
|
212
|
+
: 0;
|
|
213
|
+
|
|
214
|
+
// update everything…
|
|
215
|
+
$('#scan-devices-alive').text(alive);
|
|
216
|
+
$('#scan-devices-scanned').text(scanned);
|
|
217
|
+
$('#scan-devices-total').text(total);
|
|
218
|
+
|
|
219
|
+
// …but format runtime and remaining as MM:SS
|
|
220
|
+
$('#scan-run-time').text(formatMMSS(runtimeSec));
|
|
221
|
+
if (pctComplete < 10) {
|
|
222
|
+
$('#scan-remain-time').text('??:??');
|
|
223
|
+
} else {
|
|
224
|
+
$('#scan-remain-time').text(formatMMSS(remainingSec));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
194
228
|
$('#scan-stage').text(summary.stage);
|
|
195
229
|
}
|
|
196
230
|
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<div class="col-4">
|
|
13
13
|
<div class="card text-white overview-card" id="runtime-card">
|
|
14
|
-
<div class="card-header text-center">Runtime</div>
|
|
14
|
+
<div class="card-header text-center">Runtime / Remain</div>
|
|
15
15
|
<div class="card-body text-center">
|
|
16
|
-
<span id="scan-run-time"></span>
|
|
16
|
+
<span id="scan-run-time"></span> /
|
|
17
|
+
<span id="scan-remain-time"></span>
|
|
17
18
|
</div>
|
|
18
19
|
</div>
|
|
19
20
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0a1
|
|
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
|
|
@@ -36,7 +36,7 @@ python -m lanscape
|
|
|
36
36
|
- `--reloader` essentially flask debug mode- good for local development (default: false)
|
|
37
37
|
- `--logfile` save log output to lanscape.log
|
|
38
38
|
- `--loglevel <level>` set the logger's log level (default: INFO)
|
|
39
|
-
|
|
39
|
+
- `--flask-logging` turn on flask logging (default: false)
|
|
40
40
|
|
|
41
41
|
Examples:
|
|
42
42
|
```shell
|
|
@@ -2,16 +2,17 @@ lanscape/__init__.py,sha256=_8FoHQKR0s9B_stjs5e5CnytwbSK1JgNvE2kZBDrWbw,180
|
|
|
2
2
|
lanscape/__main__.py,sha256=Im2Qc9AIScEvjRik_X4x63n0Rie67-myQbuIEU7I-Ac,129
|
|
3
3
|
lanscape/libraries/app_scope.py,sha256=oPRrYIXOn914gF1DTVCDcy1d97hjReFlAxJNBjseBIo,2447
|
|
4
4
|
lanscape/libraries/decorators.py,sha256=Birxor-ae-CQRlBVOyB5DggHnuMQR98IYZ1FVHdGZzE,2448
|
|
5
|
-
lanscape/libraries/errors.py,sha256=
|
|
5
|
+
lanscape/libraries/errors.py,sha256=DaercNEZD_tUuXF7KsNk3SD6AqAwT-S7fvzpEybVn08,964
|
|
6
6
|
lanscape/libraries/ip_parser.py,sha256=ElXz3LU5CUYWqKOHEyrj5Y4Iv6OBtoSlbCcxhCsibfQ,2226
|
|
7
|
-
lanscape/libraries/logger.py,sha256=
|
|
7
|
+
lanscape/libraries/logger.py,sha256=doD8KKb4TNWDwVXc1VR7NK4UdharrAoRHl8vZnSAupI,1407
|
|
8
8
|
lanscape/libraries/mac_lookup.py,sha256=-dRV0ygtjjh3JkgL3GTi_5-w7pcZ1oj4XVH4chjsmRs,2121
|
|
9
|
-
lanscape/libraries/net_tools.py,sha256=
|
|
9
|
+
lanscape/libraries/net_tools.py,sha256=boiccYtpv3v37l239b9jQvYUBgz_ynm0W149ceVCSBU,11473
|
|
10
10
|
lanscape/libraries/port_manager.py,sha256=fNext3FNfGnGYRZK9RhTEwQ2K0e0YmmMlhK4zVAvoCw,1977
|
|
11
|
-
lanscape/libraries/runtime_args.py,sha256=
|
|
11
|
+
lanscape/libraries/runtime_args.py,sha256=zL8QB_Y69OBGjScytuuyHqdy2XimwpypMSPM3_etz7g,1811
|
|
12
12
|
lanscape/libraries/service_scan.py,sha256=jLU84ZoJnqSQbE30Zly2lm2zHrCGutNXjla1sEvp1hE,1949
|
|
13
|
-
lanscape/libraries/subnet_scan.py,sha256=
|
|
14
|
-
lanscape/libraries/version_manager.py,sha256=
|
|
13
|
+
lanscape/libraries/subnet_scan.py,sha256=0LW_xdoL-PRp59rJr6r6pSL3LiXEO_SJnjdrgEF_pO8,13120
|
|
14
|
+
lanscape/libraries/version_manager.py,sha256=Mh8VptGmv03d0tCpS-DFVgsxD5qWMA8iIG1KVcWOp4A,1690
|
|
15
|
+
lanscape/libraries/web_browser.py,sha256=xL2nGnqDTYJ1joMg2mT4uKX0Yq-4mI9svMjjrWyzkRg,3879
|
|
15
16
|
lanscape/resources/mac_addresses/convert_csv.py,sha256=w3Heed5z2mHYDEZNBep3_hNg4dbrp_N6J54MGxnrq4s,721
|
|
16
17
|
lanscape/resources/mac_addresses/mac_db.json,sha256=ygtFSwNwJzDlg6hmAujdgCyzUjxt9Di75J8SO4xYIs8,2187804
|
|
17
18
|
lanscape/resources/ports/convert_csv.py,sha256=mWe8zucWVfnlNEx_ZzH5Vc3tJJbdi-Ih4nm2yKNrRN0,720
|
|
@@ -22,30 +23,30 @@ lanscape/resources/ports/small.json,sha256=Mj3zGVG1F2eqZx2YkrLpTL8STeLcqB8_65IR6
|
|
|
22
23
|
lanscape/resources/services/definitions.jsonc,sha256=71w9Q7r4RoBYiIMkzzO2KdEJXaSIchNccYQueqAhD4E,8842
|
|
23
24
|
lanscape/tests/__init__.py,sha256=xYPeceOF-ppTS0wnq7CkVYQMwupmeSHxbYLbGj_imZ0,113
|
|
24
25
|
lanscape/tests/_helpers.py,sha256=wXJfUwzL3Fq4XBsC3dValCbXsf0U8FisuM_yo1de4QQ,371
|
|
25
|
-
lanscape/tests/test_api.py,sha256=
|
|
26
|
+
lanscape/tests/test_api.py,sha256=881eHwSHT5GnOB61zbt3BRzCZnSwN2lndwCM5kIHLgo,7359
|
|
26
27
|
lanscape/tests/test_env.py,sha256=ivFhCcemJ9vbe0_KtUkbqDY4r9nsDB8rVLUVjV-sNj8,673
|
|
27
28
|
lanscape/tests/test_library.py,sha256=OPcTsUoR5IureSNDbePxid2BG98mfNNIJmCIY0BVz3w,1553
|
|
28
|
-
lanscape/ui/app.py,sha256=
|
|
29
|
-
lanscape/ui/main.py,sha256=
|
|
29
|
+
lanscape/ui/app.py,sha256=Efa9gJnAcA_a3taCne9EHU4YxTCoRDd1M6UGa3rb2Qw,3126
|
|
30
|
+
lanscape/ui/main.py,sha256=zDs7epDeXRoxZAHt9aLXB3kypXdZa1pSeqCIitwN7Ig,3998
|
|
30
31
|
lanscape/ui/blueprints/__init__.py,sha256=agvgPOSVbrxddaw6EY64ZZr1CQi1Qzwcs1t0lZMv5oY,206
|
|
31
32
|
lanscape/ui/blueprints/api/__init__.py,sha256=t0QOq3vHFWmlZm_3YFPQbQzCn1a_a5cmRchtIxwy4eY,103
|
|
32
33
|
lanscape/ui/blueprints/api/port.py,sha256=2UA38umzXE8pMitx1E-_wJHyL1dYYbtM6Kg5zVtfj6A,1019
|
|
33
|
-
lanscape/ui/blueprints/api/scan.py,sha256=
|
|
34
|
+
lanscape/ui/blueprints/api/scan.py,sha256=vNHH5XIVM8hWsHpkBX2okNFXWSFWz0E1RQuom8D-9TI,2229
|
|
34
35
|
lanscape/ui/blueprints/api/tools.py,sha256=CD0NDSX8kN6_lpl0jEw-ULLsDx7pKODCMFQiaK4GCzM,1153
|
|
35
36
|
lanscape/ui/blueprints/web/__init__.py,sha256=-WRjENG8D99NfaiSDk9uAa8OX6XJq9Zmq1ck29ARL-w,92
|
|
36
|
-
lanscape/ui/blueprints/web/routes.py,sha256=
|
|
37
|
+
lanscape/ui/blueprints/web/routes.py,sha256=hl89T5_oRbTlA9Cde_xh9zAQDGRRH-Dpwm74B1_NM1Y,2511
|
|
37
38
|
lanscape/ui/static/lanscape.webmanifest,sha256=0aauJk_Bybd0B2iwzJfvPcs7AX43kVHs0dtpV6_jSWk,459
|
|
38
|
-
lanscape/ui/static/css/style.css,sha256=
|
|
39
|
-
lanscape/ui/static/img/ico/android-chrome-192x192.png,sha256=
|
|
40
|
-
lanscape/ui/static/img/ico/android-chrome-512x512.png,sha256=
|
|
41
|
-
lanscape/ui/static/img/ico/apple-touch-icon.png,sha256=
|
|
42
|
-
lanscape/ui/static/img/ico/favicon-16x16.png,sha256=
|
|
43
|
-
lanscape/ui/static/img/ico/favicon-32x32.png,sha256=
|
|
44
|
-
lanscape/ui/static/img/ico/favicon.ico,sha256=
|
|
39
|
+
lanscape/ui/static/css/style.css,sha256=ZoROzQmBIZlMdz0i9YHkx0Eomo87nQcDLJnGiiC0S3Y,15838
|
|
40
|
+
lanscape/ui/static/img/ico/android-chrome-192x192.png,sha256=JmFT6KBCCuoyxMV-mLNtF9_QJbVBvfWPUizKN700fi8,18255
|
|
41
|
+
lanscape/ui/static/img/ico/android-chrome-512x512.png,sha256=88Jjx_1-4XAnZYz64KP6FdTl_kYkNG2_kQIKteQwSh4,138055
|
|
42
|
+
lanscape/ui/static/img/ico/apple-touch-icon.png,sha256=tEJlLwBZtF4v-NC90YCfRJQ2prTsF4i3VQLK_hnv2Mw,16523
|
|
43
|
+
lanscape/ui/static/img/ico/favicon-16x16.png,sha256=HpQOZk3rziZjT1xQxKuy5WourXsfrdwuzQY1hChzBJQ,573
|
|
44
|
+
lanscape/ui/static/img/ico/favicon-32x32.png,sha256=UpgiDPIHckK19udHtACiaI3ZPbmImUUcN1GcrjpEg9s,1302
|
|
45
|
+
lanscape/ui/static/img/ico/favicon.ico,sha256=rs5vq0MPJ1LzzioOzOz5aQLVfrtS2nLRc920dOeReTw,15406
|
|
45
46
|
lanscape/ui/static/img/ico/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
46
47
|
lanscape/ui/static/js/core.js,sha256=y-f8iQPIetllUY0lSCwnGbPCk5fTJbbU6Pxm3rw1EBU,1111
|
|
47
48
|
lanscape/ui/static/js/layout-sizing.js,sha256=23UuKdEmRChg6fyqj3DRvcsNfMoa6MRt6dkaT0k7_UY,841
|
|
48
|
-
lanscape/ui/static/js/main.js,sha256=
|
|
49
|
+
lanscape/ui/static/js/main.js,sha256=s0ipGqmuuFHnH9KKoUVaDRJ10_YqYoJ-9r_YnbsH8Mw,7676
|
|
49
50
|
lanscape/ui/static/js/quietReload.js,sha256=_mHzpUsGL4Lm1hNsr3VYSOGVcgGA2y1-eZHacssTXGs,724
|
|
50
51
|
lanscape/ui/static/js/shutdown-server.js,sha256=WkO7_SNSHM_6kReUoCoExIdCf7Sl7IPiSiNxpbI-r0s,469
|
|
51
52
|
lanscape/ui/static/js/subnet-info.js,sha256=aytt0LkBx4FVq36TxiMEw3aM7XQLHg_ng1U2WDwZVF4,577
|
|
@@ -61,10 +62,10 @@ lanscape/ui/templates/core/scripts.html,sha256=TRW74VUDasOTFYkaDhKKFnEwHyNx-_rmz
|
|
|
61
62
|
lanscape/ui/templates/scan/export.html,sha256=Qi0m2xJPbC5I2rxzekXjvQ6q9gm2Lr4VJW6riLhIaU0,776
|
|
62
63
|
lanscape/ui/templates/scan/ip-table-row.html,sha256=ptY24rxJRaA4PEEQRDncaq6Q0ql5RJ87Kn0zKRCzOHw,4842
|
|
63
64
|
lanscape/ui/templates/scan/ip-table.html,sha256=ds__UP9JiTKf5IxCmTMzw--eN_yg1Pvn3Nj1KvQxeZg,940
|
|
64
|
-
lanscape/ui/templates/scan/overview.html,sha256=
|
|
65
|
+
lanscape/ui/templates/scan/overview.html,sha256=FsX-jSFhGKwCxZGKE8AMKk328UuawN6O9RNTzYvIOts,1205
|
|
65
66
|
lanscape/ui/templates/scan/scan-error.html,sha256=Q4eZM5ThrxnFaWOSTUpK8hA2ksHwhxOBTaVUCLALhyA,1032
|
|
66
|
-
lanscape-1.
|
|
67
|
-
lanscape-1.
|
|
68
|
-
lanscape-1.
|
|
69
|
-
lanscape-1.
|
|
70
|
-
lanscape-1.
|
|
67
|
+
lanscape-1.3.0a1.dist-info/licenses/LICENSE,sha256=cCO-NbS01Ilwc6djHjZ7LIgPFRkRmWdr0fH2ysXKioA,1090
|
|
68
|
+
lanscape-1.3.0a1.dist-info/METADATA,sha256=Vig-rxR7BRUlyBcpIJDeA4mPsS9HIIQHPoJIVOof5IA,2120
|
|
69
|
+
lanscape-1.3.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
70
|
+
lanscape-1.3.0a1.dist-info/top_level.txt,sha256=E9D4sjPz_6H7c85Ycy_pOS2xuv1Wm-ilKhxEprln2ps,9
|
|
71
|
+
lanscape-1.3.0a1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|