atomicshop 2.16.45__py3-none-any.whl → 2.16.48__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 +1 -1
- atomicshop/diff_check.py +3 -3
- atomicshop/mitm/connection_thread_worker.py +134 -106
- atomicshop/mitm/initialize_engines.py +0 -3
- atomicshop/mitm/recs_files.py +0 -1
- atomicshop/mitm/shared_functions.py +0 -2
- atomicshop/web_apis/__init__.py +0 -0
- atomicshop/web_apis/google_custom_search.py +0 -0
- atomicshop/web_apis/google_llm.py +42 -0
- atomicshop/websocket_parse.py +13 -6
- atomicshop/wrappers/fibratusw/install.py +1 -2
- atomicshop/wrappers/socketw/receiver.py +19 -0
- atomicshop/wrappers/socketw/sender.py +1 -1
- atomicshop/wrappers/socketw/sni.py +6 -8
- atomicshop/wrappers/socketw/socket_client.py +26 -11
- atomicshop/wrappers/sysmonw.py +1 -1
- {atomicshop-2.16.45.dist-info → atomicshop-2.16.48.dist-info}/METADATA +1 -1
- {atomicshop-2.16.45.dist-info → atomicshop-2.16.48.dist-info}/RECORD +21 -18
- {atomicshop-2.16.45.dist-info → atomicshop-2.16.48.dist-info}/LICENSE.txt +0 -0
- {atomicshop-2.16.45.dist-info → atomicshop-2.16.48.dist-info}/WHEEL +0 -0
- {atomicshop-2.16.45.dist-info → atomicshop-2.16.48.dist-info}/top_level.txt +0 -0
atomicshop/__init__.py
CHANGED
atomicshop/diff_check.py
CHANGED
|
@@ -54,9 +54,9 @@ class DiffChecker:
|
|
|
54
54
|
function input for that object. So, not always you know what your object type during class initialization.
|
|
55
55
|
:param check_object_display_name: string, name of the object to display in the message.
|
|
56
56
|
If not specified, the provided 'check_object' will be displayed.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
#:param aggregation: boolean, if True, the object will be aggregated with other objects in the list of objects.
|
|
58
|
+
# Meaning, that the object will be checked against the existing objects in the list, and if it is not
|
|
59
|
+
# in the list, it will be added to the list. If it is in the list, it will be ignored.
|
|
60
60
|
:param input_file_path: string, full file path for storing input file for current state of objects,
|
|
61
61
|
to check later if this state isn't updated. If this variable is left empty, all the content will be saved
|
|
62
62
|
in memory and input file will not be used.
|
|
@@ -127,13 +127,7 @@ def thread_worker_main(
|
|
|
127
127
|
network_logger.info(f'Protocol upgraded to Websocket')
|
|
128
128
|
|
|
129
129
|
def parse_websocket(raw_bytes):
|
|
130
|
-
|
|
131
|
-
request_decoded = websocket_frame_parser.parse_frame_bytes(raw_bytes)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
'is_deflated': is_deflated,
|
|
135
|
-
'frame': request_decoded
|
|
136
|
-
}
|
|
130
|
+
return websocket_frame_parser.parse_frame_bytes(raw_bytes)
|
|
137
131
|
|
|
138
132
|
def finish_thread():
|
|
139
133
|
# At this stage there could be several times that the same socket was used to the service server - we need to
|
|
@@ -149,6 +143,111 @@ def thread_worker_main(
|
|
|
149
143
|
|
|
150
144
|
network_logger.info("Thread Finished. Will continue listening on the Main thread")
|
|
151
145
|
|
|
146
|
+
def process_client_raw_data_request() -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Process the client raw data request.
|
|
149
|
+
|
|
150
|
+
:return: True if the socket should be closed, False if not.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
# If the message is empty, then the connection was closed already by the other side,
|
|
154
|
+
# so we can close the socket as well.
|
|
155
|
+
# If the received message from the client is not empty, then continue.
|
|
156
|
+
if not client_received_raw_data:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
# Putting the received message to the aggregating message class.
|
|
160
|
+
client_message.request_raw_bytes = client_received_raw_data
|
|
161
|
+
|
|
162
|
+
parse_http()
|
|
163
|
+
if protocol != '':
|
|
164
|
+
client_message.protocol = protocol
|
|
165
|
+
|
|
166
|
+
# Parse websocket frames only if it is not the first protocol upgrade request.
|
|
167
|
+
if protocol == 'Websocket' and cycle_count != 0:
|
|
168
|
+
client_message.request_raw_decoded = parse_websocket(client_message.request_raw_bytes)
|
|
169
|
+
|
|
170
|
+
# Custom parser, should parse HTTP body or the whole message if not HTTP.
|
|
171
|
+
parser_instance = parser(client_message)
|
|
172
|
+
parser_instance.parse()
|
|
173
|
+
|
|
174
|
+
# Converting body parsed to string on logging, since there is no strict rule for the parameter
|
|
175
|
+
# to be string.
|
|
176
|
+
parser_instance.logger.info(f"{str(client_message.request_body_parsed)[0: 100]}...")
|
|
177
|
+
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def create_responder_response():
|
|
181
|
+
# Since we're in response mode, we'll record the request anyway, after the responder did its job.
|
|
182
|
+
client_message.info = "In Server Response Mode"
|
|
183
|
+
|
|
184
|
+
# Re-initiate the 'client_message.response_list_of_raw_bytes' list, since we'll be appending
|
|
185
|
+
# new entries for empty list.
|
|
186
|
+
client_message.response_list_of_raw_bytes = list()
|
|
187
|
+
|
|
188
|
+
# If it's the first cycle and the protocol is Websocket, then we'll create the HTTP Handshake
|
|
189
|
+
# response automatically.
|
|
190
|
+
if protocol == 'Websocket' and cycle_count == 0:
|
|
191
|
+
client_message.response_list_of_raw_bytes.append(
|
|
192
|
+
websocket_parse.create_byte_http_response(client_message.request_raw_bytes))
|
|
193
|
+
# Creating response for parsed message and printing
|
|
194
|
+
responder.create_response(client_message)
|
|
195
|
+
|
|
196
|
+
# Output first 100 characters of all the responses in the list.
|
|
197
|
+
for response_raw_bytes_single in client_message.response_list_of_raw_bytes:
|
|
198
|
+
responder.logger.info(f"{response_raw_bytes_single[0: 100]}...")
|
|
199
|
+
|
|
200
|
+
def create_client_socket():
|
|
201
|
+
# If we're on localhost, then use external services list in order to resolve the domain:
|
|
202
|
+
# config['tcp']['forwarding_dns_service_ipv4_list___only_for_localhost']
|
|
203
|
+
if client_message.client_ip in base.THIS_DEVICE_IP_LIST:
|
|
204
|
+
service_client_instance = socket_client.SocketClient(
|
|
205
|
+
service_name=client_message.server_name, service_port=client_message.destination_port,
|
|
206
|
+
tls=is_tls,
|
|
207
|
+
dns_servers_list=(
|
|
208
|
+
config_static.TCPServer.forwarding_dns_service_ipv4_list___only_for_localhost),
|
|
209
|
+
logger=network_logger
|
|
210
|
+
)
|
|
211
|
+
# If we're not on localhost, then connect to domain directly.
|
|
212
|
+
else:
|
|
213
|
+
service_client_instance = socket_client.SocketClient(
|
|
214
|
+
service_name=client_message.server_name, service_port=client_message.destination_port,
|
|
215
|
+
tls=is_tls, logger=network_logger)
|
|
216
|
+
|
|
217
|
+
return service_client_instance
|
|
218
|
+
|
|
219
|
+
def process_received_response_from_service_client():
|
|
220
|
+
if client_message.error is not None:
|
|
221
|
+
statistics_error_list.append(client_message.error)
|
|
222
|
+
|
|
223
|
+
# Since we need a list for raw bytes, we'll add the 'response_raw_bytes' to our list object.
|
|
224
|
+
# But we need to re-initiate it first.
|
|
225
|
+
client_message.response_list_of_raw_bytes = list()
|
|
226
|
+
# If there was error during send or receive from the service and response was None,
|
|
227
|
+
# It means that there was no response at all because of the error.
|
|
228
|
+
if client_message.error and response_raw_bytes is None:
|
|
229
|
+
client_message.response_list_of_raw_bytes.append(None)
|
|
230
|
+
# If there was no error, but response came empty, it means that the service has closed the
|
|
231
|
+
# socket after it received the request, without sending any data.
|
|
232
|
+
elif client_message.error is None and response_raw_bytes is None:
|
|
233
|
+
client_message.response_list_of_raw_bytes.append("")
|
|
234
|
+
else:
|
|
235
|
+
client_message.response_list_of_raw_bytes.append(response_raw_bytes)
|
|
236
|
+
|
|
237
|
+
client_message.response_list_of_raw_decoded = list()
|
|
238
|
+
# Make HTTP Response parsing only if there was response at all.
|
|
239
|
+
if response_raw_bytes:
|
|
240
|
+
response_raw_decoded, is_http_response, response_parsing_error = (
|
|
241
|
+
HTTPResponseParse(response_raw_bytes).parse())
|
|
242
|
+
|
|
243
|
+
if is_http_response:
|
|
244
|
+
client_message.response_list_of_raw_decoded.append(response_raw_decoded)
|
|
245
|
+
elif protocol == 'Websocket' and cycle_count != 0:
|
|
246
|
+
response_decoded = parse_websocket(response_raw_bytes)
|
|
247
|
+
client_message.response_list_of_raw_decoded.append(response_decoded)
|
|
248
|
+
else:
|
|
249
|
+
client_message.response_list_of_raw_decoded.append(None)
|
|
250
|
+
|
|
152
251
|
# Building client message object before the loop only for any exception to occurs, since we write it to
|
|
153
252
|
# recording file in its current state.
|
|
154
253
|
client_message: ClientMessage = ClientMessage()
|
|
@@ -197,6 +296,7 @@ def thread_worker_main(
|
|
|
197
296
|
network_logger.info(f"Thread Created - Client [{client_ip}:{source_port}] | "
|
|
198
297
|
f"Destination service: [{server_name}:{destination_port}]")
|
|
199
298
|
|
|
299
|
+
end_socket: bool = False
|
|
200
300
|
service_client = None
|
|
201
301
|
# Loop while received message is not empty, if so, close socket, since other side already closed.
|
|
202
302
|
# noinspection PyTypeChecker
|
|
@@ -223,79 +323,34 @@ def thread_worker_main(
|
|
|
223
323
|
# Getting current time of message received from client.
|
|
224
324
|
client_message.request_time_received = datetime.now()
|
|
225
325
|
|
|
226
|
-
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if client_received_raw_data:
|
|
235
|
-
# Putting the received message to the aggregating message class.
|
|
236
|
-
client_message.request_raw_bytes = client_received_raw_data
|
|
237
|
-
|
|
238
|
-
parse_http()
|
|
239
|
-
if protocol != '':
|
|
240
|
-
client_message.protocol = protocol
|
|
326
|
+
# Peek if there is some data in the socket.
|
|
327
|
+
# This is needed to check if the client just connects without sending data, if so we need to try and
|
|
328
|
+
# receive data from the server and send it to the client.
|
|
329
|
+
# We will do it only on the first cycle, after that the connection should work as usual.
|
|
330
|
+
# Sometimes the client will execute connection without sending data, just for the server to send response.
|
|
331
|
+
is_socket_ready: bool = True
|
|
332
|
+
if cycle_count == 0:
|
|
333
|
+
is_socket_ready = receiver.is_socket_ready_for_read(client_socket)
|
|
241
334
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
335
|
+
if is_socket_ready:
|
|
336
|
+
network_logger.info(f"Initializing Receiver on cycle: {str(cycle_count+1)}")
|
|
337
|
+
# Getting message from the client over the socket using specific class.
|
|
338
|
+
client_received_raw_data = receiver.Receiver(
|
|
339
|
+
ssl_socket=client_socket, logger=network_logger).receive()
|
|
245
340
|
|
|
246
|
-
|
|
247
|
-
parser_instance = parser(client_message)
|
|
248
|
-
parser_instance.parse()
|
|
249
|
-
|
|
250
|
-
# Converting body parsed to string on logging, since there is no strict rule for the parameter
|
|
251
|
-
# to be string.
|
|
252
|
-
parser_instance.logger.info(f"{str(client_message.request_body_parsed)[0: 100]}...")
|
|
341
|
+
end_socket = process_client_raw_data_request()
|
|
253
342
|
|
|
343
|
+
if not end_socket:
|
|
254
344
|
# If we're in response mode, execute responder.
|
|
255
345
|
response_raw_bytes = None
|
|
256
346
|
if config_static.TCPServer.server_response_mode:
|
|
257
|
-
|
|
258
|
-
client_message.info = "In Server Response Mode"
|
|
259
|
-
|
|
260
|
-
# Re-initiate the 'client_message.response_list_of_raw_bytes' list, since we'll be appending
|
|
261
|
-
# new entries for empty list.
|
|
262
|
-
client_message.response_list_of_raw_bytes = list()
|
|
263
|
-
|
|
264
|
-
# If it's the first cycle and the protocol is Websocket, then we'll create the HTTP Handshake
|
|
265
|
-
# response automatically.
|
|
266
|
-
if protocol == 'Websocket' and cycle_count == 0:
|
|
267
|
-
client_message.response_list_of_raw_bytes.append(
|
|
268
|
-
websocket_parse.create_byte_http_response(client_message.request_raw_bytes))
|
|
269
|
-
# Creating response for parsed message and printing
|
|
270
|
-
responder.create_response(client_message)
|
|
271
|
-
|
|
272
|
-
# Output first 100 characters of all the responses in the list.
|
|
273
|
-
for response_raw_bytes in client_message.response_list_of_raw_bytes:
|
|
274
|
-
if response_raw_bytes:
|
|
275
|
-
responder.logger.info(f"{response_raw_bytes[0: 100]}...")
|
|
276
|
-
else:
|
|
277
|
-
responder.logger.info(f"Response empty...")
|
|
347
|
+
create_responder_response()
|
|
278
348
|
# Else, we're not in response mode, then execute client connect and record section.
|
|
279
349
|
else:
|
|
280
350
|
# If "service_client" object is not defined, we'll define it.
|
|
281
|
-
# If it's defined, then
|
|
282
|
-
# domain.
|
|
351
|
+
# If it's defined, then there's still active "ssl_socket" with connection to the service domain.
|
|
283
352
|
if not service_client:
|
|
284
|
-
|
|
285
|
-
# config['tcp']['forwarding_dns_service_ipv4_list___only_for_localhost']
|
|
286
|
-
if client_message.client_ip in base.THIS_DEVICE_IP_LIST:
|
|
287
|
-
service_client = socket_client.SocketClient(
|
|
288
|
-
service_name=client_message.server_name, service_port=client_message.destination_port,
|
|
289
|
-
tls=is_tls,
|
|
290
|
-
dns_servers_list=(
|
|
291
|
-
config_static.TCPServer.forwarding_dns_service_ipv4_list___only_for_localhost),
|
|
292
|
-
logger=network_logger
|
|
293
|
-
)
|
|
294
|
-
# If we're not on localhost, then connect to domain directly.
|
|
295
|
-
else:
|
|
296
|
-
service_client = socket_client.SocketClient(
|
|
297
|
-
service_name=client_message.server_name, service_port=client_message.destination_port,
|
|
298
|
-
tls=is_tls, logger=network_logger)
|
|
353
|
+
service_client = create_client_socket()
|
|
299
354
|
|
|
300
355
|
# Sending current client message and receiving a response.
|
|
301
356
|
# If there was an error it will be passed to "client_message" object class and if not, "None" will
|
|
@@ -303,40 +358,9 @@ def thread_worker_main(
|
|
|
303
358
|
# If there was connection error or socket close, then "ssl_socket" of the "service_client"
|
|
304
359
|
# will be empty.
|
|
305
360
|
response_raw_bytes, client_message.error, client_message.server_ip, service_ssl_socket = (
|
|
306
|
-
service_client.send_receive_to_service(client_message.request_raw_bytes))
|
|
307
|
-
|
|
308
|
-
if client_message.error is not None:
|
|
309
|
-
statistics_error_list.append(client_message.error)
|
|
310
|
-
|
|
311
|
-
# Since we need a list for raw bytes, we'll add the 'response_raw_bytes' to our list object.
|
|
312
|
-
# But we need to re-initiate it first.
|
|
313
|
-
client_message.response_list_of_raw_bytes = list()
|
|
314
|
-
# If there was error during send or receive from the service and response was None,
|
|
315
|
-
# It means that there was no response at all because of the error.
|
|
316
|
-
if client_message.error and response_raw_bytes is None:
|
|
317
|
-
client_message.response_list_of_raw_bytes.append(None)
|
|
318
|
-
# If there was no error, but response came empty, it means that the service has closed the
|
|
319
|
-
# socket after it received the request, without sending any data.
|
|
320
|
-
elif client_message.error is None and response_raw_bytes is None:
|
|
321
|
-
client_message.response_list_of_raw_bytes.append("")
|
|
322
|
-
else:
|
|
323
|
-
client_message.response_list_of_raw_bytes.append(response_raw_bytes)
|
|
324
|
-
|
|
325
|
-
client_message.response_list_of_raw_decoded = list()
|
|
326
|
-
# Make HTTP Response parsing only if there was response at all.
|
|
327
|
-
if response_raw_bytes:
|
|
328
|
-
response_raw_decoded, is_http_response, response_parsing_error = (
|
|
329
|
-
HTTPResponseParse(response_raw_bytes).parse())
|
|
330
|
-
|
|
331
|
-
if is_http_response:
|
|
332
|
-
client_message.response_list_of_raw_decoded.append(response_raw_decoded)
|
|
333
|
-
elif protocol == 'Websocket' and cycle_count != 0:
|
|
334
|
-
response_decoded = parse_websocket(response_raw_bytes)
|
|
335
|
-
client_message.response_list_of_raw_decoded.append(response_decoded)
|
|
336
|
-
else:
|
|
337
|
-
client_message.response_list_of_raw_decoded.append(None)
|
|
338
|
-
|
|
361
|
+
service_client.send_receive_to_service(client_message.request_raw_bytes, (not is_socket_ready)))
|
|
339
362
|
|
|
363
|
+
process_received_response_from_service_client()
|
|
340
364
|
|
|
341
365
|
# So if the socket was closed and there was an error we can break the loop
|
|
342
366
|
if not service_ssl_socket:
|
|
@@ -344,8 +368,8 @@ def thread_worker_main(
|
|
|
344
368
|
recorded = True
|
|
345
369
|
break
|
|
346
370
|
|
|
347
|
-
# If there is a response, then send it.
|
|
348
|
-
if
|
|
371
|
+
# If there is a response(s), then send it.
|
|
372
|
+
if client_message.response_list_of_raw_bytes:
|
|
349
373
|
# Sending response/s to client no matter if in record mode or not.
|
|
350
374
|
network_logger.info(
|
|
351
375
|
f"Sending messages to client: {len(client_message.response_list_of_raw_bytes)}")
|
|
@@ -368,7 +392,10 @@ def thread_worker_main(
|
|
|
368
392
|
|
|
369
393
|
record_and_statistics_write()
|
|
370
394
|
recorded = True
|
|
371
|
-
|
|
395
|
+
|
|
396
|
+
# If the message is empty, then the connection was closed already by the other side, also if there will
|
|
397
|
+
# be empty response from the server, so we can close the socket as well and exceptions will be raised.
|
|
398
|
+
if end_socket:
|
|
372
399
|
# If it's the first cycle we will record the message from the client if it came empty.
|
|
373
400
|
if cycle_count == 0:
|
|
374
401
|
record_and_statistics_write()
|
|
@@ -376,6 +403,7 @@ def thread_worker_main(
|
|
|
376
403
|
# In other cases, we'll just break the loop, since empty message means that the other side closed the
|
|
377
404
|
# connection.
|
|
378
405
|
recorded = True
|
|
406
|
+
|
|
379
407
|
break
|
|
380
408
|
|
|
381
409
|
finish_thread()
|
|
@@ -5,12 +5,9 @@ from pathlib import Path
|
|
|
5
5
|
from .. import filesystem
|
|
6
6
|
from ..file_io import tomls
|
|
7
7
|
from ..basics.classes import import_first_class_name_from_file_path
|
|
8
|
-
from ..wrappers.loggingw import loggingw
|
|
9
8
|
from .engines.__reference_general import parser___reference_general, responder___reference_general, \
|
|
10
9
|
recorder___reference_general
|
|
11
10
|
|
|
12
|
-
from . import config_static
|
|
13
|
-
|
|
14
11
|
|
|
15
12
|
class ModuleCategory:
|
|
16
13
|
def __init__(self, script_directory: str):
|
atomicshop/mitm/recs_files.py
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import google.generativeai as genai
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GoogleLLM:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
llm_api_key: str
|
|
10
|
+
) -> None:
|
|
11
|
+
self.genai = genai
|
|
12
|
+
|
|
13
|
+
os.environ["API_KEY"] = llm_api_key
|
|
14
|
+
genai.configure(api_key=os.environ["API_KEY"])
|
|
15
|
+
|
|
16
|
+
def get_current_models(self) -> list[str]:
|
|
17
|
+
""" Function to get the current models available in the Gemini API """
|
|
18
|
+
result_list: list[str] = []
|
|
19
|
+
for model in self.genai.list_models():
|
|
20
|
+
result_list.append(model.name)
|
|
21
|
+
|
|
22
|
+
return result_list
|
|
23
|
+
|
|
24
|
+
def get_answer_online(
|
|
25
|
+
self,
|
|
26
|
+
search_query: str,
|
|
27
|
+
additional_llm_instructions: str,
|
|
28
|
+
number_of_top_links: int = 2,
|
|
29
|
+
number_of_characters_per_link: int = 15000,
|
|
30
|
+
temperature: float = 0,
|
|
31
|
+
max_output_tokens: int = 4096
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Function to get the answer to a question by searching Google Custom Console API and processing the content using Gemini API.
|
|
35
|
+
:param search_query:
|
|
36
|
+
:param additional_llm_instructions:
|
|
37
|
+
:param number_of_top_links:
|
|
38
|
+
:param number_of_characters_per_link:
|
|
39
|
+
:param temperature:
|
|
40
|
+
:param max_output_tokens:
|
|
41
|
+
:return:
|
|
42
|
+
"""
|
atomicshop/websocket_parse.py
CHANGED
|
@@ -129,15 +129,15 @@ class WebsocketFrameParser:
|
|
|
129
129
|
def process_frame(current_frame):
|
|
130
130
|
if current_frame.opcode == Opcode.TEXT:
|
|
131
131
|
message = current_frame.data.decode('utf-8', errors='replace')
|
|
132
|
-
return message
|
|
132
|
+
return message, 'TEXT'
|
|
133
133
|
elif current_frame.opcode == Opcode.BINARY:
|
|
134
|
-
return current_frame.data
|
|
134
|
+
return current_frame.data, 'BINARY'
|
|
135
135
|
elif current_frame.opcode == Opcode.CLOSE:
|
|
136
|
-
|
|
136
|
+
return None, 'CLOSE'
|
|
137
137
|
elif current_frame.opcode == Opcode.PING:
|
|
138
|
-
|
|
138
|
+
return None, 'PING'
|
|
139
139
|
elif current_frame.opcode == Opcode.PONG:
|
|
140
|
-
|
|
140
|
+
return None, 'PONG'
|
|
141
141
|
else:
|
|
142
142
|
raise WebsocketParseWrongOpcode("Received unknown frame with opcode:", current_frame.opcode)
|
|
143
143
|
|
|
@@ -152,12 +152,19 @@ class WebsocketFrameParser:
|
|
|
152
152
|
|
|
153
153
|
# Parse and process frames
|
|
154
154
|
frame = parse_frame(masked, deflated)
|
|
155
|
-
|
|
155
|
+
parsed_frame, frame_opcode = process_frame(frame)
|
|
156
156
|
|
|
157
157
|
# This is basically not needed since we restart the 'reader = StreamReader()' each function execution.
|
|
158
158
|
# # After processing, reset the reader's buffer
|
|
159
159
|
# reader.buffer = b''
|
|
160
160
|
|
|
161
|
+
result: dict = {
|
|
162
|
+
'is_deflated': deflated,
|
|
163
|
+
'is_masked': masked,
|
|
164
|
+
'frame': parsed_frame,
|
|
165
|
+
'opcode': frame_opcode
|
|
166
|
+
}
|
|
167
|
+
|
|
161
168
|
return result
|
|
162
169
|
|
|
163
170
|
|
|
@@ -40,8 +40,7 @@ def install_fibratus(
|
|
|
40
40
|
exclude_string='slim')
|
|
41
41
|
|
|
42
42
|
# Install the MSI file
|
|
43
|
-
msiw.install_msi(
|
|
44
|
-
msi_path=fibratus_setup_file_path, exit_on_error=True, as_admin=True)
|
|
43
|
+
msiw.install_msi(msi_path=fibratus_setup_file_path)
|
|
45
44
|
|
|
46
45
|
count = 0
|
|
47
46
|
while count != WAIT_SECONDS_FOR_EXECUTABLE_TO_APPEAR_AFTER_INSTALLATION:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
3
|
import ssl
|
|
4
|
+
import select
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from ...print_api import print_api
|
|
@@ -20,6 +21,24 @@ def peek_first_bytes(client_socket, bytes_amount: int = 1) -> bytes:
|
|
|
20
21
|
return client_socket.recv(bytes_amount, socket.MSG_PEEK)
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def is_socket_ready_for_read(client_socket, timeout: int = 0) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check if socket is ready for read.
|
|
27
|
+
|
|
28
|
+
:param client_socket: Socket object.
|
|
29
|
+
:param timeout: Timeout in seconds. The default is no timeout.
|
|
30
|
+
|
|
31
|
+
:return: True if socket is ready for read, False otherwise.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Use select to check if the socket is ready for reading.
|
|
35
|
+
# 'readable' returns a list of sockets that are ready for reading.
|
|
36
|
+
# Since we use only one socket, it will return a list with one element if the socket is ready for reading,
|
|
37
|
+
# or an empty list if the socket is not ready for reading.
|
|
38
|
+
readable, _, _ = select.select([client_socket], [], [], timeout)
|
|
39
|
+
return bool(readable)
|
|
40
|
+
|
|
41
|
+
|
|
23
42
|
class Receiver:
|
|
24
43
|
""" Receiver Class is responsible for receiving the message from socket and populate the message class """
|
|
25
44
|
def __init__(
|
|
@@ -254,15 +254,13 @@ class SNIHandler:
|
|
|
254
254
|
|
|
255
255
|
# Try on general settings in the SNI function.
|
|
256
256
|
try:
|
|
257
|
-
# Check if SNI was passed.
|
|
258
|
-
if self.sni_received_parameters.destination_name:
|
|
259
|
-
service_name_from_sni = self.sni_received_parameters.destination_name
|
|
260
|
-
# If no SNI was passed.
|
|
261
|
-
else:
|
|
257
|
+
# Check if SNI was passed. If no SNI was passed.
|
|
258
|
+
if not self.sni_received_parameters.destination_name:
|
|
262
259
|
# If DNS server is enabled we'll get the domain from dns server.
|
|
263
260
|
if self.domain_from_dns_server:
|
|
264
|
-
|
|
265
|
-
message =
|
|
261
|
+
self.sni_received_parameters.destination_name = self.domain_from_dns_server
|
|
262
|
+
message = \
|
|
263
|
+
f"SNI Handler: No SNI was passed, using domain from DNS Server: {self.domain_from_dns_server}"
|
|
266
264
|
print_api(message, **(print_kwargs or {}))
|
|
267
265
|
# If DNS server is disabled, the domain from dns server will be empty.
|
|
268
266
|
else:
|
|
@@ -271,7 +269,7 @@ class SNIHandler:
|
|
|
271
269
|
print_api(message, **(print_kwargs or {}))
|
|
272
270
|
|
|
273
271
|
# Setting "server_hostname" as a domain.
|
|
274
|
-
self.sni_received_parameters.ssl_socket.server_hostname =
|
|
272
|
+
self.sni_received_parameters.ssl_socket.server_hostname = self.sni_received_parameters.destination_name
|
|
275
273
|
message = \
|
|
276
274
|
f"SNI Handler: port {self.sni_received_parameters.ssl_socket.getsockname()[1]}: " \
|
|
277
275
|
f"Incoming connection for [{self.sni_received_parameters.ssl_socket.server_hostname}]"
|
|
@@ -163,7 +163,7 @@ class SocketClient:
|
|
|
163
163
|
error_string: str = f"Socket Client Connect: {destination}: {exception_type}"
|
|
164
164
|
|
|
165
165
|
if exception_type in ['ConnectionRefusedError', 'ConnectionAbortedError', 'ConnectionResetError',
|
|
166
|
-
'
|
|
166
|
+
'TimeoutError'] or 'ssl' in exception_type.lower():
|
|
167
167
|
error_message: str = f"{error_string}: {exception_error}"
|
|
168
168
|
print_api(error_message, logger=self.logger, logger_method='error')
|
|
169
169
|
return None, error_message
|
|
@@ -192,7 +192,18 @@ class SocketClient:
|
|
|
192
192
|
self.logger.info(f"Closed socket to service server [{self.service_name}:{self.service_port}]")
|
|
193
193
|
|
|
194
194
|
# noinspection PyUnusedLocal
|
|
195
|
-
def send_receive_to_service(
|
|
195
|
+
def send_receive_to_service(
|
|
196
|
+
self,
|
|
197
|
+
request_bytes: Union[bytearray, bytes],
|
|
198
|
+
skip_send: bool = False
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Function to send data to service server and receive response.
|
|
202
|
+
|
|
203
|
+
:param request_bytes: The data that will be sent to the service server.
|
|
204
|
+
:param skip_send: If True, the data will not be sent to the service server. After the connection is established,
|
|
205
|
+
the function will wait for the response only.
|
|
206
|
+
"""
|
|
196
207
|
# Define variables
|
|
197
208
|
function_service_data = None
|
|
198
209
|
error_message = None
|
|
@@ -214,18 +225,22 @@ class SocketClient:
|
|
|
214
225
|
self.logger.info(
|
|
215
226
|
f"[{self.service_name}] resolves to ip: [{self.connection_ip}]. Pulled IP from the socket.")
|
|
216
227
|
|
|
217
|
-
#
|
|
218
|
-
error_on_send: str =
|
|
219
|
-
|
|
228
|
+
# noinspection PyTypeChecker
|
|
229
|
+
error_on_send: str = None
|
|
230
|
+
if not skip_send:
|
|
231
|
+
# Send the data received from the client to the service over socket
|
|
232
|
+
error_on_send = Sender(
|
|
233
|
+
ssl_socket=self.socket_instance, class_message=request_bytes, logger=self.logger).send()
|
|
220
234
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
# If the socket disconnected on data send
|
|
236
|
+
if error_on_send:
|
|
237
|
+
error_message = f"Service socket closed on data send: {error_on_send}"
|
|
238
|
+
|
|
239
|
+
# We'll close the socket and nullify the object
|
|
240
|
+
self.close_socket()
|
|
224
241
|
|
|
225
|
-
# We'll close the socket and nullify the object
|
|
226
|
-
self.close_socket()
|
|
227
242
|
# Else if send was successful
|
|
228
|
-
|
|
243
|
+
if not error_on_send:
|
|
229
244
|
function_service_data = Receiver(
|
|
230
245
|
ssl_socket=self.socket_instance, logger=self.logger).receive()
|
|
231
246
|
|
atomicshop/wrappers/sysmonw.py
CHANGED
|
@@ -49,7 +49,7 @@ def is_sysmon_running():
|
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
51
|
process_list: list = process.get_running_processes_by_cmdline_pattern(
|
|
52
|
-
pattern=SYSMON_FILE_NAME, first=True,
|
|
52
|
+
pattern=SYSMON_FILE_NAME, first=True, cmdline_case_insensitive=True)
|
|
53
53
|
|
|
54
54
|
if process_list:
|
|
55
55
|
return True
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
atomicshop/__init__.py,sha256=
|
|
1
|
+
atomicshop/__init__.py,sha256=g4si8ZVmv-cmrXDPNTeEfgvWGFAXNNlfF5E-_j8xNMo,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
|
|
@@ -9,7 +9,7 @@ atomicshop/config_init.py,sha256=50kD2lXP8sgwPekcmAbfADcY46YvXkF-6XIdA7W_638,250
|
|
|
9
9
|
atomicshop/console_output.py,sha256=AOSJjrRryE97PAGtgDL03IBtWSi02aNol8noDnW3k6M,4667
|
|
10
10
|
atomicshop/console_user_response.py,sha256=31HIy9QGXa7f-GVR8MzJauQ79E_ZqAeagF3Ks4GGdDU,3234
|
|
11
11
|
atomicshop/datetimes.py,sha256=IQZ66lmta-ZqxYbyHzm_9eugbJFSilXK1e0kfMgoXGg,18371
|
|
12
|
-
atomicshop/diff_check.py,sha256=
|
|
12
|
+
atomicshop/diff_check.py,sha256=vxTDccVbGZHEge6Ja9_ArLWwslOUgIoJAdYPylh4cZg,27176
|
|
13
13
|
atomicshop/dns.py,sha256=5Gimq_WY2arqg7BeGmR7P--fGfnH0Dsh8lrOt_H0jRY,6817
|
|
14
14
|
atomicshop/domains.py,sha256=Rxu6JhhMqFZRcoFs69IoEd1PtYca0lMCG6F1AomP7z4,3197
|
|
15
15
|
atomicshop/emails.py,sha256=I0KyODQpIMEsNRi9YWSOL8EUPBiWyon3HRdIuSj3AEU,1410
|
|
@@ -45,7 +45,7 @@ atomicshop/urls.py,sha256=aJ0NGS9qqaKeqjkkWBs80jaBBg6MYBiPuLIyPGxscVc,1557
|
|
|
45
45
|
atomicshop/uuids.py,sha256=JSQdm3ZTJiwPQ1gYe6kU0TKS_7suwVrHc8JZDGYlydM,2214
|
|
46
46
|
atomicshop/virtualization.py,sha256=LPP4vjE0Vr10R6DA4lqhfX_WaNdDGRAZUW0Am6VeGco,494
|
|
47
47
|
atomicshop/web.py,sha256=GLdTXgMxg1_0UQaXC4bOvARVyuFg7SPIeJdsCHV8rNE,11662
|
|
48
|
-
atomicshop/websocket_parse.py,sha256=
|
|
48
|
+
atomicshop/websocket_parse.py,sha256=bkOwiXCNw5bQg1J4KOG7es5kUGysyX2NdfFFVBceSzg,16662
|
|
49
49
|
atomicshop/a_installs/ubuntu/docker_rootless.py,sha256=9IPNtGZYjfy1_n6ZRt7gWz9KZgR6XCgevjqq02xk-o0,281
|
|
50
50
|
atomicshop/a_installs/ubuntu/docker_sudo.py,sha256=JzayxeyKDtiuT4Icp2L2LyFRbx4wvpyN_bHLfZ-yX5E,281
|
|
51
51
|
atomicshop/a_installs/ubuntu/elastic_search_and_kibana.py,sha256=yRB-l1zBxdiN6av-FwNkhcBlaeu4zrDPjQ0uPGgpK2I,244
|
|
@@ -126,13 +126,13 @@ atomicshop/file_io/xmls.py,sha256=zh3SuK-dNaFq2NDNhx6ivcf4GYCfGM8M10PcEwDSpxk,21
|
|
|
126
126
|
atomicshop/mitm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
127
127
|
atomicshop/mitm/config_static.py,sha256=ROAtbibSWSsF3BraUbhu-QO3MPIFqYY5KUKgsQbiSkk,7813
|
|
128
128
|
atomicshop/mitm/config_toml_editor.py,sha256=2p1CMcktWRR_NW-SmyDwylu63ad5e0-w1QPMa8ZLDBw,1635
|
|
129
|
-
atomicshop/mitm/connection_thread_worker.py,sha256=
|
|
129
|
+
atomicshop/mitm/connection_thread_worker.py,sha256=vfFClzwDfaoT4zHCOfky4DnEDoSonp9N1MCj62dS9Nk,20451
|
|
130
130
|
atomicshop/mitm/import_config.py,sha256=0Ij14aISTllTOiWYJpIUMOWobQqGofD6uafui5uWllE,9272
|
|
131
|
-
atomicshop/mitm/initialize_engines.py,sha256=
|
|
131
|
+
atomicshop/mitm/initialize_engines.py,sha256=Naseof9JGY7oxDz9ueOyW7-KYTu7QDlFxv2ABqq9DD4,8219
|
|
132
132
|
atomicshop/mitm/message.py,sha256=URR5JKSuAT8XmGIkyprEjlPW2GW4ef_gfUz_GgcFseE,2184
|
|
133
133
|
atomicshop/mitm/mitm_main.py,sha256=5c-9oxBiLueTbZr4Dyd4EEOorEUix5vSWxX9p5O1fBs,23375
|
|
134
|
-
atomicshop/mitm/recs_files.py,sha256=
|
|
135
|
-
atomicshop/mitm/shared_functions.py,sha256=
|
|
134
|
+
atomicshop/mitm/recs_files.py,sha256=gzFuTonqcXkMvhpOj1Nwse3E8umFGrKN2H5AleMjJ3w,3051
|
|
135
|
+
atomicshop/mitm/shared_functions.py,sha256=0lzeyINd44sVEfFbahJxQmz6KAMWbYrW5ou3UYfItvw,1777
|
|
136
136
|
atomicshop/mitm/statistic_analyzer.py,sha256=5_sAYGX2Xunzo_pS2W5WijNCwr_BlGJbbOO462y_wN4,27533
|
|
137
137
|
atomicshop/mitm/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
138
138
|
atomicshop/mitm/engines/create_module_template.py,sha256=tRjVSm1sD6FzML71Qbuwvita0qsusdFGm8NZLsZ-XMs,4853
|
|
@@ -175,6 +175,9 @@ atomicshop/startup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
175
175
|
atomicshop/startup/win/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
176
176
|
atomicshop/startup/win/startup_folder.py,sha256=2RZEyF-Mf8eWPlt_-OaoGKKnMs6YhELEzJZ376EI0E0,1891
|
|
177
177
|
atomicshop/startup/win/task_scheduler.py,sha256=qALe-8sfthYxsdCViH2r8OsH3x-WauDqteg5RzElPdk,4348
|
|
178
|
+
atomicshop/web_apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
179
|
+
atomicshop/web_apis/google_custom_search.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
180
|
+
atomicshop/web_apis/google_llm.py,sha256=WVLqyfZHFIGEncxdBvrHCv2FbvQw40z75uMGzq9lxB4,1291
|
|
178
181
|
atomicshop/wrappers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
179
182
|
atomicshop/wrappers/_process_wrapper_curl.py,sha256=XkZZXYl7D0Q6UfdWqy-18AvpU0yVp9i2BVD2qRcXlkk,841
|
|
180
183
|
atomicshop/wrappers/_process_wrapper_tar.py,sha256=WUMZFKNrlG4nJP9tWZ51W7BR1j_pIjsjgyAStmWjRGs,655
|
|
@@ -189,7 +192,7 @@ atomicshop/wrappers/olefilew.py,sha256=biD5m58rogifCYmYhJBrAFb9O_Bn_spLek_9HofLe
|
|
|
189
192
|
atomicshop/wrappers/pipw.py,sha256=mu4jnHkSaYNfpBiLZKMZxEX_E2LqW5BVthMZkblPB_c,1317
|
|
190
193
|
atomicshop/wrappers/process_wrapper_pbtk.py,sha256=ycPmBRnv627RWks6N8OhxJQe8Gu3h3Vwj-4HswPOw0k,599
|
|
191
194
|
atomicshop/wrappers/pyopensslw.py,sha256=OBWxA6EJ2vU_Qlf4M8m6ilcG3hyYB4yB0EsXUf7NhEU,6804
|
|
192
|
-
atomicshop/wrappers/sysmonw.py,sha256=
|
|
195
|
+
atomicshop/wrappers/sysmonw.py,sha256=CdawuWuy_uUi3ALCm6lKP7pSyKeTk1MXyzOaTMbBSO8,5346
|
|
193
196
|
atomicshop/wrappers/ubuntu_terminal.py,sha256=3UJaje_Ke5G9xEyj3b37XZ_KjR_FSSnb4gupdCyI-jE,11965
|
|
194
197
|
atomicshop/wrappers/wslw.py,sha256=2Z7X0j5M2hoRZjbHfm_vqwNXZeptsdkNCdhdcM_S9vo,6998
|
|
195
198
|
atomicshop/wrappers/certauthw/certauth.py,sha256=hKedW0DOWlEigSNm8wu4SqHkCQsGJ1tJfH7s4nr3Bk0,12223
|
|
@@ -246,7 +249,7 @@ atomicshop/wrappers/factw/rest/router.py,sha256=fdGok5ESBxcZHIBgM93l4yTPRGoeooQN
|
|
|
246
249
|
atomicshop/wrappers/factw/rest/statistics.py,sha256=vznwzKP1gEF7uXz3HsuV66BU9wrp73N_eFqpFpye9Qw,653
|
|
247
250
|
atomicshop/wrappers/factw/rest/status.py,sha256=4O3xS1poafwyUiLDkhyx4oMMe4PBwABuRPpOMnMKgIU,641
|
|
248
251
|
atomicshop/wrappers/fibratusw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
249
|
-
atomicshop/wrappers/fibratusw/install.py,sha256=
|
|
252
|
+
atomicshop/wrappers/fibratusw/install.py,sha256=GnaAAqcXRhovxZ3x5uB9RAXTMCh5xd5k1niCKTzh4Z0,3242
|
|
250
253
|
atomicshop/wrappers/loggingw/consts.py,sha256=JWiUJEydjhwatBxtIJsGTmDUSTLbmIRidtR6qRLMaIY,1608
|
|
251
254
|
atomicshop/wrappers/loggingw/filters.py,sha256=48UVhJHemCS0agXmQP8dHvAHM8r9DFphJ1TNEBP3Dlg,3545
|
|
252
255
|
atomicshop/wrappers/loggingw/formatters.py,sha256=ZY12IokVY1G_Wzn2Zlv9qjK-e8CtIK6yUgUfPHvH2BU,5802
|
|
@@ -305,18 +308,18 @@ atomicshop/wrappers/socketw/creator.py,sha256=3_OraDkw2DAWZfoSdY3svCGMOIxpjLEEY7
|
|
|
305
308
|
atomicshop/wrappers/socketw/dns_server.py,sha256=RklzINNuoMQn4PGGQEI5hiAldprbVwwvikY6u9X-jTY,49067
|
|
306
309
|
atomicshop/wrappers/socketw/exception_wrapper.py,sha256=B-X5SHLSUIWToihH2MKnOB1F4A81_X0DpLLfnYKYbEc,7067
|
|
307
310
|
atomicshop/wrappers/socketw/get_process.py,sha256=aJC-_qFUv3NgWCSUzDI72E4z8_-VTZE9NVZ0CwUoNlM,5698
|
|
308
|
-
atomicshop/wrappers/socketw/receiver.py,sha256
|
|
309
|
-
atomicshop/wrappers/socketw/sender.py,sha256=
|
|
310
|
-
atomicshop/wrappers/socketw/sni.py,sha256=
|
|
311
|
-
atomicshop/wrappers/socketw/socket_client.py,sha256=
|
|
311
|
+
atomicshop/wrappers/socketw/receiver.py,sha256=-QtKK0T_lmoAIypTYaIKOD3pgB1npWGPxcVEN37y_gk,10060
|
|
312
|
+
atomicshop/wrappers/socketw/sender.py,sha256=gwSzF51QD5paeeFav6fpbQpO8KgBO5lNztHYQyN5id0,4959
|
|
313
|
+
atomicshop/wrappers/socketw/sni.py,sha256=Nc8WMZZR21o5GXILQLVWbf7OzNPXAfE8trJY153e9Qk,17591
|
|
314
|
+
atomicshop/wrappers/socketw/socket_client.py,sha256=9_VXXo8r4upP5v0Rhzx7dJIblM23_0Ggh2PfktYj-fE,20489
|
|
312
315
|
atomicshop/wrappers/socketw/socket_server_tester.py,sha256=Qobmh4XV8ZxLUaw-eW4ESKAbeSLecCKn2OWFzMhadk0,6420
|
|
313
316
|
atomicshop/wrappers/socketw/socket_wrapper.py,sha256=WtylpezgIIBuz-A6PfM0hO1sm9Exd4j3qhDXcFc74-E,35567
|
|
314
317
|
atomicshop/wrappers/socketw/ssl_base.py,sha256=kmiif84kMhBr5yjQW17p935sfjR5JKG0LxIwBA4iVvU,2275
|
|
315
318
|
atomicshop/wrappers/socketw/statistics_csv.py,sha256=SDYI1cN0oaapvPeLxSXiJrelTy6xbZl-bopR0jAjVGE,3149
|
|
316
319
|
atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
317
320
|
atomicshop/wrappers/winregw/winreg_network.py,sha256=zZQfps-CdODQaTUADbHAwKHr5RUg7BLafnKWBbKaLN4,8728
|
|
318
|
-
atomicshop-2.16.
|
|
319
|
-
atomicshop-2.16.
|
|
320
|
-
atomicshop-2.16.
|
|
321
|
-
atomicshop-2.16.
|
|
322
|
-
atomicshop-2.16.
|
|
321
|
+
atomicshop-2.16.48.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
|
|
322
|
+
atomicshop-2.16.48.dist-info/METADATA,sha256=zxLRU6GxTBO675CZCx48ARUCy2CYk9CTkD-0jhyE3AQ,10500
|
|
323
|
+
atomicshop-2.16.48.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
324
|
+
atomicshop-2.16.48.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
|
|
325
|
+
atomicshop-2.16.48.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|