lanscape 1.3.5a2__tar.gz → 1.3.6__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 (86) hide show
  1. {lanscape-1.3.5a2/lanscape.egg-info → lanscape-1.3.6}/PKG-INFO +27 -10
  2. lanscape-1.3.6/README.md +66 -0
  3. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/__init__.py +9 -1
  4. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/decorators.py +16 -4
  5. lanscape-1.3.6/lanscape/libraries/device_alive.py +229 -0
  6. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/net_tools.py +37 -127
  7. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/scan_config.py +84 -25
  8. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/subnet_scan.py +13 -16
  9. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/main.py +0 -9
  10. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/css/style.css +35 -24
  11. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/scan-config.js +76 -2
  12. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/main.html +0 -7
  13. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/config.html +71 -10
  14. {lanscape-1.3.5a2 → lanscape-1.3.6/lanscape.egg-info}/PKG-INFO +27 -10
  15. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape.egg-info/SOURCES.txt +1 -0
  16. {lanscape-1.3.5a2 → lanscape-1.3.6}/pyproject.toml +1 -1
  17. {lanscape-1.3.5a2 → lanscape-1.3.6}/tests/test_api.py +7 -3
  18. {lanscape-1.3.5a2 → lanscape-1.3.6}/tests/test_library.py +5 -4
  19. lanscape-1.3.5a2/README.md +0 -49
  20. {lanscape-1.3.5a2 → lanscape-1.3.6}/LICENSE +0 -0
  21. {lanscape-1.3.5a2 → lanscape-1.3.6}/MANIFEST.in +0 -0
  22. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/__main__.py +0 -0
  23. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/__init__.py +0 -0
  24. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/app_scope.py +0 -0
  25. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/errors.py +0 -0
  26. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/ip_parser.py +0 -0
  27. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/logger.py +0 -0
  28. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/mac_lookup.py +0 -0
  29. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/port_manager.py +0 -0
  30. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/runtime_args.py +0 -0
  31. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/service_scan.py +0 -0
  32. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/version_manager.py +0 -0
  33. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/libraries/web_browser.py +0 -0
  34. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  35. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  36. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/ports/convert_csv.py +0 -0
  37. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/ports/full.json +0 -0
  38. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/ports/large.json +0 -0
  39. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/ports/medium.json +0 -0
  40. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/ports/small.json +0 -0
  41. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/resources/services/definitions.jsonc +0 -0
  42. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/__init__.py +0 -0
  43. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/app.py +0 -0
  44. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/__init__.py +0 -0
  45. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/api/__init__.py +0 -0
  46. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/api/port.py +0 -0
  47. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/api/scan.py +0 -0
  48. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/api/tools.py +0 -0
  49. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/web/__init__.py +0 -0
  50. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/blueprints/web/routes.py +0 -0
  51. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/shutdown_handler.py +0 -0
  52. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  53. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  54. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  55. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  56. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  57. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  58. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  59. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/core.js +0 -0
  60. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/layout-sizing.js +0 -0
  61. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/main.js +0 -0
  62. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/on-tab-close.js +0 -0
  63. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/quietReload.js +0 -0
  64. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/shutdown-server.js +0 -0
  65. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/subnet-info.js +0 -0
  66. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/js/subnet-selector.js +0 -0
  67. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/static/lanscape.webmanifest +0 -0
  68. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/base.html +0 -0
  69. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/core/head.html +0 -0
  70. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/core/scripts.html +0 -0
  71. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/error.html +0 -0
  72. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/info.html +0 -0
  73. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/export.html +0 -0
  74. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  75. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/ip-table.html +0 -0
  76. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/overview.html +0 -0
  77. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan/scan-error.html +0 -0
  78. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/scan.html +0 -0
  79. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape/ui/templates/shutdown.html +0 -0
  80. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape.egg-info/dependency_links.txt +0 -0
  81. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape.egg-info/requires.txt +0 -0
  82. {lanscape-1.3.5a2 → lanscape-1.3.6}/lanscape.egg-info/top_level.txt +0 -0
  83. {lanscape-1.3.5a2 → lanscape-1.3.6}/setup.cfg +0 -0
  84. {lanscape-1.3.5a2 → lanscape-1.3.6}/tests/test_env.py +0 -0
  85. {lanscape-1.3.5a2 → lanscape-1.3.6}/tests/test_logging.py +0 -0
  86. {lanscape-1.3.5a2 → lanscape-1.3.6}/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.6
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -25,9 +25,28 @@ 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
29
 
30
- ## Local Run
30
+
31
+ PyPi Stats:
32
+
33
+ ![Version](https://img.shields.io/pypi/v/lanscape)
34
+ ![Monthly Downloads](https://img.shields.io/pypi/dm/lanscape)
35
+
36
+ Latest release:
37
+
38
+ ![Stable](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=releases%2F*&label=Stable)
39
+ ![Beta](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=pre-releases%2F*b*&label=Beta)
40
+ ![Alpha](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=pre-releases%2F*a*&label=Alpha)
41
+
42
+ Health:
43
+
44
+ ![pytest](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test.yml?branch=main&label=pytest)
45
+ ![packaging](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test-package.yml?label=packaging)
46
+ ![pylint](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/pylint.yml?branch=main&label=pylint)
47
+
48
+
49
+ ## Installation
31
50
  ```sh
32
51
  pip install lanscape
33
52
  python -m lanscape
@@ -55,16 +74,14 @@ The program does an ARP lookup to determine the MAC address. This lookup
55
74
  can sometimes require admin-level permissions to retrieve accurate results.
56
75
  *Try elevating your shell before execution.*
57
76
 
58
- ### Message "WARNING: No libpcap provider available ! pcap won't be used"
59
- This is a missing dependency related to the ARP lookup. This is handled in the code, but you would get marginally faster/better results with this installed: [npcap download](https://npcap.com/#download)
60
-
61
77
  ### The accuracy of the devices found is low
62
- I use a combination of ARP and Ping to determine if a device is online. This method drops in stability when used in many threads.
78
+ I use a combination of ARP, ICMP & port testing to determine if a device is online. Sometimes the scan settings can use some tuning to maximize both speed and accuracy.
79
+
63
80
  Recommendations:
64
81
 
65
- - Drop parallelism value (advanced dropdown)
66
- - Use python > 3.10 im noticing threadpool improvements after this version
67
- - Create a bug - I'm curious
82
+ - Adjust scan configuration
83
+ - Configure ARP lookup [ARP lookup setup](./support/arp-issues.md)
84
+ - Create a bug
68
85
 
69
86
 
70
87
  ### Something else
@@ -0,0 +1,66 @@
1
+ # LANscape
2
+ A python based local network scanner.
3
+
4
+ ![screenshot](https://github.com/user-attachments/assets/7d77741e-3bad-4b6b-a33f-6a392adde23f)
5
+
6
+
7
+ PyPi Stats:
8
+
9
+ ![Version](https://img.shields.io/pypi/v/lanscape)
10
+ ![Monthly Downloads](https://img.shields.io/pypi/dm/lanscape)
11
+
12
+ Latest release:
13
+
14
+ ![Stable](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=releases%2F*&label=Stable)
15
+ ![Beta](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=pre-releases%2F*b*&label=Beta)
16
+ ![Alpha](https://img.shields.io/github/v/tag/mdennis281/LANScape?filter=pre-releases%2F*a*&label=Alpha)
17
+
18
+ Health:
19
+
20
+ ![pytest](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test.yml?branch=main&label=pytest)
21
+ ![packaging](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/test-package.yml?label=packaging)
22
+ ![pylint](https://img.shields.io/github/actions/workflow/status/mdennis281/LANscape/pylint.yml?branch=main&label=pylint)
23
+
24
+
25
+ ## Installation
26
+ ```sh
27
+ pip install lanscape
28
+ python -m lanscape
29
+ ```
30
+
31
+ ## Flags
32
+ - `--port <port number>` port of the flask app (default: automagic)
33
+ - `--persistent` dont shutdown server when browser tab is closed (default: false)
34
+ - `--reloader` essentially flask debug mode- good for local development (default: false)
35
+ - `--logfile <path>` save log output to the given file path
36
+ - `--loglevel <level>` set the logger's log level (default: INFO)
37
+ - `--flask-logging` turn on flask logging (default: false)
38
+
39
+ Examples:
40
+ ```shell
41
+ python -m lanscape --reloader
42
+ python -m lanscape --port 5002
43
+ python -m lanscape --logfile /tmp/lanscape.log --loglevel DEBUG
44
+ ```
45
+
46
+ ## Troubleshooting
47
+
48
+ ### MAC Address / Manufacturer is inaccurate/unknown
49
+ The program does an ARP lookup to determine the MAC address. This lookup
50
+ can sometimes require admin-level permissions to retrieve accurate results.
51
+ *Try elevating your shell before execution.*
52
+
53
+ ### The accuracy of the devices found is low
54
+ I use a combination of ARP, ICMP & port testing to determine if a device is online. Sometimes the scan settings can use some tuning to maximize both speed and accuracy.
55
+
56
+ Recommendations:
57
+
58
+ - Adjust scan configuration
59
+ - Configure ARP lookup [ARP lookup setup](./support/arp-issues.md)
60
+ - Create a bug
61
+
62
+
63
+ ### Something else
64
+ Feel free to submit a github issue detailing your experience.
65
+
66
+
@@ -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,229 @@
1
+ """
2
+ Handles device alive checks using various methods.
3
+ """
4
+
5
+ import re
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ from typing import List
10
+ import psutil
11
+
12
+ from scapy.sendrecv import srp
13
+ from scapy.layers.l2 import ARP, Ether
14
+ from icmplib import ping
15
+
16
+ from lanscape.libraries.net_tools import Device
17
+ from lanscape.libraries.scan_config import (
18
+ ScanConfig, ScanType, PingConfig,
19
+ ArpConfig, PokeConfig, ArpCacheConfig
20
+ )
21
+ from lanscape.libraries.decorators import timeout_enforcer, job_tracker
22
+
23
+
24
+ def is_device_alive(device: Device, scan_config: ScanConfig) -> bool:
25
+ """
26
+ Check if a device is alive based on the configured scan type.
27
+
28
+ Args:
29
+ device (Device): The device to check.
30
+ scan_config (ScanConfig): The configuration for the scan.
31
+
32
+ Returns:
33
+ bool: True if the device is alive, False otherwise.
34
+ """
35
+ methods = scan_config.lookup_type
36
+
37
+ if ScanType.ICMP in methods:
38
+ IcmpLookup.execute(device, scan_config.ping_config)
39
+
40
+ if ScanType.ARP_LOOKUP in methods and not device.alive:
41
+ ArpLookup.execute(device, scan_config.arp_config)
42
+
43
+ if ScanType.ICMP_THEN_ARP in methods and not device.alive:
44
+ IcmpLookup.execute(device, scan_config.ping_config)
45
+ ArpCacheLookup.execute(device, scan_config.arp_cache_config)
46
+
47
+ if ScanType.POKE_THEN_ARP in methods and not device.alive:
48
+ Poker.execute(device, scan_config.poke_config)
49
+ ArpCacheLookup.execute(device, scan_config.arp_cache_config)
50
+
51
+ return device.alive is True
52
+
53
+
54
+ class IcmpLookup():
55
+ """Class to handle ICMP ping lookups for device presence.
56
+
57
+ Raises:
58
+ NotImplementedError: If the platform is not supported.
59
+
60
+ Returns:
61
+ bool: True if the device is reachable via ICMP, False otherwise.
62
+ """
63
+ @classmethod
64
+ @job_tracker
65
+ def execute(cls, device: Device, cfg: PingConfig) -> bool:
66
+ """Perform an ICMP ping lookup for the specified device.
67
+
68
+ Args:
69
+ device (Device): The device to look up.
70
+ cfg (PingConfig): The configuration for the scan.
71
+
72
+ Returns:
73
+ bool: True if the device is reachable via ICMP, False otherwise.
74
+ """
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
87
+ return device.alive is True
88
+
89
+
90
+ class ArpCacheLookup():
91
+ """
92
+ Class to handle ARP cache lookups for device presence.
93
+ """
94
+
95
+ @classmethod
96
+ @job_tracker
97
+ def execute(cls, device: Device, cfg: ArpCacheConfig) -> bool:
98
+ """
99
+ Perform an ARP cache lookup for the specified device.
100
+
101
+ Args:
102
+ device (Device): The device to look up.
103
+
104
+ Returns:
105
+ bool: True if the device is found in the ARP cache, False otherwise.
106
+ """
107
+
108
+ command = cls._get_platform_arp_command() + [device.ip]
109
+
110
+ for _ in range(cfg.attempts):
111
+ time.sleep(cfg.wait_before)
112
+ output = subprocess.check_output(command).decode()
113
+ macs = cls._extract_mac_address(output)
114
+ if macs:
115
+ device.macs = macs
116
+ device.alive = True
117
+ break
118
+
119
+ return device.alive is True
120
+
121
+ @classmethod
122
+ def _get_platform_arp_command(cls) -> List[str]:
123
+ """
124
+ Get the ARP command to execute based on the platform.
125
+
126
+ Returns:
127
+ list[str]: The ARP command to execute.
128
+ """
129
+ if psutil.WINDOWS:
130
+ return ['arp', '-a']
131
+ if psutil.LINUX:
132
+ return ['arp', '-n']
133
+ if psutil.MACOS:
134
+ return ['arp', '-n']
135
+
136
+ raise NotImplementedError("Unsupported platform")
137
+
138
+ @classmethod
139
+ def _extract_mac_address(cls, arp_resp: str) -> List[str]:
140
+ """
141
+ Extract MAC addresses from ARP output.
142
+
143
+ Args:
144
+ arp_resp (str): The ARP command output.
145
+
146
+ Returns:
147
+ List[str]: A list of extracted MAC addresses (may be empty).
148
+ """
149
+ arp_resp = arp_resp.replace('-', ':')
150
+ return re.findall(r'..:..:..:..:..:..', arp_resp)
151
+
152
+
153
+ class ArpLookup():
154
+ """
155
+ Class to handle ARP lookups for device presence.
156
+ NOTE: This lookup method requires elevated privileges to access the ARP cache.
157
+
158
+
159
+ [Arp Lookup Requirements](/support/arp-issues.md)
160
+ """
161
+
162
+ @classmethod
163
+ @job_tracker
164
+ def execute(cls, device: Device, cfg: ArpConfig) -> bool:
165
+ """
166
+ Perform an ARP lookup for the specified device.
167
+
168
+ Args:
169
+ device (Device): The device to look up.
170
+
171
+ Returns:
172
+ bool: True if the device is found via ARP, False otherwise.
173
+ """
174
+ enforcer_timeout = cfg.timeout * 2
175
+
176
+ @timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
177
+ def do_arp_lookup():
178
+ arp_request = ARP(pdst=device.ip)
179
+ broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
180
+ packet = broadcast / arp_request
181
+
182
+ answered, _ = srp(packet, timeout=cfg.timeout, verbose=False)
183
+ alive = any(resp.psrc == device.ip for _, resp in answered)
184
+ macs = []
185
+ if alive:
186
+ macs = [resp.hwsrc for _, resp in answered if resp.psrc == device.ip]
187
+ return alive, macs
188
+
189
+ alive, macs = do_arp_lookup()
190
+ if alive:
191
+ device.alive = True
192
+ device.macs = macs
193
+
194
+ return device.alive is True
195
+
196
+
197
+ class Poker():
198
+ """
199
+ Class to handle Poking the device to populate the ARP cache.
200
+ """
201
+
202
+ @classmethod
203
+ @job_tracker
204
+ def execute(cls, device: Device, cfg: PokeConfig):
205
+ """
206
+ Perform a Poke for the specified device.
207
+ Note: the purpose of this is to simply populate the arp cache.
208
+
209
+ Args:
210
+ device (Device): The device to look up.
211
+ cfg (PokeConfig): The configuration for the Poke lookup.
212
+
213
+ Returns:
214
+ None: used to populate the arp cache
215
+ """
216
+ enforcer_timeout = cfg.timeout * cfg.attempts * 2
217
+
218
+ @timeout_enforcer(enforcer_timeout, raise_on_timeout=True)
219
+ def do_poke():
220
+ # Use a small set of common ports likely to be filtered but still trigger ARP
221
+ common_ports = [80, 443, 22]
222
+ for i in range(cfg.attempts):
223
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
224
+ sock.settimeout(cfg.timeout)
225
+ port = common_ports[i % len(common_ports)]
226
+ sock.connect_ex((device.ip, port))
227
+ sock.close()
228
+
229
+ 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()