contentctl 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. contentctl/Res.yml +102 -0
  2. contentctl/__init__.py +1 -0
  3. contentctl/actions/deploy.py +147 -0
  4. contentctl/actions/detection_testing/DataManipulation.py +149 -0
  5. contentctl/actions/detection_testing/DetectionTestingManager.py +189 -0
  6. contentctl/actions/detection_testing/GitHubService.py +405 -0
  7. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +65 -0
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +770 -0
  9. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +122 -0
  10. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +14 -0
  11. contentctl/actions/detection_testing/views/DetectionTestingView.py +122 -0
  12. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +73 -0
  13. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +52 -0
  14. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +157 -0
  15. contentctl/actions/doc_gen.py +26 -0
  16. contentctl/actions/generate.py +56 -0
  17. contentctl/actions/initialize.py +52 -0
  18. contentctl/actions/initialize_old.py +245 -0
  19. contentctl/actions/new_content.py +23 -0
  20. contentctl/actions/reporting.py +31 -0
  21. contentctl/actions/test.py +98 -0
  22. contentctl/actions/validate.py +86 -0
  23. contentctl/contentctl.py +374 -0
  24. contentctl/enrichments/attack_enrichment.py +87 -0
  25. contentctl/enrichments/cve_enrichment.py +24 -0
  26. contentctl/enrichments/splunk_app_enrichment.py +46 -0
  27. contentctl/helper/config_handler.py +30 -0
  28. contentctl/helper/link_validator.py +32 -0
  29. contentctl/helper/logger.py +0 -0
  30. contentctl/helper/utils.py +418 -0
  31. contentctl/input/baseline_builder.py +55 -0
  32. contentctl/input/basic_builder.py +58 -0
  33. contentctl/input/detection_builder.py +233 -0
  34. contentctl/input/director.py +265 -0
  35. contentctl/input/investigation_builder.py +42 -0
  36. contentctl/input/new_content_generator.py +81 -0
  37. contentctl/input/new_content_questions.py +142 -0
  38. contentctl/input/playbook_builder.py +67 -0
  39. contentctl/input/story_builder.py +106 -0
  40. contentctl/input/yml_reader.py +34 -0
  41. contentctl/objects/app.py +217 -0
  42. contentctl/objects/baseline.py +89 -0
  43. contentctl/objects/baseline_tags.py +25 -0
  44. contentctl/objects/config.py +107 -0
  45. contentctl/objects/constants.py +98 -0
  46. contentctl/objects/deployment.py +60 -0
  47. contentctl/objects/deployment_email.py +8 -0
  48. contentctl/objects/deployment_notable.py +8 -0
  49. contentctl/objects/deployment_phantom.py +10 -0
  50. contentctl/objects/deployment_rba.py +7 -0
  51. contentctl/objects/deployment_scheduling.py +10 -0
  52. contentctl/objects/deployment_slack.py +7 -0
  53. contentctl/objects/detection.py +186 -0
  54. contentctl/objects/detection_tags.py +121 -0
  55. contentctl/objects/enums.py +85 -0
  56. contentctl/objects/investigation.py +86 -0
  57. contentctl/objects/investigation_tags.py +9 -0
  58. contentctl/objects/lookup.py +16 -0
  59. contentctl/objects/macro.py +13 -0
  60. contentctl/objects/mitre_attack_enrichment.py +8 -0
  61. contentctl/objects/playbook.py +27 -0
  62. contentctl/objects/playbook_tags.py +13 -0
  63. contentctl/objects/repo_config.py +163 -0
  64. contentctl/objects/security_content_object.py +7 -0
  65. contentctl/objects/story.py +62 -0
  66. contentctl/objects/story_tags.py +32 -0
  67. contentctl/objects/test_config.py +541 -0
  68. contentctl/objects/unit_test.py +12 -0
  69. contentctl/objects/unit_test_attack_data.py +22 -0
  70. contentctl/objects/unit_test_baseline.py +11 -0
  71. contentctl/objects/unit_test_result.py +219 -0
  72. contentctl/objects/unit_test_test.py +18 -0
  73. contentctl/output/api_json_output.py +82 -0
  74. contentctl/output/attack_nav_output.py +35 -0
  75. contentctl/output/attack_nav_writer.py +75 -0
  76. contentctl/output/ba_yml_output.py +105 -0
  77. contentctl/output/conf_output.py +121 -0
  78. contentctl/output/conf_writer.py +65 -0
  79. contentctl/output/doc_md_output.py +70 -0
  80. contentctl/output/finding_report_writer.py +74 -0
  81. contentctl/output/jinja_writer.py +33 -0
  82. contentctl/output/json_writer.py +10 -0
  83. contentctl/output/new_content_yml_output.py +79 -0
  84. contentctl/output/svg_output.py +32 -0
  85. contentctl/output/templates/analyticstories_detections.j2 +22 -0
  86. contentctl/output/templates/analyticstories_investigations.j2 +21 -0
  87. contentctl/output/templates/analyticstories_stories.j2 +21 -0
  88. contentctl/output/templates/collections.j2 +8 -0
  89. contentctl/output/templates/detection_count.j2 +18 -0
  90. contentctl/output/templates/detection_coverage.j2 +18 -0
  91. contentctl/output/templates/doc_detection_page.j2 +19 -0
  92. contentctl/output/templates/doc_detections.j2 +210 -0
  93. contentctl/output/templates/doc_navigation.j2 +48 -0
  94. contentctl/output/templates/doc_navigation_pages.j2 +9 -0
  95. contentctl/output/templates/doc_playbooks.j2 +58 -0
  96. contentctl/output/templates/doc_playbooks_page.j2 +19 -0
  97. contentctl/output/templates/doc_stories.j2 +51 -0
  98. contentctl/output/templates/doc_story_page.j2 +19 -0
  99. contentctl/output/templates/es_investigations_investigations.j2 +38 -0
  100. contentctl/output/templates/es_investigations_stories.j2 +14 -0
  101. contentctl/output/templates/finding_report.j2 +11 -0
  102. contentctl/output/templates/header.j2 +7 -0
  103. contentctl/output/templates/macros.j2 +15 -0
  104. contentctl/output/templates/macros_detections.j2 +7 -0
  105. contentctl/output/templates/panel.j2 +11 -0
  106. contentctl/output/templates/savedsearches_baselines.j2 +49 -0
  107. contentctl/output/templates/savedsearches_detections.j2 +112 -0
  108. contentctl/output/templates/savedsearches_investigations.j2 +38 -0
  109. contentctl/output/templates/transforms.j2 +40 -0
  110. contentctl/output/templates/workflow_actions.j2 +18 -0
  111. contentctl/output/yml_writer.py +11 -0
  112. contentctl/templates/app_default.yml +101 -0
  113. contentctl/templates/contentctl_default.yml +32 -0
  114. contentctl/templates/datamodels_cim.conf +300 -0
  115. contentctl/templates/datamodels_custom.conf +15 -0
  116. contentctl/templates/detections/anomalous_usage_of_7zip.yml +85 -0
  117. contentctl/templates/macros/security_content_ctime.yml +5 -0
  118. contentctl/templates/macros/security_content_summariesonly.yml +3 -0
  119. contentctl/templates/splunk_app/metadata/default.meta +6 -0
  120. contentctl/templates/stories/cobalt_strike.yml +52 -0
  121. contentctl/templates/tests/anomalous_usage_of_7zip.test.yml +12 -0
  122. contentctl-1.0.0.dist-info/LICENSE.md +201 -0
  123. contentctl-1.0.0.dist-info/METADATA +223 -0
  124. contentctl-1.0.0.dist-info/RECORD +126 -0
  125. contentctl-1.0.0.dist-info/WHEEL +4 -0
  126. contentctl-1.0.0.dist-info/entry_points.txt +3 -0
contentctl/Res.yml ADDED
@@ -0,0 +1,102 @@
1
+ repo_path: /tmp/forDev/security_content
2
+ main_branch: "develop"
3
+ test_branch: "develop"
4
+ apps:
5
+ - uid: 6176
6
+ appid: "Splunk_TA_linux_sysmon"
7
+ title: "Add-on for Linux Sysmon"
8
+ release: "1.0.4"
9
+
10
+ - uid: 2757
11
+ appid: "Splunk_TA_paloalto"
12
+ title: "Palo Alto Networks Add-on for Splunk"
13
+ release: "7.1.0"
14
+
15
+ - uid: 2882
16
+ appid: "Splunk_SA_Scientific_Python_linux_x86_64"
17
+ title: "Python for Scientific Computing (for Linux 64-bit)"
18
+ release: "4.0.0"
19
+
20
+ - uid: 3719
21
+ appid: "Splunk_TA_aws-kinesis-firehose"
22
+ title: "Splunk Add-on for Amazon Kinesis Firehose"
23
+ release: "1.3.2"
24
+
25
+ - uid: 4055
26
+ appid: "splunk_ta_o365"
27
+ title: "Splunk Add-on for Microsoft Office 365"
28
+ release: "4.1.0"
29
+
30
+ - uid: 5742
31
+ appid: "mitre_attck_heatmap"
32
+ title: "MITRE ATTCK Heatmap for Splunk"
33
+ release: "1.4.0"
34
+
35
+ - uid: 742
36
+ appid: "Splunk_TA_windows"
37
+ title: "Splunk Add-on for Microsoft Windows"
38
+ release: "8.5.0"
39
+
40
+ - uid: 3258
41
+ appid: "Splunk_TA_nginx"
42
+ title: "Splunk Add-on for NGINX"
43
+ release: "3.2.0"
44
+
45
+ - uid: 5238
46
+ appid: "Splunk_TA_stream"
47
+ title: "Splunk Add-on for Stream Forwarders"
48
+ release: "8.1.0"
49
+
50
+ - uid: 5234
51
+ appid: "Splunk_TA_stream_wire_data"
52
+ title: "Splunk Add-on for Stream Wire Data"
53
+ release: "8.1.0"
54
+
55
+ - uid: 5709
56
+ appid: "Splunk_TA_microsoft_sysmon"
57
+ title: "Splunk Add-on for Sysmon"
58
+ release: "3.0.0"
59
+
60
+ - uid: 833
61
+ appid: "Splunk_TA_nix"
62
+ title: "Splunk Add-on for Unix and Linux"
63
+ release: "8.7.0"
64
+
65
+ - uid: 1809
66
+ appid: "splunk_app_stream"
67
+ title: "Splunk App for Stream"
68
+ release: "8.1.0"
69
+
70
+ - uid: 1621
71
+ appid: "Splunk_SA_CIM"
72
+ title: "Splunk Common Information Model (CIM)"
73
+ release: "5.0.2"
74
+
75
+ - uid: 2890
76
+ appid: "Splunk_ML_Toolkit"
77
+ title: "Splunk Machine Learning Toolkit"
78
+ release: "5.3.3"
79
+
80
+ - uid: 5466
81
+ appid: "Splunk_TA_zeek"
82
+ title: "TA for Zeek"
83
+ release: "1.0.5"
84
+
85
+ - uid: 3110
86
+ appid: "Splunk_TA_microsoft-cloudservices"
87
+ title: "Splunk Add-on for Microsoft Cloud Services"
88
+ release: "4.5.0"
89
+
90
+ - uid: 2734
91
+ appid: "utbox"
92
+ title: "URL Toolbox"
93
+ release: "1.9.2"
94
+
95
+ - uid: 3449
96
+ appid: "DA-ESS-ContentUpdate"
97
+ title: "Splunk ES Content Update"
98
+ release: "3.51.0"
99
+
100
+
101
+ splunkbase_username: notvalid
102
+ splunkbase_password: notvalid
contentctl/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0'
@@ -0,0 +1,147 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import requests
5
+ from requests.auth import HTTPBasicAuth
6
+
7
+ from dataclasses import dataclass
8
+ from configparser import RawConfigParser
9
+ import splunklib.client as client
10
+
11
+ from contentctl.objects.config import Config
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DeployInputDto:
16
+ path: str
17
+ config: Config
18
+
19
+
20
+ class Deploy:
21
+ def fix_newlines_in_conf_files(self, conf_path: str) -> RawConfigParser:
22
+ parser = RawConfigParser()
23
+ with open(conf_path, "r") as conf_data_file:
24
+ conf_data = conf_data_file.read()
25
+
26
+ # ConfigParser cannot read multipleline strings that simply escape the newline character with \
27
+ # To include a newline, you need to include a space at the beginning of the newline.
28
+ # We will simply replace all \NEWLINE with NEWLINESPACE (removing the leading literal \).
29
+ # We will discuss whether we intend to make these changes to the underlying conf files
30
+ # or just apply the changes here
31
+ conf_data = conf_data.replace("\\\n", "\n ")
32
+
33
+ parser.read_string(conf_data)
34
+ return parser
35
+
36
+ def execute(self, input_dto: DeployInputDto) -> None:
37
+
38
+ splunk_args = {
39
+ "host": input_dto.config.deploy.server,
40
+ "port": 8089,
41
+ "username": input_dto.config.deploy.username,
42
+ "password": input_dto.config.deploy.password,
43
+ "owner": "nobody",
44
+ "app": input_dto.config.deploy.app,
45
+ }
46
+ service = client.connect(**splunk_args)
47
+
48
+ macros_parser = self.fix_newlines_in_conf_files(
49
+ os.path.join(
50
+ input_dto.path,
51
+ input_dto.config.build.splunk_app.path,
52
+ "default",
53
+ "macros.conf",
54
+ )
55
+ )
56
+ import tqdm
57
+
58
+ bar_format_macros = (
59
+ f"Deploying macros "
60
+ + "{percentage:3.0f}%[{bar:20}]"
61
+ + "[{n_fmt}/{total_fmt} | ETA: {remaining}]"
62
+ )
63
+ bar_format_detections = (
64
+ f"Deploying saved searches"
65
+ + "{percentage:3.0f}%[{bar:20}]"
66
+ + "[{n_fmt}/{total_fmt} | ETA: {remaining}]"
67
+ )
68
+ for section in tqdm.tqdm(
69
+ macros_parser.sections(), bar_format=bar_format_macros
70
+ ):
71
+ try:
72
+ service.post("properties/macros", __stanza=section)
73
+ service.post("properties/macros/" + section, **macros_parser[section])
74
+ # print("Deployed macro: " + section)
75
+ except Exception as e:
76
+ tqdm.tqdm.write(f"Error deploying macro {section}: {str(e)}")
77
+
78
+ detection_parser = RawConfigParser()
79
+ detection_parser = self.fix_newlines_in_conf_files(
80
+ os.path.join(
81
+ input_dto.path,
82
+ input_dto.config.build.splunk_app.path,
83
+ "default",
84
+ "savedsearches.conf",
85
+ )
86
+ )
87
+ try:
88
+ service.delete("saved/searches/MSCA - Anomalous usage of 7zip - Rule")
89
+ except Exception as e:
90
+ pass
91
+
92
+ for section in tqdm.tqdm(
93
+ detection_parser.sections(), bar_format=bar_format_detections
94
+ ):
95
+ try:
96
+ if section.startswith(input_dto.config.build.splunk_app.prefix):
97
+ params = detection_parser[section]
98
+ params["name"] = section
99
+ response_actions = []
100
+ if (
101
+ input_dto.config.detection_configuration.notable
102
+ and input_dto.config.detection_configuration.notable.rule_description
103
+ ):
104
+ response_actions.append("notable")
105
+ if (
106
+ input_dto.config.detection_configuration.rba
107
+ and input_dto.config.detection_configuration.rba.enabled
108
+ ):
109
+ response_actions.append("risk")
110
+ params["actions"] = ",".join(response_actions)
111
+ params["request.ui_dispatch_app"] = "ES Content Updates"
112
+ params["request.ui_dispatch_view"] = "ES Content Updates"
113
+ params["alert_type"] = params.pop("counttype")
114
+ params["alert_comparator"] = params.pop("relation")
115
+ params["alert_threshold"] = params.pop("quantity")
116
+ params.pop("enablesched")
117
+
118
+ service.post("saved/searches", **params)
119
+
120
+ # print("Deployed detection: " + params["name"])
121
+ except Exception as e:
122
+ tqdm.tqdm.write(f"Error deploying saved search {section}: {str(e)}")
123
+
124
+ # story_parser = RawConfigParser()
125
+ # story_parser.read(os.path.join(input_dto.path, input_dto.config.build.splunk_app.path, "default", "analyticstories.conf"))
126
+
127
+ # for section in story_parser.sections():
128
+ # if section.startswith("analytic_story"):
129
+ # params = story_parser[section]
130
+ # params = dict(params.items())
131
+ # params["spec_version"] = 1
132
+ # params["version"] = 1
133
+ # name = section[17:]
134
+ # #service.post('services/analyticstories/configs/analytic_story', name=name, content=json.dumps(params))
135
+
136
+ # url = "https://3.72.220.157:8089/services/analyticstories/configs/analytic_story"
137
+ # data = dict()
138
+ # data["name"] = name
139
+ # data["content"] = params
140
+ # print(json.dumps(data))
141
+ # response = requests.post(
142
+ # url,
143
+ # auth=HTTPBasicAuth('admin', 'fgWFshd0mm7eErMj9qX'),
144
+ # data=json.dumps(data),
145
+ # verify=False
146
+ # )
147
+ # print(response.text)
@@ -0,0 +1,149 @@
1
+ import json
2
+ from datetime import datetime
3
+ from datetime import timedelta
4
+ #import fileinput
5
+ import os
6
+ import re
7
+ import io
8
+
9
+ class DataManipulation:
10
+
11
+ def manipulate_timestamp(self, file_path, sourcetype, source):
12
+
13
+
14
+ #print('Updating timestamps in attack_data before replaying')
15
+ if sourcetype == 'aws:cloudtrail':
16
+ self.manipulate_timestamp_cloudtrail(file_path)
17
+
18
+ if source == 'WinEventLog:System' or source == 'WinEventLog:Security':
19
+ self.manipulate_timestamp_windows_event_log_raw(file_path)
20
+
21
+ if source == 'exchange':
22
+ self.manipulate_timestamp_exchange_logs(file_path)
23
+
24
+
25
+ def manipulate_timestamp_exchange_logs(self, path):
26
+ f = io.open(path, "r", encoding="utf-8")
27
+
28
+ first_line = f.readline()
29
+ d = json.loads(first_line)
30
+ latest_event = datetime.strptime(d["CreationTime"],"%Y-%m-%dT%H:%M:%S")
31
+
32
+ now = datetime.now()
33
+ now = now.strftime("%Y-%m-%dT%H:%M:%S")
34
+ now = datetime.strptime(now,"%Y-%m-%dT%H:%M:%S")
35
+
36
+ difference = now - latest_event
37
+ f.close()
38
+
39
+ #Mimic the behavior of fileinput but in a threadsafe way
40
+ #Rename the file, which fileinput does for inplace.
41
+ #Note that path will now be the new file
42
+ original_backup_file = f"{path}.bak"
43
+ os.rename(path, original_backup_file)
44
+
45
+ with open(original_backup_file, "r") as original_file:
46
+ with open(path, "w") as new_file:
47
+ for line in original_file:
48
+ d = json.loads(line)
49
+ original_time = datetime.strptime(d["CreationTime"],"%Y-%m-%dT%H:%M:%S")
50
+ new_time = (difference + original_time)
51
+
52
+ original_time = original_time.strftime("%Y-%m-%dT%H:%M:%S")
53
+ new_time = new_time.strftime("%Y-%m-%dT%H:%M:%S")
54
+ #There is no end character appended, no need for end=''
55
+ new_file.write(line.replace(original_time, new_time))
56
+
57
+
58
+ os.remove(original_backup_file)
59
+
60
+ def manipulate_timestamp_windows_event_log_raw(self, path):
61
+
62
+ f = io.open(path, "r", encoding="utf-8")
63
+ self.now = datetime.now()
64
+ self.now = self.now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
65
+ self.now = datetime.strptime(self.now,"%Y-%m-%dT%H:%M:%S.%fZ")
66
+
67
+ # read raw logs
68
+ regex = r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2} [AP]M'
69
+ data = f.read()
70
+ lst_matches = re.findall(regex, data)
71
+ if len(lst_matches) > 0:
72
+ latest_event = datetime.strptime(lst_matches[-1],"%m/%d/%Y %I:%M:%S %p")
73
+ self.difference = self.now - latest_event
74
+ f.close()
75
+
76
+ result = re.sub(regex, self.replacement_function, data)
77
+
78
+ with io.open(path, "w+", encoding='utf8') as f:
79
+ f.write(result)
80
+ else:
81
+ f.close()
82
+ return
83
+
84
+
85
+ def replacement_function(self, match):
86
+ try:
87
+ event_time = datetime.strptime(match.group(),"%m/%d/%Y %I:%M:%S %p")
88
+ new_time = self.difference + event_time
89
+ return new_time.strftime("%m/%d/%Y %I:%M:%S %p")
90
+ except Exception as e:
91
+ self.logger.error("Error in timestamp replacement occured: " + str(e))
92
+ return match.group()
93
+
94
+
95
+ def manipulate_timestamp_cloudtrail(self, path):
96
+
97
+
98
+ f = io.open(path, "r", encoding="utf-8")
99
+
100
+ try:
101
+ first_line = f.readline()
102
+ d = json.loads(first_line)
103
+ latest_event = datetime.strptime(d["eventTime"],"%Y-%m-%dT%H:%M:%S.%fZ")
104
+
105
+ now = datetime.now()
106
+ now = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
107
+ now = datetime.strptime(now,"%Y-%m-%dT%H:%M:%S.%fZ")
108
+ except ValueError:
109
+ first_line = f.readline()
110
+ d = json.loads(first_line)
111
+ latest_event = datetime.strptime(d["eventTime"],"%Y-%m-%dT%H:%M:%SZ")
112
+
113
+ now = datetime.now()
114
+ now = now.strftime("%Y-%m-%dT%H:%M:%SZ")
115
+ now = datetime.strptime(now,"%Y-%m-%dT%H:%M:%SZ")
116
+
117
+ difference = now - latest_event
118
+ f.close()
119
+
120
+
121
+
122
+ #Mimic the behavior of fileinput but in a threadsafe way
123
+ #Rename the file, which fileinput does for inplace.
124
+ #Note that path will now be the new file
125
+ original_backup_file = f"{path}.bak"
126
+ os.rename(path, original_backup_file)
127
+
128
+ with open(original_backup_file, "r") as original_file:
129
+ with open(path, "w") as new_file:
130
+ for line in original_file:
131
+ try:
132
+ d = json.loads(line)
133
+ original_time = datetime.strptime(d["eventTime"],"%Y-%m-%dT%H:%M:%S.%fZ")
134
+ new_time = (difference + original_time)
135
+
136
+ original_time = original_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
137
+ new_time = new_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
138
+ new_file.write(line.replace(original_time, new_time))
139
+ except ValueError:
140
+ d = json.loads(line)
141
+ original_time = datetime.strptime(d["eventTime"],"%Y-%m-%dT%H:%M:%SZ")
142
+ new_time = (difference + original_time)
143
+
144
+ original_time = original_time.strftime("%Y-%m-%dT%H:%M:%SZ")
145
+ new_time = new_time.strftime("%Y-%m-%dT%H:%M:%SZ")
146
+ new_file.write(line.replace(original_time, new_time))
147
+
148
+
149
+ os.remove(original_backup_file)
@@ -0,0 +1,189 @@
1
+ from contentctl.objects.test_config import TestConfig
2
+ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
3
+ DetectionTestingInfrastructure,
4
+ )
5
+ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureContainer import (
6
+ DetectionTestingInfrastructureContainer,
7
+ )
8
+ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureServer import (
9
+ DetectionTestingInfrastructureServer,
10
+ )
11
+
12
+ from contentctl.objects.app import App
13
+ import pathlib
14
+ import os
15
+ from contentctl.helper.utils import Utils
16
+ from urllib.parse import urlparse
17
+ import time
18
+ from copy import deepcopy
19
+ from contentctl.objects.enums import DetectionTestingTargetInfrastructure
20
+ import signal
21
+ import datetime
22
+
23
+ # from queue import Queue
24
+
25
+ CONTAINER_APP_PATH = pathlib.Path("apps")
26
+
27
+ from dataclasses import dataclass
28
+
29
+ # import threading
30
+ import ctypes
31
+ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
32
+ DetectionTestingInfrastructure,
33
+ DetectionTestingManagerOutputDto,
34
+ )
35
+ from contentctl.actions.detection_testing.views.DetectionTestingView import (
36
+ DetectionTestingView,
37
+ )
38
+
39
+ from contentctl.objects.enums import PostTestBehavior
40
+
41
+ from pydantic import BaseModel, Field
42
+ from contentctl.input.director import DirectorOutputDto
43
+ from contentctl.objects.detection import Detection
44
+
45
+
46
+ import concurrent.futures
47
+
48
+ import tqdm
49
+
50
+
51
+ @dataclass(frozen=False)
52
+ class DetectionTestingManagerInputDto:
53
+ config: TestConfig
54
+ testContent: DirectorOutputDto
55
+ views: list[DetectionTestingView]
56
+
57
+
58
+ class DetectionTestingManager(BaseModel):
59
+ input_dto: DetectionTestingManagerInputDto
60
+ output_dto: DetectionTestingManagerOutputDto
61
+ detectionTestingInfrastructureObjects: list[DetectionTestingInfrastructure] = []
62
+
63
+ def setup(self):
64
+ # Some views, such as the Web View, will require some initial setup.
65
+ # for view in self.input_dto.views:
66
+ # view.setup()
67
+
68
+ # for content in self.input_dto.testContent.detections:
69
+ # self.pending_queue.put(content)
70
+ self.output_dto.inputQueue = self.input_dto.testContent.detections
71
+ self.create_DetectionTestingInfrastructureObjects()
72
+
73
+ def execute(self) -> DetectionTestingManagerOutputDto:
74
+ def sigint_handler(signum, frame):
75
+ print("SIGINT (Ctrl-C Received. Shutting down test...)")
76
+ self.output_dto.terminate = True
77
+ if self.input_dto.config.post_test_behavior in [
78
+ PostTestBehavior.always_pause,
79
+ PostTestBehavior.pause_on_failure,
80
+ ]:
81
+ # It is possible that we are stuck waiting at in input() prompt, so inject
82
+ # a newline '\r\n' which will cause that wait to stop
83
+ print("*******************************")
84
+ print(
85
+ "If testing is paused and you are debugging a detection, you MUST hit CTRL-D at the prompt to complete shutdown."
86
+ )
87
+ print("*******************************")
88
+
89
+ signal.signal(signal.SIGINT, sigint_handler)
90
+
91
+ with concurrent.futures.ThreadPoolExecutor(
92
+ max_workers=self.input_dto.config.num_containers,
93
+ ) as instance_pool, concurrent.futures.ThreadPoolExecutor(
94
+ max_workers=len(self.input_dto.views)
95
+ ) as view_runner, concurrent.futures.ThreadPoolExecutor(
96
+ max_workers=self.input_dto.config.num_containers,
97
+ ) as view_shutdowner:
98
+
99
+ # Start all the views
100
+ future_views = {
101
+ view_runner.submit(view.setup): view for view in self.input_dto.views
102
+ }
103
+ # Configure all the instances
104
+ future_instances_setup = {
105
+ instance_pool.submit(instance.setup): instance
106
+ for instance in self.detectionTestingInfrastructureObjects
107
+ }
108
+
109
+ # Wait for all instances to be set up
110
+ for future in concurrent.futures.as_completed(future_instances_setup):
111
+ try:
112
+ result = future.result()
113
+ except Exception as e:
114
+ self.output_dto.terminate = True
115
+ print(f"Error setting up container: {str(e)}")
116
+
117
+ # Start and wait for all tests to run
118
+ if not self.output_dto.terminate:
119
+ self.output_dto.start_time = datetime.datetime.now()
120
+ future_instances_execute = {
121
+ instance_pool.submit(instance.execute): instance
122
+ for instance in self.detectionTestingInfrastructureObjects
123
+ }
124
+ # What for execution to finish
125
+ for future in concurrent.futures.as_completed(future_instances_execute):
126
+ try:
127
+ result = future.result()
128
+ except Exception as e:
129
+ self.output_dto.terminate = True
130
+ print(f"Error running in container: {str(e)}")
131
+
132
+ self.output_dto.terminate = True
133
+
134
+ future_views_shutdowner = {
135
+ view_shutdowner.submit(view.stop): view for view in self.input_dto.views
136
+ }
137
+ for future in concurrent.futures.as_completed(future_views_shutdowner):
138
+ try:
139
+ result = future.result()
140
+ except Exception as e:
141
+ print(f"Error stopping view: {str(e)}")
142
+
143
+ for future in concurrent.futures.as_completed(future_views):
144
+ try:
145
+ result = future.result()
146
+ except Exception as e:
147
+ print(f"Error running container: {str(e)}")
148
+
149
+ return self.output_dto
150
+
151
+ def create_DetectionTestingInfrastructureObjects(self):
152
+ import sys
153
+
154
+ for index in range(self.input_dto.config.num_containers):
155
+ instanceConfig = deepcopy(self.input_dto.config)
156
+ instanceConfig.api_port += index * 2
157
+ instanceConfig.hec_port += index * 2
158
+ instanceConfig.web_ui_port += index
159
+
160
+ instanceConfig.container_name = instanceConfig.container_name % (index,)
161
+
162
+ if (
163
+ self.input_dto.config.target_infrastructure
164
+ == DetectionTestingTargetInfrastructure.container
165
+ ):
166
+
167
+ self.detectionTestingInfrastructureObjects.append(
168
+ DetectionTestingInfrastructureContainer(
169
+ config=instanceConfig, sync_obj=self.output_dto
170
+ )
171
+ )
172
+
173
+ elif (
174
+ self.input_dto.config.target_infrastructure
175
+ == DetectionTestingTargetInfrastructure.server
176
+ ):
177
+
178
+ self.detectionTestingInfrastructureObjects.append(
179
+ DetectionTestingInfrastructureServer(
180
+ config=instanceConfig, sync_obj=self.output_dto
181
+ )
182
+ )
183
+
184
+ else:
185
+
186
+ print(
187
+ f"Unsupported target infrastructure '{self.input_dto.config.target_infrastructure}'"
188
+ )
189
+ sys.exit(1)