lanscape 1.4.2a3__tar.gz → 2.1.2__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.

Files changed (102) hide show
  1. {lanscape-1.4.2a3/lanscape.egg-info → lanscape-2.1.2}/PKG-INFO +13 -3
  2. {lanscape-1.4.2a3 → lanscape-2.1.2}/README.md +1 -1
  3. lanscape-2.1.2/lanscape/__init__.py +24 -0
  4. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/app_scope.py +21 -3
  5. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/decorators.py +87 -52
  6. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/device_alive.py +83 -16
  7. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/ip_parser.py +2 -26
  8. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/net_tools.py +189 -46
  9. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/scan_config.py +103 -5
  10. lanscape-2.1.2/lanscape/core/service_scan.py +187 -0
  11. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/subnet_scan.py +30 -14
  12. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/version_manager.py +3 -2
  13. lanscape-2.1.2/lanscape/resources/ports/test_port_list_scan.json +4 -0
  14. lanscape-2.1.2/lanscape/resources/services/definitions.jsonc +632 -0
  15. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/app.py +17 -5
  16. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/__init__.py +1 -1
  17. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/api/port.py +1 -1
  18. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/api/scan.py +1 -1
  19. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/api/tools.py +4 -4
  20. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/web/routes.py +29 -2
  21. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/main.py +4 -4
  22. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/shutdown_handler.py +2 -2
  23. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/css/style.css +159 -16
  24. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/main.js +30 -2
  25. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/scan-config.js +39 -0
  26. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/base.html +6 -8
  27. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/core/head.html +1 -1
  28. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/info.html +20 -5
  29. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/main.html +5 -6
  30. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan/config.html +59 -15
  31. lanscape-2.1.2/lanscape/ui/templates/scan/device-detail.html +111 -0
  32. lanscape-2.1.2/lanscape/ui/templates/scan/ip-table-row.html +37 -0
  33. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan/ip-table.html +1 -1
  34. {lanscape-1.4.2a3 → lanscape-2.1.2/lanscape.egg-info}/PKG-INFO +13 -3
  35. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape.egg-info/SOURCES.txt +22 -16
  36. lanscape-2.1.2/lanscape.egg-info/entry_points.txt +2 -0
  37. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape.egg-info/requires.txt +6 -0
  38. {lanscape-1.4.2a3 → lanscape-2.1.2}/pyproject.toml +18 -2
  39. lanscape-2.1.2/tests/test_api.py +293 -0
  40. lanscape-2.1.2/tests/test_decorators.py +333 -0
  41. lanscape-2.1.2/tests/test_env.py +147 -0
  42. lanscape-2.1.2/tests/test_globals.py +10 -0
  43. lanscape-2.1.2/tests/test_library.py +129 -0
  44. lanscape-2.1.2/tests/test_logging.py +102 -0
  45. lanscape-2.1.2/tests/test_port_scan.py +277 -0
  46. lanscape-2.1.2/tests/test_service_scan.py +270 -0
  47. lanscape-2.1.2/tests/test_utils.py +375 -0
  48. lanscape-1.4.2a3/lanscape/__init__.py +0 -20
  49. lanscape-1.4.2a3/lanscape/libraries/service_scan.py +0 -50
  50. lanscape-1.4.2a3/lanscape/resources/services/definitions.jsonc +0 -456
  51. lanscape-1.4.2a3/lanscape/ui/templates/scan/ip-table-row.html +0 -103
  52. lanscape-1.4.2a3/tests/test_api.py +0 -243
  53. lanscape-1.4.2a3/tests/test_decorators.py +0 -29
  54. lanscape-1.4.2a3/tests/test_env.py +0 -45
  55. lanscape-1.4.2a3/tests/test_library.py +0 -104
  56. lanscape-1.4.2a3/tests/test_logging.py +0 -78
  57. lanscape-1.4.2a3/tests/test_utils.py +0 -109
  58. {lanscape-1.4.2a3 → lanscape-2.1.2}/LICENSE +0 -0
  59. {lanscape-1.4.2a3 → lanscape-2.1.2}/MANIFEST.in +0 -0
  60. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/__main__.py +0 -0
  61. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/__init__.py +0 -0
  62. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/errors.py +0 -0
  63. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/logger.py +0 -0
  64. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/mac_lookup.py +0 -0
  65. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/port_manager.py +0 -0
  66. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/runtime_args.py +0 -0
  67. {lanscape-1.4.2a3/lanscape/libraries → lanscape-2.1.2/lanscape/core}/web_browser.py +0 -0
  68. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  69. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  70. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/ports/convert_csv.py +0 -0
  71. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/ports/full.json +0 -0
  72. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/ports/large.json +0 -0
  73. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/ports/medium.json +0 -0
  74. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/resources/ports/small.json +0 -0
  75. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/__init__.py +0 -0
  76. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/api/__init__.py +0 -0
  77. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/blueprints/web/__init__.py +0 -0
  78. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  79. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  80. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  81. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  82. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  83. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  84. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  85. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/core.js +0 -0
  86. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/layout-sizing.js +0 -0
  87. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/on-tab-close.js +0 -0
  88. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/quietReload.js +0 -0
  89. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/shutdown-server.js +0 -0
  90. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/subnet-info.js +0 -0
  91. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/js/subnet-selector.js +0 -0
  92. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/static/lanscape.webmanifest +0 -0
  93. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/core/scripts.html +0 -0
  94. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/error.html +0 -0
  95. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan/export.html +0 -0
  96. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan/overview.html +0 -0
  97. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan/scan-error.html +0 -0
  98. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/scan.html +0 -0
  99. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape/ui/templates/shutdown.html +0 -0
  100. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape.egg-info/dependency_links.txt +0 -0
  101. {lanscape-1.4.2a3 → lanscape-2.1.2}/lanscape.egg-info/top_level.txt +0 -0
  102. {lanscape-1.4.2a3 → lanscape-2.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 1.4.2a3
3
+ Version: 2.1.2
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.8
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,11 @@ 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
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
31
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
32
+ Requires-Dist: openai>=1.0.0; extra == "dev"
23
33
  Dynamic: license-file
24
34
 
25
35
  # LANscape
@@ -80,7 +90,7 @@ I use a combination of ARP, ICMP & port testing to determine if a device is onli
80
90
  Recommendations:
81
91
 
82
92
  - Adjust scan configuration
83
- - Configure ARP lookup [ARP lookup setup](./support/arp-issues.md)
93
+ - Configure ARP lookup [ARP lookup setup](./docs/arp-issues.md)
84
94
  - Create a bug
85
95
 
86
96
 
@@ -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](./support/arp-issues.md)
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
- """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):
@@ -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
- # Only initialize once
58
- if not hasattr(self, "running"):
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
- def __new__(cls, *args, **kwargs):
64
- if cls._instance is None:
65
- cls._instance = super(JobStats, cls).__new__(cls)
66
- return cls._instance
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
- cls_name = qual_parts[-2] if len(qual_parts) > 1 else None
110
- cls_obj = None # resolved lazily
111
- if cls_obj is None and cls_name:
112
- mod = inspect.getmodule(func)
113
- cls_obj = getattr(mod, cls_name, None)
114
- if cls_obj and first_arg is not None:
115
- if (first_arg is cls_obj or isinstance(first_arg, cls_obj)):
116
- return f"{cls_name}.{func.__name__}"
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
- # Increment running counter and track execution time
129
- job_stats.running[fxn] += 1
130
- start = time()
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
- result = func(*args, **kwargs) # Execute the wrapped function
133
-
134
- # Update statistics after function execution
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
- return result
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(max_workers=1) as executor:
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.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.