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.
- contentctl/Res.yml +102 -0
- contentctl/__init__.py +1 -0
- contentctl/actions/deploy.py +147 -0
- contentctl/actions/detection_testing/DataManipulation.py +149 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +189 -0
- contentctl/actions/detection_testing/GitHubService.py +405 -0
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +65 -0
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +770 -0
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +122 -0
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +14 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +122 -0
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +73 -0
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +52 -0
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +157 -0
- contentctl/actions/doc_gen.py +26 -0
- contentctl/actions/generate.py +56 -0
- contentctl/actions/initialize.py +52 -0
- contentctl/actions/initialize_old.py +245 -0
- contentctl/actions/new_content.py +23 -0
- contentctl/actions/reporting.py +31 -0
- contentctl/actions/test.py +98 -0
- contentctl/actions/validate.py +86 -0
- contentctl/contentctl.py +374 -0
- contentctl/enrichments/attack_enrichment.py +87 -0
- contentctl/enrichments/cve_enrichment.py +24 -0
- contentctl/enrichments/splunk_app_enrichment.py +46 -0
- contentctl/helper/config_handler.py +30 -0
- contentctl/helper/link_validator.py +32 -0
- contentctl/helper/logger.py +0 -0
- contentctl/helper/utils.py +418 -0
- contentctl/input/baseline_builder.py +55 -0
- contentctl/input/basic_builder.py +58 -0
- contentctl/input/detection_builder.py +233 -0
- contentctl/input/director.py +265 -0
- contentctl/input/investigation_builder.py +42 -0
- contentctl/input/new_content_generator.py +81 -0
- contentctl/input/new_content_questions.py +142 -0
- contentctl/input/playbook_builder.py +67 -0
- contentctl/input/story_builder.py +106 -0
- contentctl/input/yml_reader.py +34 -0
- contentctl/objects/app.py +217 -0
- contentctl/objects/baseline.py +89 -0
- contentctl/objects/baseline_tags.py +25 -0
- contentctl/objects/config.py +107 -0
- contentctl/objects/constants.py +98 -0
- contentctl/objects/deployment.py +60 -0
- contentctl/objects/deployment_email.py +8 -0
- contentctl/objects/deployment_notable.py +8 -0
- contentctl/objects/deployment_phantom.py +10 -0
- contentctl/objects/deployment_rba.py +7 -0
- contentctl/objects/deployment_scheduling.py +10 -0
- contentctl/objects/deployment_slack.py +7 -0
- contentctl/objects/detection.py +186 -0
- contentctl/objects/detection_tags.py +121 -0
- contentctl/objects/enums.py +85 -0
- contentctl/objects/investigation.py +86 -0
- contentctl/objects/investigation_tags.py +9 -0
- contentctl/objects/lookup.py +16 -0
- contentctl/objects/macro.py +13 -0
- contentctl/objects/mitre_attack_enrichment.py +8 -0
- contentctl/objects/playbook.py +27 -0
- contentctl/objects/playbook_tags.py +13 -0
- contentctl/objects/repo_config.py +163 -0
- contentctl/objects/security_content_object.py +7 -0
- contentctl/objects/story.py +62 -0
- contentctl/objects/story_tags.py +32 -0
- contentctl/objects/test_config.py +541 -0
- contentctl/objects/unit_test.py +12 -0
- contentctl/objects/unit_test_attack_data.py +22 -0
- contentctl/objects/unit_test_baseline.py +11 -0
- contentctl/objects/unit_test_result.py +219 -0
- contentctl/objects/unit_test_test.py +18 -0
- contentctl/output/api_json_output.py +82 -0
- contentctl/output/attack_nav_output.py +35 -0
- contentctl/output/attack_nav_writer.py +75 -0
- contentctl/output/ba_yml_output.py +105 -0
- contentctl/output/conf_output.py +121 -0
- contentctl/output/conf_writer.py +65 -0
- contentctl/output/doc_md_output.py +70 -0
- contentctl/output/finding_report_writer.py +74 -0
- contentctl/output/jinja_writer.py +33 -0
- contentctl/output/json_writer.py +10 -0
- contentctl/output/new_content_yml_output.py +79 -0
- contentctl/output/svg_output.py +32 -0
- contentctl/output/templates/analyticstories_detections.j2 +22 -0
- contentctl/output/templates/analyticstories_investigations.j2 +21 -0
- contentctl/output/templates/analyticstories_stories.j2 +21 -0
- contentctl/output/templates/collections.j2 +8 -0
- contentctl/output/templates/detection_count.j2 +18 -0
- contentctl/output/templates/detection_coverage.j2 +18 -0
- contentctl/output/templates/doc_detection_page.j2 +19 -0
- contentctl/output/templates/doc_detections.j2 +210 -0
- contentctl/output/templates/doc_navigation.j2 +48 -0
- contentctl/output/templates/doc_navigation_pages.j2 +9 -0
- contentctl/output/templates/doc_playbooks.j2 +58 -0
- contentctl/output/templates/doc_playbooks_page.j2 +19 -0
- contentctl/output/templates/doc_stories.j2 +51 -0
- contentctl/output/templates/doc_story_page.j2 +19 -0
- contentctl/output/templates/es_investigations_investigations.j2 +38 -0
- contentctl/output/templates/es_investigations_stories.j2 +14 -0
- contentctl/output/templates/finding_report.j2 +11 -0
- contentctl/output/templates/header.j2 +7 -0
- contentctl/output/templates/macros.j2 +15 -0
- contentctl/output/templates/macros_detections.j2 +7 -0
- contentctl/output/templates/panel.j2 +11 -0
- contentctl/output/templates/savedsearches_baselines.j2 +49 -0
- contentctl/output/templates/savedsearches_detections.j2 +112 -0
- contentctl/output/templates/savedsearches_investigations.j2 +38 -0
- contentctl/output/templates/transforms.j2 +40 -0
- contentctl/output/templates/workflow_actions.j2 +18 -0
- contentctl/output/yml_writer.py +11 -0
- contentctl/templates/app_default.yml +101 -0
- contentctl/templates/contentctl_default.yml +32 -0
- contentctl/templates/datamodels_cim.conf +300 -0
- contentctl/templates/datamodels_custom.conf +15 -0
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +85 -0
- contentctl/templates/macros/security_content_ctime.yml +5 -0
- contentctl/templates/macros/security_content_summariesonly.yml +3 -0
- contentctl/templates/splunk_app/metadata/default.meta +6 -0
- contentctl/templates/stories/cobalt_strike.yml +52 -0
- contentctl/templates/tests/anomalous_usage_of_7zip.test.yml +12 -0
- contentctl-1.0.0.dist-info/LICENSE.md +201 -0
- contentctl-1.0.0.dist-info/METADATA +223 -0
- contentctl-1.0.0.dist-info/RECORD +126 -0
- contentctl-1.0.0.dist-info/WHEEL +4 -0
- 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)
|