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/__init__.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Local network scanner
|
|
3
3
|
"""
|
|
4
|
-
from lanscape.
|
|
4
|
+
from lanscape.core.subnet_scan import (
|
|
5
5
|
SubnetScanner,
|
|
6
|
+
ScannerResults,
|
|
6
7
|
ScanManager
|
|
7
8
|
)
|
|
8
9
|
|
|
9
|
-
from lanscape.
|
|
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.
|
|
22
|
+
from lanscape.core.port_manager import PortManager
|
|
19
23
|
|
|
20
|
-
from lanscape.
|
|
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
|
-
"""
|
|
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
|
-
|
|
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.
|
|
17
|
-
from lanscape.
|
|
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.
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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](/
|
|
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.
|
|
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.
|