lanscape 1.3.5a2__py3-none-any.whl → 1.3.6__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/__init__.py +9 -1
- lanscape/libraries/decorators.py +16 -4
- lanscape/libraries/device_alive.py +229 -0
- lanscape/libraries/net_tools.py +37 -127
- lanscape/libraries/scan_config.py +84 -25
- lanscape/libraries/subnet_scan.py +13 -16
- lanscape/ui/main.py +0 -9
- lanscape/ui/static/css/style.css +35 -24
- lanscape/ui/static/js/scan-config.js +76 -2
- lanscape/ui/templates/main.html +0 -7
- lanscape/ui/templates/scan/config.html +71 -10
- {lanscape-1.3.5a2.dist-info → lanscape-1.3.6.dist-info}/METADATA +27 -10
- {lanscape-1.3.5a2.dist-info → lanscape-1.3.6.dist-info}/RECORD +16 -15
- {lanscape-1.3.5a2.dist-info → lanscape-1.3.6.dist-info}/WHEEL +0 -0
- {lanscape-1.3.5a2.dist-info → lanscape-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.5a2.dist-info → lanscape-1.3.6.dist-info}/top_level.txt +0 -0
lanscape/__init__.py
CHANGED
|
@@ -3,10 +3,18 @@ Local network scanner
|
|
|
3
3
|
"""
|
|
4
4
|
from lanscape.libraries.subnet_scan import (
|
|
5
5
|
SubnetScanner,
|
|
6
|
-
ScanConfig,
|
|
7
6
|
ScanManager
|
|
8
7
|
)
|
|
9
8
|
|
|
9
|
+
from lanscape.libraries.scan_config import (
|
|
10
|
+
ScanConfig,
|
|
11
|
+
ArpConfig,
|
|
12
|
+
PingConfig,
|
|
13
|
+
PokeConfig,
|
|
14
|
+
ArpCacheConfig,
|
|
15
|
+
ScanType
|
|
16
|
+
)
|
|
17
|
+
|
|
10
18
|
from lanscape.libraries.port_manager import PortManager
|
|
11
19
|
|
|
12
20
|
from lanscape.libraries import net_tools
|
lanscape/libraries/decorators.py
CHANGED
|
@@ -25,6 +25,20 @@ class JobStats:
|
|
|
25
25
|
timing: DefaultDict[str, float] = field(
|
|
26
26
|
default_factory=lambda: defaultdict(float))
|
|
27
27
|
|
|
28
|
+
_instance = None
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
# Only initialize once
|
|
32
|
+
if not hasattr(self, "running"):
|
|
33
|
+
self.running = defaultdict(int)
|
|
34
|
+
self.finished = defaultdict(int)
|
|
35
|
+
self.timing = defaultdict(float)
|
|
36
|
+
|
|
37
|
+
def __new__(cls, *args, **kwargs):
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
cls._instance = super(JobStats, cls).__new__(cls)
|
|
40
|
+
return cls._instance
|
|
41
|
+
|
|
28
42
|
def __str__(self):
|
|
29
43
|
"""Return a formatted string representation of the job statistics."""
|
|
30
44
|
data = [
|
|
@@ -53,9 +67,7 @@ class JobStatsMixin: # pylint: disable=too-few-public-methods
|
|
|
53
67
|
@property
|
|
54
68
|
def job_stats(self):
|
|
55
69
|
"""Return the shared JobStats instance."""
|
|
56
|
-
|
|
57
|
-
JobStatsMixin._job_stats = JobStats()
|
|
58
|
-
return JobStatsMixin._job_stats
|
|
70
|
+
return JobStats()
|
|
59
71
|
|
|
60
72
|
|
|
61
73
|
def job_tracker(func):
|
|
@@ -81,7 +93,7 @@ def job_tracker(func):
|
|
|
81
93
|
def wrapper(*args, **kwargs):
|
|
82
94
|
"""Wrap the function to update job statistics before and after execution."""
|
|
83
95
|
class_instance = args[0]
|
|
84
|
-
job_stats =
|
|
96
|
+
job_stats = JobStats()
|
|
85
97
|
fxn = get_fxn_src_name(
|
|
86
98
|
func,
|
|
87
99
|
class_instance
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handles device alive checks using various methods.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from typing import List
|
|
10
|
+
import psutil
|
|
11
|
+
|
|
12
|
+
from scapy.sendrecv import srp
|
|
13
|
+
from scapy.layers.l2 import ARP, Ether
|
|
14
|
+
from icmplib import ping
|
|
15
|
+
|
|
16
|
+
from lanscape.libraries.net_tools import Device
|
|
17
|
+
from lanscape.libraries.scan_config import (
|
|
18
|
+
ScanConfig, ScanType, PingConfig,
|
|
19
|
+
ArpConfig, PokeConfig, ArpCacheConfig
|
|
20
|
+
)
|
|
21
|
+
from lanscape.libraries.decorators import timeout_enforcer, job_tracker
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_device_alive(device: Device, scan_config: ScanConfig) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check if a device is alive based on the configured scan type.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
device (Device): The device to check.
|
|
30
|
+
scan_config (ScanConfig): The configuration for the scan.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
bool: True if the device is alive, False otherwise.
|
|
34
|
+
"""
|
|
35
|
+
methods = scan_config.lookup_type
|
|
36
|
+
|
|
37
|
+
if ScanType.ICMP in methods:
|
|
38
|
+
IcmpLookup.execute(device, scan_config.ping_config)
|
|
39
|
+
|
|
40
|
+
if ScanType.ARP_LOOKUP in methods and not device.alive:
|
|
41
|
+
ArpLookup.execute(device, scan_config.arp_config)
|
|
42
|
+
|
|
43
|
+
if ScanType.ICMP_THEN_ARP in methods and not device.alive:
|
|
44
|
+
IcmpLookup.execute(device, scan_config.ping_config)
|
|
45
|
+
ArpCacheLookup.execute(device, scan_config.arp_cache_config)
|
|
46
|
+
|
|
47
|
+
if ScanType.POKE_THEN_ARP in methods and not device.alive:
|
|
48
|
+
Poker.execute(device, scan_config.poke_config)
|
|
49
|
+
ArpCacheLookup.execute(device, scan_config.arp_cache_config)
|
|
50
|
+
|
|
51
|
+
return device.alive is True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IcmpLookup():
|
|
55
|
+
"""Class to handle ICMP ping lookups for device presence.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
NotImplementedError: If the platform is not supported.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if the device is reachable via ICMP, False otherwise.
|
|
62
|
+
"""
|
|
63
|
+
@classmethod
|
|
64
|
+
@job_tracker
|
|
65
|
+
def execute(cls, device: Device, cfg: PingConfig) -> bool:
|
|
66
|
+
"""Perform an ICMP ping lookup for the specified device.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
device (Device): The device to look up.
|
|
70
|
+
cfg (PingConfig): The configuration for the scan.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool: True if the device is reachable via ICMP, False otherwise.
|
|
74
|
+
"""
|
|
75
|
+
# Perform up to cfg.attempts rounds of ping(count=cfg.ping_count)
|
|
76
|
+
for _ in range(cfg.attempts):
|
|
77
|
+
result = ping(
|
|
78
|
+
device.ip,
|
|
79
|
+
count=cfg.ping_count,
|
|
80
|
+
interval=cfg.retry_delay,
|
|
81
|
+
timeout=cfg.timeout,
|
|
82
|
+
privileged=psutil.WINDOWS # Use privileged mode on Windows
|
|
83
|
+
)
|
|
84
|
+
if result.is_alive:
|
|
85
|
+
device.alive = True
|
|
86
|
+
break
|
|
87
|
+
return device.alive is True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ArpCacheLookup():
|
|
91
|
+
"""
|
|
92
|
+
Class to handle ARP cache lookups for device presence.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
@job_tracker
|
|
97
|
+
def execute(cls, device: Device, cfg: ArpCacheConfig) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Perform an ARP cache lookup for the specified device.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
device (Device): The device to look up.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
bool: True if the device is found in the ARP cache, False otherwise.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
command = cls._get_platform_arp_command() + [device.ip]
|
|
109
|
+
|
|
110
|
+
for _ in range(cfg.attempts):
|
|
111
|
+
time.sleep(cfg.wait_before)
|
|
112
|
+
output = subprocess.check_output(command).decode()
|
|
113
|
+
macs = cls._extract_mac_address(output)
|
|
114
|
+
if macs:
|
|
115
|
+
device.macs = macs
|
|
116
|
+
device.alive = True
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
return device.alive is True
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def _get_platform_arp_command(cls) -> List[str]:
|
|
123
|
+
"""
|
|
124
|
+
Get the ARP command to execute based on the platform.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
list[str]: The ARP command to execute.
|
|
128
|
+
"""
|
|
129
|
+
if psutil.WINDOWS:
|
|
130
|
+
return ['arp', '-a']
|
|
131
|
+
if psutil.LINUX:
|
|
132
|
+
return ['arp', '-n']
|
|
133
|
+
if psutil.MACOS:
|
|
134
|
+
return ['arp', '-n']
|
|
135
|
+
|
|
136
|
+
raise NotImplementedError("Unsupported platform")
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _extract_mac_address(cls, arp_resp: str) -> List[str]:
|
|
140
|
+
"""
|
|
141
|
+
Extract MAC addresses from ARP output.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
arp_resp (str): The ARP command output.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List[str]: A list of extracted MAC addresses (may be empty).
|
|
148
|
+
"""
|
|
149
|
+
arp_resp = arp_resp.replace('-', ':')
|
|
150
|
+
return re.findall(r'..:..:..:..:..:..', arp_resp)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ArpLookup():
|
|
154
|
+
"""
|
|
155
|
+
Class to handle ARP lookups for device presence.
|
|
156
|
+
NOTE: This lookup method requires elevated privileges to access the ARP cache.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
[Arp Lookup Requirements](/support/arp-issues.md)
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
@job_tracker
|
|
164
|
+
def execute(cls, device: Device, cfg: ArpConfig) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Perform an ARP lookup for the specified device.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
device (Device): The device to look up.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
bool: True if the device is found via ARP, False otherwise.
|
|
173
|
+
"""
|
|
174
|
+
enforcer_timeout = cfg.timeout * 2
|
|
175
|
+
|
|
176
|
+
@timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
|
|
177
|
+
def do_arp_lookup():
|
|
178
|
+
arp_request = ARP(pdst=device.ip)
|
|
179
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
180
|
+
packet = broadcast / arp_request
|
|
181
|
+
|
|
182
|
+
answered, _ = srp(packet, timeout=cfg.timeout, verbose=False)
|
|
183
|
+
alive = any(resp.psrc == device.ip for _, resp in answered)
|
|
184
|
+
macs = []
|
|
185
|
+
if alive:
|
|
186
|
+
macs = [resp.hwsrc for _, resp in answered if resp.psrc == device.ip]
|
|
187
|
+
return alive, macs
|
|
188
|
+
|
|
189
|
+
alive, macs = do_arp_lookup()
|
|
190
|
+
if alive:
|
|
191
|
+
device.alive = True
|
|
192
|
+
device.macs = macs
|
|
193
|
+
|
|
194
|
+
return device.alive is True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Poker():
|
|
198
|
+
"""
|
|
199
|
+
Class to handle Poking the device to populate the ARP cache.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
@job_tracker
|
|
204
|
+
def execute(cls, device: Device, cfg: PokeConfig):
|
|
205
|
+
"""
|
|
206
|
+
Perform a Poke for the specified device.
|
|
207
|
+
Note: the purpose of this is to simply populate the arp cache.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
device (Device): The device to look up.
|
|
211
|
+
cfg (PokeConfig): The configuration for the Poke lookup.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
None: used to populate the arp cache
|
|
215
|
+
"""
|
|
216
|
+
enforcer_timeout = cfg.timeout * cfg.attempts * 2
|
|
217
|
+
|
|
218
|
+
@timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
|
|
219
|
+
def do_poke():
|
|
220
|
+
# Use a small set of common ports likely to be filtered but still trigger ARP
|
|
221
|
+
common_ports = [80, 443, 22]
|
|
222
|
+
for i in range(cfg.attempts):
|
|
223
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
224
|
+
sock.settimeout(cfg.timeout)
|
|
225
|
+
port = common_ports[i % len(common_ports)]
|
|
226
|
+
sock.connect_ex((device.ip, port))
|
|
227
|
+
sock.close()
|
|
228
|
+
|
|
229
|
+
do_poke()
|
lanscape/libraries/net_tools.py
CHANGED
|
@@ -4,13 +4,11 @@ import logging
|
|
|
4
4
|
import ipaddress
|
|
5
5
|
import traceback
|
|
6
6
|
import subprocess
|
|
7
|
-
from time import sleep
|
|
8
7
|
from typing import List, Dict
|
|
9
8
|
import socket
|
|
10
9
|
import struct
|
|
11
10
|
import re
|
|
12
11
|
import psutil
|
|
13
|
-
from icmplib import ping
|
|
14
12
|
|
|
15
13
|
from scapy.sendrecv import srp
|
|
16
14
|
from scapy.layers.l2 import ARP, Ether
|
|
@@ -20,116 +18,13 @@ from lanscape.libraries.service_scan import scan_service
|
|
|
20
18
|
from lanscape.libraries.mac_lookup import MacLookup, get_macs
|
|
21
19
|
from lanscape.libraries.ip_parser import get_address_count, MAX_IPS_ALLOWED
|
|
22
20
|
from lanscape.libraries.errors import DeviceError
|
|
23
|
-
from lanscape.libraries.decorators import job_tracker
|
|
24
|
-
from lanscape.libraries.scan_config import ScanType, PingConfig, ArpConfig
|
|
21
|
+
from lanscape.libraries.decorators import job_tracker
|
|
25
22
|
|
|
26
23
|
log = logging.getLogger('NetTools')
|
|
24
|
+
mac_lookup = MacLookup()
|
|
27
25
|
|
|
28
26
|
|
|
29
|
-
class
|
|
30
|
-
"""Class to check if a device is alive using ARP and/or ping scans."""
|
|
31
|
-
caught_errors: List[DeviceError] = []
|
|
32
|
-
_icmp_alive: bool = False
|
|
33
|
-
_arp_alive: bool = False
|
|
34
|
-
|
|
35
|
-
@job_tracker
|
|
36
|
-
def is_alive(
|
|
37
|
-
self,
|
|
38
|
-
ip: str,
|
|
39
|
-
scan_type: ScanType = ScanType.BOTH,
|
|
40
|
-
arp_config: ArpConfig = ArpConfig(),
|
|
41
|
-
ping_config: PingConfig = PingConfig()
|
|
42
|
-
) -> bool:
|
|
43
|
-
"""
|
|
44
|
-
Check if a device is alive by performing ARP and/or ping scans.
|
|
45
|
-
"""
|
|
46
|
-
if scan_type == ScanType.ARP:
|
|
47
|
-
return self._arp_lookup(ip, arp_config)
|
|
48
|
-
if scan_type == ScanType.PING:
|
|
49
|
-
return self._ping_lookup(ip, ping_config)
|
|
50
|
-
return self._ping_lookup(ip, ping_config) or self._arp_lookup(ip, arp_config)
|
|
51
|
-
|
|
52
|
-
@job_tracker
|
|
53
|
-
def _arp_lookup(
|
|
54
|
-
self, ip: str,
|
|
55
|
-
cfg: ArpConfig = ArpConfig()
|
|
56
|
-
) -> bool:
|
|
57
|
-
"""Perform an ARP lookup to check if the device is alive."""
|
|
58
|
-
enforcer_timeout = cfg.timeout * 1.3
|
|
59
|
-
|
|
60
|
-
@timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
|
|
61
|
-
def do_arp_lookup():
|
|
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=cfg.timeout, verbose=False)
|
|
67
|
-
self._arp_alive = any(resp.psrc == ip for _, resp in answered)
|
|
68
|
-
return self._arp_alive
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
for _ in range(cfg.attempts):
|
|
72
|
-
if do_arp_lookup():
|
|
73
|
-
return True
|
|
74
|
-
except Exception as e:
|
|
75
|
-
self.caught_errors.append(DeviceError(e))
|
|
76
|
-
return False
|
|
77
|
-
|
|
78
|
-
@job_tracker
|
|
79
|
-
def _ping_lookup(
|
|
80
|
-
self, ip: str,
|
|
81
|
-
cfg: PingConfig = PingConfig()
|
|
82
|
-
) -> bool:
|
|
83
|
-
"""Perform a ping lookup to check if the device is alive using icmplib."""
|
|
84
|
-
enforcer_timeout = cfg.timeout * cfg.ping_count * 1.3
|
|
85
|
-
|
|
86
|
-
@timeout_enforcer(enforcer_timeout, raise_on_timeout=False)
|
|
87
|
-
def do_icmp_ping():
|
|
88
|
-
try:
|
|
89
|
-
result = ping(
|
|
90
|
-
ip,
|
|
91
|
-
count=cfg.ping_count,
|
|
92
|
-
interval=cfg.retry_delay,
|
|
93
|
-
timeout=cfg.timeout,
|
|
94
|
-
privileged=psutil.WINDOWS # Use privileged mode on Windows
|
|
95
|
-
)
|
|
96
|
-
return result.is_alive
|
|
97
|
-
except Exception as e:
|
|
98
|
-
self.caught_errors.append(DeviceError(e))
|
|
99
|
-
# Fallback to system ping command
|
|
100
|
-
try:
|
|
101
|
-
if psutil.WINDOWS:
|
|
102
|
-
cmd = [
|
|
103
|
-
"ping", "-n", str(cfg.ping_count),
|
|
104
|
-
"-w", str(int(cfg.timeout * 1000)), ip
|
|
105
|
-
]
|
|
106
|
-
else:
|
|
107
|
-
cmd = ["ping", "-c",
|
|
108
|
-
str(cfg.ping_count), "-W", str(cfg.timeout), ip]
|
|
109
|
-
|
|
110
|
-
result = subprocess.run(
|
|
111
|
-
cmd, stdout=subprocess.PIPE,
|
|
112
|
-
stderr=subprocess.PIPE,
|
|
113
|
-
text=True, check=False
|
|
114
|
-
)
|
|
115
|
-
return result.returncode == 0
|
|
116
|
-
except subprocess.CalledProcessError as fallback_error:
|
|
117
|
-
self.caught_errors.append(DeviceError(fallback_error))
|
|
118
|
-
return False
|
|
119
|
-
|
|
120
|
-
try:
|
|
121
|
-
for _ in range(cfg.attempts):
|
|
122
|
-
if do_icmp_ping():
|
|
123
|
-
self._icmp_alive = True
|
|
124
|
-
return True
|
|
125
|
-
sleep(cfg.retry_delay)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
self.caught_errors.append(DeviceError(e))
|
|
128
|
-
self._icmp_alive = False
|
|
129
|
-
return False
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class Device(IPAlive):
|
|
27
|
+
class Device:
|
|
133
28
|
"""Represents a network device with metadata and scanning capabilities."""
|
|
134
29
|
|
|
135
30
|
def __init__(self, ip: str):
|
|
@@ -144,13 +39,12 @@ class Device(IPAlive):
|
|
|
144
39
|
self.services: Dict[str, List[int]] = {}
|
|
145
40
|
self.caught_errors: List[DeviceError] = []
|
|
146
41
|
self.log = logging.getLogger('Device')
|
|
147
|
-
self._mac_lookup = MacLookup()
|
|
148
42
|
|
|
149
43
|
def get_metadata(self):
|
|
150
44
|
"""Retrieve metadata such as hostname and MAC addresses."""
|
|
151
45
|
if self.alive:
|
|
152
46
|
self.hostname = self._get_hostname()
|
|
153
|
-
self.
|
|
47
|
+
self._get_mac_addresses()
|
|
154
48
|
|
|
155
49
|
def dict(self) -> dict:
|
|
156
50
|
"""Convert the device object to a dictionary."""
|
|
@@ -191,9 +85,12 @@ class Device(IPAlive):
|
|
|
191
85
|
@job_tracker
|
|
192
86
|
def _get_mac_addresses(self):
|
|
193
87
|
"""Get the possible MAC addresses of a network device given its IP address."""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
88
|
+
# job may already be done depending on
|
|
89
|
+
# the strat from isalive
|
|
90
|
+
if not self.macs:
|
|
91
|
+
self.macs = get_macs(self.ip)
|
|
92
|
+
mac_selector.import_macs(self.macs)
|
|
93
|
+
return self.macs
|
|
197
94
|
|
|
198
95
|
@job_tracker
|
|
199
96
|
def _get_hostname(self):
|
|
@@ -208,7 +105,7 @@ class Device(IPAlive):
|
|
|
208
105
|
@job_tracker
|
|
209
106
|
def _get_manufacturer(self, mac_addr=None):
|
|
210
107
|
"""Get the manufacturer of a network device given its MAC address."""
|
|
211
|
-
return
|
|
108
|
+
return mac_lookup.lookup_vendor(mac_addr) if mac_addr else None
|
|
212
109
|
|
|
213
110
|
|
|
214
111
|
class MacSelector:
|
|
@@ -562,19 +459,32 @@ def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|
|
|
562
459
|
return selected.get("subnet", "")
|
|
563
460
|
|
|
564
461
|
|
|
462
|
+
class ArpSupportChecker:
|
|
463
|
+
"""
|
|
464
|
+
Singleton class to check if ARP requests are supported on the current system.
|
|
465
|
+
The check is only performed once.
|
|
466
|
+
"""
|
|
467
|
+
_supported = None
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def is_supported(cls):
|
|
471
|
+
"""one time check if ARP requests are supported on this system"""
|
|
472
|
+
if cls._supported is not None:
|
|
473
|
+
return cls._supported
|
|
474
|
+
try:
|
|
475
|
+
arp_request = ARP(pdst='0.0.0.0')
|
|
476
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
477
|
+
packet = broadcast / arp_request
|
|
478
|
+
srp(packet, timeout=0, verbose=False)
|
|
479
|
+
cls._supported = True
|
|
480
|
+
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
481
|
+
cls._supported = False
|
|
482
|
+
return cls._supported
|
|
483
|
+
|
|
484
|
+
|
|
565
485
|
def is_arp_supported():
|
|
566
486
|
"""
|
|
567
|
-
Check if ARP requests are supported on the current
|
|
487
|
+
Check if ARP requests are supported on the current system.
|
|
488
|
+
Only runs the check once.
|
|
568
489
|
"""
|
|
569
|
-
|
|
570
|
-
arp_request = ARP(pdst='0.0.0.0')
|
|
571
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
572
|
-
packet = broadcast / arp_request
|
|
573
|
-
|
|
574
|
-
srp(packet, timeout=0, verbose=False)
|
|
575
|
-
return True
|
|
576
|
-
# Scapy_Exception = MacOS
|
|
577
|
-
# PermissionError = Linux
|
|
578
|
-
# RuntimeError = Windows
|
|
579
|
-
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
580
|
-
return False
|
|
490
|
+
return ArpSupportChecker.is_supported()
|
|
@@ -4,6 +4,7 @@ Provides classes and utilities to configure different types of network scans
|
|
|
4
4
|
including ping scans, ARP scans, and port scanning.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
from typing import List, Dict
|
|
8
9
|
import ipaddress
|
|
9
10
|
from enum import Enum
|
|
@@ -90,17 +91,81 @@ class ArpConfig(BaseModel):
|
|
|
90
91
|
return f'ArpCfg(timeout={self.timeout}, attempts={self.attempts})'
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
class ArpCacheConfig(BaseModel):
|
|
95
|
+
"""Config for fetching from ARP cache"""
|
|
96
|
+
attempts: int = 1
|
|
97
|
+
wait_before: float = 0.2
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, data: dict) -> 'ArpCacheConfig':
|
|
101
|
+
"""
|
|
102
|
+
Create an ArpCacheConfig instance from a dictionary.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data: Dictionary containing ArpCacheConfig parameters
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A new ArpCacheConfig instance with the provided settings
|
|
109
|
+
"""
|
|
110
|
+
return cls.model_validate(data)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict:
|
|
113
|
+
"""
|
|
114
|
+
Convert the ArpCacheConfig instance to a dictionary.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary representation of the ArpCacheConfig
|
|
118
|
+
"""
|
|
119
|
+
return self.model_dump()
|
|
120
|
+
|
|
121
|
+
def __str__(self):
|
|
122
|
+
return f'ArpCacheCfg(wait_before={self.wait_before}, attempts={self.attempts})'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PokeConfig(BaseModel):
|
|
126
|
+
"""
|
|
127
|
+
Poking essentially involves sending a TCP packet to a specific port on a device
|
|
128
|
+
to elicit a response. Not so much expecting a response, but it should at least
|
|
129
|
+
trigger an ARP request.
|
|
130
|
+
"""
|
|
131
|
+
attempts: int = 1
|
|
132
|
+
timeout: float = 2.0
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_dict(cls, data: dict) -> 'PokeConfig':
|
|
136
|
+
"""
|
|
137
|
+
Create a PokeConfig instance from a dictionary.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
data: Dictionary containing PokeConfig parameters
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
A new PokeConfig instance with the provided settings
|
|
144
|
+
"""
|
|
145
|
+
return cls.model_validate(data)
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> dict:
|
|
148
|
+
"""
|
|
149
|
+
Convert the PokeConfig instance to a dictionary.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary representation of the PokeConfig
|
|
153
|
+
"""
|
|
154
|
+
return self.model_dump()
|
|
155
|
+
|
|
156
|
+
|
|
93
157
|
class ScanType(Enum):
|
|
94
158
|
"""
|
|
95
159
|
Enumeration of supported network scan types.
|
|
96
160
|
|
|
97
161
|
PING: Uses ICMP echo requests to determine if hosts are alive
|
|
98
162
|
ARP: Uses Address Resolution Protocol to discover hosts on the local network
|
|
99
|
-
|
|
163
|
+
|
|
100
164
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
ICMP = 'ICMP'
|
|
166
|
+
ARP_LOOKUP = 'ARP_LOOKUP'
|
|
167
|
+
POKE_THEN_ARP = 'POKE_THEN_ARP'
|
|
168
|
+
ICMP_THEN_ARP = 'ICMP_THEN_ARP'
|
|
104
169
|
|
|
105
170
|
|
|
106
171
|
class ScanConfig(BaseModel):
|
|
@@ -113,18 +178,20 @@ class ScanConfig(BaseModel):
|
|
|
113
178
|
subnet: str
|
|
114
179
|
port_list: str
|
|
115
180
|
t_multiplier: float = 1.0
|
|
116
|
-
t_cnt_port_scan: int =
|
|
117
|
-
t_cnt_port_test: int =
|
|
118
|
-
t_cnt_isalive: int =
|
|
181
|
+
t_cnt_port_scan: int = os.cpu_count() or 4
|
|
182
|
+
t_cnt_port_test: int = (os.cpu_count() or 4) * 4
|
|
183
|
+
t_cnt_isalive: int = (os.cpu_count() or 4) * 6
|
|
119
184
|
|
|
120
185
|
task_scan_ports: bool = True
|
|
121
186
|
# below wont run if above false
|
|
122
187
|
task_scan_port_services: bool = False # disabling until more stable
|
|
123
188
|
|
|
124
|
-
lookup_type: ScanType = ScanType.
|
|
189
|
+
lookup_type: List[ScanType] = [ScanType.ICMP_THEN_ARP]
|
|
125
190
|
|
|
126
191
|
ping_config: PingConfig = Field(default_factory=PingConfig)
|
|
127
192
|
arp_config: ArpConfig = Field(default_factory=ArpConfig)
|
|
193
|
+
poke_config: PokeConfig = Field(default_factory=PokeConfig)
|
|
194
|
+
arp_cache_config: ArpCacheConfig = Field(default_factory=ArpCacheConfig)
|
|
128
195
|
|
|
129
196
|
def t_cnt(self, thread_id: str) -> int:
|
|
130
197
|
"""
|
|
@@ -143,32 +210,20 @@ class ScanConfig(BaseModel):
|
|
|
143
210
|
"""
|
|
144
211
|
Create a ScanConfig instance from a dictionary.
|
|
145
212
|
|
|
146
|
-
Handles special cases like converting string enum values to proper Enum types.
|
|
147
|
-
|
|
148
213
|
Args:
|
|
149
214
|
data: Dictionary containing ScanConfig parameters
|
|
150
215
|
|
|
151
216
|
Returns:
|
|
152
217
|
A new ScanConfig instance with the provided settings
|
|
153
218
|
"""
|
|
154
|
-
# Handle special cases before validation
|
|
155
|
-
if isinstance(data.get('lookup_type'), str):
|
|
156
|
-
data['lookup_type'] = ScanType[data['lookup_type'].upper()]
|
|
157
219
|
|
|
158
220
|
return cls.model_validate(data)
|
|
159
221
|
|
|
160
222
|
def to_dict(self) -> dict:
|
|
161
223
|
"""
|
|
162
|
-
Convert the ScanConfig instance to a dictionary.
|
|
163
|
-
|
|
164
|
-
Handles special cases like converting Enum values to strings.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
Dictionary representation of the ScanConfig
|
|
224
|
+
Convert the ScanConfig instance to a json-serializable dictionary.
|
|
168
225
|
"""
|
|
169
|
-
|
|
170
|
-
dump['lookup_type'] = self.lookup_type.value
|
|
171
|
-
return dump
|
|
226
|
+
return self.model_dump(mode="json")
|
|
172
227
|
|
|
173
228
|
def get_ports(self) -> List[int]:
|
|
174
229
|
"""
|
|
@@ -191,7 +246,7 @@ class ScanConfig(BaseModel):
|
|
|
191
246
|
def __str__(self):
|
|
192
247
|
a = f'subnet={self.subnet}'
|
|
193
248
|
b = f'ports={self.port_list}'
|
|
194
|
-
c = f'scan_type={self.lookup_type
|
|
249
|
+
c = f'scan_type={[st.value for st in self.lookup_type]}'
|
|
195
250
|
return f'ScanConfig({a}, {b}, {c})'
|
|
196
251
|
|
|
197
252
|
|
|
@@ -205,7 +260,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
205
260
|
t_cnt_isalive=64,
|
|
206
261
|
task_scan_ports=True,
|
|
207
262
|
task_scan_port_services=False,
|
|
208
|
-
lookup_type=ScanType.
|
|
263
|
+
lookup_type=[ScanType.ICMP_THEN_ARP, ScanType.ARP_LOOKUP],
|
|
209
264
|
arp_config=ArpConfig(
|
|
210
265
|
attempts=3,
|
|
211
266
|
timeout=2.5
|
|
@@ -215,6 +270,10 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
215
270
|
ping_count=2,
|
|
216
271
|
timeout=1.5,
|
|
217
272
|
retry_delay=0.5
|
|
273
|
+
),
|
|
274
|
+
arp_cache_config=ArpCacheConfig(
|
|
275
|
+
attempts=2,
|
|
276
|
+
wait_before=0.3
|
|
218
277
|
)
|
|
219
278
|
),
|
|
220
279
|
'fast': ScanConfig(
|
|
@@ -225,7 +284,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
225
284
|
t_cnt_isalive=512,
|
|
226
285
|
task_scan_ports=True,
|
|
227
286
|
task_scan_port_services=False,
|
|
228
|
-
lookup_type=ScanType.
|
|
287
|
+
lookup_type=[ScanType.POKE_THEN_ARP],
|
|
229
288
|
arp_config=ArpConfig(
|
|
230
289
|
attempts=1,
|
|
231
290
|
timeout=1.0
|
|
@@ -21,12 +21,13 @@ from tabulate import tabulate
|
|
|
21
21
|
|
|
22
22
|
# Local imports
|
|
23
23
|
from lanscape.libraries.scan_config import ScanConfig
|
|
24
|
-
from lanscape.libraries.decorators import job_tracker, terminator,
|
|
25
|
-
from lanscape.libraries.net_tools import Device
|
|
24
|
+
from lanscape.libraries.decorators import job_tracker, terminator, JobStats
|
|
25
|
+
from lanscape.libraries.net_tools import Device
|
|
26
26
|
from lanscape.libraries.errors import SubnetScanTerminationFailure
|
|
27
|
+
from lanscape.libraries.device_alive import is_device_alive
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
class SubnetScanner(
|
|
30
|
+
class SubnetScanner():
|
|
30
31
|
"""
|
|
31
32
|
Scans a subnet for devices and open ports.
|
|
32
33
|
|
|
@@ -43,6 +44,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
43
44
|
self.subnet = config.parse_subnet()
|
|
44
45
|
self.ports: List[int] = config.get_ports()
|
|
45
46
|
self.subnet_str = config.subnet
|
|
47
|
+
self.job_stats = JobStats()
|
|
46
48
|
|
|
47
49
|
# Status properties
|
|
48
50
|
self.running = False
|
|
@@ -50,11 +52,6 @@ class SubnetScanner(JobStatsMixin):
|
|
|
50
52
|
self.results = ScannerResults(self)
|
|
51
53
|
self.log: logging.Logger = logging.getLogger('SubnetScanner')
|
|
52
54
|
|
|
53
|
-
# Initial logging
|
|
54
|
-
if not is_arp_supported():
|
|
55
|
-
self.log.warning(
|
|
56
|
-
'ARP is not supported with the active runtime context. '
|
|
57
|
-
'Device discovery will be limited to ping responses.')
|
|
58
55
|
self.log.debug(f'Instantiated with uid: {self.uid}')
|
|
59
56
|
self.log.debug(
|
|
60
57
|
f'Port Count: {len(self.ports)} | Device Count: {len(self.subnet)}')
|
|
@@ -175,6 +172,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
175
172
|
t_remain = int((100 - percent) * (t_elapsed / percent)
|
|
176
173
|
) if percent else '∞'
|
|
177
174
|
buffer = f'{self.uid} - {self.subnet_str}\n'
|
|
175
|
+
buffer += f'Config: {self.cfg}\n'
|
|
178
176
|
buffer += f'Elapsed: {int(t_elapsed)} sec - Remain: {t_remain} sec\n'
|
|
179
177
|
buffer += f'Scanned: {self.results.devices_scanned}/{self.results.devices_total}'
|
|
180
178
|
buffer += f' - {percent}%\n'
|
|
@@ -239,12 +237,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
239
237
|
"""
|
|
240
238
|
Ping the given host and return True if it's reachable, False otherwise.
|
|
241
239
|
"""
|
|
242
|
-
return host.
|
|
243
|
-
host.ip,
|
|
244
|
-
scan_type=self.cfg.lookup_type,
|
|
245
|
-
ping_config=self.cfg.ping_config,
|
|
246
|
-
arp_config=self.cfg.arp_config
|
|
247
|
-
)
|
|
240
|
+
return is_device_alive(host, self.cfg)
|
|
248
241
|
|
|
249
242
|
def _set_stage(self, stage):
|
|
250
243
|
self.log.debug(f'[{self.uid}] Moving to Stage: {stage}')
|
|
@@ -272,7 +265,6 @@ class ScannerResults:
|
|
|
272
265
|
self.devices_total: int = len(list(scan.subnet))
|
|
273
266
|
self.devices_scanned: int = 0
|
|
274
267
|
self.devices: List[Device] = []
|
|
275
|
-
self.devices_alive = 0
|
|
276
268
|
|
|
277
269
|
# Status tracking
|
|
278
270
|
self.errors: List[str] = []
|
|
@@ -286,6 +278,11 @@ class ScannerResults:
|
|
|
286
278
|
self.log = logging.getLogger('ScannerResults')
|
|
287
279
|
self.log.debug(f'Instantiated Logger For Scan: {self.scan.uid}')
|
|
288
280
|
|
|
281
|
+
@property
|
|
282
|
+
def devices_alive(self):
|
|
283
|
+
"""number of alive devices found in the scan"""
|
|
284
|
+
return len(self.devices)
|
|
285
|
+
|
|
289
286
|
def scanned(self):
|
|
290
287
|
"""
|
|
291
288
|
Increment the count of scanned devices.
|
|
@@ -315,7 +312,6 @@ class ScannerResults:
|
|
|
315
312
|
"""
|
|
316
313
|
self.running = self.scan.running
|
|
317
314
|
self.run_time = int(round(time() - self.start_time, 0))
|
|
318
|
-
self.devices_alive = len(self.devices)
|
|
319
315
|
|
|
320
316
|
out = vars(self).copy()
|
|
321
317
|
out.pop('scan')
|
|
@@ -348,6 +344,7 @@ class ScannerResults:
|
|
|
348
344
|
|
|
349
345
|
# Format and return the complete buffer with table output
|
|
350
346
|
buffer = f"Scan Results - {self.scan.subnet_str} - {self.uid}\n"
|
|
347
|
+
buffer += f'Found/Scanned: {self.devices_alive}/{self.devices_scanned}\n'
|
|
351
348
|
buffer += "---------------------------------------------\n\n"
|
|
352
349
|
buffer += table
|
|
353
350
|
return buffer
|
lanscape/ui/main.py
CHANGED
|
@@ -12,7 +12,6 @@ import requests
|
|
|
12
12
|
from lanscape.libraries.logger import configure_logging
|
|
13
13
|
from lanscape.libraries.runtime_args import parse_args
|
|
14
14
|
from lanscape.libraries.web_browser import open_webapp
|
|
15
|
-
from lanscape.libraries.net_tools import is_arp_supported
|
|
16
15
|
from lanscape.libraries.version_manager import get_installed_version, is_update_available
|
|
17
16
|
from lanscape.ui.app import start_webserver_daemon, start_webserver
|
|
18
17
|
# do this so any logs generated on import are displayed
|
|
@@ -49,14 +48,6 @@ def _main():
|
|
|
49
48
|
|
|
50
49
|
args.port = get_valid_port(args.port)
|
|
51
50
|
|
|
52
|
-
if not is_arp_supported():
|
|
53
|
-
warn = (
|
|
54
|
-
'ARP is not supported, device discovery is degraded. ',
|
|
55
|
-
'For more information, see the help guide: ',
|
|
56
|
-
'https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md'
|
|
57
|
-
)
|
|
58
|
-
log.warning(''.join(warn))
|
|
59
|
-
|
|
60
51
|
try:
|
|
61
52
|
start_webserver_ui()
|
|
62
53
|
log.info('Exiting...')
|
lanscape/ui/static/css/style.css
CHANGED
|
@@ -159,24 +159,21 @@ details {
|
|
|
159
159
|
margin-bottom: 0;
|
|
160
160
|
}
|
|
161
161
|
#scan-form input {
|
|
162
|
+
margin: 0 -1px; /* sneak borders around buttons */
|
|
162
163
|
z-index: 2;
|
|
163
164
|
transition: all .2s ease-in-out;
|
|
164
165
|
border-radius: 5px !important; /* override bootstrap stuff */
|
|
165
|
-
padding-
|
|
166
|
-
margin-right: -34px;
|
|
167
|
-
margin-left: -41px;
|
|
168
|
-
padding-left: calc(41px + 5px);
|
|
166
|
+
padding-left: calc(30px + 5px);
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
|
|
170
|
+
|
|
172
171
|
#scan-form .input-group button {
|
|
173
172
|
background-color: var(--primary-bg-accent);
|
|
174
173
|
border: 0;
|
|
175
|
-
|
|
176
174
|
transition: all .2s ease-in-out;
|
|
177
175
|
height: 40px;
|
|
178
176
|
margin-top: 1px;
|
|
179
|
-
transform: translateX(.5px);
|
|
180
177
|
z-index: 3;
|
|
181
178
|
margin-left: 0;
|
|
182
179
|
}
|
|
@@ -187,13 +184,19 @@ details {
|
|
|
187
184
|
background-color: var(--primary-accent);
|
|
188
185
|
}
|
|
189
186
|
#scan-form .input-group button.start {
|
|
187
|
+
position: absolute;
|
|
188
|
+
left: 0;
|
|
190
189
|
border-radius: 5px 0 0 5px;
|
|
190
|
+
padding: 0;
|
|
191
191
|
border-right: 1px var(--border-color) solid;
|
|
192
|
-
width:
|
|
192
|
+
width: 30px; /* added because it looks gross on icon load */
|
|
193
193
|
}
|
|
194
194
|
#scan-form .input-group button.end {
|
|
195
195
|
border-radius: 0 5px 5px 0;
|
|
196
196
|
border-left: 1px var(--border-color) solid;
|
|
197
|
+
position: absolute;
|
|
198
|
+
width: 30px;
|
|
199
|
+
right: 0;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
.label-container {
|
|
@@ -333,6 +336,31 @@ details {
|
|
|
333
336
|
padding: 5px;
|
|
334
337
|
}
|
|
335
338
|
|
|
339
|
+
#advanced-modal #lookup-type-group {
|
|
340
|
+
gap: 12px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#advanced-modal .modal-body {
|
|
344
|
+
height: calc(100vh - 200px);
|
|
345
|
+
overflow-y: scroll;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#advanced-modal #lookup-type-group .form-check {
|
|
349
|
+
margin-right: 8px;
|
|
350
|
+
margin-bottom: 6px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#advanced-modal .arp-help-ico {
|
|
354
|
+
font-size: 1em;
|
|
355
|
+
vertical-align: middle;
|
|
356
|
+
margin-left: 3px;
|
|
357
|
+
color: var(--text-color);
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
opacity: 1;
|
|
360
|
+
}
|
|
361
|
+
#advanced-modal .arp-help-ico:hover {
|
|
362
|
+
color: var(--text-accent-color);
|
|
363
|
+
}
|
|
336
364
|
|
|
337
365
|
/* Card Styles */
|
|
338
366
|
.card {
|
|
@@ -758,19 +786,6 @@ html {
|
|
|
758
786
|
background-color: var(--warning-accent);
|
|
759
787
|
}
|
|
760
788
|
|
|
761
|
-
#arp-error {
|
|
762
|
-
width: calc(100% + 40px);
|
|
763
|
-
position: relative;
|
|
764
|
-
display: flex;
|
|
765
|
-
justify-content: center;
|
|
766
|
-
background-color: var(--danger-accent-transparent);
|
|
767
|
-
color: var(--text-color);
|
|
768
|
-
transform: translate3d(-20px, -20px, 0);
|
|
769
|
-
font-size: small;
|
|
770
|
-
}
|
|
771
|
-
#arp-error span {
|
|
772
|
-
text-align: center;
|
|
773
|
-
}
|
|
774
789
|
|
|
775
790
|
@media screen and (max-width: 681px) {
|
|
776
791
|
#power-button {
|
|
@@ -783,10 +798,6 @@ html {
|
|
|
783
798
|
width:98%;
|
|
784
799
|
padding: 8px;
|
|
785
800
|
}
|
|
786
|
-
#arp-error {
|
|
787
|
-
width: calc(100% + 16px);
|
|
788
|
-
transform: translate3d(-8px, -8px, 0);
|
|
789
|
-
}
|
|
790
801
|
}
|
|
791
802
|
|
|
792
803
|
@media screen and (max-width: 885px) {
|
|
@@ -13,6 +13,9 @@ $(document).ready(function() {
|
|
|
13
13
|
|
|
14
14
|
$('#t_cnt_port_scan, #t_cnt_port_test').on('input', updatePortTotals);
|
|
15
15
|
$('#ping_attempts, #ping_ping_count').on('input', updatePingTotals);
|
|
16
|
+
|
|
17
|
+
// Lookup type toggles
|
|
18
|
+
$('.lookup-type-input').on('change', onLookupTypeChanged);
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
function getScanDefaults(callback=null) {
|
|
@@ -39,7 +42,9 @@ function setScanConfig(configName) {
|
|
|
39
42
|
$('#t_cnt_isalive').val(config.t_cnt_isalive);
|
|
40
43
|
$('#task_scan_ports').prop('checked', config.task_scan_ports);
|
|
41
44
|
$('#task_scan_port_services').prop('checked', config.task_scan_port_services);
|
|
42
|
-
|
|
45
|
+
|
|
46
|
+
// lookup type (array of enum values as strings)
|
|
47
|
+
setLookupTypeUI(config.lookup_type || []);
|
|
43
48
|
|
|
44
49
|
// ping config
|
|
45
50
|
$('#ping_attempts').val(config.ping_config.attempts);
|
|
@@ -51,8 +56,21 @@ function setScanConfig(configName) {
|
|
|
51
56
|
$('#arp_attempts').val(config.arp_config.attempts);
|
|
52
57
|
$('#arp_timeout').val(config.arp_config.timeout);
|
|
53
58
|
|
|
59
|
+
// arp cache config
|
|
60
|
+
if (config.arp_cache_config) {
|
|
61
|
+
$('#arp_cache_attempts').val(config.arp_cache_config.attempts);
|
|
62
|
+
$('#arp_cache_wait_before').val(config.arp_cache_config.wait_before);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// poke config
|
|
66
|
+
if (config.poke_config) {
|
|
67
|
+
$('#poke_attempts').val(config.poke_config.attempts);
|
|
68
|
+
$('#poke_timeout').val(config.poke_config.timeout);
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
updatePortTotals();
|
|
55
72
|
updatePingTotals();
|
|
73
|
+
updateVisibility();
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function getScanConfig() {
|
|
@@ -63,7 +81,7 @@ function getScanConfig() {
|
|
|
63
81
|
t_cnt_isalive: parseInt($('#t_cnt_isalive').val()),
|
|
64
82
|
task_scan_ports: $('#task_scan_ports').is(':checked'),
|
|
65
83
|
task_scan_port_services: $('#task_scan_port_services').is(':checked'),
|
|
66
|
-
lookup_type:
|
|
84
|
+
lookup_type: getSelectedLookupTypes(),
|
|
67
85
|
ping_config: {
|
|
68
86
|
attempts: parseInt($('#ping_attempts').val()),
|
|
69
87
|
ping_count: parseInt($('#ping_ping_count').val()),
|
|
@@ -73,6 +91,14 @@ function getScanConfig() {
|
|
|
73
91
|
arp_config: {
|
|
74
92
|
attempts: parseInt($('#arp_attempts').val()),
|
|
75
93
|
timeout: parseFloat($('#arp_timeout').val())
|
|
94
|
+
},
|
|
95
|
+
arp_cache_config: {
|
|
96
|
+
attempts: parseInt($('#arp_cache_attempts').val()),
|
|
97
|
+
wait_before: parseFloat($('#arp_cache_wait_before').val())
|
|
98
|
+
},
|
|
99
|
+
poke_config: {
|
|
100
|
+
attempts: parseInt($('#poke_attempts').val()),
|
|
101
|
+
timeout: parseFloat($('#poke_timeout').val())
|
|
76
102
|
}
|
|
77
103
|
};
|
|
78
104
|
}
|
|
@@ -102,6 +128,54 @@ function updatePingTotals() {
|
|
|
102
128
|
$('#total-ping-attempts').val(attempts * count);
|
|
103
129
|
}
|
|
104
130
|
|
|
131
|
+
// Lookup type helpers
|
|
132
|
+
function setLookupTypeUI(values) {
|
|
133
|
+
const set = new Set(values || []);
|
|
134
|
+
$('.lookup-type-input').each(function() {
|
|
135
|
+
const val = $(this).val();
|
|
136
|
+
$(this).prop('checked', set.has(val));
|
|
137
|
+
});
|
|
138
|
+
updateVisibility();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getSelectedLookupTypes() {
|
|
142
|
+
const selected = [];
|
|
143
|
+
$('.lookup-type-input:checked').each(function() {
|
|
144
|
+
selected.push($(this).val());
|
|
145
|
+
});
|
|
146
|
+
return selected;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onLookupTypeChanged() {
|
|
150
|
+
updateVisibility();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updateVisibility() {
|
|
154
|
+
const types = new Set(getSelectedLookupTypes());
|
|
155
|
+
|
|
156
|
+
// Show ping if ICMP is used directly or as part of ICMP_THEN_ARP
|
|
157
|
+
const showPing = types.has('ICMP') || types.has('ICMP_THEN_ARP');
|
|
158
|
+
toggleSection('#section-ping', showPing);
|
|
159
|
+
|
|
160
|
+
// ARP active lookup (scapy) only when ARP_LOOKUP is selected
|
|
161
|
+
const showArp = types.has('ARP_LOOKUP');
|
|
162
|
+
toggleSection('#section-arp', showArp);
|
|
163
|
+
|
|
164
|
+
// ARP cache is used when we do a staged lookup that relies on cache
|
|
165
|
+
const showArpCache = types.has('ICMP_THEN_ARP') || types.has('POKE_THEN_ARP');
|
|
166
|
+
toggleSection('#section-arp-cache', showArpCache);
|
|
167
|
+
|
|
168
|
+
// Poke section only when POKE_THEN_ARP is selected
|
|
169
|
+
const showPoke = types.has('POKE_THEN_ARP');
|
|
170
|
+
toggleSection('#section-poke', showPoke);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toggleSection(selector, show) {
|
|
174
|
+
const $el = $(selector);
|
|
175
|
+
if (show) $el.removeClass('div-hide');
|
|
176
|
+
else $el.addClass('div-hide');
|
|
177
|
+
}
|
|
178
|
+
|
|
105
179
|
// expose functions globally
|
|
106
180
|
window.setScanConfig = setScanConfig;
|
|
107
181
|
window.getScanConfig = getScanConfig;
|
lanscape/ui/templates/main.html
CHANGED
|
@@ -53,13 +53,6 @@
|
|
|
53
53
|
|
|
54
54
|
<div id="content">
|
|
55
55
|
<div class="container-fluid my-4">
|
|
56
|
-
<!-- ARP Error -->
|
|
57
|
-
<div id="arp-error" class="{{ 'div-hide' if is_arp_supported else '' }}">
|
|
58
|
-
<span>
|
|
59
|
-
Unable to use ARP lookup. Device discovery is degraded.
|
|
60
|
-
<a target="_blank" href="https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md">Steps to fix</a>
|
|
61
|
-
</span>
|
|
62
|
-
</div>
|
|
63
56
|
<!-- Scan Results -->
|
|
64
57
|
<div id="scan-results" class="div-hide">
|
|
65
58
|
<div class="d-flex justify-content-between">
|
|
@@ -32,14 +32,46 @@
|
|
|
32
32
|
<h6>Device Detection</h6>
|
|
33
33
|
<div class="row mt-2">
|
|
34
34
|
<div class="col">
|
|
35
|
-
<label
|
|
36
|
-
<div class="
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
<label class="mb-1">Lookup Type</label>
|
|
36
|
+
<div id="lookup-type-group" class="d-flex flex-wrap gap-3">
|
|
37
|
+
<div class="form-check">
|
|
38
|
+
<input class="form-check-input lookup-type-input" type="checkbox" id="lookup_icmp" value="ICMP">
|
|
39
|
+
<label class="form-check-label" for="lookup_icmp">ICMP</label>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="form-check">
|
|
42
|
+
<!-- ARP lookup does not always work. Depends on host system -->
|
|
43
|
+
<input
|
|
44
|
+
class="form-check-input lookup-type-input"
|
|
45
|
+
type="checkbox"
|
|
46
|
+
id="lookup_arp_lookup"
|
|
47
|
+
value="ARP_LOOKUP"
|
|
48
|
+
{{ '' if is_arp_supported else 'disabled' }}
|
|
49
|
+
/>
|
|
50
|
+
<label class="form-check-label" for="lookup_arp_lookup">
|
|
51
|
+
ARP Lookup
|
|
52
|
+
</label>
|
|
53
|
+
{% if not is_arp_supported %}
|
|
54
|
+
<span
|
|
55
|
+
class="material-symbols-outlined arp-help-ico"
|
|
56
|
+
data-bs-toggle="tooltip"
|
|
57
|
+
data-bs-placement="top"
|
|
58
|
+
title="ARP lookup is not supported on this system, click for more info"
|
|
59
|
+
onclick="window.open('https://github.com/mdennis281/LANscape/blob/main/support/arp-issues.md', '_blank')"
|
|
60
|
+
>
|
|
61
|
+
help
|
|
62
|
+
</span>
|
|
63
|
+
{% endif %}
|
|
64
|
+
</div>
|
|
65
|
+
<div class="form-check">
|
|
66
|
+
<input class="form-check-input lookup-type-input" type="checkbox" id="lookup_icmp_then_arp" value="ICMP_THEN_ARP">
|
|
67
|
+
<label class="form-check-label" for="lookup_icmp_then_arp">ICMP then ARP Cache</label>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="form-check">
|
|
70
|
+
<input class="form-check-input lookup-type-input" type="checkbox" id="lookup_poke_then_arp" value="POKE_THEN_ARP">
|
|
71
|
+
<label class="form-check-label" for="lookup_poke_then_arp">Poke then ARP Cache</label>
|
|
72
|
+
</div>
|
|
42
73
|
</div>
|
|
74
|
+
|
|
43
75
|
</div>
|
|
44
76
|
<div class="col">
|
|
45
77
|
<label for="t_cnt_isalive">Device Lookup Threads</label>
|
|
@@ -49,7 +81,7 @@
|
|
|
49
81
|
</div>
|
|
50
82
|
|
|
51
83
|
<hr>
|
|
52
|
-
<div class="form-group mt-2">
|
|
84
|
+
<div id="section-ping" class="form-group mt-2">
|
|
53
85
|
<h6>Device Detection <span class=text-color>/</span> Ping Settings</h6>
|
|
54
86
|
<div class="row">
|
|
55
87
|
<div class="col-3">
|
|
@@ -80,8 +112,7 @@
|
|
|
80
112
|
</div>
|
|
81
113
|
|
|
82
114
|
<hr>
|
|
83
|
-
|
|
84
|
-
<div class="form-group mt-2">
|
|
115
|
+
<div id="section-arp" class="form-group mt-2">
|
|
85
116
|
<h6>Device Detection <span class=text-color>/</span> ARP Settings</h6>
|
|
86
117
|
<div class="row">
|
|
87
118
|
<div class="col">
|
|
@@ -95,6 +126,36 @@
|
|
|
95
126
|
</div>
|
|
96
127
|
</div>
|
|
97
128
|
|
|
129
|
+
<hr>
|
|
130
|
+
<div id="section-arp-cache" class="form-group mt-2 div-hide">
|
|
131
|
+
<h6>Device Detection <span class=text-color>/</span> ARP Cache Settings</h6>
|
|
132
|
+
<div class="row">
|
|
133
|
+
<div class="col">
|
|
134
|
+
<label for="arp_cache_attempts" class="form-label">Attempts</label>
|
|
135
|
+
<input type="number" id="arp_cache_attempts" class="form-control">
|
|
136
|
+
</div>
|
|
137
|
+
<div class="col">
|
|
138
|
+
<label for="arp_cache_wait_before" class="form-label">Wait Before (sec)</label>
|
|
139
|
+
<input type="number" step="0.1" id="arp_cache_wait_before" class="form-control">
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<hr>
|
|
145
|
+
<div id="section-poke" class="form-group mt-2 div-hide">
|
|
146
|
+
<h6>Device Detection <span class=text-color>/</span> Poke Settings</h6>
|
|
147
|
+
<div class="row">
|
|
148
|
+
<div class="col">
|
|
149
|
+
<label for="poke_attempts" class="form-label">Attempts</label>
|
|
150
|
+
<input type="number" id="poke_attempts" class="form-control">
|
|
151
|
+
</div>
|
|
152
|
+
<div class="col">
|
|
153
|
+
<label for="poke_timeout" class="form-label">Timeout (sec)</label>
|
|
154
|
+
<input type="number" step="0.1" id="poke_timeout" class="form-control">
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
98
159
|
<hr>
|
|
99
160
|
|
|
100
161
|
<div class="form-group mt-2">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,9 +25,28 @@ Dynamic: license-file
|
|
|
25
25
|
# LANscape
|
|
26
26
|
A python based local network scanner.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
PyPi Stats:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
Latest release:
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Health:
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
31
50
|
```sh
|
|
32
51
|
pip install lanscape
|
|
33
52
|
python -m lanscape
|
|
@@ -55,16 +74,14 @@ The program does an ARP lookup to determine the MAC address. This lookup
|
|
|
55
74
|
can sometimes require admin-level permissions to retrieve accurate results.
|
|
56
75
|
*Try elevating your shell before execution.*
|
|
57
76
|
|
|
58
|
-
### Message "WARNING: No libpcap provider available ! pcap won't be used"
|
|
59
|
-
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)
|
|
60
|
-
|
|
61
77
|
### The accuracy of the devices found is low
|
|
62
|
-
I use a combination of ARP
|
|
78
|
+
I use a combination of ARP, ICMP & port testing to determine if a device is online. Sometimes the scan settings can use some tuning to maximize both speed and accuracy.
|
|
79
|
+
|
|
63
80
|
Recommendations:
|
|
64
81
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
- Create a bug
|
|
82
|
+
- Adjust scan configuration
|
|
83
|
+
- Configure ARP lookup [ARP lookup setup](./support/arp-issues.md)
|
|
84
|
+
- Create a bug
|
|
68
85
|
|
|
69
86
|
|
|
70
87
|
### Something else
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
lanscape/__init__.py,sha256=
|
|
1
|
+
lanscape/__init__.py,sha256=YVrshFIK9GR9JrtpvJSg3BmsYcffuAdXJVxCoRCGmTE,355
|
|
2
2
|
lanscape/__main__.py,sha256=PuY42yuCLAwHrOREJ6u2DgVyGX5hZKRQeoE9pajkNfM,170
|
|
3
3
|
lanscape/libraries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
lanscape/libraries/app_scope.py,sha256=EqYQRQcbjOPPcIOhFkIepcz2cKHmoUHxWj2JSoRJmzk,2542
|
|
5
|
-
lanscape/libraries/decorators.py,sha256=
|
|
5
|
+
lanscape/libraries/decorators.py,sha256=SoNqfEInPyL-rrUKY6njK5d6Mwqu7Ix6He-BZRu2VKQ,5394
|
|
6
|
+
lanscape/libraries/device_alive.py,sha256=mwvHJp9rWV1_zfrE57aPKOsGp06fHdzrXOuGDWiZ2T8,6851
|
|
6
7
|
lanscape/libraries/errors.py,sha256=QTf42UzR9Zxj1t1mdwfLvZIp0c9a5EItELOdCR7kTmE,1322
|
|
7
8
|
lanscape/libraries/ip_parser.py,sha256=RgIEvHw_oQEcjUYOrvcpbfm4KThtH8L68WyhSOJNOfE,4201
|
|
8
9
|
lanscape/libraries/logger.py,sha256=nzo6J8UdlMdhRkOJEDOIHKztoE3Du8PQZad7ixvNgeM,2534
|
|
9
10
|
lanscape/libraries/mac_lookup.py,sha256=PxBSMe3wEVDtivCsh5NclSAguZz9rqdAS7QshBiuWvM,3519
|
|
10
|
-
lanscape/libraries/net_tools.py,sha256=
|
|
11
|
+
lanscape/libraries/net_tools.py,sha256=nIvOV_qYXmWIBYEd5PqWH1Mkadg3I1T0vbcHPkzlw6I,15853
|
|
11
12
|
lanscape/libraries/port_manager.py,sha256=3_ROOb6JEiB0NByZVtADuGcldFkgZwn1RKtvwgs9AIk,4479
|
|
12
13
|
lanscape/libraries/runtime_args.py,sha256=2vIqRrcWr-NHRSBlZGrxh1PdkPY0ytkPguu8KZqy2L8,2543
|
|
13
|
-
lanscape/libraries/scan_config.py,sha256=
|
|
14
|
+
lanscape/libraries/scan_config.py,sha256=IOtwAaiy_f1r1KcsyTUVurOmo3kUmWdquAxWh8Gv39A,8329
|
|
14
15
|
lanscape/libraries/service_scan.py,sha256=ZF4LL2gSI0T5oYEMjc8pCVtJkV1KIk4xP-AeEAkSdLc,1944
|
|
15
|
-
lanscape/libraries/subnet_scan.py,sha256=
|
|
16
|
+
lanscape/libraries/subnet_scan.py,sha256=lL2LzuxoTgPyvsdkPPEcZDqQPbCAGMHAjoX2slKGUKc,14136
|
|
16
17
|
lanscape/libraries/version_manager.py,sha256=fIAv6bVHb_VCOrqT8Do53trxwEgR2mnZV82v3YGxJ14,2944
|
|
17
18
|
lanscape/libraries/web_browser.py,sha256=23MuGIrBYdGhw6ejj6OWxwReeKIlWhtWukc1dKV_3_0,6736
|
|
18
19
|
lanscape/resources/mac_addresses/convert_csv.py,sha256=hvlyLs0XmuuhBuvXBNRGP1cKJzYVRSf8VfUJ1VqROms,1189
|
|
@@ -25,7 +26,7 @@ lanscape/resources/ports/small.json,sha256=F_lo_5xHwHBfOVfVgxP7ejblR3R62SNtC1Mm3
|
|
|
25
26
|
lanscape/resources/services/definitions.jsonc,sha256=4k7XnqIpWr1xuF7EUMgZf7y6aVDxOL_if7GGsT-JEuY,8387
|
|
26
27
|
lanscape/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
lanscape/ui/app.py,sha256=bL3K3NkGV1RY67kGHEQIWUl-Jm_MVN4WsIlsRQuK-SU,3760
|
|
28
|
-
lanscape/ui/main.py,sha256=
|
|
29
|
+
lanscape/ui/main.py,sha256=8Be2UL_ezWLpaEYYejjd8iyM2_hG1pXlkHdVjn-lySc,4196
|
|
29
30
|
lanscape/ui/shutdown_handler.py,sha256=eJC9A0MDMgLv_KQnJlUy3Kz2xCGDAf5opVdd8nBtABE,1710
|
|
30
31
|
lanscape/ui/blueprints/__init__.py,sha256=WLnPfPE06684wArgKDDLfsW-1hKFlLe75AR7uLAPlKc,270
|
|
31
32
|
lanscape/ui/blueprints/api/__init__.py,sha256=5Z4Y7B36O-bNFenpomfuNhPuJ9dW_MC0TPUU3pCFVfA,103
|
|
@@ -35,7 +36,7 @@ lanscape/ui/blueprints/api/tools.py,sha256=Dm5eAlWajr670H3O6note5hS_BZ888IGp2Rsd
|
|
|
35
36
|
lanscape/ui/blueprints/web/__init__.py,sha256=NvgnjP0X4LwqVhSEyh5RUzoG45N44kHK1MEFlfvBxTg,118
|
|
36
37
|
lanscape/ui/blueprints/web/routes.py,sha256=883puFTHePUEgwS4a2-Co6-gdLwr5NbOx0goUck7054,3571
|
|
37
38
|
lanscape/ui/static/lanscape.webmanifest,sha256=07CqA-PQsO35KJD8R96sI3Pxix6UuBjijPDCuy9vM3s,446
|
|
38
|
-
lanscape/ui/static/css/style.css,sha256=
|
|
39
|
+
lanscape/ui/static/css/style.css,sha256=O2BA0DiLlXYe_wLxM_uzrCc1VjV_SxSWQEHLCghy_0A,17598
|
|
39
40
|
lanscape/ui/static/img/ico/android-chrome-192x192.png,sha256=JmFT6KBCCuoyxMV-mLNtF9_QJbVBvfWPUizKN700fi8,18255
|
|
40
41
|
lanscape/ui/static/img/ico/android-chrome-512x512.png,sha256=88Jjx_1-4XAnZYz64KP6FdTl_kYkNG2_kQIKteQwSh4,138055
|
|
41
42
|
lanscape/ui/static/img/ico/apple-touch-icon.png,sha256=tEJlLwBZtF4v-NC90YCfRJQ2prTsF4i3VQLK_hnv2Mw,16523
|
|
@@ -48,26 +49,26 @@ lanscape/ui/static/js/layout-sizing.js,sha256=U2dsyJi-YKpOpudu3kg2whiU4047ghzDTY
|
|
|
48
49
|
lanscape/ui/static/js/main.js,sha256=zCT_4Vql-XzmoMq9fDjC1p4W8WowjIuctwNJ2ymeonI,6538
|
|
49
50
|
lanscape/ui/static/js/on-tab-close.js,sha256=3icxYWlLpY81iLoW7kQTJeWQ3UnyyboG0dESHF2wLPQ,1376
|
|
50
51
|
lanscape/ui/static/js/quietReload.js,sha256=8POH0t1KVzGCJas9fNsOVbBG-ULwZT2wiS14URgkeAU,702
|
|
51
|
-
lanscape/ui/static/js/scan-config.js,sha256=
|
|
52
|
+
lanscape/ui/static/js/scan-config.js,sha256=EOPo-0liYn3UPuhm2kaQrT7G-UumxQ25_rh1c1DaTj4,6021
|
|
52
53
|
lanscape/ui/static/js/shutdown-server.js,sha256=Mx8UGmmktHaCK7DL8TVUxah6VEcN0wwLFfhbCId-K8U,453
|
|
53
54
|
lanscape/ui/static/js/subnet-info.js,sha256=osZM6CGs-TC5QpBJWkNWCtXNOKzjyIiWKHwKi4vlDf8,559
|
|
54
55
|
lanscape/ui/static/js/subnet-selector.js,sha256=2YKCAuKU2Ti1CmJrqi4_vNTD2LQbxx7chIDqND_1eAY,358
|
|
55
56
|
lanscape/ui/templates/base.html,sha256=cnKU-kHUBpt6Xg4gPLE4dWkcXr3H-K4Vz1Im8xDKdOQ,1448
|
|
56
57
|
lanscape/ui/templates/error.html,sha256=bqGJbf_ix9wtpUlXk5zvz_XyFpeTbEO-4f0ImgLtUGk,1033
|
|
57
58
|
lanscape/ui/templates/info.html,sha256=gu8SGFUaFQc46Rm4TkZD8HM4Tnls-ZWsJ0FJNhx9wEs,2545
|
|
58
|
-
lanscape/ui/templates/main.html,sha256=
|
|
59
|
+
lanscape/ui/templates/main.html,sha256=ubxKUQcWQ27A0YpvBgj6AnIqgVSfW3exYXyn7EgrKJM,3895
|
|
59
60
|
lanscape/ui/templates/scan.html,sha256=00QX2_1S_1wGzk42r00LjEkJvoioCLs6JgjOibi6r20,376
|
|
60
61
|
lanscape/ui/templates/shutdown.html,sha256=iXVCq2yl5TjZfNFl4esbDJra3gJA2VQpae0jj4ipy9w,701
|
|
61
62
|
lanscape/ui/templates/core/head.html,sha256=eZiebt24xYd_NALe-fFL25rb4uFjUrF4XJjxFH61MgM,779
|
|
62
63
|
lanscape/ui/templates/core/scripts.html,sha256=rSRi4Ut8iejajMPhOc5bzEz-Z3EHxpj_3PxwwyyhmTQ,640
|
|
63
|
-
lanscape/ui/templates/scan/config.html,sha256=
|
|
64
|
+
lanscape/ui/templates/scan/config.html,sha256=WK0kOq7EintAvch8ospSofFLYB3qCmQ4i-xnVOj-4l0,11376
|
|
64
65
|
lanscape/ui/templates/scan/export.html,sha256=Nvs_unojzT3qhN_ZnEgYHou2C9wqWGr3dVr2UiLnYjY,749
|
|
65
66
|
lanscape/ui/templates/scan/ip-table-row.html,sha256=RANDsfW4xBATdtiLrxUlRouFSjgwetZqdzH-MJgj2mE,4740
|
|
66
67
|
lanscape/ui/templates/scan/ip-table.html,sha256=CP7AG8WHOgy3AyYCIN0wA2wO6n0H1X0F9IOncQtpPvE,914
|
|
67
68
|
lanscape/ui/templates/scan/overview.html,sha256=xWj9jWDPg2KcPLvS8fnSins23_UXjKCdb2NJwNG2U2Q,1176
|
|
68
69
|
lanscape/ui/templates/scan/scan-error.html,sha256=wmAYQ13IJHUoO8fAGNDjMvNml7tu4rsIU3Vav71ETlA,999
|
|
69
|
-
lanscape-1.3.
|
|
70
|
-
lanscape-1.3.
|
|
71
|
-
lanscape-1.3.
|
|
72
|
-
lanscape-1.3.
|
|
73
|
-
lanscape-1.3.
|
|
70
|
+
lanscape-1.3.6.dist-info/licenses/LICENSE,sha256=VLoE0IrNTIc09dFm7hMN0qzk4T3q8V0NaPcFQqMemDs,1070
|
|
71
|
+
lanscape-1.3.6.dist-info/METADATA,sha256=SF8Efe09_0l_fIyoP4BKq5nKNqPawso_zRDG6YOFgMI,3121
|
|
72
|
+
lanscape-1.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
73
|
+
lanscape-1.3.6.dist-info/top_level.txt,sha256=E9D4sjPz_6H7c85Ycy_pOS2xuv1Wm-ilKhxEprln2ps,9
|
|
74
|
+
lanscape-1.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|