atomicshop 2.12.26__py3-none-any.whl → 2.13.1__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.12.26'
4
+ __version__ = '2.13.1'
@@ -24,6 +24,18 @@ def get_first_key_and_value_dict(input_dict: dict) -> dict:
24
24
  return {first_key: input_dict[first_key]}
25
25
 
26
26
 
27
+ def get_last_added_key_value(input_dict: dict) -> dict:
28
+ """
29
+ The function will return the last added key and value in a dictionary.
30
+
31
+ :param input_dict: dict, the dictionary to get the last added key and value.
32
+ :return: dict, the last added key and value in the dictionary.
33
+ """
34
+
35
+ last_key = list(input_dict.keys())[-1]
36
+ return {last_key: input_dict[last_key]}
37
+
38
+
27
39
  def remove_keys(input_dict: dict, key_list: list) -> None:
28
40
  """
29
41
  The function will remove a key from dictionary without raising an exception if it doesn't exist.
@@ -0,0 +1,10 @@
1
+ """
2
+ # Add static files to your package / module pyproject.toml:
3
+ [tool.setuptools.package-data]
4
+ "atomicshop.addons" = ["**"]
5
+
6
+ # Read relative path of your module inside your package / module script:
7
+ from importlib.resources import files
8
+ PACKAGE_DLL_PATH = 'addons/process_list/compiled/Win10x64/process_list.dll'
9
+ FULL_DLL_PATH = str(files(__package__).joinpath(PACKAGE_DLL_PATH))
10
+ """
atomicshop/diff_check.py CHANGED
@@ -314,10 +314,10 @@ class DiffChecker:
314
314
  try:
315
315
  if self.save_as == 'txt':
316
316
  self.previous_content = file_io.read_file(
317
- self.input_file_path, stderr=False, **(print_kwargs or {}))
317
+ self.input_file_path, stdout=False, stderr=False, **(print_kwargs or {}))
318
318
  elif self.save_as == 'json':
319
319
  self.previous_content = jsons.read_json_file(
320
- self.input_file_path, stderr=False, **(print_kwargs or {}))
320
+ self.input_file_path, stdout=False, stderr=False, **(print_kwargs or {}))
321
321
  except FileNotFoundError as except_object:
322
322
  message = f"Input File [{Path(except_object.filename).name}] doesn't exist - Will create new one."
323
323
  print_api(message, color='yellow', **(print_kwargs or {}))
@@ -433,10 +433,11 @@ class DiffChecker:
433
433
  if self.input_file_path:
434
434
  if self.save_as == 'txt':
435
435
  # noinspection PyTypeChecker
436
- file_io.write_file(self.previous_content, self.input_file_path, **(print_kwargs or {}))
436
+ file_io.write_file(self.previous_content, self.input_file_path, stdout=False, **(print_kwargs or {}))
437
437
  elif self.save_as == 'json':
438
438
  jsons.write_json_file(
439
- self.previous_content, self.input_file_path, use_default_indent=True, **(print_kwargs or {}))
439
+ self.previous_content, self.input_file_path, use_default_indent=True, stdout=False,
440
+ **(print_kwargs or {}))
440
441
 
441
442
  return result, message
442
443
 
atomicshop/dns.py CHANGED
@@ -18,15 +18,6 @@ TYPES_DICT = {
18
18
  '255': 'ANY'
19
19
  }
20
20
 
21
- # Event Tracing DNS info.
22
- ETW_DNS_INFO = {
23
- 'provider_name': 'Microsoft-Windows-DNS-Client',
24
- 'provider_guid': '{1C95126E-7EEA-49A9-A3FE-A378B03DDB4D}',
25
- # Event ID 3008 got DNS Queries and DNS Answers. Meaning, that information in ETW will arrive after DNS Response
26
- # is received and not After DNS Query is sent.
27
- 'event_id': 3008
28
- }
29
-
30
21
 
31
22
  def resolve_dns_localhost(domain_name: str, dns_servers_list: list = None, print_kwargs: dict = None) -> str:
32
23
  """
@@ -0,0 +1,38 @@
1
+ ETW_DNS = {
2
+ 'provider_name': 'Microsoft-Windows-DNS-Client',
3
+ 'provider_guid': '{1C95126E-7EEA-49A9-A3FE-A378B03DDB4D}',
4
+ 'event_ids': {
5
+ # Event ID 3008 got DNS Queries and DNS Answers. Meaning, that information in ETW will arrive after DNS Response
6
+ # is received and not after DNS Query Request is sent.
7
+ 'dns_request_response': 3008
8
+ }
9
+ }
10
+
11
+
12
+ # Event Tracing Kernel-Process.
13
+ ETW_KERNEL_PROCESS = {
14
+ 'provider_name': 'Microsoft-Windows-Kernel-Process',
15
+ 'provider_guid': '{22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716}',
16
+ 'event_ids': {
17
+ 'process_create': 1 # Emits all the processes that are started.
18
+ }
19
+ }
20
+
21
+
22
+ # You need SYSTEM privileges to execute this one. Try with psexec -s -i
23
+ ETW_SECURITY_AUDITING = {
24
+ 'provider_name': 'Microsoft-Windows-Security-Auditing',
25
+ 'provider_guid': '{54849625-5478-4994-A5BA-3E3B0328C30D}',
26
+ 'event_ids': {
27
+ 'process_create': 4688 # Emits all the processes that are started.
28
+ }
29
+ }
30
+
31
+
32
+ ETW_SYSMON = {
33
+ 'provider_name': 'Microsoft-Windows-Sysmon',
34
+ 'provider_guid': '{5770385F-C22A-43E0-BF4C-06F5698FFBD9}',
35
+ 'event_ids': {
36
+ 'process_create': 1 # Emits all the processes that are started.
37
+ }
38
+ }
File without changes
@@ -1,18 +1,31 @@
1
- from . import trace
2
- from .. import dns
3
- from ..wrappers.psutilw import psutilw
4
- from ..basics import dicts
5
- from ..process_poller import ProcessPollerPool
6
- from ..print_api import print_api
1
+ import time
7
2
 
3
+ from .. import trace, const
4
+ from ...wrappers.psutilw import psutilw
5
+ from ...basics import dicts
6
+ from ...process_poller import ProcessPollerPool
7
+ from ...print_api import print_api
8
+ from ... import dns
8
9
 
9
- class DnsTrace:
10
+
11
+ ETW_DEFAULT_SESSION_NAME: str = 'AtomicShopDnsTrace'
12
+
13
+ PROVIDER_NAME: str = const.ETW_DNS['provider_name']
14
+ PROVIDER_GUID: str = const.ETW_DNS['provider_guid']
15
+ REQUEST_RESP_EVENT_ID: int = const.ETW_DNS['event_ids']['dns_request_response']
16
+
17
+ WAIT_FOR_PROCESS_POLLER_PID_SECONDS: int = 3
18
+ WAIT_FOR_PROCESS_POLLER_PID_COUNTS: int = WAIT_FOR_PROCESS_POLLER_PID_SECONDS * 10
19
+
20
+
21
+ class DnsRequestResponseTrace:
10
22
  def __init__(
11
23
  self,
12
- enable_process_poller: bool = False,
13
24
  attrs: list = None,
14
25
  session_name: str = None,
15
- close_existing_session_name: bool = True
26
+ close_existing_session_name: bool = True,
27
+ enable_process_poller: bool = False,
28
+ process_poller_etw_session_name: str = None
16
29
  ):
17
30
  """
18
31
  DnsTrace class use to trace DNS events from Windows Event Tracing for EventId 3008.
@@ -28,6 +41,7 @@ class DnsTrace:
28
41
  False: if ETW session with 'session_name' exists, you will be notified and the new session will not be
29
42
  created. Instead, the existing session will be used. If there is a buffer from the previous session,
30
43
  you will get the events from the buffer.
44
+ :param process_poller_etw_session_name: The name of the ETW session for tracing process creation.
31
45
 
32
46
  -------------------------------------------------
33
47
 
@@ -35,7 +49,13 @@ class DnsTrace:
35
49
  from atomicshop.etw import dns_trace
36
50
 
37
51
 
38
- dns_trace_w = dns_trace.DnsTrace(enable_process_poller=True, attrs=['pid', 'name', 'cmdline', 'domain', 'query_type'])
52
+ dns_trace_w = dns_trace.DnsTrace(
53
+ attrs=['pid', 'name', 'cmdline', 'domain', 'query_type'],
54
+ session_name='MyDnsTrace',
55
+ close_existing_session_name=True,
56
+ enable_process_poller=True,
57
+ process_poller_etw_session_name='MyProcessTrace'
58
+ )
39
59
  dns_trace_w.start()
40
60
  while True:
41
61
  dns_dict = dns_trace_w.emit()
@@ -46,16 +66,21 @@ class DnsTrace:
46
66
  self.enable_process_poller = enable_process_poller
47
67
  self.attrs = attrs
48
68
 
69
+ if not session_name:
70
+ session_name = ETW_DEFAULT_SESSION_NAME
71
+
49
72
  self.event_trace = trace.EventTrace(
50
- providers=[(dns.ETW_DNS_INFO['provider_name'], dns.ETW_DNS_INFO['provider_guid'])],
73
+ providers=[(PROVIDER_NAME, PROVIDER_GUID)],
51
74
  # lambda x: self.event_queue.put(x),
52
- event_id_filters=[dns.ETW_DNS_INFO['event_id']],
75
+ event_id_filters=[REQUEST_RESP_EVENT_ID],
53
76
  session_name=session_name,
54
77
  close_existing_session_name=close_existing_session_name
55
78
  )
56
79
 
57
80
  if self.enable_process_poller:
58
- self.process_poller = ProcessPollerPool(store_cycles=200, operation='process', poller_method='process_dll')
81
+ self.process_poller = ProcessPollerPool(
82
+ operation='process', poller_method='sysmon_etw',
83
+ sysmon_etw_session_name=process_poller_etw_session_name)
59
84
 
60
85
  def start(self):
61
86
  if self.enable_process_poller:
@@ -82,6 +107,11 @@ class DnsTrace:
82
107
  :return: Dictionary with the event data.
83
108
  """
84
109
 
110
+ # Get the processes first, since we need the process name and command line.
111
+ # If they're not ready, we will get just pids from DNS tracing.
112
+ if self.enable_process_poller:
113
+ self._get_processes_from_poller()
114
+
85
115
  event = self.event_trace.emit()
86
116
 
87
117
  event_dict: dict = {
@@ -92,12 +122,6 @@ class DnsTrace:
92
122
  'query_type': dns.TYPES_DICT[str(event[1]['QueryType'])]
93
123
  }
94
124
 
95
- # # Get process name only from psutil, just in case.
96
- # try:
97
- # process_name = psutilw.get_process_name_by_pid(event_dict['pid'])
98
- # except psutil.NoSuchProcess:
99
- # process_name = str(event_dict['pid'])
100
-
101
125
  # Defining list if ips and other answers, which aren't IPs.
102
126
  list_of_ips = list()
103
127
  list_of_other_domains = list()
@@ -135,10 +159,19 @@ class DnsTrace:
135
159
  event_dict['status'] = 'Error'
136
160
 
137
161
  if self.enable_process_poller:
138
- processes = self.process_poller.processes
139
-
140
- if isinstance(processes, BaseException):
141
- raise processes
162
+ processes = self.process_poller.get_processes()
163
+ if event_dict['pid'] not in processes:
164
+ counter = 0
165
+ while counter < WAIT_FOR_PROCESS_POLLER_PID_COUNTS:
166
+ processes = self.process_poller.get_processes()
167
+ if event_dict['pid'] not in processes:
168
+ time.sleep(0.1)
169
+ counter += 1
170
+ else:
171
+ break
172
+
173
+ if counter == WAIT_FOR_PROCESS_POLLER_PID_COUNTS:
174
+ print_api(f"Error: Couldn't get the process name for PID: {event_dict['pid']}.", color='red')
142
175
 
143
176
  event_dict = psutilw.cross_single_connection_with_processes(event_dict, processes)
144
177
  # If it was impossible to get the process name from the process poller, get it from psutil.
@@ -150,3 +183,13 @@ class DnsTrace:
150
183
  event_dict, self.attrs, skip_keys_not_in_list=True)
151
184
 
152
185
  return event_dict
186
+
187
+ def _get_processes_from_poller(self):
188
+ processes: dict = {}
189
+ while not processes:
190
+ processes = self.process_poller.get_processes()
191
+
192
+ if isinstance(processes, BaseException):
193
+ raise processes
194
+
195
+ return processes
@@ -0,0 +1,116 @@
1
+ from .. import trace, const
2
+ from ...wrappers.psutilw import psutilw
3
+ from ...wrappers import sysmonw
4
+ from ...basics import dicts
5
+ from ...print_api import print_api
6
+
7
+
8
+ PROVIDER_NAME: str = const.ETW_SYSMON['provider_name']
9
+ PROVIDER_GUID: str = const.ETW_SYSMON['provider_guid']
10
+ PROCESS_CREATION_EVENT_ID: int = const.ETW_SYSMON['event_ids']['process_create']
11
+
12
+
13
+ class SysmonProcessCreationTrace:
14
+ def __init__(
15
+ self,
16
+ attrs: list = None,
17
+ session_name: str = None,
18
+ close_existing_session_name: bool = True,
19
+ sysmon_directory: str = None
20
+ ):
21
+ """
22
+ DnsTrace class use to trace DNS events from Windows Event Tracing for EventId 3008.
23
+
24
+ :param attrs: List of attributes to return. If None, all attributes will be returned.
25
+ :param session_name: The name of the session to create. If not provided, a UUID will be generated.
26
+ :param close_existing_session_name: Boolean to close existing session names.
27
+ True: if ETW session with 'session_name' exists, you will be notified and the session will be closed.
28
+ Then the new session with this name will be created.
29
+ False: if ETW session with 'session_name' exists, you will be notified and the new session will not be
30
+ created. Instead, the existing session will be used. If there is a buffer from the previous session,
31
+ you will get the events from the buffer.
32
+ :param sysmon_directory: The directory where Sysmon is located. If not provided, "C:\\Windows\\Sysmon" will be
33
+ used. If 'Sysmon.exe' is not found in the directory, it will be downloaded from the internet.
34
+
35
+ -------------------------------------------------
36
+
37
+ Usage Example:
38
+ from atomicshop.etw import dns_trace
39
+
40
+
41
+ dns_trace_w = dns_trace.DnsTrace(
42
+ attrs=['pid', 'name', 'cmdline', 'domain', 'query_type'],
43
+ session_name='MyDnsTrace',
44
+ close_existing_session_name=True,
45
+ enable_process_poller=True,
46
+ process_poller_etw_session_name='MyProcessTrace'
47
+ )
48
+ dns_trace_w.start()
49
+ while True:
50
+ dns_dict = dns_trace_w.emit()
51
+ print(dns_dict)
52
+ dns_trace_w.stop()
53
+ """
54
+
55
+ self.attrs = attrs
56
+ self.sysmon_directory: str = sysmon_directory
57
+
58
+ self.event_trace = trace.EventTrace(
59
+ providers=[(PROVIDER_NAME, PROVIDER_GUID)],
60
+ # lambda x: self.event_queue.put(x),
61
+ event_id_filters=[PROCESS_CREATION_EVENT_ID],
62
+ session_name=session_name,
63
+ close_existing_session_name=close_existing_session_name
64
+ )
65
+
66
+ def start(self):
67
+ sysmonw.start_as_service(
68
+ installation_path=self.sysmon_directory, download_sysmon_if_not_found=True, skip_if_running=True)
69
+ self.event_trace.start()
70
+
71
+ def stop(self):
72
+ self.event_trace.stop()
73
+
74
+ def emit(self):
75
+ """
76
+ Function that will return the next event from the queue.
77
+ The queue is blocking, so if there is no event in the queue, the function will wait until there is one.
78
+
79
+ Usage Example:
80
+ while True:
81
+ dns_dict = dns_trace.emit()
82
+ print(dns_dict)
83
+
84
+ :return: Dictionary with the event data.
85
+ """
86
+
87
+ event = self.event_trace.emit()
88
+
89
+ event_dict: dict = {
90
+ 'etw_id': event[0],
91
+ 'pid': event[1]['ProcessId'],
92
+ 'process_guid': event[1]['ProcessGuid'],
93
+ 'image': event[1]['Image'],
94
+ 'file_version': event[1]['FileVersion'],
95
+ 'product': event[1]['Product'],
96
+ 'company': event[1]['Company'],
97
+ 'original_file_name': event[1]['OriginalFileName'],
98
+ 'command_line': event[1]['CommandLine'],
99
+ 'current_directory': event[1]['CurrentDirectory'],
100
+ 'user': event[1]['User'],
101
+ 'logon_id': event[1]['LogonId'],
102
+ 'logon_guid': event[1]['LogonGuid'],
103
+ 'terminal_session_id': event[1]['TerminalSessionId'],
104
+ 'integrity_level': event[1]['IntegrityLevel'],
105
+ 'hashes': event[1]['Hashes'],
106
+ 'parent_process_guid': event[1]['ParentProcessGuid'],
107
+ 'parent_process_id': event[1]['ParentProcessId'],
108
+ 'parent_image': event[1]['ParentImage'],
109
+ 'parent_command_line': event[1]['ParentCommandLine']
110
+ }
111
+
112
+ if self.attrs:
113
+ event_dict = dicts.reorder_keys(
114
+ event_dict, self.attrs, skip_keys_not_in_list=True)
115
+
116
+ return event_dict
@@ -41,7 +41,8 @@ class ChangeMonitor:
41
41
  'url_playwright_jpeg'],
42
42
  None] = None,
43
43
  object_type_settings: dict = None,
44
- etw_session_name: str = None
44
+ etw_session_name: str = None,
45
+ etw_process_session_name: str = None
45
46
  ):
46
47
  """
47
48
  :param object_type: string, type of object to check. The type must be one of the following:
@@ -88,6 +89,8 @@ class ChangeMonitor:
88
89
  with logman and other tools: logman query -ets
89
90
  If not provided, a default name will be generated.
90
91
  'dns': 'AtomicShopDnsTrace'
92
+ :param etw_process_session_name: string, the name of the ETW session for tracing process creation.
93
+ This is needed to correlate the process cmd with the DNS requests PIDs.
91
94
 
92
95
  If 'input_directory' is not specified, the 'input_file_name' is not specified, and
93
96
  'generate_input_file_name' is False, then the input file will not be used and the object will be stored
@@ -104,6 +107,7 @@ class ChangeMonitor:
104
107
  self.object_type = object_type
105
108
  self.object_type_settings: dict = object_type_settings
106
109
  self.etw_session_name: str = etw_session_name
110
+ self.etw_process_session_name: str = etw_process_session_name
107
111
 
108
112
  # === Additional variables ========================================
109
113
 
@@ -1,14 +1,13 @@
1
1
  from pathlib import Path
2
2
  from typing import Union
3
3
 
4
- from ...etw.trace_dns import DnsTrace
4
+ from ...etws.traces import trace_dns
5
5
  from ...print_api import print_api
6
6
  from ...import diff_check
7
7
 
8
8
 
9
9
  INPUT_FILE_DEFAULT_NAME: str = 'known_domains.json'
10
10
  INPUT_STATISTICS_FILE_DEFAULT_NAME: str = 'dns_statistics.json'
11
- ETW_DEFAULT_SESSION_NAME: str = 'AtomicShopDnsTrace'
12
11
 
13
12
 
14
13
  class DnsCheck:
@@ -22,15 +21,16 @@ class DnsCheck:
22
21
  self.diff_checker_statistics: Union[diff_check.DiffChecker, None] = None
23
22
  self.settings: dict = change_monitor_instance.object_type_settings
24
23
 
25
- if change_monitor_instance.etw_session_name:
26
- self.etw_session_name: str = change_monitor_instance.etw_session_name
27
- else:
28
- self.etw_session_name: str = ETW_DEFAULT_SESSION_NAME
24
+ self.etw_session_name: str = change_monitor_instance.etw_session_name
29
25
 
30
- self.fetch_engine: DnsTrace = (
31
- DnsTrace(
32
- enable_process_poller=True, attrs=['name', 'cmdline', 'domain', 'query_type'],
33
- session_name=self.etw_session_name, close_existing_session_name=True)
26
+ self.fetch_engine: trace_dns.DnsRequestResponseTrace = (
27
+ trace_dns.DnsRequestResponseTrace(
28
+ attrs=['name', 'cmdline', 'domain', 'query_type'],
29
+ session_name=self.etw_session_name,
30
+ close_existing_session_name=True,
31
+ enable_process_poller=True,
32
+ process_poller_etw_session_name=change_monitor_instance.etw_process_session_name
33
+ )
34
34
  )
35
35
 
36
36
  if self.settings['alert_always'] and self.settings['alert_about_missing_entries_after_learning']:
@@ -106,7 +106,7 @@ class DnsCheck:
106
106
 
107
107
  def _aggregation_process(self, event_dict: dict, return_list: list, print_kwargs: dict = None):
108
108
  self.diff_checker_aggregation.check_object = [event_dict]
109
-
109
+
110
110
  # Check if 'known_domains' list was updated from previous cycle.
111
111
  result, message = self.diff_checker_aggregation.check_list_of_dicts(
112
112
  sort_by_keys=['cmdline', 'name'], print_kwargs=print_kwargs)
atomicshop/process.py CHANGED
@@ -11,7 +11,7 @@ from .basics import strings
11
11
  from .wrappers import ubuntu_terminal
12
12
 
13
13
  if os.name == 'nt':
14
- from .process_poller import GetProcessList
14
+ from . import process_poller
15
15
 
16
16
 
17
17
  def is_command_exists(cmd: str) -> bool:
@@ -265,7 +265,7 @@ def match_pattern_against_running_processes_cmdlines(
265
265
  """
266
266
 
267
267
  # Get the list of all the currently running processes.
268
- get_process_list = GetProcessList(get_method='pywin32', connect_on_init=True)
268
+ get_process_list = process_poller.GetProcessList(get_method='pywin32', connect_on_init=True)
269
269
  processes = get_process_list.get_processes(as_dict=False)
270
270
 
271
271
  # Iterate through all the current process, while fetching executable file 'name' and the command line.
@@ -5,8 +5,10 @@ from typing import Literal, Union
5
5
 
6
6
  from .wrappers.pywin32w import wmi_win32process
7
7
  from .wrappers.psutilw import psutilw
8
+ from .etws.traces import trace_sysmon_process_creation
8
9
  from .basics import list_of_dicts, dicts
9
10
  from .process_name_cmd import ProcessNameCmdline
11
+ from .print_api import print_api
10
12
 
11
13
 
12
14
  def get_process_time_tester(
@@ -40,16 +42,24 @@ test = get_process_list.get_processes()
40
42
 
41
43
 
42
44
  class GetProcessList:
45
+ """
46
+ The class is responsible for getting the list of running processes.
47
+
48
+ Example of one time polling with 'pywin32' method:
49
+ from atomicshop import process_poller
50
+ process_list: dict = \
51
+ process_poller.GetProcessList(get_method='pywin32', connect_on_init=True).get_processes(as_dict=True)
52
+ """
43
53
  def __init__(
44
54
  self,
45
- get_method: Literal['psutil', 'pywin32', 'process_dll'] = 'process_dll',
55
+ get_method: Literal['psutil', 'pywin32', 'process_dll', 'sysmon_etw'] = 'process_dll',
46
56
  connect_on_init: bool = False
47
57
  ):
48
58
  """
49
59
  :param get_method: str, The method to get the list of processes. Default is 'process_list_dll'.
50
60
  'psutil': Get the list of processes by 'psutil' library. Resource intensive and slow.
51
61
  'pywin32': Get the list of processes by 'pywin32' library, using WMI. Not resource intensive, but slow.
52
- 'process_dll'. Not resource intensive and fast. Probably works only in Windows 10 x64
62
+ 'process_dll'. Not resource intensive and fast. Probably works only in Windows 10 x64.
53
63
  :param connect_on_init: bool, if True, will connect to the service on init. 'psutil' don't need to connect.
54
64
  """
55
65
  self.get_method = get_method
@@ -86,7 +96,7 @@ class GetProcessList:
86
96
  """
87
97
  The function will get the list of opened processes and return it as a list of dicts.
88
98
 
89
- :return: list of dicts, of opened processes.
99
+ :return: dict while key is pid or list of dicts, of opened processes (depending on 'as_dict' setting).
90
100
  """
91
101
 
92
102
  if as_dict:
@@ -132,19 +142,14 @@ class ProcessPollerPool:
132
142
  Later, I'll find a solution to make it more efficient.
133
143
  """
134
144
  def __init__(
135
- self, store_cycles: int = 200,
145
+ self,
136
146
  interval_seconds: Union[int, float] = 0,
137
147
  operation: Literal['thread', 'process'] = 'thread',
138
- poller_method: Literal['psutil', 'pywin32', 'process_dll'] = 'process_dll',
148
+ poller_method: Literal['psutil', 'pywin32', 'process_dll', 'sysmon_etw'] = 'sysmon_etw',
149
+ sysmon_etw_session_name: str = None,
150
+ sysmon_directory: str = None
139
151
  ):
140
152
  """
141
- :param store_cycles: int, how many cycles to store. Each cycle is polling processes.
142
- Example: Specifying 3 will store last 3 polled cycles of processes.
143
-
144
- Default is 200, which means that 200 latest cycles original PIDs and their process names will be stored.
145
-
146
- You can execute the 'get_process_time_tester' function in order to find the optimal number of cycles
147
- and how much time it will take.
148
153
  :param interval_seconds: float, how many seconds to wait between each cycle.
149
154
  Default is 0, which means that the polling will be as fast as possible.
150
155
 
@@ -162,6 +167,18 @@ class ProcessPollerPool:
162
167
  'psutil': Get the list of processes by 'psutil' library. Resource intensive and slow.
163
168
  'pywin32': Get the list of processes by 'pywin32' library, using WMI. Not resource intensive, but slow.
164
169
  'process_dll'. Not resource intensive and fast. Probably works only in Windows 10 x64.
170
+ 'sysmon_etw': Get the list of processes with running SysMon by ETW - Event Tracing for Windows.
171
+ In this case 'store_cycles' and 'interval_seconds' are irrelevant, since the ETW is real-time.
172
+ Steps we take:
173
+ 1. Check if SysMon is Running. If not, check if the executable exists in specified
174
+ location and start it as a service.
175
+ 2. Start the "Microsoft-Windows-Sysmon" ETW session.
176
+ 3. Take a snapshot of current processes and their CMDs with psutil and store it in a dict.
177
+ 4. Each new process creation from ETW updates the dict.
178
+ :param sysmon_etw_session_name: str, only for 'sysmon_etw' get_method.
179
+ The name of the ETW session for tracing process creation.
180
+ :param sysmon_directory: str, only for 'sysmon_etw' get_method.
181
+ The directory where the SysMon executable is located. If non-existed will be downloaded.
165
182
  ---------------------------------------------
166
183
  If there is an exception, ProcessPollerPool.processes will be set to the exception.
167
184
  While getting the processes you can use this to execute the exception:
@@ -172,20 +189,20 @@ class ProcessPollerPool:
172
189
  raise processes
173
190
  """
174
191
 
175
- self.store_cycles: int = store_cycles
176
192
  self.interval_seconds: float = interval_seconds
177
193
  self.operation: str = operation
178
194
  self.poller_method = poller_method
179
-
180
- self.get_processes_list = GetProcessList(get_method=self.poller_method)
195
+ self.sysmon_etw_session_name: str = sysmon_etw_session_name
196
+ self.sysmon_directory: str = sysmon_directory
181
197
 
182
198
  # Current process pool.
183
- self.processes: dict = dict()
199
+ self._processes: dict = dict()
184
200
 
185
201
  # The variable is responsible to stop the thread if it is running.
186
- self.running: bool = False
202
+ self._running: bool = False
187
203
 
188
- self.queue = multiprocessing.Queue()
204
+ self._process_queue = multiprocessing.Queue()
205
+ self._running_state_queue = multiprocessing.Queue()
189
206
 
190
207
  def start(self):
191
208
  if self.operation == 'thread':
@@ -195,66 +212,116 @@ class ProcessPollerPool:
195
212
  else:
196
213
  raise ValueError(f'Invalid operation type [{self.operation}]')
197
214
 
198
- def stop(self):
199
- self.running = False
200
-
201
- def _start_thread(self):
202
- self.running = True
203
- # threading.Thread(target=self._worker, args=(self.process_polling_instance,)).start()
204
- thread = threading.Thread(target=self._worker)
215
+ thread = threading.Thread(target=self._thread_get_queue)
205
216
  thread.daemon = True
206
217
  thread.start()
207
218
 
208
- def _start_process(self):
209
- self.running = True
210
- multiprocessing.Process(target=self._worker).start()
219
+ def stop(self):
220
+ self._running = False
221
+ self._running_state_queue.put(False)
211
222
 
212
- thread = threading.Thread(target=self._thread_get_queue)
223
+ def get_processes(self):
224
+ return self._processes
225
+
226
+ def _start_thread(self):
227
+ self._running = True
228
+
229
+ thread = threading.Thread(
230
+ target=_worker, args=(
231
+ self.poller_method, self._running_state_queue, self.interval_seconds,
232
+ self._process_queue, self.sysmon_etw_session_name, self.sysmon_directory,
233
+ )
234
+ )
213
235
  thread.daemon = True
214
236
  thread.start()
215
237
 
216
- def _worker(self):
217
- # We must initiate the connection inside the thread/process, because it is not thread-safe.
218
- self.get_processes_list.connect()
219
-
220
- exception = None
221
- list_of_processes: list = list()
222
- while self.running:
223
- try:
224
- # If the list is full (to specified 'store_cycles'), remove the first element.
225
- if len(list_of_processes) == self.store_cycles:
226
- del list_of_processes[0]
227
-
228
- # Get the current processes and reinitialize the instance of the dict.
229
- current_processes: dict = dict(self.get_processes_list.get_processes())
238
+ def _start_process(self):
239
+ self._running = True
240
+ multiprocessing.Process(
241
+ target=_worker, args=(
242
+ self.poller_method, self._running_state_queue, self.interval_seconds,
243
+ self._process_queue, self.sysmon_etw_session_name, self.sysmon_directory,
244
+ )).start()
230
245
 
231
- # Remove Command lines that contains only numbers, since they are useless.
232
- for pid, process_info in current_processes.items():
233
- if process_info['cmdline'].isnumeric():
234
- current_processes[pid]['cmdline'] = str()
235
- elif process_info['cmdline'] == 'Error':
236
- current_processes[pid]['cmdline'] = str()
246
+ def _thread_get_queue(self):
247
+ while True:
248
+ self._processes = self._process_queue.get()
237
249
 
238
- # Append the current processes to the list.
239
- list_of_processes.append(current_processes)
240
250
 
241
- # Merge all dicts in the list to one dict, updating with most recent PIDs.
242
- self.processes = list_of_dicts.merge_to_dict(list_of_processes)
251
+ def _worker(
252
+ poller_method, running_state_queue, interval_seconds, process_queue, sysmon_etw_session_name, sysmon_directory):
253
+ def _worker_to_get_running_state():
254
+ nonlocal running_state
255
+ running_state = running_state_queue.get()
243
256
 
244
- if self.operation == 'process':
245
- self.queue.put(self.processes)
257
+ running_state: bool = True
246
258
 
247
- time.sleep(self.interval_seconds)
248
- except KeyboardInterrupt as e:
249
- self.running = False
250
- exception = e
251
- except Exception as e:
252
- self.running = False
253
- exception = e
259
+ thread = threading.Thread(target=_worker_to_get_running_state)
260
+ thread.daemon = True
261
+ thread.start()
254
262
 
255
- if not self.running:
256
- self.queue.put(exception)
263
+ if poller_method == 'sysmon_etw':
264
+ poller_instance = trace_sysmon_process_creation.SysmonProcessCreationTrace(
265
+ attrs=['pid', 'original_file_name', 'command_line'],
266
+ session_name=sysmon_etw_session_name,
267
+ close_existing_session_name=True,
268
+ sysmon_directory=sysmon_directory
269
+ )
257
270
 
258
- def _thread_get_queue(self):
259
- while True:
260
- self.processes = self.queue.get()
271
+ # We must initiate the connection inside the thread/process, because it is not thread-safe.
272
+ poller_instance.start()
273
+
274
+ processes = GetProcessList(get_method='pywin32', connect_on_init=True).get_processes(as_dict=True)
275
+ process_queue.put(processes)
276
+ else:
277
+ poller_instance = GetProcessList(get_method=poller_method)
278
+ poller_instance.connect()
279
+ processes = {}
280
+
281
+ exception = None
282
+ list_of_processes: list = list()
283
+ while running_state:
284
+ try:
285
+ if poller_method == 'sysmon_etw':
286
+ # Get the current processes and reinitialize the instance of the dict.
287
+ current_cycle: dict = poller_instance.emit()
288
+ current_processes: dict = {int(current_cycle['pid']): {
289
+ 'name': current_cycle['original_file_name'],
290
+ 'cmdline': current_cycle['command_line']}
291
+ }
292
+ else:
293
+ # Get the current processes and reinitialize the instance of the dict.
294
+ current_processes: dict = dict(poller_instance.get_processes())
295
+
296
+ # Remove Command lines that contains only numbers, since they are useless.
297
+ for pid, process_info in current_processes.items():
298
+ if process_info['cmdline'].isnumeric():
299
+ current_processes[pid]['cmdline'] = str()
300
+ elif process_info['cmdline'] == 'Error':
301
+ current_processes[pid]['cmdline'] = str()
302
+
303
+ # This loop is essential for keeping the command lines.
304
+ # When the process unloads from memory, the last polling will have only pid and executable name, but not
305
+ # the command line. This loop will keep the command line from the previous polling if this happens.
306
+ for pid, process_info in current_processes.items():
307
+ if pid in processes:
308
+ if processes[pid]['name'] == current_processes[pid]['name']:
309
+ if current_processes[pid]['cmdline'] == '':
310
+ current_processes[pid]['cmdline'] = processes[pid]['cmdline']
311
+ processes.update(current_processes)
312
+
313
+ process_queue.put(processes)
314
+
315
+ # Since ETW is a blocking operation, we don't need to sleep.
316
+ if poller_method != 'sysmon_etw':
317
+ time.sleep(interval_seconds)
318
+ except KeyboardInterrupt as e:
319
+ running_state = False
320
+ exception = e
321
+ except Exception as e:
322
+ running_state = False
323
+ exception = e
324
+ print_api(f'Exception in ProcessPollerPool: {e}', color='red')
325
+
326
+ if not running_state:
327
+ process_queue.put(exception)
@@ -164,6 +164,15 @@ def filter_processes_with_present_connections(processes) -> list:
164
164
 
165
165
 
166
166
  class PsutilProcesses:
167
+ """
168
+ Class to get all the current processes.
169
+
170
+ Example get current running processes as dicts as
171
+ {'<pid'>: {'name': '<process_name>', 'cmdline': '<process_cmdline>'}}:
172
+ from atomicshop.wrappers.psutilw import psutilw
173
+ processes = psutilw.PsutilProcesses().get_processes_as_dict(
174
+ attrs=['pid', 'name', 'cmdline'], cmdline_to_string=True)
175
+ """
167
176
  def __init__(self):
168
177
  self.processes = None
169
178
 
@@ -0,0 +1,157 @@
1
+ import os
2
+ import subprocess
3
+
4
+ from .. import filesystem, web, process
5
+
6
+
7
+ # Define paths and configuration
8
+ DEFAULT_INSTALLATION_PATH: str = 'C:\\Sysmon'
9
+ SYSMON_FILE_NAME: str = 'Sysmon.exe'
10
+ SYSINTERNALS_SYSMON_URL: str = 'https://download.sysinternals.com/files/Sysmon.zip'
11
+ SYSMON_CONFIG_FILE_NAME: str = 'sysmonconfig.xml'
12
+ SYSMON_CONFIG_FILE_PATH: str = os.path.join(DEFAULT_INSTALLATION_PATH, SYSMON_CONFIG_FILE_NAME)
13
+
14
+
15
+ class ConfigFileNotFoundError(Exception):
16
+ pass
17
+
18
+
19
+ class SymonExecutableNotFoundError(Exception):
20
+ pass
21
+
22
+
23
+ class SysmonAlreadyRunningError(Exception):
24
+ pass
25
+
26
+
27
+ def download_sysmon(installation_path: str = None):
28
+ """
29
+ Install Sysmon on the system.
30
+
31
+ :param installation_path: string, full path where to put the Sysmon executable.
32
+ """
33
+
34
+ if not installation_path:
35
+ installation_path = DEFAULT_INSTALLATION_PATH
36
+
37
+ # Check if the file exists
38
+ if not os.path.exists(installation_path):
39
+ filesystem.create_directory(installation_path)
40
+
41
+ web.download_and_extract_file(SYSINTERNALS_SYSMON_URL, installation_path)
42
+
43
+
44
+ def is_sysmon_running():
45
+ """
46
+ Check if Sysmon is running.
47
+
48
+ :return: boolean, True if Sysmon is running, False otherwise.
49
+ """
50
+
51
+ process_list: list = process.match_pattern_against_running_processes_cmdlines(
52
+ pattern=SYSMON_FILE_NAME, first=True, process_name_case_insensitive=True)
53
+
54
+ if process_list:
55
+ return True
56
+ else:
57
+ return False
58
+
59
+
60
+ def start_as_service(
61
+ installation_path: str = None,
62
+ config_file_path: str = None,
63
+ use_config_in_same_directory: bool = False,
64
+ download_sysmon_if_not_found: bool = False,
65
+ skip_if_running: bool = False
66
+ ):
67
+ """
68
+ Start Sysmon as a service. Besides starting, it installs itself as a service, meaning that on the next boot,
69
+ it will start automatically.
70
+
71
+ :param installation_path: string, full path where to put the Sysmon executable.
72
+ :param config_file_path: string, full path to the configuration file.
73
+ :param use_config_in_same_directory: boolean, if True, the function will use the configuration file in the same
74
+ directory as the Sysmon executable.
75
+ :param download_sysmon_if_not_found: boolean, if True, the function will download Sysmon if it is not
76
+ found in the 'installation_path'.
77
+ :param skip_if_running: boolean,
78
+ True, the function will not start Sysmon if it is already running.
79
+ False, the function will raise 'SysmonAlreadyRunningError' exception if it is already running.
80
+ """
81
+
82
+ # Check if sysmon already running.
83
+ if is_sysmon_running():
84
+ if skip_if_running:
85
+ return
86
+ else:
87
+ raise SysmonAlreadyRunningError("Sysmon is already running.")
88
+
89
+ if config_file_path and use_config_in_same_directory:
90
+ raise ValueError("You cannot use both 'config_file_path' and 'use_config_in_same_directory'.")
91
+
92
+ if use_config_in_same_directory:
93
+ config_file_path = SYSMON_CONFIG_FILE_PATH
94
+
95
+ if config_file_path:
96
+ # Check if the file exists
97
+ if not os.path.exists(config_file_path):
98
+ raise ConfigFileNotFoundError(f"Configuration file '{config_file_path}' not found.")
99
+
100
+ if not installation_path:
101
+ installation_path = DEFAULT_INSTALLATION_PATH
102
+
103
+ sysmon_file_path: str = os.path.join(installation_path, SYSMON_FILE_NAME)
104
+
105
+ # Check if the file exists
106
+ if not os.path.exists(sysmon_file_path):
107
+ if download_sysmon_if_not_found:
108
+ download_sysmon(installation_path)
109
+ else:
110
+ raise SymonExecutableNotFoundError(f"Sysmon executable '{sysmon_file_path}' not found.")
111
+
112
+ # Start Sysmon as a service.
113
+ if config_file_path:
114
+ subprocess.run([sysmon_file_path, '-accepteula', '-i', config_file_path])
115
+ else:
116
+ subprocess.run([sysmon_file_path, '-accepteula', '-i'])
117
+
118
+
119
+ def stop_service(installation_path: str = None):
120
+ """
121
+ Stop Sysmon service.
122
+
123
+ :param installation_path: string, full path where to put the Sysmon executable.
124
+ """
125
+
126
+ if not installation_path:
127
+ installation_path = DEFAULT_INSTALLATION_PATH
128
+
129
+ sysmon_file_path: str = os.path.join(installation_path, SYSMON_FILE_NAME)
130
+
131
+ # Check if the file exists
132
+ if not os.path.exists(sysmon_file_path):
133
+ raise SymonExecutableNotFoundError(f"Sysmon executable '{sysmon_file_path}' not found.")
134
+
135
+ # Stop Sysmon service.
136
+ subprocess.run([sysmon_file_path, '-u'])
137
+
138
+
139
+ def change_config_on_the_fly(config_file_path: str, installation_path: str = None):
140
+ """
141
+ Change the Sysmon configuration on the fly.
142
+
143
+ :param config_file_path: string, full path to the configuration file.
144
+ :param installation_path: string, full path where to put the Sysmon executable.
145
+ """
146
+
147
+ if not installation_path:
148
+ installation_path = DEFAULT_INSTALLATION_PATH
149
+
150
+ sysmon_file_path: str = os.path.join(installation_path, SYSMON_FILE_NAME)
151
+
152
+ # Check if the file exists
153
+ if not os.path.exists(sysmon_file_path):
154
+ raise SymonExecutableNotFoundError(f"Sysmon executable '{sysmon_file_path}' not found.")
155
+
156
+ # Change the configuration on the fly.
157
+ subprocess.run([sysmon_file_path, '-c', config_file_path])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.12.26
3
+ Version: 2.13.1
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=Wq0bf4jFBHfU0lsKuJb7kZ5yruxZGg3_luyY2rWwPss,124
1
+ atomicshop/__init__.py,sha256=aqwPLStJUJ8YnGq3I420Yccvy8q9JIXVsivcavrcOtM,123
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,8 +9,8 @@ atomicshop/config_init.py,sha256=z2RXD_mw9nQlAOpuGry1h9QT-2LhNscXgGAktN3dCVQ,249
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=olsL01S5tkXk4WPzucxujqgLOh198BLgJntDnGYukRU,15533
12
- atomicshop/diff_check.py,sha256=0H4OeRxJodFIubpAoy2dFY9hixzLbkJ8NNlH6K3Xdus,27033
13
- atomicshop/dns.py,sha256=bNZOo5jVPzq7OT2qCPukXoK3zb1oOsyaelUwQEyK1SA,2500
12
+ atomicshop/diff_check.py,sha256=RJvzJhyYAZyRPKVDk1dS7UwZCx0kq__WDZ6N0rNfZUY,27110
13
+ atomicshop/dns.py,sha256=h4uZKoz4wbBlLOOduL1GtRcTm-YpiPnGOEGxUm7hhOI,2140
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
@@ -25,9 +25,9 @@ atomicshop/on_exit.py,sha256=Wf1iy2e0b0Zu7oRxrct3VkLdQ_x9B32-z_JerKTt9Z0,5493
25
25
  atomicshop/pbtkmultifile_argparse.py,sha256=aEk8nhvoQVu-xyfZosK3ma17CwIgOjzO1erXXdjwtS4,4574
26
26
  atomicshop/permissions.py,sha256=P6tiUKV-Gw-c3ePEVsst9bqWaHJbB4ZlJB4xbDYVpEs,4436
27
27
  atomicshop/print_api.py,sha256=DhbCQd0MWZZ5GYEk4oTu1opRFC-b31g1VWZgTGewG2Y,11568
28
- atomicshop/process.py,sha256=Zgb4CUjy9gIBaawvtCOEcxGUCqvqPyARk0lpBjRzxWE,15950
28
+ atomicshop/process.py,sha256=R1BtXWjG2g2Q3WlsyhbIlXZz0UkQeagY7fQyBOIX_DM,15951
29
29
  atomicshop/process_name_cmd.py,sha256=CtaSp3mgxxJKCCVW8BLx6BJNx4giCklU_T7USiCEwfc,5162
30
- atomicshop/process_poller.py,sha256=naqoq-gJN1kkaOm_MfoM7teIkh5OniXromVR4pb0pjY,11680
30
+ atomicshop/process_poller.py,sha256=sEuuFu3gmUrp4tY1LVxE27L0eK5LIwNKjTbV7KtzeRM,15029
31
31
  atomicshop/python_file_patcher.py,sha256=kd3rBWvTcosLEk-7TycNdfKW9fZbe161iVwmH4niUo0,5515
32
32
  atomicshop/python_functions.py,sha256=zJg4ogUwECxrDD7xdDN5JikIUctITM5lsyabr_ZNsRw,4435
33
33
  atomicshop/question_answer_engine.py,sha256=DuOn7QEgKKfqZu2cR8mVeFIfFgayfBHiW-jY2VPq_Fo,841
@@ -83,7 +83,7 @@ atomicshop/basics/argparse_template.py,sha256=horwgSf3MX1ZgRnYxtmmQuz9OU_vKrKggF
83
83
  atomicshop/basics/booleans.py,sha256=va3LYIaSOhjdifW4ZEesnIQxBICNHyQjUAkYelzchhE,2047
84
84
  atomicshop/basics/bytes_arrays.py,sha256=WvSRDhIGt1ywF95t-yNgpxLm1nlZUbM1Dz6QckcyE8Y,5915
85
85
  atomicshop/basics/classes.py,sha256=EijW_g4EhdNBnKPMG3nT3HjFspTchtM7to6zm9Ad_Mk,9771
86
- atomicshop/basics/dicts.py,sha256=JevEkLsQdH4Tqn8rspSey40jW6h8pJ2SP5fcuFBR4dU,13651
86
+ atomicshop/basics/dicts.py,sha256=DeYHIh940pMMBrFhpXt4dsigFVYzTrlqWymNo4Pq_Js,14049
87
87
  atomicshop/basics/dicts_nested.py,sha256=StYxYnYPa0SEJr1lmEwAv5zfERWWqoULeyG8e0zRAwE,4107
88
88
  atomicshop/basics/enumerations.py,sha256=41VVQYh_vnVapggxKg2IRU5e_EiMpZzX1n1mtxvoSzM,1364
89
89
  atomicshop/basics/enums.py,sha256=CeV8MfqWHihK7vvV6Mbzq7rD9JykeQfrJeFdLVzfHI4,3547
@@ -96,16 +96,20 @@ atomicshop/basics/list_of_dicts.py,sha256=qI2uoYIcHjR8RSD5vtkqhpMgL6XTYRGJDcr9cb
96
96
  atomicshop/basics/lists.py,sha256=I0C62vrDrNwCTNl0EjUZNa1Jsd8l0rTkp28GEx9QoEI,4258
97
97
  atomicshop/basics/multiprocesses.py,sha256=nSskxJSlEdalPM_Uf8cc9kAYYlVwYM1GonBLAhCL2mM,18831
98
98
  atomicshop/basics/numbers.py,sha256=ESX0z_7o_ok3sOmCKAUBoZinATklgMy2v-4RndqXlVM,1837
99
+ atomicshop/basics/package_module.py,sha256=fBd0uVgFce25ZCVtLq83iyowRlbwdWYFj_t4Ml7LU14,391
99
100
  atomicshop/basics/randoms.py,sha256=DmYLtnIhDK29tAQrGP1Nt-A-v8WC7WIEB8Edi-nk3N4,282
100
101
  atomicshop/basics/strings.py,sha256=T4MpEpwxqsiOSnXcwYkqMKB5okHiJfvUCO7t5kcRtBg,18316
101
102
  atomicshop/basics/threads.py,sha256=xvgdDJdmgN0wmmARoZ-H7Kvl1GOcEbvgaeGL4M3Hcx8,2819
102
103
  atomicshop/basics/timeit_template.py,sha256=fYLrk-X_dhdVtnPU22tarrhhvlggeW6FdKCXM8zkX68,405
103
104
  atomicshop/basics/tracebacks.py,sha256=cNfh_oAwF55kSIdqtv3boHZQIoQI8TajxkTnwJwpweI,535
104
- atomicshop/etw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
- atomicshop/etw/providers.py,sha256=fVmWi-uGdtnsQTDpu_ty6dzx0GMhGokiST73LNBEJ38,129
106
- atomicshop/etw/sessions.py,sha256=k3miewU278xn829cqDbsuH_bmZHPQE9-Zn-hINbxUSE,1330
107
- atomicshop/etw/trace.py,sha256=tjBvV4RxCSn8Aoxj8NVxmqHekdECne_y4Zv2lbh11ak,5341
108
- atomicshop/etw/trace_dns.py,sha256=f4homrWp4qMrmjC9UrEWjr9p9MrUg7SwOVbE22_IYgw,6728
105
+ atomicshop/etws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
+ atomicshop/etws/const.py,sha256=v3x_IdCYeSKbCGywiZFOZln80ldpwKW5nuMDuUe51Jg,1257
107
+ atomicshop/etws/providers.py,sha256=fVmWi-uGdtnsQTDpu_ty6dzx0GMhGokiST73LNBEJ38,129
108
+ atomicshop/etws/sessions.py,sha256=k3miewU278xn829cqDbsuH_bmZHPQE9-Zn-hINbxUSE,1330
109
+ atomicshop/etws/trace.py,sha256=tjBvV4RxCSn8Aoxj8NVxmqHekdECne_y4Zv2lbh11ak,5341
110
+ atomicshop/etws/traces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
+ atomicshop/etws/traces/trace_dns.py,sha256=wRFwfFgVgMbYJu1Sf0Yy9LPoMrf9kXejl2Xjk3PNELQ,8430
112
+ atomicshop/etws/traces/trace_sysmon_process_creation.py,sha256=pjb0qzNZttucEWOKv2j_BGVqaSeKjBL4O8oCsAteiGU,4785
109
113
  atomicshop/file_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
114
  atomicshop/file_io/csvs.py,sha256=y8cJtnlN-NNxNupzJgSeGq9aQ4wNxYLFPX9vNNlUiIc,5830
111
115
  atomicshop/file_io/docxs.py,sha256=6tcYFGp0vRsHR47VwcRqwhdt2DQOwrAUYhrwN996n9U,5117
@@ -135,9 +139,9 @@ atomicshop/mitm/engines/__reference_general/parser___reference_general.py,sha256
135
139
  atomicshop/mitm/engines/__reference_general/recorder___reference_general.py,sha256=KENDVf9OwXD9gwSh4B1XxACCe7iHYjrvnW1t6F64wdE,695
136
140
  atomicshop/mitm/engines/__reference_general/responder___reference_general.py,sha256=1AM49UaFTKA0AHw-k3SV3uH3QbG-o6ux0c-GoWkKNU0,6993
137
141
  atomicshop/monitor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
- atomicshop/monitor/change_monitor.py,sha256=dGhk5bJPxLCHa2FOVkort99E7vjVojra9GlvhpcKSqE,7551
142
+ atomicshop/monitor/change_monitor.py,sha256=eJRP7NBnA-Y_n6Ofm_jKrR5XguJV6gEmxFdtdGMBF3k,7866
139
143
  atomicshop/monitor/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
- atomicshop/monitor/checks/dns.py,sha256=YLCFoolq35dnzdnBnfUA2ag-4HfvzcHfRdm3mzX8R8o,7184
144
+ atomicshop/monitor/checks/dns.py,sha256=_DxInUSiiMa8Bx1sYJ6fRzRl7n_eAsoaGHhUk9XgF7w,7182
141
145
  atomicshop/monitor/checks/file.py,sha256=2tIDSlX2KZNc_9i9ji1tcOqupbFTIOj7cKXLyBEDWMk,3263
142
146
  atomicshop/monitor/checks/network.py,sha256=CGZWl4WlQrxayZeVF9JspJXwYA-zWx8ECWTVGSlXc98,3825
143
147
  atomicshop/monitor/checks/process_running.py,sha256=x66wd6-l466r8sbRQaIli0yswyGt1dH2DVXkGDL6O0Q,1891
@@ -163,6 +167,7 @@ atomicshop/wrappers/pipw.py,sha256=mu4jnHkSaYNfpBiLZKMZxEX_E2LqW5BVthMZkblPB_c,1
163
167
  atomicshop/wrappers/process_wrapper_pbtk.py,sha256=ycPmBRnv627RWks6N8OhxJQe8Gu3h3Vwj-4HswPOw0k,599
164
168
  atomicshop/wrappers/pycharmw.py,sha256=OHcaVlyhIqhgRioPhkeS5krDZ_NezZjpBCvyRiLjWwI,2723
165
169
  atomicshop/wrappers/pyopensslw.py,sha256=OBWxA6EJ2vU_Qlf4M8m6ilcG3hyYB4yB0EsXUf7NhEU,6804
170
+ atomicshop/wrappers/sysmonw.py,sha256=impveU8MhbD2oizBoGLuj00wuyB8l_EiRRGIESdFESs,5359
166
171
  atomicshop/wrappers/ubuntu_terminal.py,sha256=BBZD3EH6KSDORd5IZBZM-ti4U6Qh1sZwftx42s7hqB4,10917
167
172
  atomicshop/wrappers/wslw.py,sha256=AKphiHLSddL7ErevUowr3f9Y1AgGz_R3KZ3NssW07h8,6959
168
173
  atomicshop/wrappers/certauthw/certauth.py,sha256=hKedW0DOWlEigSNm8wu4SqHkCQsGJ1tJfH7s4nr3Bk0,12223
@@ -239,7 +244,7 @@ atomicshop/wrappers/playwrightw/waits.py,sha256=308fdOu6YDqQ7K7xywj7R27sSmFanPBQ
239
244
  atomicshop/wrappers/psutilw/cpus.py,sha256=w6LPBMINqS-T_X8vzdYkLS2Wzuve28Ydp_GafTCngrc,236
240
245
  atomicshop/wrappers/psutilw/disks.py,sha256=3ZSVoommKH1TWo37j_83frB-NqXF4Nf5q5mBCX8G4jE,9221
241
246
  atomicshop/wrappers/psutilw/memories.py,sha256=_S0aL8iaoIHebd1vOFrY_T9aROM5Jx2D5CvDh_4j0Vc,528
242
- atomicshop/wrappers/psutilw/psutilw.py,sha256=G22ZQfGnqX15-feD8KUXfEZO4pFkIEnB8zgPzJ2jc7M,20868
247
+ atomicshop/wrappers/psutilw/psutilw.py,sha256=q3EwgprqyrR4zLCjl4l5DHFOQoukEvQMIPjNB504oQ0,21262
243
248
  atomicshop/wrappers/psycopgw/psycopgw.py,sha256=XJvVf0oAUjCHkrYfKeFuGCpfn0Oxj3u4SbKMKA1508E,7118
244
249
  atomicshop/wrappers/pywin32w/console.py,sha256=LstHajPLgXp9qQxFNR44QfH10nOnNp3bCJquxaTquns,1175
245
250
  atomicshop/wrappers/pywin32w/winshell.py,sha256=i2bKiMldPU7_azsD5xGQDdMwjaM7suKJd3k0Szmcs6c,723
@@ -260,8 +265,8 @@ atomicshop/wrappers/socketw/socket_server_tester.py,sha256=AhpurHJmP2kgzHaUbq5ey
260
265
  atomicshop/wrappers/socketw/socket_wrapper.py,sha256=aXBwlEIJhFT0-c4i8iNlFx2It9VpCEpsv--5Oqcpxao,11624
261
266
  atomicshop/wrappers/socketw/ssl_base.py,sha256=k4V3gwkbq10MvOH4btU4onLX2GNOsSfUAdcHmL1rpVE,2274
262
267
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=t3dtDEfN47CfYVi0CW6Kc2QHTEeZVyYhc57IYYh5nmA,826
263
- atomicshop-2.12.26.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
264
- atomicshop-2.12.26.dist-info/METADATA,sha256=HYm5lvYyf77XdD_MWANy8aWX2kpFD7kD-EmOZg19yOQ,10479
265
- atomicshop-2.12.26.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
266
- atomicshop-2.12.26.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
267
- atomicshop-2.12.26.dist-info/RECORD,,
268
+ atomicshop-2.13.1.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
269
+ atomicshop-2.13.1.dist-info/METADATA,sha256=yOVvRROyXl4BM9Qr9EzI5BTZgvEo0OG3-uCo9Ryni2Y,10478
270
+ atomicshop-2.13.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
271
+ atomicshop-2.13.1.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
272
+ atomicshop-2.13.1.dist-info/RECORD,,
File without changes
File without changes
File without changes
File without changes