lanscape 1.3.8a1__py3-none-any.whl → 2.4.0a2__py3-none-any.whl
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/__init__.py +8 -4
- lanscape/{libraries → core}/app_scope.py +21 -3
- lanscape/core/decorators.py +231 -0
- lanscape/{libraries → core}/device_alive.py +83 -16
- lanscape/{libraries → core}/ip_parser.py +2 -26
- lanscape/{libraries → core}/net_tools.py +209 -66
- lanscape/{libraries → core}/runtime_args.py +6 -0
- lanscape/{libraries → core}/scan_config.py +103 -5
- lanscape/core/service_scan.py +222 -0
- lanscape/{libraries → core}/subnet_scan.py +30 -14
- lanscape/{libraries → core}/version_manager.py +15 -17
- lanscape/resources/ports/test_port_list_scan.json +4 -0
- lanscape/resources/services/definitions.jsonc +576 -400
- lanscape/ui/app.py +17 -5
- lanscape/ui/blueprints/__init__.py +1 -1
- lanscape/ui/blueprints/api/port.py +15 -1
- lanscape/ui/blueprints/api/scan.py +1 -1
- lanscape/ui/blueprints/api/tools.py +4 -4
- lanscape/ui/blueprints/web/routes.py +29 -2
- lanscape/ui/main.py +46 -19
- lanscape/ui/shutdown_handler.py +2 -2
- lanscape/ui/static/css/style.css +186 -20
- lanscape/ui/static/js/core.js +14 -0
- lanscape/ui/static/js/main.js +30 -2
- lanscape/ui/static/js/quietReload.js +3 -0
- lanscape/ui/static/js/scan-config.js +56 -6
- lanscape/ui/templates/base.html +6 -8
- lanscape/ui/templates/core/head.html +1 -1
- lanscape/ui/templates/info.html +20 -5
- lanscape/ui/templates/main.html +33 -36
- lanscape/ui/templates/scan/config.html +214 -176
- lanscape/ui/templates/scan/device-detail.html +111 -0
- lanscape/ui/templates/scan/ip-table-row.html +17 -83
- lanscape/ui/templates/scan/ip-table.html +5 -5
- lanscape/ui/ws/__init__.py +31 -0
- lanscape/ui/ws/delta.py +170 -0
- lanscape/ui/ws/handlers/__init__.py +20 -0
- lanscape/ui/ws/handlers/base.py +145 -0
- lanscape/ui/ws/handlers/port.py +184 -0
- lanscape/ui/ws/handlers/scan.py +352 -0
- lanscape/ui/ws/handlers/tools.py +145 -0
- lanscape/ui/ws/protocol.py +86 -0
- lanscape/ui/ws/server.py +375 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
- lanscape-2.4.0a2.dist-info/RECORD +85 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
- lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
- lanscape/libraries/decorators.py +0 -170
- lanscape/libraries/service_scan.py +0 -50
- lanscape/libraries/web_browser.py +0 -210
- lanscape-1.3.8a1.dist-info/RECORD +0 -74
- /lanscape/{libraries → core}/__init__.py +0 -0
- /lanscape/{libraries → core}/errors.py +0 -0
- /lanscape/{libraries → core}/logger.py +0 -0
- /lanscape/{libraries → core}/mac_lookup.py +0 -0
- /lanscape/{libraries → core}/port_manager.py +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
- {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,77 +1,185 @@
|
|
|
1
1
|
"""Network tools for scanning and managing devices on a network."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import ipaddress
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
from typing import List, Dict
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
8
6
|
import socket
|
|
9
7
|
import struct
|
|
10
|
-
import
|
|
11
|
-
import
|
|
8
|
+
import subprocess
|
|
9
|
+
import traceback
|
|
10
|
+
from time import sleep
|
|
11
|
+
from typing import List, Dict, Optional
|
|
12
12
|
|
|
13
|
+
import psutil
|
|
13
14
|
from scapy.sendrecv import srp
|
|
14
15
|
from scapy.layers.l2 import ARP, Ether
|
|
15
16
|
from scapy.error import Scapy_Exception
|
|
16
17
|
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
from pydantic import BaseModel, PrivateAttr
|
|
19
|
+
try:
|
|
20
|
+
from pydantic import ConfigDict, computed_field, model_serializer # pydantic v2
|
|
21
|
+
_PYD_V2 = True
|
|
22
|
+
except Exception: # pragma: no cover
|
|
23
|
+
CONFIG_DICT = None # type: ignore # pylint: disable=invalid-name
|
|
24
|
+
COMPUTED_FIELD = None # type: ignore # pylint: disable=invalid-name
|
|
25
|
+
MODEL_SERIALIZER = None # type: ignore # pylint: disable=invalid-name
|
|
26
|
+
_PYD_V2 = False
|
|
27
|
+
else:
|
|
28
|
+
CONFIG_DICT = ConfigDict # pylint: disable=invalid-name
|
|
29
|
+
COMPUTED_FIELD = computed_field # pylint: disable=invalid-name
|
|
30
|
+
MODEL_SERIALIZER = model_serializer # pylint: disable=invalid-name
|
|
31
|
+
|
|
32
|
+
from lanscape.core.service_scan import scan_service
|
|
33
|
+
from lanscape.core.mac_lookup import MacLookup, get_macs
|
|
34
|
+
from lanscape.core.ip_parser import get_address_count, MAX_IPS_ALLOWED, parse_ip_input
|
|
35
|
+
from lanscape.core.errors import DeviceError
|
|
36
|
+
from lanscape.core.decorators import job_tracker, run_once, timeout_enforcer
|
|
37
|
+
from lanscape.core.scan_config import ServiceScanConfig, PortScanConfig, ScanType
|
|
22
38
|
|
|
23
39
|
log = logging.getLogger('NetTools')
|
|
24
40
|
mac_lookup = MacLookup()
|
|
25
41
|
|
|
26
42
|
|
|
27
|
-
class Device:
|
|
43
|
+
class Device(BaseModel):
|
|
28
44
|
"""Represents a network device with metadata and scanning capabilities."""
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
ip: str
|
|
47
|
+
alive: Optional[bool] = None
|
|
48
|
+
hostname: Optional[str] = None
|
|
49
|
+
macs: List[str] = []
|
|
50
|
+
manufacturer: Optional[str] = None
|
|
51
|
+
ports: List[int] = []
|
|
52
|
+
stage: str = 'found'
|
|
53
|
+
ports_scanned: int = 0
|
|
54
|
+
services: Dict[str, List[int]] = {}
|
|
55
|
+
caught_errors: List[DeviceError] = []
|
|
56
|
+
job_stats: Optional[Dict] = None
|
|
57
|
+
|
|
58
|
+
_log: logging.Logger = PrivateAttr(default_factory=lambda: logging.getLogger('Device'))
|
|
59
|
+
# Support pydantic v1 and v2 configs
|
|
60
|
+
if _PYD_V2 and CONFIG_DICT:
|
|
61
|
+
model_config = CONFIG_DICT(arbitrary_types_allowed=True) # type: ignore[assignment]
|
|
62
|
+
else: # pragma: no cover
|
|
63
|
+
class Config: # pylint: disable=too-few-public-methods
|
|
64
|
+
"""Pydantic v1 configuration."""
|
|
65
|
+
arbitrary_types_allowed = True
|
|
66
|
+
extra = 'allow'
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def log(self) -> logging.Logger:
|
|
70
|
+
"""Get the logger instance for this device."""
|
|
71
|
+
return self._log
|
|
72
|
+
|
|
73
|
+
# Computed fields for pydantic v2 (included in model_dump)
|
|
74
|
+
if _PYD_V2 and COMPUTED_FIELD:
|
|
75
|
+
@COMPUTED_FIELD(return_type=str) # type: ignore[misc]
|
|
76
|
+
@property
|
|
77
|
+
def mac_addr(self) -> str:
|
|
78
|
+
"""Get the primary MAC address for this device."""
|
|
79
|
+
return self.get_mac() or ""
|
|
80
|
+
|
|
81
|
+
@MODEL_SERIALIZER(mode='wrap') # type: ignore[misc]
|
|
82
|
+
def _serialize(self, serializer):
|
|
83
|
+
"""Serialize device data for output."""
|
|
84
|
+
data = serializer(self)
|
|
85
|
+
# Remove internals
|
|
86
|
+
data.pop('job_stats', None)
|
|
87
|
+
# Ensure mac_addr present (computed_field already adds it)
|
|
88
|
+
data['mac_addr'] = data.get('mac_addr') or (self.get_mac() or '')
|
|
89
|
+
# Ensure manufacturer present; prefer explicit model value
|
|
90
|
+
manuf = data.get('manufacturer')
|
|
91
|
+
if not manuf:
|
|
92
|
+
data['manufacturer'] = self._get_manufacturer(
|
|
93
|
+
data['mac_addr']) if data['mac_addr'] else None
|
|
94
|
+
return data
|
|
42
95
|
|
|
43
96
|
def get_metadata(self):
|
|
44
97
|
"""Retrieve metadata such as hostname and MAC addresses."""
|
|
45
98
|
if self.alive:
|
|
46
99
|
self.hostname = self._get_hostname()
|
|
47
100
|
self._get_mac_addresses()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
if not self.manufacturer:
|
|
102
|
+
self.manufacturer = self._get_manufacturer(
|
|
103
|
+
self.get_mac()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Fallback for pydantic v1: use dict() and enrich output
|
|
107
|
+
if not _PYD_V2:
|
|
108
|
+
def dict(self, *args, **kwargs) -> dict: # type: ignore[override]
|
|
109
|
+
"""Generate dictionary representation for pydantic v1."""
|
|
110
|
+
data = super().dict(*args, **kwargs)
|
|
111
|
+
data.pop('job_stats', None)
|
|
112
|
+
mac_addr = self.get_mac() or ''
|
|
113
|
+
data['mac_addr'] = mac_addr
|
|
114
|
+
if not data.get('manufacturer'):
|
|
115
|
+
data['manufacturer'] = self._get_manufacturer(mac_addr) if mac_addr else None
|
|
116
|
+
return data
|
|
117
|
+
else:
|
|
118
|
+
# In v2, route dict() to model_dump() so callers get the serialized enrichment
|
|
119
|
+
def dict(self, *args, **kwargs) -> dict: # type: ignore[override]
|
|
120
|
+
"""Generate dictionary representation for pydantic v2."""
|
|
121
|
+
try:
|
|
122
|
+
return self.model_dump(*args, **kwargs) # type: ignore[attr-defined]
|
|
123
|
+
except Exception:
|
|
124
|
+
# Safety fallback (shouldn't normally hit)
|
|
125
|
+
data = self.__dict__.copy()
|
|
126
|
+
data.pop('_log', None)
|
|
127
|
+
data.pop('job_stats', None)
|
|
128
|
+
mac_addr = self.get_mac() or ''
|
|
129
|
+
data['mac_addr'] = mac_addr
|
|
130
|
+
if not data.get('manufacturer'):
|
|
131
|
+
data['manufacturer'] = self._get_manufacturer(mac_addr) if mac_addr else None
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
def test_port(self, port: int, port_config: Optional[PortScanConfig] = None) -> bool:
|
|
61
135
|
"""Test if a specific port is open on the device."""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
if port_config is None:
|
|
137
|
+
port_config = PortScanConfig() # Use defaults
|
|
138
|
+
|
|
139
|
+
# Calculate timeout enforcer: (timeout * (retries+1) * 1.5)
|
|
140
|
+
enforcer_timeout = port_config.timeout * (port_config.retries + 1) * 1.5
|
|
141
|
+
|
|
142
|
+
@timeout_enforcer(enforcer_timeout, False)
|
|
143
|
+
def do_test():
|
|
144
|
+
for attempt in range(port_config.retries + 1):
|
|
145
|
+
sock = None
|
|
146
|
+
try:
|
|
147
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
148
|
+
sock.settimeout(port_config.timeout)
|
|
149
|
+
result = sock.connect_ex((self.ip, port))
|
|
150
|
+
if result == 0:
|
|
151
|
+
if port not in self.ports:
|
|
152
|
+
self.ports.append(port)
|
|
153
|
+
return True
|
|
154
|
+
except OSError as e:
|
|
155
|
+
# Handle socket creation failures (e.g., "Too many open files")
|
|
156
|
+
# Log and continue to retry if attempts remain
|
|
157
|
+
log = logging.getLogger('Device.test_port')
|
|
158
|
+
log.debug(f"OSError on {self.ip}:{port} attempt {attempt + 1}: {e}")
|
|
159
|
+
except Exception:
|
|
160
|
+
pass # Connection failed, try again if retries remain
|
|
161
|
+
finally:
|
|
162
|
+
# Always close socket if it was created
|
|
163
|
+
if sock is not None:
|
|
164
|
+
try:
|
|
165
|
+
sock.close()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass # Ignore errors during cleanup
|
|
168
|
+
|
|
169
|
+
# Wait before retry (except on last attempt)
|
|
170
|
+
if attempt < port_config.retries:
|
|
171
|
+
sleep(port_config.retry_delay)
|
|
172
|
+
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
ans = do_test() or False
|
|
176
|
+
self.ports_scanned += 1
|
|
177
|
+
return ans
|
|
70
178
|
|
|
71
179
|
@job_tracker
|
|
72
|
-
def scan_service(self, port: int):
|
|
180
|
+
def scan_service(self, port: int, cfg: ServiceScanConfig):
|
|
73
181
|
"""Scan a specific port for services."""
|
|
74
|
-
service = scan_service(self.ip, port)
|
|
182
|
+
service = scan_service(self.ip, port, cfg)
|
|
75
183
|
service_ports = self.services.get(service, [])
|
|
76
184
|
service_ports.append(port)
|
|
77
185
|
self.services[service] = service_ports
|
|
@@ -459,32 +567,67 @@ def smart_select_primary_subnet(subnets: List[dict] = None) -> str:
|
|
|
459
567
|
return selected.get("subnet", "")
|
|
460
568
|
|
|
461
569
|
|
|
462
|
-
|
|
570
|
+
def is_internal_block(subnet: str) -> bool:
|
|
463
571
|
"""
|
|
464
|
-
|
|
465
|
-
|
|
572
|
+
Check if a subnet contains only internal/private IP addresses.
|
|
573
|
+
|
|
574
|
+
Supports CIDR notation, IP ranges, comma-separated lists, and single IPs.
|
|
575
|
+
For ranges and complex inputs, samples representative IPs for efficiency.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
subnet: IP subnet string in various formats
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
bool: True if all sampled IPs are private/internal, False otherwise
|
|
466
582
|
"""
|
|
467
|
-
|
|
583
|
+
try:
|
|
584
|
+
# Handle comma-separated subnets recursively
|
|
585
|
+
if ',' in subnet:
|
|
586
|
+
return all(is_internal_block(part.strip()) for part in subnet.split(','))
|
|
468
587
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if cls._supported is not None:
|
|
473
|
-
return cls._supported
|
|
474
|
-
try:
|
|
475
|
-
arp_request = ARP(pdst='0.0.0.0')
|
|
476
|
-
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
477
|
-
packet = broadcast / arp_request
|
|
478
|
-
srp(packet, timeout=0, verbose=False)
|
|
479
|
-
cls._supported = True
|
|
480
|
-
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
481
|
-
cls._supported = False
|
|
482
|
-
return cls._supported
|
|
588
|
+
# Handle CIDR notation directly
|
|
589
|
+
if '/' in subnet:
|
|
590
|
+
return ipaddress.IPv4Network(subnet, strict=False).is_private
|
|
483
591
|
|
|
592
|
+
# Handle ranges and single IPs by parsing and sampling
|
|
593
|
+
ip_list = parse_ip_input(subnet)
|
|
594
|
+
sample_ips = ([ip_list[0], ip_list[-1]] if len(ip_list) > 1 else ip_list)
|
|
595
|
+
return all(ipaddress.IPv4Address(ip).is_private for ip in sample_ips)
|
|
596
|
+
|
|
597
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
598
|
+
return False # Assume external for unparseable input
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def scan_config_uses_arp(config) -> bool:
|
|
602
|
+
"""
|
|
603
|
+
Check if a scan configuration uses ARP-based scanning methods.
|
|
484
604
|
|
|
605
|
+
Args:
|
|
606
|
+
config: ScanConfig instance
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
bool: True if the configuration uses ARP scanning, False otherwise
|
|
610
|
+
"""
|
|
611
|
+
arp_scan_types = {
|
|
612
|
+
ScanType.ARP_LOOKUP,
|
|
613
|
+
ScanType.POKE_THEN_ARP,
|
|
614
|
+
ScanType.ICMP_THEN_ARP
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return any(scan_type in arp_scan_types for scan_type in config.lookup_type)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@run_once
|
|
485
621
|
def is_arp_supported():
|
|
486
622
|
"""
|
|
487
623
|
Check if ARP requests are supported on the current system.
|
|
488
624
|
Only runs the check once.
|
|
489
625
|
"""
|
|
490
|
-
|
|
626
|
+
try:
|
|
627
|
+
arp_request = ARP(pdst='0.0.0.0')
|
|
628
|
+
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
|
|
629
|
+
packet = broadcast / arp_request
|
|
630
|
+
srp(packet, timeout=0, verbose=False)
|
|
631
|
+
return True
|
|
632
|
+
except (Scapy_Exception, PermissionError, RuntimeError):
|
|
633
|
+
return False
|
|
@@ -14,6 +14,8 @@ class RuntimeArgs:
|
|
|
14
14
|
loglevel: str = 'INFO'
|
|
15
15
|
flask_logging: bool = False
|
|
16
16
|
persistent: bool = False
|
|
17
|
+
ws_server: bool = False
|
|
18
|
+
ws_port: int = 8766
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def parse_args() -> RuntimeArgs:
|
|
@@ -35,6 +37,10 @@ def parse_args() -> RuntimeArgs:
|
|
|
35
37
|
help='Don\'t exit after browser is closed')
|
|
36
38
|
parser.add_argument('--debug', action='store_true',
|
|
37
39
|
help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
|
|
40
|
+
parser.add_argument('--ws-server', action='store_true',
|
|
41
|
+
help='Start WebSocket server instead of Flask UI')
|
|
42
|
+
parser.add_argument('--ws-port', type=int, default=8766,
|
|
43
|
+
help='Port for WebSocket server (default: 8766)')
|
|
38
44
|
|
|
39
45
|
# Parse the arguments
|
|
40
46
|
args = parser.parse_args()
|
|
@@ -11,8 +11,8 @@ from enum import Enum
|
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, Field
|
|
13
13
|
|
|
14
|
-
from lanscape.
|
|
15
|
-
from lanscape.
|
|
14
|
+
from lanscape.core.port_manager import PortManager
|
|
15
|
+
from lanscape.core.ip_parser import parse_ip_input
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class PingConfig(BaseModel):
|
|
@@ -154,6 +154,87 @@ class PokeConfig(BaseModel):
|
|
|
154
154
|
return self.model_dump()
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
class ServiceScanStrategy(Enum):
|
|
158
|
+
"""
|
|
159
|
+
Enumeration of strategies for service scanning on open ports.
|
|
160
|
+
|
|
161
|
+
LAZY: Several common probes to see if we can identify the service.
|
|
162
|
+
BASIC: Common probes plus probes correlated to the port number.
|
|
163
|
+
AGGRESSIVE: All known probes in parallel to try to elicit a response.
|
|
164
|
+
"""
|
|
165
|
+
LAZY = 'LAZY'
|
|
166
|
+
BASIC = 'BASIC'
|
|
167
|
+
AGGRESSIVE = 'AGGRESSIVE'
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ServiceScanConfig(BaseModel):
|
|
171
|
+
"""
|
|
172
|
+
Configuration for service scanning on open ports.
|
|
173
|
+
"""
|
|
174
|
+
timeout: float = 5.0
|
|
175
|
+
lookup_type: ServiceScanStrategy = ServiceScanStrategy.BASIC
|
|
176
|
+
max_concurrent_probes: int = 10
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, data: dict) -> 'ServiceScanConfig':
|
|
180
|
+
"""
|
|
181
|
+
Create a ServiceScanConfig instance from a dictionary.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
data: Dictionary containing ServiceScanConfig parameters
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A new ServiceScanConfig instance with the provided settings
|
|
188
|
+
"""
|
|
189
|
+
return cls.model_validate(data)
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict:
|
|
192
|
+
"""
|
|
193
|
+
Convert the ServiceScanConfig instance to a dictionary.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary representation of the ServiceScanConfig
|
|
197
|
+
"""
|
|
198
|
+
return self.model_dump()
|
|
199
|
+
|
|
200
|
+
def __str__(self):
|
|
201
|
+
return f'ServiceScanCfg(timeout={self.timeout})'
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PortScanConfig(BaseModel):
|
|
205
|
+
"""
|
|
206
|
+
Configuration for port scanning.
|
|
207
|
+
"""
|
|
208
|
+
timeout: float = 1.0
|
|
209
|
+
retries: int = 0
|
|
210
|
+
retry_delay: float = 0.1
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_dict(cls, data: dict) -> 'PortScanConfig':
|
|
214
|
+
"""
|
|
215
|
+
Create a PortScanConfig instance from a dictionary.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
data: Dictionary containing PortScanConfig parameters
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
A new PortScanConfig instance with the provided settings
|
|
222
|
+
"""
|
|
223
|
+
return cls.model_validate(data)
|
|
224
|
+
|
|
225
|
+
def to_dict(self) -> dict:
|
|
226
|
+
"""
|
|
227
|
+
Convert the PortScanConfig instance to a dictionary.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Dictionary representation of the PortScanConfig
|
|
231
|
+
"""
|
|
232
|
+
return self.model_dump()
|
|
233
|
+
|
|
234
|
+
def __str__(self):
|
|
235
|
+
return f'PortScanCfg(timeout={self.timeout}, retry_delay={self.retry_delay})'
|
|
236
|
+
|
|
237
|
+
|
|
157
238
|
class ScanType(Enum):
|
|
158
239
|
"""
|
|
159
240
|
Enumeration of supported network scan types.
|
|
@@ -184,7 +265,7 @@ class ScanConfig(BaseModel):
|
|
|
184
265
|
|
|
185
266
|
task_scan_ports: bool = True
|
|
186
267
|
# below wont run if above false
|
|
187
|
-
task_scan_port_services: bool =
|
|
268
|
+
task_scan_port_services: bool = True
|
|
188
269
|
|
|
189
270
|
lookup_type: List[ScanType] = [ScanType.ICMP_THEN_ARP]
|
|
190
271
|
|
|
@@ -192,6 +273,8 @@ class ScanConfig(BaseModel):
|
|
|
192
273
|
arp_config: ArpConfig = Field(default_factory=ArpConfig)
|
|
193
274
|
poke_config: PokeConfig = Field(default_factory=PokeConfig)
|
|
194
275
|
arp_cache_config: ArpCacheConfig = Field(default_factory=ArpCacheConfig)
|
|
276
|
+
port_scan_config: PortScanConfig = Field(default_factory=PortScanConfig)
|
|
277
|
+
service_scan_config: ServiceScanConfig = Field(default_factory=ServiceScanConfig)
|
|
195
278
|
|
|
196
279
|
def t_cnt(self, thread_id: str) -> int:
|
|
197
280
|
"""
|
|
@@ -259,7 +342,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
259
342
|
t_cnt_port_test=64,
|
|
260
343
|
t_cnt_isalive=64,
|
|
261
344
|
task_scan_ports=True,
|
|
262
|
-
task_scan_port_services=
|
|
345
|
+
task_scan_port_services=True,
|
|
263
346
|
lookup_type=[ScanType.ICMP_THEN_ARP, ScanType.ARP_LOOKUP],
|
|
264
347
|
arp_config=ArpConfig(
|
|
265
348
|
attempts=3,
|
|
@@ -274,6 +357,16 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
274
357
|
arp_cache_config=ArpCacheConfig(
|
|
275
358
|
attempts=2,
|
|
276
359
|
wait_before=0.3
|
|
360
|
+
),
|
|
361
|
+
port_scan_config=PortScanConfig(
|
|
362
|
+
timeout=2.5,
|
|
363
|
+
retries=1,
|
|
364
|
+
retry_delay=0.2
|
|
365
|
+
),
|
|
366
|
+
service_scan_config=ServiceScanConfig(
|
|
367
|
+
timeout=8.0,
|
|
368
|
+
lookup_type=ServiceScanStrategy.AGGRESSIVE,
|
|
369
|
+
max_concurrent_probes=5
|
|
277
370
|
)
|
|
278
371
|
),
|
|
279
372
|
'fast': ScanConfig(
|
|
@@ -283,7 +376,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
283
376
|
t_cnt_port_test=256,
|
|
284
377
|
t_cnt_isalive=512,
|
|
285
378
|
task_scan_ports=True,
|
|
286
|
-
task_scan_port_services=
|
|
379
|
+
task_scan_port_services=True,
|
|
287
380
|
lookup_type=[ScanType.POKE_THEN_ARP],
|
|
288
381
|
arp_config=ArpConfig(
|
|
289
382
|
attempts=1,
|
|
@@ -294,6 +387,11 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
|
|
|
294
387
|
ping_count=1,
|
|
295
388
|
timeout=0.5,
|
|
296
389
|
retry_delay=0.25
|
|
390
|
+
),
|
|
391
|
+
service_scan_config=ServiceScanConfig(
|
|
392
|
+
timeout=2.0,
|
|
393
|
+
lookup_type=ServiceScanStrategy.LAZY,
|
|
394
|
+
max_concurrent_probes=15
|
|
297
395
|
)
|
|
298
396
|
)
|
|
299
397
|
}
|