contentctl 5.0.0a2__py3-none-any.whl → 5.0.0a3__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/__init__.py +1 -1
- contentctl/actions/build.py +88 -55
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
- contentctl/actions/detection_testing/GitService.py +2 -4
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +3 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +78 -50
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +31 -25
- contentctl/actions/validate.py +54 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +10 -10
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -39
- contentctl/input/director.py +69 -37
- contentctl/input/new_content_questions.py +26 -34
- contentctl/input/yml_reader.py +22 -17
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
- contentctl/objects/alert_action.py +8 -8
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +64 -54
- contentctl/objects/base_test.py +2 -1
- contentctl/objects/base_test_result.py +16 -8
- contentctl/objects/baseline.py +41 -30
- contentctl/objects/baseline_tags.py +29 -22
- contentctl/objects/config.py +1 -1
- contentctl/objects/constants.py +29 -58
- contentctl/objects/correlation_search.py +75 -55
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +13 -13
- contentctl/objects/deployment.py +44 -37
- contentctl/objects/deployment_email.py +1 -1
- contentctl/objects/deployment_notable.py +2 -1
- contentctl/objects/deployment_phantom.py +5 -5
- contentctl/objects/deployment_rba.py +1 -1
- contentctl/objects/deployment_scheduling.py +1 -1
- contentctl/objects/deployment_slack.py +1 -1
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +54 -64
- contentctl/objects/drilldown.py +66 -35
- contentctl/objects/enums.py +61 -43
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +41 -26
- contentctl/objects/investigation_tags.py +29 -17
- contentctl/objects/lookup.py +234 -113
- contentctl/objects/macro.py +55 -38
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +22 -16
- contentctl/objects/rba.py +14 -8
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +27 -20
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +45 -44
- contentctl/objects/story_tags.py +56 -44
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +4 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +22 -22
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +230 -174
- contentctl/output/data_source_writer.py +38 -25
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +20 -8
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/transforms.j2 +2 -2
- contentctl/output/yml_writer.py +18 -24
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
- contentctl-5.0.0a3.dist-info/RECORD +168 -0
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/observable.py +0 -39
- contentctl-5.0.0a2.dist-info/RECORD +0 -170
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import List,Union
|
|
1
|
+
from typing import List, Union
|
|
3
2
|
import pathlib
|
|
4
3
|
|
|
5
4
|
from contentctl.objects.detection import Detection
|
|
6
5
|
from contentctl.output.attack_nav_writer import AttackNavWriter
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
class AttackNavOutput
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
class AttackNavOutput:
|
|
9
|
+
def writeObjects(
|
|
10
|
+
self, detections: List[Detection], output_path: pathlib.Path
|
|
11
|
+
) -> None:
|
|
12
|
+
techniques: dict[str, dict[str, Union[List[str], int]]] = {}
|
|
13
13
|
for detection in detections:
|
|
14
14
|
for tactic in detection.tags.mitre_attack_id:
|
|
15
15
|
if tactic not in techniques:
|
|
16
|
-
techniques[tactic] = {
|
|
17
|
-
|
|
16
|
+
techniques[tactic] = {"score": 0, "file_paths": []}
|
|
17
|
+
|
|
18
18
|
detection_url = f"https://github.com/splunk/security_content/blob/develop/detections/{detection.source}/{detection.file_path.name}"
|
|
19
|
-
techniques[tactic][
|
|
20
|
-
techniques[tactic][
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
techniques[tactic]["score"] += 1
|
|
20
|
+
techniques[tactic]["file_paths"].append(detection_url)
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
23
|
for detection in objects:
|
|
24
24
|
if detection.tags.mitre_attack_enrichments:
|
|
25
25
|
for mitre_attack_enrichment in detection.tags.mitre_attack_enrichments:
|
|
@@ -31,16 +31,16 @@ class AttackNavOutput():
|
|
|
31
31
|
else:
|
|
32
32
|
techniques[mitre_attack_enrichment.mitre_attack_id]['score'] = techniques[mitre_attack_enrichment.mitre_attack_id]['score'] + 1
|
|
33
33
|
techniques[mitre_attack_enrichment.mitre_attack_id]['file_paths'].append('https://github.com/splunk/security_content/blob/develop/detections/' + detection.getSource() + '/' + self.convertNameToFileName(detection.name))
|
|
34
|
-
|
|
35
|
-
AttackNavWriter.writeAttackNavFile(techniques, output_path /
|
|
36
|
-
|
|
34
|
+
"""
|
|
35
|
+
AttackNavWriter.writeAttackNavFile(techniques, output_path / "coverage.json")
|
|
37
36
|
|
|
38
37
|
def convertNameToFileName(self, name: str):
|
|
39
|
-
file_name =
|
|
40
|
-
.replace(
|
|
41
|
-
.replace(
|
|
42
|
-
.replace(
|
|
43
|
-
.replace(
|
|
38
|
+
file_name = (
|
|
39
|
+
name.replace(" ", "_")
|
|
40
|
+
.replace("-", "_")
|
|
41
|
+
.replace(".", "_")
|
|
42
|
+
.replace("/", "_")
|
|
44
43
|
.lower()
|
|
45
|
-
|
|
44
|
+
)
|
|
45
|
+
file_name = file_name + ".yml"
|
|
46
46
|
return file_name
|
|
@@ -1,75 +1,67 @@
|
|
|
1
|
-
|
|
2
1
|
import json
|
|
3
2
|
from typing import Union, List
|
|
4
3
|
import pathlib
|
|
4
|
+
|
|
5
5
|
VERSION = "4.3"
|
|
6
6
|
NAME = "Detection Coverage"
|
|
7
7
|
DESCRIPTION = "security_content detection coverage"
|
|
8
8
|
DOMAIN = "mitre-enterprise"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class AttackNavWriter
|
|
12
|
-
|
|
11
|
+
class AttackNavWriter:
|
|
13
12
|
@staticmethod
|
|
14
|
-
def writeAttackNavFile(
|
|
13
|
+
def writeAttackNavFile(
|
|
14
|
+
mitre_techniques: dict[str, dict[str, Union[List[str], int]]],
|
|
15
|
+
output_path: pathlib.Path,
|
|
16
|
+
) -> None:
|
|
15
17
|
max_count = 0
|
|
16
18
|
for technique_id in mitre_techniques.keys():
|
|
17
|
-
if mitre_techniques[technique_id][
|
|
18
|
-
max_count = mitre_techniques[technique_id][
|
|
19
|
-
|
|
19
|
+
if mitre_techniques[technique_id]["score"] > max_count:
|
|
20
|
+
max_count = mitre_techniques[technique_id]["score"]
|
|
21
|
+
|
|
20
22
|
layer_json = {
|
|
21
23
|
"version": VERSION,
|
|
22
24
|
"name": NAME,
|
|
23
25
|
"description": DESCRIPTION,
|
|
24
26
|
"domain": DOMAIN,
|
|
25
|
-
"techniques": []
|
|
27
|
+
"techniques": [],
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
layer_json["gradient"] = {
|
|
29
|
-
"colors": [
|
|
30
|
-
"#ffffff",
|
|
31
|
-
"#66b1ff",
|
|
32
|
-
"#096ed7"
|
|
33
|
-
],
|
|
31
|
+
"colors": ["#ffffff", "#66b1ff", "#096ed7"],
|
|
34
32
|
"minValue": 0,
|
|
35
|
-
"maxValue": max_count
|
|
33
|
+
"maxValue": max_count,
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
layer_json["filters"] = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
"platforms": [
|
|
38
|
+
"Windows",
|
|
39
|
+
"Linux",
|
|
40
|
+
"macOS",
|
|
41
|
+
"AWS",
|
|
42
|
+
"GCP",
|
|
43
|
+
"Azure",
|
|
44
|
+
"Office 365",
|
|
45
|
+
"SaaS",
|
|
46
|
+
]
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
layer_json["legendItems"] = [
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
"color": "#ffffff"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"label": "Some detections available",
|
|
58
|
-
"color": "#66b1ff"
|
|
59
|
-
}
|
|
50
|
+
{"label": "NO available detections", "color": "#ffffff"},
|
|
51
|
+
{"label": "Some detections available", "color": "#66b1ff"},
|
|
60
52
|
]
|
|
61
53
|
|
|
62
|
-
layer_json[
|
|
63
|
-
layer_json[
|
|
54
|
+
layer_json["showTacticRowBackground"] = True
|
|
55
|
+
layer_json["tacticRowBackground"] = "#dddddd"
|
|
64
56
|
layer_json["sorting"] = 3
|
|
65
57
|
|
|
66
58
|
for technique_id in mitre_techniques.keys():
|
|
67
59
|
layer_technique = {
|
|
68
60
|
"techniqueID": technique_id,
|
|
69
|
-
"score": mitre_techniques[technique_id][
|
|
70
|
-
"comment": "\n\n".join(mitre_techniques[technique_id][
|
|
61
|
+
"score": mitre_techniques[technique_id]["score"],
|
|
62
|
+
"comment": "\n\n".join(mitre_techniques[technique_id]["file_paths"]),
|
|
71
63
|
}
|
|
72
64
|
layer_json["techniques"].append(layer_technique)
|
|
73
65
|
|
|
74
|
-
with open(output_path,
|
|
66
|
+
with open(output_path, "w") as outfile:
|
|
75
67
|
json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
|
contentctl/output/conf_output.py
CHANGED
|
@@ -1,220 +1,276 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import TYPE_CHECKING, Callable
|
|
4
|
+
|
|
3
5
|
if TYPE_CHECKING:
|
|
6
|
+
from contentctl.objects.baseline import Baseline
|
|
7
|
+
from contentctl.objects.dashboard import Dashboard
|
|
4
8
|
from contentctl.objects.detection import Detection
|
|
9
|
+
from contentctl.objects.investigation import Investigation
|
|
5
10
|
from contentctl.objects.lookup import Lookup
|
|
6
11
|
from contentctl.objects.macro import Macro
|
|
7
|
-
from contentctl.objects.dashboard import Dashboard
|
|
8
12
|
from contentctl.objects.story import Story
|
|
9
|
-
from contentctl.objects.baseline import Baseline
|
|
10
|
-
from contentctl.objects.investigation import Investigation
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
import pathlib
|
|
13
15
|
import shutil
|
|
14
16
|
import tarfile
|
|
15
|
-
|
|
16
|
-
import timeit
|
|
17
|
-
import datetime
|
|
18
|
-
from contentctl.output.conf_writer import ConfWriter
|
|
17
|
+
|
|
19
18
|
from contentctl.objects.config import build
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
# These must be imported separately because they are not just used for typing,
|
|
21
|
+
# they are used in isinstance (which requires the object to be imported)
|
|
22
|
+
from contentctl.objects.lookup import FileBackedLookup, MlModel
|
|
23
|
+
from contentctl.output.conf_writer import ConfWriter
|
|
24
|
+
|
|
23
25
|
|
|
26
|
+
class ConfOutput:
|
|
27
|
+
config: build
|
|
24
28
|
|
|
25
29
|
def __init__(self, config: build):
|
|
26
30
|
self.config = config
|
|
27
31
|
|
|
28
|
-
#Create the build directory if it does not exist
|
|
32
|
+
# Create the build directory if it does not exist
|
|
29
33
|
config.getPackageDirectoryPath().parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
-
|
|
31
|
-
#Remove the app path, if it exists
|
|
34
|
+
|
|
35
|
+
# Remove the app path, if it exists
|
|
32
36
|
shutil.rmtree(config.getPackageDirectoryPath(), ignore_errors=True)
|
|
33
|
-
|
|
34
|
-
#Copy all the template files into the app
|
|
37
|
+
|
|
38
|
+
# Copy all the template files into the app
|
|
35
39
|
shutil.copytree(config.getAppTemplatePath(), config.getPackageDirectoryPath())
|
|
36
|
-
|
|
37
40
|
|
|
38
41
|
def writeHeaders(self) -> set[pathlib.Path]:
|
|
39
|
-
written_files:set[pathlib.Path] = set()
|
|
40
|
-
for output_app_path in [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
written_files: set[pathlib.Path] = set()
|
|
43
|
+
for output_app_path in [
|
|
44
|
+
"default/analyticstories.conf",
|
|
45
|
+
"default/savedsearches.conf",
|
|
46
|
+
"default/collections.conf",
|
|
47
|
+
"default/es_investigations.conf",
|
|
48
|
+
"default/macros.conf",
|
|
49
|
+
"default/transforms.conf",
|
|
50
|
+
"default/workflow_actions.conf",
|
|
51
|
+
"default/app.conf",
|
|
52
|
+
"default/content-version.conf",
|
|
53
|
+
]:
|
|
54
|
+
written_files.add(
|
|
55
|
+
ConfWriter.writeConfFileHeader(
|
|
56
|
+
pathlib.Path(output_app_path), self.config
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
51
60
|
return written_files
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
# The contents of app.manifest are not a conf file, but json.
|
|
63
|
+
# DO NOT write a header for this file type, simply create the file
|
|
64
|
+
with open(
|
|
65
|
+
self.config.getPackageDirectoryPath() / pathlib.Path("app.manifest"), "w"
|
|
66
|
+
):
|
|
57
67
|
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
|
|
69
|
+
def writeMiscellaneousAppFiles(self) -> set[pathlib.Path]:
|
|
70
|
+
written_files: set[pathlib.Path] = set()
|
|
71
|
+
|
|
72
|
+
written_files.add(
|
|
73
|
+
ConfWriter.writeConfFile(
|
|
74
|
+
pathlib.Path("default/content-version.conf"),
|
|
75
|
+
"content-version.j2",
|
|
76
|
+
self.config,
|
|
77
|
+
[self.config.app],
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
written_files.add(
|
|
82
|
+
ConfWriter.writeManifestFile(
|
|
83
|
+
pathlib.Path("app.manifest"),
|
|
84
|
+
"app.manifest.j2",
|
|
85
|
+
self.config,
|
|
86
|
+
[self.config.app],
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
75
90
|
written_files.add(ConfWriter.writeServerConf(self.config))
|
|
76
91
|
|
|
77
92
|
written_files.add(ConfWriter.writeAppConf(self.config))
|
|
78
|
-
|
|
79
93
|
|
|
80
94
|
return written_files
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
def writeDetections(self, objects: list[Detection]) -> set[pathlib.Path]:
|
|
97
|
+
written_files: set[pathlib.Path] = set()
|
|
98
|
+
for output_app_path, template_name in [
|
|
99
|
+
("default/savedsearches.conf", "savedsearches_detections.j2"),
|
|
100
|
+
("default/analyticstories.conf", "analyticstories_detections.j2"),
|
|
101
|
+
]:
|
|
102
|
+
written_files.add(
|
|
103
|
+
ConfWriter.writeConfFile(
|
|
104
|
+
pathlib.Path(output_app_path), template_name, self.config, objects
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return written_files
|
|
108
|
+
|
|
109
|
+
def writeStories(self, objects: list[Story]) -> set[pathlib.Path]:
|
|
110
|
+
written_files: set[pathlib.Path] = set()
|
|
111
|
+
written_files.add(
|
|
112
|
+
ConfWriter.writeConfFile(
|
|
113
|
+
pathlib.Path("default/analyticstories.conf"),
|
|
114
|
+
"analyticstories_stories.j2",
|
|
115
|
+
self.config,
|
|
116
|
+
objects,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
89
119
|
return written_files
|
|
90
120
|
|
|
121
|
+
def writeBaselines(self, objects: list[Baseline]) -> set[pathlib.Path]:
|
|
122
|
+
written_files: set[pathlib.Path] = set()
|
|
123
|
+
written_files.add(
|
|
124
|
+
ConfWriter.writeConfFile(
|
|
125
|
+
pathlib.Path("default/savedsearches.conf"),
|
|
126
|
+
"savedsearches_baselines.j2",
|
|
127
|
+
self.config,
|
|
128
|
+
objects,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
return written_files
|
|
132
|
+
|
|
133
|
+
def writeInvestigations(self, objects: list[Investigation]) -> set[pathlib.Path]:
|
|
134
|
+
written_files: set[pathlib.Path] = set()
|
|
135
|
+
for output_app_path, template_name in [
|
|
136
|
+
("default/savedsearches.conf", "savedsearches_investigations.j2"),
|
|
137
|
+
("default/analyticstories.conf", "analyticstories_investigations.j2"),
|
|
138
|
+
]:
|
|
139
|
+
ConfWriter.writeConfFile(
|
|
140
|
+
pathlib.Path(output_app_path), template_name, self.config, objects
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
workbench_panels: list[Investigation] = []
|
|
144
|
+
for investigation in objects:
|
|
145
|
+
if investigation.inputs:
|
|
146
|
+
response_file_name_xml = (
|
|
147
|
+
investigation.lowercase_name + "___response_task.xml"
|
|
148
|
+
)
|
|
149
|
+
workbench_panels.append(investigation)
|
|
150
|
+
investigation.search = investigation.search.replace(">", ">")
|
|
151
|
+
investigation.search = investigation.search.replace("<", "<")
|
|
152
|
+
|
|
153
|
+
ConfWriter.writeXmlFileHeader(
|
|
154
|
+
pathlib.Path(
|
|
155
|
+
f"default/data/ui/panels/workbench_panel_{response_file_name_xml}"
|
|
156
|
+
),
|
|
157
|
+
self.config,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
ConfWriter.writeXmlFile(
|
|
161
|
+
pathlib.Path(
|
|
162
|
+
f"default/data/ui/panels/workbench_panel_{response_file_name_xml}"
|
|
163
|
+
),
|
|
164
|
+
"panel.j2",
|
|
165
|
+
self.config,
|
|
166
|
+
[investigation.search],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
for output_app_path, template_name in [
|
|
170
|
+
("default/es_investigations.conf", "es_investigations_investigations.j2"),
|
|
171
|
+
("default/workflow_actions.conf", "workflow_actions.j2"),
|
|
172
|
+
]:
|
|
173
|
+
written_files.add(
|
|
174
|
+
ConfWriter.writeConfFile(
|
|
175
|
+
pathlib.Path(output_app_path),
|
|
176
|
+
template_name,
|
|
177
|
+
self.config,
|
|
178
|
+
workbench_panels,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
return written_files
|
|
182
|
+
|
|
183
|
+
def writeLookups(self, objects: list[Lookup]) -> set[pathlib.Path]:
|
|
184
|
+
written_files: set[pathlib.Path] = set()
|
|
185
|
+
for output_app_path, template_name in [
|
|
186
|
+
("default/collections.conf", "collections.j2"),
|
|
187
|
+
("default/transforms.conf", "transforms.j2"),
|
|
188
|
+
]:
|
|
189
|
+
# DO NOT write MlModels to transforms.conf. The enumeration of
|
|
190
|
+
# those files happens in the MLTK app by enumerating the __mlspl_*
|
|
191
|
+
# files in the lookups/ directory of the app
|
|
192
|
+
written_files.add(
|
|
193
|
+
ConfWriter.writeConfFile(
|
|
194
|
+
pathlib.Path(output_app_path),
|
|
195
|
+
template_name,
|
|
196
|
+
self.config,
|
|
197
|
+
[lookup for lookup in objects if not isinstance(lookup, MlModel)],
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Get the path to the lookups folder
|
|
202
|
+
lookup_folder = self.config.getPackageDirectoryPath() / "lookups"
|
|
203
|
+
|
|
204
|
+
# Make the new folder for the lookups
|
|
205
|
+
# This folder almost certainly already exists because mitre_enrichment.csv has been writtent here from the app template.
|
|
206
|
+
lookup_folder.mkdir(exist_ok=True)
|
|
207
|
+
|
|
208
|
+
# Copy each lookup into the folder
|
|
209
|
+
for lookup in objects:
|
|
210
|
+
# All File backed lookups, including __mlspl_ files, should be copied here,
|
|
211
|
+
# even though the MLModel info was intentionally not written to the
|
|
212
|
+
# transforms.conf file as noted above.
|
|
213
|
+
if isinstance(lookup, FileBackedLookup):
|
|
214
|
+
shutil.copy(lookup.filename, lookup_folder / lookup.app_filename.name)
|
|
215
|
+
return written_files
|
|
216
|
+
|
|
217
|
+
def writeMacros(self, objects: list[Macro]) -> set[pathlib.Path]:
|
|
218
|
+
written_files: set[pathlib.Path] = set()
|
|
219
|
+
written_files.add(
|
|
220
|
+
ConfWriter.writeConfFile(
|
|
221
|
+
pathlib.Path("default/macros.conf"), "macros.j2", self.config, objects
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
return written_files
|
|
225
|
+
|
|
226
|
+
def writeDashboards(self, objects: list[Dashboard]) -> set[pathlib.Path]:
|
|
227
|
+
written_files: set[pathlib.Path] = set()
|
|
228
|
+
written_files.update(ConfWriter.writeDashboardFiles(self.config, objects))
|
|
229
|
+
return written_files
|
|
91
230
|
|
|
92
|
-
def writeStories(self, objects:list[Story]) -> set[pathlib.Path]:
|
|
93
|
-
written_files:set[pathlib.Path] = set()
|
|
94
|
-
written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/analyticstories.conf'),
|
|
95
|
-
'analyticstories_stories.j2',
|
|
96
|
-
self.config, objects))
|
|
97
|
-
return written_files
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def writeBaselines(self, objects:list[Baseline]) -> set[pathlib.Path]:
|
|
101
|
-
written_files:set[pathlib.Path] = set()
|
|
102
|
-
written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/savedsearches.conf'),
|
|
103
|
-
'savedsearches_baselines.j2',
|
|
104
|
-
self.config, objects))
|
|
105
|
-
return written_files
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def writeInvestigations(self, objects:list[Investigation]) -> set[pathlib.Path]:
|
|
109
|
-
written_files:set[pathlib.Path] = set()
|
|
110
|
-
for output_app_path, template_name in [ ('default/savedsearches.conf', 'savedsearches_investigations.j2'),
|
|
111
|
-
('default/analyticstories.conf', 'analyticstories_investigations.j2')]:
|
|
112
|
-
ConfWriter.writeConfFile(pathlib.Path(output_app_path),
|
|
113
|
-
template_name,
|
|
114
|
-
self.config,
|
|
115
|
-
objects)
|
|
116
|
-
|
|
117
|
-
workbench_panels:list[Investigation] = []
|
|
118
|
-
for investigation in objects:
|
|
119
|
-
if investigation.inputs:
|
|
120
|
-
response_file_name_xml = investigation.lowercase_name + "___response_task.xml"
|
|
121
|
-
workbench_panels.append(investigation)
|
|
122
|
-
investigation.search = investigation.search.replace(">",">")
|
|
123
|
-
investigation.search = investigation.search.replace("<","<")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
ConfWriter.writeXmlFileHeader(pathlib.Path(f'default/data/ui/panels/workbench_panel_{response_file_name_xml}'),
|
|
127
|
-
self.config)
|
|
128
|
-
|
|
129
|
-
ConfWriter.writeXmlFile( pathlib.Path(f'default/data/ui/panels/workbench_panel_{response_file_name_xml}'),
|
|
130
|
-
'panel.j2',
|
|
131
|
-
self.config,[investigation.search])
|
|
132
|
-
|
|
133
|
-
for output_app_path, template_name in [ ('default/es_investigations.conf', 'es_investigations_investigations.j2'),
|
|
134
|
-
('default/workflow_actions.conf', 'workflow_actions.j2')]:
|
|
135
|
-
written_files.add( ConfWriter.writeConfFile(pathlib.Path(output_app_path),
|
|
136
|
-
template_name,
|
|
137
|
-
self.config,
|
|
138
|
-
workbench_panels))
|
|
139
|
-
return written_files
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def writeLookups(self, objects:list[Lookup]) -> set[pathlib.Path]:
|
|
143
|
-
written_files:set[pathlib.Path] = set()
|
|
144
|
-
for output_app_path, template_name in [ ('default/collections.conf', 'collections.j2'),
|
|
145
|
-
('default/transforms.conf', 'transforms.j2')]:
|
|
146
|
-
written_files.add(ConfWriter.writeConfFile(pathlib.Path(output_app_path),
|
|
147
|
-
template_name,
|
|
148
|
-
self.config,
|
|
149
|
-
objects))
|
|
150
|
-
|
|
151
|
-
#Get the path to the lookups folder
|
|
152
|
-
lookup_folder = self.config.getPackageDirectoryPath()/"lookups"
|
|
153
|
-
|
|
154
|
-
# Make the new folder for the lookups
|
|
155
|
-
# This folder almost certainly already exists because mitre_enrichment.csv has been writtent here from the app template.
|
|
156
|
-
lookup_folder.mkdir(exist_ok=True)
|
|
157
|
-
|
|
158
|
-
#Copy each lookup into the folder
|
|
159
|
-
for lookup in objects:
|
|
160
|
-
if isinstance(lookup, FileBackedLookup):
|
|
161
|
-
shutil.copy(lookup.filename, lookup_folder/lookup.app_filename.name)
|
|
162
|
-
return written_files
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def writeMacros(self, objects:list[Macro]) -> set[pathlib.Path]:
|
|
166
|
-
written_files:set[pathlib.Path] = set()
|
|
167
|
-
written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/macros.conf'),
|
|
168
|
-
'macros.j2',
|
|
169
|
-
self.config, objects))
|
|
170
|
-
return written_files
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def writeDashboards(self, objects:list[Dashboard]) -> set[pathlib.Path]:
|
|
174
|
-
written_files:set[pathlib.Path] = set()
|
|
175
|
-
written_files.update(ConfWriter.writeDashboardFiles(self.config, objects))
|
|
176
|
-
return written_files
|
|
177
|
-
|
|
178
|
-
|
|
179
231
|
def packageAppTar(self) -> None:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
232
|
+
with tarfile.open(
|
|
233
|
+
self.config.getPackageFilePath(include_version=True), "w:gz"
|
|
234
|
+
) as app_archive:
|
|
235
|
+
app_archive.add(
|
|
236
|
+
self.config.getPackageDirectoryPath(),
|
|
237
|
+
arcname=self.config.getPackageDirectoryPath().name,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
shutil.copy2(
|
|
241
|
+
self.config.getPackageFilePath(include_version=True),
|
|
242
|
+
self.config.getPackageFilePath(include_version=False),
|
|
243
|
+
follow_symlinks=False,
|
|
244
|
+
)
|
|
245
|
+
|
|
188
246
|
def packageAppSlim(self) -> None:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
247
|
+
raise Exception(
|
|
248
|
+
"Packaging with splunk-packaging-toolkit not currently supported as slim only supports Python 3.7. "
|
|
249
|
+
"Please raise an issue in the contentctl GitHub if you encounter this exception."
|
|
250
|
+
)
|
|
192
251
|
try:
|
|
252
|
+
import logging
|
|
253
|
+
|
|
193
254
|
import slim
|
|
194
255
|
from slim.utils import SlimLogger
|
|
195
|
-
|
|
196
|
-
#In order to avoid significant output, only emit FATAL log messages
|
|
256
|
+
|
|
257
|
+
# In order to avoid significant output, only emit FATAL log messages
|
|
197
258
|
SlimLogger.set_level(logging.ERROR)
|
|
198
259
|
try:
|
|
199
|
-
slim.package(
|
|
260
|
+
slim.package(
|
|
261
|
+
source=self.config.getPackageDirectoryPath(),
|
|
262
|
+
output_dir=pathlib.Path(self.config.getBuildDir()),
|
|
263
|
+
)
|
|
200
264
|
except SystemExit as e:
|
|
201
265
|
raise Exception(f"Error building package with slim: {str(e)}")
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
|
|
204
267
|
except Exception as e:
|
|
205
|
-
print(
|
|
206
|
-
|
|
207
|
-
|
|
268
|
+
print(
|
|
269
|
+
"Failed to import Splunk Packaging Toolkit (slim). slim requires Python<3.10. "
|
|
270
|
+
"Packaging app with tar instead. This should still work, but appinspect may catch "
|
|
271
|
+
"errors that otherwise would have been flagged by slim."
|
|
272
|
+
)
|
|
208
273
|
raise Exception(f"slim (splunk packaging toolkit) not installed: {str(e)}")
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def packageApp(self, method: Callable[[ConfOutput],None]=packageAppTar)->None:
|
|
213
|
-
return method(self)
|
|
214
274
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def getElapsedTime(self, startTime:float)->datetime.timedelta:
|
|
218
|
-
return datetime.timedelta(seconds=round(timeit.default_timer() - startTime))
|
|
219
|
-
|
|
220
|
-
|
|
275
|
+
def packageApp(self, method: Callable[[ConfOutput], None] = packageAppTar) -> None:
|
|
276
|
+
return method(self)
|