lanscape 2.1.0b1__tar.gz → 2.2.0a2__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.
- {lanscape-2.1.0b1/lanscape.egg-info → lanscape-2.2.0a2}/PKG-INFO +4 -1
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/device_alive.py +80 -13
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/net_tools.py +52 -2
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/subnet_scan.py +13 -4
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/main.py +13 -12
- {lanscape-2.1.0b1 → lanscape-2.2.0a2/lanscape.egg-info}/PKG-INFO +4 -1
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape.egg-info/SOURCES.txt +1 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape.egg-info/requires.txt +3 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/pyproject.toml +6 -3
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_api.py +22 -10
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_decorators.py +2 -2
- lanscape-2.2.0a2/tests/test_globals.py +10 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_library.py +34 -13
- lanscape-2.2.0a2/tests/test_utils.py +375 -0
- lanscape-2.1.0b1/tests/test_utils.py +0 -160
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/LICENSE +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/MANIFEST.in +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/README.md +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/__main__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/app_scope.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/decorators.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/errors.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/ip_parser.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/logger.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/mac_lookup.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/port_manager.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/runtime_args.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/scan_config.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/service_scan.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/version_manager.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/core/web_browser.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/convert_csv.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/full.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/large.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/medium.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/small.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/ports/test_port_list_scan.json +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/resources/services/definitions.jsonc +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/app.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/api/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/api/port.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/api/scan.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/api/tools.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/web/__init__.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/blueprints/web/routes.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/shutdown_handler.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/css/style.css +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/core.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/layout-sizing.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/main.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/on-tab-close.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/quietReload.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/scan-config.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/shutdown-server.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/subnet-info.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/js/subnet-selector.js +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/static/lanscape.webmanifest +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/base.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/core/head.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/core/scripts.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/error.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/info.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/main.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/config.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/device-detail.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/export.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/ip-table.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/overview.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan/scan-error.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/scan.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape/ui/templates/shutdown.html +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape.egg-info/dependency_links.txt +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape.egg-info/entry_points.txt +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/lanscape.egg-info/top_level.txt +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/setup.cfg +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_env.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_logging.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_port_scan.py +0 -0
- {lanscape-2.1.0b1 → lanscape-2.2.0a2}/tests/test_service_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0a2
|
|
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,12 @@ Requires-Dist: scapy<3.0,>=2.3.2
|
|
|
25
25
|
Requires-Dist: tabulate==0.9.0
|
|
26
26
|
Requires-Dist: pydantic
|
|
27
27
|
Requires-Dist: icmplib
|
|
28
|
+
Requires-Dist: pwa-launcher>=0.3.0
|
|
28
29
|
Provides-Extra: dev
|
|
29
30
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
31
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
33
|
+
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
31
34
|
Dynamic: license-file
|
|
32
35
|
|
|
33
36
|
# LANscape
|
|
@@ -12,8 +12,9 @@ import psutil
|
|
|
12
12
|
from scapy.sendrecv import srp
|
|
13
13
|
from scapy.layers.l2 import ARP, Ether
|
|
14
14
|
from icmplib import ping
|
|
15
|
+
from icmplib.exceptions import SocketPermissionError
|
|
15
16
|
|
|
16
|
-
from lanscape.core.net_tools import Device
|
|
17
|
+
from lanscape.core.net_tools import Device, DeviceError
|
|
17
18
|
from lanscape.core.scan_config import (
|
|
18
19
|
ScanConfig, ScanType, PingConfig,
|
|
19
20
|
ArpConfig, PokeConfig, ArpCacheConfig
|
|
@@ -72,18 +73,84 @@ class IcmpLookup():
|
|
|
72
73
|
Returns:
|
|
73
74
|
bool: True if the device is reachable via ICMP, False otherwise.
|
|
74
75
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
try:
|
|
77
|
+
# Try using icmplib first
|
|
78
|
+
for _ in range(cfg.attempts):
|
|
79
|
+
result = ping(
|
|
80
|
+
device.ip,
|
|
81
|
+
count=cfg.ping_count,
|
|
82
|
+
interval=cfg.retry_delay,
|
|
83
|
+
timeout=cfg.timeout,
|
|
84
|
+
privileged=psutil.WINDOWS # Use privileged mode on Windows
|
|
85
|
+
)
|
|
86
|
+
if result.is_alive:
|
|
87
|
+
device.alive = True
|
|
88
|
+
break
|
|
89
|
+
return device.alive is True
|
|
90
|
+
except SocketPermissionError:
|
|
91
|
+
# Fallback to system ping command when raw sockets aren't available
|
|
92
|
+
return cls._ping_fallback(device, cfg)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _ping_fallback(cls, device: Device, cfg: PingConfig) -> bool:
|
|
96
|
+
"""Fallback ping using system ping command via subprocess.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
device (Device): The device to ping.
|
|
100
|
+
cfg (PingConfig): The ping configuration.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
bool: True if the device responds to ping, False otherwise.
|
|
104
|
+
"""
|
|
105
|
+
cmd = []
|
|
106
|
+
|
|
107
|
+
if psutil.WINDOWS:
|
|
108
|
+
# -n count, -w timeout in ms
|
|
109
|
+
cmd = ['ping', '-n', str(cfg.ping_count), '-w', str(int(cfg.timeout * 1000)), device.ip]
|
|
110
|
+
else: # Linux, macOS, and other Unix-like systems
|
|
111
|
+
# -c count, -W timeout in s
|
|
112
|
+
cmd = ['ping', '-c', str(cfg.ping_count), '-W', str(int(cfg.timeout)), device.ip]
|
|
113
|
+
|
|
114
|
+
for r in range(cfg.attempts):
|
|
115
|
+
try:
|
|
116
|
+
# Remove check=True to handle return codes manually
|
|
117
|
+
# Add timeout to prevent hanging
|
|
118
|
+
timeout_val = cfg.timeout * cfg.ping_count + 5
|
|
119
|
+
proc = subprocess.run(
|
|
120
|
+
cmd,
|
|
121
|
+
text=True,
|
|
122
|
+
stdout=subprocess.PIPE,
|
|
123
|
+
stderr=subprocess.PIPE,
|
|
124
|
+
timeout=timeout_val,
|
|
125
|
+
check=False # Handle return codes manually
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Check if ping was successful
|
|
129
|
+
if proc.returncode == 0:
|
|
130
|
+
output = proc.stdout.lower()
|
|
131
|
+
|
|
132
|
+
# Windows/Linux both include "TTL" on a successful reply
|
|
133
|
+
if psutil.WINDOWS or psutil.LINUX:
|
|
134
|
+
if 'ttl' in output:
|
|
135
|
+
device.alive = True
|
|
136
|
+
return True # Early return on success
|
|
137
|
+
|
|
138
|
+
# some distributions of Linux and macOS
|
|
139
|
+
if psutil.MACOS or psutil.LINUX:
|
|
140
|
+
bad = '100.0% packet loss'
|
|
141
|
+
good = 'ping statistics'
|
|
142
|
+
# mac doesnt include TTL, so we check good is there, and bad is not
|
|
143
|
+
if good in output and bad not in output:
|
|
144
|
+
device.alive = True
|
|
145
|
+
return True # Early return on success
|
|
146
|
+
|
|
147
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
|
|
148
|
+
FileNotFoundError) as e:
|
|
149
|
+
device.caught_errors.append(DeviceError(e))
|
|
150
|
+
|
|
151
|
+
if r < cfg.attempts - 1:
|
|
152
|
+
time.sleep(cfg.retry_delay)
|
|
153
|
+
|
|
87
154
|
return device.alive is True
|
|
88
155
|
|
|
89
156
|
|
|
@@ -31,10 +31,10 @@ else:
|
|
|
31
31
|
|
|
32
32
|
from lanscape.core.service_scan import scan_service
|
|
33
33
|
from lanscape.core.mac_lookup import MacLookup, get_macs
|
|
34
|
-
from lanscape.core.ip_parser import get_address_count, MAX_IPS_ALLOWED
|
|
34
|
+
from lanscape.core.ip_parser import get_address_count, MAX_IPS_ALLOWED, parse_ip_input
|
|
35
35
|
from lanscape.core.errors import DeviceError
|
|
36
36
|
from lanscape.core.decorators import job_tracker, run_once, timeout_enforcer
|
|
37
|
-
from lanscape.core.scan_config import ServiceScanConfig, PortScanConfig
|
|
37
|
+
from lanscape.core.scan_config import ServiceScanConfig, PortScanConfig, ScanType
|
|
38
38
|
|
|
39
39
|
log = logging.getLogger('NetTools')
|
|
40
40
|
mac_lookup = MacLookup()
|
|
@@ -552,6 +552,56 @@ def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|
|
|
552
552
|
return selected.get("subnet", "")
|
|
553
553
|
|
|
554
554
|
|
|
555
|
+
def is_internal_block(subnet: str) -> bool:
|
|
556
|
+
"""
|
|
557
|
+
Check if a subnet contains only internal/private IP addresses.
|
|
558
|
+
|
|
559
|
+
Supports CIDR notation, IP ranges, comma-separated lists, and single IPs.
|
|
560
|
+
For ranges and complex inputs, samples representative IPs for efficiency.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
subnet: IP subnet string in various formats
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
bool: True if all sampled IPs are private/internal, False otherwise
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
# Handle comma-separated subnets recursively
|
|
570
|
+
if ',' in subnet:
|
|
571
|
+
return all(is_internal_block(part.strip()) for part in subnet.split(','))
|
|
572
|
+
|
|
573
|
+
# Handle CIDR notation directly
|
|
574
|
+
if '/' in subnet:
|
|
575
|
+
return ipaddress.IPv4Network(subnet, strict=False).is_private
|
|
576
|
+
|
|
577
|
+
# Handle ranges and single IPs by parsing and sampling
|
|
578
|
+
ip_list = parse_ip_input(subnet)
|
|
579
|
+
sample_ips = ([ip_list[0], ip_list[-1]] if len(ip_list) > 1 else ip_list)
|
|
580
|
+
return all(ipaddress.IPv4Address(ip).is_private for ip in sample_ips)
|
|
581
|
+
|
|
582
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
583
|
+
return False # Assume external for unparseable input
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def scan_config_uses_arp(config) -> bool:
|
|
587
|
+
"""
|
|
588
|
+
Check if a scan configuration uses ARP-based scanning methods.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
config: ScanConfig instance
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
bool: True if the configuration uses ARP scanning, False otherwise
|
|
595
|
+
"""
|
|
596
|
+
arp_scan_types = {
|
|
597
|
+
ScanType.ARP_LOOKUP,
|
|
598
|
+
ScanType.POKE_THEN_ARP,
|
|
599
|
+
ScanType.ICMP_THEN_ARP
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return any(scan_type in arp_scan_types for scan_type in config.lookup_type)
|
|
603
|
+
|
|
604
|
+
|
|
555
605
|
@run_once
|
|
556
606
|
def is_arp_supported():
|
|
557
607
|
"""
|
|
@@ -22,7 +22,9 @@ from tabulate import tabulate
|
|
|
22
22
|
# Local imports
|
|
23
23
|
from lanscape.core.scan_config import ScanConfig
|
|
24
24
|
from lanscape.core.decorators import job_tracker, terminator, JobStats
|
|
25
|
-
from lanscape.core.net_tools import
|
|
25
|
+
from lanscape.core.net_tools import (
|
|
26
|
+
Device, is_internal_block, scan_config_uses_arp
|
|
27
|
+
)
|
|
26
28
|
from lanscape.core.errors import SubnetScanTerminationFailure
|
|
27
29
|
from lanscape.core.device_alive import is_device_alive
|
|
28
30
|
|
|
@@ -301,11 +303,11 @@ class ScannerResults:
|
|
|
301
303
|
Calculate the runtime of the scan in seconds.
|
|
302
304
|
|
|
303
305
|
Returns:
|
|
304
|
-
|
|
306
|
+
float: Runtime in seconds
|
|
305
307
|
"""
|
|
306
308
|
if self.scan.running:
|
|
307
|
-
return
|
|
308
|
-
return
|
|
309
|
+
return time() - self.start_time
|
|
310
|
+
return self.end_time - self.start_time
|
|
309
311
|
|
|
310
312
|
def export(self, out_type=dict) -> Union[str, dict]:
|
|
311
313
|
"""
|
|
@@ -385,6 +387,13 @@ class ScanManager:
|
|
|
385
387
|
Returns:
|
|
386
388
|
SubnetScanner: The newly created scan instance
|
|
387
389
|
"""
|
|
390
|
+
if not is_internal_block(config.subnet) and scan_config_uses_arp(config):
|
|
391
|
+
self.log.warning(
|
|
392
|
+
f"ARP scanning detected for external subnet '{config.subnet}'. "
|
|
393
|
+
"ARP requests typically only work within the local network segment. "
|
|
394
|
+
"Consider using ICMP scanning for external IP ranges."
|
|
395
|
+
)
|
|
396
|
+
|
|
388
397
|
scan = SubnetScanner(config)
|
|
389
398
|
self._start(scan)
|
|
390
399
|
self.log.info(f'Scan started - {config}')
|
|
@@ -8,6 +8,9 @@ import logging
|
|
|
8
8
|
import traceback
|
|
9
9
|
import os
|
|
10
10
|
import requests
|
|
11
|
+
from subprocess import Popen
|
|
12
|
+
|
|
13
|
+
from pwa_launcher import open_pwa
|
|
11
14
|
|
|
12
15
|
from lanscape.core.logger import configure_logging
|
|
13
16
|
from lanscape.core.runtime_args import parse_args
|
|
@@ -70,7 +73,7 @@ def try_check_update():
|
|
|
70
73
|
log.warning('Unable to check for updates.')
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
def open_browser(url: str, wait=2) ->
|
|
76
|
+
def open_browser(url: str, wait=2) -> Popen:
|
|
74
77
|
"""
|
|
75
78
|
Open a browser window to the specified
|
|
76
79
|
url after waiting for the server to start
|
|
@@ -78,12 +81,12 @@ def open_browser(url: str, wait=2) -> bool:
|
|
|
78
81
|
try:
|
|
79
82
|
time.sleep(wait)
|
|
80
83
|
log.info(f'Starting UI - http://127.0.0.1:{args.port}')
|
|
81
|
-
return
|
|
84
|
+
return open_pwa(url)
|
|
82
85
|
|
|
83
86
|
except BaseException:
|
|
84
87
|
log.debug(traceback.format_exc())
|
|
85
88
|
log.info(f'Unable to open web browser, server running on {url}')
|
|
86
|
-
return
|
|
89
|
+
return None
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
def start_webserver_ui():
|
|
@@ -97,19 +100,17 @@ def start_webserver_ui():
|
|
|
97
100
|
# if it was, dont open the browser again
|
|
98
101
|
log.info('Opening UI as daemon')
|
|
99
102
|
if not IS_FLASK_RELOAD:
|
|
100
|
-
|
|
101
|
-
target=open_browser,
|
|
102
|
-
args=(uri,),
|
|
103
|
-
daemon=True
|
|
104
|
-
).start()
|
|
103
|
+
open_browser(uri)
|
|
105
104
|
start_webserver(args)
|
|
106
105
|
else:
|
|
107
106
|
flask_thread = start_webserver_daemon(args)
|
|
108
|
-
|
|
107
|
+
proc = open_browser(uri)
|
|
108
|
+
|
|
109
|
+
if proc:
|
|
110
|
+
app_closed = proc.wait()
|
|
111
|
+
else:
|
|
112
|
+
app_closed = False
|
|
109
113
|
|
|
110
|
-
# depending on env, open_browser may or
|
|
111
|
-
# may not be coupled with the closure of UI
|
|
112
|
-
# (if in browser tab, it's uncoupled)
|
|
113
114
|
if not app_closed or args.persistent:
|
|
114
115
|
# not doing a direct join so i can still
|
|
115
116
|
# terminate the app with ctrl+c
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lanscape
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0a2
|
|
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,12 @@ Requires-Dist: scapy<3.0,>=2.3.2
|
|
|
25
25
|
Requires-Dist: tabulate==0.9.0
|
|
26
26
|
Requires-Dist: pydantic
|
|
27
27
|
Requires-Dist: icmplib
|
|
28
|
+
Requires-Dist: pwa-launcher>=0.3.0
|
|
28
29
|
Provides-Extra: dev
|
|
29
30
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
31
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
33
|
+
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
31
34
|
Dynamic: license-file
|
|
32
35
|
|
|
33
36
|
# LANscape
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lanscape"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0a2"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Michael Dennis", email="michael@dipduo.com" },
|
|
6
6
|
]
|
|
@@ -27,13 +27,16 @@ dependencies = [
|
|
|
27
27
|
"scapy>=2.3.2,<3.0",
|
|
28
28
|
"tabulate==0.9.0",
|
|
29
29
|
"pydantic",
|
|
30
|
-
"icmplib"
|
|
30
|
+
"icmplib",
|
|
31
|
+
"pwa-launcher>=0.3.0"
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
[project.optional-dependencies]
|
|
34
35
|
dev = [
|
|
35
36
|
"pytest>=8.0",
|
|
36
|
-
"pytest-cov>=5.0"
|
|
37
|
+
"pytest-cov>=5.0",
|
|
38
|
+
"pytest-xdist>=3.0",
|
|
39
|
+
"openai>=1.0.0"
|
|
37
40
|
]
|
|
38
41
|
[project.urls]
|
|
39
42
|
Homepage = "https://github.com/mdennis281/py-lanscape"
|
|
@@ -8,10 +8,13 @@ from unittest.mock import patch
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
-
from lanscape.ui.app import app
|
|
12
|
-
from lanscape.core.net_tools import get_network_subnet
|
|
13
11
|
|
|
14
|
-
from tests.
|
|
12
|
+
from tests.test_globals import (
|
|
13
|
+
TEST_SUBNET,
|
|
14
|
+
MIN_EXPECTED_RUNTIME,
|
|
15
|
+
MIN_EXPECTED_ALIVE_DEVICES
|
|
16
|
+
)
|
|
17
|
+
from lanscape.ui.app import app
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
@pytest.fixture
|
|
@@ -36,9 +39,10 @@ def updated_port_list():
|
|
|
36
39
|
def test_scan_config():
|
|
37
40
|
"""Create a test scan configuration."""
|
|
38
41
|
return {
|
|
39
|
-
'subnet':
|
|
42
|
+
'subnet': TEST_SUBNET,
|
|
40
43
|
'port_list': 'test_port_list_scan',
|
|
41
|
-
'lookup_type': ['POKE_THEN_ARP']
|
|
44
|
+
'lookup_type': ['ICMP','POKE_THEN_ARP'], # Use ICMP for reliable external IP detection
|
|
45
|
+
'ping_config': {'timeout': 0.8, 'attempts': 2} # Reasonable timeout for external IPs
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
# API Port Management Tests
|
|
@@ -236,6 +240,13 @@ def test_scan_api_async(api_client, test_scan_config):
|
|
|
236
240
|
"""
|
|
237
241
|
Test the full scan API lifecycle with progress monitoring
|
|
238
242
|
"""
|
|
243
|
+
|
|
244
|
+
def _get_scan_response(scan_id):
|
|
245
|
+
"""Consolidated method to get scan response."""
|
|
246
|
+
response = api_client.get(f'/api/scan/{scan_id}/summary')
|
|
247
|
+
assert response.status_code == 200
|
|
248
|
+
return json.loads(response.data)
|
|
249
|
+
|
|
239
250
|
# Create the port list first (since test_scan_config references it)
|
|
240
251
|
sample_port_list = {'80': 'http', '443': 'https'}
|
|
241
252
|
api_client.post('/api/port/list/test_port_list_scan', json=sample_port_list)
|
|
@@ -255,9 +266,7 @@ def test_scan_api_async(api_client, test_scan_config):
|
|
|
255
266
|
|
|
256
267
|
while percent_complete < 100 and iteration < max_iterations:
|
|
257
268
|
# Get scan summary
|
|
258
|
-
|
|
259
|
-
assert response.status_code == 200
|
|
260
|
-
summary = json.loads(response.data)
|
|
269
|
+
summary = _get_scan_response(scan_id)
|
|
261
270
|
assert summary['running'] or summary['stage'] == 'complete'
|
|
262
271
|
|
|
263
272
|
percent_complete = summary['percent_complete']
|
|
@@ -270,12 +279,15 @@ def test_scan_api_async(api_client, test_scan_config):
|
|
|
270
279
|
time.sleep(2)
|
|
271
280
|
iteration += 1
|
|
272
281
|
|
|
282
|
+
time.sleep(1)
|
|
283
|
+
summary = _get_scan_response(scan_id)
|
|
284
|
+
|
|
273
285
|
# Verify final scan state
|
|
274
286
|
assert not summary['running']
|
|
275
287
|
assert summary['stage'] == 'complete'
|
|
276
|
-
assert summary['runtime']
|
|
288
|
+
assert summary['runtime'] >= MIN_EXPECTED_RUNTIME # Should take measurable time for network ops
|
|
277
289
|
|
|
278
290
|
# Validate device counts
|
|
279
291
|
devices = summary['devices']
|
|
280
292
|
assert devices['scanned'] == devices['total']
|
|
281
|
-
assert devices['alive']
|
|
293
|
+
assert MIN_EXPECTED_ALIVE_DEVICES <= devices['alive']
|
|
@@ -287,12 +287,12 @@ def test_job_tracker_multiple_different_functions():
|
|
|
287
287
|
|
|
288
288
|
@job_tracker
|
|
289
289
|
def function_a():
|
|
290
|
-
time.sleep(0.
|
|
290
|
+
time.sleep(0.1)
|
|
291
291
|
return "a"
|
|
292
292
|
|
|
293
293
|
@job_tracker
|
|
294
294
|
def function_b():
|
|
295
|
-
time.sleep(0.
|
|
295
|
+
time.sleep(0.3)
|
|
296
296
|
return "b"
|
|
297
297
|
|
|
298
298
|
function_a()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Globals for tests in the LANscape project.
|
|
3
|
+
Provides shared configuration values used across multiple test files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from tests._helpers import right_size_subnet
|
|
7
|
+
|
|
8
|
+
TEST_SUBNET = f"1.1.1.1/28, {right_size_subnet()}"
|
|
9
|
+
MIN_EXPECTED_RUNTIME = 0.2
|
|
10
|
+
MIN_EXPECTED_ALIVE_DEVICES = 5
|
|
@@ -9,7 +9,11 @@ from lanscape.core.net_tools import smart_select_primary_subnet
|
|
|
9
9
|
from lanscape.core.subnet_scan import ScanManager
|
|
10
10
|
from lanscape.core.scan_config import ScanConfig, ScanType
|
|
11
11
|
|
|
12
|
-
from tests.
|
|
12
|
+
from tests.test_globals import (
|
|
13
|
+
TEST_SUBNET,
|
|
14
|
+
MIN_EXPECTED_RUNTIME,
|
|
15
|
+
MIN_EXPECTED_ALIVE_DEVICES
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@pytest.fixture
|
|
@@ -57,21 +61,33 @@ def test_scan_config():
|
|
|
57
61
|
assert cfg2.lookup_type == [ScanType.POKE_THEN_ARP]
|
|
58
62
|
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
@pytest.mark.slow
|
|
62
|
-
def test_scan(scan_manager):
|
|
64
|
+
def test_smart_select_primary_subnet():
|
|
63
65
|
"""
|
|
64
|
-
Test the
|
|
65
|
-
Verifies that
|
|
66
|
+
Test the smart_select_primary_subnet functionality without running actual scans.
|
|
67
|
+
Verifies that the subnet detection works on the current system.
|
|
66
68
|
"""
|
|
67
69
|
subnet = smart_select_primary_subnet()
|
|
68
70
|
assert subnet is not None
|
|
71
|
+
assert '/' in subnet # Should be in CIDR format
|
|
72
|
+
# Verify it's a valid subnet format
|
|
73
|
+
parts = subnet.split('/')
|
|
74
|
+
assert len(parts) == 2
|
|
75
|
+
assert int(parts[1]) <= 32 # Valid CIDR mask
|
|
69
76
|
|
|
77
|
+
|
|
78
|
+
@pytest.mark.integration
|
|
79
|
+
@pytest.mark.slow
|
|
80
|
+
def test_scan(scan_manager):
|
|
81
|
+
"""
|
|
82
|
+
Test the network scanning functionality with a fixed external subnet.
|
|
83
|
+
Verifies that the scan engine works correctly with external public IPs.
|
|
84
|
+
"""
|
|
70
85
|
cfg = ScanConfig(
|
|
71
|
-
subnet=
|
|
72
|
-
t_multiplier=1.0,
|
|
86
|
+
subnet=TEST_SUBNET,
|
|
73
87
|
port_list='small',
|
|
74
|
-
lookup_type=[ScanType.POKE_THEN_ARP]
|
|
88
|
+
lookup_type=[ScanType.ICMP, ScanType.POKE_THEN_ARP],
|
|
89
|
+
t_cnt_isalive=2, # Limit threads to extend runtime
|
|
90
|
+
ping_config={'timeout': 0.8, 'attempts': 2} # Reasonable timeout for external IPs
|
|
75
91
|
)
|
|
76
92
|
scan = scan_manager.new_scan(cfg)
|
|
77
93
|
assert scan.running
|
|
@@ -101,8 +117,13 @@ def test_scan(scan_manager):
|
|
|
101
117
|
# device must be alive to be in this list
|
|
102
118
|
assert d.alive
|
|
103
119
|
|
|
104
|
-
# find
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# ensure everything got scanned
|
|
120
|
+
# For external IPs, we may not find responsive devices but scan should complete
|
|
121
|
+
# The main goal is to test that the scan engine works correctly
|
|
108
122
|
assert scan.results.devices_scanned == scan.results.devices_total
|
|
123
|
+
|
|
124
|
+
# Verify scan took measurable time (should be > 0 for real network operations)
|
|
125
|
+
assert scan.results.get_runtime() >= MIN_EXPECTED_RUNTIME
|
|
126
|
+
|
|
127
|
+
# For external ranges, alive device count should be within expected bounds
|
|
128
|
+
alive_count = len([d for d in scan.results.devices if d.alive])
|
|
129
|
+
assert MIN_EXPECTED_ALIVE_DEVICES <= alive_count
|