atomicshop 2.15.11__py3-none-any.whl → 3.10.5__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.
- atomicshop/__init__.py +1 -1
- atomicshop/{addons/mains → a_mains}/FACT/update_extract.py +3 -2
- atomicshop/a_mains/dns_gateway_setting.py +11 -0
- atomicshop/a_mains/get_local_tcp_ports.py +85 -0
- atomicshop/a_mains/github_wrapper.py +11 -0
- atomicshop/a_mains/install_ca_certificate.py +172 -0
- atomicshop/a_mains/process_from_port.py +119 -0
- atomicshop/a_mains/set_default_dns_gateway.py +90 -0
- atomicshop/a_mains/update_config_toml.py +38 -0
- atomicshop/basics/ansi_escape_codes.py +3 -1
- atomicshop/basics/argparse_template.py +2 -0
- atomicshop/basics/booleans.py +27 -30
- atomicshop/basics/bytes_arrays.py +43 -0
- atomicshop/basics/classes.py +149 -1
- atomicshop/basics/enums.py +2 -2
- atomicshop/basics/exceptions.py +5 -1
- atomicshop/basics/list_of_classes.py +29 -0
- atomicshop/basics/multiprocesses.py +374 -50
- atomicshop/basics/strings.py +72 -3
- atomicshop/basics/threads.py +14 -0
- atomicshop/basics/tracebacks.py +13 -3
- atomicshop/certificates.py +153 -52
- atomicshop/config_init.py +11 -6
- atomicshop/console_user_response.py +7 -14
- atomicshop/consoles.py +9 -0
- atomicshop/datetimes.py +1 -1
- atomicshop/diff_check.py +3 -3
- atomicshop/dns.py +128 -3
- atomicshop/etws/_pywintrace_fix.py +17 -0
- atomicshop/etws/trace.py +40 -42
- atomicshop/etws/traces/trace_dns.py +56 -44
- atomicshop/etws/traces/trace_tcp.py +130 -0
- atomicshop/file_io/csvs.py +27 -5
- atomicshop/file_io/docxs.py +34 -17
- atomicshop/file_io/file_io.py +31 -17
- atomicshop/file_io/jsons.py +49 -0
- atomicshop/file_io/tomls.py +139 -0
- atomicshop/filesystem.py +616 -291
- atomicshop/get_process_list.py +3 -3
- atomicshop/http_parse.py +149 -93
- atomicshop/ip_addresses.py +6 -1
- atomicshop/mitm/centered_settings.py +132 -0
- atomicshop/mitm/config_static.py +207 -0
- atomicshop/mitm/config_toml_editor.py +55 -0
- atomicshop/mitm/connection_thread_worker.py +875 -357
- atomicshop/mitm/engines/__parent/parser___parent.py +4 -17
- atomicshop/mitm/engines/__parent/recorder___parent.py +108 -51
- atomicshop/mitm/engines/__parent/requester___parent.py +116 -0
- atomicshop/mitm/engines/__parent/responder___parent.py +75 -114
- atomicshop/mitm/engines/__reference_general/parser___reference_general.py +10 -7
- atomicshop/mitm/engines/__reference_general/recorder___reference_general.py +5 -5
- atomicshop/mitm/engines/__reference_general/requester___reference_general.py +47 -0
- atomicshop/mitm/engines/__reference_general/responder___reference_general.py +95 -13
- atomicshop/mitm/engines/create_module_template.py +58 -14
- atomicshop/mitm/import_config.py +359 -139
- atomicshop/mitm/initialize_engines.py +160 -80
- atomicshop/mitm/message.py +64 -23
- atomicshop/mitm/mitm_main.py +892 -0
- atomicshop/mitm/recs_files.py +183 -0
- atomicshop/mitm/shared_functions.py +4 -10
- atomicshop/mitm/ssh_tester.py +82 -0
- atomicshop/mitm/statistic_analyzer.py +136 -40
- atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py +265 -83
- atomicshop/monitor/checks/dns.py +1 -1
- atomicshop/networks.py +671 -0
- atomicshop/on_exit.py +39 -9
- atomicshop/package_mains_processor.py +84 -0
- atomicshop/permissions/permissions.py +22 -0
- atomicshop/permissions/ubuntu_permissions.py +239 -0
- atomicshop/permissions/win_permissions.py +33 -0
- atomicshop/print_api.py +24 -42
- atomicshop/process.py +24 -6
- atomicshop/process_poller/process_pool.py +0 -1
- atomicshop/process_poller/simple_process_pool.py +204 -5
- atomicshop/python_file_patcher.py +1 -1
- atomicshop/python_functions.py +27 -75
- atomicshop/speech_recognize.py +8 -0
- atomicshop/ssh_remote.py +158 -172
- atomicshop/system_resource_monitor.py +61 -47
- atomicshop/system_resources.py +8 -8
- atomicshop/tempfiles.py +1 -2
- atomicshop/urls.py +6 -0
- atomicshop/venvs.py +28 -0
- atomicshop/versioning.py +27 -0
- atomicshop/web.py +98 -27
- atomicshop/web_apis/google_custom_search.py +44 -0
- atomicshop/web_apis/google_llm.py +188 -0
- atomicshop/websocket_parse.py +450 -0
- atomicshop/wrappers/certauthw/certauth.py +1 -0
- atomicshop/wrappers/cryptographyw.py +29 -8
- atomicshop/wrappers/ctyping/etw_winapi/const.py +97 -47
- atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py +178 -49
- atomicshop/wrappers/ctyping/file_details_winapi.py +67 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py +2 -1
- atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py +2 -2
- atomicshop/wrappers/ctyping/setup_device.py +466 -0
- atomicshop/wrappers/ctyping/win_console.py +39 -0
- atomicshop/wrappers/dockerw/dockerw.py +113 -2
- atomicshop/wrappers/elasticsearchw/config_basic.py +0 -12
- atomicshop/wrappers/elasticsearchw/elastic_infra.py +75 -0
- atomicshop/wrappers/elasticsearchw/elasticsearchw.py +2 -20
- atomicshop/wrappers/factw/get_file_data.py +12 -5
- atomicshop/wrappers/factw/install/install_after_restart.py +89 -5
- atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py +20 -14
- atomicshop/wrappers/githubw.py +537 -54
- atomicshop/wrappers/loggingw/consts.py +1 -1
- atomicshop/wrappers/loggingw/filters.py +23 -0
- atomicshop/wrappers/loggingw/formatters.py +12 -0
- atomicshop/wrappers/loggingw/handlers.py +214 -107
- atomicshop/wrappers/loggingw/loggers.py +19 -0
- atomicshop/wrappers/loggingw/loggingw.py +860 -22
- atomicshop/wrappers/loggingw/reading.py +134 -112
- atomicshop/wrappers/mongodbw/mongo_infra.py +31 -0
- atomicshop/wrappers/mongodbw/mongodbw.py +1324 -36
- atomicshop/wrappers/netshw.py +271 -0
- atomicshop/wrappers/playwrightw/engine.py +34 -19
- atomicshop/wrappers/playwrightw/infra.py +5 -0
- atomicshop/wrappers/playwrightw/javascript.py +7 -3
- atomicshop/wrappers/playwrightw/keyboard.py +14 -0
- atomicshop/wrappers/playwrightw/scenarios.py +172 -5
- atomicshop/wrappers/playwrightw/waits.py +9 -7
- atomicshop/wrappers/powershell_networking.py +80 -0
- atomicshop/wrappers/psutilw/processes.py +37 -1
- atomicshop/wrappers/psutilw/psutil_networks.py +85 -0
- atomicshop/wrappers/pyopensslw.py +9 -2
- atomicshop/wrappers/pywin32w/cert_store.py +116 -0
- atomicshop/wrappers/pywin32w/win_event_log/fetch.py +174 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_create.py +3 -105
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_terminate.py +3 -57
- atomicshop/wrappers/pywin32w/wmis/msft_netipaddress.py +113 -0
- atomicshop/wrappers/pywin32w/wmis/win32_networkadapterconfiguration.py +259 -0
- atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py +112 -0
- atomicshop/wrappers/pywin32w/wmis/wmi_helpers.py +236 -0
- atomicshop/wrappers/socketw/accepter.py +21 -7
- atomicshop/wrappers/socketw/certificator.py +216 -150
- atomicshop/wrappers/socketw/creator.py +190 -50
- atomicshop/wrappers/socketw/dns_server.py +491 -182
- atomicshop/wrappers/socketw/exception_wrapper.py +45 -52
- atomicshop/wrappers/socketw/process_getter.py +86 -0
- atomicshop/wrappers/socketw/receiver.py +144 -102
- atomicshop/wrappers/socketw/sender.py +65 -35
- atomicshop/wrappers/socketw/sni.py +334 -165
- atomicshop/wrappers/socketw/socket_base.py +134 -0
- atomicshop/wrappers/socketw/socket_client.py +137 -95
- atomicshop/wrappers/socketw/socket_server_tester.py +11 -7
- atomicshop/wrappers/socketw/socket_wrapper.py +717 -116
- atomicshop/wrappers/socketw/ssl_base.py +15 -14
- atomicshop/wrappers/socketw/statistics_csv.py +148 -17
- atomicshop/wrappers/sysmonw.py +1 -1
- atomicshop/wrappers/ubuntu_terminal.py +65 -26
- atomicshop/wrappers/win_auditw.py +189 -0
- atomicshop/wrappers/winregw/__init__.py +0 -0
- atomicshop/wrappers/winregw/winreg_installed_software.py +58 -0
- atomicshop/wrappers/winregw/winreg_network.py +232 -0
- {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/METADATA +31 -51
- atomicshop-3.10.5.dist-info/RECORD +306 -0
- {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/WHEEL +1 -1
- atomicshop/_basics_temp.py +0 -101
- atomicshop/a_installs/win/fibratus.py +0 -9
- atomicshop/a_installs/win/mongodb.py +0 -9
- atomicshop/a_installs/win/pycharm.py +0 -9
- atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh +0 -3
- atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd +0 -2
- atomicshop/addons/mains/__pycache__/install_fibratus_windows.cpython-312.pyc +0 -0
- atomicshop/addons/mains/__pycache__/msi_unpacker.cpython-312.pyc +0 -0
- atomicshop/addons/mains/install_docker_rootless_ubuntu.py +0 -11
- atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py +0 -11
- atomicshop/addons/mains/install_elastic_search_and_kibana_ubuntu.py +0 -10
- atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py +0 -9
- atomicshop/addons/package_setup/CreateWheel.cmd +0 -7
- atomicshop/addons/package_setup/Setup in Edit mode.cmd +0 -6
- atomicshop/addons/package_setup/Setup.cmd +0 -7
- atomicshop/archiver/_search_in_zip.py +0 -189
- atomicshop/archiver/archiver.py +0 -34
- atomicshop/archiver/search_in_archive.py +0 -250
- atomicshop/archiver/sevenz_app_w.py +0 -86
- atomicshop/archiver/sevenzs.py +0 -44
- atomicshop/archiver/zips.py +0 -293
- atomicshop/file_types.py +0 -24
- atomicshop/mitm/config_editor.py +0 -37
- atomicshop/mitm/engines/create_module_template_example.py +0 -13
- atomicshop/mitm/initialize_mitm_server.py +0 -268
- atomicshop/pbtkmultifile_argparse.py +0 -88
- atomicshop/permissions.py +0 -151
- atomicshop/script_as_string_processor.py +0 -38
- atomicshop/ssh_scripts/process_from_ipv4.py +0 -37
- atomicshop/ssh_scripts/process_from_port.py +0 -27
- atomicshop/wrappers/_process_wrapper_curl.py +0 -27
- atomicshop/wrappers/_process_wrapper_tar.py +0 -21
- atomicshop/wrappers/dockerw/install_docker.py +0 -209
- atomicshop/wrappers/elasticsearchw/infrastructure.py +0 -265
- atomicshop/wrappers/elasticsearchw/install_elastic.py +0 -232
- atomicshop/wrappers/ffmpegw.py +0 -125
- atomicshop/wrappers/fibratusw/install.py +0 -81
- atomicshop/wrappers/mongodbw/infrastructure.py +0 -53
- atomicshop/wrappers/mongodbw/install_mongodb.py +0 -190
- atomicshop/wrappers/msiw.py +0 -149
- atomicshop/wrappers/nodejsw/install_nodejs.py +0 -139
- atomicshop/wrappers/process_wrapper_pbtk.py +0 -16
- atomicshop/wrappers/psutilw/networks.py +0 -45
- atomicshop/wrappers/pycharmw.py +0 -81
- atomicshop/wrappers/socketw/base.py +0 -59
- atomicshop/wrappers/socketw/get_process.py +0 -107
- atomicshop/wrappers/wslw.py +0 -191
- atomicshop-2.15.11.dist-info/RECORD +0 -302
- /atomicshop/{addons/mains → a_mains}/FACT/factw_fact_extractor_docker_image_main_sudo.py +0 -0
- /atomicshop/{addons → a_mains/addons}/PlayWrightCodegen.cmd +0 -0
- /atomicshop/{addons → a_mains/addons}/ScriptExecution.cmd +0 -0
- /atomicshop/{addons → a_mains/addons}/inits/init_to_import_all_modules.py +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/ReadMe.txt +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/compile.cmd +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.dll +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.exp +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.lib +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/process_list.cpp +0 -0
- /atomicshop/{archiver → permissions}/__init__.py +0 -0
- /atomicshop/{wrappers/fibratusw → web_apis}/__init__.py +0 -0
- /atomicshop/wrappers/{nodejsw → pywin32w/wmis}/__init__.py +0 -0
- /atomicshop/wrappers/pywin32w/{wmi_win32process.py → wmis/win32process.py} +0 -0
- {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info/licenses}/LICENSE.txt +0 -0
- {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/top_level.txt +0 -0
atomicshop/etws/trace.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import queue
|
|
2
2
|
import sys
|
|
3
|
-
import
|
|
3
|
+
import multiprocessing.managers
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
|
|
5
6
|
# Import FireEye Event Tracing library.
|
|
6
7
|
import etw
|
|
@@ -8,27 +9,25 @@ import etw
|
|
|
8
9
|
from ..print_api import print_api
|
|
9
10
|
from . import sessions
|
|
10
11
|
from ..process_poller import simple_process_pool
|
|
11
|
-
from ..wrappers.psutilw import psutilw
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
WAIT_FOR_PROCESS_POLLER_PID_SECONDS: int = 3
|
|
15
|
-
WAIT_FOR_PROCESS_POLLER_PID_COUNTS: int = WAIT_FOR_PROCESS_POLLER_PID_SECONDS * 10
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
class EventTrace(etw.ETW):
|
|
19
15
|
def __init__(
|
|
20
16
|
self,
|
|
21
17
|
providers: list,
|
|
22
|
-
event_callback=None,
|
|
18
|
+
event_callback: callable = None,
|
|
23
19
|
event_id_filters: list = None,
|
|
24
20
|
session_name: str = None,
|
|
25
21
|
close_existing_session_name: bool = True,
|
|
26
|
-
enable_process_poller: bool = False
|
|
22
|
+
enable_process_poller: bool = False,
|
|
23
|
+
process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = None
|
|
27
24
|
):
|
|
28
25
|
"""
|
|
29
26
|
:param providers: List of tuples with provider name and provider GUID.
|
|
30
27
|
tuple[0] = provider name
|
|
31
28
|
tuple[1] = provider GUID
|
|
29
|
+
|
|
30
|
+
Example: [('Microsoft-Windows-DNS-Client', '{1c95126e-7ee8-4e23-86b2-6e7e4a5a8e9b}')]
|
|
32
31
|
:param event_callback: Reference to the callable callback function that will be called for each occurring event.
|
|
33
32
|
:param event_id_filters: List of event IDs that we want to filter. If not provided, all events will be returned.
|
|
34
33
|
The default in the 'etw.ETW' method is 'None'.
|
|
@@ -37,6 +36,13 @@ class EventTrace(etw.ETW):
|
|
|
37
36
|
:param enable_process_poller: Boolean to enable process poller. Gets the process PID, Name and CommandLine.
|
|
38
37
|
Since the DNS events doesn't contain the process name and command line, only PID.
|
|
39
38
|
Then DNS events will be enriched with the process name and command line from the process poller.
|
|
39
|
+
:param process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy,
|
|
40
|
+
multiprocessing shared dict proxy that contains current processes.
|
|
41
|
+
Check the 'atomicshop\process_poller\simple_process_pool.py' SimpleProcessPool class for more information.
|
|
42
|
+
|
|
43
|
+
If None, the process poller will create a new shared dict proxy.
|
|
44
|
+
If provided, then the provided shared dict proxy will be used.
|
|
45
|
+
Off course valid only if 'enable_process_poller' is True.
|
|
40
46
|
|
|
41
47
|
------------------------------------------
|
|
42
48
|
|
|
@@ -60,6 +66,7 @@ class EventTrace(etw.ETW):
|
|
|
60
66
|
self.event_queue = queue.Queue()
|
|
61
67
|
self.close_existing_session_name: bool = close_existing_session_name
|
|
62
68
|
self.enable_process_poller: bool = enable_process_poller
|
|
69
|
+
self.process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = process_pool_shared_dict_proxy
|
|
63
70
|
|
|
64
71
|
# If no callback function is provided, we will use the default one, which will put the event in the queue.
|
|
65
72
|
if not event_callback:
|
|
@@ -73,8 +80,16 @@ class EventTrace(etw.ETW):
|
|
|
73
80
|
for provider in providers:
|
|
74
81
|
etw_format_providers.append(etw.ProviderInfo(provider[0], etw.GUID(provider[1])))
|
|
75
82
|
|
|
83
|
+
self.self_hosted_poller: bool = False
|
|
76
84
|
if self.enable_process_poller:
|
|
77
|
-
self.
|
|
85
|
+
if self.process_pool_shared_dict_proxy is None:
|
|
86
|
+
self.self_hosted_poller = True
|
|
87
|
+
self.process_poller = simple_process_pool.SimpleProcessPool()
|
|
88
|
+
self.multiprocessing_manager: multiprocessing.managers.SyncManager = multiprocessing.Manager()
|
|
89
|
+
self.process_pool_shared_dict_proxy = self.multiprocessing_manager.dict()
|
|
90
|
+
|
|
91
|
+
self.pid_process_converter = simple_process_pool.PidProcessConverter(
|
|
92
|
+
process_pool_shared_dict_proxy=self.process_pool_shared_dict_proxy)
|
|
78
93
|
|
|
79
94
|
super().__init__(
|
|
80
95
|
providers=etw_format_providers, event_callback=function_callable, event_id_filters=event_id_filters,
|
|
@@ -82,7 +97,7 @@ class EventTrace(etw.ETW):
|
|
|
82
97
|
)
|
|
83
98
|
|
|
84
99
|
def start(self):
|
|
85
|
-
if self.enable_process_poller:
|
|
100
|
+
if self.enable_process_poller and self.self_hosted_poller:
|
|
86
101
|
self.process_poller.start()
|
|
87
102
|
|
|
88
103
|
# Check if the session name already exists.
|
|
@@ -107,9 +122,11 @@ class EventTrace(etw.ETW):
|
|
|
107
122
|
def stop(self):
|
|
108
123
|
super().stop()
|
|
109
124
|
|
|
110
|
-
if self.
|
|
125
|
+
if self.self_hosted_poller:
|
|
111
126
|
self.process_poller.stop()
|
|
112
127
|
|
|
128
|
+
self.multiprocessing_manager.shutdown()
|
|
129
|
+
|
|
113
130
|
def emit(self):
|
|
114
131
|
"""
|
|
115
132
|
The Function will return the next event from the queue.
|
|
@@ -127,44 +144,25 @@ class EventTrace(etw.ETW):
|
|
|
127
144
|
:return: etw event object.
|
|
128
145
|
"""
|
|
129
146
|
|
|
130
|
-
# Get the processes first, since we need the process name and command line.
|
|
131
|
-
# If they're not ready, we will get just pids from DNS tracing.
|
|
132
|
-
if self.enable_process_poller:
|
|
133
|
-
self._get_processes_from_poller()
|
|
134
|
-
|
|
135
147
|
event: tuple = self.event_queue.get()
|
|
136
148
|
|
|
149
|
+
current_datetime = datetime.now()
|
|
150
|
+
readable_time = current_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')
|
|
151
|
+
|
|
137
152
|
event_dict: dict = {
|
|
138
153
|
'EventId': event[0],
|
|
139
154
|
'EventHeader': event[1],
|
|
140
|
-
'
|
|
155
|
+
'timestamp': readable_time
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
if 'ProcessId' not in event[1]:
|
|
159
|
+
event_dict['pid'] = event[1]['EventHeader']['ProcessId']
|
|
160
|
+
else:
|
|
161
|
+
event_dict['pid'] = event[1]['ProcessId']
|
|
162
|
+
|
|
143
163
|
if self.enable_process_poller:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
while counter < WAIT_FOR_PROCESS_POLLER_PID_COUNTS:
|
|
148
|
-
processes = self.process_poller.get_processes()
|
|
149
|
-
if event_dict['pid'] not in processes:
|
|
150
|
-
time.sleep(0.1)
|
|
151
|
-
counter += 1
|
|
152
|
-
else:
|
|
153
|
-
break
|
|
154
|
-
|
|
155
|
-
if counter == WAIT_FOR_PROCESS_POLLER_PID_COUNTS:
|
|
156
|
-
print_api(f"Error: Couldn't get the process name for PID: {event_dict['pid']}.", color='red')
|
|
157
|
-
|
|
158
|
-
event_dict = psutilw.cross_single_connection_with_processes(event_dict, processes)
|
|
164
|
+
process_info: dict = self.pid_process_converter.get_process_by_pid(event_dict['pid'])
|
|
165
|
+
event_dict['name'] = process_info['name']
|
|
166
|
+
event_dict['cmdline'] = process_info['cmdline']
|
|
159
167
|
|
|
160
168
|
return event_dict
|
|
161
|
-
|
|
162
|
-
def _get_processes_from_poller(self):
|
|
163
|
-
processes: dict = {}
|
|
164
|
-
while not processes:
|
|
165
|
-
processes = self.process_poller.get_processes()
|
|
166
|
-
|
|
167
|
-
if isinstance(processes, BaseException):
|
|
168
|
-
raise processes
|
|
169
|
-
|
|
170
|
-
return processes
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import multiprocessing.managers
|
|
2
|
+
|
|
1
3
|
from .. import trace, const
|
|
2
4
|
from ...basics import dicts
|
|
3
|
-
from ... import dns
|
|
5
|
+
from ... import dns, ip_addresses
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
ETW_DEFAULT_SESSION_NAME: str = 'AtomicShopDnsTrace'
|
|
@@ -9,9 +11,6 @@ PROVIDER_NAME: str = const.ETW_DNS['provider_name']
|
|
|
9
11
|
PROVIDER_GUID: str = const.ETW_DNS['provider_guid']
|
|
10
12
|
REQUEST_RESP_EVENT_ID: int = const.ETW_DNS['event_ids']['dns_request_response']
|
|
11
13
|
|
|
12
|
-
WAIT_FOR_PROCESS_POLLER_PID_SECONDS: int = 3
|
|
13
|
-
WAIT_FOR_PROCESS_POLLER_PID_COUNTS: int = WAIT_FOR_PROCESS_POLLER_PID_SECONDS * 10
|
|
14
|
-
|
|
15
14
|
|
|
16
15
|
class DnsRequestResponseTrace:
|
|
17
16
|
"""DnsTrace class use to trace DNS events from Windows Event Tracing for EventId 3008."""
|
|
@@ -20,7 +19,8 @@ class DnsRequestResponseTrace:
|
|
|
20
19
|
attrs: list = None,
|
|
21
20
|
session_name: str = None,
|
|
22
21
|
close_existing_session_name: bool = True,
|
|
23
|
-
skip_record_list: list = None
|
|
22
|
+
skip_record_list: list = None,
|
|
23
|
+
process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = None
|
|
24
24
|
):
|
|
25
25
|
"""
|
|
26
26
|
:param attrs: List of attributes to return. If None, all attributes will be returned.
|
|
@@ -32,6 +32,13 @@ class DnsRequestResponseTrace:
|
|
|
32
32
|
created. Instead, the existing session will be used. If there is a buffer from the previous session,
|
|
33
33
|
you will get the events from the buffer.
|
|
34
34
|
:param skip_record_list: List of DNS Records to skip emitting. Example: ['PTR', 'SRV']
|
|
35
|
+
:param process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy, multiprocessing shared dict proxy
|
|
36
|
+
that contains current processes.
|
|
37
|
+
Check the 'atomicshop\process_poller\simple_process_pool.py' SimpleProcessPool class for more information.
|
|
38
|
+
|
|
39
|
+
For this specific class it means that you can run the process poller outside of this class and pass the
|
|
40
|
+
'process_pool_shared_dict_proxy' to this class. Then you can get the process name and command line for
|
|
41
|
+
the DNS events from the 'process_pool_shared_dict_proxy' and use it also in other classes.
|
|
35
42
|
|
|
36
43
|
-------------------------------------------------
|
|
37
44
|
|
|
@@ -40,7 +47,7 @@ class DnsRequestResponseTrace:
|
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
dns_trace_w = dns_trace.DnsTrace(
|
|
43
|
-
attrs=['pid', 'name', 'cmdline', '
|
|
50
|
+
attrs=['pid', 'name', 'cmdline', 'query', 'query_type'],
|
|
44
51
|
session_name='MyDnsTrace',
|
|
45
52
|
close_existing_session_name=True,
|
|
46
53
|
enable_process_poller=True
|
|
@@ -53,6 +60,7 @@ class DnsRequestResponseTrace:
|
|
|
53
60
|
"""
|
|
54
61
|
|
|
55
62
|
self.attrs = attrs
|
|
63
|
+
self.process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = process_pool_shared_dict_proxy
|
|
56
64
|
|
|
57
65
|
if skip_record_list:
|
|
58
66
|
self.skip_record_list: list = skip_record_list
|
|
@@ -68,7 +76,8 @@ class DnsRequestResponseTrace:
|
|
|
68
76
|
event_id_filters=[REQUEST_RESP_EVENT_ID],
|
|
69
77
|
session_name=session_name,
|
|
70
78
|
close_existing_session_name=close_existing_session_name,
|
|
71
|
-
enable_process_poller=True
|
|
79
|
+
enable_process_poller=True,
|
|
80
|
+
process_pool_shared_dict_proxy=self.process_pool_shared_dict_proxy
|
|
72
81
|
)
|
|
73
82
|
|
|
74
83
|
def start(self):
|
|
@@ -90,13 +99,52 @@ class DnsRequestResponseTrace:
|
|
|
90
99
|
:return: Dictionary with the event data.
|
|
91
100
|
"""
|
|
92
101
|
|
|
102
|
+
# Get the event from ETW as is.
|
|
93
103
|
event = self.event_trace.emit()
|
|
94
104
|
|
|
105
|
+
# Get the raw query results string from the event.
|
|
106
|
+
query_results: str = event['EventHeader']['QueryResults']
|
|
107
|
+
|
|
108
|
+
if query_results != '':
|
|
109
|
+
query_results_list: list = query_results.split(';')
|
|
110
|
+
|
|
111
|
+
addresses_ips: list = list()
|
|
112
|
+
addresses_cnames: list = list()
|
|
113
|
+
for query_result in query_results_list:
|
|
114
|
+
# If there is a type in the query result, it means it is a cname (domain).
|
|
115
|
+
if 'type' in query_result:
|
|
116
|
+
query_result = query_result.split(' ')[-1]
|
|
117
|
+
|
|
118
|
+
# But we'll still make sure that the query result is an IP address or not.
|
|
119
|
+
if ip_addresses.is_ip_address(query_result):
|
|
120
|
+
addresses_ips.append(query_result)
|
|
121
|
+
# If it is not empty, then it is a cname.
|
|
122
|
+
elif query_result != '':
|
|
123
|
+
addresses_cnames.append(query_result)
|
|
124
|
+
# if the query results are empty, then we'll just set the addresses to empty lists.
|
|
125
|
+
else:
|
|
126
|
+
addresses_ips: list = list()
|
|
127
|
+
addresses_cnames: list = list()
|
|
128
|
+
|
|
129
|
+
status_id: str = str(event['EventHeader']['QueryStatus'])
|
|
130
|
+
|
|
131
|
+
# Getting the 'QueryStatus' key. If DNS Query Status is '0' then it was executed successfully.
|
|
132
|
+
# And if not, it means there was an error. The 'QueryStatus' indicate what number of an error it is.
|
|
133
|
+
if status_id == '0':
|
|
134
|
+
status = 'Success'
|
|
135
|
+
else:
|
|
136
|
+
status = 'Error'
|
|
137
|
+
|
|
95
138
|
event_dict: dict = {
|
|
139
|
+
'timestamp': event['timestamp'],
|
|
96
140
|
'event_id': event['EventId'],
|
|
97
|
-
'
|
|
141
|
+
'query': event['EventHeader']['QueryName'],
|
|
98
142
|
'query_type_id': str(event['EventHeader']['QueryType']),
|
|
99
143
|
'query_type': dns.TYPES_DICT[str(event['EventHeader']['QueryType'])],
|
|
144
|
+
'result_ips': ','.join(addresses_ips),
|
|
145
|
+
'result_cnames': ','.join(addresses_cnames),
|
|
146
|
+
'status_id': status_id,
|
|
147
|
+
'status': status,
|
|
100
148
|
'pid': event['pid'],
|
|
101
149
|
'name': event['name'],
|
|
102
150
|
'cmdline': event['cmdline']
|
|
@@ -107,42 +155,6 @@ class DnsRequestResponseTrace:
|
|
|
107
155
|
if event_dict['query_type'] in self.skip_record_list:
|
|
108
156
|
return self.emit()
|
|
109
157
|
|
|
110
|
-
# Defining list if ips and other answers, which aren't IPs.
|
|
111
|
-
list_of_ips = list()
|
|
112
|
-
list_of_other_domains = list()
|
|
113
|
-
# Parse DNS results, only if 'QueryResults' key isn't empty, since many of the events are, mostly due errors.
|
|
114
|
-
if event['EventHeader']['QueryResults']:
|
|
115
|
-
# 'QueryResults' key contains a string with all the 'Answers' divided by type and ';' character.
|
|
116
|
-
# Basically, we can parse each type out of string, but we need only IPs and other answers.
|
|
117
|
-
list_of_parameters = event['EventHeader']['QueryResults'].split(';')
|
|
118
|
-
|
|
119
|
-
# Iterating through all the parameters that we got from 'QueryResults' key.
|
|
120
|
-
for parameter in list_of_parameters:
|
|
121
|
-
# If 'type' string is present it means that entry is a domain;
|
|
122
|
-
if 'type' in parameter:
|
|
123
|
-
# Remove the 'type' string and get the domain name.
|
|
124
|
-
current_iteration_parameter = parameter.rsplit(' ', maxsplit=1)[1]
|
|
125
|
-
# Add the variable to the list of other answers.
|
|
126
|
-
list_of_other_domains.append(current_iteration_parameter)
|
|
127
|
-
# If 'type' string is not present it means that entry is an IP.
|
|
128
|
-
else:
|
|
129
|
-
# Sometimes the last parameter in the 'QueryResults' key after ';' character will be empty, skip it.
|
|
130
|
-
if parameter:
|
|
131
|
-
list_of_ips.append(parameter)
|
|
132
|
-
|
|
133
|
-
event_dict['ips'] = list_of_ips
|
|
134
|
-
event_dict['other_domains'] = list_of_other_domains
|
|
135
|
-
|
|
136
|
-
# Getting the 'QueryStatus' key.
|
|
137
|
-
event_dict['status_id'] = event['EventHeader']['QueryStatus']
|
|
138
|
-
|
|
139
|
-
# Getting the 'QueryStatus' key. If DNS Query Status is '0' then it was executed successfully.
|
|
140
|
-
# And if not, it means there was an error. The 'QueryStatus' indicate what number of an error it is.
|
|
141
|
-
if event['EventHeader']['QueryStatus'] == '0':
|
|
142
|
-
event_dict['status'] = 'Success'
|
|
143
|
-
else:
|
|
144
|
-
event_dict['status'] = 'Error'
|
|
145
|
-
|
|
146
158
|
if self.attrs:
|
|
147
159
|
event_dict = dicts.reorder_keys(
|
|
148
160
|
event_dict, self.attrs, skip_keys_not_in_list=True)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import multiprocessing.managers
|
|
2
|
+
|
|
3
|
+
from .. import trace, providers
|
|
4
|
+
from ...basics import dicts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ETW_DEFAULT_SESSION_NAME: str = 'AtomicShopTcpTrace'
|
|
8
|
+
|
|
9
|
+
PROVIDER_NAME: str = "Microsoft-Windows-TCPIP"
|
|
10
|
+
PROVIDER_GUID: str = '{' + providers.get_provider_guid_by_name(PROVIDER_NAME) + '}'
|
|
11
|
+
REQUEST_RESP_EVENT_ID: int = 1033
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TcpIpNewConnectionsTrace:
|
|
15
|
+
"""
|
|
16
|
+
TcpIpNewConnectionsTrace class use to trace new connection events from Windows Event Tracing:
|
|
17
|
+
Provider: Microsoft-Windows-TCPIP
|
|
18
|
+
EventId: 1033
|
|
19
|
+
"""
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
attrs: list = None,
|
|
23
|
+
session_name: str = None,
|
|
24
|
+
close_existing_session_name: bool = True,
|
|
25
|
+
process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = None
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
:param attrs: List of attributes to return. If None, all attributes will be returned.
|
|
29
|
+
:param session_name: The name of the session to create. If not provided, a UUID will be generated.
|
|
30
|
+
:param close_existing_session_name: Boolean to close existing session names.
|
|
31
|
+
True: if ETW session with 'session_name' exists, you will be notified and the session will be closed.
|
|
32
|
+
Then the new session with this name will be created.
|
|
33
|
+
False: if ETW session with 'session_name' exists, you will be notified and the new session will not be
|
|
34
|
+
created. Instead, the existing session will be used. If there is a buffer from the previous session,
|
|
35
|
+
you will get the events from the buffer.
|
|
36
|
+
:param process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy, multiprocessing shared dict proxy
|
|
37
|
+
that contains current processes.
|
|
38
|
+
Check the 'atomicshop\process_poller\simple_process_pool.py' SimpleProcessPool class for more information.
|
|
39
|
+
|
|
40
|
+
For this specific class it means that you can run the process poller outside of this class and pass the
|
|
41
|
+
'process_pool_shared_dict_proxy' to this class. Then you can get the process name and command line for
|
|
42
|
+
the DNS events from the 'process_pool_shared_dict_proxy' and use it also in other classes.
|
|
43
|
+
|
|
44
|
+
-------------------------------------------------
|
|
45
|
+
|
|
46
|
+
Usage Example:
|
|
47
|
+
from atomicshop.etw import tcp_trace
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
tcp_trace_w = tcp_trace.TcpIpNewConnectionsTrace(
|
|
51
|
+
attrs=['pid', 'name', 'cmdline', 'domain', 'query_type'],
|
|
52
|
+
session_name='MyTcpTrace',
|
|
53
|
+
close_existing_session_name=True,
|
|
54
|
+
enable_process_poller=True
|
|
55
|
+
)
|
|
56
|
+
tcp_trace_w.start()
|
|
57
|
+
while True:
|
|
58
|
+
tcp_dict = tcp_trace_w.emit()
|
|
59
|
+
print(tcp_dict)
|
|
60
|
+
tcp_trace_w.stop()
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
self.attrs = attrs
|
|
64
|
+
self.process_pool_shared_dict_proxy: multiprocessing.managers.DictProxy = process_pool_shared_dict_proxy
|
|
65
|
+
|
|
66
|
+
if not session_name:
|
|
67
|
+
session_name = ETW_DEFAULT_SESSION_NAME
|
|
68
|
+
|
|
69
|
+
self.event_trace = trace.EventTrace(
|
|
70
|
+
providers=[(PROVIDER_NAME, PROVIDER_GUID)],
|
|
71
|
+
# lambda x: self.event_queue.put(x),
|
|
72
|
+
event_id_filters=[REQUEST_RESP_EVENT_ID],
|
|
73
|
+
session_name=session_name,
|
|
74
|
+
close_existing_session_name=close_existing_session_name,
|
|
75
|
+
enable_process_poller=True,
|
|
76
|
+
process_pool_shared_dict_proxy=self.process_pool_shared_dict_proxy
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
self.event_trace.start()
|
|
81
|
+
|
|
82
|
+
def stop(self):
|
|
83
|
+
self.event_trace.stop()
|
|
84
|
+
|
|
85
|
+
def emit(self):
|
|
86
|
+
"""
|
|
87
|
+
Function that will return the next event from the queue.
|
|
88
|
+
The queue is blocking, so if there is no event in the queue, the function will wait until there is one.
|
|
89
|
+
|
|
90
|
+
Usage Example:
|
|
91
|
+
while True:
|
|
92
|
+
tcp_dict = tcp_trace.emit()
|
|
93
|
+
print(tcp_dict)
|
|
94
|
+
|
|
95
|
+
:return: Dictionary with the event data.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Get the event from ETW as is.
|
|
99
|
+
event = self.event_trace.emit()
|
|
100
|
+
|
|
101
|
+
local_address_port: str = event['EventHeader']['LocalAddress']
|
|
102
|
+
remote_address_port: str = event['EventHeader']['RemoteAddress']
|
|
103
|
+
|
|
104
|
+
if 'ffff' in local_address_port:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
local_address, local_port = local_address_port.rsplit(':', 1)
|
|
108
|
+
local_address = local_address.replace('[', '').replace(']', '')
|
|
109
|
+
|
|
110
|
+
remote_address, remote_port = remote_address_port.rsplit(':', 1)
|
|
111
|
+
remote_address = remote_address.replace('[', '').replace(']', '')
|
|
112
|
+
|
|
113
|
+
event_dict: dict = {
|
|
114
|
+
'timestamp': event['timestamp'],
|
|
115
|
+
'event_id': event['EventId'],
|
|
116
|
+
'local_ip': local_address,
|
|
117
|
+
'local_port': local_port,
|
|
118
|
+
'remote_ip': remote_address,
|
|
119
|
+
'remote_port': remote_port,
|
|
120
|
+
'status': event['EventHeader']['Status'],
|
|
121
|
+
'pid': event['pid'],
|
|
122
|
+
'name': event['name'],
|
|
123
|
+
'cmdline': event['cmdline']
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if self.attrs:
|
|
127
|
+
event_dict = dicts.reorder_keys(
|
|
128
|
+
event_dict, self.attrs, skip_keys_not_in_list=True)
|
|
129
|
+
|
|
130
|
+
return event_dict
|
atomicshop/file_io/csvs.py
CHANGED
|
@@ -2,11 +2,10 @@ import csv
|
|
|
2
2
|
import io
|
|
3
3
|
from typing import Tuple, List
|
|
4
4
|
|
|
5
|
-
from .file_io import read_file_decorator
|
|
6
5
|
from . import file_io
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
@read_file_decorator
|
|
8
|
+
@file_io.read_file_decorator
|
|
10
9
|
def read_csv_to_list_of_dicts_by_header(
|
|
11
10
|
file_path: str,
|
|
12
11
|
file_mode: str = 'r',
|
|
@@ -53,7 +52,7 @@ def read_csv_to_list_of_dicts_by_header(
|
|
|
53
52
|
return csv_list, header
|
|
54
53
|
|
|
55
54
|
|
|
56
|
-
@read_file_decorator
|
|
55
|
+
@file_io.read_file_decorator
|
|
57
56
|
def read_csv_to_list_of_lists(
|
|
58
57
|
file_path: str,
|
|
59
58
|
file_mode: str = 'r',
|
|
@@ -103,7 +102,8 @@ def read_csv_to_list_of_lists(
|
|
|
103
102
|
def write_list_to_csv(
|
|
104
103
|
content_list: list,
|
|
105
104
|
file_path: str,
|
|
106
|
-
mode: str = 'w'
|
|
105
|
+
mode: str = 'w',
|
|
106
|
+
encoding: str = None
|
|
107
107
|
) -> None:
|
|
108
108
|
"""
|
|
109
109
|
This function got dual purpose:
|
|
@@ -115,10 +115,12 @@ def write_list_to_csv(
|
|
|
115
115
|
:param content_list: List object that each iteration contains dictionary with same keys and different values.
|
|
116
116
|
:param file_path: Full file path to CSV file.
|
|
117
117
|
:param mode: String, file writing mode. Default is 'w'.
|
|
118
|
+
:param encoding: String, encoding of the file. Default is 'None'.
|
|
119
|
+
Example: 'utf-8', 'utf-16', 'cp1252'.
|
|
118
120
|
:return: None.
|
|
119
121
|
"""
|
|
120
122
|
|
|
121
|
-
with open(file_path, mode=mode, newline='') as csv_file:
|
|
123
|
+
with open(file_path, mode=mode, newline='', encoding=encoding) as csv_file:
|
|
122
124
|
if len(content_list) > 0 and isinstance(content_list[0], dict):
|
|
123
125
|
# Treat the list as list of dictionaries.
|
|
124
126
|
header = content_list[0].keys()
|
|
@@ -254,3 +256,23 @@ def escape_csv_line_to_list(csv_line: list) -> list:
|
|
|
254
256
|
result_csv_entries.append(escape_csv_value(entry))
|
|
255
257
|
|
|
256
258
|
return result_csv_entries
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_number_of_cells_in_string_line(line: str) -> int:
|
|
262
|
+
"""
|
|
263
|
+
Function to get number of cells in CSV line.
|
|
264
|
+
|
|
265
|
+
:param line: String, line of CSV file.
|
|
266
|
+
:return: int, number of cells in the line.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Create CSV reader from 'input_file'. By default, the first row will be the header if 'fieldnames' is None.
|
|
270
|
+
csv_reader = csv.reader([line])
|
|
271
|
+
|
|
272
|
+
# Get the first row of the CSV file.
|
|
273
|
+
csv_list = list(csv_reader)
|
|
274
|
+
|
|
275
|
+
# Get the number of cells in the first row.
|
|
276
|
+
number_of_cells = len(csv_list[0])
|
|
277
|
+
|
|
278
|
+
return number_of_cells
|
atomicshop/file_io/docxs.py
CHANGED
|
@@ -12,7 +12,7 @@ def get_hyperlinks(docx_path):
|
|
|
12
12
|
:return: list of strings, hyperlinks.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
hyperlinks: list = list()
|
|
15
|
+
hyperlinks: list[dict] = list()
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
doc = Document(docx_path)
|
|
@@ -26,7 +26,19 @@ def get_hyperlinks(docx_path):
|
|
|
26
26
|
if not paragraph.hyperlinks:
|
|
27
27
|
continue
|
|
28
28
|
for hyperlink in paragraph.hyperlinks:
|
|
29
|
-
|
|
29
|
+
# Hyperlinks are stored in docx (document.xml) without the fragment part.
|
|
30
|
+
# Fragment is the anchor of the link, for example: 'https://www.example.com#anchor'.
|
|
31
|
+
# So the hyperlink.address is stored as 'https://www.example.com'.
|
|
32
|
+
# And the fragment is stored in the hyperlink.fragment as 'anchor'.
|
|
33
|
+
# For the full hyperlink, we need to concatenate the address and the fragment.
|
|
34
|
+
# If there is no anchor in the link the fragment will be empty string ('').
|
|
35
|
+
# Basically, we don't need to add the fragment to the hyperlink if it's empty, we can just use the url.
|
|
36
|
+
# if hyperlink.fragment:
|
|
37
|
+
# hyperlinks.append(hyperlink.address + "#" + hyperlink.fragment)
|
|
38
|
+
hyperlinks.append({
|
|
39
|
+
'url': hyperlink.url,
|
|
40
|
+
'text': hyperlink.text
|
|
41
|
+
})
|
|
30
42
|
|
|
31
43
|
return hyperlinks
|
|
32
44
|
|
|
@@ -66,24 +78,27 @@ def search_for_hyperlink_in_files(directory_path: str, hyperlink: str, relative_
|
|
|
66
78
|
raise NotADirectoryError(f"Directory doesn't exist: {directory_path}")
|
|
67
79
|
|
|
68
80
|
# Get all the docx files in the specified directory.
|
|
69
|
-
files = filesystem.
|
|
70
|
-
directory_path, file_name_check_pattern="
|
|
81
|
+
files = filesystem.get_paths_from_directory(
|
|
82
|
+
directory_path, get_file=True, file_name_check_pattern="*.docx",
|
|
71
83
|
add_relative_directory=True, relative_file_name_as_directory=True)
|
|
72
84
|
|
|
73
|
-
found_in_files: list = list()
|
|
85
|
+
found_in_files: list[dict] = list()
|
|
74
86
|
for file_path in files:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
doc_hyperlinks = get_hyperlinks(file_path.path)
|
|
88
|
+
for doc_link in doc_hyperlinks:
|
|
89
|
+
if hyperlink in doc_link['url']:
|
|
90
|
+
if relative_paths:
|
|
91
|
+
path: str = file_path.relative_dir
|
|
92
|
+
else:
|
|
93
|
+
path: str = file_path.path
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
result_list = found_in_files
|
|
95
|
+
found_in_files.append({
|
|
96
|
+
'path':path,
|
|
97
|
+
'link':doc_link['url'],
|
|
98
|
+
'text':doc_link['text']
|
|
99
|
+
})
|
|
85
100
|
|
|
86
|
-
return
|
|
101
|
+
return found_in_files
|
|
87
102
|
|
|
88
103
|
|
|
89
104
|
def search_for_hyperlink_in_files_interface_main(script_directory: str = None):
|
|
@@ -126,10 +141,12 @@ def search_for_hyperlink_in_files_interface_main(script_directory: str = None):
|
|
|
126
141
|
found_in_files = search_for_hyperlink_in_files(
|
|
127
142
|
config['directory_path'], config['hyperlink'], relative_paths=config['relative_paths'])
|
|
128
143
|
|
|
129
|
-
print_api(f"Found
|
|
144
|
+
print_api(f"Found [{len(found_in_files)}] links:", color="blue")
|
|
130
145
|
|
|
131
146
|
for index, found_file in enumerate(found_in_files):
|
|
132
147
|
print_api(f"[{index+1}]", print_end="", color="green")
|
|
133
|
-
print_api(f" {found_file}")
|
|
148
|
+
print_api(f" {found_file['path']}")
|
|
149
|
+
print_api(f" {found_file['link']}", color="cyan")
|
|
150
|
+
print_api(f" {found_file['text']}", color="orange")
|
|
134
151
|
|
|
135
152
|
input('[*] Press [Enter] to exit...')
|