lanscape 1.3.5a1__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.

Files changed (87) hide show
  1. {lanscape-1.3.5a1/lanscape.egg-info → lanscape-1.3.5a2}/PKG-INFO +1 -1
  2. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/app_scope.py +0 -1
  3. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/decorators.py +10 -5
  4. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/errors.py +10 -0
  5. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/ip_parser.py +73 -1
  6. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/logger.py +29 -1
  7. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/mac_lookup.py +5 -0
  8. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/net_tools.py +124 -66
  9. lanscape-1.3.5a2/lanscape/libraries/port_manager.py +150 -0
  10. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/scan_config.py +98 -3
  11. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/service_scan.py +3 -3
  12. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/subnet_scan.py +104 -16
  13. lanscape-1.3.5a2/lanscape/libraries/version_manager.py +99 -0
  14. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/web_browser.py +75 -58
  15. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/mac_addresses/convert_csv.py +13 -2
  16. lanscape-1.3.5a2/lanscape/resources/ports/convert_csv.py +40 -0
  17. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/app.py +24 -6
  18. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/__init__.py +4 -1
  19. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/__init__.py +2 -0
  20. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/port.py +46 -0
  21. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/scan.py +57 -5
  22. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/api/tools.py +1 -0
  23. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/web/__init__.py +4 -0
  24. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/blueprints/web/routes.py +52 -2
  25. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/main.py +1 -1
  26. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/shutdown_handler.py +5 -1
  27. {lanscape-1.3.5a1 → lanscape-1.3.5a2/lanscape.egg-info}/PKG-INFO +1 -1
  28. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/pyproject.toml +1 -1
  29. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/tests/test_api.py +21 -2
  30. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/tests/test_env.py +18 -0
  31. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/tests/test_library.py +22 -3
  32. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/tests/test_logging.py +27 -4
  33. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/tests/test_utils.py +27 -3
  34. lanscape-1.3.5a1/lanscape/libraries/port_manager.py +0 -67
  35. lanscape-1.3.5a1/lanscape/libraries/version_manager.py +0 -56
  36. lanscape-1.3.5a1/lanscape/resources/ports/convert_csv.py +0 -30
  37. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/LICENSE +0 -0
  38. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/MANIFEST.in +0 -0
  39. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/README.md +0 -0
  40. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/__init__.py +0 -0
  41. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/__main__.py +0 -0
  42. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/__init__.py +0 -0
  43. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/libraries/runtime_args.py +0 -0
  44. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  45. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/ports/full.json +0 -0
  46. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/ports/large.json +0 -0
  47. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/ports/medium.json +0 -0
  48. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/ports/small.json +0 -0
  49. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/resources/services/definitions.jsonc +0 -0
  50. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/__init__.py +0 -0
  51. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/css/style.css +0 -0
  52. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  53. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  54. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  55. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  56. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  57. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  58. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  59. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/core.js +0 -0
  60. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/layout-sizing.js +0 -0
  61. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/main.js +0 -0
  62. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/on-tab-close.js +0 -0
  63. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/quietReload.js +0 -0
  64. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/scan-config.js +0 -0
  65. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/shutdown-server.js +0 -0
  66. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/subnet-info.js +0 -0
  67. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/js/subnet-selector.js +0 -0
  68. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/static/lanscape.webmanifest +0 -0
  69. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/base.html +0 -0
  70. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/core/head.html +0 -0
  71. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/core/scripts.html +0 -0
  72. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/error.html +0 -0
  73. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/info.html +0 -0
  74. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/main.html +0 -0
  75. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/config.html +0 -0
  76. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/export.html +0 -0
  77. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  78. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/ip-table.html +0 -0
  79. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/overview.html +0 -0
  80. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan/scan-error.html +0 -0
  81. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/scan.html +0 -0
  82. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape/ui/templates/shutdown.html +0 -0
  83. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape.egg-info/SOURCES.txt +0 -0
  84. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape.egg-info/dependency_links.txt +0 -0
  85. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape.egg-info/requires.txt +0 -0
  86. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/lanscape.egg-info/top_level.txt +0 -0
  87. {lanscape-1.3.5a1 → lanscape-1.3.5a2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 1.3.5a1
3
+ Version: 1.3.5a2
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -5,7 +5,6 @@ Resource and environment management utilities for Lanscape.
5
5
 
6
6
  from pathlib import Path
7
7
  import json
8
- import sys
9
8
  import re
10
9
 
11
10
 
@@ -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(default_factory=lambda: defaultdict(int))
22
- finished: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int))
23
- timing: DefaultDict[str, float] = field(default_factory=lambda: defaultdict(float))
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, including running count, finished count, and average timing.
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, helps facilitate termination of a job.
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,3 +1,8 @@
1
+ """
2
+ MAC address lookup and resolution service.
3
+ This module provides functionality to look up MAC addresses and resolve them
4
+ """
5
+
1
6
  import re
2
7
  import logging
3
8
  import platform
@@ -1,3 +1,5 @@
1
+ """Network tools for scanning and managing devices on a network."""
2
+
1
3
  import logging
2
4
  import ipaddress
3
5
  import traceback
@@ -29,7 +31,6 @@ class IPAlive(JobStatsMixin):
29
31
  caught_errors: List[DeviceError] = []
30
32
  _icmp_alive: bool = False
31
33
  _arp_alive: bool = False
32
-
33
34
 
34
35
  @job_tracker
35
36
  def is_alive(
@@ -99,15 +100,16 @@ class IPAlive(JobStatsMixin):
99
100
  try:
100
101
  if psutil.WINDOWS:
101
102
  cmd = [
102
- "ping", "-n", str(cfg.ping_count),
103
+ "ping", "-n", str(cfg.ping_count),
103
104
  "-w", str(int(cfg.timeout * 1000)), ip
104
105
  ]
105
106
  else:
106
- cmd = ["ping", "-c", str(cfg.ping_count), "-W", str(cfg.timeout), ip]
107
+ cmd = ["ping", "-c",
108
+ str(cfg.ping_count), "-W", str(cfg.timeout), ip]
107
109
 
108
110
  result = subprocess.run(
109
- cmd, stdout=subprocess.PIPE,
110
- stderr=subprocess.PIPE,
111
+ cmd, stdout=subprocess.PIPE,
112
+ stderr=subprocess.PIPE,
111
113
  text=True, check=False
112
114
  )
113
115
  return result.returncode == 0
@@ -245,6 +247,7 @@ class MacSelector:
245
247
  self.macs[mac] = self.macs.get(mac, 0) + 1
246
248
 
247
249
  def clear(self):
250
+ """Clear the stored MAC addresses."""
248
251
  self.macs = {}
249
252
 
250
253
 
@@ -281,15 +284,15 @@ def get_ip_address(interface: str):
281
284
  # Call the appropriate function based on the platform
282
285
  if psutil.WINDOWS:
283
286
  return windows()
284
- else: # Linux, macOS, and other Unix-like systems
285
- return unix_like()
287
+
288
+ # Linux, macOS, and other Unix-like systems
289
+ return unix_like()
286
290
 
287
291
 
288
292
  def get_netmask(interface: str):
289
293
  """
290
294
  Get the netmask of a network interface.
291
295
  """
292
-
293
296
  def unix_like(): # Combined Linux and macOS
294
297
  try:
295
298
  # pylint: disable=import-outside-toplevel, import-error
@@ -316,8 +319,9 @@ def get_netmask(interface: str):
316
319
 
317
320
  if psutil.WINDOWS:
318
321
  return windows()
319
- else: # Linux, macOS, and other Unix-like systems
320
- return unix_like()
322
+
323
+ # Linux, macOS, and other Unix-like systems
324
+ return unix_like()
321
325
 
322
326
 
323
327
  def get_cidr_from_netmask(netmask: str):
@@ -329,71 +333,122 @@ def get_cidr_from_netmask(netmask: str):
329
333
  return str(len(binary_str.rstrip('0')))
330
334
 
331
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
+
332
423
  def get_primary_interface():
333
424
  """
334
425
  Get the primary network interface that is likely handling internet traffic.
335
426
  Uses heuristics to identify the most probable interface.
336
427
  """
337
428
  # Try to find the interface with the default gateway
338
- try:
339
- if psutil.WINDOWS:
340
- # On Windows, parse route print output
341
- output = subprocess.check_output(
342
- "route print 0.0.0.0", shell=True, text=True)
343
- lines = output.strip().split('\n')
344
- for line in lines:
345
- if '0.0.0.0' in line and 'Gateway' not in line: # Skip header
346
- parts = [p for p in line.split() if p]
347
- if len(parts) >= 4:
348
- interface_idx = parts[3]
349
- # Find interface name in the output
350
- for iface_name, addrs in psutil.net_if_addrs().items():
351
- if str(interface_idx) in iface_name:
352
- return iface_name
353
- else:
354
- # Linux/Unix/Mac - use ip route or netstat
355
- try:
356
- output = subprocess.check_output(
357
- "ip route show default 2>/dev/null || netstat -rn | grep default", shell=True, text=True)
358
- for line in output.split('\n'):
359
- if 'default via' in line and 'dev' in line:
360
- return line.split('dev')[1].split()[0]
361
- elif 'default' in line:
362
- parts = line.split()
363
- if len(parts) > 3:
364
- # Interface is usually the last column
365
- return parts[-1]
366
- except (subprocess.SubprocessError, IndexError, FileNotFoundError):
367
- pass
368
- except Exception as e:
369
- 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
370
437
 
371
438
  # Fallback: Identify likely candidates based on heuristics
372
- candidates = []
373
-
374
- for interface, addrs in psutil.net_if_addrs().items():
375
- stats = psutil.net_if_stats().get(interface)
376
- if stats and stats.isup:
377
- ipv4_addrs = [
378
- addr for addr in addrs if addr.family == socket.AF_INET]
379
- if ipv4_addrs:
380
- # Skip loopback and common virtual interfaces
381
- is_loopback = any(addr.address.startswith('127.')
382
- for addr in ipv4_addrs)
383
- is_virtual = any(name in interface.lower() for name in
384
- ['loop', 'vmnet', 'vbox', 'docker', 'virtual', 'veth'])
385
-
386
- if not is_loopback and not is_virtual:
387
- candidates.append(interface)
439
+ candidates = _get_candidate_interfaces()
440
+ if not candidates:
441
+ return None
388
442
 
389
443
  # Prioritize interfaces with names typically used for physical connections
390
- for prefix in ['eth', 'en', 'wlan', 'wifi', 'wl', 'wi']:
444
+ physical_prefixes = ['eth', 'en', 'wlan', 'wifi', 'wl', 'wi']
445
+ for prefix in physical_prefixes:
391
446
  for interface in candidates:
392
447
  if interface.lower().startswith(prefix):
393
448
  return interface
394
449
 
395
- # Otherwise return the first candidate or None
396
- return candidates[0] if candidates else None
450
+ # Otherwise return the first candidate
451
+ return candidates[0]
397
452
 
398
453
 
399
454
  def get_host_ip_mask(ip_with_cidr: str):
@@ -456,13 +511,16 @@ def network_from_snicaddr(snicaddr: psutil._common.snicaddr) -> str:
456
511
  """
457
512
  if not snicaddr.address or not snicaddr.netmask:
458
513
  return None
459
- elif snicaddr.family == socket.AF_INET:
514
+
515
+ if snicaddr.family == socket.AF_INET:
460
516
  addr = f"{snicaddr.address}/{get_cidr_from_netmask(snicaddr.netmask)}"
461
- elif snicaddr.family == socket.AF_INET6:
517
+ return get_host_ip_mask(addr)
518
+
519
+ if snicaddr.family == socket.AF_INET6:
462
520
  addr = f"{snicaddr.address}/{snicaddr.netmask}"
463
- else:
464
- return f"{snicaddr.address}"
465
- return get_host_ip_mask(addr)
521
+ return get_host_ip_mask(addr)
522
+
523
+ return f"{snicaddr.address}"
466
524
 
467
525
 
468
526
  def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
@@ -0,0 +1,150 @@
1
+ """
2
+ Port Manager module for managing port list configurations.
3
+
4
+ This module provides functionality to create, read, update, and delete port lists
5
+ that are stored as JSON files. Each port list contains port numbers and their
6
+ associated services. The module handles validation of port data and provides
7
+ methods for working with port list configurations.
8
+ """
9
+
10
+ import json
11
+ from typing import List
12
+ from pathlib import Path
13
+ from .app_scope import ResourceManager
14
+
15
+ PORT_DIR = 'ports'
16
+
17
+
18
+ class PortManager:
19
+ """
20
+ Manager class for port list operations.
21
+
22
+ Handles the creation, retrieval, updating, and deletion of port lists.
23
+ Port lists are stored as JSON files with port numbers as keys and
24
+ service names as values.
25
+ """
26
+
27
+ def __init__(self):
28
+ """
29
+ Initialize the PortManager.
30
+
31
+ Creates the ports directory if it doesn't exist and initializes
32
+ the ResourceManager for file operations.
33
+ """
34
+ Path(PORT_DIR).mkdir(parents=True, exist_ok=True)
35
+ self.rm = ResourceManager(PORT_DIR)
36
+
37
+ def get_port_lists(self) -> List[str]:
38
+ """
39
+ Get a list of all available port list names.
40
+
41
+ Returns:
42
+ List[str]: Names of all available port lists (without .json extension)
43
+ """
44
+ return [f.replace('.json', '') for f in self.rm.list() if f.endswith('.json')]
45
+
46
+ def get_port_list(self, port_list: str) -> dict:
47
+ """
48
+ Retrieve a port list by name.
49
+
50
+ Args:
51
+ port_list (str): The name of the port list to retrieve
52
+
53
+ Returns:
54
+ dict: A dictionary of port numbers to service names
55
+
56
+ Raises:
57
+ ValueError: If the specified port list does not exist
58
+ """
59
+ if port_list not in self.get_port_lists():
60
+ msg = f"Port list '{port_list}' does not exist. "
61
+ msg += f"Available port lists: {self.get_port_lists()}"
62
+ raise ValueError(msg)
63
+
64
+ data = json.loads(self.rm.get(f'{port_list}.json'))
65
+
66
+ return data if self.validate_port_data(data) else None
67
+
68
+ def create_port_list(self, port_list: str, data: dict) -> bool:
69
+ """
70
+ Create a new port list.
71
+
72
+ Args:
73
+ port_list (str): Name for the new port list
74
+ data (dict): Dictionary mapping port numbers to service names
75
+
76
+ Returns:
77
+ bool: True if creation was successful, False otherwise
78
+ """
79
+ if port_list in self.get_port_lists():
80
+ return False
81
+ if not self.validate_port_data(data):
82
+ return False
83
+
84
+ self.rm.create(f'{port_list}.json', json.dumps(data, indent=2))
85
+
86
+ return True
87
+
88
+ def update_port_list(self, port_list: str, data: dict) -> bool:
89
+ """
90
+ Update an existing port list.
91
+
92
+ Args:
93
+ port_list (str): Name of the port list to update
94
+ data (dict): New dictionary mapping port numbers to service names
95
+
96
+ Returns:
97
+ bool: True if update was successful, False otherwise
98
+ """
99
+ if port_list not in self.get_port_lists():
100
+ return False
101
+ if not self.validate_port_data(data):
102
+ return False
103
+
104
+ self.rm.update(f'{port_list}.json', json.dumps(data, indent=2))
105
+
106
+ return True
107
+
108
+ def delete_port_list(self, port_list: str) -> bool:
109
+ """
110
+ Delete a port list.
111
+
112
+ Args:
113
+ port_list (str): Name of the port list to delete
114
+
115
+ Returns:
116
+ bool: True if deletion was successful, False otherwise
117
+ """
118
+ if port_list not in self.get_port_lists():
119
+ return False
120
+
121
+ self.rm.delete(f'{port_list}.json')
122
+
123
+ return True
124
+
125
+ def validate_port_data(self, port_data: dict) -> bool:
126
+ """
127
+ Validate port data structure and content.
128
+
129
+ Ensures that:
130
+ - Keys can be converted to integers
131
+ - Values are strings
132
+ - Port numbers are within valid range (0-65535)
133
+
134
+ Args:
135
+ port_data (dict): Dictionary mapping port numbers to service names
136
+
137
+ Returns:
138
+ bool: True if data is valid, False otherwise
139
+ """
140
+ try:
141
+ for port, service in port_data.items():
142
+ port = int(port) # throws if not int
143
+ if not isinstance(service, str):
144
+ return False
145
+
146
+ if not 0 <= port <= 65535:
147
+ return False
148
+ return True
149
+ except BaseException:
150
+ return False