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.

Files changed (58) hide show
  1. lanscape/__init__.py +8 -4
  2. lanscape/{libraries → core}/app_scope.py +21 -3
  3. lanscape/core/decorators.py +231 -0
  4. lanscape/{libraries → core}/device_alive.py +83 -16
  5. lanscape/{libraries → core}/ip_parser.py +2 -26
  6. lanscape/{libraries → core}/net_tools.py +209 -66
  7. lanscape/{libraries → core}/runtime_args.py +6 -0
  8. lanscape/{libraries → core}/scan_config.py +103 -5
  9. lanscape/core/service_scan.py +222 -0
  10. lanscape/{libraries → core}/subnet_scan.py +30 -14
  11. lanscape/{libraries → core}/version_manager.py +15 -17
  12. lanscape/resources/ports/test_port_list_scan.json +4 -0
  13. lanscape/resources/services/definitions.jsonc +576 -400
  14. lanscape/ui/app.py +17 -5
  15. lanscape/ui/blueprints/__init__.py +1 -1
  16. lanscape/ui/blueprints/api/port.py +15 -1
  17. lanscape/ui/blueprints/api/scan.py +1 -1
  18. lanscape/ui/blueprints/api/tools.py +4 -4
  19. lanscape/ui/blueprints/web/routes.py +29 -2
  20. lanscape/ui/main.py +46 -19
  21. lanscape/ui/shutdown_handler.py +2 -2
  22. lanscape/ui/static/css/style.css +186 -20
  23. lanscape/ui/static/js/core.js +14 -0
  24. lanscape/ui/static/js/main.js +30 -2
  25. lanscape/ui/static/js/quietReload.js +3 -0
  26. lanscape/ui/static/js/scan-config.js +56 -6
  27. lanscape/ui/templates/base.html +6 -8
  28. lanscape/ui/templates/core/head.html +1 -1
  29. lanscape/ui/templates/info.html +20 -5
  30. lanscape/ui/templates/main.html +33 -36
  31. lanscape/ui/templates/scan/config.html +214 -176
  32. lanscape/ui/templates/scan/device-detail.html +111 -0
  33. lanscape/ui/templates/scan/ip-table-row.html +17 -83
  34. lanscape/ui/templates/scan/ip-table.html +5 -5
  35. lanscape/ui/ws/__init__.py +31 -0
  36. lanscape/ui/ws/delta.py +170 -0
  37. lanscape/ui/ws/handlers/__init__.py +20 -0
  38. lanscape/ui/ws/handlers/base.py +145 -0
  39. lanscape/ui/ws/handlers/port.py +184 -0
  40. lanscape/ui/ws/handlers/scan.py +352 -0
  41. lanscape/ui/ws/handlers/tools.py +145 -0
  42. lanscape/ui/ws/protocol.py +86 -0
  43. lanscape/ui/ws/server.py +375 -0
  44. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
  45. lanscape-2.4.0a2.dist-info/RECORD +85 -0
  46. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
  47. lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
  48. lanscape/libraries/decorators.py +0 -170
  49. lanscape/libraries/service_scan.py +0 -50
  50. lanscape/libraries/web_browser.py +0 -210
  51. lanscape-1.3.8a1.dist-info/RECORD +0 -74
  52. /lanscape/{libraries → core}/__init__.py +0 -0
  53. /lanscape/{libraries → core}/errors.py +0 -0
  54. /lanscape/{libraries → core}/logger.py +0 -0
  55. /lanscape/{libraries → core}/mac_lookup.py +0 -0
  56. /lanscape/{libraries → core}/port_manager.py +0 -0
  57. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
  58. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,352 @@
1
+ """
2
+ WebSocket handler for network scanning operations.
3
+
4
+ Provides handlers for:
5
+ - Starting scans (threaded and synchronous)
6
+ - Getting scan results (full and delta)
7
+ - Getting scan summary/status
8
+ - Terminating scans
9
+ - Subscribing to real-time scan updates
10
+ """
11
+
12
+ import asyncio
13
+ import json
14
+ import logging
15
+ from typing import Any, Callable, Optional
16
+
17
+ from lanscape.core.subnet_scan import ScanManager, ScanConfig
18
+ from lanscape.ui.ws.handlers.base import BaseHandler
19
+ from lanscape.ui.ws.delta import ScanDeltaTracker
20
+
21
+
22
+ class ScanHandler(BaseHandler):
23
+ """
24
+ Handler for scan-related WebSocket actions.
25
+
26
+ Supports actions:
27
+ - scan.start: Start a new scan (non-blocking)
28
+ - scan.start_sync: Start a scan and wait for completion
29
+ - scan.get: Get full scan results
30
+ - scan.get_delta: Get only changed results since last request
31
+ - scan.summary: Get scan summary/progress
32
+ - scan.terminate: Stop a running scan
33
+ - scan.subscribe: Subscribe to real-time scan updates
34
+ - scan.unsubscribe: Unsubscribe from scan updates
35
+ """
36
+
37
+ def __init__(self, scan_manager: Optional[ScanManager] = None):
38
+ """
39
+ Initialize the scan handler.
40
+
41
+ Args:
42
+ scan_manager: Optional ScanManager instance. If not provided,
43
+ uses the singleton instance.
44
+ """
45
+ super().__init__()
46
+ self._scan_manager = scan_manager or ScanManager()
47
+ self._delta_trackers: dict[str, ScanDeltaTracker] = {}
48
+ self._subscriptions: dict[str, set] = {} # scan_id -> set of client_ids
49
+ self.log = logging.getLogger('ScanHandler')
50
+
51
+ # Register handlers
52
+ self.register('start', self._handle_start)
53
+ self.register('start_sync', self._handle_start_sync)
54
+ self.register('get', self._handle_get)
55
+ self.register('get_delta', self._handle_get_delta)
56
+ self.register('summary', self._handle_summary)
57
+ self.register('terminate', self._handle_terminate)
58
+ self.register('subscribe', self._handle_subscribe)
59
+ self.register('unsubscribe', self._handle_unsubscribe)
60
+ self.register('list', self._handle_list)
61
+
62
+ @property
63
+ def prefix(self) -> str:
64
+ """Return the action prefix for this handler."""
65
+ return 'scan'
66
+
67
+ def _handle_start(
68
+ self,
69
+ params: dict[str, Any],
70
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
71
+ ) -> dict:
72
+ """
73
+ Start a new network scan.
74
+
75
+ Params:
76
+ subnet: Target subnet to scan
77
+ port_list: Name of the port list to use
78
+ ... (other ScanConfig parameters)
79
+
80
+ Returns:
81
+ Dict with scan_id and status
82
+ """
83
+ config = ScanConfig.from_dict(params)
84
+ scan = self._scan_manager.new_scan(config)
85
+ self.log.info(f"Started scan {scan.uid} for {config.subnet}")
86
+
87
+ return {
88
+ 'scan_id': scan.uid,
89
+ 'status': 'running'
90
+ }
91
+
92
+ async def _handle_start_sync(
93
+ self,
94
+ params: dict[str, Any],
95
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
96
+ ) -> dict:
97
+ """
98
+ Start a scan and wait for completion.
99
+
100
+ Params:
101
+ Same as _handle_start
102
+
103
+ Returns:
104
+ Dict with scan_id and status='complete'
105
+ """
106
+ config = ScanConfig.from_dict(params)
107
+ scan = self._scan_manager.new_scan(config)
108
+ self.log.info(f"Started sync scan {scan.uid} for {config.subnet}")
109
+
110
+ # Wait for completion in a non-blocking way
111
+ while scan.running:
112
+ await asyncio.sleep(0.5)
113
+
114
+ return {
115
+ 'scan_id': scan.uid,
116
+ 'status': 'complete'
117
+ }
118
+
119
+ def _handle_get(
120
+ self,
121
+ params: dict[str, Any],
122
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
123
+ ) -> dict:
124
+ """
125
+ Get full scan results.
126
+
127
+ Params:
128
+ scan_id: The scan ID to retrieve
129
+
130
+ Returns:
131
+ Full scan results as dict
132
+ """
133
+ scan_id = self._get_param(params, 'scan_id', required=True)
134
+ scan = self._scan_manager.get_scan(scan_id)
135
+
136
+ if scan is None:
137
+ raise ValueError(f"Scan not found: {scan_id}")
138
+
139
+ return json.loads(scan.results.export(str))
140
+
141
+ def _handle_get_delta(
142
+ self,
143
+ params: dict[str, Any],
144
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
145
+ ) -> dict:
146
+ """
147
+ Get only changed scan results since last request.
148
+
149
+ Params:
150
+ scan_id: The scan ID to retrieve
151
+ client_id: Client identifier for tracking deltas
152
+
153
+ Returns:
154
+ Delta update containing only changed devices and metadata
155
+ """
156
+ scan_id = self._get_param(params, 'scan_id', required=True)
157
+ client_id = self._get_param(params, 'client_id', default='default')
158
+
159
+ scan = self._scan_manager.get_scan(scan_id)
160
+ if scan is None:
161
+ raise ValueError(f"Scan not found: {scan_id}")
162
+
163
+ # Get or create delta tracker for this client
164
+ tracker_key = f"{scan_id}_{client_id}"
165
+ if tracker_key not in self._delta_trackers:
166
+ self._delta_trackers[tracker_key] = ScanDeltaTracker()
167
+
168
+ tracker = self._delta_trackers[tracker_key]
169
+ full_results = json.loads(scan.results.export(str))
170
+ delta = tracker.get_scan_delta(full_results)
171
+
172
+ # Add scan status info
173
+ delta['scan_id'] = scan_id
174
+ delta['running'] = scan.running
175
+
176
+ # Add calculated progress from backend (more accurate than simple host count)
177
+ if delta.get('metadata') is None:
178
+ delta['metadata'] = {}
179
+ delta['metadata']['percent_complete'] = scan.calc_percent_complete()
180
+
181
+ return delta
182
+
183
+ def _handle_summary(
184
+ self,
185
+ params: dict[str, Any],
186
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
187
+ ) -> dict:
188
+ """
189
+ Get scan summary/progress.
190
+
191
+ Params:
192
+ scan_id: The scan ID to get summary for
193
+
194
+ Returns:
195
+ Dict with running status, percent complete, stage, runtime, and device counts
196
+ """
197
+ scan_id = self._get_param(params, 'scan_id', required=True)
198
+ scan = self._scan_manager.get_scan(scan_id)
199
+
200
+ if scan is None:
201
+ raise ValueError(f"Scan not found: {scan_id}")
202
+
203
+ return {
204
+ 'scan_id': scan_id,
205
+ 'running': scan.running,
206
+ 'percent_complete': scan.calc_percent_complete(),
207
+ 'stage': scan.results.stage,
208
+ 'runtime': scan.results.get_runtime(),
209
+ 'devices': {
210
+ 'scanned': scan.results.devices_scanned,
211
+ 'alive': len(scan.results.devices),
212
+ 'total': scan.results.devices_total
213
+ }
214
+ }
215
+
216
+ def _handle_terminate(
217
+ self,
218
+ params: dict[str, Any],
219
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
220
+ ) -> dict:
221
+ """
222
+ Terminate a running scan.
223
+
224
+ Params:
225
+ scan_id: The scan ID to terminate
226
+
227
+ Returns:
228
+ Dict with success status
229
+ """
230
+ scan_id = self._get_param(params, 'scan_id', required=True)
231
+ scan = self._scan_manager.get_scan(scan_id)
232
+
233
+ if scan is None:
234
+ raise ValueError(f"Scan not found: {scan_id}")
235
+
236
+ scan.terminate()
237
+ self.log.info(f"Terminated scan {scan_id}")
238
+
239
+ return {'success': True, 'scan_id': scan_id}
240
+
241
+ def _handle_subscribe(
242
+ self,
243
+ params: dict[str, Any],
244
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
245
+ ) -> dict:
246
+ """
247
+ Subscribe to real-time scan updates.
248
+
249
+ Params:
250
+ scan_id: The scan ID to subscribe to
251
+ client_id: Client identifier for the subscription
252
+
253
+ Returns:
254
+ Dict with subscription confirmation
255
+ """
256
+ scan_id = self._get_param(params, 'scan_id', required=True)
257
+ client_id = self._get_param(params, 'client_id', required=True)
258
+
259
+ scan = self._scan_manager.get_scan(scan_id)
260
+ if scan is None:
261
+ raise ValueError(f"Scan not found: {scan_id}")
262
+
263
+ if scan_id not in self._subscriptions:
264
+ self._subscriptions[scan_id] = set()
265
+ self._subscriptions[scan_id].add(client_id)
266
+
267
+ self.log.debug(f"Client {client_id} subscribed to scan {scan_id}")
268
+
269
+ return {
270
+ 'subscribed': True,
271
+ 'scan_id': scan_id,
272
+ 'client_id': client_id
273
+ }
274
+
275
+ def _handle_unsubscribe(
276
+ self,
277
+ params: dict[str, Any],
278
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
279
+ ) -> dict:
280
+ """
281
+ Unsubscribe from scan updates.
282
+
283
+ Params:
284
+ scan_id: The scan ID to unsubscribe from
285
+ client_id: Client identifier for the subscription
286
+
287
+ Returns:
288
+ Dict with unsubscription confirmation
289
+ """
290
+ scan_id = self._get_param(params, 'scan_id', required=True)
291
+ client_id = self._get_param(params, 'client_id', required=True)
292
+
293
+ if scan_id in self._subscriptions:
294
+ self._subscriptions[scan_id].discard(client_id)
295
+
296
+ # Clean up delta tracker
297
+ tracker_key = f"{scan_id}_{client_id}"
298
+ self._delta_trackers.pop(tracker_key, None)
299
+
300
+ self.log.debug(f"Client {client_id} unsubscribed from scan {scan_id}")
301
+
302
+ return {
303
+ 'unsubscribed': True,
304
+ 'scan_id': scan_id,
305
+ 'client_id': client_id
306
+ }
307
+
308
+ def _handle_list(
309
+ self,
310
+ params: dict[str, Any], # pylint: disable=unused-argument
311
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
312
+ ) -> list:
313
+ """
314
+ List all scans.
315
+
316
+ Returns:
317
+ List of scan summaries
318
+ """
319
+ scans = []
320
+ for scan in self._scan_manager.scans:
321
+ scans.append({
322
+ 'scan_id': scan.uid,
323
+ 'subnet': scan.subnet_str,
324
+ 'running': scan.running,
325
+ 'stage': scan.results.stage,
326
+ 'devices_found': len(scan.results.devices)
327
+ })
328
+ return scans
329
+
330
+ def get_subscriptions(self, scan_id: str) -> set:
331
+ """
332
+ Get all client IDs subscribed to a scan.
333
+
334
+ Args:
335
+ scan_id: The scan ID
336
+
337
+ Returns:
338
+ Set of client IDs
339
+ """
340
+ return self._subscriptions.get(scan_id, set())
341
+
342
+ def cleanup_client(self, client_id: str) -> None:
343
+ """
344
+ Clean up all subscriptions for a client.
345
+
346
+ Args:
347
+ client_id: The client ID to clean up
348
+ """
349
+ for scan_id in list(self._subscriptions.keys()):
350
+ self._subscriptions[scan_id].discard(client_id)
351
+ tracker_key = f"{scan_id}_{client_id}"
352
+ self._delta_trackers.pop(tracker_key, None)
@@ -0,0 +1,145 @@
1
+ """
2
+ WebSocket handler for utility tools.
3
+
4
+ Provides handlers for:
5
+ - Subnet validation and listing
6
+ - Default configuration retrieval
7
+ """
8
+
9
+ import traceback
10
+ from typing import Any, Callable, Optional
11
+
12
+ from lanscape.core.net_tools import get_all_network_subnets, is_arp_supported
13
+ from lanscape.core.ip_parser import parse_ip_input
14
+ from lanscape.core.errors import SubnetTooLargeError
15
+ from lanscape.core.scan_config import DEFAULT_CONFIGS
16
+ from lanscape.ui.ws.handlers.base import BaseHandler
17
+
18
+
19
+ class ToolsHandler(BaseHandler):
20
+ """
21
+ Handler for utility tool WebSocket actions.
22
+
23
+ Supports actions:
24
+ - tools.subnet_test: Validate a subnet string
25
+ - tools.subnet_list: List all network subnets on the system
26
+ - tools.config_defaults: Get default scan configurations
27
+ - tools.arp_supported: Check if ARP is supported on this system
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize the tools handler."""
32
+ super().__init__()
33
+
34
+ # Register handlers
35
+ self.register('subnet_test', self._handle_subnet_test)
36
+ self.register('subnet_list', self._handle_subnet_list)
37
+ self.register('config_defaults', self._handle_config_defaults)
38
+ self.register('arp_supported', self._handle_arp_supported)
39
+
40
+ @property
41
+ def prefix(self) -> str:
42
+ """Return the action prefix for this handler."""
43
+ return 'tools'
44
+
45
+ def _handle_subnet_test(
46
+ self,
47
+ params: dict[str, Any],
48
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
49
+ ) -> dict:
50
+ """
51
+ Validate a subnet string.
52
+
53
+ Params:
54
+ subnet: The subnet string to validate
55
+
56
+ Returns:
57
+ Dict with 'valid', 'msg', and 'count' fields
58
+ """
59
+ subnet = self._get_param(params, 'subnet')
60
+
61
+ if not subnet:
62
+ return {'valid': False, 'msg': 'Subnet cannot be blank', 'count': -1}
63
+
64
+ try:
65
+ ips = parse_ip_input(subnet)
66
+ length = len(ips)
67
+ return {
68
+ 'valid': True,
69
+ 'msg': f"{length} IP{'s' if length > 1 else ''}",
70
+ 'count': length
71
+ }
72
+ except SubnetTooLargeError:
73
+ return {
74
+ 'valid': False,
75
+ 'msg': 'subnet too large',
76
+ 'error': traceback.format_exc(),
77
+ 'count': -1
78
+ }
79
+ except Exception:
80
+ return {
81
+ 'valid': False,
82
+ 'msg': 'invalid subnet',
83
+ 'error': traceback.format_exc(),
84
+ 'count': -1
85
+ }
86
+
87
+ def _handle_subnet_list(
88
+ self,
89
+ params: dict[str, Any], # pylint: disable=unused-argument
90
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
91
+ ) -> list | dict:
92
+ """
93
+ List all network subnets on the system.
94
+
95
+ Returns:
96
+ List of subnet information or error dict
97
+ """
98
+ try:
99
+ return get_all_network_subnets()
100
+ except Exception:
101
+ return {'error': traceback.format_exc()}
102
+
103
+ def _handle_config_defaults(
104
+ self,
105
+ params: dict[str, Any], # pylint: disable=unused-argument
106
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
107
+ ) -> dict:
108
+ """
109
+ Get default scan configurations.
110
+
111
+ Adjusts presets that rely on ARP_LOOKUP when ARP is not supported.
112
+
113
+ Returns:
114
+ Dict of preset name -> ScanConfig dict
115
+ """
116
+ arp_supported = is_arp_supported()
117
+
118
+ configs = {}
119
+ for key, config in DEFAULT_CONFIGS.items():
120
+ config_dict = config.to_dict()
121
+
122
+ if not arp_supported:
123
+ lookup_types = list(config_dict.get('lookup_type') or [])
124
+ if 'ARP_LOOKUP' in lookup_types:
125
+ lookup_types = [lt for lt in lookup_types if lt != 'ARP_LOOKUP']
126
+ if 'POKE_THEN_ARP' not in lookup_types:
127
+ lookup_types.append('POKE_THEN_ARP')
128
+ config_dict['lookup_type'] = lookup_types
129
+
130
+ configs[key] = config_dict
131
+
132
+ return configs
133
+
134
+ def _handle_arp_supported(
135
+ self,
136
+ params: dict[str, Any], # pylint: disable=unused-argument
137
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
138
+ ) -> dict:
139
+ """
140
+ Check if ARP is supported on this system.
141
+
142
+ Returns:
143
+ Dict with 'supported' boolean
144
+ """
145
+ return {'supported': is_arp_supported()}
@@ -0,0 +1,86 @@
1
+ """
2
+ WebSocket protocol definitions for LANscape.
3
+
4
+ Defines the message format and types used for communication between
5
+ WebSocket clients and the LANscape server.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import Any, Optional
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class MessageType(str, Enum):
15
+ """Types of WebSocket messages."""
16
+ # Requests
17
+ REQUEST = "request"
18
+ # Responses
19
+ RESPONSE = "response"
20
+ ERROR = "error"
21
+ # Push notifications (server-initiated)
22
+ EVENT = "event"
23
+
24
+
25
+ class WSMessage(BaseModel):
26
+ """Base WebSocket message structure."""
27
+ type: MessageType
28
+ id: Optional[str] = None # Message ID for request/response correlation
29
+
30
+
31
+ class WSRequest(WSMessage):
32
+ """
33
+ WebSocket request message from client.
34
+
35
+ Attributes:
36
+ action: The action to perform (e.g., 'scan.start', 'port.list')
37
+ params: Optional parameters for the action
38
+ """
39
+ type: MessageType = Field(default=MessageType.REQUEST)
40
+ action: str
41
+ params: Optional[dict[str, Any]] = None
42
+
43
+
44
+ class WSResponse(WSMessage):
45
+ """
46
+ WebSocket response message from server.
47
+
48
+ Attributes:
49
+ action: The action this is responding to
50
+ data: The response data
51
+ success: Whether the action was successful
52
+ """
53
+ type: MessageType = Field(default=MessageType.RESPONSE)
54
+ action: str
55
+ data: Any = None
56
+ success: bool = True
57
+
58
+
59
+ class WSError(WSMessage):
60
+ """
61
+ WebSocket error message from server.
62
+
63
+ Attributes:
64
+ action: The action that caused the error
65
+ error: Error message
66
+ traceback: Optional full traceback for debugging
67
+ """
68
+ type: MessageType = Field(default=MessageType.ERROR)
69
+ action: Optional[str] = None
70
+ error: str
71
+ traceback: Optional[str] = None
72
+
73
+
74
+ class WSEvent(WSMessage):
75
+ """
76
+ WebSocket event message from server (push notification).
77
+
78
+ Used for real-time updates like scan progress and results.
79
+
80
+ Attributes:
81
+ event: The event name (e.g., 'scan.progress', 'scan.device_found')
82
+ data: Event data
83
+ """
84
+ type: MessageType = Field(default=MessageType.EVENT)
85
+ event: str
86
+ data: Any = None