atomicshop 2.14.3__py3-none-any.whl → 2.14.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

atomicshop/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Atomic Basic functions and classes to make developer life easier"""
2
2
 
3
3
  __author__ = "Den Kras"
4
- __version__ = '2.14.3'
4
+ __version__ = '2.14.5'
atomicshop/datetimes.py CHANGED
@@ -47,7 +47,7 @@ class MonthToNumber:
47
47
  'דצמבר': '12'}
48
48
 
49
49
 
50
- def get_datetime_from_complex_string_by_pattern(complex_string: str, date_pattern: str):
50
+ def get_datetime_from_complex_string_by_pattern(complex_string: str, date_pattern: str) -> tuple[datetime, str, float]:
51
51
  """
52
52
  Function will get datetime object from a complex string by pattern.
53
53
 
@@ -65,7 +65,8 @@ def get_datetime_from_complex_string_by_pattern(complex_string: str, date_patter
65
65
  if date_str:
66
66
  # Convert the date string to a datetime object based on the given pattern
67
67
  date_obj = datetime.datetime.strptime(date_str.group(), date_pattern)
68
- return date_obj
68
+ date_timestamp = date_obj.timestamp()
69
+ return date_obj, date_str.group(), date_timestamp
69
70
  else:
70
71
  raise ValueError("No valid date found in the string")
71
72
 
@@ -1,5 +1,21 @@
1
+ from typing import Literal
2
+
1
3
  from ..wrappers.ctyping.etw_winapi import etw_functions
2
4
 
3
5
 
4
- def get_providers():
5
- return etw_functions.get_all_providers()
6
+ def get_providers(key_as: Literal['name', 'guid'] = 'name'):
7
+ return etw_functions.get_all_providers(key_as=key_as)
8
+
9
+
10
+ def get_provider_guid_by_name(provider_name):
11
+ providers = get_providers(key_as='name')
12
+
13
+ try:
14
+ provider_guid = providers[provider_name]
15
+ except KeyError:
16
+ provider_guid = None
17
+
18
+ if not provider_guid:
19
+ raise ValueError(f"Provider '{provider_name}' not found")
20
+
21
+ return provider_guid
@@ -1,7 +1,7 @@
1
1
  from ..wrappers.ctyping.etw_winapi import etw_functions
2
2
 
3
3
 
4
- def stop_and_delete(session_name) -> tuple[bool, int]:
4
+ def stop_and_delete(session_name: str) -> tuple[bool, int]:
5
5
  """
6
6
  Stop and delete ETW session.
7
7
 
atomicshop/etws/trace.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import queue
2
2
  import sys
3
3
  import time
4
- from typing import Literal
5
4
 
6
5
  # Import FireEye Event Tracing library.
7
6
  import etw
@@ -36,7 +36,7 @@ def read_csv_to_list_of_dicts_by_header(
36
36
  All the lines of the CSV file will be considered as content.
37
37
  :param file_object: file object of the 'open()' function in the decorator. Decorator executes the 'with open()'
38
38
  statement and passes to this function. That's why the default is 'None', since we get it from the decorator.
39
- :return: list.
39
+ :return: tuple(list of entries, header(list of cell names)).
40
40
  """
41
41
 
42
42
  # The header fields will be separated to list of "csv_reader.fieldnames".
@@ -83,8 +83,14 @@ class ModuleCategory:
83
83
 
84
84
  # Initiating logger for each engine by its name
85
85
  # initiate_logger(current_module.engine_name, log_file_extension)
86
- loggingw.get_logger_with_stream_handler_and_timedfilehandler(
87
- logger_name=self.engine_name, directory_path=logs_path, disable_duplicate_ms=True)
86
+ loggingw.get_complex_logger(
87
+ logger_name=self.engine_name,
88
+ directory_path=logs_path,
89
+ add_stream=True,
90
+ add_timedfile=True,
91
+ formatter_streamhandler='DEFAULT',
92
+ formatter_filehandler='DEFAULT'
93
+ )
88
94
 
89
95
 
90
96
  # Assigning external class object by message domain received from client. If the domain is not in the list,
@@ -41,8 +41,15 @@ def initialize_mitm_server(config_static):
41
41
  config['certificates']['sni_server_certificate_from_server_socket_download_directory'])
42
42
 
43
43
  # Create a logger that will log messages to file, Initiate System logger.
44
- system_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
45
- "system", config['log']['logs_path'], disable_duplicate_ms=True)
44
+ logger_name = "system"
45
+ system_logger = loggingw.get_complex_logger(
46
+ logger_name=logger_name,
47
+ file_path=f'{config['log']['logs_path']}{os.sep}{logger_name}.txt',
48
+ add_stream=True,
49
+ add_timedfile=True,
50
+ formatter_streamhandler='DEFAULT',
51
+ formatter_filehandler='DEFAULT'
52
+ )
46
53
 
47
54
  # Writing first log.
48
55
  system_logger.info("======================================")
@@ -175,14 +182,24 @@ def initialize_mitm_server(config_static):
175
182
  config_static.CONFIG_EXTENDED['certificates']['domains_all_times'] = list(domains_engine_list_full)
176
183
 
177
184
  # Creating Statistics logger.
178
- statistics_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
179
- logger_name="statistics", directory_path=config['log']['logs_path'],
180
- file_extension=config_static.CSV_EXTENSION, formatter_message_only=True, header=STATISTICS_HEADER
185
+ statistics_logger = loggingw.get_complex_logger(
186
+ logger_name="statistics",
187
+ directory_path=config['log']['logs_path'],
188
+ add_timedfile=True,
189
+ formatter_filehandler='MESSAGE',
190
+ file_type='csv',
191
+ header=STATISTICS_HEADER
181
192
  )
182
193
 
183
194
  network_logger_name = "network"
184
- network_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
185
- logger_name=network_logger_name, directory_path=config['log']['logs_path'], disable_duplicate_ms=True)
195
+ network_logger = loggingw.get_complex_logger(
196
+ logger_name=network_logger_name,
197
+ directory_path=config['log']['logs_path'],
198
+ add_stream=True,
199
+ add_timedfile=True,
200
+ formatter_streamhandler='DEFAULT',
201
+ formatter_filehandler='DEFAULT'
202
+ )
186
203
  system_logger.info(f"Loaded network logger: {network_logger}")
187
204
 
188
205
  # Initiate Listener logger, which is a child of network logger, so he uses the same settings and handlers
@@ -1,8 +1,12 @@
1
+ import os
1
2
  import datetime
3
+ import statistics
4
+ import json
5
+ from typing import Literal
2
6
 
3
7
  from .. import filesystem, domains, datetimes, urls
4
8
  from ..basics import dicts
5
- from ..file_io import tomls, xlsxs
9
+ from ..file_io import tomls, xlsxs, csvs, jsons
6
10
  from ..wrappers.loggingw import reading
7
11
  from ..print_api import print_api
8
12
 
@@ -154,10 +158,10 @@ def analyze(main_file_path: str):
154
158
  summary_path: str = filesystem.check_absolute_path___add_full(config['report_file_path'], script_directory)
155
159
 
156
160
  # Get the content from statistics files.
157
- statistics_content: list = reading.get_logs(
161
+ statistics_content: list = reading.get_all_log_files_into_list(
158
162
  config['statistic_files_path'],
159
163
  file_name_pattern='statistics*.csv',
160
- log_type='csv',
164
+ log_type='csv'
161
165
  )
162
166
 
163
167
  # Initialize loop.
@@ -465,3 +469,372 @@ def analyze(main_file_path: str):
465
469
  xlsxs.write_xlsx(combined_sorted_stats, file_path=summary_path)
466
470
 
467
471
  return
472
+
473
+
474
+ # ======================================================================================================================
475
+
476
+
477
+ def calculate_moving_average(
478
+ file_path: str,
479
+ moving_average_window_days,
480
+ top_bottom_deviation_percentage: float,
481
+ print_kwargs: dict = None
482
+ ):
483
+ """
484
+ This function calculates the moving average of the daily statistics.
485
+
486
+ :param file_path: string, the path to the 'statistics.csv' file.
487
+ :param moving_average_window_days: integer, the window size for the moving average.
488
+ :param top_bottom_deviation_percentage: float, the percentage of deviation from the moving average to the top or
489
+ bottom.
490
+ :param print_kwargs: dict, the print_api arguments.
491
+ """
492
+
493
+ date_pattern: str = '%Y_%m_%d'
494
+
495
+ # Get all the file paths and their midnight rotations.
496
+ logs_paths: list = reading.get_logs_paths(
497
+ log_file_path=file_path,
498
+ date_pattern=date_pattern
499
+ )
500
+
501
+ statistics_content: dict = {}
502
+ # Read each file to its day.
503
+ for log_path_dict in logs_paths:
504
+ date_string = log_path_dict['date_string']
505
+ statistics_content[date_string] = {}
506
+
507
+ statistics_content[date_string]['file'] = log_path_dict
508
+
509
+ log_file_content, log_file_header = (
510
+ csvs.read_csv_to_list_of_dicts_by_header(log_path_dict['file_path'], **(print_kwargs or {})))
511
+ statistics_content[date_string]['content'] = log_file_content
512
+ statistics_content[date_string]['header'] = log_file_header
513
+
514
+ statistics_content[date_string]['content_no_errors'] = get_content_without_errors(log_file_content)
515
+
516
+ # Get the data dictionary from the statistics content.
517
+ statistics_content[date_string]['statistics_daily'] = compute_statistics_from_content(
518
+ statistics_content[date_string]['content_no_errors']
519
+ )
520
+
521
+ moving_average_dict: dict = compute_moving_averages_from_average_statistics(
522
+ statistics_content,
523
+ moving_average_window_days
524
+ )
525
+
526
+ # Add the moving average to the statistics content.
527
+ for day, day_dict in statistics_content.items():
528
+ try:
529
+ day_dict['moving_average'] = moving_average_dict[day]
530
+ except KeyError:
531
+ day_dict['moving_average'] = {}
532
+
533
+ # Find deviation from the moving average to the bottom or top by specified percentage.
534
+ deviation_list: list = find_deviation_from_moving_average(
535
+ statistics_content, top_bottom_deviation_percentage)
536
+
537
+ return deviation_list
538
+
539
+
540
+ def get_content_without_errors(content: list) -> list:
541
+ """
542
+ This function gets the 'statistics.csv' file content without errors from the 'content' list.
543
+
544
+ :param content: list, the content list.
545
+ :return: list, the content without errors.
546
+ """
547
+
548
+ traffic_statistics_without_errors: list = []
549
+ for line in content:
550
+ # Skip empty lines, headers and errors.
551
+ if line['host'] == 'host' or line['command'] == '':
552
+ continue
553
+
554
+ traffic_statistics_without_errors.append(line)
555
+
556
+ return traffic_statistics_without_errors
557
+
558
+
559
+ def get_data_dict_from_statistics_content(content: list) -> dict:
560
+ """
561
+ This function gets the data dictionary from the 'statistics.csv' file content.
562
+
563
+ :param content: list, the content list.
564
+ :return: dict, the data dictionary.
565
+ """
566
+
567
+ hosts_requests_responses: dict = {}
568
+ for line in content:
569
+ # If subdomain is not in the dictionary, add it.
570
+ if line['host'] not in hosts_requests_responses:
571
+ hosts_requests_responses[line['host']] = {
572
+ 'request_sizes': [],
573
+ 'response_sizes': []
574
+ }
575
+
576
+ # Append the sizes.
577
+ try:
578
+ hosts_requests_responses[line['host']]['request_sizes'].append(int(line['request_size_bytes']))
579
+ hosts_requests_responses[line['host']]['response_sizes'].append(
580
+ int(line['response_size_bytes']))
581
+ except ValueError:
582
+ print_api(line, color='yellow')
583
+ raise
584
+
585
+ return hosts_requests_responses
586
+
587
+
588
+ def compute_statistics_from_data_dict(data_dict: dict):
589
+ """
590
+ This function computes the statistics from the data dictionary.
591
+
592
+ :param data_dict: dict, the data dictionary.
593
+ :return: dict, the statistics dictionary.
594
+ """
595
+
596
+ for host, host_dict in data_dict.items():
597
+ count = len(host_dict['request_sizes'])
598
+ avg_request_size = statistics.mean(host_dict['request_sizes']) if count > 0 else 0
599
+ median_request_size = statistics.median(host_dict['request_sizes']) if count > 0 else 0
600
+ avg_response_size = statistics.mean(host_dict['response_sizes']) if count > 0 else 0
601
+ median_response_size = statistics.median(host_dict['response_sizes']) if count > 0 else 0
602
+
603
+ data_dict[host]['count'] = count
604
+ data_dict[host]['avg_request_size'] = avg_request_size
605
+ data_dict[host]['median_request_size'] = median_request_size
606
+ data_dict[host]['avg_response_size'] = avg_response_size
607
+ data_dict[host]['median_response_size'] = median_response_size
608
+
609
+
610
+ def compute_statistics_from_content(content: list):
611
+ """
612
+ This function computes the statistics from the 'statistics.csv' file content.
613
+
614
+ :param content: list, the content list.
615
+ :return: dict, the statistics dictionary.
616
+ """
617
+
618
+ hosts_requests_responses: dict = get_data_dict_from_statistics_content(content)
619
+ compute_statistics_from_data_dict(hosts_requests_responses)
620
+
621
+ return hosts_requests_responses
622
+
623
+
624
+ def compute_moving_averages_from_average_statistics(
625
+ average_statistics_dict: dict,
626
+ moving_average_window_days: int
627
+ ):
628
+ """
629
+ This function computes the moving averages from the average statistics dictionary.
630
+
631
+ :param average_statistics_dict: dict, the average statistics dictionary.
632
+ :param moving_average_window_days: integer, the window size for the moving average.
633
+ :return: dict, the moving averages dictionary.
634
+ """
635
+
636
+ moving_average: dict = {}
637
+ for day_index, (day, day_dict) in enumerate(average_statistics_dict.items()):
638
+ current_day = day_index + 1
639
+ if current_day < moving_average_window_days:
640
+ continue
641
+
642
+ # Create list of the previous 'moving_average_window_days' days.
643
+ previous_days_content_list = (
644
+ list(average_statistics_dict.values()))[current_day-moving_average_window_days:current_day]
645
+
646
+ # Compute the moving averages.
647
+ moving_average[day] = compute_average_for_current_day_from_past_x_days(previous_days_content_list)
648
+
649
+ return moving_average
650
+
651
+
652
+ def compute_average_for_current_day_from_past_x_days(previous_days_content_list: list) -> dict:
653
+ """
654
+ This function computes the average for the current day from the past x days.
655
+
656
+ :param previous_days_content_list: list, the list of the previous days content.
657
+ :return: dict, the average dictionary.
658
+ """
659
+
660
+ moving_average: dict = {}
661
+ for entry in previous_days_content_list:
662
+ statistics_daily = entry['statistics_daily']
663
+ for host, host_dict in statistics_daily.items():
664
+ if host not in moving_average:
665
+ moving_average[host] = {
666
+ 'counts': [],
667
+ 'avg_request_sizes': [],
668
+ 'avg_response_sizes': [],
669
+ }
670
+
671
+ moving_average[host]['counts'].append(int(host_dict['count']))
672
+ moving_average[host]['avg_request_sizes'].append(float(host_dict['avg_request_size']))
673
+ moving_average[host]['avg_response_sizes'].append(float(host_dict['avg_response_size']))
674
+
675
+ # Compute the moving average.
676
+ moving_average_results: dict = {}
677
+ for host, host_dict in moving_average.items():
678
+ ma_count = statistics.mean(host_dict['counts'])
679
+ ma_request_size = statistics.mean(host_dict['avg_request_sizes'])
680
+ ma_response_size = statistics.mean(host_dict['avg_response_sizes'])
681
+
682
+ moving_average_results[host] = {
683
+ 'ma_count': ma_count,
684
+ 'ma_request_size': ma_request_size,
685
+ 'ma_response_size': ma_response_size,
686
+ 'counts': host_dict['counts'],
687
+ 'avg_request_sizes': host_dict['avg_request_sizes'],
688
+ 'avg_response_sizes': host_dict['avg_response_sizes']
689
+ }
690
+
691
+ return moving_average_results
692
+
693
+
694
+ def find_deviation_from_moving_average(
695
+ statistics_content: dict,
696
+ top_bottom_deviation_percentage: float
697
+ ) -> list:
698
+ """
699
+ This function finds the deviation from the moving average to the bottom or top by specified percentage.
700
+
701
+ :param statistics_content: dict, the statistics content dictionary.
702
+ :param top_bottom_deviation_percentage: float, the percentage of deviation from the moving average to the top or
703
+ bottom.
704
+ :return: list, the deviation list.
705
+ """
706
+
707
+ def _check_deviation(
708
+ check_type: Literal['count', 'avg_request_size', 'avg_response_size'],
709
+ ma_check_type: Literal['ma_count', 'ma_request_size', 'ma_response_size'],
710
+ day_statistics_content_dict: dict,
711
+ moving_averages_dict: dict
712
+ ):
713
+ """
714
+ This function checks the deviation for the host.
715
+ """
716
+
717
+ nonlocal message
718
+
719
+ host_moving_average_by_type = moving_averages_dict[host][ma_check_type]
720
+ check_type_moving_by_percent = (
721
+ host_moving_average_by_type * top_bottom_deviation_percentage)
722
+ check_type_moving_above = host_moving_average_by_type + check_type_moving_by_percent
723
+ check_type_moving_below = host_moving_average_by_type - check_type_moving_by_percent
724
+
725
+ deviation_type = None
726
+ if day_statistics_content_dict[check_type] > check_type_moving_above:
727
+ deviation_type = 'above'
728
+ elif day_statistics_content_dict[check_type] < check_type_moving_below:
729
+ deviation_type = 'below'
730
+
731
+ if deviation_type:
732
+ message = f'[{check_type}] is [{deviation_type}] the moving average.'
733
+ deviation_list.append({
734
+ 'day': day,
735
+ 'host': host,
736
+ 'message': message,
737
+ 'value': day_statistics_content_dict[check_type],
738
+ 'ma_value': host_moving_average_by_type,
739
+ 'check_type': check_type,
740
+ 'percentage': top_bottom_deviation_percentage,
741
+ 'ma_value_checked': check_type_moving_above,
742
+ 'deviation_type': deviation_type,
743
+ 'data': day_statistics_content_dict,
744
+ 'ma_data': moving_averages_dict[host]
745
+ })
746
+
747
+ deviation_list: list = []
748
+ for day_index, (day, day_dict) in enumerate(statistics_content.items()):
749
+ # If it's the first day, there is no previous day moving average.
750
+ if day_index == 0:
751
+ previous_day_moving_average_dict = {}
752
+ else:
753
+ previous_day_moving_average_dict = list(statistics_content.values())[day_index-1].get('moving_average', {})
754
+
755
+ # If there is no moving average for previous day continue to the next day.
756
+ if not previous_day_moving_average_dict:
757
+ continue
758
+
759
+ for host, host_dict in day_dict['statistics_daily'].items():
760
+ # If the host is not in the moving averages, then this is clear deviation.
761
+ # It means that in the current day, there were no requests for this host.
762
+ if host not in previous_day_moving_average_dict:
763
+ message = f'Host not in the moving averages: {host}'
764
+ deviation_list.append({
765
+ 'day': day,
766
+ 'host': host,
767
+ 'data': host_dict,
768
+ 'message': message,
769
+ 'type': 'clear'
770
+ })
771
+ continue
772
+
773
+ _check_deviation(
774
+ 'count', 'ma_count', host_dict, previous_day_moving_average_dict)
775
+ _check_deviation(
776
+ 'avg_request_size', 'ma_request_size', host_dict, previous_day_moving_average_dict)
777
+ _check_deviation(
778
+ 'avg_response_size', 'ma_response_size', host_dict, previous_day_moving_average_dict)
779
+
780
+ return deviation_list
781
+
782
+
783
+ def moving_average_calculator_main(
784
+ statistics_file_path: str,
785
+ output_directory: str,
786
+ moving_average_window_days: int,
787
+ top_bottom_deviation_percentage: float
788
+ ) -> int:
789
+ """
790
+ This function is the main function for the moving average calculator.
791
+
792
+ :param statistics_file_path: string, the statistics file path.
793
+ :param output_directory: string, the output directory.
794
+ :param moving_average_window_days: integer, the moving average window days.
795
+ :param top_bottom_deviation_percentage: float, the top bottom deviation percentage. Example: 0.1 for 10%.
796
+ :return: integer, the return code.
797
+ -----------------------------
798
+
799
+ Example:
800
+ import sys
801
+ from atomicshop.mitm import statistic_analyzer
802
+
803
+
804
+ def main():
805
+ return statistic_analyzer.moving_average_calculator_main(
806
+ statistics_file_path='statistics.csv',
807
+ output_directory='output',
808
+ moving_average_window_days=7,
809
+ top_bottom_deviation_percentage=0.1
810
+ )
811
+
812
+
813
+ if __name__ == '__main__':
814
+ sys.exit(main())
815
+ """
816
+
817
+ def convert_data_value_to_string(value_key: str, list_index: int) -> None:
818
+ deviation_list[list_index]['data'][value_key] = json.dumps(deviation_list[list_index]['data'][value_key])
819
+
820
+ def convert_value_to_string(value_key: str, list_index: int) -> None:
821
+ if value_key in deviation_list[list_index]:
822
+ deviation_list[list_index][value_key] = json.dumps(deviation_list[list_index][value_key])
823
+
824
+ deviation_list = calculate_moving_average(
825
+ statistics_file_path,
826
+ moving_average_window_days,
827
+ top_bottom_deviation_percentage
828
+ )
829
+
830
+ if deviation_list:
831
+ for deviation_list_index, deviation in enumerate(deviation_list):
832
+ convert_data_value_to_string('request_sizes', deviation_list_index)
833
+ convert_data_value_to_string('response_sizes', deviation_list_index)
834
+ convert_value_to_string('ma_data', deviation_list_index)
835
+
836
+ file_path = output_directory + os.sep + 'deviation.json'
837
+ print_api(f'Deviation Found, saving to file: {file_path}', color='blue')
838
+ jsons.write_json_file(deviation_list, file_path, use_default_indent=True)
839
+
840
+ return 0