lanscape 1.3.4__tar.gz → 1.3.5a2__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.4/lanscape.egg-info → lanscape-1.3.5a2}/PKG-INFO +1 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/app_scope.py +0 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/decorators.py +10 -5
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/errors.py +10 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/ip_parser.py +73 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/logger.py +29 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/mac_lookup.py +5 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/net_tools.py +139 -68
- lanscape-1.3.5a2/lanscape/libraries/port_manager.py +150 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/runtime_args.py +12 -0
- lanscape-1.3.5a2/lanscape/libraries/scan_config.py +240 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/service_scan.py +3 -3
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/subnet_scan.py +104 -16
- lanscape-1.3.5a2/lanscape/libraries/version_manager.py +99 -0
- lanscape-1.3.5a2/lanscape/libraries/web_browser.py +210 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/mac_addresses/convert_csv.py +13 -2
- lanscape-1.3.5a2/lanscape/resources/ports/convert_csv.py +40 -0
- lanscape-1.3.5a2/lanscape/ui/__init__.py +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/app.py +32 -36
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/__init__.py +4 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/__init__.py +2 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/port.py +46 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/scan.py +58 -10
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/tools.py +17 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/web/__init__.py +4 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/blueprints/web/routes.py +52 -5
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/main.py +17 -7
- lanscape-1.3.5a2/lanscape/ui/shutdown_handler.py +57 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/css/style.css +94 -20
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/main.js +25 -48
- lanscape-1.3.5a2/lanscape/ui/static/js/scan-config.js +107 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/lanscape.webmanifest +4 -3
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/main.html +39 -36
- lanscape-1.3.5a2/lanscape/ui/templates/scan/config.html +168 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2/lanscape.egg-info}/PKG-INFO +1 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape.egg-info/SOURCES.txt +4 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/pyproject.toml +1 -1
- {lanscape-1.3.4 → lanscape-1.3.5a2}/tests/test_api.py +22 -4
- {lanscape-1.3.4 → lanscape-1.3.5a2}/tests/test_env.py +18 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/tests/test_library.py +22 -3
- {lanscape-1.3.4 → lanscape-1.3.5a2}/tests/test_logging.py +27 -4
- {lanscape-1.3.4 → lanscape-1.3.5a2}/tests/test_utils.py +27 -3
- lanscape-1.3.4/lanscape/libraries/port_manager.py +0 -67
- lanscape-1.3.4/lanscape/libraries/scan_config.py +0 -97
- lanscape-1.3.4/lanscape/libraries/version_manager.py +0 -56
- lanscape-1.3.4/lanscape/libraries/web_browser.py +0 -142
- lanscape-1.3.4/lanscape/resources/ports/convert_csv.py +0 -30
- {lanscape-1.3.4 → lanscape-1.3.5a2}/LICENSE +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/MANIFEST.in +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/README.md +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/__init__.py +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/__main__.py +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/libraries/__init__.py +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/ports/full.json +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/ports/large.json +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/ports/small.json +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/core.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/base.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/error.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/info.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape.egg-info/requires.txt +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-1.3.4 → lanscape-1.3.5a2}/setup.cfg +0 -0
|
@@ -18,9 +18,12 @@ class JobStats:
|
|
|
18
18
|
"""
|
|
19
19
|
Tracks statistics for job execution, including running, finished, and timing data.
|
|
20
20
|
"""
|
|
21
|
-
running: DefaultDict[str, int] = field(
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
running: DefaultDict[str, int] = field(
|
|
22
|
+
default_factory=lambda: defaultdict(int))
|
|
23
|
+
finished: DefaultDict[str, int] = field(
|
|
24
|
+
default_factory=lambda: defaultdict(int))
|
|
25
|
+
timing: DefaultDict[str, float] = field(
|
|
26
|
+
default_factory=lambda: defaultdict(float))
|
|
24
27
|
|
|
25
28
|
def __str__(self):
|
|
26
29
|
"""Return a formatted string representation of the job statistics."""
|
|
@@ -57,7 +60,8 @@ class JobStatsMixin: # pylint: disable=too-few-public-methods
|
|
|
57
60
|
|
|
58
61
|
def job_tracker(func):
|
|
59
62
|
"""
|
|
60
|
-
Decorator to track job statistics for a method,
|
|
63
|
+
Decorator to track job statistics for a method,
|
|
64
|
+
including running count, finished count, and average timing.
|
|
61
65
|
"""
|
|
62
66
|
def get_fxn_src_name(func, first_arg) -> str:
|
|
63
67
|
"""
|
|
@@ -112,7 +116,8 @@ def job_tracker(func):
|
|
|
112
116
|
|
|
113
117
|
def terminator(func):
|
|
114
118
|
"""
|
|
115
|
-
Decorator designed specifically for the SubnetScanner class,
|
|
119
|
+
Decorator designed specifically for the SubnetScanner class,
|
|
120
|
+
helps facilitate termination of a job.
|
|
116
121
|
"""
|
|
117
122
|
def wrapper(*args, **kwargs):
|
|
118
123
|
"""Wrap the function to check if the scan is running before execution."""
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions used by the lanscape application.
|
|
3
|
+
|
|
4
|
+
This module contains custom exception classes for handling various error cases
|
|
5
|
+
in the network scanning and device management operations.
|
|
6
|
+
"""
|
|
1
7
|
|
|
2
8
|
|
|
3
9
|
class SubnetTooLargeError(Exception):
|
|
@@ -9,12 +15,16 @@ class SubnetTooLargeError(Exception):
|
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
class SubnetScanTerminationFailure(Exception):
|
|
18
|
+
"""Exception raised when subnet scanning threads cannot be terminated properly."""
|
|
19
|
+
|
|
12
20
|
def __init__(self, running_threads):
|
|
13
21
|
super().__init__(
|
|
14
22
|
f'Unable to terminate active threads: {running_threads}')
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
class DeviceError(Exception):
|
|
26
|
+
"""Exception wrapper for device-related errors to provide context about failure source."""
|
|
27
|
+
|
|
18
28
|
def __init__(self, e: Exception):
|
|
19
29
|
self.base: Exception = e
|
|
20
30
|
self.method = self._attempt_extract_method()
|
|
@@ -1,11 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IP address parsing module for network scanning operations.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for parsing various IP address formats including:
|
|
5
|
+
- Single IP addresses
|
|
6
|
+
- CIDR notation subnets
|
|
7
|
+
- IP ranges with hyphens (e.g., 192.168.1.1-192.168.1.10)
|
|
8
|
+
- Shorthand IP ranges (e.g., 192.168.1.1-10)
|
|
9
|
+
|
|
10
|
+
It also includes validation to prevent processing excessively large IP ranges.
|
|
11
|
+
"""
|
|
1
12
|
import ipaddress
|
|
2
|
-
from .errors import SubnetTooLargeError
|
|
3
13
|
import re
|
|
4
14
|
|
|
15
|
+
from lanscape.libraries.errors import SubnetTooLargeError
|
|
16
|
+
|
|
5
17
|
MAX_IPS_ALLOWED = 100000
|
|
6
18
|
|
|
7
19
|
|
|
8
20
|
def parse_ip_input(ip_input):
|
|
21
|
+
"""
|
|
22
|
+
Parse various IP address format inputs into a list of IPv4Address objects.
|
|
23
|
+
|
|
24
|
+
Supports:
|
|
25
|
+
- Comma-separated entries
|
|
26
|
+
- CIDR notation (e.g., 192.168.1.0/24)
|
|
27
|
+
- IP ranges with a hyphen (e.g., 192.168.1.1-192.168.1.10)
|
|
28
|
+
- Shorthand IP ranges (e.g., 192.168.1.1-10)
|
|
29
|
+
- Single IP addresses
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ip_input (str): String containing IP addresses in various formats
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
list: List of IPv4Address objects
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
SubnetTooLargeError: If the number of IPs exceeds MAX_IPS_ALLOWED
|
|
39
|
+
"""
|
|
9
40
|
# Split input on commas for multiple entries
|
|
10
41
|
entries = [entry.strip() for entry in ip_input.split(',')]
|
|
11
42
|
ip_ranges = []
|
|
@@ -36,6 +67,15 @@ def parse_ip_input(ip_input):
|
|
|
36
67
|
|
|
37
68
|
|
|
38
69
|
def get_address_count(subnet: str):
|
|
70
|
+
"""
|
|
71
|
+
Get the number of addresses in an IP subnet.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
subnet (str): Subnet in CIDR notation
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
int: Number of addresses in the subnet, or 0 if invalid
|
|
78
|
+
"""
|
|
39
79
|
try:
|
|
40
80
|
net = ipaddress.IPv4Network(subnet, strict=False)
|
|
41
81
|
return net.num_addresses
|
|
@@ -44,6 +84,17 @@ def get_address_count(subnet: str):
|
|
|
44
84
|
|
|
45
85
|
|
|
46
86
|
def parse_ip_range(entry):
|
|
87
|
+
"""
|
|
88
|
+
Parse an IP range specified with a hyphen (e.g., 192.168.1.1-192.168.1.10).
|
|
89
|
+
|
|
90
|
+
Also handles partial end IPs by using the start IP's prefix.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
entry (str): String containing an IP range with a hyphen
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
list: List of IPv4Address objects in the range (inclusive)
|
|
97
|
+
"""
|
|
47
98
|
start_ip, end_ip = entry.split('-')
|
|
48
99
|
start_ip = ipaddress.IPv4Address(start_ip.strip())
|
|
49
100
|
|
|
@@ -56,6 +107,17 @@ def parse_ip_range(entry):
|
|
|
56
107
|
|
|
57
108
|
|
|
58
109
|
def parse_shorthand_ip_range(entry):
|
|
110
|
+
"""
|
|
111
|
+
Parse a shorthand IP range (e.g., 192.168.1.1-10).
|
|
112
|
+
|
|
113
|
+
In this format, only the last octet of the end IP is specified.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
entry (str): String containing a shorthand IP range
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
list: List of IPv4Address objects in the range (inclusive)
|
|
120
|
+
"""
|
|
59
121
|
start_ip, end_part = entry.split('-')
|
|
60
122
|
start_ip = ipaddress.IPv4Address(start_ip.strip())
|
|
61
123
|
end_ip = start_ip.exploded.rsplit('.', 1)[0] + '.' + end_part.strip()
|
|
@@ -64,6 +126,16 @@ def parse_shorthand_ip_range(entry):
|
|
|
64
126
|
|
|
65
127
|
|
|
66
128
|
def ip_range_to_list(start_ip, end_ip):
|
|
129
|
+
"""
|
|
130
|
+
Convert an IP range defined by start and end addresses to a list of addresses.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
start_ip (IPv4Address): The starting IP address
|
|
134
|
+
end_ip (IPv4Address): The ending IP address
|
|
135
|
+
|
|
136
|
+
Yields:
|
|
137
|
+
IPv4Address: Each IP address in the range (inclusive)
|
|
138
|
+
"""
|
|
67
139
|
# Yield the range of IPs
|
|
68
140
|
for ip_int in range(int(start_ip), int(end_ip) + 1):
|
|
69
141
|
yield ipaddress.IPv4Address(ip_int)
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging configuration module for the lanscape application.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to configure logging for both console and file output,
|
|
5
|
+
with options to control log levels and disable Flask's verbose logging output.
|
|
6
|
+
"""
|
|
1
7
|
import logging
|
|
2
8
|
from logging.handlers import RotatingFileHandler
|
|
3
|
-
import click
|
|
4
9
|
from typing import Optional
|
|
5
10
|
|
|
11
|
+
import click
|
|
12
|
+
|
|
6
13
|
|
|
7
14
|
def configure_logging(loglevel: str, logfile: Optional[str], flask_logging: bool = False) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Configure the application's logging system.
|
|
17
|
+
|
|
18
|
+
Sets up logging with the specified log level and optionally directs output to a file.
|
|
19
|
+
When a logfile is specified, rotating file handlers are configured to manage log size.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
loglevel (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
23
|
+
logfile (Optional[str]): Path to log file, or None for console-only logging
|
|
24
|
+
flask_logging (bool): Whether to allow Flask's default logging (defaults to False)
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If an invalid log level is specified
|
|
28
|
+
"""
|
|
8
29
|
numeric_level = getattr(logging, loglevel.upper(), None)
|
|
9
30
|
if not isinstance(numeric_level, int):
|
|
10
31
|
raise ValueError(f'Invalid log level: {loglevel}')
|
|
@@ -30,10 +51,17 @@ def configure_logging(loglevel: str, logfile: Optional[str], flask_logging: bool
|
|
|
30
51
|
|
|
31
52
|
|
|
32
53
|
def disable_flask_logging() -> None:
|
|
54
|
+
"""
|
|
55
|
+
Disable Flask and Werkzeug logging output.
|
|
33
56
|
|
|
57
|
+
Overrides click's echo and secho functions to suppress output and
|
|
58
|
+
sets Werkzeug's logger level to ERROR to reduce log verbosity.
|
|
59
|
+
"""
|
|
34
60
|
def override_click_logging():
|
|
61
|
+
# pylint: disable=unused-argument
|
|
35
62
|
def secho(text, file=None, nl=None, err=None, color=None, **styles):
|
|
36
63
|
pass
|
|
64
|
+
# pylint: disable=unused-argument
|
|
37
65
|
|
|
38
66
|
def echo(text, file=None, nl=None, err=None, color=None, **styles):
|
|
39
67
|
pass
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
"""Network tools for scanning and managing devices on a network."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
|
-
import platform
|
|
3
4
|
import ipaddress
|
|
4
5
|
import traceback
|
|
5
6
|
import subprocess
|
|
@@ -28,6 +29,8 @@ log = logging.getLogger('NetTools')
|
|
|
28
29
|
class IPAlive(JobStatsMixin):
|
|
29
30
|
"""Class to check if a device is alive using ARP and/or ping scans."""
|
|
30
31
|
caught_errors: List[DeviceError] = []
|
|
32
|
+
_icmp_alive: bool = False
|
|
33
|
+
_arp_alive: bool = False
|
|
31
34
|
|
|
32
35
|
@job_tracker
|
|
33
36
|
def is_alive(
|
|
@@ -95,11 +98,22 @@ class IPAlive(JobStatsMixin):
|
|
|
95
98
|
self.caught_errors.append(DeviceError(e))
|
|
96
99
|
# Fallback to system ping command
|
|
97
100
|
try:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
)
|
|
101
115
|
return result.returncode == 0
|
|
102
|
-
except
|
|
116
|
+
except subprocess.CalledProcessError as fallback_error:
|
|
103
117
|
self.caught_errors.append(DeviceError(fallback_error))
|
|
104
118
|
return False
|
|
105
119
|
|
|
@@ -117,6 +131,7 @@ class IPAlive(JobStatsMixin):
|
|
|
117
131
|
|
|
118
132
|
class Device(IPAlive):
|
|
119
133
|
"""Represents a network device with metadata and scanning capabilities."""
|
|
134
|
+
|
|
120
135
|
def __init__(self, ip: str):
|
|
121
136
|
super().__init__()
|
|
122
137
|
self.ip: str = ip
|
|
@@ -232,6 +247,7 @@ class MacSelector:
|
|
|
232
247
|
self.macs[mac] = self.macs.get(mac, 0) + 1
|
|
233
248
|
|
|
234
249
|
def clear(self):
|
|
250
|
+
"""Clear the stored MAC addresses."""
|
|
235
251
|
self.macs = {}
|
|
236
252
|
|
|
237
253
|
|
|
@@ -245,7 +261,7 @@ def get_ip_address(interface: str):
|
|
|
245
261
|
def unix_like(): # Combined Linux and macOS
|
|
246
262
|
try:
|
|
247
263
|
# pylint: disable=import-outside-toplevel, import-error
|
|
248
|
-
import fcntl
|
|
264
|
+
import fcntl
|
|
249
265
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
250
266
|
ip_address = socket.inet_ntoa(fcntl.ioctl(
|
|
251
267
|
sock.fileno(),
|
|
@@ -268,19 +284,19 @@ def get_ip_address(interface: str):
|
|
|
268
284
|
# Call the appropriate function based on the platform
|
|
269
285
|
if psutil.WINDOWS:
|
|
270
286
|
return windows()
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
|
|
288
|
+
# Linux, macOS, and other Unix-like systems
|
|
289
|
+
return unix_like()
|
|
273
290
|
|
|
274
291
|
|
|
275
292
|
def get_netmask(interface: str):
|
|
276
293
|
"""
|
|
277
294
|
Get the netmask of a network interface.
|
|
278
295
|
"""
|
|
279
|
-
|
|
280
296
|
def unix_like(): # Combined Linux and macOS
|
|
281
297
|
try:
|
|
282
298
|
# pylint: disable=import-outside-toplevel, import-error
|
|
283
|
-
import fcntl
|
|
299
|
+
import fcntl
|
|
284
300
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
285
301
|
netmask = socket.inet_ntoa(fcntl.ioctl(
|
|
286
302
|
sock.fileno(),
|
|
@@ -303,8 +319,9 @@ def get_netmask(interface: str):
|
|
|
303
319
|
|
|
304
320
|
if psutil.WINDOWS:
|
|
305
321
|
return windows()
|
|
306
|
-
|
|
307
|
-
|
|
322
|
+
|
|
323
|
+
# Linux, macOS, and other Unix-like systems
|
|
324
|
+
return unix_like()
|
|
308
325
|
|
|
309
326
|
|
|
310
327
|
def get_cidr_from_netmask(netmask: str):
|
|
@@ -316,71 +333,122 @@ def get_cidr_from_netmask(netmask: str):
|
|
|
316
333
|
return str(len(binary_str.rstrip('0')))
|
|
317
334
|
|
|
318
335
|
|
|
336
|
+
def _find_interface_by_default_gateway_windows():
|
|
337
|
+
"""Find the network interface with the default gateway on Windows."""
|
|
338
|
+
try:
|
|
339
|
+
output = subprocess.check_output(
|
|
340
|
+
"route print 0.0.0.0", shell=True, text=True)
|
|
341
|
+
return _parse_windows_route_output(output)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
log.debug(f"Error finding Windows interface by gateway: {e}")
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parse_windows_route_output(output):
|
|
348
|
+
"""Parse the output of Windows route command to extract interface index."""
|
|
349
|
+
lines = output.strip().split('\n')
|
|
350
|
+
interface_idx = None
|
|
351
|
+
|
|
352
|
+
# First find the interface index from the routing table
|
|
353
|
+
for line in lines:
|
|
354
|
+
if '0.0.0.0' in line and 'Gateway' not in line: # Skip header
|
|
355
|
+
parts = [p for p in line.split() if p]
|
|
356
|
+
if len(parts) >= 4:
|
|
357
|
+
interface_idx = parts[3]
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
# If we found an index, find the corresponding interface name
|
|
361
|
+
if interface_idx:
|
|
362
|
+
for iface_name in psutil.net_if_addrs():
|
|
363
|
+
if str(interface_idx) in iface_name:
|
|
364
|
+
return iface_name
|
|
365
|
+
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _find_interface_by_default_gateway_unix():
|
|
370
|
+
"""Find the network interface with the default gateway on Unix-like systems."""
|
|
371
|
+
try:
|
|
372
|
+
cmd = "ip route show default 2>/dev/null || netstat -rn | grep default"
|
|
373
|
+
output = subprocess.check_output(cmd, shell=True, text=True)
|
|
374
|
+
return _parse_unix_route_output(output)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
log.debug(f"Error finding Unix interface by gateway: {e}")
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _parse_unix_route_output(output):
|
|
381
|
+
"""Parse the output of Unix route commands to extract interface name."""
|
|
382
|
+
for line in output.split('\n'):
|
|
383
|
+
# Parse lines with 'default via ... dev ...'
|
|
384
|
+
if 'default via' in line and 'dev' in line:
|
|
385
|
+
return line.split('dev')[1].split()[0]
|
|
386
|
+
|
|
387
|
+
# Parse simpler 'default ...' lines
|
|
388
|
+
if 'default' in line:
|
|
389
|
+
parts = line.split()
|
|
390
|
+
if len(parts) > 3:
|
|
391
|
+
# Interface is usually the last column
|
|
392
|
+
return parts[-1]
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _get_candidate_interfaces():
|
|
397
|
+
"""Get a list of candidate network interfaces."""
|
|
398
|
+
candidates = []
|
|
399
|
+
for interface, addrs in psutil.net_if_addrs().items():
|
|
400
|
+
stats = psutil.net_if_stats().get(interface)
|
|
401
|
+
if not stats or not stats.isup:
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
ipv4_addrs = [addr for addr in addrs if addr.family == socket.AF_INET]
|
|
405
|
+
if not ipv4_addrs:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
# Skip loopback and common virtual interfaces
|
|
409
|
+
is_loopback = any(addr.address.startswith('127.')
|
|
410
|
+
for addr in ipv4_addrs)
|
|
411
|
+
if is_loopback:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
virtual_names = ['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth']
|
|
415
|
+
is_virtual = any(name in interface.lower() for name in virtual_names)
|
|
416
|
+
if is_virtual:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
candidates.append(interface)
|
|
420
|
+
return candidates
|
|
421
|
+
|
|
422
|
+
|
|
319
423
|
def get_primary_interface():
|
|
320
424
|
"""
|
|
321
425
|
Get the primary network interface that is likely handling internet traffic.
|
|
322
426
|
Uses heuristics to identify the most probable interface.
|
|
323
427
|
"""
|
|
324
428
|
# Try to find the interface with the default gateway
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
parts = [p for p in line.split() if p]
|
|
334
|
-
if len(parts) >= 4:
|
|
335
|
-
interface_idx = parts[3]
|
|
336
|
-
# Find interface name in the output
|
|
337
|
-
for iface_name, addrs in psutil.net_if_addrs().items():
|
|
338
|
-
if str(interface_idx) in iface_name:
|
|
339
|
-
return iface_name
|
|
340
|
-
else:
|
|
341
|
-
# Linux/Unix/Mac - use ip route or netstat
|
|
342
|
-
try:
|
|
343
|
-
output = subprocess.check_output(
|
|
344
|
-
"ip route show default 2>/dev/null || netstat -rn | grep default", shell=True, text=True)
|
|
345
|
-
for line in output.split('\n'):
|
|
346
|
-
if 'default via' in line and 'dev' in line:
|
|
347
|
-
return line.split('dev')[1].split()[0]
|
|
348
|
-
elif 'default' in line:
|
|
349
|
-
parts = line.split()
|
|
350
|
-
if len(parts) > 3:
|
|
351
|
-
# Interface is usually the last column
|
|
352
|
-
return parts[-1]
|
|
353
|
-
except (subprocess.SubprocessError, IndexError, FileNotFoundError):
|
|
354
|
-
pass
|
|
355
|
-
except Exception as e:
|
|
356
|
-
log.debug(f"Error determining primary interface: {e}")
|
|
429
|
+
if psutil.WINDOWS:
|
|
430
|
+
interface = _find_interface_by_default_gateway_windows()
|
|
431
|
+
if interface:
|
|
432
|
+
return interface
|
|
433
|
+
else:
|
|
434
|
+
interface = _find_interface_by_default_gateway_unix()
|
|
435
|
+
if interface:
|
|
436
|
+
return interface
|
|
357
437
|
|
|
358
438
|
# Fallback: Identify likely candidates based on heuristics
|
|
359
|
-
candidates =
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
stats = psutil.net_if_stats().get(interface)
|
|
363
|
-
if stats and stats.isup:
|
|
364
|
-
ipv4_addrs = [
|
|
365
|
-
addr for addr in addrs if addr.family == socket.AF_INET]
|
|
366
|
-
if ipv4_addrs:
|
|
367
|
-
# Skip loopback and common virtual interfaces
|
|
368
|
-
is_loopback = any(addr.address.startswith('127.')
|
|
369
|
-
for addr in ipv4_addrs)
|
|
370
|
-
is_virtual = any(name in interface.lower() for name in
|
|
371
|
-
['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth'])
|
|
372
|
-
|
|
373
|
-
if not is_loopback and not is_virtual:
|
|
374
|
-
candidates.append(interface)
|
|
439
|
+
candidates = _get_candidate_interfaces()
|
|
440
|
+
if not candidates:
|
|
441
|
+
return None
|
|
375
442
|
|
|
376
443
|
# Prioritize interfaces with names typically used for physical connections
|
|
377
|
-
|
|
444
|
+
physical_prefixes = ['eth', 'en', 'wlan', 'wifi', 'wl', 'wi']
|
|
445
|
+
for prefix in physical_prefixes:
|
|
378
446
|
for interface in candidates:
|
|
379
447
|
if interface.lower().startswith(prefix):
|
|
380
448
|
return interface
|
|
381
449
|
|
|
382
|
-
# Otherwise return the first candidate
|
|
383
|
-
return candidates[0]
|
|
450
|
+
# Otherwise return the first candidate
|
|
451
|
+
return candidates[0]
|
|
384
452
|
|
|
385
453
|
|
|
386
454
|
def get_host_ip_mask(ip_with_cidr: str):
|
|
@@ -443,13 +511,16 @@ def network_from_snicaddr(snicaddr: psutil._common.snicaddr) -> str:
|
|
|
443
511
|
"""
|
|
444
512
|
if not snicaddr.address or not snicaddr.netmask:
|
|
445
513
|
return None
|
|
446
|
-
|
|
514
|
+
|
|
515
|
+
if snicaddr.family == socket.AF_INET:
|
|
447
516
|
addr = f"{snicaddr.address}/{get_cidr_from_netmask(snicaddr.netmask)}"
|
|
448
|
-
|
|
517
|
+
return get_host_ip_mask(addr)
|
|
518
|
+
|
|
519
|
+
if snicaddr.family == socket.AF_INET6:
|
|
449
520
|
addr = f"{snicaddr.address}/{snicaddr.netmask}"
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return
|
|
521
|
+
return get_host_ip_mask(addr)
|
|
522
|
+
|
|
523
|
+
return f"{snicaddr.address}"
|
|
453
524
|
|
|
454
525
|
|
|
455
526
|
def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|