lanscape 1.3.5a2__tar.gz → 1.3.6a2__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 (85) hide show
  1. {lanscape-1.3.5a2/lanscape.egg-info → lanscape-1.3.6a2}/PKG-INFO +18 -2
  2. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/README.md +17 -1
  3. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/__init__.py +9 -1
  4. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/decorators.py +16 -4
  5. lanscape-1.3.6a2/lanscape/libraries/device_alive.py +227 -0
  6. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/net_tools.py +37 -127
  7. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/scan_config.py +84 -25
  8. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/subnet_scan.py +13 -16
  9. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/main.py +0 -9
  10. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/css/style.css +35 -24
  11. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/scan-config.js +76 -2
  12. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/main.html +0 -7
  13. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/config.html +71 -10
  14. {lanscape-1.3.5a2 → lanscape-1.3.6a2/lanscape.egg-info}/PKG-INFO +18 -2
  15. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/SOURCES.txt +1 -0
  16. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/pyproject.toml +1 -1
  17. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_api.py +7 -3
  18. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_library.py +5 -4
  19. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/LICENSE +0 -0
  20. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/MANIFEST.in +0 -0
  21. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/__main__.py +0 -0
  22. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/__init__.py +0 -0
  23. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/app_scope.py +0 -0
  24. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/errors.py +0 -0
  25. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/ip_parser.py +0 -0
  26. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/logger.py +0 -0
  27. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/mac_lookup.py +0 -0
  28. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/port_manager.py +0 -0
  29. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/runtime_args.py +0 -0
  30. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/service_scan.py +0 -0
  31. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/version_manager.py +0 -0
  32. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/web_browser.py +0 -0
  33. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  34. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  35. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/convert_csv.py +0 -0
  36. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/full.json +0 -0
  37. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/large.json +0 -0
  38. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/medium.json +0 -0
  39. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/small.json +0 -0
  40. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/services/definitions.jsonc +0 -0
  41. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/__init__.py +0 -0
  42. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/app.py +0 -0
  43. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/__init__.py +0 -0
  44. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/__init__.py +0 -0
  45. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/port.py +0 -0
  46. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/scan.py +0 -0
  47. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/tools.py +0 -0
  48. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/web/__init__.py +0 -0
  49. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/web/routes.py +0 -0
  50. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/shutdown_handler.py +0 -0
  51. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  52. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  53. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  54. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  55. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  56. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  57. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  58. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/core.js +0 -0
  59. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/layout-sizing.js +0 -0
  60. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/main.js +0 -0
  61. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/on-tab-close.js +0 -0
  62. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/quietReload.js +0 -0
  63. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/shutdown-server.js +0 -0
  64. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/subnet-info.js +0 -0
  65. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/subnet-selector.js +0 -0
  66. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/lanscape.webmanifest +0 -0
  67. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/base.html +0 -0
  68. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/core/head.html +0 -0
  69. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/core/scripts.html +0 -0
  70. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/error.html +0 -0
  71. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/info.html +0 -0
  72. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/export.html +0 -0
  73. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  74. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/ip-table.html +0 -0
  75. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/overview.html +0 -0
  76. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/scan-error.html +0 -0
  77. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan.html +0 -0
  78. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/shutdown.html +0 -0
  79. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/dependency_links.txt +0 -0
  80. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/requires.txt +0 -0
  81. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/top_level.txt +0 -0
  82. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/setup.cfg +0 -0
  83. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_env.py +0 -0
  84. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_logging.py +0 -0
  85. {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 1.3.5a2
3
+ Version: 1.3.6a2
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -25,7 +25,23 @@ Dynamic: license-file
25
25
  # LANscape
26
26
  A python based local network scanner.
27
27
 
28
- ![screenshot](https://github.com/user-attachments/assets/ba09c656-9fd9-4d74-8426-506d9a5c316c)
28
+ ![screenshot](https://github.com/user-attachments/assets/7d77741e-3bad-4b6b-a33f-6a392adde23f)
29
+
30
+
31
+ PyPi Stats:
32
+
33
+ ![Monthly Downloads](https://img.shields.io/pypi/dm/lanscape)
34
+
35
+ Latest release:
36
+
37
+ ![Releases](https://img.shields.io/github/v/tag/mdennis281/LANscape?sort=date&filter=releases%2F*)
38
+
39
+ Tests:
40
+
41
+ ![pytest](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test.yml?branch=main&label=pytest)
42
+ ![packaging](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test-package.yml?label=packaging)
43
+ ![pylint](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/pylint.yml?branch=main&label=pylint)
44
+
29
45
 
30
46
  ## Local Run
31
47
  ```sh
@@ -1,7 +1,23 @@
1
1
  # LANscape
2
2
  A python based local network scanner.
3
3
 
4
- ![screenshot](https://github.com/user-attachments/assets/ba09c656-9fd9-4d74-8426-506d9a5c316c)
4
+ ![screenshot](https://github.com/user-attachments/assets/7d77741e-3bad-4b6b-a33f-6a392adde23f)
5
+
6
+
7
+ PyPi Stats:
8
+
9
+ ![Monthly Downloads](https://img.shields.io/pypi/dm/lanscape)
10
+
11
+ Latest release:
12
+
13
+ ![Releases](https://img.shields.io/github/v/tag/mdennis281/LANscape?sort=date&filter=releases%2F*)
14
+
15
+ Tests:
16
+
17
+ ![pytest](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test.yml?branch=main&label=pytest)
18
+ ![packaging](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test-package.yml?label=packaging)
19
+ ![pylint](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/pylint.yml?branch=main&label=pylint)
20
+
5
21
 
6
22
  ## Local Run
7
23
  ```sh
@@ -3,10 +3,18 @@ Local network scanner
3
3
  """
4
4
  from lanscape.libraries.subnet_scan import (
5
5
  SubnetScanner,
6
- ScanConfig,
7
6
  ScanManager
8
7
  )
9
8
 
9
+ from lanscape.libraries.scan_config import (
10
+ ScanConfig,
11
+ ArpConfig,
12
+ PingConfig,
13
+ PokeConfig,
14
+ ArpCacheConfig,
15
+ ScanType
16
+ )
17
+
10
18
  from lanscape.libraries.port_manager import PortManager
11
19
 
12
20
  from lanscape.libraries import net_tools
@@ -25,6 +25,20 @@ class JobStats:
25
25
  timing: DefaultDict[str, float] = field(
26
26
  default_factory=lambda: defaultdict(float))
27
27
 
28
+ _instance = None
29
+
30
+ def __init__(self):
31
+ # Only initialize once
32
+ if not hasattr(self, "running"):
33
+ self.running = defaultdict(int)
34
+ self.finished = defaultdict(int)
35
+ self.timing = defaultdict(float)
36
+
37
+ def __new__(cls, *args, **kwargs):
38
+ if cls._instance is None:
39
+ cls._instance = super(JobStats, cls).__new__(cls)
40
+ return cls._instance
41
+
28
42
  def __str__(self):
29
43
  """Return a formatted string representation of the job statistics."""
30
44
  data = [
@@ -53,9 +67,7 @@ class JobStatsMixin: # pylint: disable=too-few-public-methods
53
67
  @property
54
68
  def job_stats(self):
55
69
  """Return the shared JobStats instance."""
56
- if JobStatsMixin._job_stats is None:
57
- JobStatsMixin._job_stats = JobStats()
58
- return JobStatsMixin._job_stats
70
+ return JobStats()
59
71
 
60
72
 
61
73
  def job_tracker(func):
@@ -81,7 +93,7 @@ def job_tracker(func):
81
93
  def wrapper(*args, **kwargs):
82
94
  """Wrap the function to update job statistics before and after execution."""
83
95
  class_instance = args[0]
84
- job_stats = class_instance.job_stats
96
+ job_stats = JobStats()
85
97
  fxn = get_fxn_src_name(
86
98
  func,
87
99
  class_instance
@@ -0,0 +1,227 @@
1
+ """
2
+ Handles device alive checks using various methods.
3
+ """
4
+
5
+ import re
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ import random
10
+ from typing import List
11
+ import psutil
12
+
13
+ from scapy.sendrecv import srp
14
+ from scapy.layers.l2 import ARP, Ether
15
+ from icmplib import ping
16
+
17
+ from lanscape.libraries.net_tools import Device
18
+ from lanscape.libraries.scan_config import (
19
+ ScanConfig, ScanType, PingConfig,
20
+ ArpConfig, PokeConfig, ArpCacheConfig
21
+ )
22
+ from lanscape.libraries.decorators import timeout_enforcer, job_tracker
23
+
24
+
25
+ def is_device_alive(device: Device, scan_config: ScanConfig) -> bool:
26
+ """
27
+ Check if a device is alive based on the configured scan type.
28
+
29
+ Args:
30
+ device (Device): The device to check.
31
+ scan_config (ScanConfig): The configuration for the scan.
32
+
33
+ Returns:
34
+ bool: True if the device is alive, False otherwise.
35
+ """
36
+ methods = scan_config.lookup_type
37
+
38
+ if ScanType.ICMP in methods:
39
+ IcmpLookup.execute(device, scan_config.ping_config)
40
+
41
+ if ScanType.ARP_LOOKUP in methods and not device.alive:
42
+ ArpLookup.execute(device, scan_config.arp_config)
43
+
44
+ if ScanType.ICMP_THEN_ARP in methods and not device.alive:
45
+ IcmpLookup.execute(device, scan_config.ping_config)
46
+ ArpCacheLookup.execute(device, scan_config.arp_cache_config)
47
+
48
+ if ScanType.POKE_THEN_ARP in methods and not device.alive:
49
+ Poker.execute(device, scan_config.poke_config)
50
+ ArpCacheLookup.execute(device, scan_config.arp_cache_config)
51
+
52
+ return device.alive is True
53
+
54
+
55
+ class IcmpLookup():
56
+ """Class to handle ICMP ping lookups for device presence.
57
+
58
+ Raises:
59
+ NotImplementedError: If the platform is not supported.
60
+
61
+ Returns:
62
+ bool: True if the device is reachable via ICMP, False otherwise.
63
+ """
64
+ @classmethod
65
+ @job_tracker
66
+ def execute(cls, device: Device, cfg: PingConfig) -> bool:
67
+ """Perform an ICMP ping lookup for the specified device.
68
+
69
+ Args:
70
+ device (Device): The device to look up.
71
+ cfg (PingConfig): The configuration for the scan.
72
+
73
+ Returns:
74
+ bool: True if the device is reachable via ICMP, False otherwise.
75
+ """
76
+ # Perform up to cfg.attempts rounds of ping(count=cfg.ping_count)
77
+ for _ in range(cfg.attempts):
78
+ result = ping(
79
+ device.ip,
80
+ count=cfg.ping_count,
81
+ interval=cfg.retry_delay,
82
+ timeout=cfg.timeout,
83
+ privileged=psutil.WINDOWS # Use privileged mode on Windows
84
+ )
85
+ if result.is_alive:
86
+ device.alive = True
87
+ break
88
+ return device.alive is True
89
+
90
+
91
+ class ArpCacheLookup():
92
+ """
93
+ Class to handle ARP cache lookups for device presence.
94
+ """
95
+
96
+ @classmethod
97
+ @job_tracker
98
+ def execute(cls, device: Device, cfg: ArpCacheConfig) -> bool:
99
+ """
100
+ Perform an ARP cache lookup for the specified device.
101
+
102
+ Args:
103
+ device (Device): The device to look up.
104
+
105
+ Returns:
106
+ bool: True if the device is found in the ARP cache, False otherwise.
107
+ """
108
+
109
+ command = cls._get_platform_arp_command() + [device.ip]
110
+
111
+ for _ in range(cfg.attempts):
112
+ time.sleep(cfg.wait_before)
113
+ output = subprocess.check_output(command).decode()
114
+ macs = cls._extract_mac_address(output)
115
+ if macs:
116
+ device.macs = macs
117
+ device.alive = True
118
+ break
119
+
120
+ return device.alive is True
121
+
122
+ @classmethod
123
+ def _get_platform_arp_command(cls) -> List[str]:
124
+ """
125
+ Get the ARP command to execute based on the platform.
126
+
127
+ Returns:
128
+ list[str]: The ARP command to execute.
129
+ """
130
+ if psutil.WINDOWS:
131
+ return ['arp', '-a']
132
+ if psutil.LINUX:
133
+ return ['arp', '-n']
134
+ if psutil.MACOS:
135
+ return ['arp', '-n']
136
+
137
+ raise NotImplementedError("Unsupported platform")
138
+
139
+ @classmethod
140
+ def _extract_mac_address(cls, arp_resp: str) -> List[str]:
141
+ """
142
+ Extract MAC addresses from ARP output.
143
+
144
+ Args:
145
+ arp_resp (str): The ARP command output.
146
+
147
+ Returns:
148
+ List[str]: A list of extracted MAC addresses (may be empty).
149
+ """
150
+ arp_resp = arp_resp.replace('-', ':')
151
+ return re.findall(r'..:..:..:..:..:..', arp_resp)
152
+
153
+
154
+ class ArpLookup():
155
+ """
156
+ Class to handle ARP lookups for device presence.
157
+ NOTE: This lookup method requires elevated privileges to access the ARP cache.
158
+
159
+
160
+ [Arp Lookup Requirements](/support/arp-issues.md)
161
+ """
162
+
163
+ @classmethod
164
+ @job_tracker
165
+ def execute(cls, device: Device, cfg: ArpConfig) -> bool:
166
+ """
167
+ Perform an ARP lookup for the specified device.
168
+
169
+ Args:
170
+ device (Device): The device to look up.
171
+
172
+ Returns:
173
+ bool: True if the device is found via ARP, False otherwise.
174
+ """
175
+ enforcer_timeout = cfg.timeout * 2
176
+
177
+ @timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
178
+ def do_arp_lookup():
179
+ arp_request = ARP(pdst=device.ip)
180
+ broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
181
+ packet = broadcast / arp_request
182
+
183
+ answered, _ = srp(packet, timeout=cfg.timeout, verbose=False)
184
+ alive = any(resp.psrc == device.ip for _, resp in answered)
185
+ macs = []
186
+ if alive:
187
+ macs = [resp.hwsrc for _, resp in answered if resp.psrc == device.ip]
188
+ return alive, macs
189
+
190
+ alive, macs = do_arp_lookup()
191
+ if alive:
192
+ device.alive = True
193
+ device.macs = macs
194
+
195
+ return device.alive is True
196
+
197
+
198
+ class Poker():
199
+ """
200
+ Class to handle Poking the device to populate the ARP cache.
201
+ """
202
+
203
+ @classmethod
204
+ @job_tracker
205
+ def execute(cls, device: Device, cfg: PokeConfig):
206
+ """
207
+ Perform a Poke for the specified device.
208
+ Note: the purpose of this is to simply populate the arp cache.
209
+
210
+ Args:
211
+ device (Device): The device to look up.
212
+ cfg (PokeConfig): The configuration for the Poke lookup.
213
+
214
+ Returns:
215
+ None: used to populate the arp cache
216
+ """
217
+ enforcer_timeout = cfg.timeout * cfg.attempts * 2
218
+
219
+ @timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
220
+ def do_poke():
221
+ for _ in range(cfg.attempts):
222
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
223
+ sock.settimeout(cfg.timeout)
224
+ sock.connect_ex((device.ip, random.randint(1024, 65535))) # port shouldn't matter
225
+ sock.close()
226
+
227
+ do_poke()
@@ -4,13 +4,11 @@ import logging
4
4
  import ipaddress
5
5
  import traceback
6
6
  import subprocess
7
- from time import sleep
8
7
  from typing import List, Dict
9
8
  import socket
10
9
  import struct
11
10
  import re
12
11
  import psutil
13
- from icmplib import ping
14
12
 
15
13
  from scapy.sendrecv import srp
16
14
  from scapy.layers.l2 import ARP, Ether
@@ -20,116 +18,13 @@ from lanscape.libraries.service_scan import scan_service
20
18
  from lanscape.libraries.mac_lookup import MacLookup, get_macs
21
19
  from lanscape.libraries.ip_parser import get_address_count, MAX_IPS_ALLOWED
22
20
  from lanscape.libraries.errors import DeviceError
23
- from lanscape.libraries.decorators import job_tracker, JobStatsMixin, timeout_enforcer
24
- from lanscape.libraries.scan_config import ScanType, PingConfig, ArpConfig
21
+ from lanscape.libraries.decorators import job_tracker
25
22
 
26
23
  log = logging.getLogger('NetTools')
24
+ mac_lookup = MacLookup()
27
25
 
28
26
 
29
- class IPAlive(JobStatsMixin):
30
- """Class to check if a device is alive using ARP and/or ping scans."""
31
- caught_errors: List[DeviceError] = []
32
- _icmp_alive: bool = False
33
- _arp_alive: bool = False
34
-
35
- @job_tracker
36
- def is_alive(
37
- self,
38
- ip: str,
39
- scan_type: ScanType = ScanType.BOTH,
40
- arp_config: ArpConfig = ArpConfig(),
41
- ping_config: PingConfig = PingConfig()
42
- ) -> bool:
43
- """
44
- Check if a device is alive by performing ARP and/or ping scans.
45
- """
46
- if scan_type == ScanType.ARP:
47
- return self._arp_lookup(ip, arp_config)
48
- if scan_type == ScanType.PING:
49
- return self._ping_lookup(ip, ping_config)
50
- return self._ping_lookup(ip, ping_config) or self._arp_lookup(ip, arp_config)
51
-
52
- @job_tracker
53
- def _arp_lookup(
54
- self, ip: str,
55
- cfg: ArpConfig = ArpConfig()
56
- ) -> bool:
57
- """Perform an ARP lookup to check if the device is alive."""
58
- enforcer_timeout = cfg.timeout * 1.3
59
-
60
- @timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
61
- def do_arp_lookup():
62
- arp_request = ARP(pdst=ip)
63
- broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
64
- packet = broadcast / arp_request
65
-
66
- answered, _ = srp(packet, timeout=cfg.timeout, verbose=False)
67
- self._arp_alive = any(resp.psrc == ip for _, resp in answered)
68
- return self._arp_alive
69
-
70
- try:
71
- for _ in range(cfg.attempts):
72
- if do_arp_lookup():
73
- return True
74
- except Exception as e:
75
- self.caught_errors.append(DeviceError(e))
76
- return False
77
-
78
- @job_tracker
79
- def _ping_lookup(
80
- self, ip: str,
81
- cfg: PingConfig = PingConfig()
82
- ) -> bool:
83
- """Perform a ping lookup to check if the device is alive using icmplib."""
84
- enforcer_timeout = cfg.timeout * cfg.ping_count * 1.3
85
-
86
- @timeout_enforcer(enforcer_timeout, raise_on_timeout=False)
87
- def do_icmp_ping():
88
- try:
89
- result = ping(
90
- ip,
91
- count=cfg.ping_count,
92
- interval=cfg.retry_delay,
93
- timeout=cfg.timeout,
94
- privileged=psutil.WINDOWS # Use privileged mode on Windows
95
- )
96
- return result.is_alive
97
- except Exception as e:
98
- self.caught_errors.append(DeviceError(e))
99
- # Fallback to system ping command
100
- try:
101
- if psutil.WINDOWS:
102
- cmd = [
103
- "ping", "-n", str(cfg.ping_count),
104
- "-w", str(int(cfg.timeout * 1000)), ip
105
- ]
106
- else:
107
- cmd = ["ping", "-c",
108
- str(cfg.ping_count), "-W", str(cfg.timeout), ip]
109
-
110
- result = subprocess.run(
111
- cmd, stdout=subprocess.PIPE,
112
- stderr=subprocess.PIPE,
113
- text=True, check=False
114
- )
115
- return result.returncode == 0
116
- except subprocess.CalledProcessError as fallback_error:
117
- self.caught_errors.append(DeviceError(fallback_error))
118
- return False
119
-
120
- try:
121
- for _ in range(cfg.attempts):
122
- if do_icmp_ping():
123
- self._icmp_alive = True
124
- return True
125
- sleep(cfg.retry_delay)
126
- except Exception as e:
127
- self.caught_errors.append(DeviceError(e))
128
- self._icmp_alive = False
129
- return False
130
-
131
-
132
- class Device(IPAlive):
27
+ class Device:
133
28
  """Represents a network device with metadata and scanning capabilities."""
134
29
 
135
30
  def __init__(self, ip: str):
@@ -144,13 +39,12 @@ class Device(IPAlive):
144
39
  self.services: Dict[str, List[int]] = {}
145
40
  self.caught_errors: List[DeviceError] = []
146
41
  self.log = logging.getLogger('Device')
147
- self._mac_lookup = MacLookup()
148
42
 
149
43
  def get_metadata(self):
150
44
  """Retrieve metadata such as hostname and MAC addresses."""
151
45
  if self.alive:
152
46
  self.hostname = self._get_hostname()
153
- self.macs = self._get_mac_addresses()
47
+ self._get_mac_addresses()
154
48
 
155
49
  def dict(self) -> dict:
156
50
  """Convert the device object to a dictionary."""
@@ -191,9 +85,12 @@ class Device(IPAlive):
191
85
  @job_tracker
192
86
  def _get_mac_addresses(self):
193
87
  """Get the possible MAC addresses of a network device given its IP address."""
194
- macs = get_macs(self.ip)
195
- mac_selector.import_macs(macs)
196
- return macs
88
+ # job may already be done depending on
89
+ # the strat from isalive
90
+ if not self.macs:
91
+ self.macs = get_macs(self.ip)
92
+ mac_selector.import_macs(self.macs)
93
+ return self.macs
197
94
 
198
95
  @job_tracker
199
96
  def _get_hostname(self):
@@ -208,7 +105,7 @@ class Device(IPAlive):
208
105
  @job_tracker
209
106
  def _get_manufacturer(self, mac_addr=None):
210
107
  """Get the manufacturer of a network device given its MAC address."""
211
- return self._mac_lookup.lookup_vendor(mac_addr) if mac_addr else None
108
+ return mac_lookup.lookup_vendor(mac_addr) if mac_addr else None
212
109
 
213
110
 
214
111
  class MacSelector:
@@ -562,19 +459,32 @@ def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
562
459
  return selected.get("subnet", "")
563
460
 
564
461
 
462
+ class ArpSupportChecker:
463
+ """
464
+ Singleton class to check if ARP requests are supported on the current system.
465
+ The check is only performed once.
466
+ """
467
+ _supported = None
468
+
469
+ @classmethod
470
+ def is_supported(cls):
471
+ """one time check if ARP requests are supported on this system"""
472
+ if cls._supported is not None:
473
+ return cls._supported
474
+ try:
475
+ arp_request = ARP(pdst='0.0.0.0')
476
+ broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
477
+ packet = broadcast / arp_request
478
+ srp(packet, timeout=0, verbose=False)
479
+ cls._supported = True
480
+ except (Scapy_Exception, PermissionError, RuntimeError):
481
+ cls._supported = False
482
+ return cls._supported
483
+
484
+
565
485
  def is_arp_supported():
566
486
  """
567
- Check if ARP requests are supported on the current platform.
487
+ Check if ARP requests are supported on the current system.
488
+ Only runs the check once.
568
489
  """
569
- try:
570
- arp_request = ARP(pdst='0.0.0.0')
571
- broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
572
- packet = broadcast / arp_request
573
-
574
- srp(packet, timeout=0, verbose=False)
575
- return True
576
- # Scapy_Exception = MacOS
577
- # PermissionError = Linux
578
- # RuntimeError = Windows
579
- except (Scapy_Exception, PermissionError, RuntimeError):
580
- return False
490
+ return ArpSupportChecker.is_supported()