lanscape 2.1.0b1__tar.gz → 2.2.0a1__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.
Files changed (93) hide show
  1. {lanscape-2.1.0b1/lanscape.egg-info → lanscape-2.2.0a1}/PKG-INFO +4 -1
  2. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/device_alive.py +80 -13
  3. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/net_tools.py +52 -2
  4. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/subnet_scan.py +13 -4
  5. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/main.py +13 -12
  6. {lanscape-2.1.0b1 → lanscape-2.2.0a1/lanscape.egg-info}/PKG-INFO +4 -1
  7. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape.egg-info/SOURCES.txt +1 -0
  8. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape.egg-info/requires.txt +3 -0
  9. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/pyproject.toml +6 -3
  10. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_api.py +22 -10
  11. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_decorators.py +2 -2
  12. lanscape-2.2.0a1/tests/test_globals.py +10 -0
  13. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_library.py +34 -13
  14. lanscape-2.2.0a1/tests/test_utils.py +375 -0
  15. lanscape-2.1.0b1/tests/test_utils.py +0 -160
  16. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/LICENSE +0 -0
  17. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/MANIFEST.in +0 -0
  18. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/README.md +0 -0
  19. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/__init__.py +0 -0
  20. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/__main__.py +0 -0
  21. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/__init__.py +0 -0
  22. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/app_scope.py +0 -0
  23. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/decorators.py +0 -0
  24. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/errors.py +0 -0
  25. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/ip_parser.py +0 -0
  26. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/logger.py +0 -0
  27. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/mac_lookup.py +0 -0
  28. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/port_manager.py +0 -0
  29. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/runtime_args.py +0 -0
  30. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/scan_config.py +0 -0
  31. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/service_scan.py +0 -0
  32. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/version_manager.py +0 -0
  33. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/core/web_browser.py +0 -0
  34. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  35. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  36. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/convert_csv.py +0 -0
  37. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/full.json +0 -0
  38. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/large.json +0 -0
  39. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/medium.json +0 -0
  40. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/small.json +0 -0
  41. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/ports/test_port_list_scan.json +0 -0
  42. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/resources/services/definitions.jsonc +0 -0
  43. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/__init__.py +0 -0
  44. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/app.py +0 -0
  45. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/__init__.py +0 -0
  46. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/api/__init__.py +0 -0
  47. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/api/port.py +0 -0
  48. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/api/scan.py +0 -0
  49. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/api/tools.py +0 -0
  50. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/web/__init__.py +0 -0
  51. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/blueprints/web/routes.py +0 -0
  52. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/shutdown_handler.py +0 -0
  53. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/css/style.css +0 -0
  54. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  55. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  56. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  57. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  58. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  59. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  60. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  61. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/core.js +0 -0
  62. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/layout-sizing.js +0 -0
  63. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/main.js +0 -0
  64. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/on-tab-close.js +0 -0
  65. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/quietReload.js +0 -0
  66. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/scan-config.js +0 -0
  67. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/shutdown-server.js +0 -0
  68. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/subnet-info.js +0 -0
  69. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/js/subnet-selector.js +0 -0
  70. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/static/lanscape.webmanifest +0 -0
  71. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/base.html +0 -0
  72. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/core/head.html +0 -0
  73. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/core/scripts.html +0 -0
  74. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/error.html +0 -0
  75. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/info.html +0 -0
  76. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/main.html +0 -0
  77. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/config.html +0 -0
  78. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/device-detail.html +0 -0
  79. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/export.html +0 -0
  80. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  81. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/ip-table.html +0 -0
  82. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/overview.html +0 -0
  83. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan/scan-error.html +0 -0
  84. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/scan.html +0 -0
  85. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape/ui/templates/shutdown.html +0 -0
  86. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape.egg-info/dependency_links.txt +0 -0
  87. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape.egg-info/entry_points.txt +0 -0
  88. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/lanscape.egg-info/top_level.txt +0 -0
  89. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/setup.cfg +0 -0
  90. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_env.py +0 -0
  91. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_logging.py +0 -0
  92. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_port_scan.py +0 -0
  93. {lanscape-2.1.0b1 → lanscape-2.2.0a1}/tests/test_service_scan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 2.1.0b1
3
+ Version: 2.2.0a1
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
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
- # 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
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 Device
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
- int: Runtime in seconds
306
+ float: Runtime in seconds
305
307
  """
306
308
  if self.scan.running:
307
- return int(time() - self.start_time)
308
- return int(self.end_time - self.start_time)
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) -> bool:
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 open_webapp(url)
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 False
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
- threading.Thread(
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
- app_closed = open_browser(uri)
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.1.0b1
3
+ Version: 2.2.0a1
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
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
@@ -82,6 +82,7 @@ lanscape/ui/templates/scan/scan-error.html
82
82
  tests/test_api.py
83
83
  tests/test_decorators.py
84
84
  tests/test_env.py
85
+ tests/test_globals.py
85
86
  tests/test_library.py
86
87
  tests/test_logging.py
87
88
  tests/test_port_scan.py
@@ -6,7 +6,10 @@ scapy<3.0,>=2.3.2
6
6
  tabulate==0.9.0
7
7
  pydantic
8
8
  icmplib
9
+ pwa-launcher
9
10
 
10
11
  [dev]
11
12
  pytest>=8.0
12
13
  pytest-cov>=5.0
14
+ pytest-xdist>=3.0
15
+ openai>=1.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lanscape"
3
- version = "2.1.0b1"
3
+ version = "2.2.0a1"
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"
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._helpers import right_size_subnet
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': right_size_subnet(get_network_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
- response = api_client.get(f'/api/scan/{scan_id}/summary')
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'] > 0
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'] > 0
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.01)
290
+ time.sleep(0.1)
291
291
  return "a"
292
292
 
293
293
  @job_tracker
294
294
  def function_b():
295
- time.sleep(0.02)
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._helpers import right_size_subnet
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
- @pytest.mark.integration
61
- @pytest.mark.slow
62
- def test_scan(scan_manager):
64
+ def test_smart_select_primary_subnet():
63
65
  """
64
- Test the network scanning functionality with a dynamically selected subnet.
65
- Verifies that devices can be discovered and that scan results are valid.
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=right_size_subnet(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 at least one device
105
- assert len(scan.results.devices) > 0
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