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
lanscape/__init__.py CHANGED
@@ -1,20 +1,24 @@
1
1
  """
2
2
  Local network scanner
3
3
  """
4
- from lanscape.libraries.subnet_scan import (
4
+ from lanscape.core.subnet_scan import (
5
5
  SubnetScanner,
6
+ ScannerResults,
6
7
  ScanManager
7
8
  )
8
9
 
9
- from lanscape.libraries.scan_config import (
10
+ from lanscape.core.scan_config import (
10
11
  ScanConfig,
11
12
  ArpConfig,
12
13
  PingConfig,
13
14
  PokeConfig,
14
15
  ArpCacheConfig,
16
+ PortScanConfig,
17
+ ServiceScanConfig,
18
+ ServiceScanStrategy,
15
19
  ScanType
16
20
  )
17
21
 
18
- from lanscape.libraries.port_manager import PortManager
22
+ from lanscape.core.port_manager import PortManager
19
23
 
20
- from lanscape.libraries import net_tools
24
+ from lanscape.core import net_tools
@@ -5,7 +5,6 @@ Resource and environment management utilities for Lanscape.
5
5
 
6
6
  from pathlib import Path
7
7
  import json
8
- import re
9
8
 
10
9
 
11
10
  class ResourceManager:
@@ -32,9 +31,28 @@ class ResourceManager:
32
31
  return json.loads(self.get(asset_name))
33
32
 
34
33
  def get_jsonc(self, asset_name: str):
35
- """Get JSON content with comments removed."""
34
+ """AI Slop to get JSONC (JSON with comments) content of an asset as a JSON object."""
36
35
  content = self.get(asset_name)
37
- cleaned_content = re.sub(r'//.*', '', content)
36
+
37
+ def strip_jsonc_lines(text):
38
+ result = []
39
+ in_string = False
40
+ escape = False
41
+ for line in text.splitlines():
42
+ new_line = []
43
+ i = 0
44
+ while i < len(line):
45
+ char = line[i]
46
+ if char == '"' and not escape:
47
+ in_string = not in_string
48
+ if not in_string and line[i:i + 2] == "//":
49
+ break # Ignore rest of line (comment)
50
+ new_line.append(char)
51
+ escape = (char == '\\' and not escape)
52
+ i += 1
53
+ result.append(''.join(new_line))
54
+ return '\n'.join(result)
55
+ cleaned_content = strip_jsonc_lines(content)
38
56
  return json.loads(cleaned_content)
39
57
 
40
58
  def update(self, asset_name: str, content: str):
@@ -0,0 +1,231 @@
1
+
2
+ """Decorators and job tracking utilities for Lanscape."""
3
+
4
+ from time import time
5
+ from collections import defaultdict
6
+ import functools
7
+ import concurrent.futures
8
+ import logging
9
+ import threading
10
+ from tabulate import tabulate
11
+
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def run_once(func):
17
+ """Ensure a function executes only once and cache the result."""
18
+
19
+ cache_attr = "_run_once_cache"
20
+ ran_attr = "_run_once_ran"
21
+
22
+ @functools.wraps(func)
23
+ def wrapper(*args, **kwargs):
24
+ if getattr(wrapper, ran_attr, False):
25
+ return getattr(wrapper, cache_attr)
26
+
27
+ start = time()
28
+ result = func(*args, **kwargs)
29
+ elapsed = time() - start
30
+
31
+ setattr(wrapper, cache_attr, result)
32
+ setattr(wrapper, ran_attr, True)
33
+
34
+ log.debug("run_once executed %s in %.4fs", func.__qualname__, elapsed)
35
+ return result
36
+
37
+ return wrapper
38
+
39
+
40
+ class JobStats:
41
+ """
42
+ Thread-safe singleton for tracking job statistics across all classes.
43
+ Tracks statistics for job execution, including running, finished, and timing data.
44
+ """
45
+
46
+ _instance = None
47
+ _lock = threading.Lock()
48
+
49
+ def __new__(cls):
50
+ if cls._instance is None:
51
+ with cls._lock:
52
+ if cls._instance is None: # Double-checked locking
53
+ cls._instance = super().__new__(cls)
54
+ return cls._instance
55
+
56
+ def __init__(self):
57
+ if not hasattr(self, '_initialized'):
58
+ self._stats_lock = threading.RLock()
59
+ self.running = defaultdict(int)
60
+ self.finished = defaultdict(int)
61
+ self.timing = defaultdict(float)
62
+ self._initialized = True
63
+
64
+ def start_job(self, func_name: str):
65
+ """Thread-safe increment of running counter."""
66
+ with self._stats_lock:
67
+ self.running[func_name] += 1
68
+
69
+ def finish_job(self, func_name: str, elapsed_time: float):
70
+ """Thread-safe update of job completion and timing."""
71
+ with self._stats_lock:
72
+ self.running[func_name] -= 1
73
+ self.finished[func_name] += 1
74
+
75
+ # Calculate running average
76
+ count = self.finished[func_name]
77
+ old_avg = self.timing[func_name]
78
+ new_avg = (old_avg * (count - 1) + elapsed_time) / count
79
+ self.timing[func_name] = round(new_avg, 4)
80
+
81
+ # Cleanup running if zero
82
+ if self.running[func_name] <= 0:
83
+ self.running.pop(func_name, None)
84
+
85
+ def clear_stats(self):
86
+ """Clear all statistics (useful between scans)."""
87
+ with self._stats_lock:
88
+ self.running.clear()
89
+ self.finished.clear()
90
+ self.timing.clear()
91
+
92
+ def get_stats_copy(self) -> dict:
93
+ """Get a thread-safe copy of current statistics."""
94
+ with self._stats_lock:
95
+ return {
96
+ 'running': dict(self.running),
97
+ 'finished': dict(self.finished),
98
+ 'timing': dict(self.timing)
99
+ }
100
+
101
+ @classmethod
102
+ def reset_for_testing(cls):
103
+ """Reset singleton instance for testing purposes only."""
104
+ with cls._lock:
105
+ if cls._instance:
106
+ cls._instance.clear_stats()
107
+ cls._instance = None
108
+
109
+ def __str__(self):
110
+ """Return a formatted string representation of the job statistics."""
111
+ data = [
112
+ [
113
+ name,
114
+ self.running.get(name, 0),
115
+ self.finished.get(name, 0),
116
+ self.timing.get(name, 0.0)
117
+ ]
118
+ for name in set(self.running) | set(self.finished)
119
+ ]
120
+ headers = ["Function", "Running", "Finished", "Avg Time (s)"]
121
+ return tabulate(
122
+ data,
123
+ headers=headers,
124
+ tablefmt="grid"
125
+ )
126
+
127
+
128
+ class JobStatsMixin: # pylint: disable=too-few-public-methods
129
+ """
130
+ Singleton mixin that provides shared job_stats property across all instances.
131
+ """
132
+ _job_stats = None
133
+
134
+ @property
135
+ def job_stats(self):
136
+ """Return the shared JobStats instance."""
137
+ return JobStats()
138
+
139
+
140
+ def job_tracker(func):
141
+ """
142
+ Decorator to track job statistics for a method,
143
+ including running count, finished count, and average timing.
144
+ """
145
+ def get_fxn_src_name(func, first_arg) -> str:
146
+ """
147
+ Return the function name with the class name prepended if available.
148
+ """
149
+ qual_parts = func.__qualname__.split(".")
150
+
151
+ # If function has class context (e.g., "ClassName.method_name")
152
+ if len(qual_parts) > 1:
153
+ cls_name = qual_parts[-2]
154
+
155
+ # Check if first_arg is an instance and has the expected class name
156
+ if first_arg is not None and hasattr(first_arg, '__class__'):
157
+ if first_arg.__class__.__name__ == cls_name:
158
+ return f"{cls_name}.{func.__name__}"
159
+
160
+ return func.__name__
161
+
162
+ @functools.wraps(func)
163
+ def wrapper(*args, **kwargs):
164
+ """Wrap the function to update job statistics before and after execution."""
165
+ job_stats = JobStats()
166
+
167
+ # Determine function name for tracking
168
+ if args:
169
+ fxn = get_fxn_src_name(func, args[0])
170
+ else:
171
+ fxn = func.__name__
172
+
173
+ # Start job tracking
174
+ job_stats.start_job(fxn)
175
+ start = time()
176
+
177
+ try:
178
+ result = func(*args, **kwargs) # Execute the wrapped function
179
+ return result
180
+ finally:
181
+ # Always update statistics, even if function raises exception
182
+ elapsed = time() - start
183
+ job_stats.finish_job(fxn, elapsed)
184
+
185
+ return wrapper
186
+
187
+
188
+ def terminator(func):
189
+ """
190
+ Decorator designed specifically for the SubnetScanner class,
191
+ helps facilitate termination of a job.
192
+ """
193
+ def wrapper(*args, **kwargs):
194
+ """Wrap the function to check if the scan is running before execution."""
195
+ scan = args[0] # aka self
196
+ if not scan.running:
197
+ return None
198
+ return func(*args, **kwargs)
199
+
200
+ return wrapper
201
+
202
+
203
+ def timeout_enforcer(timeout: int, raise_on_timeout: bool = True):
204
+ """
205
+ Decorator to enforce a timeout on a function.
206
+
207
+ Args:
208
+ timeout (int): Timeout length in seconds.
209
+ raise_on_timeout (bool): Whether to raise an exception if the timeout is exceeded.
210
+ """
211
+ def decorator(func):
212
+ @functools.wraps(func)
213
+ def wrapper(*args, **kwargs):
214
+ """Wrap the function to enforce a timeout on its execution."""
215
+ with concurrent.futures.ThreadPoolExecutor(
216
+ max_workers=1,
217
+ thread_name_prefix="TimeoutEnforcer") as executor:
218
+ future = executor.submit(func, *args, **kwargs)
219
+ try:
220
+ return future.result(
221
+ timeout=timeout
222
+ )
223
+ except concurrent.futures.TimeoutError as exc:
224
+ if raise_on_timeout:
225
+ raise TimeoutError(
226
+ f"Function '{func.__name__}' exceeded timeout of "
227
+ f"{timeout} seconds."
228
+ ) from exc
229
+ return None # Return None if not raising an exception
230
+ return wrapper
231
+ return decorator
@@ -12,13 +12,14 @@ import psutil
12
12
  from scapy.sendrecv import srp
13
13
  from scapy.layers.l2 import ARP, Ether
14
14
  from icmplib import ping
15
+ from icmplib.exceptions import SocketPermissionError
15
16
 
16
- from lanscape.libraries.net_tools import Device
17
- from lanscape.libraries.scan_config import (
17
+ from lanscape.core.net_tools import Device, DeviceError
18
+ from lanscape.core.scan_config import (
18
19
  ScanConfig, ScanType, PingConfig,
19
20
  ArpConfig, PokeConfig, ArpCacheConfig
20
21
  )
21
- from lanscape.libraries.decorators import timeout_enforcer, job_tracker
22
+ from lanscape.core.decorators import timeout_enforcer, job_tracker
22
23
 
23
24
 
24
25
  def is_device_alive(device: Device, scan_config: ScanConfig) -> bool:
@@ -72,18 +73,84 @@ class IcmpLookup():
72
73
  Returns:
73
74
  bool: True if the device is reachable via ICMP, False otherwise.
74
75
  """
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
76
+ try:
77
+ # Try using icmplib first
78
+ for _ in range(cfg.attempts):
79
+ result = ping(
80
+ device.ip,
81
+ count=cfg.ping_count,
82
+ interval=cfg.retry_delay,
83
+ timeout=cfg.timeout,
84
+ privileged=psutil.WINDOWS # Use privileged mode on Windows
85
+ )
86
+ if result.is_alive:
87
+ device.alive = True
88
+ break
89
+ return device.alive is True
90
+ except SocketPermissionError:
91
+ # Fallback to system ping command when raw sockets aren't available
92
+ return cls._ping_fallback(device, cfg)
93
+
94
+ @classmethod
95
+ def _ping_fallback(cls, device: Device, cfg: PingConfig) -> bool:
96
+ """Fallback ping using system ping command via subprocess.
97
+
98
+ Args:
99
+ device (Device): The device to ping.
100
+ cfg (PingConfig): The ping configuration.
101
+
102
+ Returns:
103
+ bool: True if the device responds to ping, False otherwise.
104
+ """
105
+ cmd = []
106
+
107
+ if psutil.WINDOWS:
108
+ # -n count, -w timeout in ms
109
+ cmd = ['ping', '-n', str(cfg.ping_count), '-w', str(int(cfg.timeout * 1000)), device.ip]
110
+ else: # Linux, macOS, and other Unix-like systems
111
+ # -c count, -W timeout in s
112
+ cmd = ['ping', '-c', str(cfg.ping_count), '-W', str(int(cfg.timeout)), device.ip]
113
+
114
+ for r in range(cfg.attempts):
115
+ try:
116
+ # Remove check=True to handle return codes manually
117
+ # Add timeout to prevent hanging
118
+ timeout_val = cfg.timeout * cfg.ping_count + 5
119
+ proc = subprocess.run(
120
+ cmd,
121
+ text=True,
122
+ stdout=subprocess.PIPE,
123
+ stderr=subprocess.PIPE,
124
+ timeout=timeout_val,
125
+ check=False # Handle return codes manually
126
+ )
127
+
128
+ # Check if ping was successful
129
+ if proc.returncode == 0:
130
+ output = proc.stdout.lower()
131
+
132
+ # Windows/Linux both include "TTL" on a successful reply
133
+ if psutil.WINDOWS or psutil.LINUX:
134
+ if 'ttl' in output:
135
+ device.alive = True
136
+ return True # Early return on success
137
+
138
+ # some distributions of Linux and macOS
139
+ if psutil.MACOS or psutil.LINUX:
140
+ bad = '100.0% packet loss'
141
+ good = 'ping statistics'
142
+ # mac doesnt include TTL, so we check good is there, and bad is not
143
+ if good in output and bad not in output:
144
+ device.alive = True
145
+ return True # Early return on success
146
+
147
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
148
+ FileNotFoundError) as e:
149
+ device.caught_errors.append(DeviceError(e))
150
+
151
+ if r < cfg.attempts - 1:
152
+ time.sleep(cfg.retry_delay)
153
+
87
154
  return device.alive is True
88
155
 
89
156
 
@@ -156,7 +223,7 @@ class ArpLookup():
156
223
  NOTE: This lookup method requires elevated privileges to access the ARP cache.
157
224
 
158
225
 
159
- [Arp Lookup Requirements](/support/arp-issues.md)
226
+ [Arp Lookup Requirements](/docs/arp-issues.md)
160
227
  """
161
228
 
162
229
  @classmethod
@@ -10,9 +10,8 @@ This module provides utilities for parsing various IP address formats including:
10
10
  It also includes validation to prevent processing excessively large IP ranges.
11
11
  """
12
12
  import ipaddress
13
- import re
14
13
 
15
- from lanscape.libraries.errors import SubnetTooLargeError
14
+ from lanscape.core.errors import SubnetTooLargeError
16
15
 
17
16
  MAX_IPS_ALLOWED = 100000
18
17
 
@@ -50,14 +49,10 @@ def parse_ip_input(ip_input):
50
49
  for ip in net.hosts():
51
50
  ip_ranges.append(ip)
52
51
 
53
- # Handle IP range (e.g., 10.0.0.15-10.0.0.25)
52
+ # Handle IP range (e.g., 10.0.0.15-10.0.0.25) and (e.g., 10.0.9.1-253)
54
53
  elif '-' in entry:
55
54
  ip_ranges += parse_ip_range(entry)
56
55
 
57
- # Handle shorthand IP range (e.g., 10.0.9.1-253)
58
- elif re.search(r'\d+\-\d+', entry):
59
- ip_ranges += parse_shorthand_ip_range(entry)
60
-
61
56
  # If no CIDR or range, assume a single IP
62
57
  else:
63
58
  ip_ranges.append(ipaddress.IPv4Address(entry))
@@ -106,25 +101,6 @@ def parse_ip_range(entry):
106
101
  return list(ip_range_to_list(start_ip, end_ip))
107
102
 
108
103
 
109
- def parse_shorthand_ip_range(entry):
110
- """
111
- Parse a shorthand IP range (e.g., 192.168.1.1-10).
112
-
113
- In this format, only the last octet of the end IP is specified.
114
-
115
- Args:
116
- entry (str): String containing a shorthand IP range
117
-
118
- Returns:
119
- list: List of IPv4Address objects in the range (inclusive)
120
- """
121
- start_ip, end_part = entry.split('-')
122
- start_ip = ipaddress.IPv4Address(start_ip.strip())
123
- end_ip = start_ip.exploded.rsplit('.', 1)[0] + '.' + end_part.strip()
124
-
125
- return list(ip_range_to_list(start_ip, ipaddress.IPv4Address(end_ip)))
126
-
127
-
128
104
  def ip_range_to_list(start_ip, end_ip):
129
105
  """
130
106
  Convert an IP range defined by start and end addresses to a list of addresses.