contentctl 5.0.0a2__py3-none-any.whl → 5.0.1__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 (114) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +2 -4
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +83 -53
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +10 -10
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +255 -323
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +111 -46
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +47 -35
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +32 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +53 -31
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +22 -16
  74. contentctl/objects/rba.py +68 -11
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +54 -49
  81. contentctl/objects/story_tags.py +56 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  98. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  99. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  100. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  101. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  102. contentctl/output/templates/savedsearches_detections.j2 +2 -8
  103. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  104. contentctl/output/templates/transforms.j2 +2 -4
  105. contentctl/output/yml_writer.py +18 -24
  106. contentctl/templates/stories/cobalt_strike.yml +1 -0
  107. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/METADATA +1 -1
  108. contentctl-5.0.1.dist-info/RECORD +168 -0
  109. contentctl/actions/initialize_old.py +0 -245
  110. contentctl/objects/observable.py +0 -39
  111. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  112. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/LICENSE.md +0 -0
  113. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/WHEEL +0 -0
  114. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/entry_points.txt +0 -0
@@ -1,25 +1,25 @@
1
- import os
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
- def writeObjects(self, detections: List[Detection], output_path: pathlib.Path) -> None:
12
- techniques:dict[str,dict[str,Union[List[str],int]]] = {}
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] = {'score':0,'file_paths':[]}
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]['score'] += 1
20
- techniques[tactic]['file_paths'].append(detection_url)
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 / 'coverage.json')
36
-
34
+ """
35
+ AttackNavWriter.writeAttackNavFile(techniques, output_path / "coverage.json")
37
36
 
38
37
  def convertNameToFileName(self, name: str):
39
- file_name = 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
- file_name = file_name + '.yml'
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(mitre_techniques : dict[str,dict[str,Union[List[str],int]]], output_path : pathlib.Path) -> None:
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]['score'] > max_count:
18
- max_count = mitre_techniques[technique_id]['score']
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
- "platforms":
40
- ["Windows",
41
- "Linux",
42
- "macOS",
43
- "AWS",
44
- "GCP",
45
- "Azure",
46
- "Office 365",
47
- "SaaS"
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
- "label": "NO available detections",
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['showTacticRowBackground'] = True
63
- layer_json['tacticRowBackground'] = "#dddddd"
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]['score'],
70
- "comment": "\n\n".join(mitre_techniques[technique_id]['file_paths'])
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, 'w') as outfile:
66
+ with open(output_path, "w") as outfile:
75
67
  json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
@@ -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
- from contentctl.objects.lookup import FileBackedLookup
14
+ import pathlib
13
15
  import shutil
14
16
  import tarfile
15
- import pathlib
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
- class ConfOutput:
22
- config: build
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 ['default/analyticstories.conf',
41
- 'default/savedsearches.conf',
42
- 'default/collections.conf',
43
- 'default/es_investigations.conf',
44
- 'default/macros.conf',
45
- 'default/transforms.conf',
46
- 'default/workflow_actions.conf',
47
- 'default/app.conf',
48
- 'default/content-version.conf']:
49
- written_files.add(ConfWriter.writeConfFileHeader(pathlib.Path(output_app_path),self.config))
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
- #The contents of app.manifest are not a conf file, but json.
55
- #DO NOT write a header for this file type, simply create the file
56
- with open(self.config.getPackageDirectoryPath() / pathlib.Path('app.manifest'), 'w') as f:
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
- def writeMiscellaneousAppFiles(self)->set[pathlib.Path]:
63
- written_files:set[pathlib.Path] = set()
64
-
65
- written_files.add(ConfWriter.writeConfFile(pathlib.Path("default/content-version.conf"),
66
- "content-version.j2",
67
- self.config,
68
- [self.config.app]))
69
-
70
- written_files.add(ConfWriter.writeManifestFile(pathlib.Path("app.manifest"),
71
- "app.manifest.j2",
72
- self.config,
73
- [self.config.app]))
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
- def writeDetections(self, objects:list[Detection]) -> set[pathlib.Path]:
84
- written_files:set[pathlib.Path] = set()
85
- for output_app_path, template_name in [ ('default/savedsearches.conf', 'savedsearches_detections.j2'),
86
- ('default/analyticstories.conf', 'analyticstories_detections.j2')]:
87
- written_files.add(ConfWriter.writeConfFile(pathlib.Path(output_app_path),
88
- template_name, self.config, objects))
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("<", "&lt;")
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(">","&gt;")
123
- investigation.search = investigation.search.replace("<","&lt;")
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
- with tarfile.open(self.config.getPackageFilePath(include_version=True), "w:gz") as app_archive:
182
- app_archive.add(self.config.getPackageDirectoryPath(), arcname=self.config.getPackageDirectoryPath().name)
183
-
184
- shutil.copy2(self.config.getPackageFilePath(include_version=True),
185
- self.config.getPackageFilePath(include_version=False),
186
- follow_symlinks=False)
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
- raise Exception("Packaging with splunk-packaging-toolkit not currently supported as slim only supports Python 3.7. "
191
- "Please raise an issue in the contentctl GitHub if you encounter this exception.")
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
- import logging
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(source=self.config.getPackageDirectoryPath(), output_dir=pathlib.Path(self.config.getBuildDir()))
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("Failed to import Splunk Packaging Toolkit (slim). slim requires Python<3.10. "
206
- "Packaging app with tar instead. This should still work, but appinspect may catch "
207
- "errors that otherwise would have been flagged by slim.")
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)