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.

Files changed (58) hide show
  1. lanscape/__init__.py +8 -4
  2. lanscape/{libraries → core}/app_scope.py +21 -3
  3. lanscape/core/decorators.py +231 -0
  4. lanscape/{libraries → core}/device_alive.py +83 -16
  5. lanscape/{libraries → core}/ip_parser.py +2 -26
  6. lanscape/{libraries → core}/net_tools.py +209 -66
  7. lanscape/{libraries → core}/runtime_args.py +6 -0
  8. lanscape/{libraries → core}/scan_config.py +103 -5
  9. lanscape/core/service_scan.py +222 -0
  10. lanscape/{libraries → core}/subnet_scan.py +30 -14
  11. lanscape/{libraries → core}/version_manager.py +15 -17
  12. lanscape/resources/ports/test_port_list_scan.json +4 -0
  13. lanscape/resources/services/definitions.jsonc +576 -400
  14. lanscape/ui/app.py +17 -5
  15. lanscape/ui/blueprints/__init__.py +1 -1
  16. lanscape/ui/blueprints/api/port.py +15 -1
  17. lanscape/ui/blueprints/api/scan.py +1 -1
  18. lanscape/ui/blueprints/api/tools.py +4 -4
  19. lanscape/ui/blueprints/web/routes.py +29 -2
  20. lanscape/ui/main.py +46 -19
  21. lanscape/ui/shutdown_handler.py +2 -2
  22. lanscape/ui/static/css/style.css +186 -20
  23. lanscape/ui/static/js/core.js +14 -0
  24. lanscape/ui/static/js/main.js +30 -2
  25. lanscape/ui/static/js/quietReload.js +3 -0
  26. lanscape/ui/static/js/scan-config.js +56 -6
  27. lanscape/ui/templates/base.html +6 -8
  28. lanscape/ui/templates/core/head.html +1 -1
  29. lanscape/ui/templates/info.html +20 -5
  30. lanscape/ui/templates/main.html +33 -36
  31. lanscape/ui/templates/scan/config.html +214 -176
  32. lanscape/ui/templates/scan/device-detail.html +111 -0
  33. lanscape/ui/templates/scan/ip-table-row.html +17 -83
  34. lanscape/ui/templates/scan/ip-table.html +5 -5
  35. lanscape/ui/ws/__init__.py +31 -0
  36. lanscape/ui/ws/delta.py +170 -0
  37. lanscape/ui/ws/handlers/__init__.py +20 -0
  38. lanscape/ui/ws/handlers/base.py +145 -0
  39. lanscape/ui/ws/handlers/port.py +184 -0
  40. lanscape/ui/ws/handlers/scan.py +352 -0
  41. lanscape/ui/ws/handlers/tools.py +145 -0
  42. lanscape/ui/ws/protocol.py +86 -0
  43. lanscape/ui/ws/server.py +375 -0
  44. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
  45. lanscape-2.4.0a2.dist-info/RECORD +85 -0
  46. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
  47. lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
  48. lanscape/libraries/decorators.py +0 -170
  49. lanscape/libraries/service_scan.py +0 -50
  50. lanscape/libraries/web_browser.py +0 -210
  51. lanscape-1.3.8a1.dist-info/RECORD +0 -74
  52. /lanscape/{libraries → core}/__init__.py +0 -0
  53. /lanscape/{libraries → core}/errors.py +0 -0
  54. /lanscape/{libraries → core}/logger.py +0 -0
  55. /lanscape/{libraries → core}/mac_lookup.py +0 -0
  56. /lanscape/{libraries → core}/port_manager.py +0 -0
  57. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
  58. {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 traceback
6
- import subprocess
7
- from typing import List, Dict
4
+ import logging
5
+ import re
8
6
  import socket
9
7
  import struct
10
- import re
11
- import psutil
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 lanscape.libraries.service_scan import scan_service
18
- from lanscape.libraries.mac_lookup import MacLookup, get_macs
19
- from lanscape.libraries.ip_parser import get_address_count, MAX_IPS_ALLOWED
20
- from lanscape.libraries.errors import DeviceError
21
- from lanscape.libraries.decorators import job_tracker
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
- def __init__(self, ip: str):
31
- super().__init__()
32
- self.ip: str = ip
33
- self.alive: bool = None
34
- self.hostname: str = None
35
- self.macs: List[str] = []
36
- self.manufacturer: str = None
37
- self.ports: List[int] = []
38
- self.stage: str = 'found'
39
- self.services: Dict[str, List[int]] = {}
40
- self.caught_errors: List[DeviceError] = []
41
- self.log = logging.getLogger('Device')
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
- def dict(self) -> dict:
50
- """Convert the device object to a dictionary."""
51
- obj = vars(self).copy()
52
- obj.pop('log')
53
- obj.pop('job_stats', None) # Remove job_stats if it exists
54
- primary_mac = self.get_mac()
55
- obj['mac_addr'] = primary_mac
56
- obj['manufacturer'] = self._get_manufacturer(primary_mac)
57
-
58
- return obj
59
-
60
- def test_port(self, port: int) -> bool:
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
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
63
- sock.settimeout(1)
64
- result = sock.connect_ex((self.ip, port))
65
- sock.close()
66
- if result == 0:
67
- self.ports.append(port)
68
- return True
69
- return False
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
- class ArpSupportChecker:
570
+ def is_internal_block(subnet: str) -> bool:
463
571
  """
464
- Singleton class to check if ARP requests are supported on the current system.
465
- The check is only performed once.
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
- _supported = None
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
- @classmethod
470
- def is_supported(cls):
471
- """one time check if ARP requests are supported on this system"""
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
- return ArpSupportChecker.is_supported()
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.libraries.port_manager import PortManager
15
- from lanscape.libraries.ip_parser import parse_ip_input
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 = False # disabling until more stable
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=False,
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=False,
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
  }