lanscape 1.3.5a1__py3-none-any.whl → 1.3.6a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lanscape might be problematic. Click here for more details.
- lanscape/__init__.py +9 -1
- lanscape/libraries/app_scope.py +0 -1
- lanscape/libraries/decorators.py +26 -9
- lanscape/libraries/device_alive.py +227 -0
- lanscape/libraries/errors.py +10 -0
- lanscape/libraries/ip_parser.py +73 -1
- lanscape/libraries/logger.py +29 -1
- lanscape/libraries/mac_lookup.py +5 -0
- lanscape/libraries/net_tools.py +156 -188
- lanscape/libraries/port_manager.py +83 -0
- lanscape/libraries/scan_config.py +173 -19
- lanscape/libraries/service_scan.py +3 -3
- lanscape/libraries/subnet_scan.py +111 -26
- lanscape/libraries/version_manager.py +50 -7
- lanscape/libraries/web_browser.py +75 -58
- lanscape/resources/mac_addresses/convert_csv.py +13 -2
- lanscape/resources/ports/convert_csv.py +13 -3
- lanscape/ui/app.py +24 -6
- lanscape/ui/blueprints/__init__.py +4 -1
- lanscape/ui/blueprints/api/__init__.py +2 -0
- lanscape/ui/blueprints/api/port.py +46 -0
- lanscape/ui/blueprints/api/scan.py +57 -5
- lanscape/ui/blueprints/api/tools.py +1 -0
- lanscape/ui/blueprints/web/__init__.py +4 -0
- lanscape/ui/blueprints/web/routes.py +52 -2
- lanscape/ui/main.py +1 -10
- lanscape/ui/shutdown_handler.py +5 -1
- lanscape/ui/static/css/style.css +35 -24
- lanscape/ui/static/js/scan-config.js +76 -2
- lanscape/ui/templates/main.html +0 -7
- lanscape/ui/templates/scan/config.html +71 -10
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/METADATA +1 -1
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/RECORD +36 -35
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/WHEEL +0 -0
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration module for network scanning operations.
|
|
3
|
+
Provides classes and utilities to configure different types of network scans
|
|
4
|
+
including ping scans, ARP scans, and port scanning.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
1
8
|
from typing import List, Dict
|
|
2
9
|
import ipaddress
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
10
|
from enum import Enum
|
|
5
11
|
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
6
13
|
|
|
7
14
|
from lanscape.libraries.port_manager import PortManager
|
|
8
15
|
from lanscape.libraries.ip_parser import parse_ip_input
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
class PingConfig(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Configuration settings for ICMP ping-based network scanning.
|
|
21
|
+
|
|
22
|
+
Controls parameters such as the number of ping attempts, count per ping,
|
|
23
|
+
timeout values, and retry delays to optimize ping scanning behavior.
|
|
24
|
+
"""
|
|
12
25
|
attempts: int = 2
|
|
13
26
|
ping_count: int = 1
|
|
14
27
|
timeout: float = 1.0
|
|
@@ -16,9 +29,24 @@ class PingConfig(BaseModel):
|
|
|
16
29
|
|
|
17
30
|
@classmethod
|
|
18
31
|
def from_dict(cls, data: dict) -> 'PingConfig':
|
|
32
|
+
"""
|
|
33
|
+
Create a PingConfig instance from a dictionary.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data: Dictionary containing PingConfig parameters
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A new PingConfig instance with the provided settings
|
|
40
|
+
"""
|
|
19
41
|
return cls.model_validate(data)
|
|
20
42
|
|
|
21
43
|
def to_dict(self) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
Convert the PingConfig instance to a dictionary.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary representation of the PingConfig
|
|
49
|
+
"""
|
|
22
50
|
return self.model_dump()
|
|
23
51
|
|
|
24
52
|
def __str__(self):
|
|
@@ -39,64 +67,186 @@ class ArpConfig(BaseModel):
|
|
|
39
67
|
|
|
40
68
|
@classmethod
|
|
41
69
|
def from_dict(cls, data: dict) -> 'ArpConfig':
|
|
70
|
+
"""
|
|
71
|
+
Create an ArpConfig instance from a dictionary.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
data: Dictionary containing ArpConfig parameters
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A new ArpConfig instance with the provided settings
|
|
78
|
+
"""
|
|
42
79
|
return cls.model_validate(data)
|
|
43
80
|
|
|
44
81
|
def to_dict(self) -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Convert the ArpConfig instance to a dictionary.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary representation of the ArpConfig
|
|
87
|
+
"""
|
|
45
88
|
return self.model_dump()
|
|
46
89
|
|
|
47
90
|
def __str__(self):
|
|
48
91
|
return f'ArpCfg(timeout={self.timeout}, attempts={self.attempts})'
|
|
49
92
|
|
|
50
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
|
+
|
|
51
157
|
class ScanType(Enum):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
158
|
+
"""
|
|
159
|
+
Enumeration of supported network scan types.
|
|
160
|
+
|
|
161
|
+
PING: Uses ICMP echo requests to determine if hosts are alive
|
|
162
|
+
ARP: Uses Address Resolution Protocol to discover hosts on the local network
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
ICMP = 'ICMP'
|
|
166
|
+
ARP_LOOKUP = 'ARP_LOOKUP'
|
|
167
|
+
POKE_THEN_ARP = 'POKE_THEN_ARP'
|
|
168
|
+
ICMP_THEN_ARP = 'ICMP_THEN_ARP'
|
|
55
169
|
|
|
56
170
|
|
|
57
171
|
class ScanConfig(BaseModel):
|
|
172
|
+
"""
|
|
173
|
+
Main configuration class for network scanning operations.
|
|
174
|
+
|
|
175
|
+
Contains settings for subnet targets, port ranges, thread counts,
|
|
176
|
+
scan tasks to perform, and configurations for different scan methods.
|
|
177
|
+
"""
|
|
58
178
|
subnet: str
|
|
59
179
|
port_list: str
|
|
60
180
|
t_multiplier: float = 1.0
|
|
61
|
-
t_cnt_port_scan: int =
|
|
62
|
-
t_cnt_port_test: int =
|
|
63
|
-
t_cnt_isalive: int =
|
|
181
|
+
t_cnt_port_scan: int = os.cpu_count()
|
|
182
|
+
t_cnt_port_test: int = os.cpu_count() * 4
|
|
183
|
+
t_cnt_isalive: int = os.cpu_count() * 6
|
|
64
184
|
|
|
65
185
|
task_scan_ports: bool = True
|
|
66
186
|
# below wont run if above false
|
|
67
187
|
task_scan_port_services: bool = False # disabling until more stable
|
|
68
188
|
|
|
69
|
-
lookup_type: ScanType = ScanType.
|
|
189
|
+
lookup_type: List[ScanType] = [ScanType.ICMP_THEN_ARP]
|
|
70
190
|
|
|
71
191
|
ping_config: PingConfig = Field(default_factory=PingConfig)
|
|
72
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)
|
|
195
|
+
|
|
196
|
+
def t_cnt(self, thread_id: str) -> int:
|
|
197
|
+
"""
|
|
198
|
+
Calculate thread count for a specific operation based on multiplier.
|
|
73
199
|
|
|
74
|
-
|
|
75
|
-
|
|
200
|
+
Args:
|
|
201
|
+
thread_id: String identifier for the thread type (e.g., 'port_scan')
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Calculated thread count for the specified operation
|
|
205
|
+
"""
|
|
206
|
+
return int(int(getattr(self, f't_cnt_{thread_id}')) * float(self.t_multiplier))
|
|
76
207
|
|
|
77
208
|
@classmethod
|
|
78
209
|
def from_dict(cls, data: dict) -> 'ScanConfig':
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
210
|
+
"""
|
|
211
|
+
Create a ScanConfig instance from a dictionary.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
data: Dictionary containing ScanConfig parameters
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
A new ScanConfig instance with the provided settings
|
|
218
|
+
"""
|
|
82
219
|
|
|
83
220
|
return cls.model_validate(data)
|
|
84
221
|
|
|
85
222
|
def to_dict(self) -> dict:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
223
|
+
"""
|
|
224
|
+
Convert the ScanConfig instance to a json-serializable dictionary.
|
|
225
|
+
"""
|
|
226
|
+
return self.model_dump(mode="json")
|
|
89
227
|
|
|
90
228
|
def get_ports(self) -> List[int]:
|
|
229
|
+
"""
|
|
230
|
+
Get the list of ports to scan based on the configured port list name.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of port numbers to scan
|
|
234
|
+
"""
|
|
91
235
|
return PortManager().get_port_list(self.port_list).keys()
|
|
92
236
|
|
|
93
237
|
def parse_subnet(self) -> List[ipaddress.IPv4Network]:
|
|
238
|
+
"""
|
|
239
|
+
Parse the configured subnet string into IPv4Network objects.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of IPv4Network objects representing the target networks
|
|
243
|
+
"""
|
|
94
244
|
return parse_ip_input(self.subnet)
|
|
95
245
|
|
|
96
246
|
def __str__(self):
|
|
97
247
|
a = f'subnet={self.subnet}'
|
|
98
248
|
b = f'ports={self.port_list}'
|
|
99
|
-
c = f'scan_type={self.lookup_type
|
|
249
|
+
c = f'scan_type={[st.value for st in self.lookup_type]}'
|
|
100
250
|
return f'ScanConfig({a}, {b}, {c})'
|
|
101
251
|
|
|
102
252
|
|
|
@@ -110,7 +260,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
110
260
|
t_cnt_isalive=64,
|
|
111
261
|
task_scan_ports=True,
|
|
112
262
|
task_scan_port_services=False,
|
|
113
|
-
lookup_type=ScanType.
|
|
263
|
+
lookup_type=[ScanType.ICMP_THEN_ARP, ScanType.ARP_LOOKUP],
|
|
114
264
|
arp_config=ArpConfig(
|
|
115
265
|
attempts=3,
|
|
116
266
|
timeout=2.5
|
|
@@ -120,6 +270,10 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
120
270
|
ping_count=2,
|
|
121
271
|
timeout=1.5,
|
|
122
272
|
retry_delay=0.5
|
|
273
|
+
),
|
|
274
|
+
arp_cache_config=ArpCacheConfig(
|
|
275
|
+
attempts=2,
|
|
276
|
+
wait_before=0.3
|
|
123
277
|
)
|
|
124
278
|
),
|
|
125
279
|
'fast': ScanConfig(
|
|
@@ -130,7 +284,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
130
284
|
t_cnt_isalive=512,
|
|
131
285
|
task_scan_ports=True,
|
|
132
286
|
task_scan_port_services=False,
|
|
133
|
-
lookup_type=ScanType.
|
|
287
|
+
lookup_type=[ScanType.POKE_THEN_ARP],
|
|
134
288
|
arp_config=ArpConfig(
|
|
135
289
|
attempts=1,
|
|
136
290
|
timeout=1.0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
"""Service scanning module for identifying services running on network ports."""
|
|
1
2
|
import asyncio
|
|
2
3
|
import logging
|
|
3
4
|
import traceback
|
|
4
|
-
from .app_scope import ResourceManager
|
|
5
|
+
from lanscape.libraries.app_scope import ResourceManager
|
|
5
6
|
|
|
6
7
|
log = logging.getLogger('ServiceScan')
|
|
7
8
|
SERVICES = ResourceManager('services').get_jsonc('definitions.jsonc')
|
|
@@ -24,8 +25,7 @@ def scan_service(ip: str, port: int, timeout=10) -> str:
|
|
|
24
25
|
reader, writer = await asyncio.wait_for(asyncio.open_connection(ip, port), timeout=5)
|
|
25
26
|
|
|
26
27
|
# Send a probe appropriate for common services
|
|
27
|
-
probe = "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".
|
|
28
|
-
ip).encode("utf-8")
|
|
28
|
+
probe = f"GET / HTTP/1.1\r\nHost: {ip}\r\n\r\n".encode("utf-8")
|
|
29
29
|
writer.write(probe)
|
|
30
30
|
await writer.drain()
|
|
31
31
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
Network subnet scanning module for LANscape.
|
|
3
|
+
Provides classes for performing network discovery, device scanning, and port scanning.
|
|
4
|
+
Handles scan management, result tracking, and scan termination.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Standard library imports
|
|
3
8
|
import os
|
|
4
9
|
import json
|
|
5
10
|
import uuid
|
|
@@ -7,33 +12,46 @@ import logging
|
|
|
7
12
|
import ipaddress
|
|
8
13
|
import traceback
|
|
9
14
|
import threading
|
|
10
|
-
from time import time
|
|
11
|
-
from time import sleep
|
|
15
|
+
from time import time, sleep
|
|
12
16
|
from typing import List, Union
|
|
13
|
-
from tabulate import tabulate
|
|
14
17
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
from
|
|
19
|
+
# Third-party imports
|
|
20
|
+
from tabulate import tabulate
|
|
21
|
+
|
|
22
|
+
# Local imports
|
|
23
|
+
from lanscape.libraries.scan_config import ScanConfig
|
|
24
|
+
from lanscape.libraries.decorators import job_tracker, terminator, JobStats
|
|
25
|
+
from lanscape.libraries.net_tools import Device
|
|
26
|
+
from lanscape.libraries.errors import SubnetScanTerminationFailure
|
|
27
|
+
from lanscape.libraries.device_alive import is_device_alive
|
|
28
|
+
|
|
18
29
|
|
|
30
|
+
class SubnetScanner():
|
|
31
|
+
"""
|
|
32
|
+
Scans a subnet for devices and open ports.
|
|
33
|
+
|
|
34
|
+
Manages the scanning process including device discovery and port scanning.
|
|
35
|
+
Tracks scan progress and provides mechanisms for controlled termination.
|
|
36
|
+
"""
|
|
19
37
|
|
|
20
|
-
class SubnetScanner(JobStatsMixin):
|
|
21
38
|
def __init__(
|
|
22
39
|
self,
|
|
23
40
|
config: ScanConfig
|
|
24
41
|
):
|
|
42
|
+
# Config and network properties
|
|
25
43
|
self.cfg = config
|
|
26
44
|
self.subnet = config.parse_subnet()
|
|
27
45
|
self.ports: List[int] = config.get_ports()
|
|
28
|
-
self.running = False
|
|
29
46
|
self.subnet_str = config.subnet
|
|
47
|
+
self.job_stats = JobStats()
|
|
30
48
|
|
|
49
|
+
# Status properties
|
|
50
|
+
self.running = False
|
|
31
51
|
self.uid = str(uuid.uuid4())
|
|
32
52
|
self.results = ScannerResults(self)
|
|
33
53
|
self.log: logging.Logger = logging.getLogger('SubnetScanner')
|
|
34
|
-
|
|
35
|
-
self.log.warning(
|
|
36
|
-
'ARP is not supported with the active runtime context. Device discovery will be limited to ping responses.')
|
|
54
|
+
|
|
37
55
|
self.log.debug(f'Instantiated with uid: {self.uid}')
|
|
38
56
|
self.log.debug(
|
|
39
57
|
f'Port Count: {len(self.ports)} | Device Count: {len(self.subnet)}')
|
|
@@ -68,16 +86,36 @@ class SubnetScanner(JobStatsMixin):
|
|
|
68
86
|
return self.results
|
|
69
87
|
|
|
70
88
|
def terminate(self):
|
|
89
|
+
"""
|
|
90
|
+
Terminate the scan operation.
|
|
91
|
+
|
|
92
|
+
Attempts a graceful shutdown of all scan operations and waits for running
|
|
93
|
+
tasks to complete. Raises an exception if termination takes too long.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
bool: True if terminated successfully
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
SubnetScanTerminationFailure: If the scan cannot be terminated within the timeout
|
|
100
|
+
"""
|
|
71
101
|
self.running = False
|
|
72
102
|
self._set_stage('terminating')
|
|
73
|
-
for
|
|
74
|
-
if not
|
|
103
|
+
for _ in range(20):
|
|
104
|
+
if not self.job_stats.running:
|
|
75
105
|
self._set_stage('terminated')
|
|
76
106
|
return True
|
|
77
107
|
sleep(.5)
|
|
78
108
|
raise SubnetScanTerminationFailure(self.job_stats.running)
|
|
79
109
|
|
|
80
110
|
def calc_percent_complete(self) -> int: # 0 - 100
|
|
111
|
+
"""
|
|
112
|
+
Calculate the percentage completion of the scan.
|
|
113
|
+
|
|
114
|
+
Uses scan statistics and job timing information to estimate progress.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
int: Completion percentage (0-100)
|
|
118
|
+
"""
|
|
81
119
|
if not self.running:
|
|
82
120
|
return 100
|
|
83
121
|
|
|
@@ -85,7 +123,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
85
123
|
avg_host_detail_sec = self.job_stats.timing.get(
|
|
86
124
|
'_get_host_details', 4.5)
|
|
87
125
|
# assume 10% alive percentage if the scan just started
|
|
88
|
-
if
|
|
126
|
+
if self.results.devices and self.results.devices_scanned:
|
|
89
127
|
est_subnet_alive_percent = (
|
|
90
128
|
# avoid div 0
|
|
91
129
|
len(self.results.devices)) / (self.results.devices_scanned)
|
|
@@ -134,6 +172,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
134
172
|
t_remain = int((100 - percent) * (t_elapsed / percent)
|
|
135
173
|
) if percent else '∞'
|
|
136
174
|
buffer = f'{self.uid} - {self.subnet_str}\n'
|
|
175
|
+
buffer += f'Config: {self.cfg}\n'
|
|
137
176
|
buffer += f'Elapsed: {int(t_elapsed)} sec - Remain: {t_remain} sec\n'
|
|
138
177
|
buffer += f'Scanned: {self.results.devices_scanned}/{self.results.devices_total}'
|
|
139
178
|
buffer += f' - {percent}%\n'
|
|
@@ -198,12 +237,7 @@ class SubnetScanner(JobStatsMixin):
|
|
|
198
237
|
"""
|
|
199
238
|
Ping the given host and return True if it's reachable, False otherwise.
|
|
200
239
|
"""
|
|
201
|
-
return host.
|
|
202
|
-
host.ip,
|
|
203
|
-
scan_type=self.cfg.lookup_type,
|
|
204
|
-
ping_config=self.cfg.ping_config,
|
|
205
|
-
arp_config=self.cfg.arp_config
|
|
206
|
-
)
|
|
240
|
+
return is_device_alive(host, self.cfg)
|
|
207
241
|
|
|
208
242
|
def _set_stage(self, stage):
|
|
209
243
|
self.log.debug(f'[{self.uid}] Moving to Stage: {stage}')
|
|
@@ -213,41 +247,71 @@ class SubnetScanner(JobStatsMixin):
|
|
|
213
247
|
|
|
214
248
|
|
|
215
249
|
class ScannerResults:
|
|
250
|
+
"""
|
|
251
|
+
Stores and manages the results of a subnet scan.
|
|
252
|
+
|
|
253
|
+
Tracks devices found, scan statistics, and provides export functionality
|
|
254
|
+
for scan results. Also handles runtime calculation and progress tracking.
|
|
255
|
+
"""
|
|
256
|
+
|
|
216
257
|
def __init__(self, scan: SubnetScanner):
|
|
258
|
+
# Parent reference and identifiers
|
|
217
259
|
self.scan = scan
|
|
218
260
|
self.port_list: str = scan.cfg.port_list
|
|
219
261
|
self.subnet: str = scan.subnet_str
|
|
220
262
|
self.uid = scan.uid
|
|
221
263
|
|
|
264
|
+
# Scan statistics
|
|
222
265
|
self.devices_total: int = len(list(scan.subnet))
|
|
223
266
|
self.devices_scanned: int = 0
|
|
224
267
|
self.devices: List[Device] = []
|
|
225
268
|
|
|
269
|
+
# Status tracking
|
|
226
270
|
self.errors: List[str] = []
|
|
227
271
|
self.running: bool = False
|
|
228
272
|
self.start_time: float = time()
|
|
229
273
|
self.end_time: int = None
|
|
230
274
|
self.stage = 'instantiated'
|
|
275
|
+
self.run_time = 0
|
|
231
276
|
|
|
277
|
+
# Logging
|
|
232
278
|
self.log = logging.getLogger('ScannerResults')
|
|
233
279
|
self.log.debug(f'Instantiated Logger For Scan: {self.scan.uid}')
|
|
234
280
|
|
|
281
|
+
@property
|
|
282
|
+
def devices_alive(self):
|
|
283
|
+
"""number of alive devices found in the scan"""
|
|
284
|
+
return len(self.devices)
|
|
285
|
+
|
|
235
286
|
def scanned(self):
|
|
287
|
+
"""
|
|
288
|
+
Increment the count of scanned devices.
|
|
289
|
+
"""
|
|
236
290
|
self.devices_scanned += 1
|
|
237
291
|
|
|
238
292
|
def get_runtime(self):
|
|
293
|
+
"""
|
|
294
|
+
Calculate the runtime of the scan in seconds.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
int: Runtime in seconds
|
|
298
|
+
"""
|
|
239
299
|
if self.scan.running:
|
|
240
300
|
return int(time() - self.start_time)
|
|
241
301
|
return int(self.end_time - self.start_time)
|
|
242
302
|
|
|
243
303
|
def export(self, out_type=dict) -> Union[str, dict]:
|
|
244
304
|
"""
|
|
245
|
-
|
|
246
|
-
"""
|
|
305
|
+
Export scan results in the specified format.
|
|
247
306
|
|
|
307
|
+
Args:
|
|
308
|
+
out_type: The output type (dict or str)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Union[str, dict]: Scan results in the specified format
|
|
312
|
+
"""
|
|
248
313
|
self.running = self.scan.running
|
|
249
314
|
self.run_time = int(round(time() - self.start_time, 0))
|
|
250
|
-
self.devices_alive = len(self.devices)
|
|
251
315
|
|
|
252
316
|
out = vars(self).copy()
|
|
253
317
|
out.pop('scan')
|
|
@@ -255,9 +319,9 @@ class ScannerResults:
|
|
|
255
319
|
out['cfg'] = vars(self.scan.cfg)
|
|
256
320
|
|
|
257
321
|
devices: List[Device] = out.pop('devices')
|
|
258
|
-
|
|
322
|
+
sorted_devices = sorted(
|
|
259
323
|
devices, key=lambda obj: ipaddress.IPv4Address(obj.ip))
|
|
260
|
-
out['devices'] = [device.dict() for device in
|
|
324
|
+
out['devices'] = [device.dict() for device in sorted_devices]
|
|
261
325
|
|
|
262
326
|
if out_type == str:
|
|
263
327
|
return json.dumps(out, default=str, indent=2)
|
|
@@ -280,6 +344,7 @@ class ScannerResults:
|
|
|
280
344
|
|
|
281
345
|
# Format and return the complete buffer with table output
|
|
282
346
|
buffer = f"Scan Results - {self.scan.subnet_str} - {self.uid}\n"
|
|
347
|
+
buffer += f'Found/Scanned: {self.devices_alive}/{self.devices_scanned}\n'
|
|
283
348
|
buffer += "---------------------------------------------\n\n"
|
|
284
349
|
buffer += table
|
|
285
350
|
return buffer
|
|
@@ -304,6 +369,15 @@ class ScanManager:
|
|
|
304
369
|
self.log = logging.getLogger('ScanManager')
|
|
305
370
|
|
|
306
371
|
def new_scan(self, config: ScanConfig) -> SubnetScanner:
|
|
372
|
+
"""
|
|
373
|
+
Create and start a new scan with the given configuration.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
config: The scan configuration
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
SubnetScanner: The newly created scan instance
|
|
380
|
+
"""
|
|
307
381
|
scan = SubnetScanner(config)
|
|
308
382
|
self._start(scan)
|
|
309
383
|
self.log.info(f'Scan started - {config}')
|
|
@@ -317,6 +391,7 @@ class ScanManager:
|
|
|
317
391
|
for scan in self.scans:
|
|
318
392
|
if scan.uid == scan_id:
|
|
319
393
|
return scan
|
|
394
|
+
return None # Explicitly return None for consistency
|
|
320
395
|
|
|
321
396
|
def terminate_scans(self):
|
|
322
397
|
"""
|
|
@@ -327,12 +402,22 @@ class ScanManager:
|
|
|
327
402
|
scan.terminate()
|
|
328
403
|
|
|
329
404
|
def wait_until_complete(self, scan_id: str) -> SubnetScanner:
|
|
405
|
+
"""Wait for a scan to complete."""
|
|
330
406
|
scan = self.get_scan(scan_id)
|
|
331
407
|
while scan.running:
|
|
332
408
|
sleep(.5)
|
|
333
409
|
return scan
|
|
334
410
|
|
|
335
411
|
def _start(self, scan: SubnetScanner):
|
|
412
|
+
"""
|
|
413
|
+
Start a scan in a separate thread.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
scan: The scan to start
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Thread: The thread running the scan
|
|
420
|
+
"""
|
|
336
421
|
t = threading.Thread(target=scan.start)
|
|
337
422
|
t.start()
|
|
338
423
|
return t
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version management module for LANscape.
|
|
3
|
+
Handles version checking, update detection, and retrieving package information
|
|
4
|
+
from both local installation and PyPI repository.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import logging
|
|
2
|
-
import requests
|
|
3
8
|
import traceback
|
|
4
9
|
from importlib.metadata import version, PackageNotFoundError
|
|
5
10
|
from random import randint
|
|
6
11
|
|
|
12
|
+
import requests
|
|
13
|
+
|
|
7
14
|
from .app_scope import is_local_run
|
|
8
15
|
|
|
9
16
|
log = logging.getLogger('VersionManager')
|
|
@@ -11,10 +18,23 @@ log = logging.getLogger('VersionManager')
|
|
|
11
18
|
PACKAGE = 'lanscape'
|
|
12
19
|
LOCAL_VERSION = '0.0.0'
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
# Used to cache PyPI version during runtime
|
|
22
|
+
LATEST_VERSION = None
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
def is_update_available(package=PACKAGE) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check if an update is available for the package.
|
|
28
|
+
|
|
29
|
+
Compares the installed version with the latest version available on PyPI.
|
|
30
|
+
Ignores pre-release versions (alpha/beta) and local development installs.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
package: The package name to check for updates
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Boolean indicating if an update is available
|
|
37
|
+
"""
|
|
18
38
|
installed = get_installed_version(package)
|
|
19
39
|
available = lookup_latest_version(package)
|
|
20
40
|
|
|
@@ -30,23 +50,46 @@ def is_update_available(package=PACKAGE) -> bool:
|
|
|
30
50
|
|
|
31
51
|
|
|
32
52
|
def lookup_latest_version(package=PACKAGE):
|
|
53
|
+
"""
|
|
54
|
+
Retrieve the latest version of the package from PyPI.
|
|
55
|
+
|
|
56
|
+
Caches the result for subsequent calls during the same runtime.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
package: The package name to lookup
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The latest version string from PyPI or None if retrieval fails
|
|
63
|
+
"""
|
|
33
64
|
# Fetch the latest version from PyPI
|
|
34
|
-
global
|
|
35
|
-
if not
|
|
65
|
+
global LATEST_VERSION # pylint: disable=global-statement
|
|
66
|
+
if not LATEST_VERSION:
|
|
36
67
|
no_cache = f'?cachebust={randint(0, 6969)}'
|
|
37
68
|
url = f"https://pypi.org/pypi/{package}/json{no_cache}"
|
|
38
69
|
try:
|
|
39
70
|
response = requests.get(url, timeout=5)
|
|
40
71
|
response.raise_for_status() # Raise an exception for HTTP errors
|
|
41
|
-
|
|
42
|
-
log.debug(f'Latest pypi version: {
|
|
72
|
+
LATEST_VERSION = response.json()['info']['version']
|
|
73
|
+
log.debug(f'Latest pypi version: {LATEST_VERSION}')
|
|
43
74
|
except BaseException:
|
|
44
75
|
log.debug(traceback.format_exc())
|
|
45
76
|
log.warning('Unable to fetch package version from PyPi')
|
|
46
|
-
return
|
|
77
|
+
return LATEST_VERSION
|
|
47
78
|
|
|
48
79
|
|
|
49
80
|
def get_installed_version(package=PACKAGE):
|
|
81
|
+
"""
|
|
82
|
+
Get the installed version of the package.
|
|
83
|
+
|
|
84
|
+
Returns the current installed version or a default local version
|
|
85
|
+
if running in development mode or if the package is not found.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
package: The package name to check
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The installed version string or LOCAL_VERSION for local development
|
|
92
|
+
"""
|
|
50
93
|
if not is_local_run():
|
|
51
94
|
try:
|
|
52
95
|
return version(package)
|