atomicshop 2.16.9__py3-none-any.whl → 2.16.11__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 atomicshop might be problematic. Click here for more details.

atomicshop/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Atomic Basic functions and classes to make developer life easier"""
2
2
 
3
3
  __author__ = "Den Kras"
4
- __version__ = '2.16.9'
4
+ __version__ = '2.16.11'
atomicshop/dns.py CHANGED
@@ -5,6 +5,7 @@ import dns.resolver
5
5
  from .print_api import print_api
6
6
  from .permissions import permissions
7
7
  from .wrappers.pywin32w.wmis import win32networkadapter
8
+ from .wrappers.winregw import winreg_network
8
9
 
9
10
 
10
11
  # Defining Dictionary of Numeric to String DNS Query Types.
@@ -71,10 +72,7 @@ def get_default_dns_gateway() -> tuple[bool, list[str]]:
71
72
  :return: tuple(is dynamic boolean, list of DNS server IPv4s).
72
73
  """
73
74
 
74
- resolver = dns.resolver.Resolver()
75
- dns_servers = list(resolver.nameservers)
76
-
77
- is_dynamic = win32networkadapter.is_adapter_dns_gateway_from_dhcp(use_default_interface=True)
75
+ is_dynamic, dns_servers = winreg_network.get_default_dns_gateway()
78
76
  return is_dynamic, dns_servers
79
77
 
80
78
 
@@ -21,6 +21,15 @@ def write_file_decorator(function_name):
21
21
 
22
22
  print_api(message=f"Writing file: {kwargs['file_path']}", **kwargs)
23
23
 
24
+ if kwargs['enable_long_file_path']:
25
+ # A simpler string method would be to add '\\?\' to the beginning of the file path.
26
+ # kwargs['file_path'] = rf"\\?\{kwargs['file_path']}"
27
+
28
+ # Enable long file path.
29
+ from ctypes import windll
30
+ # Enable long file path.
31
+ windll.kernel32.SetFileAttributesW(kwargs['file_path'], 0x80)
32
+
24
33
  try:
25
34
  with open(kwargs['file_path'], kwargs['file_mode'], encoding=kwargs['encoding']) as output_file:
26
35
  # Pass the 'output_file' object to kwargs that will pass the object to the executing function.
@@ -78,6 +87,7 @@ def write_file(
78
87
  file_path: str,
79
88
  file_mode: str = 'w',
80
89
  encoding: str = None,
90
+ enable_long_file_path: bool = False,
81
91
  file_object=None,
82
92
  **kwargs) -> None:
83
93
  """
@@ -89,6 +99,8 @@ def write_file(
89
99
  Default is 'w'.
90
100
  :param encoding: string, write the file with encoding. Example: 'utf-8'. 'None' is default, since it is default
91
101
  in 'open()' function.
102
+ :param enable_long_file_path: Boolean, by default Windows has a limit of 260 characters for file path. If True,
103
+ the long file path will be enabled, and the limit will be 32,767 characters.
92
104
  :param file_object: file object of the 'open()' function in the decorator. Decorator executes the 'with open()'
93
105
  statement and passes to this function. That's why the default is 'None', since we get it from the decorator.
94
106
  :return:
@@ -19,6 +19,7 @@ LIST_OF_BOOLEANS: list = [
19
19
  ('tcp', 'enable'),
20
20
  ('tcp', 'engines_usage'),
21
21
  ('tcp', 'server_response_mode'),
22
+ ('logrec', 'enable_request_response_recordings_in_logs'),
22
23
  ('certificates', 'default_server_certificate_usage'),
23
24
  ('certificates', 'sni_add_new_domains_to_default_server_certificate'),
24
25
  ('certificates', 'custom_server_certificate_usage'),
@@ -34,8 +35,7 @@ LIST_OF_BOOLEANS: list = [
34
35
  TOML_TO_STATIC_CATEGORIES: dict = {
35
36
  'dns': 'DNSServer',
36
37
  'tcp': 'TCPServer',
37
- 'log': 'Log',
38
- 'recorder': 'Recorder',
38
+ 'logrec': 'LogRec',
39
39
  'certificates': 'Certificates',
40
40
  'skip_extensions': 'SkipExtensions',
41
41
  'process_name': 'ProcessName'
@@ -101,13 +101,12 @@ class TCPServer:
101
101
 
102
102
 
103
103
  @dataclass
104
- class Log:
104
+ class LogRec:
105
105
  logs_path: str
106
-
107
-
108
- @dataclass
109
- class Recorder:
110
106
  recordings_path: str
107
+ enable_request_response_recordings_in_logs: bool
108
+
109
+ recordings_directory_name: str = 'recs'
111
110
 
112
111
 
113
112
  @dataclass
@@ -24,7 +24,25 @@ def thread_worker_main(
24
24
  reference_module
25
25
  ):
26
26
  def output_statistics_csv_row():
27
- status_code = ','.join([str(x.code) for x in client_message.response_list_of_raw_decoded])
27
+ # If there is no '.code' attribute in HTTPResponse, this means that this is not a HTTP message, so there is no
28
+ # status code.
29
+ try:
30
+ http_status_code: str = ','.join([str(x.code) for x in client_message.response_list_of_raw_decoded])
31
+ except AttributeError:
32
+ http_status_code: str = str()
33
+
34
+ # Same goes for the '.path' attribute, if it is not HTTP message then there will be no path.
35
+ try:
36
+ http_path: str = client_message.request_raw_decoded.path
37
+ except AttributeError:
38
+ http_path: str = str()
39
+
40
+ # Same goes for the '.command' attribute, if it is not HTTP message then there will be no command.
41
+ try:
42
+ http_command: str = client_message.request_raw_decoded.command
43
+ except AttributeError:
44
+ http_command: str = str()
45
+
28
46
  response_size_bytes = ','.join([str(len(x)) for x in client_message.response_list_of_raw_bytes])
29
47
 
30
48
  statistics_writer.write_row(
@@ -32,13 +50,13 @@ def thread_worker_main(
32
50
  tls_type=tls_type,
33
51
  tls_version=tls_version,
34
52
  protocol=client_message.protocol,
35
- path=client_message.request_raw_decoded.path,
36
- status_code=status_code,
37
- command=client_message.request_raw_decoded.command,
53
+ path=http_path,
54
+ status_code=http_status_code,
55
+ command=http_command,
38
56
  request_time_sent=client_message.request_time_received,
39
57
  request_size_bytes=len(client_message.request_raw_bytes),
40
58
  response_size_bytes=response_size_bytes,
41
- recorded_file_path=recorded_file,
59
+ recorded_file_path=client_message.recorded_file_path,
42
60
  process_cmd=process_commandline,
43
61
  error=str())
44
62
 
@@ -126,7 +144,7 @@ def thread_worker_main(
126
144
  message = "There was an exception in HTTP Parsing module!"
127
145
  print_api(
128
146
  message, error_type=True, logger=network_logger, logger_method='critical',
129
- traceback_string=True, oneline=True)
147
+ traceback_string=True)
130
148
  # Socket connection can be closed since we have a problem in current thread and break the loop
131
149
  client_connection_boolean = False
132
150
  break
@@ -160,10 +178,10 @@ def thread_worker_main(
160
178
  message = "Exception in Parser"
161
179
  print_api(
162
180
  message, error_type=True, logger=parser.logger, logger_method='critical',
163
- traceback_string=True, oneline=True)
181
+ traceback_string=True)
164
182
  print_api(
165
183
  message, error_type=True, logger=network_logger, logger_method='critical',
166
- traceback_string=True, oneline=True)
184
+ traceback_string=True)
167
185
  # At this point we can pass the exception and continue the script.
168
186
  pass
169
187
  # Socket connection can be closed since we have a problem in current thread and break the loop
@@ -193,10 +211,10 @@ def thread_worker_main(
193
211
  message = "Exception in Responder"
194
212
  print_api(
195
213
  message, error_type=True, logger=responder.logger, logger_method='critical',
196
- traceback_string=True, oneline=True)
214
+ traceback_string=True)
197
215
  print_api(
198
216
  message, error_type=True, logger=network_logger, logger_method='critical',
199
- traceback_string=True, oneline=True)
217
+ traceback_string=True)
200
218
  pass
201
219
  # Socket connection can be closed since we have a problem in current thread and break the loop.
202
220
  client_connection_boolean = False
@@ -220,8 +238,9 @@ def thread_worker_main(
220
238
  service_client = socket_client.SocketClient(
221
239
  service_name=client_message.server_name, service_port=client_message.destination_port,
222
240
  tls=is_tls,
223
- dns_servers_list=
224
- config_static.TCPServer.forwarding_dns_service_ipv4_list___only_for_localhost)
241
+ dns_servers_list=(
242
+ config_static.TCPServer.forwarding_dns_service_ipv4_list___only_for_localhost)
243
+ )
225
244
  # If we're not on localhost, then connect to domain directly.
226
245
  else:
227
246
  service_client = socket_client.SocketClient(
@@ -253,18 +272,19 @@ def thread_worker_main(
253
272
 
254
273
  # This is the point after the response mode check was finished.
255
274
  # Recording the message, doesn't matter what type of mode this is.
256
- try:
257
- recorded_file = recorder(class_client_message=client_message,
258
- record_path=config_static.Recorder.recordings_path).record()
259
- except Exception:
260
- message = "Exception in Recorder"
261
- print_api(
262
- message, error_type=True, logger=recorder.logger, logger_method='critical',
263
- traceback_string=True, oneline=True)
264
- print_api(
265
- message, error_type=True, logger=network_logger, logger_method='critical',
266
- traceback_string=True, oneline=True)
267
- pass
275
+ if config_static.LogRec.enable_request_response_recordings_in_logs:
276
+ try:
277
+ recorded_file = recorder(class_client_message=client_message,
278
+ record_path=config_static.LogRec.recordings_path).record()
279
+ client_message.recorded_file_path = recorded_file
280
+ except Exception:
281
+ message = "Exception in Recorder"
282
+ print_api(
283
+ message, error_type=True, logger=recorder.logger, logger_method='critical',
284
+ traceback_string=True)
285
+ print_api(
286
+ message, error_type=True, logger=network_logger, logger_method='critical',
287
+ traceback_string=True)
268
288
 
269
289
  function_recorded = True
270
290
 
@@ -294,7 +314,7 @@ def thread_worker_main(
294
314
  message = "Not sending anything to the client, since there is no response available"
295
315
  print_api(
296
316
  message, error_type=True, logger=network_logger, logger_method='critical',
297
- traceback_string=True, oneline=True)
317
+ traceback_string=True)
298
318
  # Pass the exception
299
319
  pass
300
320
  # Break the while loop
@@ -312,18 +332,19 @@ def thread_worker_main(
312
332
  # === At this point while loop of 'client_connection_boolean' was broken =======================================
313
333
  # If recorder wasn't executed before, then execute it now
314
334
  if not function_recorded:
315
- try:
316
- recorded_file = recorder(
317
- class_client_message=client_message, record_path=config_static.Recorder.recordings_path).record()
318
- except Exception:
319
- message = "Exception in Recorder"
320
- print_api(
321
- message, error_type=True, logger=recorder.logger, logger_method='critical',
322
- traceback_string=True, oneline=True)
323
- print_api(
324
- message, error_type=True, logger=network_logger, logger_method='critical',
325
- traceback_string=True, oneline=True)
326
- pass
335
+ if config_static.LogRec.enable_request_response_recordings_in_logs:
336
+ try:
337
+ recorded_file = recorder(
338
+ class_client_message=client_message, record_path=config_static.LogRec.recordings_path).record()
339
+ client_message.recorded_file_path = recorded_file
340
+ except Exception:
341
+ message = "Exception in Recorder"
342
+ print_api(
343
+ message, error_type=True, logger=recorder.logger, logger_method='critical',
344
+ traceback_string=True)
345
+ print_api(
346
+ message, error_type=True, logger=network_logger, logger_method='critical',
347
+ traceback_string=True)
327
348
 
328
349
  # Save statistics file.
329
350
  output_statistics_csv_row()
@@ -343,5 +364,4 @@ def thread_worker_main(
343
364
  except Exception:
344
365
  message = "Undocumented exception in thread worker"
345
366
  print_api(
346
- message, error_type=True, logger=network_logger, logger_method='critical',
347
- traceback_string=True, oneline=True)
367
+ message, error_type=True, logger=network_logger, logger_method='critical', traceback_string=True)
@@ -1,4 +1,3 @@
1
- # v1.0.0 - 26.03.2023 14:00
2
1
  from ...shared_functions import create_custom_logger
3
2
  from ...message import ClientMessage
4
3
 
@@ -3,8 +3,8 @@ from datetime import datetime
3
3
 
4
4
  from ...shared_functions import build_module_names, create_custom_logger, get_json
5
5
  from ... import message, recs_files
6
- from ....urls import url_parser
7
- from .... import filesystem
6
+ from .... import filesystem, urls
7
+ from ....file_io import file_io
8
8
 
9
9
 
10
10
  # The class that is responsible for Recording Requests / Responses.
@@ -52,7 +52,7 @@ class RecorderParent:
52
52
  # This will happen if the message is not HTTP.
53
53
  try:
54
54
  # Parse the url to components.
55
- http_path_parsed = url_parser(self.class_client_message.request_raw_decoded.path)
55
+ http_path_parsed = urls.url_parser(self.class_client_message.request_raw_decoded.path)
56
56
  # Get only directories.
57
57
  http_path_directories_string = '-'.join(http_path_parsed['directories'])
58
58
  # Add '_' character before 'http_path' to look better on the file name.
@@ -64,7 +64,7 @@ class RecorderParent:
64
64
  # If HTTP Path is not defined, 'http_path' will be empty, and it will not interfere with file name.
65
65
  self.record_file_path: str = \
66
66
  self.engine_record_path + os.sep + \
67
- day_time_format + "_" + self.class_client_message.server_name + http_path + self.file_extension
67
+ day_time_format + "_" + self.class_client_message.server_name + self.file_extension
68
68
 
69
69
  def convert_messages(self):
70
70
  """
@@ -91,8 +91,7 @@ class RecorderParent:
91
91
  record_message = get_json(self.class_client_message)
92
92
 
93
93
  # Since we already dumped the object to dictionary string, we'll just save the object to regular file.
94
- with open(self.record_file_path, 'w') as output_file:
95
- output_file.write(record_message)
94
+ file_io.write_file(record_message, self.record_file_path, enable_long_file_path=True)
96
95
 
97
96
  self.logger.info(f"Recorded to file: {self.record_file_path}")
98
97
 
@@ -1,4 +1,3 @@
1
- # v1.0.0 - 26.03.2023 14:20
2
1
  # Using to convert status code to status phrase / string.
3
2
  from http import HTTPStatus
4
3
  # Parsing PATH template to variables.
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from pathlib import Path
2
3
 
3
4
  from ..print_api import print_api
@@ -131,13 +132,14 @@ def manipulations_after_import():
131
132
  config_static.SkipExtensions.SKIP_EXTENSION_ID_LIST = skip_extensions
132
133
 
133
134
  # If the paths are relative, convert them to absolute paths.
134
- config_static.Log.logs_path = filesystem.check_absolute_path___add_full(
135
- config_static.Log.logs_path, config_static.MainConfig.SCRIPT_DIRECTORY)
136
- config_static.Recorder.recordings_path = filesystem.check_absolute_path___add_full(
137
- config_static.Recorder.recordings_path, config_static.MainConfig.SCRIPT_DIRECTORY)
135
+ config_static.LogRec.logs_path = filesystem.check_absolute_path___add_full(
136
+ config_static.LogRec.logs_path, config_static.MainConfig.SCRIPT_DIRECTORY)
138
137
  config_static.Certificates.custom_server_certificate_path = filesystem.check_absolute_path___add_full(
139
138
  config_static.Certificates.custom_server_certificate_path, config_static.MainConfig.SCRIPT_DIRECTORY)
140
139
 
140
+ config_static.LogRec.recordings_path = (
141
+ config_static.LogRec.logs_path + os.sep + config_static.LogRec.recordings_directory_name)
142
+
141
143
  # At this point the user that sets the config can set it to null or empty string ''. We will make sure
142
144
  # that the path is None if it's empty.
143
145
  if config_static.Certificates.custom_private_key_path:
@@ -17,15 +17,19 @@ from .connection_thread_worker import thread_worker_main
17
17
  from . import config_static, recs_files
18
18
 
19
19
 
20
+ NETWORK_INTERFACE_IS_DYNAMIC: bool = bool()
21
+ NETWORK_INTERFACE_IPV4_ADDRESS_LIST: list[str] = list()
22
+
23
+
20
24
  def exit_cleanup():
21
- if config_static.DNSServer.set_default_dns_gateway:
25
+ if permissions.is_admin():
22
26
  is_dns_dynamic, current_dns_gateway = dns.get_default_dns_gateway()
23
27
  print_api(f'Current DNS Gateway: {current_dns_gateway}')
24
28
 
25
- if current_dns_gateway == config_static.DNSServer.set_default_dns_gateway and not is_dns_dynamic:
26
- if permissions.is_admin():
27
- dns.set_connection_dns_gateway_dynamic(use_default_connection=True)
28
- print_api("Returned default DNS gateway...", color='blue')
29
+ if is_dns_dynamic != NETWORK_INTERFACE_IS_DYNAMIC or \
30
+ (not is_dns_dynamic and current_dns_gateway != NETWORK_INTERFACE_IPV4_ADDRESS_LIST):
31
+ dns.set_connection_dns_gateway_dynamic(use_default_connection=True)
32
+ print_api("Returned default DNS gateway...", color='blue')
29
33
 
30
34
 
31
35
  def initialize_mitm_server(config_file_path: str):
@@ -42,20 +46,22 @@ def initialize_mitm_server(config_file_path: str):
42
46
  return result
43
47
 
44
48
  # Create folders.
45
- filesystem.create_directory(config_static.Log.logs_path)
46
- filesystem.create_directory(config_static.Recorder.recordings_path)
49
+ filesystem.create_directory(config_static.LogRec.logs_path)
50
+
51
+ if config_static.LogRec.enable_request_response_recordings_in_logs:
52
+ filesystem.create_directory(config_static.LogRec.recordings_path)
53
+ # Compress recordings of the previous days if there are any.
54
+ recs_files.recs_archiver_in_process(config_static.LogRec.recordings_path)
55
+
47
56
  if config_static.Certificates.sni_get_server_certificate_from_server_socket:
48
57
  filesystem.create_directory(
49
58
  config_static.Certificates.sni_server_certificate_from_server_socket_download_directory)
50
59
 
51
- # Compress recordings of the previous days if there are any.
52
- recs_files.recs_archiver_in_process(config_static.Recorder.recordings_path)
53
-
54
60
  # Create a logger that will log messages to file, Initiate System logger.
55
61
  logger_name = "system"
56
62
  system_logger = loggingw.create_logger(
57
63
  logger_name=logger_name,
58
- file_path=f"{config_static.Log.logs_path}{os.sep}{logger_name}.txt",
64
+ file_path=f"{config_static.LogRec.logs_path}{os.sep}{logger_name}.txt",
59
65
  add_stream=True,
60
66
  add_timedfile=True,
61
67
  formatter_streamhandler='DEFAULT',
@@ -68,8 +74,9 @@ def initialize_mitm_server(config_file_path: str):
68
74
  system_logger.info(f"Python Version: {get_current_python_version_string()}")
69
75
  system_logger.info(f"Script Version: {config_static.SCRIPT_VERSION}")
70
76
  system_logger.info(f"Atomic Workshop Version: {atomicshop.__version__}")
71
- system_logger.info(f"Log folder: {config_static.Log.logs_path}")
72
- system_logger.info(f"Recordings folder for Requests/Responses: {config_static.Recorder.recordings_path}")
77
+ system_logger.info(f"Log folder: {config_static.LogRec.logs_path}")
78
+ if config_static.LogRec.enable_request_response_recordings_in_logs:
79
+ system_logger.info(f"Recordings folder for Requests/Responses: {config_static.LogRec.recordings_path}")
73
80
  system_logger.info(f"Loaded system logger: {system_logger}")
74
81
 
75
82
  system_logger.info(f"TCP Server Target IP: {config_static.DNSServer.target_tcp_server_ipv4}")
@@ -114,7 +121,7 @@ def initialize_mitm_server(config_file_path: str):
114
121
  # Initialize engine.
115
122
  current_module = ModuleCategory(config_static.MainConfig.SCRIPT_DIRECTORY)
116
123
  current_module.fill_engine_fields_from_config(engine_config_path)
117
- current_module.initialize_engine(logs_path=config_static.Log.logs_path,
124
+ current_module.initialize_engine(logs_path=config_static.LogRec.logs_path,
118
125
  logger=system_logger)
119
126
 
120
127
  # Extending the full engine domain list with this list.
@@ -125,7 +132,7 @@ def initialize_mitm_server(config_file_path: str):
125
132
  # ==== Initialize Reference Module =============================================================================
126
133
  reference_module = ModuleCategory(config_static.MainConfig.SCRIPT_DIRECTORY)
127
134
  reference_module.fill_engine_fields_from_general_reference(config_static.MainConfig.ENGINES_DIRECTORY_PATH)
128
- reference_module.initialize_engine(logs_path=config_static.Log.logs_path,
135
+ reference_module.initialize_engine(logs_path=config_static.LogRec.logs_path,
129
136
  logger=system_logger, stdout=False, reference_general=True)
130
137
  # === EOF Initialize Reference Module ==========================================================================
131
138
  # === Engine logging ===========================================================================================
@@ -151,8 +158,12 @@ def initialize_mitm_server(config_file_path: str):
151
158
  message = f"[*] Engine domains found, but the DNS routing is set not to use them for routing."
152
159
  print_api(message, color="yellow", logger=system_logger)
153
160
  elif not engines_list and config_static.DNSServer.resolve_to_tcp_server_only_engine_domains:
154
- raise ValueError("No engines were found, but the DNS routing is set to use them for routing.\n"
155
- "Please check your DNS configuration in the 'config.ini' file.")
161
+ error_message = (
162
+ f"No engines were found in: [{config_static.MainConfig.ENGINES_DIRECTORY_PATH}]\n"
163
+ f"But the DNS routing is set to use them for routing.\n"
164
+ f"Please check your DNS configuration in the 'config.ini' file.")
165
+ print_api(error_message, color="red")
166
+ return 1
156
167
 
157
168
  if config_static.DNSServer.resolve_to_tcp_server_all_domains:
158
169
  print_api("All domains will be routed by the DNS server to Built-in TCP Server.", logger=system_logger)
@@ -175,8 +186,12 @@ def initialize_mitm_server(config_file_path: str):
175
186
  message = f"Engines found, and the TCP server is set to use them for processing."
176
187
  print_api(message, logger=system_logger)
177
188
  elif not engines_list and config_static.TCPServer.engines_usage:
178
- raise ValueError("No engines were found, but the TCP server is set to use them for processing.\n"
179
- "Please check your TCP configuration in the 'config.ini' file.")
189
+ error_message = (
190
+ f"No engines were found in: [{config_static.MainConfig.ENGINES_DIRECTORY_PATH}]\n"
191
+ f"But the TCP server is set to use them for processing.\n"
192
+ f"Please check your TCP configuration in the 'config.ini' file.")
193
+ print_api(error_message, color="red")
194
+ return 1
180
195
  else:
181
196
  print_api("TCP Server is disabled.", logger=system_logger, color="yellow")
182
197
 
@@ -188,7 +203,7 @@ def initialize_mitm_server(config_file_path: str):
188
203
  network_logger_name = "network"
189
204
  network_logger = loggingw.create_logger(
190
205
  logger_name=network_logger_name,
191
- directory_path=config_static.Log.logs_path,
206
+ directory_path=config_static.LogRec.logs_path,
192
207
  add_stream=True,
193
208
  add_timedfile=True,
194
209
  formatter_streamhandler='DEFAULT',
@@ -216,7 +231,7 @@ def initialize_mitm_server(config_file_path: str):
216
231
  # Passing the engine domain list to DNS server to work with.
217
232
  # 'list' function re-initializes the current list, or else it will be the same instance object.
218
233
  tcp_resolve_domain_list=list(config_static.Certificates.domains_all_times),
219
- log_directory_path=config_static.Log.logs_path,
234
+ log_directory_path=config_static.LogRec.logs_path,
220
235
  offline_mode=config_static.DNSServer.offline_mode,
221
236
  resolve_to_tcp_server_only_tcp_resolve_domains=(
222
237
  config_static.DNSServer.resolve_to_tcp_server_only_engine_domains),
@@ -268,7 +283,7 @@ def initialize_mitm_server(config_file_path: str):
268
283
  ssh_pass=config_static.ProcessName.ssh_pass,
269
284
  ssh_script_to_execute=config_static.ProcessName.ssh_script_to_execute,
270
285
  logger=listener_logger,
271
- statistics_logs_directory=config_static.Log.logs_path,
286
+ statistics_logs_directory=config_static.LogRec.logs_path,
272
287
  forwarding_dns_service_ipv4_list___only_for_localhost=(
273
288
  config_static.TCPServer.forwarding_dns_service_ipv4_list___only_for_localhost),
274
289
  skip_extension_id_list=config_static.SkipExtensions.SKIP_EXTENSION_ID_LIST,
@@ -298,11 +313,18 @@ def initialize_mitm_server(config_file_path: str):
298
313
  set_dns_gateway = True
299
314
 
300
315
  if set_dns_gateway:
301
- # noinspection PyTypeChecker
302
- dns.set_connection_dns_gateway_static(
303
- dns_servers=dns_gateway_server_list,
304
- use_default_connection=True
305
- )
316
+ # Get current network interface state.
317
+ global NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST
318
+ NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST = dns.get_default_dns_gateway()
319
+
320
+ # Set the DNS gateway to the specified one only if the DNS gateway is dynamic or it is static but different
321
+ # from the one specified in the configuration file.
322
+ if (NETWORK_INTERFACE_IS_DYNAMIC or (not NETWORK_INTERFACE_IS_DYNAMIC and
323
+ NETWORK_INTERFACE_IPV4_ADDRESS_LIST != dns_gateway_server_list)):
324
+ dns.set_connection_dns_gateway_static(
325
+ dns_servers=dns_gateway_server_list,
326
+ use_default_connection=True
327
+ )
306
328
 
307
329
  # General exception handler will catch all the exceptions that occurred in the threads and write it to the log.
308
330
  # noinspection PyBroadException
@@ -319,13 +341,14 @@ def initialize_mitm_server(config_file_path: str):
319
341
  socket_thread.start()
320
342
  except Exception:
321
343
  message = f"Unhandled Exception occurred in 'loop_for_incoming_sockets' function"
322
- print_api(message, error_type=True, color="red", logger=network_logger, traceback_string=True, oneline=True)
344
+ print_api(message, error_type=True, color="red", logger=network_logger, traceback_string=True)
323
345
 
324
346
  # Compress recordings each day in a separate process.
325
347
  recs_archiver_thread = threading.Thread(target=_loop_at_midnight_recs_archive)
326
348
  recs_archiver_thread.daemon = True
327
349
  recs_archiver_thread.start()
328
350
 
351
+ if config_static.DNSServer.enable or config_static.TCPServer.enable:
329
352
  # This is needed for Keyboard Exception.
330
353
  while True:
331
354
  time.sleep(1)
@@ -338,7 +361,8 @@ def _loop_at_midnight_recs_archive():
338
361
  current_date = datetime.datetime.now().strftime('%d')
339
362
  # If it's midnight, start the archiving process.
340
363
  if current_date != previous_date:
341
- recs_files.recs_archiver_in_process(config_static.Recorder.recordings_path)
364
+ if config_static.LogRec.enable_request_response_recordings_in_logs:
365
+ recs_files.recs_archiver_in_process(config_static.LogRec.recordings_path)
342
366
  # Update the previous date.
343
367
  previous_date = current_date
344
368
  # Sleep for 1 minute.
@@ -21,6 +21,7 @@ class ClientMessage:
21
21
  self.info: str = str()
22
22
  self.error: str = str()
23
23
  self.protocol: str = str()
24
+ self.recorded_file_path: str = str()
24
25
 
25
26
  def reinitialize(self) -> None:
26
27
  """
@@ -21,32 +21,45 @@ def recs_archiver(recs_directory: str) -> list:
21
21
 
22
22
  today_date_string = datetime.datetime.now().strftime(REC_FILE_DATE_FORMAT)
23
23
 
24
- all_recs_files = reading.get_logs_paths(
25
- log_files_directory_path=recs_directory,
26
- file_name_pattern='*.json',
27
- date_format=REC_FILE_DATE_FORMAT
28
- )
29
-
30
- archive_directories: list = list()
31
- for recs_file_dict in all_recs_files:
32
- # We don't need to archive today's files.
33
- if today_date_string == recs_file_dict['date_string']:
34
- continue
35
-
36
- target_directory_path: str = f"{recs_directory}{os.sep}{recs_file_dict['date_string']}"
37
- if target_directory_path not in archive_directories:
38
- archive_directories.append(target_directory_path)
39
-
40
- filesystem.create_directory(target_directory_path)
41
- filesystem.move_file(
42
- recs_file_dict['file_path'], f'{target_directory_path}{os.sep}{recs_file_dict["file_name"]}')
43
-
44
- # Archive directories.
24
+ # There should not be recording json files in recs root.
25
+ files_in_recs_root: list = filesystem.get_file_paths_from_directory(
26
+ recs_directory, file_name_check_pattern='*.json', recursive=False)
27
+ if files_in_recs_root:
28
+ raise NotImplementedError("The files in recs root directory are not implemented yet.")
29
+
30
+ # Each engine should have its own directory inside recordings. We will find all the directories inside recs folder.
31
+ directory_paths_in_recs: list = filesystem.get_directory_paths_from_directory(recs_directory, recursive=False)
32
+
33
+ file_list_per_directory: list = list()
34
+ for directory_path in directory_paths_in_recs:
35
+ all_recs_files = reading.get_logs_paths(
36
+ log_files_directory_path=directory_path,
37
+ file_name_pattern='*.json',
38
+ date_format=REC_FILE_DATE_FORMAT
39
+ )
40
+ file_list_per_directory.append((directory_path, all_recs_files))
41
+
45
42
  archived_files: list = list()
46
- for archive_directory in archive_directories:
47
- archived_file: str = zips.archive_directory(
48
- archive_directory, remove_original=True, include_root_directory=True)
49
- archived_files.append(archived_file)
43
+ for directory_path, all_recs_files in file_list_per_directory:
44
+ archive_directories: list = list()
45
+ for recs_file_dict in all_recs_files:
46
+ # We don't need to archive today's files.
47
+ if today_date_string == recs_file_dict['date_string']:
48
+ continue
49
+
50
+ target_directory_path: str = f"{directory_path}{os.sep}{recs_file_dict['date_string']}"
51
+ if target_directory_path not in archive_directories:
52
+ archive_directories.append(target_directory_path)
53
+
54
+ filesystem.create_directory(target_directory_path)
55
+ filesystem.move_file(
56
+ recs_file_dict['file_path'], f'{target_directory_path}{os.sep}{recs_file_dict["file_name"]}')
57
+
58
+ # Archive directories.
59
+ for archive_directory in archive_directories:
60
+ archived_file: str = zips.archive_directory(
61
+ archive_directory, remove_original=True, include_root_directory=True)
62
+ archived_files.append(archived_file)
50
63
 
51
64
  return archived_files
52
65
 
@@ -58,4 +71,3 @@ def recs_archiver_in_process(recs_directory: str):
58
71
 
59
72
  process = multiprocessing.Process(target=recs_archiver, args=(recs_directory,))
60
73
  process.start()
61
-
atomicshop/ssh_remote.py CHANGED
@@ -126,24 +126,24 @@ class SSHRemote:
126
126
  except paramiko.ssh_exception.NoValidConnectionsError as e:
127
127
  error = str(e)
128
128
  # Logging the error also. Since the process name isn't critical, we'll continue script execution.
129
- print_api(error, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
129
+ print_api(error, logger=self.logger, logger_method='error', traceback_string=True)
130
130
  pass
131
131
  except paramiko.ssh_exception.SSHException as e:
132
132
  error = str(e)
133
133
  # Logging the error also. Since the process name isn't critical, we'll continue script execution.
134
- print_api(error, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
134
+ print_api(error, logger=self.logger, logger_method='error', traceback_string=True)
135
135
  pass
136
136
  except ConnectionResetError:
137
137
  # Returning the error.
138
138
  error = "An existing connection was forcibly closed by the remote host."
139
139
  # Logging the error also. Since the process name isn't critical, we'll continue script execution.
140
- print_api(error, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
140
+ print_api(error, logger=self.logger, logger_method='error', traceback_string=True)
141
141
  pass
142
142
  except TimeoutError:
143
143
  # Returning the error.
144
144
  error = "Connection timed out."
145
145
  # Logging the error also. Since the process name isn't critical, we'll continue script execution.
146
- print_api(error, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
146
+ print_api(error, logger=self.logger, logger_method='error', traceback_string=True)
147
147
  pass
148
148
 
149
149
  return error
@@ -165,24 +165,24 @@ class SSHRemote:
165
165
  except AttributeError as function_exception_object:
166
166
  if function_exception_object.name == "open_session":
167
167
  result_exception = "'SSHRemote().connect' wasn't executed."
168
- print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
168
+ print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True)
169
169
 
170
170
  # Since getting Process name is not the main feature of the server, we can pass the exception
171
171
  pass
172
172
  else:
173
173
  result_exception = f"Couldn't execute script over SSH. Unknown yet exception with 'AttributeError' " \
174
174
  f"and name: {function_exception_object.name}"
175
- print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
175
+ print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True)
176
176
  # Since getting Process name is not the main feature of the server, we can pass the exception
177
177
  pass
178
178
  except socket.error:
179
179
  result_exception = "Couldn't execute script over SSH. SSH socket closed."
180
- print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
180
+ print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True)
181
181
  # Since getting Process name is not the main feature of the server, we can pass the exception
182
182
  pass
183
183
  except Exception:
184
184
  result_exception = "Couldn't execute script over SSH. Unknown yet exception."
185
- print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
185
+ print_api(result_exception, logger=self.logger, logger_method='error', traceback_string=True)
186
186
  # Since getting Process name is not the main feature of the server, we can pass the exception
187
187
  pass
188
188
 
@@ -334,7 +334,7 @@ class SSHRemote:
334
334
  # Basically we don't care much about SSH exceptions. Just log them and pass to record.
335
335
  except Exception as function_exception_object:
336
336
  execution_error = function_exception_object
337
- print_api(execution_error, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
337
+ print_api(execution_error, logger=self.logger, logger_method='error', traceback_string=True)
338
338
  pass
339
339
 
340
340
  # Closing SSH connection at this stage.
@@ -114,7 +114,11 @@ def call_method(
114
114
  # Check if the method executed successfully.
115
115
  for result in results:
116
116
  if result != 0:
117
- raise WmiMethodExecutionError(f"Failed to execute method '{method_name}' with error code: {result}")
117
+ if result == 91:
118
+ raise PermissionError(f"Failed to execute method '{method_name}' with error code: {result}, "
119
+ f"Try with Admin rights.")
120
+ else:
121
+ raise WmiMethodExecutionError(f"Failed to execute method '{method_name}' with error code: {result}")
118
122
 
119
123
 
120
124
  """
@@ -133,35 +133,3 @@ def set_dns_server(
133
133
 
134
134
  # Set DNS servers
135
135
  helpers.call_method(adapter_config, 'SetDNSServerSearchOrder', dns_servers)
136
-
137
-
138
- def is_adapter_dns_gateway_from_dhcp(
139
- use_default_interface: bool = False,
140
- connection_name: str = None,
141
- mac_address: str = None
142
- ) -> bool:
143
- """
144
- Check if the adapter is set to obtain the DNS servers automatically from DHCP.
145
- :param use_default_interface: bool, if True, the default network interface will be used.
146
- This is the adapter that your internet is being used from.
147
- :param connection_name: string, adapter name as shown in the network settings.
148
- :param mac_address: string, MAC address of the adapter. Format: '00:00:00:00:00:00'.
149
- :return: bool, True if DHCP is enabled, False otherwise.
150
- """
151
-
152
- adapter_config, current_adapter = get_wmi_network_configuration(
153
- use_default_interface=use_default_interface, connection_name=connection_name, mac_address=mac_address)
154
-
155
- # If DHCP is not enabled.
156
- if not adapter_config.DHCPEnabled:
157
- # Then it is obvious that DNS Gateway is also Statis.
158
- return False
159
- # If DHCP is enabled.
160
- else:
161
- # Then we need to check if Default IP gateway is the same as DNS Gateway, if so.
162
- if adapter_config.DefaultIPGateway == adapter_config.DNSServerSearchOrder:
163
- # Then it is set dynamically from DHCP.
164
- return True
165
- else:
166
- # If not, so it is static.
167
- return False
@@ -230,17 +230,16 @@ class DnsServer:
230
230
  # This error happens when the client closes the connection before the server.
231
231
  # This is not an error for a DNS Server, but we'll log it anyway only with the full DNS logger.
232
232
  message = "Error: to receive DNS request, An existing connection was forcibly closed"
233
- # print_api(message, logger=self.logger, logger_method='error', traceback_string=True, oneline=True)
233
+ # print_api(message, logger=self.logger, logger_method='error', traceback_string=True)
234
234
  print_api(
235
- message, logger=self.dns_full_logger, logger_method='error', traceback_string=True,
236
- oneline=True)
235
+ message, logger=self.dns_full_logger, logger_method='error', traceback_string=True)
237
236
  self.dns_full_logger.info("==========")
238
237
  pass
239
238
  continue
240
239
  except Exception:
241
240
  message = "Unknown Exception: to receive DNS request"
242
241
  print_api(
243
- message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
242
+ message, logger=self.logger, logger_method='critical', traceback_string=True)
244
243
  self.logger.info("==========")
245
244
  pass
246
245
  continue
@@ -417,9 +416,9 @@ class DnsServer:
417
416
  except ValueError:
418
417
  message = f"Looks like wrong type of response for QTYPE: {qtype_string}. Response: "
419
418
  print_api(message, logger=self.logger, logger_method='critical',
420
- traceback_string=True, oneline=True)
419
+ traceback_string=True)
421
420
  print_api(f"{dns_built_response}", logger=self.logger, logger_method='critical',
422
- traceback_string=True, oneline=True)
421
+ traceback_string=True)
423
422
  # Pass the exception.
424
423
  pass
425
424
  # Continue to the next DNS request, since there's nothing to do here right now.
@@ -430,9 +429,9 @@ class DnsServer:
430
429
  (f"Unknown exception while creating response for QTYPE: {qtype_string}. "
431
430
  f"Response: ")
432
431
  print_api(message, logger=self.logger, logger_method='critical',
433
- traceback_string=True, oneline=True)
432
+ traceback_string=True)
434
433
  print_api(f"{dns_built_response}", logger=self.logger, logger_method='critical',
435
- traceback_string=True, oneline=True)
434
+ traceback_string=True)
436
435
  # Pass the exception.
437
436
  pass
438
437
  # Continue to the next DNS request, since there's nothing to do here right now.
@@ -479,7 +478,7 @@ class DnsServer:
479
478
  google_dns_ipv4_socket.recvfrom(self.buffer_size_receive)
480
479
  except TimeoutError as function_exception_object:
481
480
  print_api(function_exception_object, logger=self.logger, logger_method='error',
482
- traceback_string=True, oneline=True)
481
+ traceback_string=True)
483
482
  google_dns_ipv4_socket.close()
484
483
  counter += 1
485
484
  # Pass the exception.
@@ -725,7 +724,7 @@ class DnsServer:
725
724
  except Exception:
726
725
  message = "Unknown Exception: to parse DNS request"
727
726
  print_api(
728
- message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
727
+ message, logger=self.logger, logger_method='critical', traceback_string=True)
729
728
  self.logger.info("==========")
730
729
  pass
731
730
  continue
@@ -27,14 +27,12 @@ def connection_exception_decorator(function_name):
27
27
  message = f"Socket Accept: {kwargs['domain_from_dns_server']}:{port}: " \
28
28
  f"* Established connection was aborted by software on the host..."
29
29
  wrapper_handle_connection_exceptions.message = message
30
- print_api(message, logger_method='error', traceback_string=True, **kwargs['print_kwargs'])
31
- pass
30
+ print_api(message, logger_method='error', traceback_string=True, oneline=True, **kwargs['print_kwargs'])
32
31
  except ConnectionResetError:
33
32
  message = f"Socket Accept: {kwargs['domain_from_dns_server']}:{port}: " \
34
33
  f"* An existing connection was forcibly closed by the remote host..."
35
34
  wrapper_handle_connection_exceptions.message = message
36
35
  print_api(message, logger_method='error', traceback_string=True, oneline=True, **kwargs['print_kwargs'])
37
- pass
38
36
  except ssl.SSLEOFError as e:
39
37
  # A subclass of SSLError raised when the SSL connection has been terminated abruptly. Generally, you
40
38
  # shouldn't try to reuse the underlying transport when this error is encountered.
@@ -47,7 +45,7 @@ def connection_exception_decorator(function_name):
47
45
  message = \
48
46
  f"Socket Accept: {kwargs['domain_from_dns_server']}:{port}: {message}"
49
47
  wrapper_handle_connection_exceptions.message = message
50
- print_api(message, error_type=True, logger_method='error', **kwargs['print_kwargs'])
48
+ print_api(message, error_type=True, logger_method='error', oneline=True, **kwargs['print_kwargs'])
51
49
  except Exception:
52
50
  message = f"Socket Accept: port {port}: {message}"
53
51
  wrapper_handle_connection_exceptions.message = message
@@ -61,7 +59,7 @@ def connection_exception_decorator(function_name):
61
59
  message = \
62
60
  f"Socket Accept: {kwargs['domain_from_dns_server']}:{port}: {message}"
63
61
  wrapper_handle_connection_exceptions.message = message
64
- print_api(message, logger_method='error', **kwargs['print_kwargs'])
62
+ print_api(message, logger_method='error', oneline=True, **kwargs['print_kwargs'])
65
63
  except Exception:
66
64
  message = f"Socket Accept: port {port}: {message}"
67
65
  wrapper_handle_connection_exceptions.message = message
@@ -88,14 +86,14 @@ def connection_exception_decorator(function_name):
88
86
 
89
87
  message = "SSLError on accept. Not documented..."
90
88
  wrapper_handle_connection_exceptions.message = message
91
- print_api(message, logger_method='error', **kwargs['print_kwargs'])
89
+ print_api(message, logger_method='error', oneline=True, **kwargs['print_kwargs'])
92
90
 
93
91
  message = f'ssl.SSLError:{exception_object}'
94
92
  wrapper_handle_connection_exceptions.message = message
95
93
  message = \
96
94
  f"Socket Accept: {kwargs['domain_from_dns_server']}:{port}: {message}"
97
95
  wrapper_handle_connection_exceptions.message = message
98
- print_api(message, logger_method='error', **kwargs['print_kwargs'])
96
+ print_api(message, logger_method='error', oneline=True, **kwargs['print_kwargs'])
99
97
  pass
100
98
  except FileNotFoundError:
101
99
  message = "'SSLSocket.accept()' crashed: 'FileNotFoundError'. Some problem with SSL during Handshake - " \
@@ -67,20 +67,20 @@ class GetCommandLine:
67
67
  except ModuleNotFoundError as function_exception_object:
68
68
  execution_error = f"Module not installed: {function_exception_object}"
69
69
  print_api(
70
- execution_error, error_type=True, logger_method="error", traceback_string=True, oneline=True,
70
+ execution_error, error_type=True, logger_method="error", traceback_string=True,
71
71
  **print_kwargs)
72
72
  pass
73
73
  except psutil.AccessDenied:
74
74
  execution_error = f"Access Denied for 'psutil' to read system process command line. " \
75
75
  f"Run script with Admin Rights."
76
76
  print_api(
77
- execution_error, error_type=True, logger_method="error", traceback_string=True, oneline=True,
77
+ execution_error, error_type=True, logger_method="error", traceback_string=True,
78
78
  **print_kwargs)
79
79
  pass
80
80
  except Exception:
81
81
  execution_error = "There was undocumented exception in localhost script execution."
82
82
  print_api(
83
- execution_error, error_type=True, logger_method="error", traceback_string=True, oneline=True,
83
+ execution_error, error_type=True, logger_method="error", traceback_string=True,
84
84
  **print_kwargs)
85
85
  pass
86
86
 
@@ -49,17 +49,17 @@ class Receiver:
49
49
  class_data = self.ssl_socket.recv(self.buffer_size_receive)
50
50
  except ConnectionAbortedError:
51
51
  message = "* Connection was aborted by the client. Exiting..."
52
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
52
+ print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
53
53
  # This will be treated as empty message - indicate that socket was closed and will be handled properly.
54
54
  pass
55
55
  except ConnectionResetError:
56
56
  message = "* Connection was forcibly closed by the client. Exiting..."
57
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
57
+ print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
58
58
  # This will be treated as empty message - indicate that socket was closed and will be handled properly.
59
59
  pass
60
60
  except ssl.SSLError:
61
61
  message = "* Encountered SSL error on packet receive. Exiting..."
62
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
62
+ print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
63
63
  # This will be treated as empty message - indicate that socket was closed and will be handled properly.
64
64
  pass
65
65
 
@@ -50,7 +50,7 @@ class Sender:
50
50
  self.logger.info(f"Sent the message to destination.")
51
51
  except ConnectionResetError:
52
52
  message = "* Couldn't reach the server - Connection was reset. Exiting..."
53
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True, oneline=True)
53
+ print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
54
54
  # Since the connection is down, it will be handled in thread_worker_main
55
55
  function_result = False
56
56
  pass
@@ -267,7 +267,7 @@ class SNIHandler:
267
267
  print_api(message, **(print_kwargs or {}))
268
268
  except Exception as exception_object:
269
269
  message = f"SNI Handler: Undocumented exception general settings section: {exception_object}"
270
- print_api(message, error_type=True, logger_method="error", traceback_string=True, oneline=True,
270
+ print_api(message, error_type=True, logger_method="error", traceback_string=True,
271
271
  **(print_kwargs or {}))
272
272
  pass
273
273
 
File without changes
@@ -0,0 +1,174 @@
1
+ import winreg
2
+ import socket
3
+
4
+
5
+ def get_network_interfaces_settings(
6
+ interface_guid: str = None
7
+ ) -> dict:
8
+ """
9
+ Get network interface settings from the Windows registry.
10
+
11
+ :param interface_guid: str, GUID of the network interface to retrieve settings for.
12
+ If None, settings for all interfaces will be retrieved.
13
+ :return: dict, network interface settings.
14
+ """
15
+ registry_path = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces"
16
+ network_info = {}
17
+
18
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path) as interfaces_key:
19
+ interface_count = winreg.QueryInfoKey(interfaces_key)[0]
20
+
21
+ for i in range(interface_count):
22
+ current_interface_guid = winreg.EnumKey(interfaces_key, i)
23
+
24
+ # If an interface GUID is provided, and it doesn't match the current one, skip it
25
+ if interface_guid and interface_guid != current_interface_guid:
26
+ continue
27
+
28
+ interface_path = f"{registry_path}\\{current_interface_guid}"
29
+ interface_data = {}
30
+
31
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, interface_path) as key:
32
+ value_count = winreg.QueryInfoKey(key)[1]
33
+
34
+ for j in range(value_count):
35
+ value_name, value_data, _ = winreg.EnumValue(key, j)
36
+ interface_data[value_name] = value_data
37
+
38
+ # Populate the dictionary for the current interface
39
+ network_info[current_interface_guid] = interface_data
40
+
41
+ return network_info
42
+
43
+
44
+ def get_network_connections_to_guids() -> dict:
45
+ """
46
+ Get a dictionary mapping network connection names to their corresponding GUIDs.
47
+
48
+ :return: dict, GUIDs to connection names.
49
+ """
50
+ adapters = {}
51
+ registry_path = r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}"
52
+
53
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path) as network_key:
54
+ adapter_count = winreg.QueryInfoKey(network_key)[0]
55
+
56
+ for i in range(adapter_count):
57
+ adapter_guid = winreg.EnumKey(network_key, i)
58
+ adapter_path = f"{registry_path}\\{adapter_guid}\\Connection"
59
+
60
+ try:
61
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, adapter_path) as connection_key:
62
+ adapter_name, _ = winreg.QueryValueEx(connection_key, "Name")
63
+ adapters[adapter_guid] = adapter_name
64
+ except FileNotFoundError:
65
+ # Some GUIDs might not have a corresponding 'Connection' key, so we skip them
66
+ continue
67
+
68
+ return adapters
69
+
70
+
71
+ def get_enum_info_by_pnpinstanceid(
72
+ pnp_instance_id: str
73
+ ) -> dict:
74
+ """
75
+ Get all information from the Enum registry key for a device with a specific PnPInstanceId.
76
+
77
+ :param pnp_instance_id: str, PnPInstanceId of the device.
78
+ :return: dict, device information.
79
+ """
80
+ enum_registry_path = r"SYSTEM\CurrentControlSet\Enum"
81
+ device_info = {}
82
+
83
+ # Construct the full path to the device in the Enum registry
84
+ hardware_path = f"{enum_registry_path}\\{pnp_instance_id}"
85
+
86
+ # Open the registry key corresponding to the device
87
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, hardware_path) as hardware_key:
88
+ num_values = winreg.QueryInfoKey(hardware_key)[1]
89
+
90
+ # Retrieve all values under this key
91
+ for i in range(num_values):
92
+ value_name, value_data, _ = winreg.EnumValue(hardware_key, i)
93
+ device_info[value_name] = value_data
94
+
95
+ return device_info
96
+
97
+
98
+ def get_network_connections_details(get_enum_info: bool = True) -> dict:
99
+ """
100
+ Get network adapter details from the Windows registry.
101
+
102
+ :param get_enum_info: bool, if True, retrieve all information from the corresponding Enum key.
103
+ This is useful for getting additional information about the network adapter, like make, model, diver details.
104
+ :return: dict, network adapter details.
105
+ """
106
+ adapter_details = {}
107
+ network_registry_path = r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}"
108
+
109
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, network_registry_path) as network_key:
110
+ adapter_count = winreg.QueryInfoKey(network_key)[0]
111
+
112
+ for i in range(adapter_count):
113
+ adapter_guid = winreg.EnumKey(network_key, i)
114
+ adapter_path = f"{network_registry_path}\\{adapter_guid}\\Connection"
115
+
116
+ try:
117
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, adapter_path) as connection_key:
118
+ adapter_name, _ = winreg.QueryValueEx(connection_key, "Name")
119
+ pnp_instance_id, _ = winreg.QueryValueEx(connection_key, "PnPInstanceId")
120
+
121
+ # Get all information from the corresponding Enum key
122
+ if get_enum_info:
123
+ enum_info: dict = get_enum_info_by_pnpinstanceid(pnp_instance_id)
124
+ else:
125
+ enum_info: dict = {}
126
+
127
+ # Store the retrieved information
128
+ adapter_details[adapter_guid] = {
129
+ "Name": adapter_name,
130
+ "PnPInstanceId": pnp_instance_id,
131
+ "EnumInfo": enum_info
132
+ }
133
+
134
+ except FileNotFoundError:
135
+ continue
136
+
137
+ return adapter_details
138
+
139
+
140
+ def get_default_dns_gateway() -> tuple[bool, list[str]]:
141
+ """
142
+ Get the default DNS gateway from the Windows registry.
143
+
144
+ :return: tuple(is dynamic boolean, list of DNS server IPv4s).
145
+ """
146
+
147
+ # Get current default IPv4 address of the interface that is being used for internet.
148
+ default_ipv4_address: str = socket.gethostbyname(socket.gethostname())
149
+
150
+ # Get all network interface settings from the registry.
151
+ all_interfaces_configurations = get_network_interfaces_settings()
152
+
153
+ # Find the interface that has this IPv4 assigned.
154
+ function_result: tuple = tuple()
155
+ for interface_guid, interface_settings in all_interfaces_configurations.items():
156
+ current_interface_static_ipv4_address: list = interface_settings.get('IPAddress', None)
157
+ current_interface_dynamic_ipv4_address: str = interface_settings.get('DhcpIPAddress', None)
158
+
159
+ static_and_ip_match: bool = (
160
+ current_interface_static_ipv4_address and
161
+ current_interface_static_ipv4_address[0] == default_ipv4_address)
162
+ dynamic_and_ip_match: bool = (
163
+ current_interface_dynamic_ipv4_address and
164
+ current_interface_dynamic_ipv4_address == default_ipv4_address)
165
+ if static_and_ip_match or dynamic_and_ip_match:
166
+ if interface_settings['NameServer']:
167
+ function_result = (False, interface_settings['NameServer'].split(' '))
168
+ else:
169
+ function_result = (True, interface_settings['DhcpNameServer'].split(' '))
170
+
171
+ break
172
+
173
+ # noinspection PyTypeChecker
174
+ return function_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.16.9
3
+ Version: 2.16.11
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- atomicshop/__init__.py,sha256=OcWxpppRIMnIhxEtq4LFdHVIdOeaBfBEvavImWUIk8s,123
1
+ atomicshop/__init__.py,sha256=x-01pyB2pWZ28cjWHPctucw-rKzH4SbGkVDLBeV4ww8,124
2
2
  atomicshop/_basics_temp.py,sha256=6cu2dd6r2dLrd1BRNcVDKTHlsHs_26Gpw8QS6v32lQ0,3699
3
3
  atomicshop/_create_pdf_demo.py,sha256=Yi-PGZuMg0RKvQmLqVeLIZYadqEZwUm-4A9JxBl_vYA,3713
4
4
  atomicshop/_patch_import.py,sha256=ENp55sKVJ0e6-4lBvZnpz9PQCt3Otbur7F6aXDlyje4,6334
@@ -10,7 +10,7 @@ atomicshop/console_output.py,sha256=AOSJjrRryE97PAGtgDL03IBtWSi02aNol8noDnW3k6M,
10
10
  atomicshop/console_user_response.py,sha256=31HIy9QGXa7f-GVR8MzJauQ79E_ZqAeagF3Ks4GGdDU,3234
11
11
  atomicshop/datetimes.py,sha256=IQZ66lmta-ZqxYbyHzm_9eugbJFSilXK1e0kfMgoXGg,18371
12
12
  atomicshop/diff_check.py,sha256=Q9RCqRa-jEgo7Fujx08_JTuZ6qcgttMI6aNYB6zN9Ik,27173
13
- atomicshop/dns.py,sha256=5WrAczISjtR9y2rUbqQ8QQxqSGqR5j8dXenUAW0O5Tc,6460
13
+ atomicshop/dns.py,sha256=J4yX6vCaRdL0McYWnlJ9arCDKW-yRui7Y5WyL5BoD6M,6391
14
14
  atomicshop/domains.py,sha256=Rxu6JhhMqFZRcoFs69IoEd1PtYca0lMCG6F1AomP7z4,3197
15
15
  atomicshop/emails.py,sha256=I0KyODQpIMEsNRi9YWSOL8EUPBiWyon3HRdIuSj3AEU,1410
16
16
  atomicshop/file_types.py,sha256=-0jzQMRlmU1AP9DARjk-HJm1tVE22E6ngP2mRblyEjY,763
@@ -35,7 +35,7 @@ atomicshop/scheduling.py,sha256=MvF20M6uU0Kh_CQn2ERxMTLvvF-ToBrdMhXNrKxYFj8,4682
35
35
  atomicshop/script_as_string_processor.py,sha256=uAIWwhHE-eP2FniuwBqEiM6VzyQX96uwdE3aA31rIm8,1883
36
36
  atomicshop/sound.py,sha256=tHiQQbFBk7EYN3pAfGNcxfF9oNsoYnZgu9z9iq8hxQE,24352
37
37
  atomicshop/speech_recognize.py,sha256=55-dIjgkpF93mvJnJuxSFuft5H5eRvGNlUj9BeIOZxk,5903
38
- atomicshop/ssh_remote.py,sha256=Sas3nrQv8ardxR51t59xZZsYm8nvUcA7tMSqEDViRLk,17155
38
+ atomicshop/ssh_remote.py,sha256=HUP4FHaEHexI0EnoLI9i2AgVQNTpsMNPNC4ZCI22EAA,17029
39
39
  atomicshop/sys_functions.py,sha256=MTBxRve5bh58SPvhX3gMiGqHlSBuI_rdNN1NnnBBWqI,906
40
40
  atomicshop/system_resource_monitor.py,sha256=WvnnQrD5W9NRqOWI2YNcL-ut2UrvhrYToVlRR2c1vs8,13720
41
41
  atomicshop/system_resources.py,sha256=0mhDZBEcMzToCOw5ArJhtqYjktOW6iJGdyRkJ01Cpwk,9272
@@ -115,29 +115,29 @@ atomicshop/etws/traces/trace_sysmon_process_creation.py,sha256=OM-bkK38uYMwWLZKN
115
115
  atomicshop/file_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  atomicshop/file_io/csvs.py,sha256=oZiaIEd1q50ypNdd9mlHWb-f7HAdGa_D6jLd3T_4sWU,8777
117
117
  atomicshop/file_io/docxs.py,sha256=rZnv2VMOvct6KBSQn-bHhwbKOi8886jB5u387flq-0E,5755
118
- atomicshop/file_io/file_io.py,sha256=A4_0xwruhQ8jSPpCCTlIBmuLLVB0GHQgiSVznVi4cV0,6395
118
+ atomicshop/file_io/file_io.py,sha256=fkOgoeS8Ow69rj6KfEDmnMeFT3FhJmOJ0X-C_7Udiik,7047
119
119
  atomicshop/file_io/jsons.py,sha256=q9ZU8slBKnHLrtn3TnbK1qxrRpj5ZvCm6AlsFzoANjo,5303
120
120
  atomicshop/file_io/tomls.py,sha256=ol8EvQPf9sryTmZUf1v55BYSUQ6ml7HVVBHpNKbsIlA,9768
121
121
  atomicshop/file_io/xlsxs.py,sha256=v_dyg9GD4LqgWi6wA1QuWRZ8zG4ZwB6Dz52ytdcmmmI,2184
122
122
  atomicshop/file_io/xmls.py,sha256=zh3SuK-dNaFq2NDNhx6ivcf4GYCfGM8M10PcEwDSpxk,2104
123
123
  atomicshop/mitm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
- atomicshop/mitm/config_static.py,sha256=PZp9AAW2ihSUZCen1fhuwFW7Rucd9jA1Pw6pK6Q3DVM,6994
124
+ atomicshop/mitm/config_static.py,sha256=uDuzGl4M96WYJv6WWiQ89speJSCmXzNKwMKcKdaP6ME,7105
125
125
  atomicshop/mitm/config_toml_editor.py,sha256=2p1CMcktWRR_NW-SmyDwylu63ad5e0-w1QPMa8ZLDBw,1635
126
- atomicshop/mitm/connection_thread_worker.py,sha256=I5Oq6tSUIXAS6FYlflZzYDtC1rbIYdIKax_R1WsFjso,19093
127
- atomicshop/mitm/import_config.py,sha256=fvUESPMkpRvkL7HtMaUqQRPwPF0eHV5V6pwueg4DQkU,7881
126
+ atomicshop/mitm/connection_thread_worker.py,sha256=1MBpRoLpzWJMvxqQKizo6IVQ4XYsbKGsjxianNQLUlE,20051
127
+ atomicshop/mitm/import_config.py,sha256=5peDr6cT0ZWK3J53yG-VEew77CKrvB88CphM10SQd3I,7868
128
128
  atomicshop/mitm/initialize_engines.py,sha256=YoSNksMdu4vHjr5xy77t9t5W74zyDZIdjIXrzd3eRXc,8204
129
- atomicshop/mitm/initialize_mitm_server.py,sha256=hwqVk6PMc2iDpl0bLwbUlPrxEmu7Ql75-WWCJxiKh1g,19320
130
- atomicshop/mitm/message.py,sha256=fHvYEyEx-FAXbIMrKMJ_ybBinezF6IFbPlwCqpRrF44,1768
131
- atomicshop/mitm/recs_files.py,sha256=O2nhK9awFAsglUjMlNfgHi-0PbJVGIKQxmEoUE00XUg,2030
129
+ atomicshop/mitm/initialize_mitm_server.py,sha256=oSmL-MspKZY_5s-RDlTZ7-v4gIK_L-6Jyus3R1YeTe8,20665
130
+ atomicshop/mitm/message.py,sha256=d_sm3O_aoZf87dDQP44xOMNEG-uZBN1ZecQgMCacbZs,1814
131
+ atomicshop/mitm/recs_files.py,sha256=lVe-H9IOxm8QShW1-KHlSSHINqRN7pHu3bVxgg5NsTw,2927
132
132
  atomicshop/mitm/shared_functions.py,sha256=hplm98tz8pgJ4WHUVI9sf_oVqUM2KJ1Y2pD6EFSb8P0,1879
133
133
  atomicshop/mitm/statistic_analyzer.py,sha256=E0ba1PjsUEcmQUPPw2YH911lyWkbQb9OSAdgB3pihsY,24658
134
134
  atomicshop/mitm/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
135
  atomicshop/mitm/engines/create_module_template.py,sha256=tRjVSm1sD6FzML71Qbuwvita0qsusdFGm8NZLsZ-XMs,4853
136
136
  atomicshop/mitm/engines/create_module_template_example.py,sha256=X5xhvbV6-g9jU_bQVhf_crZmaH50LRWz3bS-faQ18ds,489
137
137
  atomicshop/mitm/engines/__parent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
- atomicshop/mitm/engines/__parent/parser___parent.py,sha256=sBvPlFMOCTdS-qgW9uTKvvJCFT5nNW9n3UyhaFcjAfg,1365
139
- atomicshop/mitm/engines/__parent/recorder___parent.py,sha256=kIBfETIgRX40-MfoqrCao6dbbAubp2QC_j29B3EY7Do,4666
140
- atomicshop/mitm/engines/__parent/responder___parent.py,sha256=e2cdw3tuvNSXp4C2xKdrz-ytveFih1-OV_rpxhJ3zEM,12214
138
+ atomicshop/mitm/engines/__parent/parser___parent.py,sha256=Q1hEhbOXa8oBm9uD1VG2hLRCzUzpMjGeeS2Npd0a4Hw,1336
139
+ atomicshop/mitm/engines/__parent/recorder___parent.py,sha256=G1TNVxUKFfRopUgENcLCQS0XGzNJmKYBep70wWHvPAQ,4650
140
+ atomicshop/mitm/engines/__parent/responder___parent.py,sha256=gufSZP2Pic6iF7eTzbIi0z5-TPIDeIgoRQAplUwtcNk,12185
141
141
  atomicshop/mitm/engines/__reference_general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  atomicshop/mitm/engines/__reference_general/parser___reference_general.py,sha256=QolWZKm8SiPxxSoyWY_UK7ODam7EUMAgVfOPFnXxODE,2987
143
143
  atomicshop/mitm/engines/__reference_general/recorder___reference_general.py,sha256=KENDVf9OwXD9gwSh4B1XxACCe7iHYjrvnW1t6F64wdE,695
@@ -286,27 +286,29 @@ atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_create.py,sha256=1
286
286
  atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_terminate.py,sha256=0k09fiAwKDJO404bjxUWSSSLOiNANl-VTJDD_YLq-I8,3763
287
287
  atomicshop/wrappers/pywin32w/win_event_log/subscribes/schannel_logging.py,sha256=8nxIcNcbeEuvoBwhujgh7-oIpL9A6J-gg1NM8hOGAVA,3442
288
288
  atomicshop/wrappers/pywin32w/wmis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
289
- atomicshop/wrappers/pywin32w/wmis/helpers.py,sha256=A2Eo1BrS8bNLRNc0V4_724w4dTZasDQFqrvfhf_HY_g,4790
290
- atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py,sha256=LUtcfva5VHhzWFv7W2pE1u-A54sgTqy1jIOg09YfS3g,6828
289
+ atomicshop/wrappers/pywin32w/wmis/helpers.py,sha256=uMXa27UfBpqXInvnmk7CZlqwRI2pg_I_HXelxO9nLLg,5020
290
+ atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py,sha256=9H9MdS__GDBMm8H-xINEPFrJ8j-ErIb1ZqzkulTwLTo,5443
291
291
  atomicshop/wrappers/pywin32w/wmis/win32process.py,sha256=qMzXtJ5hBZ5ydAyqpDbSx0nO2RJQL95HdmV5SzNKMhk,6826
292
292
  atomicshop/wrappers/socketw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
293
293
  atomicshop/wrappers/socketw/accepter.py,sha256=hZZKVYlF3LOHQJsSIEKXZUf6QXXWm-AtqXZevvaYigE,1732
294
294
  atomicshop/wrappers/socketw/base.py,sha256=evoOIxg5Xff3THJnrVX00D5HobaOpDp6_e_gso7TJmA,2191
295
295
  atomicshop/wrappers/socketw/certificator.py,sha256=3CpQKtcW68FSbH6LVSEZTqWBS6Yg_-3K0x4nFkId4UY,12236
296
296
  atomicshop/wrappers/socketw/creator.py,sha256=3_OraDkw2DAWZfoSdY3svCGMOIxpjLEEY7NxWd7M5P4,9873
297
- atomicshop/wrappers/socketw/dns_server.py,sha256=ml8prNrLPFqsoXAEsV6LbGZliMerzI_beE5uM1hmmw4,45278
298
- atomicshop/wrappers/socketw/exception_wrapper.py,sha256=NQ_BH8xLFxi5MlL4bjKMBv8zIb3gy8wqml8UGhBPjzE,7033
299
- atomicshop/wrappers/socketw/get_process.py,sha256=6WJ7PbG2tcyJ5ylDQUnP82TlU0YMH94Ccbhfdj0W4t8,5992
300
- atomicshop/wrappers/socketw/receiver.py,sha256=m8hXKOa8dqEQGUdcbYjshH8-j0CsMGRkge2ifYKhaAw,9050
301
- atomicshop/wrappers/socketw/sender.py,sha256=hHpBLc0LCfOIUErq2mc0ATfp0tDDQ5XhcYT4hRAZARU,3680
302
- atomicshop/wrappers/socketw/sni.py,sha256=JUlPmM5e_Wzv2QcYxOrW_mwYZlft2JMPin6nSXFW9ug,17169
297
+ atomicshop/wrappers/socketw/dns_server.py,sha256=VIpz6cluQ53nZ14QGhnXU9zJqlcOa5fEZsHeKJWr9ec,45127
298
+ atomicshop/wrappers/socketw/exception_wrapper.py,sha256=B-X5SHLSUIWToihH2MKnOB1F4A81_X0DpLLfnYKYbEc,7067
299
+ atomicshop/wrappers/socketw/get_process.py,sha256=_YMVxYhVlzjJpeOR36tphZ5QWeKYwk03Ilw0muCxDbg,5950
300
+ atomicshop/wrappers/socketw/receiver.py,sha256=G3hDTacm7nwwUNHEbKZpxO0c8rHcl0NeKpZy7Xc6zpA,9008
301
+ atomicshop/wrappers/socketw/sender.py,sha256=d7YQFlCBMFTYtkGxbS-8cm5rh5WWFeBVvrEivWHYstI,3666
302
+ atomicshop/wrappers/socketw/sni.py,sha256=fVwyh3h9IqfLMnf4__bMIzcF4c-Kk9mlbDWMRXKN-ow,17155
303
303
  atomicshop/wrappers/socketw/socket_client.py,sha256=FNmTt94YvjZP0X4RPb7icO3xD_nBHQ_XynnObdWFiAU,19682
304
304
  atomicshop/wrappers/socketw/socket_server_tester.py,sha256=SdchUf9qrPk1Rrat0RzvMeN_2NioD7b7a97MkToCYgM,6332
305
305
  atomicshop/wrappers/socketw/socket_wrapper.py,sha256=g7f_8RkW80EZeQWNTqGYnfrQkgAI56T3SwWybq7ZsXg,28521
306
306
  atomicshop/wrappers/socketw/ssl_base.py,sha256=k4V3gwkbq10MvOH4btU4onLX2GNOsSfUAdcHmL1rpVE,2274
307
307
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=Jc0D12crkKRaqoCRQ-2Mz1zm6n4UUx9dXakf-N2TYWA,3065
308
- atomicshop-2.16.9.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
309
- atomicshop-2.16.9.dist-info/METADATA,sha256=8bL36P8qwQEc8SHqOOoozQbo-fb80ZtWfFCLwHF7BsE,10502
310
- atomicshop-2.16.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
311
- atomicshop-2.16.9.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
312
- atomicshop-2.16.9.dist-info/RECORD,,
308
+ atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
309
+ atomicshop/wrappers/winregw/winreg_network.py,sha256=bQ8Jql8bVGBJ0dt3VQ56lga_1LBOMLI3Km_otvvbU6c,7138
310
+ atomicshop-2.16.11.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
311
+ atomicshop-2.16.11.dist-info/METADATA,sha256=zixHONTS2uk6ixSrH4AIvVi7_E9uCZ_0aqYDrzwlgyc,10503
312
+ atomicshop-2.16.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
313
+ atomicshop-2.16.11.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
314
+ atomicshop-2.16.11.dist-info/RECORD,,