lanscape 1.3.5a2__tar.gz → 1.3.6a1__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.6a1}/PKG-INFO +1 -1
  2. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/__init__.py +9 -1
  3. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/decorators.py +16 -4
  4. lanscape-1.3.6a1/lanscape/libraries/device_alive.py +227 -0
  5. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/net_tools.py +37 -127
  6. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/scan_config.py +84 -25
  7. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/subnet_scan.py +13 -16
  8. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/main.py +0 -9
  9. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/css/style.css +35 -24
  10. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/scan-config.js +76 -2
  11. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/main.html +0 -7
  12. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/config.html +71 -10
  13. {lanscape-1.3.5a2 → lanscape-1.3.6a1/lanscape.egg-info}/PKG-INFO +1 -1
  14. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape.egg-info/SOURCES.txt +1 -0
  15. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/pyproject.toml +1 -1
  16. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/tests/test_api.py +7 -3
  17. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/tests/test_library.py +5 -4
  18. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/LICENSE +0 -0
  19. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/MANIFEST.in +0 -0
  20. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/README.md +0 -0
  21. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/__main__.py +0 -0
  22. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/__init__.py +0 -0
  23. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/app_scope.py +0 -0
  24. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/errors.py +0 -0
  25. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/ip_parser.py +0 -0
  26. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/logger.py +0 -0
  27. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/mac_lookup.py +0 -0
  28. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/port_manager.py +0 -0
  29. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/runtime_args.py +0 -0
  30. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/service_scan.py +0 -0
  31. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/version_manager.py +0 -0
  32. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/libraries/web_browser.py +0 -0
  33. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  34. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  35. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/ports/convert_csv.py +0 -0
  36. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/ports/full.json +0 -0
  37. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/ports/large.json +0 -0
  38. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/ports/medium.json +0 -0
  39. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/ports/small.json +0 -0
  40. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/resources/services/definitions.jsonc +0 -0
  41. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/__init__.py +0 -0
  42. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/app.py +0 -0
  43. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/__init__.py +0 -0
  44. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
  45. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/api/port.py +0 -0
  46. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/api/scan.py +0 -0
  47. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/api/tools.py +0 -0
  48. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
  49. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/blueprints/web/routes.py +0 -0
  50. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/shutdown_handler.py +0 -0
  51. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  52. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  53. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  54. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  55. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  56. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  57. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  58. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/core.js +0 -0
  59. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
  60. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/main.js +0 -0
  61. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
  62. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/quietReload.js +0 -0
  63. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
  64. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/subnet-info.js +0 -0
  65. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
  66. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/static/lanscape.webmanifest +0 -0
  67. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/base.html +0 -0
  68. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/core/head.html +0 -0
  69. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/core/scripts.html +0 -0
  70. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/error.html +0 -0
  71. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/info.html +0 -0
  72. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/export.html +0 -0
  73. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  74. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/ip-table.html +0 -0
  75. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/overview.html +0 -0
  76. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
  77. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/scan.html +0 -0
  78. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape/ui/templates/shutdown.html +0 -0
  79. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape.egg-info/dependency_links.txt +0 -0
  80. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape.egg-info/requires.txt +0 -0
  81. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/lanscape.egg-info/top_level.txt +0 -0
  82. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/setup.cfg +0 -0
  83. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/tests/test_env.py +0 -0
  84. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/tests/test_logging.py +0 -0
  85. {lanscape-1.3.5a2 → lanscape-1.3.6a1}/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.6a1
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -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()
@@ -4,6 +4,7 @@ Provides classes and utilities to configure different types of network scans
4
4
  including ping scans, ARP scans, and port scanning.
5
5
  """
6
6
 
7
+ import os
7
8
  from typing import List, Dict
8
9
  import ipaddress
9
10
  from enum import Enum
@@ -90,17 +91,81 @@ class ArpConfig(BaseModel):
90
91
  return f'ArpCfg(timeout={self.timeout}, attempts={self.attempts})'
91
92
 
92
93
 
94
+ class ArpCacheConfig(BaseModel):
95
+ """Config for fetching from ARP cache"""
96
+ attempts: int = 1
97
+ wait_before: float = 0.2
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: dict) -> 'ArpCacheConfig':
101
+ """
102
+ Create an ArpCacheConfig instance from a dictionary.
103
+
104
+ Args:
105
+ data: Dictionary containing ArpCacheConfig parameters
106
+
107
+ Returns:
108
+ A new ArpCacheConfig instance with the provided settings
109
+ """
110
+ return cls.model_validate(data)
111
+
112
+ def to_dict(self) -> dict:
113
+ """
114
+ Convert the ArpCacheConfig instance to a dictionary.
115
+
116
+ Returns:
117
+ Dictionary representation of the ArpCacheConfig
118
+ """
119
+ return self.model_dump()
120
+
121
+ def __str__(self):
122
+ return f'ArpCacheCfg(wait_before={self.wait_before}, attempts={self.attempts})'
123
+
124
+
125
+ class PokeConfig(BaseModel):
126
+ """
127
+ Poking essentially involves sending a TCP packet to a specific port on a device
128
+ to elicit a response. Not so much expecting a response, but it should at least
129
+ trigger an ARP request.
130
+ """
131
+ attempts: int = 1
132
+ timeout: float = 2.0
133
+
134
+ @classmethod
135
+ def from_dict(cls, data: dict) -> 'PokeConfig':
136
+ """
137
+ Create a PokeConfig instance from a dictionary.
138
+
139
+ Args:
140
+ data: Dictionary containing PokeConfig parameters
141
+
142
+ Returns:
143
+ A new PokeConfig instance with the provided settings
144
+ """
145
+ return cls.model_validate(data)
146
+
147
+ def to_dict(self) -> dict:
148
+ """
149
+ Convert the PokeConfig instance to a dictionary.
150
+
151
+ Returns:
152
+ Dictionary representation of the PokeConfig
153
+ """
154
+ return self.model_dump()
155
+
156
+
93
157
  class ScanType(Enum):
94
158
  """
95
159
  Enumeration of supported network scan types.
96
160
 
97
161
  PING: Uses ICMP echo requests to determine if hosts are alive
98
162
  ARP: Uses Address Resolution Protocol to discover hosts on the local network
99
- BOTH: Uses both PING and ARP methods for maximum coverage
163
+
100
164
  """
101
- PING = 'ping'
102
- ARP = 'arp'
103
- BOTH = 'both'
165
+ ICMP = 'ICMP'
166
+ ARP_LOOKUP = 'ARP_LOOKUP'
167
+ POKE_THEN_ARP = 'POKE_THEN_ARP'
168
+ ICMP_THEN_ARP = 'ICMP_THEN_ARP'
104
169
 
105
170
 
106
171
  class ScanConfig(BaseModel):
@@ -113,18 +178,20 @@ class ScanConfig(BaseModel):
113
178
  subnet: str
114
179
  port_list: str
115
180
  t_multiplier: float = 1.0
116
- t_cnt_port_scan: int = 10
117
- t_cnt_port_test: int = 128
118
- t_cnt_isalive: int = 256
181
+ t_cnt_port_scan: int = os.cpu_count()
182
+ t_cnt_port_test: int = os.cpu_count() * 4
183
+ t_cnt_isalive: int = os.cpu_count() * 6
119
184
 
120
185
  task_scan_ports: bool = True
121
186
  # below wont run if above false
122
187
  task_scan_port_services: bool = False # disabling until more stable
123
188
 
124
- lookup_type: ScanType = ScanType.BOTH
189
+ lookup_type: List[ScanType] = [ScanType.ICMP_THEN_ARP]
125
190
 
126
191
  ping_config: PingConfig = Field(default_factory=PingConfig)
127
192
  arp_config: ArpConfig = Field(default_factory=ArpConfig)
193
+ poke_config: PokeConfig = Field(default_factory=PokeConfig)
194
+ arp_cache_config: ArpCacheConfig = Field(default_factory=ArpCacheConfig)
128
195
 
129
196
  def t_cnt(self, thread_id: str) -> int:
130
197
  """
@@ -143,32 +210,20 @@ class ScanConfig(BaseModel):
143
210
  """
144
211
  Create a ScanConfig instance from a dictionary.
145
212
 
146
- Handles special cases like converting string enum values to proper Enum types.
147
-
148
213
  Args:
149
214
  data: Dictionary containing ScanConfig parameters
150
215
 
151
216
  Returns:
152
217
  A new ScanConfig instance with the provided settings
153
218
  """
154
- # Handle special cases before validation
155
- if isinstance(data.get('lookup_type'), str):
156
- data['lookup_type'] = ScanType[data['lookup_type'].upper()]
157
219
 
158
220
  return cls.model_validate(data)
159
221
 
160
222
  def to_dict(self) -> dict:
161
223
  """
162
- Convert the ScanConfig instance to a dictionary.
163
-
164
- Handles special cases like converting Enum values to strings.
165
-
166
- Returns:
167
- Dictionary representation of the ScanConfig
224
+ Convert the ScanConfig instance to a json-serializable dictionary.
168
225
  """
169
- dump = self.model_dump()
170
- dump['lookup_type'] = self.lookup_type.value
171
- return dump
226
+ return self.model_dump(mode="json")
172
227
 
173
228
  def get_ports(self) -> List[int]:
174
229
  """
@@ -191,7 +246,7 @@ class ScanConfig(BaseModel):
191
246
  def __str__(self):
192
247
  a = f'subnet={self.subnet}'
193
248
  b = f'ports={self.port_list}'
194
- c = f'scan_type={self.lookup_type.value}'
249
+ c = f'scan_type={[st.value for st in self.lookup_type]}'
195
250
  return f'ScanConfig({a}, {b}, {c})'
196
251
 
197
252
 
@@ -205,7 +260,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
205
260
  t_cnt_isalive=64,
206
261
  task_scan_ports=True,
207
262
  task_scan_port_services=False,
208
- lookup_type=ScanType.BOTH,
263
+ lookup_type=[ScanType.ICMP_THEN_ARP, ScanType.ARP_LOOKUP],
209
264
  arp_config=ArpConfig(
210
265
  attempts=3,
211
266
  timeout=2.5
@@ -215,6 +270,10 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
215
270
  ping_count=2,
216
271
  timeout=1.5,
217
272
  retry_delay=0.5
273
+ ),
274
+ arp_cache_config=ArpCacheConfig(
275
+ attempts=2,
276
+ wait_before=0.3
218
277
  )
219
278
  ),
220
279
  'fast': ScanConfig(
@@ -225,7 +284,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
225
284
  t_cnt_isalive=512,
226
285
  task_scan_ports=True,
227
286
  task_scan_port_services=False,
228
- lookup_type=ScanType.BOTH,
287
+ lookup_type=[ScanType.POKE_THEN_ARP],
229
288
  arp_config=ArpConfig(
230
289
  attempts=1,
231
290
  timeout=1.0