lanscape 1.4.2a3__tar.gz → 2.4.0a1__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.4.2a3/lanscape.egg-info → lanscape-2.4.0a1}/PKG-INFO +18 -3
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/README.md +1 -1
- lanscape-2.4.0a1/lanscape/__init__.py +24 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/app_scope.py +21 -3
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/decorators.py +87 -52
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/device_alive.py +83 -16
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/ip_parser.py +2 -26
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/net_tools.py +205 -47
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/runtime_args.py +6 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/scan_config.py +103 -5
- lanscape-2.4.0a1/lanscape/core/service_scan.py +222 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/subnet_scan.py +30 -14
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/version_manager.py +3 -2
- lanscape-2.4.0a1/lanscape/resources/ports/test_port_list_scan.json +4 -0
- lanscape-2.4.0a1/lanscape/resources/services/definitions.jsonc +632 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/app.py +17 -5
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/__init__.py +1 -1
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/port.py +15 -1
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/scan.py +1 -1
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/tools.py +4 -4
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/routes.py +29 -2
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/main.py +47 -19
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/shutdown_handler.py +2 -2
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/css/style.css +178 -20
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/core.js +14 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/main.js +30 -2
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/quietReload.js +3 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/scan-config.js +56 -6
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/base.html +6 -8
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/core/head.html +1 -1
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/info.html +20 -5
- lanscape-2.4.0a1/lanscape/ui/templates/main.html +91 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/config.html +62 -16
- lanscape-2.4.0a1/lanscape/ui/templates/scan/device-detail.html +111 -0
- lanscape-2.4.0a1/lanscape/ui/templates/scan/ip-table-row.html +37 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/ip-table.html +5 -5
- lanscape-2.4.0a1/lanscape/ui/ws/__init__.py +31 -0
- lanscape-2.4.0a1/lanscape/ui/ws/delta.py +170 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/base.py +145 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/port.py +184 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape-2.4.0a1/lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape-2.4.0a1/lanscape/ui/ws/protocol.py +86 -0
- lanscape-2.4.0a1/lanscape/ui/ws/server.py +375 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1/lanscape.egg-info}/PKG-INFO +18 -3
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape.egg-info/SOURCES.txt +33 -17
- lanscape-2.4.0a1/lanscape.egg-info/entry_points.txt +2 -0
- lanscape-2.4.0a1/lanscape.egg-info/requires.txt +19 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/pyproject.toml +24 -3
- lanscape-2.4.0a1/tests/test_api.py +322 -0
- lanscape-2.4.0a1/tests/test_decorators.py +333 -0
- lanscape-2.4.0a1/tests/test_env.py +147 -0
- lanscape-2.4.0a1/tests/test_globals.py +10 -0
- lanscape-2.4.0a1/tests/test_library.py +129 -0
- lanscape-2.4.0a1/tests/test_logging.py +102 -0
- lanscape-2.4.0a1/tests/test_port_scan.py +277 -0
- lanscape-2.4.0a1/tests/test_port_scan_linux_fd_exhaustion.py +297 -0
- lanscape-2.4.0a1/tests/test_service_scan.py +270 -0
- lanscape-2.4.0a1/tests/test_utils.py +375 -0
- lanscape-2.4.0a1/tests/test_websocket.py +940 -0
- lanscape-1.4.2a3/lanscape/__init__.py +0 -20
- lanscape-1.4.2a3/lanscape/libraries/service_scan.py +0 -50
- lanscape-1.4.2a3/lanscape/libraries/web_browser.py +0 -210
- lanscape-1.4.2a3/lanscape/resources/services/definitions.jsonc +0 -456
- lanscape-1.4.2a3/lanscape/ui/templates/main.html +0 -94
- lanscape-1.4.2a3/lanscape/ui/templates/scan/ip-table-row.html +0 -103
- lanscape-1.4.2a3/lanscape.egg-info/requires.txt +0 -8
- lanscape-1.4.2a3/tests/test_api.py +0 -243
- lanscape-1.4.2a3/tests/test_decorators.py +0 -29
- lanscape-1.4.2a3/tests/test_env.py +0 -45
- lanscape-1.4.2a3/tests/test_library.py +0 -104
- lanscape-1.4.2a3/tests/test_logging.py +0 -78
- lanscape-1.4.2a3/tests/test_utils.py +0 -109
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/LICENSE +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/MANIFEST.in +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/__main__.py +0 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/__init__.py +0 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/errors.py +0 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/logger.py +0 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/mac_lookup.py +0 -0
- {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.4.0a1/lanscape/core}/port_manager.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/__init__.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-1.4.2a3 → lanscape-2.4.0a1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.4.0a1
|
|
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
|
|
|
@@ -56,7 +56,7 @@ I use a combination of ARP, ICMP & port testing to determine if a device is onli
|
|
|
56
56
|
Recommendations:
|
|
57
57
|
|
|
58
58
|
- Adjust scan configuration
|
|
59
|
-
- Configure ARP lookup [ARP lookup setup](./
|
|
59
|
+
- Configure ARP lookup [ARP lookup setup](./docs/arp-issues.md)
|
|
60
60
|
- Create a bug
|
|
61
61
|
|
|
62
62
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local network scanner
|
|
3
|
+
"""
|
|
4
|
+
from lanscape.core.subnet_scan import (
|
|
5
|
+
SubnetScanner,
|
|
6
|
+
ScannerResults,
|
|
7
|
+
ScanManager
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from lanscape.core.scan_config import (
|
|
11
|
+
ScanConfig,
|
|
12
|
+
ArpConfig,
|
|
13
|
+
PingConfig,
|
|
14
|
+
PokeConfig,
|
|
15
|
+
ArpCacheConfig,
|
|
16
|
+
PortScanConfig,
|
|
17
|
+
ServiceScanConfig,
|
|
18
|
+
ServiceScanStrategy,
|
|
19
|
+
ScanType
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from lanscape.core.port_manager import PortManager
|
|
23
|
+
|
|
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):
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
"""Decorators and job tracking utilities for Lanscape."""
|
|
3
3
|
|
|
4
4
|
from time import time
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from typing import DefaultDict
|
|
7
5
|
from collections import defaultdict
|
|
8
|
-
import inspect
|
|
9
6
|
import functools
|
|
10
7
|
import concurrent.futures
|
|
11
8
|
import logging
|
|
9
|
+
import threading
|
|
12
10
|
from tabulate import tabulate
|
|
13
11
|
|
|
14
12
|
|
|
@@ -39,31 +37,74 @@ def run_once(func):
|
|
|
39
37
|
return wrapper
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
@dataclass
|
|
43
40
|
class JobStats:
|
|
44
41
|
"""
|
|
42
|
+
Thread-safe singleton for tracking job statistics across all classes.
|
|
45
43
|
Tracks statistics for job execution, including running, finished, and timing data.
|
|
46
44
|
"""
|
|
47
|
-
running: DefaultDict[str, int] = field(
|
|
48
|
-
default_factory=lambda: defaultdict(int))
|
|
49
|
-
finished: DefaultDict[str, int] = field(
|
|
50
|
-
default_factory=lambda: defaultdict(int))
|
|
51
|
-
timing: DefaultDict[str, float] = field(
|
|
52
|
-
default_factory=lambda: defaultdict(float))
|
|
53
45
|
|
|
54
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
55
|
|
|
56
56
|
def __init__(self):
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if not hasattr(self, '_initialized'):
|
|
58
|
+
self._stats_lock = threading.RLock()
|
|
59
59
|
self.running = defaultdict(int)
|
|
60
60
|
self.finished = defaultdict(int)
|
|
61
61
|
self.timing = defaultdict(float)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
67
108
|
|
|
68
109
|
def __str__(self):
|
|
69
110
|
"""Return a formatted string representation of the job statistics."""
|
|
@@ -106,48 +147,40 @@ def job_tracker(func):
|
|
|
106
147
|
Return the function name with the class name prepended if available.
|
|
107
148
|
"""
|
|
108
149
|
qual_parts = func.__qualname__.split(".")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
|
|
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
|
+
|
|
117
160
|
return func.__name__
|
|
118
161
|
|
|
162
|
+
@functools.wraps(func)
|
|
119
163
|
def wrapper(*args, **kwargs):
|
|
120
164
|
"""Wrap the function to update job statistics before and after execution."""
|
|
121
|
-
class_instance = args[0]
|
|
122
165
|
job_stats = JobStats()
|
|
123
|
-
fxn = get_fxn_src_name(
|
|
124
|
-
func,
|
|
125
|
-
class_instance
|
|
126
|
-
)
|
|
127
166
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
# Determine function name for tracking
|
|
168
|
+
if args:
|
|
169
|
+
fxn = get_fxn_src_name(func, args[0])
|
|
170
|
+
else:
|
|
171
|
+
fxn = func.__name__
|
|
131
172
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
elapsed = time() - start
|
|
136
|
-
job_stats.running[fxn] -= 1
|
|
137
|
-
job_stats.finished[fxn] += 1
|
|
138
|
-
|
|
139
|
-
# Calculate the new average timing for the function
|
|
140
|
-
job_stats.timing[fxn] = round(
|
|
141
|
-
((job_stats.finished[fxn] - 1) * job_stats.timing[fxn] + elapsed)
|
|
142
|
-
/ job_stats.finished[fxn],
|
|
143
|
-
4
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Clean up if no more running instances of this function
|
|
147
|
-
if job_stats.running[fxn] == 0:
|
|
148
|
-
job_stats.running.pop(fxn)
|
|
173
|
+
# Start job tracking
|
|
174
|
+
job_stats.start_job(fxn)
|
|
175
|
+
start = time()
|
|
149
176
|
|
|
150
|
-
|
|
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)
|
|
151
184
|
|
|
152
185
|
return wrapper
|
|
153
186
|
|
|
@@ -179,7 +212,9 @@ def timeout_enforcer(timeout: int, raise_on_timeout: bool = True):
|
|
|
179
212
|
@functools.wraps(func)
|
|
180
213
|
def wrapper(*args, **kwargs):
|
|
181
214
|
"""Wrap the function to enforce a timeout on its execution."""
|
|
182
|
-
with concurrent.futures.ThreadPoolExecutor(
|
|
215
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
216
|
+
max_workers=1,
|
|
217
|
+
thread_name_prefix="TimeoutEnforcer") as executor:
|
|
183
218
|
future = executor.submit(func, *args, **kwargs)
|
|
184
219
|
try:
|
|
185
220
|
return future.result(
|
|
@@ -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.
|