diff --git a/ConfigExamples/fullConfig.yaml b/ConfigExamples/fullConfig.yaml index 1e6c3d0..53bb114 100644 --- a/ConfigExamples/fullConfig.yaml +++ b/ConfigExamples/fullConfig.yaml @@ -8,6 +8,7 @@ collect: # Settings determining which audit logs to collect and how to do it Audit.Exchange: True Audit.SharePoint: True DLP.All: True + rustEngine: True # Use False to revert to the old Python engine. If running from python instead of executable, make sure to install the python wheel in the RustEngineWheels folder schedule: 0 1 0 # How often to run in days/hours/minutes. Delete this line to just run once and exit. maxThreads: 50 # Maximum number of simultaneous threads retrieving logs retries: 3 # Times to retry retrieving a content blob if it fails diff --git a/Linux/LINUX-OfficeAuditLogCollector-V1.5 b/Linux/LINUX-OfficeAuditLogCollector-V2.0 similarity index 91% rename from Linux/LINUX-OfficeAuditLogCollector-V1.5 rename to Linux/LINUX-OfficeAuditLogCollector-V2.0 index 461cb20..d4b5b22 100644 Binary files a/Linux/LINUX-OfficeAuditLogCollector-V1.5 and b/Linux/LINUX-OfficeAuditLogCollector-V2.0 differ diff --git a/README.md b/README.md index de93093..c9e40dd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ +# Announcement: + +To hugely boost performance and add reliability the engine of the log collector has been rewritten in Rust. Consider downloading the newest +executable to automatically use it. + +If you run python code directly instead of using the executables, install the RustEngine wheel under +the "RustEngineWheels" folder in this repo. To turn off the new engine (in case of issues or for whatever reason), use the following +in your config.yaml: + +``` +collect: + rustEngine: False +``` +In my own tests the Rust engine has been at least 10x faster and stable. If you run into any problems, please use the +above setting to revert to the old engine, and consider creating an issue here on Github so I can fix it. + # Office365 audit log collector Collect/retrieve Office365, Azure and DLP audit logs, optionally filter them, then send them to one or more outputs diff --git a/RustEngineWheels/alc-0.1.0-cp310-none-win_amd64.whl b/RustEngineWheels/alc-0.1.0-cp310-none-win_amd64.whl new file mode 100644 index 0000000..8be8b5f Binary files /dev/null and b/RustEngineWheels/alc-0.1.0-cp310-none-win_amd64.whl differ diff --git a/RustEngineWheels/alc-0.1.0-cp38-cp38-linux_x86_64.whl b/RustEngineWheels/alc-0.1.0-cp38-cp38-linux_x86_64.whl new file mode 100644 index 0000000..e6ea93a Binary files /dev/null and b/RustEngineWheels/alc-0.1.0-cp38-cp38-linux_x86_64.whl differ diff --git a/Source/AuditLogCollector.py b/Source/AuditLogCollector.py index 7316ab0..7739fc4 100644 --- a/Source/AuditLogCollector.py +++ b/Source/AuditLogCollector.py @@ -1,5 +1,6 @@ from Interfaces import AzureOMSInterface, SqlInterface, GraylogInterface, PRTGInterface, FileInterface, \ AzureTableInterface, AzureBlobInterface, FluentdInterface +import alc # Rust based log collector Engine import AuditLogSubscriber import ApiConnection import os @@ -45,6 +46,7 @@ def __init__(self, config_path, **kwargs): self.run_started = None self.logs_retrieved = 0 self.errors_retrieving = 0 + self.retries = 0 def force_stop(self, *args): @@ -65,13 +67,46 @@ def run_once(self): """ self._prepare_to_run() logging.log(level=logging.INFO, msg='Starting run @ {}. Content: {}.'.format( - datetime.datetime.now(), self._remaining_content_types)) - self._start_monitoring() - self._get_all_available_content() - while self.monitor_thread.is_alive(): - self.monitor_thread.join(1) + datetime.datetime.now(), self.config['collect', 'contentTypes'])) + if not self.config['collect', 'rustEngine'] is False: + self._start_interfaces() + self.receive_results_from_rust_engine() + self._stop_interfaces(force=False) + else: + self._start_monitoring() + self._get_all_available_content() + while self.monitor_thread.is_alive(): + self.monitor_thread.join(1) self._finish_run() + def receive_results_from_rust_engine(self): + + runs = self._get_needed_runs(content_types=self.config['collect', 'contentTypes'].copy()) + engine = alc.RustEngine(self.tenant_id, self.client_key, self.secret_key, self.publisher_id or self.tenant_id, + self.config['collect', 'contentTypes'], runs, + self.config['collect', 'maxThreads'] or 50, + self.config['collect', 'retries'] or 3) + engine.run_once() + last_received = datetime.datetime.now() + while True: + try: + result = engine.get_result() + except ValueError: # RustEngine throws this error when no logs are in the results recv queue + now = datetime.datetime.now() + if now - last_received > datetime.timedelta(seconds=60): + logging.error("Timed out waiting for results from engine") + break + last_received = now + except EOFError: # RustEngine throws this error when all content has been retrieved + logging.info("Rust engine finished receiving all content") + break + else: + content_json, content_id, content_expiration, content_type = result + self._handle_retrieved_content(content_id=content_id, content_expiration=content_expiration, + content_type=content_type, results=json.loads(content_json)) + self.logs_retrieved += 1 + _, _, self.retries, self.errors_retrieving = engine.stop() + def run_scheduled(self): """ Run according to the schedule set in the config file. Collector will not exit unless manually stopped. @@ -155,8 +190,8 @@ def _log_statistics(self): """ Write run statistics to log file / console. """ - logging.info("Finished. Total logs retrieved: {}. Total logs with errors: {}. Run time: {}.".format( - self.logs_retrieved, self.errors_retrieving, datetime.datetime.now() - self.run_started)) + logging.info("Finished. Total logs retrieved: {}. Total retries: {}. Total logs with errors: {}. Run time: {}." + .format(self.logs_retrieved, self.retries, self.errors_retrieving, datetime.datetime.now() - self.run_started)) for interface in self._all_enabled_interfaces: logging.info("{} reports: {} successfully sent, {} errors".format( interface.__class__.__name__, interface.successfully_sent, interface.unsuccessfully_sent)) @@ -224,12 +259,15 @@ def _auto_subscribe(self): logging.info("Auto subscribing to: {}".format(content_type)) subscriber.set_sub_status(content_type=content_type, action='start') - def _get_all_available_content(self): + def _get_needed_runs(self, content_types): """ - Start a thread to retrieve available content blobs for each content type to be collected. + Return the start- and end times needed to retrieve content for each content type. If the timespan to retrieve + logs for exceeds 24 hours, we need to split it up into 24 hour runs (limit by Office API). """ + runs = {} end_time = datetime.datetime.now(datetime.timezone.utc) - for content_type in self._remaining_content_types.copy(): + for content_type in content_types: + runs[content_type] = [] if self.config['collect', 'resume'] and content_type in self._last_run_times.keys(): start_time = self._last_run_times[content_type] logging.info("{} - resuming from: {}".format(content_type, start_time)) @@ -244,15 +282,29 @@ def _get_all_available_content(self): if end_time - start_time > datetime.timedelta(hours=24): split_start_time = start_time split_end_time = start_time + datetime.timedelta(hours=24) - self._start_get_available_content_thread( - content_type=content_type, start_time=split_start_time, end_time=split_end_time) + formatted_start_time = str(split_start_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] + formatted_end_time = str(split_end_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] + runs[content_type].append((formatted_start_time, formatted_end_time)) start_time = split_end_time self._remaining_content_types.append(content_type) else: - self._start_get_available_content_thread( - content_type=content_type, start_time=start_time, end_time=end_time) + formatted_start_time = str(start_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] + formatted_end_time = str(end_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] + runs[content_type].append((formatted_start_time, formatted_end_time)) break self._last_run_times[content_type] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ") + return runs + + def _get_all_available_content(self): + """ + Start a thread to retrieve available content blobs for each content type to be collected. + """ + runs = self._get_needed_runs(content_types=self._remaining_content_types.copy()) + for content_type, run_dates in runs.items(): + for run_date in run_dates: + start_time, end_time = run_date + self._start_get_available_content_thread( + content_type=content_type, start_time=start_time, end_time=end_time) def _start_get_available_content_thread(self, content_type, start_time, end_time): @@ -268,12 +320,10 @@ def _get_available_content(self, content_type, start_time, end_time): """ try: logging.log(level=logging.DEBUG, msg='Getting available content for type: "{}"'.format(content_type)) - formatted_end_time = str(end_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] - formatted_start_time = str(start_time).replace(' ', 'T').rsplit('.', maxsplit=1)[0] logging.info("Retrieving {}. Start time: {}. End time: {}.".format( - content_type, formatted_start_time, formatted_end_time)) + content_type, start_time, end_time)) response = self.make_api_request(url='subscriptions/content?contentType={0}&startTime={1}&endTime={2}'. - format(content_type, formatted_start_time, formatted_end_time)) + format(content_type, start_time, end_time)) self.blobs_to_collect[content_type] += response.json() while 'NextPageUri' in response.headers.keys() and response.headers['NextPageUri']: logging.log(level=logging.DEBUG, msg='Getting next page of content for type: "{0}"'. @@ -351,6 +401,7 @@ def _retrieve_content(self, content_json, content_type, retries): return except Exception as e: if retries: + self.retries += 1 time.sleep(self.config['collect', 'retryCooldown'] or 3) return self._retrieve_content(content_json=content_json, content_type=content_type, retries=retries - 1) else: @@ -358,17 +409,20 @@ def _retrieve_content(self, content_json, content_type, retries): logging.error("Error retrieving content: {}".format(e)) return else: - self._handle_retrieved_content(content_json=content_json, content_type=content_type, results=results) + self._handle_retrieved_content( + content_id=content_json['contentId'], content_expiration=content_json['contentExpiration'], + content_type=content_type, results=results) - def _handle_retrieved_content(self, content_json, content_type, results): + def _handle_retrieved_content(self, content_id, content_expiration, content_type, results): """ Check known logs, filter results and output what remains. - :param content_json: JSON dict of the content blob as retrieved from the API (dict) + :param content_id: ID of content blob from API (str) + :param content_expiration: date string of expiration of content blob from API (str) :param content_type: Type of API being retrieved for, e.g. 'Audit.Exchange' (str) :param results: list of JSON """ if self.config['collect', 'skipKnownLogs']: - self._known_content[content_json['contentId']] = content_json['contentExpiration'] + self._known_content[content_id] = content_expiration for log in results.copy(): if self.config['collect', 'skipKnownLogs']: if log['Id'] in self.known_logs: diff --git a/Source/GUI.py b/Source/GUI.py deleted file mode 100644 index 6053d79..0000000 --- a/Source/GUI.py +++ /dev/null @@ -1,289 +0,0 @@ -import AuditLogCollector, AuditLogSubscriber -from kivymd.app import MDApp -from kivy.lang.builder import Builder -from kivy.config import Config -from kivy.clock import Clock -from kivy.properties import StringProperty -import os -import sys -import json -import time -import logging -import threading -import datetime -root_path = os.path.split(__file__)[0] -if getattr(sys, 'frozen', False): - icon_path = os.path.join(sys._MEIPASS, os.path.join(root_path, "icon.ico")) -else: - icon_path = os.path.join(root_path, "icon.ico") -Config.set('kivy','window_icon', icon_path) -Config.set('graphics', 'resizable', False) -Config.set('graphics', 'width', '450') -Config.set('graphics', 'height', '600') -Config.write() - - -class GUI(MDApp): - - tenant_id = StringProperty() - client_key = StringProperty() - secret_key = StringProperty() - publisher_id = StringProperty() - - def __init__(self, tenant_id="", client_key="", secret_key="", publisher_id="", **kwargs): - - self.title = "Audit log collector" - super().__init__(**kwargs) - self.root_widget = None - self.publisher_id = publisher_id - self.tenant_id = tenant_id - self.client_key = client_key - self.secret_key = secret_key - self.subscriber = AuditLogSubscriber.AuditLogSubscriber() - self.collector = AuditLogCollector.AuditLogCollector() - self.successfully_finished = None - self.running_continuously = False - self.last_run_start = None - self.run_thread = None - - def track_continuous(self, *args): - - if not self.running_continuously: - return - elif self.run_thread and self.run_thread.is_alive(): - time.sleep(1) - return Clock.schedule_once(self.track_continuous) - - target_time = self.last_run_start + datetime.timedelta( - hours=self.root_widget.ids.tab_widget.ids.config_widget.ids.run_time_slider.value) - if datetime.datetime.now() >= target_time: - self.run_collector() - Clock.schedule_once(self.track_run) - else: - time_until_next = str(target_time - datetime.datetime.now()).split('.')[0] - self.root_widget.ids.tab_widget.ids.collector_widget.ids.next_run_label.text = time_until_next - time.sleep(1) - return Clock.schedule_once(self.track_continuous) - - def track_run(self, *args): - - prefix = self.root_widget.ids.tab_widget - if self.run_thread.is_alive(): - prefix.ids.collector_widget.ids.status_label.text = "Status: Running" - self._update_run_statistics() - time.sleep(0.5) - Clock.schedule_once(self.track_run) - else: - if self.successfully_finished is True: - prefix.ids.collector_widget.ids.status_label.text = "Status: Finished" - else: - prefix.ids.collector_widget.ids.status_label.text = "Error: {}".format(self.successfully_finished) - self._update_run_statistics() - prefix.ids.collector_widget.ids.run_time.text = \ - str(datetime.datetime.now() - self.collector.run_started).split(".")[0] - if not self.running_continuously: - prefix.ids.collector_widget.ids.run_once_button.disabled = False - prefix.ids.collector_widget.ids.run_continuous_button.disabled = False - prefix.ids.collector_widget.ids.run_continuous_button.text = 'Run continuously' - - def _update_run_statistics(self): - - prefix = self.root_widget.ids.tab_widget - prefix.ids.collector_widget.ids.run_time.text = str(datetime.datetime.now() - self.collector.run_started) - prefix.ids.collector_widget.ids.retrieved_label.text = str(self.collector.logs_retrieved) - prefix.ids.collector_widget.ids.azure_sent_label.text = str( - self.collector.azure_oms_interface.successfully_sent) - prefix.ids.collector_widget.ids.azure_error_label.text = str( - self.collector.azure_oms_interface.unsuccessfully_sent) - prefix.ids.collector_widget.ids.graylog_sent_label.text = str( - self.collector.graylog_interface.successfully_sent) - prefix.ids.collector_widget.ids.graylog_error_label.text = str( - self.collector.graylog_interface.unsuccessfully_sent) - - def run_once(self): - - self.run_thread = threading.Thread(target=self.run_collector, daemon=True) - self.run_thread.start() - Clock.schedule_once(self.track_run) - - def run_continuous(self): - - if not self.running_continuously: - self.running_continuously = True - self.run_thread = threading.Thread(target=self.run_collector, daemon=True) - self.run_thread.start() - Clock.schedule_once(self.track_continuous) - Clock.schedule_once(self.track_run) - else: - self.running_continuously = False - self.root_widget.ids.tab_widget.ids.collector_widget.ids.next_run_label.text = "-" - self.root_widget.ids.tab_widget.ids.collector_widget.ids.run_continuous_button.text = \ - 'Run continuously' - - def run_collector(self): - - self._prepare_to_run() - self.last_run_start = datetime.datetime.now() - self.root_widget.ids.tab_widget.ids.collector_widget.ids.next_run_label.text = "-" - try: - self.collector.run_once() - self.successfully_finished = True - except Exception as e: - self.successfully_finished = e - - def _prepare_to_run(self): - - prefix = self.root_widget.ids.tab_widget - prefix.ids.collector_widget.ids.run_once_button.disabled = True - prefix.ids.collector_widget.ids.run_continuous_button.disabled = True - if self.running_continuously: - prefix.ids.collector_widget.ids.run_continuous_button.text = 'Stop running continuously' - self.collector.content_types = prefix.ids.subscriber_widget.enabled_content_types - self.collector.resume = prefix.ids.config_widget.ids.resume_switch.active - fallback_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta( - hours=prefix.ids.config_widget.ids.collect_time_slider.value) - self.collector._fallback_time = fallback_time - self.collector.file_output = prefix.ids.config_widget.ids.file_output_switch.active - self.collector.output_path = prefix.ids.config_widget.ids.file_output_path.text - self.collector.azure_oms_output = prefix.ids.config_widget.ids.oms_output_switch.active - self.collector.graylog_output = prefix.ids.config_widget.ids.graylog_output_switch.active - self.collector.azure_oms_interface.workspace_id = prefix.ids.config_widget.ids.oms_id.text - self.collector.azure_oms_interface.shared_key = prefix.ids.config_widget.ids.oms_key.text - self.collector.graylog_interface.gl_address = prefix.ids.config_widget.ids.graylog_ip.text - self.collector.graylog_interface.gl_port = prefix.ids.config_widget.ids.graylog_port.text - if prefix.ids.config_widget.ids.log_switch.active: - logging.basicConfig(filemode='w', filename='logs.txt', level=logging.DEBUG) - - @property - def guid_example(self): - - return "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - - def build(self): - - self.icon = icon_path - self.theme_cls.theme_style = "Dark" - from UX import MainWidget - Builder.load_file(os.path.join(root_path, 'UX/MainWidget.kv')) - self.root_widget = MainWidget.MainWidget() - prefix = self.root_widget.ids.tab_widget - prefix.ids.config_widget.ids.clear_known_content.disabled = not os.path.exists('known_content') - prefix.ids.config_widget.ids.clear_last_run_times.disabled = not os.path.exists('last_run_times') - self.load_settings() - return self.root_widget - - def login(self, tenant_id, client_key, secret_key, publisher_id): - - if self.collector._headers: - return self.logout() - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.disabled = True - self.tenant_id, self.client_key, self.secret_key, self.publisher_id = \ - tenant_id, client_key, secret_key, publisher_id - self.subscriber.tenant_id = tenant_id - self.subscriber.client_key = client_key - self.subscriber.secret_key = secret_key - self.subscriber.publisher_id = publisher_id - if not tenant_id or not client_key or not secret_key: - self.root_widget.connection_widget.ids.status_label.text = \ - "[color=#ff0000]Error logging in: provide tenant ID, client key and secret key. Find them in your " \ - "Azure AD app registration.[/color]" - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.disabled = False - return - try: - self.subscriber.login() - self.root_widget.ids.status_label.text = "[color=#00ff00]Connected![/color]" - except Exception as e: - self.root_widget.ids.status_label.text = "[color=#ff0000]Error logging in: {}[/color]".format(e) - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.disabled = False - return - login_headers = self.subscriber.headers - self.collector._headers = login_headers - - self.root_widget.ids.tab_widget.ids.subscriber_widget.activate_switches() - self.root_widget.ids.tab_widget.ids.subscriber_widget.set_switches() - self.root_widget.ids.tab_widget.ids.collector_widget.ids.run_once_button.disabled = False - self.root_widget.ids.tab_widget.ids.collector_widget.ids.run_continuous_button.disabled = False - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.text = 'Disconnect' - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.disabled = False - - def logout(self): - - self.collector._headers = None - self.subscriber._headers = None - self.root_widget.ids.status_label.text = "[color=#ffff00]Not logged in.[/color]" - self.root_widget.ids.tab_widget.ids.subscriber_widget.deactivate_switches(reset_value=True) - self.root_widget.ids.tab_widget.ids.collector_widget.ids.run_once_button.disabled = True - self.root_widget.ids.tab_widget.ids.collector_widget.ids.run_continuous_button.disabled = True - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.text = 'Connect' - self.root_widget.ids.tab_widget.ids.connection_widget.ids.login_button.disabled = False - - def save_settings(self): - - prefix = self.root_widget.ids.tab_widget - settings = dict() - settings['tenant_id'] = self.tenant_id - settings['client_key'] = self.client_key - settings['include_secret_key'] = prefix.ids.config_widget.ids.include_secret_key_switch.active - if prefix.ids.config_widget.ids.include_secret_key_switch.active: - settings['secret_key'] = self.secret_key - settings['publisher_id'] = self.publisher_id - settings['resume'] = prefix.ids.config_widget.ids.resume_switch.active - settings['run_time'] = prefix.ids.config_widget.ids.run_time_slider.value - settings['fallback_time'] = prefix.ids.config_widget.ids.collect_time_slider.value - settings['file_output'] = prefix.ids.config_widget.ids.file_output_switch.active - settings['output_path'] = prefix.ids.config_widget.ids.file_output_path.text - settings['azure_oms_output'] = prefix.ids.config_widget.ids.oms_output_switch.active - settings['graylog_output'] = prefix.ids.config_widget.ids.graylog_output_switch.active - settings['oms_workspace_id'] = prefix.ids.config_widget.ids.oms_id.text - settings['oms_shared_key'] = prefix.ids.config_widget.ids.oms_key.text - settings['gl_address'] = prefix.ids.config_widget.ids.graylog_ip.text - settings['gl_port'] = prefix.ids.config_widget.ids.graylog_port.text - settings['debug_logging'] = prefix.ids.config_widget.ids.log_switch.active - settings_file = os.path.join(root_path, 'gui_settings.json') - with open(settings_file, 'w') as ofile: - json.dump(settings, ofile) - - def load_settings(self): - - settings_file = os.path.join(root_path, 'gui_settings.json') - if not os.path.exists(settings_file): - return - with open(settings_file, 'r') as ofile: - settings = json.load(ofile) - prefix = self.root_widget.ids.tab_widget - self.tenant_id = settings['tenant_id'] - self.client_key = settings['client_key'] - prefix.ids.config_widget.ids.include_secret_key_switch.active = settings['include_secret_key'] - if prefix.ids.config_widget.ids.include_secret_key_switch.active: - self.secret_key = settings['secret_key'] - self.publisher_id = settings['publisher_id'] - prefix.ids.config_widget.ids.resume_switch.active = settings['resume'] - prefix.ids.config_widget.ids.run_time_slider.value = settings['run_time'] - prefix.ids.config_widget.ids.collect_time_slider.value = settings['fallback_time'] - prefix.ids.config_widget.ids.file_output_switch.active = settings['file_output'] - prefix.ids.config_widget.ids.file_output_path.text = settings['output_path'] - prefix.ids.config_widget.ids.oms_output_switch.active = settings['azure_oms_output'] - prefix.ids.config_widget.ids.graylog_output_switch.active = settings['graylog_output'] - prefix.ids.config_widget.ids.oms_id.text = settings['oms_workspace_id'] - prefix.ids.config_widget.ids.oms_key.text = settings['oms_shared_key'] - prefix.ids.config_widget.ids.graylog_ip.text = settings['gl_address'] - prefix.ids.config_widget.ids.graylog_port.text = settings['gl_port'] - prefix.ids.config_widget.ids.log_switch.active = settings['debug_logging'] - - def clear_known_content(self): - - self.root_widget.ids.tab_widget.ids.config_widget.ids.clear_known_content.disabled = True - if os.path.exists('known_content'): - os.remove('known_content') - - def clear_last_run_times(self): - - self.root_widget.ids.tab_widget.ids.config_widget.ids.clear_last_run_times.disabled = True - if os.path.exists('last_run_times'): - os.remove('last_run_times') - - -if __name__ == '__main__': - - gui = GUI() - gui.run() diff --git a/Source/RustEngine/Cargo.lock b/Source/RustEngine/Cargo.lock new file mode 100644 index 0000000..405ee30 --- /dev/null +++ b/Source/RustEngine/Cargo.lock @@ -0,0 +1,1329 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "alc" +version = "0.1.0" +dependencies = [ + "chrono", + "futures", + "log", + "pyo3", + "reqwest", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "simple_logger", + "tokio", + "tokio-stream", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e" +dependencies = [ + "unindent", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "pyo3" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd86513975ed69bf3fb5d4a286cdcda66dbc56f84bdf4832b6c82b459f4417b2" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450e2e56cbfa67bbe224cef93312b7a76d81c471d4e0c459d24d4bfaf3d75b53" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e653782972eba2fe86e8319ade54b97822c65fb1ccc1e116368372faa6ebc9" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317ce641f29f4e10e75765630bf4d28b2008612226fcc80b27f334fee8184d0f" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59342fce58a05983688e8d81209d06f67f0fcb1597253ef63b390b2da2417522" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_logger" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" +dependencies = [ + "atty", + "colored", + "log", + "time 0.3.9", + "winapi", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "target-lexicon" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unindent" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Source/RustEngine/Cargo.toml b/Source/RustEngine/Cargo.toml new file mode 100644 index 0000000..7296b12 --- /dev/null +++ b/Source/RustEngine/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "alc" +version = "0.1.0" +edition = "2021" + +[lib] +# The name of the native library. This is the name which will be used in Python to import the +# library (i.e. `import string_sum`). If you change this, you must also change the name of the +# `#[pymodule]` in `src/lib.rs`. +name = "alc" +# "cdylib" is necessary to produce a shared library for Python to import from. +# +# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able +# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.: +# crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.16.4", features = ["extension-module"] } +log = "0.4.16" +simple_logger = "2.1.0" +chrono = "0.4.19" +futures = "0.3.21" +reqwest = {version = "0.11.10", features = ["blocking", "json"]} +tokio = {version="1.17.0", features=["full"]} +tokio-stream = "0.1.8" +serde="1.0.136" +serde_yaml = "0.8.23" +serde_json="1.0.79" +serde_derive = "1.0.136" \ No newline at end of file diff --git a/Source/RustEngine/src/api_connection.rs b/Source/RustEngine/src/api_connection.rs new file mode 100644 index 0000000..8f77aa6 --- /dev/null +++ b/Source/RustEngine/src/api_connection.rs @@ -0,0 +1,283 @@ +use std::collections::HashMap; +use reqwest; +use log::{debug, info, warn, error}; +use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap}; +use tokio; +use serde_json; +use chrono::{DateTime}; +use futures::{SinkExt, StreamExt}; +use futures::channel::mpsc::{Receiver, Sender}; +use crate::data_structures::{JsonList, StatusMessage, GetBlobConfig, GetContentConfig, + AuthResult, ContentToRetrieve}; + + +/// Return a logged in API connection object. Use the Headers value to make API requests. +pub fn get_api_connection(tenant_id: String, client_id: String, secret_key: String, + publisher_id: String) -> ApiConnection { + + let mut api = ApiConnection { + tenant_id, + client_id, + secret_key, + publisher_id, + headers: HeaderMap::new(), + }; + api.login(); + api +} +/// Abstraction of an API connection to Azure Management APIs. Can be used to login to the API +/// which sets the headers. These headers can then be used to make authenticated requests. +pub struct ApiConnection { + pub tenant_id: String, + pub client_id: String, + secret_key: String, + pub publisher_id: String, + pub headers: HeaderMap, +} +impl ApiConnection { + /// Use tenant_id, client_id and secret_key to request a bearer token and store it in + /// our headers. Must be called once before requesting any content. + fn login(&mut self) { + let auth_url = + format!("https://login.microsoftonline.com/{}/oauth2/token", + self.tenant_id.to_string()); + let resource = "https://manage.office.com"; + let params = [("grant_type", "client_credentials"), ("client_id", &self.client_id), + ("client_secret", &self.secret_key), ("resource", &resource)]; + self.headers.insert(CONTENT_TYPE, + "application/x-www-form-urlencoded".parse().unwrap()); + let login_client = reqwest::blocking::Client::new(); + let json: AuthResult = login_client + .post(auth_url) + .headers(self.headers.clone()) + .form(¶ms) + .send() + .unwrap() + .json() + .unwrap(); + let token = format!("bearer {}", json.access_token); + self.headers.insert(AUTHORIZATION, token.parse().unwrap()); + } +} + + +/// Create a URL that can retrieve the first page of content for each passed content type. Each +/// content type can have multiple runs specified. A run consists of a start- and end date to +/// retrieve data for. Max. time span is 24, so if the user wants to retrieve for e.g. 72 hours, +/// we need 3 runs of 24 hours each. The runs object looks like e.g.: +/// Runs{Audit.Exchange: [(start_date, end_date), (start_date, end_date), (start_date, end_date)} +pub fn create_base_urls( + content_types: Vec, tenant_id: String, publisher_id: String, + runs: HashMap>) -> Vec<(String, String)> { + let mut urls_to_get: Vec<(String, String)> = Vec::new(); + for content_type in content_types.iter() { + let content_runs = runs.get(content_type).unwrap(); + for content_run in content_runs.into_iter() { + let (start_time, end_time) = content_run; + urls_to_get.push( + (content_type.to_string(), + format!("https://manage.office.com/api/v1.0/{}/activity/feed/\ + subscriptions/content?contentType={}&startTime={}&endTime={}\ + &PublisherIdentifier={}", + tenant_id, content_type, start_time, end_time, publisher_id) + )); + } + } + urls_to_get +} + + +/// Get available content blobs to retrieve. A base URL receices the initial page of content blobs. +/// The response header could specify 'NextPageUri', which if it exists specifies the URL for the +/// next page of content. This is sent over the blobs_tx channel to retrieve as well. If no +/// additional pages exist, a status message is sent to indicate all content blobs for this +/// content type have been retrieved. +#[tokio::main(flavor="multi_thread", worker_threads=200)] +pub async fn get_content_blobs(config: GetBlobConfig, blobs_rx: Receiver<(String, String)>) { + blobs_rx.for_each_concurrent(config.threads, |(content_type, url)| { + let blobs_tx = config.blobs_tx.clone(); + let blob_error_tx = config.blob_error_tx.clone(); + let status_tx = config.status_tx.clone(); + let content_tx = config.content_tx.clone(); + let client = config.client.clone(); + let headers = config.headers.clone(); + let content_type = content_type.clone(); + let url = url.clone(); + async move { + match client.get(url.clone()).timeout(std::time::Duration::from_secs(5)). + headers(headers.clone()).send().await { + Ok(resp) => { + handle_blob_response(resp, blobs_tx, status_tx, content_tx, blob_error_tx, + content_type, url).await; + }, + Err(e) => { + error!("Err getting blob response {}", e); + handle_blob_response_error(status_tx, blob_error_tx, content_type, url).await; + } + } + } + }).await; + debug!("Exit blob thread"); +} + + +/// Deal with the response of a successful content blob request. Try to decode into JSON to +/// retrieve the content URIs of the content inside the blob. Also check response header for another +/// page of content blobs. +async fn handle_blob_response( + resp: reqwest::Response, blobs_tx: Sender<(String, String)>, + mut status_tx: Sender, content_tx: Sender, + mut blob_error_tx: Sender<(String, String)>, content_type: String, url: String) { + + handle_blob_response_paging(&resp, blobs_tx, status_tx.clone(), + content_type.clone()).await; + match resp.json::>>().await { + Ok(i) => { + handle_blob_response_content_uris(status_tx, content_tx, content_type, i) + .await; + }, + Err(e) => { + warn!("Err getting blob JSON {}", e); + match blob_error_tx.send((content_type, url)).await { + Err(e) => { + error!("Could not resend failed blob, dropping it: {}", e); + status_tx.send(StatusMessage::ErrorContentBlob).await.unwrap(); + }, + _=> (), + } + } + } +} + + +/// Determine if a content blob response header contains a reference to another page of blobs. +async fn handle_blob_response_paging( + resp: &reqwest::Response, mut blobs_tx: Sender<(String, String)>, + mut status_tx: Sender, content_type: String) { + + let next_or_not = resp.headers().get("NextPageUri"); + match next_or_not { + Some(i) => { + let new_url = i.to_str().unwrap().to_string(); + blobs_tx.send((content_type.clone(), new_url)).await.unwrap(); + }, + None => { + status_tx. + send(StatusMessage::FinishedContentBlobs).await.unwrap(); + } + }; +} + + +/// Deal with successfully received and decoded content blobs. Send the URIs of content to retrieve +/// over the content_tx channel for the content thread to retrieve. +async fn handle_blob_response_content_uris( + mut status_tx: Sender, mut content_tx: Sender, + content_type: String, content_json: JsonList) { + + for json_dict in content_json.into_iter() { + if json_dict.contains_key("contentUri") == false { + warn!("Invalid blob!: {:?}", json_dict); + } else { + let url = json_dict + .get("contentUri").unwrap() + .to_string() + .strip_prefix('"').unwrap().strip_suffix('"').unwrap() + .to_string(); + let expiration = json_dict.get("contentExpiration").unwrap() + .to_string() + .strip_prefix('"').unwrap().strip_suffix('"').unwrap() + .to_string(); + let content_id = json_dict.get("contentId").unwrap() + .to_string() + .strip_prefix('"').unwrap().strip_suffix('"').unwrap() + .to_string(); + let content_to_retrieve = ContentToRetrieve { + expiration, content_type: content_type.clone(), content_id, url}; + + content_tx.send(content_to_retrieve).await.unwrap(); + status_tx.send(StatusMessage::FoundNewContentBlob).await.unwrap(); + } + }; +} + +/// Deal with error while requesting a content blob. +async fn handle_blob_response_error( + mut status_tx: Sender, mut blob_error_tx: Sender<(String, String)>, + content_type: String, url: String) { + + match blob_error_tx.send((content_type, url)).await { + Err(e) => { + error!("Could not resend failed blob, dropping it: {}", e); + status_tx.send(StatusMessage::ErrorContentBlob).await.unwrap(); + }, + _=> (), + } +} + + +/// Retrieve the actual ContentUris found in the JSON body of content blobs. +#[tokio::main(flavor="multi_thread", worker_threads=200)] +pub async fn get_content(config: GetContentConfig, content_rx: Receiver) { + content_rx.for_each_concurrent(config.threads, |content_to_retrieve| { + let client = config.client.clone(); + let headers = config.headers.clone(); + let result_tx = config.result_tx.clone(); + let status_tx = config.status_tx.clone(); + let content_error_tx = config.content_error_tx.clone(); + async move { + match client.get(content_to_retrieve.url.clone()) + .timeout(std::time::Duration::from_secs(5)).headers(headers).send().await { + Ok(resp) => { + handle_content_response(resp, result_tx, status_tx, content_error_tx, + content_to_retrieve).await; + }, + Err(_) => { + handle_content_response_error(status_tx, content_error_tx, content_to_retrieve) + .await; + } + } + } + }).await; + debug!("Exit content thread"); +} + + +/// Deal with successful content request response. +async fn handle_content_response( + resp: reqwest::Response, result_tx: std::sync::mpsc::Sender<(String, ContentToRetrieve)>, + mut status_tx: Sender, mut content_error_tx: Sender, + content_to_retrieve: ContentToRetrieve) { + + match resp.text().await { + Ok(json) => { + result_tx.send((json, content_to_retrieve)).unwrap(); + status_tx.send(StatusMessage::RetrievedContentBlob).await.unwrap(); + } + Err(e) => { + warn!("Error interpreting JSON: {}", e); + match content_error_tx.send(content_to_retrieve).await { + Err(e) => { + error!("Could not resend failed content, dropping it: {}", e); + status_tx.send(StatusMessage::ErrorContentBlob).await.unwrap(); + }, + _=> (), + } + } + } +} + + +/// Deal with error response requesting a contentURI. +async fn handle_content_response_error( + mut status_tx: Sender, mut content_error_tx: Sender, + content_to_retrieve: ContentToRetrieve) { + + match content_error_tx.send(content_to_retrieve).await { + Err(e) => { + error!("Could not resend failed content, dropping it: {}", e); + status_tx.send(StatusMessage::ErrorContentBlob).await.unwrap(); + }, + _=> (), + } +} diff --git a/Source/RustEngine/src/data_structures.rs b/Source/RustEngine/src/data_structures.rs new file mode 100644 index 0000000..250f1cf --- /dev/null +++ b/Source/RustEngine/src/data_structures.rs @@ -0,0 +1,90 @@ +use futures::channel::mpsc::{Sender, Receiver}; +use std::collections::HashMap; +use reqwest::header::HeaderMap; +use serde_derive::{Deserialize}; + +/// List of JSON responses (used to represent content blobs) +pub type JsonList = Vec>; + + +/// Representation of Office API json response after sending an auth request. We need the bearer +/// token. +#[derive(Deserialize, Debug)] +pub struct AuthResult { + pub access_token: String, +} + + +/// Representation of content we need to retrieve. ID, expiration and content type are passed to +/// python along with the retrieved content. ID an expiration are needed for avoiding known logs, +/// content type for categorization in outputs. +pub struct ContentToRetrieve { + pub content_type: String, + pub content_id: String, + pub expiration: String, + pub url: String +} + +/// Messages for status channel between main threads and the blob/content retrieving threads. +/// Mainly used to keep track of which content still needs retrieving and which is finished, which +/// is necessary for knowing when to terminate. +pub enum StatusMessage { + BeingThrottled, + FinishedContentBlobs, // Finished getting all content blobs for e.g. Audit.Exchange + FoundNewContentBlob, // Found a new blob to retrieved + RetrievedContentBlob, // Finished retrieving a new blob + ErrorContentBlob, // Could not retrieve a blob +} + +/// Used by thread getting content blobs +pub struct GetBlobConfig { + pub client: reqwest::Client, + pub headers: HeaderMap, + pub status_tx: Sender, + pub blobs_tx: Sender<(String, String)>, + pub blob_error_tx: Sender<(String, String)>, + pub content_tx: Sender, + pub threads: usize, +} + + +/// Used by thread getting content +pub struct GetContentConfig { + pub client: reqwest::Client, + pub headers: HeaderMap, + pub result_tx: std::sync::mpsc::Sender<(String, ContentToRetrieve)>, + pub content_error_tx: Sender, + pub status_tx: Sender, + pub threads: usize, +} + + +/// Used by message loop keeping track of progress and terminating other threads when they are +/// finished. +pub struct MessageLoopConfig { + pub status_rx: Receiver, + pub stats_tx: std::sync::mpsc::Sender<(usize, usize, usize, usize)>, + pub blobs_tx: Sender<(String, String)>, + pub blob_error_rx: Receiver<(String, String)>, + pub content_tx: Sender, + pub content_error_rx: Receiver, + pub urls: Vec<(String, String)>, + pub content_types: Vec, + pub retries: usize, +} + + +/// These stats are passed back to python after a run has finished to show to end-user. +pub struct RunStatistics { + pub blobs_found: usize, + pub blobs_successful: usize, + pub blobs_error: usize, + pub blobs_retried: usize, +} +impl RunStatistics { + pub fn new() -> RunStatistics { + RunStatistics { + blobs_found: 0, blobs_successful: 0, blobs_error: 0, blobs_retried: 0 + } + } +} diff --git a/Source/RustEngine/src/lib.rs b/Source/RustEngine/src/lib.rs new file mode 100644 index 0000000..4cb6992 --- /dev/null +++ b/Source/RustEngine/src/lib.rs @@ -0,0 +1,312 @@ +use std::thread; +use std::collections::HashMap; +use log::{debug, info, warn, error}; +use futures::{SinkExt}; +use futures::channel::mpsc::channel; +use futures::channel::mpsc::{Sender, Receiver}; +use pyo3::prelude::*; +use crate::data_structures::{ContentToRetrieve, RunStatistics}; + + +mod api_connection; +mod data_structures; + + +#[pyclass] +/// # Rust Engine +/// A class instantiated in Python. Python will call the run_once method below, which will start +/// three background threads responsible for retrieving content. Python will then call +/// the get_result method on a loop to drain the results from the results channel until it is +/// disconnected. The three background threads are: +/// - blob_thread: find content blobs and send results to content channel +/// - content_thread: retrieve content blobs from content channel, send results to results channel +/// - message_loop_thread: keep track of progress, terminate after all content is retrieved +pub struct RustEngine { + tenant_id: String, + client_id: String, + secret_key: String, + publisher_id: String, + content_types: Vec, + runs: HashMap>, + result_rx: Option>, + stats_rx: Option>, + threads: usize, + retries: usize, +} + +#[pymethods] +impl RustEngine { + + #[new] + pub fn new(tenant_id: String, client_id: String, secret_key:String, publisher_id: String, + content_types: Vec, runs: HashMap>, + threads: usize, retries: usize) + -> RustEngine { + RustEngine { + result_rx: None, + stats_rx: None, + tenant_id, + client_id, + secret_key, + publisher_id, + content_types, + runs, + threads, + retries, + } + } + + /// Non-blocking. Call once to start retrieving logs, which will arrive in the results_rx + /// receiver. Call get_results iteratively to drain the results channel. + pub fn run_once(&mut self) { + let api = api_connection::get_api_connection( + self.tenant_id.clone(), self.client_id.clone(), + self.secret_key.clone(), self.publisher_id.clone()); + let (result_rx, stats_rx) = get_available_content( + api, self.content_types.clone(), self.runs.clone(), self.threads, + self.retries); + self.result_rx = Some(result_rx); + self.stats_rx = Some(stats_rx); + } + + /// ValueError means nothing in the channel right now, but more will come. EOFError means + /// all results received, no more will come. Message loop closes the results channel when + /// all content has been retrieved. + pub fn get_result(&self) -> PyResult<(String, String, String, String)> { + match self.result_rx.as_ref().unwrap().try_recv() { + Ok((i,j) ) => { + Ok((i, j.content_id, j.expiration, j.content_type)) + }, + Err(std::sync::mpsc::TryRecvError::Empty) => { + Err(pyo3::exceptions::PyValueError::new_err("No logs ready")) + }, + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + Err(pyo3::exceptions::PyEOFError::new_err("Finished run")) + } + } + } + + /// Receive the run results. This can only happen when the message_loop thread had exited its' + /// loop, so if we return the results we know the engine has stopped. + pub fn stop(&self) -> PyResult<(usize, usize, usize, usize)> { + Ok(self.stats_rx.as_ref().unwrap().try_recv().unwrap()) + } +} + + +/// Initialize a config object for each sub thread to run +/// - Blob thread: Collect available content blobs +/// - Content thread: Collect the blobs found by blob thread +/// - Message loop: Communicates with other threads to handle retries and terminate when finished +fn initialize_configs( + api: api_connection::ApiConnection, content_types: Vec, + runs: HashMap>, retries: usize, threads:usize) + -> (data_structures::GetBlobConfig, data_structures::GetContentConfig, + data_structures::MessageLoopConfig, Receiver<(String, String)>, Receiver, + std::sync::mpsc::Receiver<(String, ContentToRetrieve)>, + std::sync::mpsc::Receiver<(usize, usize, usize, usize)>) { + + let urls = api_connection::create_base_urls( + content_types.clone(), api.tenant_id, api.publisher_id, runs); + + // Create channels to communicate with async closures + let (status_tx, status_rx): + (Sender, Receiver) = + channel(100000); + let (blobs_tx, blobs_rx): (Sender<(String, String)>, Receiver<(String, String)>) = + channel(100000); + let (blob_error_tx, blob_error_rx): + (Sender<(String, String)>, Receiver<(String, String)>) = channel(100000); + let (content_tx, content_rx): (Sender, Receiver) = + channel(100000); + let (content_error_tx, content_error_rx): + (Sender, Receiver) = channel(100000000); + let (result_tx, result_rx): + (std::sync::mpsc::Sender<(String, ContentToRetrieve)>, + std::sync::mpsc::Receiver<(String, ContentToRetrieve)>) = + std::sync::mpsc::channel(); + let (stats_tx, stats_rx): + (std::sync::mpsc::Sender<(usize, usize, usize, usize)>, + std::sync::mpsc::Receiver<(usize, usize, usize, usize)>) = std::sync::mpsc::channel(); + + let blob_config = data_structures::GetBlobConfig { client: reqwest::Client::new(), headers: api.headers.clone(), + status_tx: status_tx.clone(), blobs_tx: blobs_tx.clone(), + blob_error_tx: blob_error_tx.clone(), content_tx: content_tx.clone(), threads + }; + + let content_config = data_structures::GetContentConfig { + client: reqwest::Client::new(), headers: api.headers.clone(), result_tx: result_tx.clone(), + content_error_tx: content_error_tx.clone(), status_tx: status_tx.clone(), threads + }; + + let message_loop_config = data_structures::MessageLoopConfig { + content_tx: content_tx.clone(), blobs_tx: blobs_tx.clone(), stats_tx: stats_tx.clone(), + urls, content_error_rx, status_rx, blob_error_rx, content_types, retries}; + return (blob_config, content_config, message_loop_config, blobs_rx, content_rx, result_rx, stats_rx) +} + + +/// Get all the available log content for a list of content types and runs (start- and end times +/// of content to receive). +fn get_available_content(api: api_connection::ApiConnection, content_types: Vec, + runs: HashMap>, threads: usize, + retries: usize) + -> (std::sync::mpsc::Receiver<(String, ContentToRetrieve)>, + std::sync::mpsc::Receiver<(usize, usize, usize, usize)>) { + + let (blob_config, content_config, message_loop_config, + blobs_rx, content_rx, result_rx, stats_rx) + = initialize_configs(api, content_types, runs, retries, threads); + spawn_blob_collector(blob_config, content_config, message_loop_config, blobs_rx, content_rx); + (result_rx, stats_rx) +} + +/// Spawn threads running the actual collectors, and a message loop thread to keep track of +/// progress and terminate once finished. +fn spawn_blob_collector( + blob_config: data_structures::GetBlobConfig, content_config: data_structures::GetContentConfig, + message_loop_config: data_structures::MessageLoopConfig, blobs_rx: Receiver<(String, String)>, + content_rx: Receiver<(ContentToRetrieve)>) { + + thread::spawn( move || {api_connection::get_content_blobs(blob_config, blobs_rx);}); + thread::spawn( move || {api_connection::get_content(content_config, content_rx);}); + thread::spawn(move || {message_loop(message_loop_config)}); +} + +/// Receive status updates to keep track of when all content has been retrieved. Also handle +/// retrying any failed content or dropping it after too many retries. Every time content is foudn +/// awaiting_content_blobs is incremented; every time content is retrieved or could not be +/// retrieved awaiting_content_blobs is decremented. When it reaches 0 we know we are done. +#[tokio::main] +pub async fn message_loop(mut config: data_structures::MessageLoopConfig) { + + // Send base URLS for content blob retrieval then keep track of when they've all come in + let mut awaiting_content_types:usize = 0; + for (content_type, base_url) in config.urls.into_iter() { + config.blobs_tx.clone().send((content_type, base_url)).await.unwrap(); + awaiting_content_types += 1; + } + // Keep track of found and retrieved content blobs + let mut awaiting_content_blobs: usize = 0; + // Keep track of retry count for failed blobs + let mut retry_map :HashMap = HashMap::new(); + // Keep stats to return to python after run finishes + let mut stats = RunStatistics::new(); + // Loop ends with the run itself, signalling the program is done. + loop { + // Receive status message indicated found content and retrieved content. When all blobs have + // been found, and all found blobs have been retrieved, we are done. + match config.status_rx.try_next() { + Ok(Some(msg)) => { + match msg { + // awaiting_content_types is initially the size of content type * runs for each + // content type. When retrieving pages if we don't get a NextPageUri response + // header, we know we have found all possible blobs for that content type and + // we decrement awaiting_content_types. When it hits 0 we know we found all + // content that can possible be retrieved. + data_structures::StatusMessage::FinishedContentBlobs => { + if awaiting_content_types > 0 { + awaiting_content_types -= 1; + } + }, + // We have found a new content blob while iterating through the pages of them. + // It has been queued up to be retrieved. + data_structures::StatusMessage::FoundNewContentBlob => { + awaiting_content_blobs +=1; + stats.blobs_found += 1; + }, + // A queued up content blob has actually been retrieved so we are done with it. + // When awaiting_content_blobs hits 0 we are done retrieving all actual content + // and we can exit. + data_structures::StatusMessage::RetrievedContentBlob => { + awaiting_content_blobs -= 1; + stats.blobs_successful += 1; + if awaiting_content_types == 0 && awaiting_content_blobs == 0 { + config.content_tx.close_channel(); + break; + } + }, + // A queued up content blob could not be retrieved so we are done with it. + // When awaiting_content_blobs hits 0 we are done retrieving all actual content + // and we can exit. + data_structures::StatusMessage::ErrorContentBlob => { + awaiting_content_blobs -= 1; + stats.blobs_error += 1; + if awaiting_content_types == 0 && awaiting_content_blobs == 0 { + config.content_tx.close_channel(); + break; + } + } + data_structures::StatusMessage::BeingThrottled => warn!("Throttled!"), // TODO: handle being throttled + } + }, + _ => () + } + // Check channel for content pages that could not be retrieved and retry them the user + // defined amount of times. If we can't in that amount of times then give up. + match config.blob_error_rx.try_next() { + Ok(Some((content_type, url))) => { + if retry_map.contains_key(&url) == true { + let retries_left = retry_map.get_mut(&url).unwrap(); + if retries_left == &mut 0 { + error!("Gave up on blob {}", url); + awaiting_content_types -= 1; + stats.blobs_error += 1; + } else { + *retries_left -= 1; + stats.blobs_retried += 1; + warn!("Retry blob {} {}", retries_left, url); + config.blobs_tx.send((content_type, url)).await.unwrap(); + + } + } else { + retry_map.insert(url.clone(), config.retries - 1); + stats.blobs_retried += 1; + warn!("Retry blob {} {}", config.retries - 1, url); + config.blobs_tx.send((content_type, url)).await.unwrap(); + } + }, + _ => (), + }; + // Check channel for content blobs that could not be retrieved and retry them the user + // defined amount of times. If we can't in that amount of times then give up. + match config.content_error_rx.try_next() { + Ok(Some(content)) => { + if retry_map.contains_key(&content.url) == true { + let retries_left = retry_map.get_mut(&content.url).unwrap(); + if retries_left == &mut 0 { + error!("Gave up on content {}", content.url); + awaiting_content_blobs -= 1; + stats.blobs_error += 1; + } else { + *retries_left -= 1; + stats.blobs_retried += 1; + warn!("Retry content {} {}", retries_left, content.url); + config.content_tx.send(content).await.unwrap(); + + } + } else { + retry_map.insert(content.url.to_string(), config.retries - 1); + stats.blobs_retried += 1; + warn!("Retry content {} {}", config.retries - 1, content.url); + config.content_tx.send(content).await.unwrap(); + } + } + _ => (), + } + /* + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + println!{"Pending content types: {}, Pending content blobs: {}", + awaiting_content_types, awaiting_content_blobs} + */ + } + // We send back stats after exiting the loop, signalling the end of the run. + config.stats_tx.send((stats.blobs_found, stats.blobs_successful, stats.blobs_retried, + stats.blobs_error)).unwrap(); +} + +#[pymodule] +fn alc(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/Source/UX/CollectorWidget.kv b/Source/UX/CollectorWidget.kv deleted file mode 100644 index f677e76..0000000 --- a/Source/UX/CollectorWidget.kv +++ /dev/null @@ -1,89 +0,0 @@ -: - orientation: 'tb-lr' - BoxLayout: - orientation: "vertical" - size_hint_y: None - height: '70dp' - ScalingLabel: - text: "Collect logs once on demand." - MDRaisedButton: - id: run_once_button - disabled: True - padding: 10, 0 - text: "Run once" - on_press: app.run_once() - MDSeparator: - BoxLayout: - orientation: "vertical" - size_hint_y: None - height: '70dp' - ScalingLabel: - text: "Collect logs periodically using configured time settings." - BoxLayout: - MDRaisedButton: - id: run_continuous_button - disabled: True - padding: 10, 0 - text: "Run continuously" - on_press: app.run_continuous() - ScalingLabel: - padding: 20, 0 - text: "Next run in:" - ScalingLabel: - id: next_run_label - text: "-" - MDSeparator: - BoxLayout: - size_hint_y: None - height: '30dp' - ScalingLabel: - id: status_label - text: "Status: idle" - BoxLayout - size_hint_y: None - height: '200dp' - orientation: "vertical" - BoxLayout: - ScalingLabel: - id: run_time - text: "-" - BoxLayout: - ScalingLabel: - id: retrieved_label - size_hint_x: 0.2 - text: "0" - ScalingLabel: - text: "logs retrieved" - BoxLayout: - ScalingLabel: - id: azure_sent_label - size_hint_x: 0.2 - text: "0" - ScalingLabel: - text: "logs successfully sent to Azure Log Analytics" - BoxLayout: - ScalingLabel: - id: azure_error_label - size_hint_x: 0.2 - text: "0" - ScalingLabel: - text: "logs unsuccessfully sent to Azure Log Analytics" - BoxLayout: - ScalingLabel: - id: graylog_sent_label - size_hint_x: 0.2 - text: "0" - ScalingLabel: - text: "logs successfully sent to Graylog" - BoxLayout: - ScalingLabel: - id: graylog_error_label - size_hint_x: 0.2 - text: "0" - ScalingLabel: - text: "logs unsuccessfully sent to Graylog" - -: - text_size: root.width, None - size: self.texture_size - markup: True diff --git a/Source/UX/CollectorWidget.py b/Source/UX/CollectorWidget.py deleted file mode 100644 index a1c28f8..0000000 --- a/Source/UX/CollectorWidget.py +++ /dev/null @@ -1,9 +0,0 @@ -from kivy.uix import stacklayout -from kivymd.uix import tab - - -class CollectorWidget(tab.MDTabsBase, stacklayout.StackLayout): - - def __init__(self, **kwargs): - - super().__init__(**kwargs) \ No newline at end of file diff --git a/Source/UX/ConfigWidget.kv b/Source/UX/ConfigWidget.kv deleted file mode 100644 index 1e1985b..0000000 --- a/Source/UX/ConfigWidget.kv +++ /dev/null @@ -1,209 +0,0 @@ -: - do_scroll_x: False - do_scroll_y: True - always_overscroll: False - effect_cls: 'ScrollEffect' - StackLayout: - size_hint_y: None - height: self.minimum_height - padding: 10, 10 - MDLabel: - text: "Save settings:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: save_settings - padding: 5, 10 - size_hint_y: None - height: '100dp' - orientation: "vertical" - BoxLayout: - orientation: "horizontal" - MDSwitch: - id: include_secret_key_switch - active: False - size_hint_y: 1 - ScalingLabel: - size_hint_y: 1 - padding: 25, 0 - text: "Include secret key" - MDRaisedButton: - text: "Save settings" - on_press: app.save_settings() - MDSeparator: - MDLabel: - text: "Cache settings:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: cache_settings - size_hint_y: None - height: '280dp' - orientation: "vertical" - BoxLayout: - orientation: "vertical" - ScalingLabel: - size_hint_y: 1 - text: "The known content cache contains the IDs of all previously retrieved content. Known content is skipped when encountered." - MDRaisedButton: - id: clear_known_content - text: "Clear known content" - on_press: app.clear_known_content() - BoxLayout: - orientation: "vertical" - ScalingLabel: - size_hint_y: 1 - text: 'The last run times cache contains the last run time of each content type. On the next run content is retrieved started from the last run time. If there is no last run time, the "Period to retrieve" setting is used' - MDRaisedButton: - id: clear_last_run_times - text: "Clear last run times" - on_press: app.clear_last_run_times() - MDSeparator: - MDLabel: - text: "Time settings:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: time_settings - padding: 5, 10 - size_hint_y: None - height: '150dp' - orientation: "vertical" - BoxLayout: - orientation: "horizontal" - MDSwitch: - id: resume_switch - active: True - size_hint_y: 1 - ScalingLabel: - size_hint_y: 1 - padding: 25, 0 - text: "Resume from last run time" - BoxLayout: - ScalingLabel: - id: run_time_description - text: "Time between runs:" - size_hint_x: 0.4 - MDLabel: - id: run_time_label - text: str(run_time_slider.value) + " hours" - size_hint_x: 0.2 - padding: 1, 10 - MDSlider: - id: run_time_slider - hint: False - size_hint_x: 0.4 - step: 1 - min: 1 - max: 48 - value: 1 - BoxLayout: - ScalingLabel: - text: "Period to retrieve:" - size_hint_x: None - width: run_time_description.width - MDLabel: - id: collect_time_label - size_hint_x: None - width: run_time_label.width - text: str(collect_time_slider.value) + " hours" - padding: 1, 10 - MDSlider: - id: collect_time_slider - size_hint_x: None - width: run_time_slider.width - hint: False - step: 1 - min: 1 - max: 48 - value: 1 - MDSeparator: - MDLabel: - padding: 5, 5 - text: "File output:" - halign: "center" - size_hint_y: None - height: '20dp' - BoxLayout: - id: file_output - orientation: "vertical" - size_hint_y: None - height: "120dp" - MDSwitch: - id: file_output_switch - active: False - size_hint_y: 1 - MDTextField: - id: file_output_path - padding: 5, 25 - hint_text: "Output file path" - text: 'Output.txt' - MDLabel: - text: "Azure Log Analytics output:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: oms_output - size_hint_y: None - height: "140dp" - orientation: "vertical" - MDSwitch: - id: oms_output_switch - active: False - size_hint_y: 1 - MDTextField: - id: oms_id - padding: 5, 20 - hint_text: "Workspace ID" - text: app.guid_example - MDTextField: - id: oms_key - padding: 5, 25 - hint_text: "Workspace shared key" - text: app.guid_example - MDLabel: - text: "Graylog output:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: graylog_output - size_hint_y: None - height: "120dp" - orientation: "vertical" - MDSwitch: - id: graylog_output_switch - active: False - size_hint_y: 1 - StackLayout: - orientation: 'lr-tb' - MDTextField: - id: graylog_ip - size_hint_x: 0.5 - padding: 5, 1 - hint_text: "Graylog address" - text: '0.0.0.0' - MDTextField: - id: graylog_port - size_hint_x: 0.5 - padding: 5, 1 - hint_text: "Graylog port" - text: '5000' - MDLabel: - text: "Logging settings:" - halign: "center" - size_hint_y: None - height: '10dp' - BoxLayout: - id: logging - orientation: "vertical" - size_hint_y: None - height: "50dp" - MDSwitch: - id: log_switch - active: False - size_hint_y: 1 \ No newline at end of file diff --git a/Source/UX/ConfigWidget.py b/Source/UX/ConfigWidget.py deleted file mode 100644 index 6b6ce9a..0000000 --- a/Source/UX/ConfigWidget.py +++ /dev/null @@ -1,10 +0,0 @@ -from kivymd.uix import stacklayout, tab -from kivy.uix import scrollview -import os - - -class ConfigWidget(scrollview.ScrollView, tab.MDTabsBase): - - def __init__(self, **kwargs): - - super().__init__(**kwargs) diff --git a/Source/UX/ConnectionWidget.kv b/Source/UX/ConnectionWidget.kv deleted file mode 100644 index ad29efc..0000000 --- a/Source/UX/ConnectionWidget.kv +++ /dev/null @@ -1,36 +0,0 @@ -: - StackLayout: - padding: 5, 20 - orientation: "tb-lr" - StackLayout: - size_hint_y: 0.2 - orientation: 'tb-lr' - ScalingLabel: - text: "Log in to an Azure AD App Registration. The registered app must have the proper permissions, and your tenant must have audit logs enabled. Refer to the docs for more info." - MDTextField: - id: tenant_id - hint_text: "Tenant ID" - text: app.tenant_id - MDTextField: - id: client_key - hint_text: "Client key" - text: app.client_key - MDTextField: - id: secret_key - hint_text: "Secret key" - password_mask: "*" - password: True - text: app.secret_key - MDTextField: - id: publisher_id - hint_text: "Publisher ID" - text: app.publisher_id - MDRaisedButton: - id: login_button - text: "Login" - on_press: app.login(tenant_id.text, client_key.text, secret_key.text, publisher_id.text) - -: - text_size: root.width, None - size: self.texture_size - markup: True diff --git a/Source/UX/ConnectionWidget.py b/Source/UX/ConnectionWidget.py deleted file mode 100644 index fd0d77d..0000000 --- a/Source/UX/ConnectionWidget.py +++ /dev/null @@ -1,13 +0,0 @@ -from kivy.uix import boxlayout -from kivymd.uix import tab -from kivy.properties import ListProperty - - -class ConnectionWidget(tab.MDTabsBase, boxlayout.BoxLayout): - - status_color = ListProperty() - - def __init__(self, **kwargs): - - self.status_color = [1, 1, 0] - super().__init__(**kwargs) \ No newline at end of file diff --git a/Source/UX/MainWidget.kv b/Source/UX/MainWidget.kv deleted file mode 100644 index 73509f4..0000000 --- a/Source/UX/MainWidget.kv +++ /dev/null @@ -1,34 +0,0 @@ -: - orientation: "vertical" - TabWidget: - id: tab_widget - size_hint: 1, 0.9 - BoxLayout: - orientation: "vertical" - size_hint_y: 0.1 - MDSeparator: - size_hint: 1, 0.05 - ScalingLabel: - id: status_label - valign: "middle" - halign: "center" - text: "[color=#ffff00]Not logged in.[/color]" - -: - lock_swiping: True - ConnectionWidget: - id: connection_widget - title: "1. Connect" - SubscriberWidget: - id: subscriber_widget - title: "2. Subscribe" - ConfigWidget: - id: config_widget - title: "3. Configure" - CollectorWidget: - id: collector_widget - title: "4. Collect" - -: - text_size: root.width, None - size: self.texture_size diff --git a/Source/UX/MainWidget.py b/Source/UX/MainWidget.py deleted file mode 100644 index a13d6ca..0000000 --- a/Source/UX/MainWidget.py +++ /dev/null @@ -1,21 +0,0 @@ -from kivy.uix import boxlayout -from kivymd.uix import tab -from kivy.lang.builder import Builder -from . import ConfigWidget, ConnectionWidget, CollectorWidget, SubscriberWidget -import os - -root_dir = os.path.split(__file__)[0] -Builder.load_file(os.path.join(root_dir, 'ConnectionWidget.kv')) -Builder.load_file(os.path.join(root_dir, 'SubscriberWidget.kv')) -Builder.load_file(os.path.join(root_dir, 'ConfigWidget.kv')) -Builder.load_file(os.path.join(root_dir, 'CollectorWidget.kv')) - - -class MainWidget(boxlayout.BoxLayout): - - pass - - -class TabWidget(tab.MDTabs): - - pass diff --git a/Source/UX/SubscriberWidget.kv b/Source/UX/SubscriberWidget.kv deleted file mode 100644 index 94184da..0000000 --- a/Source/UX/SubscriberWidget.kv +++ /dev/null @@ -1,80 +0,0 @@ -: - orientation: "tb-lr" - StackLayout: - padding: 5, 5 - size_hint_y: 1/6 - ScalingLabel: - text: "In order to retrieve Audit Logs, you must subscribe your tenant to the relevant feeds. Enable the feeds you wish to retrieve below." - StackLayout: - padding: 5, 5 - orientation: "tb-lr" - size_hint_y: 1/6 - ScalingLabel: - text: "Audit.AzureActiveDirectory" - size_hint_y: 0.1 - MDSwitch: - valign: 'top' - active: False - size_hint_x: 0.1 - id: Audit.AzureActiveDirectory - on_release: root.on_switch_press(*args, name="Audit.AzureActiveDirectory") - disabled: True - StackLayout: - padding: 5, 5 - orientation: "tb-lr" - size_hint_y: 1/6 - ScalingLabel: - text: "Audit.General" - size_hint_y: 0.1 - MDSwitch: - valign: 'top' - active: False - size_hint_x: 0.1 - id: Audit.General - on_release: root.on_switch_press(*args, name="Audit.General") - disabled: True - StackLayout: - padding: 5, 5 - orientation: "tb-lr" - size_hint_y: 1/6 - ScalingLabel: - text: "Audit.Exchange" - size_hint_y: 0.1 - MDSwitch: - valign: 'top' - active: False - size_hint_x: 0.1 - id: Audit.Exchange - on_release: root.on_switch_press(*args, name="Audit.Exchange") - disabled: True - StackLayout: - padding: 5, 5 - orientation: "tb-lr" - size_hint_y: 1/6 - ScalingLabel: - text: "Audit.SharePoint" - size_hint_y: 0.1 - MDSwitch: - valign: 'top' - active: False - size_hint_x: 0.1 - id: Audit.SharePoint - on_release: root.on_switch_press(*args, name="Audit.Sharepoint") - disabled: True - StackLayout: - padding: 5, 5 - orientation: "tb-lr" - size_hint_y: 1/6 - ScalingLabel: - text: "DLP.All" - size_hint_y: 0.1 - MDSwitch: - valign: 'top' - active: False - size_hint_x: 0.1 - id: DLP.All - on_release: root.on_switch_press(*args, name="DLP.All") - disabled: True -: - text_size: root.width, None - size: self.texture_size diff --git a/Source/UX/SubscriberWidget.py b/Source/UX/SubscriberWidget.py deleted file mode 100644 index 0bd36dc..0000000 --- a/Source/UX/SubscriberWidget.py +++ /dev/null @@ -1,51 +0,0 @@ -from kivy.uix import stacklayout -from kivymd.uix import tab -from kivy.app import App - - -class SubscriberWidget(tab.MDTabsBase, stacklayout.StackLayout): - - def __init__(self, **kwargs): - - super().__init__(**kwargs) - - @property - def content_types(self): - - return ['Audit.AzureActiveDirectory', 'Audit.General', 'Audit.Exchange', 'Audit.SharePoint', 'DLP.All'] - - @property - def enabled_content_types(self): - - return [x for x in self.content_types if self.ids[x].active] - - def activate_switches(self): - - for content_type in self.content_types: - self.ids[content_type].disabled = False - - def deactivate_switches(self, reset_value=False): - - for content_type in self.content_types: - self.ids[content_type].disabled = True - if reset_value: - self.ids[content_type].active = False - - def on_switch_press(self, *args, name): - - App.get_running_app().subscriber.set_sub_status(content_type=name, action='start' if args[0].active else 'stop') - self.set_switches() - - def set_switches(self): - - status = App.get_running_app().subscriber.get_sub_status() - if status == '': - return App.get_running_app().disconnect() - disabled_content_types = self.content_types.copy() - for s in status: - if s['status'].lower() == 'enabled': - disabled_content_types.remove(s['contentType']) - for disabled_content_type in disabled_content_types: - self.ids[disabled_content_type].active = False - for enabled_content_type in [x for x in self.content_types if x not in disabled_content_types]: - self.ids[enabled_content_type].active = True \ No newline at end of file diff --git a/Source/requirements.txt b/Source/requirements.txt deleted file mode 100644 index 2b95d84..0000000 Binary files a/Source/requirements.txt and /dev/null differ diff --git a/Windows/WIN-OfficeAuditLogCollector-V1.5.exe b/Windows/WIN-OfficeAuditLogCollector-V2.0.exe similarity index 77% rename from Windows/WIN-OfficeAuditLogCollector-V1.5.exe rename to Windows/WIN-OfficeAuditLogCollector-V2.0.exe index ed8a154..0cf8b78 100644 Binary files a/Windows/WIN-OfficeAuditLogCollector-V1.5.exe and b/Windows/WIN-OfficeAuditLogCollector-V2.0.exe differ diff --git a/pip_requirements_linux.txt b/pip_requirements_linux.txt new file mode 100644 index 0000000..d743e7d Binary files /dev/null and b/pip_requirements_linux.txt differ diff --git a/pip_requirements_win.txt b/pip_requirements_win.txt new file mode 100644 index 0000000..83bd1d0 Binary files /dev/null and b/pip_requirements_win.txt differ