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.
- {lanscape-1.3.5a2/lanscape.egg-info → lanscape-1.3.6a2}/PKG-INFO +18 -2
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/README.md +17 -1
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/__init__.py +9 -1
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/decorators.py +16 -4
- lanscape-1.3.6a2/lanscape/libraries/device_alive.py +227 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/net_tools.py +37 -127
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/scan_config.py +84 -25
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/subnet_scan.py +13 -16
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/main.py +0 -9
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/css/style.css +35 -24
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/scan-config.js +76 -2
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/main.html +0 -7
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/config.html +71 -10
- {lanscape-1.3.5a2 → lanscape-1.3.6a2/lanscape.egg-info}/PKG-INFO +18 -2
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/SOURCES.txt +1 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/pyproject.toml +1 -1
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_api.py +7 -3
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_library.py +5 -4
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/LICENSE +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/MANIFEST.in +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/__main__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/__init__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/app_scope.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/errors.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/ip_parser.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/logger.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/mac_lookup.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/port_manager.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/runtime_args.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/service_scan.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/version_manager.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/libraries/web_browser.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/__init__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/app.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/port.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/scan.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/api/tools.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/blueprints/web/routes.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/shutdown_handler.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/core.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/main.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/base.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/info.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/requires.txt +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/setup.cfg +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_env.py +0 -0
- {lanscape-1.3.5a2 → lanscape-1.3.6a2}/tests/test_logging.py +0 -0
- {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.
|
|
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
|
-

|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
PyPi Stats:
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Latest release:
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
Tests:
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
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
|
-

|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
PyPi Stats:
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Latest release:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
Tests:
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
|
487
|
+
Check if ARP requests are supported on the current system.
|
|
488
|
+
Only runs the check once.
|
|
568
489
|
"""
|
|
569
|
-
|
|
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()
|