atomicshop 2.12.25__py3-none-any.whl → 2.13.0__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.25'
4
+ __version__ = '2.13.00'
@@ -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.