lanscape 1.3.2a7__tar.gz → 1.3.2a9__tar.gz
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-1.3.2a7/lanscape.egg-info → lanscape-1.3.2a9}/PKG-INFO +1 -1
- lanscape-1.3.2a9/lanscape/libraries/__init__.py +0 -0
- lanscape-1.3.2a9/lanscape/libraries/app_scope.py +75 -0
- lanscape-1.3.2a9/lanscape/libraries/decorators.py +153 -0
- lanscape-1.3.2a9/lanscape/libraries/errors.py +32 -0
- lanscape-1.3.2a9/lanscape/libraries/ip_parser.py +69 -0
- lanscape-1.3.2a9/lanscape/libraries/logger.py +45 -0
- lanscape-1.3.2a9/lanscape/libraries/mac_lookup.py +102 -0
- lanscape-1.3.2a9/lanscape/libraries/net_tools.py +516 -0
- lanscape-1.3.2a9/lanscape/libraries/port_manager.py +67 -0
- lanscape-1.3.2a9/lanscape/libraries/runtime_args.py +54 -0
- lanscape-1.3.2a9/lanscape/libraries/scan_config.py +97 -0
- lanscape-1.3.2a9/lanscape/libraries/service_scan.py +50 -0
- lanscape-1.3.2a9/lanscape/libraries/subnet_scan.py +338 -0
- lanscape-1.3.2a9/lanscape/libraries/version_manager.py +56 -0
- lanscape-1.3.2a9/lanscape/libraries/web_browser.py +142 -0
- lanscape-1.3.2a9/lanscape/resources/mac_addresses/convert_csv.py +30 -0
- lanscape-1.3.2a9/lanscape/resources/ports/convert_csv.py +30 -0
- lanscape-1.3.2a9/lanscape/ui/app.py +128 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/__init__.py +7 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/api/__init__.py +3 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/api/port.py +33 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/api/scan.py +75 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/api/tools.py +36 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/web/__init__.py +3 -0
- lanscape-1.3.2a9/lanscape/ui/blueprints/web/routes.py +78 -0
- lanscape-1.3.2a9/lanscape/ui/main.py +137 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9/lanscape.egg-info}/PKG-INFO +1 -1
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape.egg-info/SOURCES.txt +26 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/pyproject.toml +7 -2
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/LICENSE +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/MANIFEST.in +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/README.md +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/__init__.py +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/__main__.py +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/css/style.css +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/core.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/main.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/base.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/info.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/main.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape.egg-info/requires.txt +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/setup.cfg +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/tests/test_api.py +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/tests/test_env.py +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/tests/test_library.py +0 -0
- {lanscape-1.3.2a7 → lanscape-1.3.2a9}/tests/test_utils.py +0 -0
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Resource and environment management utilities for Lanscape.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResourceManager:
|
|
13
|
+
"""
|
|
14
|
+
A class to manage assets in the resources folder.
|
|
15
|
+
Works locally and if installed based on relative path from this file.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, asset_folder: str):
|
|
19
|
+
"""Initialize the resource manager with a specific asset folder."""
|
|
20
|
+
self.asset_dir = self._get_resource_path() / asset_folder
|
|
21
|
+
|
|
22
|
+
def list(self):
|
|
23
|
+
"""List all asset names in the asset directory."""
|
|
24
|
+
return [p.name for p in self.asset_dir.iterdir()]
|
|
25
|
+
|
|
26
|
+
def get(self, asset_name: str):
|
|
27
|
+
"""Get the content of an asset as a string."""
|
|
28
|
+
with open(self.asset_dir / asset_name, 'r', encoding='utf-8') as f:
|
|
29
|
+
return f.read()
|
|
30
|
+
|
|
31
|
+
def get_json(self, asset_name: str):
|
|
32
|
+
"""Get the content of an asset as a JSON object."""
|
|
33
|
+
return json.loads(self.get(asset_name))
|
|
34
|
+
|
|
35
|
+
def get_jsonc(self, asset_name: str):
|
|
36
|
+
"""Get JSON content with comments removed."""
|
|
37
|
+
content = self.get(asset_name)
|
|
38
|
+
cleaned_content = re.sub(r'//.*', '', content)
|
|
39
|
+
return json.loads(cleaned_content)
|
|
40
|
+
|
|
41
|
+
def update(self, asset_name: str, content: str):
|
|
42
|
+
"""Update the content of an existing asset."""
|
|
43
|
+
with open(self.asset_dir / asset_name, 'w', encoding='utf-8') as f:
|
|
44
|
+
f.write(content)
|
|
45
|
+
|
|
46
|
+
def create(self, asset_name: str, content: str):
|
|
47
|
+
"""Create a new asset with the given content."""
|
|
48
|
+
if (self.asset_dir / asset_name).exists():
|
|
49
|
+
raise FileExistsError(f"File {asset_name} already exists")
|
|
50
|
+
with open(self.asset_dir / asset_name, 'w', encoding='utf-8') as f:
|
|
51
|
+
f.write(content)
|
|
52
|
+
|
|
53
|
+
def delete(self, asset_name: str):
|
|
54
|
+
"""Delete an asset from the asset directory."""
|
|
55
|
+
(self.asset_dir / asset_name).unlink()
|
|
56
|
+
|
|
57
|
+
def _get_resource_path(self) -> Path:
|
|
58
|
+
"""Get the path to the resources directory."""
|
|
59
|
+
base_dir = Path(__file__).parent.parent
|
|
60
|
+
resource_dir = base_dir / "resources"
|
|
61
|
+
return resource_dir
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_local_run() -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Determine if the code is running locally or as an installed PyPI package.
|
|
67
|
+
Returns True if running locally, False if installed as a package.
|
|
68
|
+
"""
|
|
69
|
+
module_path = Path(__file__).parent
|
|
70
|
+
|
|
71
|
+
package_folders = ["site-packages", "dist-packages"]
|
|
72
|
+
parts = [part in module_path.parts for part in package_folders]
|
|
73
|
+
if any(parts):
|
|
74
|
+
return False
|
|
75
|
+
return True # Installed package
|
|
@@ -0,0 +1,153 @@
|
|
|
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(default_factory=lambda: defaultdict(int))
|
|
22
|
+
finished: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int))
|
|
23
|
+
timing: DefaultDict[str, float] = field(default_factory=lambda: defaultdict(float))
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
"""Return a formatted string representation of the job statistics."""
|
|
27
|
+
data = [
|
|
28
|
+
[
|
|
29
|
+
name,
|
|
30
|
+
self.running.get(name, 0),
|
|
31
|
+
self.finished.get(name, 0),
|
|
32
|
+
self.timing.get(name, 0.0)
|
|
33
|
+
]
|
|
34
|
+
for name in set(self.running) | set(self.finished)
|
|
35
|
+
]
|
|
36
|
+
headers = ["Function", "Running", "Finished", "Avg Time (s)"]
|
|
37
|
+
return tabulate(
|
|
38
|
+
data,
|
|
39
|
+
headers=headers,
|
|
40
|
+
tablefmt="grid"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class JobStatsMixin: # pylint: disable=too-few-public-methods
|
|
45
|
+
"""
|
|
46
|
+
Singleton mixin that provides shared job_stats property across all instances.
|
|
47
|
+
"""
|
|
48
|
+
_job_stats = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def job_stats(self):
|
|
52
|
+
"""Return the shared JobStats instance."""
|
|
53
|
+
if JobStatsMixin._job_stats is None:
|
|
54
|
+
JobStatsMixin._job_stats = JobStats()
|
|
55
|
+
return JobStatsMixin._job_stats
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def job_tracker(func):
|
|
59
|
+
"""
|
|
60
|
+
Decorator to track job statistics for a method, including running count, finished count, and average timing.
|
|
61
|
+
"""
|
|
62
|
+
def get_fxn_src_name(func, first_arg) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Return the function name with the class name prepended if available.
|
|
65
|
+
"""
|
|
66
|
+
qual_parts = func.__qualname__.split(".")
|
|
67
|
+
cls_name = qual_parts[-2] if len(qual_parts) > 1 else None
|
|
68
|
+
cls_obj = None # resolved lazily
|
|
69
|
+
if cls_obj is None and cls_name:
|
|
70
|
+
mod = inspect.getmodule(func)
|
|
71
|
+
cls_obj = getattr(mod, cls_name, None)
|
|
72
|
+
if cls_obj and first_arg is not None:
|
|
73
|
+
if (first_arg is cls_obj or isinstance(first_arg, cls_obj)):
|
|
74
|
+
return f"{cls_name}.{func.__name__}"
|
|
75
|
+
return func.__name__
|
|
76
|
+
|
|
77
|
+
def wrapper(*args, **kwargs):
|
|
78
|
+
"""Wrap the function to update job statistics before and after execution."""
|
|
79
|
+
class_instance = args[0]
|
|
80
|
+
job_stats = class_instance.job_stats
|
|
81
|
+
fxn = get_fxn_src_name(
|
|
82
|
+
func,
|
|
83
|
+
class_instance
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Increment running counter and track execution time
|
|
87
|
+
job_stats.running[fxn] += 1
|
|
88
|
+
start = time()
|
|
89
|
+
|
|
90
|
+
result = func(*args, **kwargs) # Execute the wrapped function
|
|
91
|
+
|
|
92
|
+
# Update statistics after function execution
|
|
93
|
+
elapsed = time() - start
|
|
94
|
+
job_stats.running[fxn] -= 1
|
|
95
|
+
job_stats.finished[fxn] += 1
|
|
96
|
+
|
|
97
|
+
# Calculate the new average timing for the function
|
|
98
|
+
job_stats.timing[fxn] = round(
|
|
99
|
+
((job_stats.finished[fxn] - 1) * job_stats.timing[fxn] + elapsed)
|
|
100
|
+
/ job_stats.finished[fxn],
|
|
101
|
+
4
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Clean up if no more running instances of this function
|
|
105
|
+
if job_stats.running[fxn] == 0:
|
|
106
|
+
job_stats.running.pop(fxn)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
return wrapper
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def terminator(func):
|
|
114
|
+
"""
|
|
115
|
+
Decorator designed specifically for the SubnetScanner class, helps facilitate termination of a job.
|
|
116
|
+
"""
|
|
117
|
+
def wrapper(*args, **kwargs):
|
|
118
|
+
"""Wrap the function to check if the scan is running before execution."""
|
|
119
|
+
scan = args[0] # aka self
|
|
120
|
+
if not scan.running:
|
|
121
|
+
return None
|
|
122
|
+
return func(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
return wrapper
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def timeout_enforcer(timeout: int, raise_on_timeout: bool = True):
|
|
128
|
+
"""
|
|
129
|
+
Decorator to enforce a timeout on a function.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
timeout (int): Timeout length in seconds.
|
|
133
|
+
raise_on_timeout (bool): Whether to raise an exception if the timeout is exceeded.
|
|
134
|
+
"""
|
|
135
|
+
def decorator(func):
|
|
136
|
+
@functools.wraps(func)
|
|
137
|
+
def wrapper(*args, **kwargs):
|
|
138
|
+
"""Wrap the function to enforce a timeout on its execution."""
|
|
139
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
140
|
+
future = executor.submit(func, *args, **kwargs)
|
|
141
|
+
try:
|
|
142
|
+
return future.result(
|
|
143
|
+
timeout=timeout
|
|
144
|
+
)
|
|
145
|
+
except concurrent.futures.TimeoutError as exc:
|
|
146
|
+
if raise_on_timeout:
|
|
147
|
+
raise TimeoutError(
|
|
148
|
+
f"Function '{func.__name__}' exceeded timeout of "
|
|
149
|
+
f"{timeout} seconds."
|
|
150
|
+
) from exc
|
|
151
|
+
return None # Return None if not raising an exception
|
|
152
|
+
return wrapper
|
|
153
|
+
return decorator
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class SubnetTooLargeError(Exception):
|
|
4
|
+
"""Custom exception raised when the subnet size exceeds the allowed limit."""
|
|
5
|
+
|
|
6
|
+
def __init__(self, subnet):
|
|
7
|
+
self.subnet = subnet
|
|
8
|
+
super().__init__(f"Subnet {subnet} exceeds the limit of IP addresses.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubnetScanTerminationFailure(Exception):
|
|
12
|
+
def __init__(self, running_threads):
|
|
13
|
+
super().__init__(
|
|
14
|
+
f'Unable to terminate active threads: {running_threads}')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeviceError(Exception):
|
|
18
|
+
def __init__(self, e: Exception):
|
|
19
|
+
self.base: Exception = e
|
|
20
|
+
self.method = self._attempt_extract_method()
|
|
21
|
+
|
|
22
|
+
def _attempt_extract_method(self):
|
|
23
|
+
try:
|
|
24
|
+
tb = self.base.__traceback__
|
|
25
|
+
frame = tb.tb_frame
|
|
26
|
+
return frame.f_code.co_name
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(e)
|
|
29
|
+
return 'unknown'
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return f'Error(source={self.method}, msg={self.base})'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
from .errors import SubnetTooLargeError
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
MAX_IPS_ALLOWED = 100000
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_ip_input(ip_input):
|
|
9
|
+
# Split input on commas for multiple entries
|
|
10
|
+
entries = [entry.strip() for entry in ip_input.split(',')]
|
|
11
|
+
ip_ranges = []
|
|
12
|
+
|
|
13
|
+
for entry in entries:
|
|
14
|
+
# Handle CIDR notation or IP/32
|
|
15
|
+
if '/' in entry:
|
|
16
|
+
net = ipaddress.IPv4Network(entry, strict=False)
|
|
17
|
+
if net.num_addresses > MAX_IPS_ALLOWED:
|
|
18
|
+
raise SubnetTooLargeError(ip_input)
|
|
19
|
+
for ip in net.hosts():
|
|
20
|
+
ip_ranges.append(ip)
|
|
21
|
+
|
|
22
|
+
# Handle IP range (e.g., 10.0.0.15-10.0.0.25)
|
|
23
|
+
elif '-' in entry:
|
|
24
|
+
ip_ranges += parse_ip_range(entry)
|
|
25
|
+
|
|
26
|
+
# Handle shorthand IP range (e.g., 10.0.9.1-253)
|
|
27
|
+
elif re.search(r'\d+\-\d+', entry):
|
|
28
|
+
ip_ranges += parse_shorthand_ip_range(entry)
|
|
29
|
+
|
|
30
|
+
# If no CIDR or range, assume a single IP
|
|
31
|
+
else:
|
|
32
|
+
ip_ranges.append(ipaddress.IPv4Address(entry))
|
|
33
|
+
if len(ip_ranges) > MAX_IPS_ALLOWED:
|
|
34
|
+
raise SubnetTooLargeError(ip_input)
|
|
35
|
+
return ip_ranges
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_address_count(subnet: str):
|
|
39
|
+
try:
|
|
40
|
+
net = ipaddress.IPv4Network(subnet, strict=False)
|
|
41
|
+
return net.num_addresses
|
|
42
|
+
except BaseException:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_ip_range(entry):
|
|
47
|
+
start_ip, end_ip = entry.split('-')
|
|
48
|
+
start_ip = ipaddress.IPv4Address(start_ip.strip())
|
|
49
|
+
|
|
50
|
+
# Handle case where the second part is a partial IP (e.g., '253')
|
|
51
|
+
if '.' not in end_ip:
|
|
52
|
+
end_ip = start_ip.exploded.rsplit('.', 1)[0] + '.' + end_ip.strip()
|
|
53
|
+
|
|
54
|
+
end_ip = ipaddress.IPv4Address(end_ip.strip())
|
|
55
|
+
return list(ip_range_to_list(start_ip, end_ip))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_shorthand_ip_range(entry):
|
|
59
|
+
start_ip, end_part = entry.split('-')
|
|
60
|
+
start_ip = ipaddress.IPv4Address(start_ip.strip())
|
|
61
|
+
end_ip = start_ip.exploded.rsplit('.', 1)[0] + '.' + end_part.strip()
|
|
62
|
+
|
|
63
|
+
return list(ip_range_to_list(start_ip, ipaddress.IPv4Address(end_ip)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ip_range_to_list(start_ip, end_ip):
|
|
67
|
+
# Yield the range of IPs
|
|
68
|
+
for ip_int in range(int(start_ip), int(end_ip) + 1):
|
|
69
|
+
yield ipaddress.IPv4Address(ip_int)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import RotatingFileHandler
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_logging(loglevel: str, logfile: bool, flask_logging: bool = False) -> None:
|
|
7
|
+
numeric_level = getattr(logging, loglevel.upper(), None)
|
|
8
|
+
if not isinstance(numeric_level, int):
|
|
9
|
+
raise ValueError(f'Invalid log level: {loglevel}')
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(level=numeric_level,
|
|
12
|
+
format='[%(name)s] %(levelname)s - %(message)s')
|
|
13
|
+
|
|
14
|
+
# flask spams too much on info
|
|
15
|
+
if not flask_logging:
|
|
16
|
+
disable_flask_logging()
|
|
17
|
+
|
|
18
|
+
if logfile:
|
|
19
|
+
handler = RotatingFileHandler(
|
|
20
|
+
'lanscape.log', maxBytes=100000, backupCount=3)
|
|
21
|
+
handler.setLevel(numeric_level)
|
|
22
|
+
formatter = logging.Formatter(
|
|
23
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
24
|
+
handler.setFormatter(formatter)
|
|
25
|
+
logging.getLogger().addHandler(handler)
|
|
26
|
+
else:
|
|
27
|
+
# For console, it defaults to basicConfig
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def disable_flask_logging() -> None:
|
|
32
|
+
|
|
33
|
+
def override_click_logging():
|
|
34
|
+
def secho(text, file=None, nl=None, err=None, color=None, **styles):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def echo(text, file=None, nl=None, err=None, color=None, **styles):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
click.echo = echo
|
|
41
|
+
click.secho = secho
|
|
42
|
+
werkzeug_log = logging.getLogger('werkzeug')
|
|
43
|
+
werkzeug_log.setLevel(logging.ERROR)
|
|
44
|
+
|
|
45
|
+
override_click_logging()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import logging
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from scapy.sendrecv import srp
|
|
7
|
+
from scapy.layers.l2 import ARP, Ether
|
|
8
|
+
from .app_scope import ResourceManager
|
|
9
|
+
from .decorators import job_tracker, JobStatsMixin
|
|
10
|
+
from .errors import DeviceError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger('MacLookup')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MacLookup:
|
|
17
|
+
"""High-level MAC address lookup service."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._db = ResourceManager('mac_addresses').get_json('mac_db.json')
|
|
21
|
+
self._resolver = MacResolver()
|
|
22
|
+
|
|
23
|
+
def lookup_vendor(self, mac: str) -> Optional[str]:
|
|
24
|
+
"""
|
|
25
|
+
Lookup a MAC address in the database and return the vendor name.
|
|
26
|
+
"""
|
|
27
|
+
if mac:
|
|
28
|
+
for m in self._db:
|
|
29
|
+
if mac.upper().startswith(str(m).upper()):
|
|
30
|
+
return self._db[m]
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def resolve_mac_addresses(self, ip: str) -> List[str]:
|
|
34
|
+
"""
|
|
35
|
+
Get MAC addresses for an IP address using available methods.
|
|
36
|
+
"""
|
|
37
|
+
return self._resolver.get_macs(ip)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MacResolver(JobStatsMixin):
|
|
41
|
+
"""Handles MAC address resolution using various methods."""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.caught_errors: List[DeviceError] = []
|
|
46
|
+
|
|
47
|
+
def get_macs(self, ip: str) -> List[str]:
|
|
48
|
+
"""Try to get the MAC address using Scapy, fallback to ARP if it fails."""
|
|
49
|
+
if mac := self._get_mac_by_scapy(ip):
|
|
50
|
+
log.debug(f"Used Scapy to resolve ip {ip} to mac {mac}")
|
|
51
|
+
return mac
|
|
52
|
+
arp = self._get_mac_by_arp(ip)
|
|
53
|
+
log.debug(f"Used ARP to resolve ip {ip} to mac {arp}")
|
|
54
|
+
return arp
|
|
55
|
+
|
|
56
|
+
@job_tracker
|
|
57
|
+
def _get_mac_by_arp(self, ip: str) -> List[str]:
|
|
58
|
+
"""Retrieve the last MAC address instance using the ARP command."""
|
|
59
|
+
try:
|
|
60
|
+
# Use the appropriate ARP command based on the platform
|
|
61
|
+
cmd = f"arp -a {ip}" if platform.system() == "Windows" else f"arp {ip}"
|
|
62
|
+
|
|
63
|
+
# Execute the ARP command and decode the output
|
|
64
|
+
output = subprocess.check_output(
|
|
65
|
+
cmd, shell=True
|
|
66
|
+
).decode().replace('-', ':')
|
|
67
|
+
|
|
68
|
+
macs = re.findall(r'..:..:..:..:..:..', output)
|
|
69
|
+
# found that typically last mac is the correct one
|
|
70
|
+
return macs
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.caught_errors.append(DeviceError(e))
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
@job_tracker
|
|
76
|
+
def _get_mac_by_scapy(self, ip: str) -> List[str]:
|
|
77
|
+
"""Retrieve the MAC address using the Scapy library."""
|
|
78
|
+
try:
|
|
79
|
+
# Construct and send an ARP request
|
|
80
|
+
arp_request = ARP(pdst=ip)
|
|
81
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
82
|
+
packet = broadcast / arp_request
|
|
83
|
+
|
|
84
|
+
# Send the packet and wait for a response
|
|
85
|
+
result = srp(packet, timeout=1, verbose=0)[0]
|
|
86
|
+
|
|
87
|
+
# Extract the MAC addresses from the response
|
|
88
|
+
return [res[1].hwsrc for res in result]
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.caught_errors.append(DeviceError(e))
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Backward compatibility functions
|
|
95
|
+
def lookup_mac(mac: str) -> Optional[str]:
|
|
96
|
+
"""Backward compatibility function for MAC vendor lookup."""
|
|
97
|
+
return MacLookup().lookup_vendor(mac)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_macs(ip: str) -> List[str]:
|
|
101
|
+
"""Backward compatibility function for MAC resolution."""
|
|
102
|
+
return MacResolver().get_macs(ip)
|