lanscape 1.3.8a1__py3-none-any.whl → 2.4.0a2__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 +8 -4
- lanscape/{libraries → core}/app_scope.py +21 -3
- lanscape/core/decorators.py +231 -0
- lanscape/{libraries → core}/device_alive.py +83 -16
- lanscape/{libraries → core}/ip_parser.py +2 -26
- lanscape/{libraries → core}/net_tools.py +209 -66
- lanscape/{libraries → core}/runtime_args.py +6 -0
- lanscape/{libraries → core}/scan_config.py +103 -5
- lanscape/core/service_scan.py +222 -0
- lanscape/{libraries → core}/subnet_scan.py +30 -14
- lanscape/{libraries → core}/version_manager.py +15 -17
- lanscape/resources/ports/test_port_list_scan.json +4 -0
- lanscape/resources/services/definitions.jsonc +576 -400
- lanscape/ui/app.py +17 -5
- lanscape/ui/blueprints/__init__.py +1 -1
- lanscape/ui/blueprints/api/port.py +15 -1
- lanscape/ui/blueprints/api/scan.py +1 -1
- lanscape/ui/blueprints/api/tools.py +4 -4
- lanscape/ui/blueprints/web/routes.py +29 -2
- lanscape/ui/main.py +46 -19
- lanscape/ui/shutdown_handler.py +2 -2
- lanscape/ui/static/css/style.css +186 -20
- lanscape/ui/static/js/core.js +14 -0
- lanscape/ui/static/js/main.js +30 -2
- lanscape/ui/static/js/quietReload.js +3 -0
- lanscape/ui/static/js/scan-config.js +56 -6
- lanscape/ui/templates/base.html +6 -8
- lanscape/ui/templates/core/head.html +1 -1
- lanscape/ui/templates/info.html +20 -5
- lanscape/ui/templates/main.html +33 -36
- lanscape/ui/templates/scan/config.html +214 -176
- lanscape/ui/templates/scan/device-detail.html +111 -0
- lanscape/ui/templates/scan/ip-table-row.html +17 -83
- lanscape/ui/templates/scan/ip-table.html +5 -5
- lanscape/ui/ws/__init__.py +31 -0
- lanscape/ui/ws/delta.py +170 -0
- lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape/ui/ws/handlers/base.py +145 -0
- lanscape/ui/ws/handlers/port.py +184 -0
- lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape/ui/ws/protocol.py +86 -0
- lanscape/ui/ws/server.py +375 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
- lanscape-2.4.0a2.dist-info/RECORD +85 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
- lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
- lanscape/libraries/decorators.py +0 -170
- lanscape/libraries/service_scan.py +0 -50
- lanscape/libraries/web_browser.py +0 -210
- lanscape-1.3.8a1.dist-info/RECORD +0 -74
- /lanscape/{libraries → core}/__init__.py +0 -0
- /lanscape/{libraries → core}/errors.py +0 -0
- /lanscape/{libraries → core}/logger.py +0 -0
- /lanscape/{libraries → core}/mac_lookup.py +0 -0
- /lanscape/{libraries → core}/port_manager.py +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
lanscape/ui/ws/server.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket server for LANscape.
|
|
3
|
+
|
|
4
|
+
Provides an async WebSocket server that can run independently of the Flask UI.
|
|
5
|
+
Handles client connections, message routing, and real-time scan updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import websockets
|
|
15
|
+
from websockets.server import WebSocketServerProtocol
|
|
16
|
+
|
|
17
|
+
from lanscape.ui.ws.protocol import (
|
|
18
|
+
WSRequest,
|
|
19
|
+
WSResponse,
|
|
20
|
+
WSError,
|
|
21
|
+
WSEvent
|
|
22
|
+
)
|
|
23
|
+
from lanscape.ui.ws.handlers import (
|
|
24
|
+
ScanHandler,
|
|
25
|
+
PortHandler,
|
|
26
|
+
ToolsHandler
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class WebSocketServer:
|
|
30
|
+
"""
|
|
31
|
+
Async WebSocket server for LANscape.
|
|
32
|
+
|
|
33
|
+
Provides a standalone WebSocket interface to all LANscape functionality.
|
|
34
|
+
Supports real-time scan updates via subscriptions.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
DEFAULT_HOST = 'localhost'
|
|
38
|
+
DEFAULT_PORT = 8766
|
|
39
|
+
|
|
40
|
+
def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the WebSocket server.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
host: Host to bind to (default: 0.0.0.0)
|
|
46
|
+
port: Port to listen on (default: 8766)
|
|
47
|
+
"""
|
|
48
|
+
self.host = host
|
|
49
|
+
self.port = port
|
|
50
|
+
self.log = logging.getLogger('WebSocketServer')
|
|
51
|
+
|
|
52
|
+
# Initialize handlers
|
|
53
|
+
self._scan_handler = ScanHandler()
|
|
54
|
+
self._port_handler = PortHandler()
|
|
55
|
+
self._tools_handler = ToolsHandler()
|
|
56
|
+
|
|
57
|
+
self._handlers = [
|
|
58
|
+
self._scan_handler,
|
|
59
|
+
self._port_handler,
|
|
60
|
+
self._tools_handler
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Active connections
|
|
64
|
+
self._clients: dict[str, WebSocketServerProtocol] = {}
|
|
65
|
+
|
|
66
|
+
# Track scans that were running (to detect completion)
|
|
67
|
+
self._previously_running_scans: set[str] = set()
|
|
68
|
+
|
|
69
|
+
# Server instance
|
|
70
|
+
self._server = None
|
|
71
|
+
self._running = False
|
|
72
|
+
|
|
73
|
+
# Background tasks
|
|
74
|
+
self._update_task: Optional[asyncio.Task] = None
|
|
75
|
+
|
|
76
|
+
def get_actions(self) -> list[str]:
|
|
77
|
+
"""
|
|
78
|
+
Get all supported actions.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of all action names supported by all handlers
|
|
82
|
+
"""
|
|
83
|
+
actions = []
|
|
84
|
+
for handler in self._handlers:
|
|
85
|
+
actions.extend(handler.get_actions())
|
|
86
|
+
return actions
|
|
87
|
+
|
|
88
|
+
async def start(self) -> None:
|
|
89
|
+
"""Start the WebSocket server."""
|
|
90
|
+
self.log.info(f"Starting WebSocket server on ws://{self.host}:{self.port}")
|
|
91
|
+
|
|
92
|
+
self._running = True
|
|
93
|
+
|
|
94
|
+
# Minimal WebSocket server configuration - let the library handle everything
|
|
95
|
+
self._server = await websockets.serve(
|
|
96
|
+
self._handle_connection,
|
|
97
|
+
self.host,
|
|
98
|
+
self.port
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Start the background update task
|
|
102
|
+
self._update_task = asyncio.create_task(self._broadcast_scan_updates())
|
|
103
|
+
|
|
104
|
+
self.log.info("WebSocket server started")
|
|
105
|
+
|
|
106
|
+
async def stop(self) -> None:
|
|
107
|
+
"""Stop the WebSocket server."""
|
|
108
|
+
self.log.info("Stopping WebSocket server...")
|
|
109
|
+
self._running = False
|
|
110
|
+
|
|
111
|
+
if self._update_task:
|
|
112
|
+
self._update_task.cancel()
|
|
113
|
+
try:
|
|
114
|
+
await self._update_task
|
|
115
|
+
except asyncio.CancelledError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
if self._server:
|
|
119
|
+
self._server.close()
|
|
120
|
+
await self._server.wait_closed()
|
|
121
|
+
|
|
122
|
+
# Close all client connections
|
|
123
|
+
for client_id, ws in list(self._clients.items()):
|
|
124
|
+
try:
|
|
125
|
+
await ws.close()
|
|
126
|
+
except Exception as e:
|
|
127
|
+
self.log.debug(f"Error closing client {client_id}: {e}")
|
|
128
|
+
|
|
129
|
+
self._clients.clear()
|
|
130
|
+
self.log.info("WebSocket server stopped")
|
|
131
|
+
|
|
132
|
+
async def serve_forever(self) -> None:
|
|
133
|
+
"""Run the server until stopped."""
|
|
134
|
+
await self.start()
|
|
135
|
+
try:
|
|
136
|
+
await self._server.wait_closed()
|
|
137
|
+
except asyncio.CancelledError:
|
|
138
|
+
await self.stop()
|
|
139
|
+
|
|
140
|
+
async def _handle_connection(
|
|
141
|
+
self,
|
|
142
|
+
websocket: WebSocketServerProtocol
|
|
143
|
+
) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Handle a new WebSocket connection.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
websocket: The WebSocket connection
|
|
149
|
+
"""
|
|
150
|
+
client_id = str(uuid.uuid4())
|
|
151
|
+
self._clients[client_id] = websocket
|
|
152
|
+
self.log.info(f"Client connected: {client_id}")
|
|
153
|
+
|
|
154
|
+
# Send welcome message with client_id
|
|
155
|
+
await self._send_event(
|
|
156
|
+
websocket,
|
|
157
|
+
'connection.established',
|
|
158
|
+
{'client_id': client_id, 'actions': self.get_actions()}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
async for message in websocket:
|
|
163
|
+
await self._handle_message(client_id, websocket, message)
|
|
164
|
+
except websockets.ConnectionClosed:
|
|
165
|
+
self.log.info(f"Client disconnected: {client_id}")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.log.error(f"Error handling client {client_id}: {e}")
|
|
168
|
+
finally:
|
|
169
|
+
self._cleanup_client(client_id)
|
|
170
|
+
|
|
171
|
+
async def _handle_message(
|
|
172
|
+
self,
|
|
173
|
+
client_id: str,
|
|
174
|
+
websocket: WebSocketServerProtocol,
|
|
175
|
+
message: str
|
|
176
|
+
) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Handle an incoming WebSocket message.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
client_id: The client identifier
|
|
182
|
+
websocket: The WebSocket connection
|
|
183
|
+
message: The raw message string
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(message)
|
|
187
|
+
request = WSRequest.model_validate(data)
|
|
188
|
+
except json.JSONDecodeError as e:
|
|
189
|
+
error = WSError(error=f"Invalid JSON: {e}")
|
|
190
|
+
await self._send(websocket, error)
|
|
191
|
+
return
|
|
192
|
+
except Exception as e:
|
|
193
|
+
error = WSError(error=f"Invalid request format: {e}")
|
|
194
|
+
await self._send(websocket, error)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self.log.debug(f"[{client_id}] Request: {request.action}")
|
|
198
|
+
|
|
199
|
+
# Find the appropriate handler
|
|
200
|
+
response = None
|
|
201
|
+
for handler in self._handlers:
|
|
202
|
+
if handler.can_handle(request.action):
|
|
203
|
+
# Create a send_event callback for this client
|
|
204
|
+
async def send_event(event: str, data: dict) -> None:
|
|
205
|
+
await self._send_event(websocket, event, data)
|
|
206
|
+
|
|
207
|
+
response = await handler.handle(request, send_event)
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
if response is None:
|
|
211
|
+
response = WSError(
|
|
212
|
+
id=request.id,
|
|
213
|
+
action=request.action,
|
|
214
|
+
error=f"Unknown action: {request.action}. "
|
|
215
|
+
f"Available actions: {self.get_actions()}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
await self._send(websocket, response)
|
|
219
|
+
|
|
220
|
+
async def _send(
|
|
221
|
+
self,
|
|
222
|
+
websocket: WebSocketServerProtocol,
|
|
223
|
+
message: WSResponse | WSError | WSEvent
|
|
224
|
+
) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Send a message to a client.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
websocket: The WebSocket connection
|
|
230
|
+
message: The message to send
|
|
231
|
+
"""
|
|
232
|
+
try:
|
|
233
|
+
await websocket.send(message.model_dump_json())
|
|
234
|
+
except websockets.ConnectionClosed:
|
|
235
|
+
pass
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.log.error(f"Error sending message: {e}")
|
|
238
|
+
|
|
239
|
+
async def _send_event(
|
|
240
|
+
self,
|
|
241
|
+
websocket: WebSocketServerProtocol,
|
|
242
|
+
event: str,
|
|
243
|
+
data: dict
|
|
244
|
+
) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Send an event to a client.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
websocket: The WebSocket connection
|
|
250
|
+
event: The event name
|
|
251
|
+
data: The event data
|
|
252
|
+
"""
|
|
253
|
+
message = WSEvent(event=event, data=data)
|
|
254
|
+
await self._send(websocket, message)
|
|
255
|
+
|
|
256
|
+
async def _broadcast_scan_updates(self) -> None:
|
|
257
|
+
"""
|
|
258
|
+
Background task to broadcast scan updates to subscribed clients.
|
|
259
|
+
|
|
260
|
+
Sends delta updates every 500ms for active scans.
|
|
261
|
+
"""
|
|
262
|
+
while self._running:
|
|
263
|
+
try:
|
|
264
|
+
await asyncio.sleep(0.5)
|
|
265
|
+
await self._send_updates_for_active_scans()
|
|
266
|
+
except asyncio.CancelledError:
|
|
267
|
+
break
|
|
268
|
+
except Exception as e:
|
|
269
|
+
self.log.error(f"Error in broadcast loop: {e}")
|
|
270
|
+
|
|
271
|
+
async def _send_updates_for_active_scans(self) -> None:
|
|
272
|
+
"""Send delta updates for all active scans to subscribed clients."""
|
|
273
|
+
# pylint: disable=protected-access
|
|
274
|
+
currently_running = set()
|
|
275
|
+
|
|
276
|
+
for scan in self._scan_handler._scan_manager.scans:
|
|
277
|
+
if scan.running:
|
|
278
|
+
currently_running.add(scan.uid)
|
|
279
|
+
await self._send_scan_update_to_subscribers(scan)
|
|
280
|
+
elif scan.uid in self._previously_running_scans:
|
|
281
|
+
# Scan just completed - send final update with complete event
|
|
282
|
+
await self._send_scan_complete_to_subscribers(scan)
|
|
283
|
+
|
|
284
|
+
# Update tracking set
|
|
285
|
+
self._previously_running_scans = currently_running
|
|
286
|
+
|
|
287
|
+
async def _send_scan_complete_to_subscribers(self, scan) -> None:
|
|
288
|
+
"""Send scan complete event to all subscribed clients."""
|
|
289
|
+
subscribed_clients = self._scan_handler.get_subscriptions(scan.uid)
|
|
290
|
+
|
|
291
|
+
for client_id in subscribed_clients:
|
|
292
|
+
websocket = self._clients.get(client_id)
|
|
293
|
+
if websocket is None:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
# Send final delta with all remaining changes
|
|
298
|
+
# pylint: disable=protected-access
|
|
299
|
+
delta = self._scan_handler._handle_get_delta(
|
|
300
|
+
{'scan_id': scan.uid, 'client_id': client_id},
|
|
301
|
+
None
|
|
302
|
+
)
|
|
303
|
+
# Force the complete stage in metadata
|
|
304
|
+
if 'metadata' in delta:
|
|
305
|
+
delta['metadata']['running'] = False
|
|
306
|
+
delta['metadata']['stage'] = 'complete'
|
|
307
|
+
|
|
308
|
+
await self._send_event(websocket, 'scan.complete', delta)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
self.log.debug(f"Error sending complete to {client_id}: {e}")
|
|
311
|
+
|
|
312
|
+
async def _send_scan_update_to_subscribers(self, scan) -> None:
|
|
313
|
+
"""Send scan update to all subscribed clients."""
|
|
314
|
+
subscribed_clients = self._scan_handler.get_subscriptions(scan.uid)
|
|
315
|
+
|
|
316
|
+
for client_id in subscribed_clients:
|
|
317
|
+
websocket = self._clients.get(client_id)
|
|
318
|
+
if websocket is None:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
await self._try_send_delta_update(websocket, scan.uid, client_id)
|
|
322
|
+
|
|
323
|
+
async def _try_send_delta_update(
|
|
324
|
+
self,
|
|
325
|
+
websocket: WebSocketServerProtocol,
|
|
326
|
+
scan_id: str,
|
|
327
|
+
client_id: str
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Try to send a delta update to a client."""
|
|
330
|
+
try:
|
|
331
|
+
# pylint: disable=protected-access
|
|
332
|
+
delta = self._scan_handler._handle_get_delta(
|
|
333
|
+
{'scan_id': scan_id, 'client_id': client_id},
|
|
334
|
+
None
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if delta.get('has_changes'):
|
|
338
|
+
await self._send_event(websocket, 'scan.update', delta)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.log.debug(f"Error sending update to {client_id}: {e}")
|
|
341
|
+
|
|
342
|
+
def _cleanup_client(self, client_id: str) -> None:
|
|
343
|
+
"""
|
|
344
|
+
Clean up resources for a disconnected client.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
client_id: The client identifier
|
|
348
|
+
"""
|
|
349
|
+
self._clients.pop(client_id, None)
|
|
350
|
+
self._scan_handler.cleanup_client(client_id)
|
|
351
|
+
self.log.debug(f"Cleaned up client: {client_id}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def run_server(host: str = WebSocketServer.DEFAULT_HOST,
|
|
355
|
+
port: int = WebSocketServer.DEFAULT_PORT) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Run the WebSocket server.
|
|
358
|
+
|
|
359
|
+
This is a convenience function to start the server synchronously.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
host: Host to bind to
|
|
363
|
+
port: Port to listen on
|
|
364
|
+
"""
|
|
365
|
+
server = WebSocketServer(host, port)
|
|
366
|
+
asyncio.run(server.serve_forever())
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == '__main__':
|
|
370
|
+
# Configure logging when run directly
|
|
371
|
+
logging.basicConfig(
|
|
372
|
+
level=logging.INFO,
|
|
373
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
374
|
+
)
|
|
375
|
+
run_server()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.4.0a2
|
|
4
4
|
Summary: A python based local network scanner
|
|
5
5
|
Author-email: Michael Dennis <michael@dipduo.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,8 +8,13 @@ Project-URL: Homepage, https://github.com/mdennis281/py-lanscape
|
|
|
8
8
|
Project-URL: Issues, https://github.com/mdennis281/py-lanscape/issues
|
|
9
9
|
Keywords: network,scanner,lan,local,python
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
16
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.10
|
|
13
18
|
Description-Content-Type: text/markdown
|
|
14
19
|
License-File: LICENSE
|
|
15
20
|
Requires-Dist: Flask<5.0,>=3.0
|
|
@@ -20,6 +25,16 @@ Requires-Dist: scapy<3.0,>=2.3.2
|
|
|
20
25
|
Requires-Dist: tabulate==0.9.0
|
|
21
26
|
Requires-Dist: pydantic
|
|
22
27
|
Requires-Dist: icmplib
|
|
28
|
+
Requires-Dist: pwa-launcher>=1.1.0
|
|
29
|
+
Requires-Dist: websockets<14.0,>=12.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
35
|
+
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pylint>=3.0; extra == "dev"
|
|
37
|
+
Requires-Dist: autopep8>=2.0; extra == "dev"
|
|
23
38
|
Dynamic: license-file
|
|
24
39
|
|
|
25
40
|
# LANscape
|
|
@@ -80,7 +95,7 @@ I use a combination of ARP, ICMP & port testing to determine if a device is onli
|
|
|
80
95
|
Recommendations:
|
|
81
96
|
|
|
82
97
|
- Adjust scan configuration
|
|
83
|
-
- Configure ARP lookup [ARP lookup setup](./
|
|
98
|
+
- Configure ARP lookup [ARP lookup setup](./docs/arp-issues.md)
|
|
84
99
|
- Create a bug
|
|
85
100
|
|
|
86
101
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
lanscape/__init__.py,sha256=ibc4hZ6Esm7fsqocLRpc2v30BVWrKpFQ-iMJisoDtL8,423
|
|
2
|
+
lanscape/__main__.py,sha256=PuY42yuCLAwHrOREJ6u2DgVyGX5hZKRQeoE9pajkNfM,170
|
|
3
|
+
lanscape/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
lanscape/core/app_scope.py,sha256=qfzX8Ed4bFdxHMGjgnLlWuLZDTCBKObermz91KbGVn0,3298
|
|
5
|
+
lanscape/core/decorators.py,sha256=CZbPEfnLS1OF-uejQweetadzqf0pVo736jKko4Xs-g4,7264
|
|
6
|
+
lanscape/core/device_alive.py,sha256=VY2dsoy6_MWUxcysZEFcsSoCtDkLMYzwqy0U_sbVWE0,9609
|
|
7
|
+
lanscape/core/errors.py,sha256=QTf42UzR9Zxj1t1mdwfLvZIp0c9a5EItELOdCR7kTmE,1322
|
|
8
|
+
lanscape/core/ip_parser.py,sha256=kn5H4ERitLnreRAqifWphwbxdjItGqwu50lsMCPDMcA,3474
|
|
9
|
+
lanscape/core/logger.py,sha256=nzo6J8UdlMdhRkOJEDOIHKztoE3Du8PQZad7ixvNgeM,2534
|
|
10
|
+
lanscape/core/mac_lookup.py,sha256=PxBSMe3wEVDtivCsh5NclSAguZz9rqdAS7QshBiuWvM,3519
|
|
11
|
+
lanscape/core/net_tools.py,sha256=PsyH-I6ylrkJaVas_YUttYPhU2Cs8I5uCLibPzXxuoM,22076
|
|
12
|
+
lanscape/core/port_manager.py,sha256=3_ROOb6JEiB0NByZVtADuGcldFkgZwn1RKtvwgs9AIk,4479
|
|
13
|
+
lanscape/core/runtime_args.py,sha256=vZNDqb75hr3OQccgJip3XtYYljwa1tIzXQ5PdzHeIDg,2865
|
|
14
|
+
lanscape/core/scan_config.py,sha256=A2ZKXqXKW9nrP6yLb7b9b3XqSY_cQB3LZ5K0LVCSebE,11114
|
|
15
|
+
lanscape/core/service_scan.py,sha256=umDVOoCNNVJhWMtxDV-rn7hcS7t_V073srSMPTl5gMo,7943
|
|
16
|
+
lanscape/core/subnet_scan.py,sha256=PtSOk92dK05-reyr8LBkOXaI15qpYnar5nDqALCX1tQ,14850
|
|
17
|
+
lanscape/core/version_manager.py,sha256=eGjyKgsv31QO0W26se9pPQ1TwmEN8qn37dHULtoocqc,2841
|
|
18
|
+
lanscape/resources/mac_addresses/convert_csv.py,sha256=hvlyLs0XmuuhBuvXBNRGP1cKJzYVRSf8VfUJ1VqROms,1189
|
|
19
|
+
lanscape/resources/mac_addresses/mac_db.json,sha256=Lng2yJerwO7vjefzxzgtE203hry1lIsCadHL1A5Rptg,2136137
|
|
20
|
+
lanscape/resources/ports/convert_csv.py,sha256=MMLDa-5pGMsn4A2_k45jHsRYffrRY_0Z2D1ziiikeQA,1143
|
|
21
|
+
lanscape/resources/ports/full.json,sha256=O8XBW52QvEVSGMQDbXe4-c4qq6XAecw6KJW4m2HkTLo,1441444
|
|
22
|
+
lanscape/resources/ports/large.json,sha256=CzlCcIGCBW1QAgjz4NDerCYA8HcYf6lNxehh7F928y0,138410
|
|
23
|
+
lanscape/resources/ports/medium.json,sha256=T5Rc7wa47MtroHxuZrHSftOqRWbQzhZULJdE1vpsTvU,3518
|
|
24
|
+
lanscape/resources/ports/small.json,sha256=F_lo_5xHwHBfOVfVgxP7ejblR3R62SNtC1Mm33brhYc,376
|
|
25
|
+
lanscape/resources/ports/test_port_list_scan.json,sha256=qXuWGQ_sGIRCVrhJxMeWcHKYdjaMv8O6OVWutiqCwVo,36
|
|
26
|
+
lanscape/resources/services/definitions.jsonc,sha256=M9BDeK-mh25sEVj8xDEYbU2ix7EETVWhbiYmMb14Gjg,20905
|
|
27
|
+
lanscape/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
lanscape/ui/app.py,sha256=rg4UGHgbVHpU2jdabSwBoSqGna7WGOunPkPc5Tvds9w,4076
|
|
29
|
+
lanscape/ui/main.py,sha256=cL3og5eO-SdyX-ev-DhcuvO7htn5u50TtRTGv2HeK-A,5047
|
|
30
|
+
lanscape/ui/shutdown_handler.py,sha256=HrEnWrdYSzLDVsPgD8tf9FgtAwQrZNECDu6wnEs27EY,1704
|
|
31
|
+
lanscape/ui/blueprints/__init__.py,sha256=EjPtaR5Nh17pGiOVYTJULVNaZntpFZOiYyep8rBWAiE,265
|
|
32
|
+
lanscape/ui/blueprints/api/__init__.py,sha256=5Z4Y7B36O-bNFenpomfuNhPuJ9dW_MC0TPUU3pCFVfA,103
|
|
33
|
+
lanscape/ui/blueprints/api/port.py,sha256=enqJ87hpVHSTBoaV9bJ7IgsvHrWDTga704wV33dtwUg,2384
|
|
34
|
+
lanscape/ui/blueprints/api/scan.py,sha256=2rsW4xkI4X2Q2ocwaE469aU1VxQ3xHuBRjD9xE36WdI,3326
|
|
35
|
+
lanscape/ui/blueprints/api/tools.py,sha256=jr9gt0VhvBFgJ61MLgNIM6hin-MUmJLGdmStPx3e3Yc,2432
|
|
36
|
+
lanscape/ui/blueprints/web/__init__.py,sha256=NvgnjP0X4LwqVhSEyh5RUzoG45N44kHK1MEFlfvBxTg,118
|
|
37
|
+
lanscape/ui/blueprints/web/routes.py,sha256=f5TzfTzelJ_erslyBXTOpFr4BlIfB1Mb1ye6ioH7IL0,4534
|
|
38
|
+
lanscape/ui/static/lanscape.webmanifest,sha256=07CqA-PQsO35KJD8R96sI3Pxix6UuBjijPDCuy9vM3s,446
|
|
39
|
+
lanscape/ui/static/css/style.css,sha256=UWu0fOCVhNKRtKawaCy4hp-dU5Q-ac2WiSdIwRQMvFs,21442
|
|
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
|
|
46
|
+
lanscape/ui/static/img/ico/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
47
|
+
lanscape/ui/static/js/core.js,sha256=TzyoD1NCnIY-2JLArymw0JoMOq0CMeJrZDmZyMdMK2I,1394
|
|
48
|
+
lanscape/ui/static/js/layout-sizing.js,sha256=U2dsyJi-YKpOpudu3kg2whiU4047ghzDTY3ExYUhpPs,810
|
|
49
|
+
lanscape/ui/static/js/main.js,sha256=Bgb5Ld_UPWzBVzxWkLD0kJGjOoLJyzni2TKE6Uk99VA,7428
|
|
50
|
+
lanscape/ui/static/js/on-tab-close.js,sha256=3icxYWlLpY81iLoW7kQTJeWQ3UnyyboG0dESHF2wLPQ,1376
|
|
51
|
+
lanscape/ui/static/js/quietReload.js,sha256=T7lvxVejMGJr1cKdABedyiaTVukZfRDIKV7Eyv-jeyc,802
|
|
52
|
+
lanscape/ui/static/js/scan-config.js,sha256=nQ2dWkgpf8yer-lCMTZ25DBqOuWt0SIyzzfdP_fS-bE,8086
|
|
53
|
+
lanscape/ui/static/js/shutdown-server.js,sha256=Mx8UGmmktHaCK7DL8TVUxah6VEcN0wwLFfhbCId-K8U,453
|
|
54
|
+
lanscape/ui/static/js/subnet-info.js,sha256=osZM6CGs-TC5QpBJWkNWCtXNOKzjyIiWKHwKi4vlDf8,559
|
|
55
|
+
lanscape/ui/static/js/subnet-selector.js,sha256=2YKCAuKU2Ti1CmJrqi4_vNTD2LQbxx7chIDqND_1eAY,358
|
|
56
|
+
lanscape/ui/templates/base.html,sha256=HIHLFEUMDTGEaeuSpeNg-vkXXb9VNbZnbFWlYetVL18,1377
|
|
57
|
+
lanscape/ui/templates/error.html,sha256=bqGJbf_ix9wtpUlXk5zvz_XyFpeTbEO-4f0ImgLtUGk,1033
|
|
58
|
+
lanscape/ui/templates/info.html,sha256=SQ6RpTs9_v9HF32mr3FBsh6vTJneYqFz_WrC9diXzHg,2958
|
|
59
|
+
lanscape/ui/templates/main.html,sha256=CVBwJCNBjzOTFDCiIicT1wuptjCUfZzMc7YSvu4OXxA,3626
|
|
60
|
+
lanscape/ui/templates/scan.html,sha256=00QX2_1S_1wGzk42r00LjEkJvoioCLs6JgjOibi6r20,376
|
|
61
|
+
lanscape/ui/templates/shutdown.html,sha256=iXVCq2yl5TjZfNFl4esbDJra3gJA2VQpae0jj4ipy9w,701
|
|
62
|
+
lanscape/ui/templates/core/head.html,sha256=zP1RkTYuaKCC6RtnSEHFKPw3wKWfSyV0HZg5XsAxWik,719
|
|
63
|
+
lanscape/ui/templates/core/scripts.html,sha256=rSRi4Ut8iejajMPhOc5bzEz-Z3EHxpj_3PxwwyyhmTQ,640
|
|
64
|
+
lanscape/ui/templates/scan/config.html,sha256=vI5ZuJLZE5FwbTRkQyXwIRP-bwe_cZdSyzDZVZu1s_w,14821
|
|
65
|
+
lanscape/ui/templates/scan/device-detail.html,sha256=3N0WcdnWopbSFwsnKogBaHOYsLMAfKBZdkP7HQG4vLA,4794
|
|
66
|
+
lanscape/ui/templates/scan/export.html,sha256=Nvs_unojzT3qhN_ZnEgYHou2C9wqWGr3dVr2UiLnYjY,749
|
|
67
|
+
lanscape/ui/templates/scan/ip-table-row.html,sha256=viAvjJcye3jYpDsWGR2To8BGqr1NxuF3gPO2ECQnxUU,1302
|
|
68
|
+
lanscape/ui/templates/scan/ip-table.html,sha256=8pkaStlVeGJ3h6spk8eKu3Ft5HffH7KpHD3vjECyHa0,977
|
|
69
|
+
lanscape/ui/templates/scan/overview.html,sha256=xWj9jWDPg2KcPLvS8fnSins23_UXjKCdb2NJwNG2U2Q,1176
|
|
70
|
+
lanscape/ui/templates/scan/scan-error.html,sha256=wmAYQ13IJHUoO8fAGNDjMvNml7tu4rsIU3Vav71ETlA,999
|
|
71
|
+
lanscape/ui/ws/__init__.py,sha256=kwvrI6WAgxRMToI3NAHdDph4FtbvR7cWODh4WrXHoxg,688
|
|
72
|
+
lanscape/ui/ws/delta.py,sha256=sMmjG8darYosdOds1Sdl2MqwqG44P6Ypb27FJnMz0-E,4798
|
|
73
|
+
lanscape/ui/ws/protocol.py,sha256=i6ULVg-hNyXvKRUKEDJHa2_RfMunieEU8VIYTNXxfD0,2135
|
|
74
|
+
lanscape/ui/ws/server.py,sha256=-rAiWOmPS5UOy5TA9eTAd0oBGLM988qW9b5SWqQNuso,11734
|
|
75
|
+
lanscape/ui/ws/handlers/__init__.py,sha256=D3m4CvaVl-TURkBqADmLaEoBvGcHSIRnFzsBcwXkUy8,544
|
|
76
|
+
lanscape/ui/ws/handlers/base.py,sha256=Uu0FUBSG4F_2SsqORushu132EF_6mo-7uYhJTNu9SpI,4051
|
|
77
|
+
lanscape/ui/ws/handlers/port.py,sha256=0bFFP_BtoHbmg0O5iaCKtVW08GNcrlc3DMhVUixaGT0,5397
|
|
78
|
+
lanscape/ui/ws/handlers/scan.py,sha256=OniNOp-RmmkhlcOpnll4BVYOWnxyAJH7VP2owImsAhk,10977
|
|
79
|
+
lanscape/ui/ws/handlers/tools.py,sha256=Un24y8YqRUHzr2KW4S7IG5Tdl3nu8hHoxVDF8E3H6yM,4533
|
|
80
|
+
lanscape-2.4.0a2.dist-info/licenses/LICENSE,sha256=VLoE0IrNTIc09dFm7hMN0qzk4T3q8V0NaPcFQqMemDs,1070
|
|
81
|
+
lanscape-2.4.0a2.dist-info/METADATA,sha256=ruSveDjVkHapd_gA5CNPTxIwFFj5sznyl4HZ2iS31Bw,3793
|
|
82
|
+
lanscape-2.4.0a2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
83
|
+
lanscape-2.4.0a2.dist-info/entry_points.txt,sha256=evxSxUikFa1OEd4e0Boky9sLH87HdgM0YqB_AbB2HYc,51
|
|
84
|
+
lanscape-2.4.0a2.dist-info/top_level.txt,sha256=E9D4sjPz_6H7c85Ycy_pOS2xuv1Wm-ilKhxEprln2ps,9
|
|
85
|
+
lanscape-2.4.0a2.dist-info/RECORD,,
|
lanscape/libraries/decorators.py
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
Decorators and job tracking utilities for Lanscape.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from time import time
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import DefaultDict
|
|
9
|
-
from collections import defaultdict
|
|
10
|
-
import inspect
|
|
11
|
-
import functools
|
|
12
|
-
import concurrent.futures
|
|
13
|
-
from tabulate import tabulate
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass
|
|
17
|
-
class JobStats:
|
|
18
|
-
"""
|
|
19
|
-
Tracks statistics for job execution, including running, finished, and timing data.
|
|
20
|
-
"""
|
|
21
|
-
running: DefaultDict[str, int] = field(
|
|
22
|
-
default_factory=lambda: defaultdict(int))
|
|
23
|
-
finished: DefaultDict[str, int] = field(
|
|
24
|
-
default_factory=lambda: defaultdict(int))
|
|
25
|
-
timing: DefaultDict[str, float] = field(
|
|
26
|
-
default_factory=lambda: defaultdict(float))
|
|
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
|
-
|
|
42
|
-
def __str__(self):
|
|
43
|
-
"""Return a formatted string representation of the job statistics."""
|
|
44
|
-
data = [
|
|
45
|
-
[
|
|
46
|
-
name,
|
|
47
|
-
self.running.get(name, 0),
|
|
48
|
-
self.finished.get(name, 0),
|
|
49
|
-
self.timing.get(name, 0.0)
|
|
50
|
-
]
|
|
51
|
-
for name in set(self.running) | set(self.finished)
|
|
52
|
-
]
|
|
53
|
-
headers = ["Function", "Running", "Finished", "Avg Time (s)"]
|
|
54
|
-
return tabulate(
|
|
55
|
-
data,
|
|
56
|
-
headers=headers,
|
|
57
|
-
tablefmt="grid"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class JobStatsMixin: # pylint: disable=too-few-public-methods
|
|
62
|
-
"""
|
|
63
|
-
Singleton mixin that provides shared job_stats property across all instances.
|
|
64
|
-
"""
|
|
65
|
-
_job_stats = None
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def job_stats(self):
|
|
69
|
-
"""Return the shared JobStats instance."""
|
|
70
|
-
return JobStats()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def job_tracker(func):
|
|
74
|
-
"""
|
|
75
|
-
Decorator to track job statistics for a method,
|
|
76
|
-
including running count, finished count, and average timing.
|
|
77
|
-
"""
|
|
78
|
-
def get_fxn_src_name(func, first_arg) -> str:
|
|
79
|
-
"""
|
|
80
|
-
Return the function name with the class name prepended if available.
|
|
81
|
-
"""
|
|
82
|
-
qual_parts = func.__qualname__.split(".")
|
|
83
|
-
cls_name = qual_parts[-2] if len(qual_parts) > 1 else None
|
|
84
|
-
cls_obj = None # resolved lazily
|
|
85
|
-
if cls_obj is None and cls_name:
|
|
86
|
-
mod = inspect.getmodule(func)
|
|
87
|
-
cls_obj = getattr(mod, cls_name, None)
|
|
88
|
-
if cls_obj and first_arg is not None:
|
|
89
|
-
if (first_arg is cls_obj or isinstance(first_arg, cls_obj)):
|
|
90
|
-
return f"{cls_name}.{func.__name__}"
|
|
91
|
-
return func.__name__
|
|
92
|
-
|
|
93
|
-
def wrapper(*args, **kwargs):
|
|
94
|
-
"""Wrap the function to update job statistics before and after execution."""
|
|
95
|
-
class_instance = args[0]
|
|
96
|
-
job_stats = JobStats()
|
|
97
|
-
fxn = get_fxn_src_name(
|
|
98
|
-
func,
|
|
99
|
-
class_instance
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Increment running counter and track execution time
|
|
103
|
-
job_stats.running[fxn] += 1
|
|
104
|
-
start = time()
|
|
105
|
-
|
|
106
|
-
result = func(*args, **kwargs) # Execute the wrapped function
|
|
107
|
-
|
|
108
|
-
# Update statistics after function execution
|
|
109
|
-
elapsed = time() - start
|
|
110
|
-
job_stats.running[fxn] -= 1
|
|
111
|
-
job_stats.finished[fxn] += 1
|
|
112
|
-
|
|
113
|
-
# Calculate the new average timing for the function
|
|
114
|
-
job_stats.timing[fxn] = round(
|
|
115
|
-
((job_stats.finished[fxn] - 1) * job_stats.timing[fxn] + elapsed)
|
|
116
|
-
/ job_stats.finished[fxn],
|
|
117
|
-
4
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# Clean up if no more running instances of this function
|
|
121
|
-
if job_stats.running[fxn] == 0:
|
|
122
|
-
job_stats.running.pop(fxn)
|
|
123
|
-
|
|
124
|
-
return result
|
|
125
|
-
|
|
126
|
-
return wrapper
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def terminator(func):
|
|
130
|
-
"""
|
|
131
|
-
Decorator designed specifically for the SubnetScanner class,
|
|
132
|
-
helps facilitate termination of a job.
|
|
133
|
-
"""
|
|
134
|
-
def wrapper(*args, **kwargs):
|
|
135
|
-
"""Wrap the function to check if the scan is running before execution."""
|
|
136
|
-
scan = args[0] # aka self
|
|
137
|
-
if not scan.running:
|
|
138
|
-
return None
|
|
139
|
-
return func(*args, **kwargs)
|
|
140
|
-
|
|
141
|
-
return wrapper
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def timeout_enforcer(timeout: int, raise_on_timeout: bool = True):
|
|
145
|
-
"""
|
|
146
|
-
Decorator to enforce a timeout on a function.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
timeout (int): Timeout length in seconds.
|
|
150
|
-
raise_on_timeout (bool): Whether to raise an exception if the timeout is exceeded.
|
|
151
|
-
"""
|
|
152
|
-
def decorator(func):
|
|
153
|
-
@functools.wraps(func)
|
|
154
|
-
def wrapper(*args, **kwargs):
|
|
155
|
-
"""Wrap the function to enforce a timeout on its execution."""
|
|
156
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
157
|
-
future = executor.submit(func, *args, **kwargs)
|
|
158
|
-
try:
|
|
159
|
-
return future.result(
|
|
160
|
-
timeout=timeout
|
|
161
|
-
)
|
|
162
|
-
except concurrent.futures.TimeoutError as exc:
|
|
163
|
-
if raise_on_timeout:
|
|
164
|
-
raise TimeoutError(
|
|
165
|
-
f"Function '{func.__name__}' exceeded timeout of "
|
|
166
|
-
f"{timeout} seconds."
|
|
167
|
-
) from exc
|
|
168
|
-
return None # Return None if not raising an exception
|
|
169
|
-
return wrapper
|
|
170
|
-
return decorator
|