contentctl 4.4.6__py3-none-any.whl → 5.0.0a0__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 (69) hide show
  1. contentctl/actions/build.py +39 -27
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +0 -1
  3. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +32 -26
  4. contentctl/actions/detection_testing/progress_bar.py +6 -6
  5. contentctl/actions/detection_testing/views/DetectionTestingView.py +4 -4
  6. contentctl/actions/new_content.py +98 -81
  7. contentctl/actions/test.py +4 -5
  8. contentctl/actions/validate.py +2 -1
  9. contentctl/contentctl.py +114 -79
  10. contentctl/helper/utils.py +0 -14
  11. contentctl/input/director.py +5 -5
  12. contentctl/input/new_content_questions.py +2 -2
  13. contentctl/input/yml_reader.py +11 -6
  14. contentctl/objects/abstract_security_content_objects/detection_abstract.py +228 -120
  15. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +5 -7
  16. contentctl/objects/alert_action.py +2 -1
  17. contentctl/objects/atomic.py +1 -0
  18. contentctl/objects/base_test.py +4 -3
  19. contentctl/objects/base_test_result.py +3 -3
  20. contentctl/objects/baseline.py +26 -6
  21. contentctl/objects/baseline_tags.py +2 -3
  22. contentctl/objects/config.py +26 -45
  23. contentctl/objects/constants.py +4 -1
  24. contentctl/objects/correlation_search.py +89 -95
  25. contentctl/objects/data_source.py +5 -6
  26. contentctl/objects/deployment.py +2 -10
  27. contentctl/objects/deployment_email.py +2 -1
  28. contentctl/objects/deployment_notable.py +2 -1
  29. contentctl/objects/deployment_phantom.py +2 -1
  30. contentctl/objects/deployment_rba.py +2 -1
  31. contentctl/objects/deployment_scheduling.py +2 -1
  32. contentctl/objects/deployment_slack.py +2 -1
  33. contentctl/objects/detection_tags.py +7 -42
  34. contentctl/objects/drilldown.py +1 -0
  35. contentctl/objects/enums.py +21 -58
  36. contentctl/objects/investigation.py +6 -5
  37. contentctl/objects/investigation_tags.py +2 -3
  38. contentctl/objects/lookup.py +145 -63
  39. contentctl/objects/macro.py +2 -3
  40. contentctl/objects/mitre_attack_enrichment.py +2 -2
  41. contentctl/objects/observable.py +3 -1
  42. contentctl/objects/playbook_tags.py +5 -1
  43. contentctl/objects/rba.py +90 -0
  44. contentctl/objects/risk_event.py +87 -144
  45. contentctl/objects/story_tags.py +1 -2
  46. contentctl/objects/test_attack_data.py +2 -1
  47. contentctl/objects/unit_test_baseline.py +2 -1
  48. contentctl/output/api_json_output.py +233 -220
  49. contentctl/output/conf_output.py +51 -44
  50. contentctl/output/conf_writer.py +201 -125
  51. contentctl/output/data_source_writer.py +0 -1
  52. contentctl/output/json_writer.py +2 -4
  53. contentctl/output/svg_output.py +1 -1
  54. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  55. contentctl/output/templates/collections.j2 +1 -1
  56. contentctl/output/templates/doc_detections.j2 +0 -5
  57. contentctl/output/templates/savedsearches_detections.j2 +8 -3
  58. contentctl/output/templates/transforms.j2 +4 -4
  59. contentctl/output/yml_writer.py +15 -0
  60. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  61. {contentctl-4.4.6.dist-info → contentctl-5.0.0a0.dist-info}/METADATA +6 -5
  62. {contentctl-4.4.6.dist-info → contentctl-5.0.0a0.dist-info}/RECORD +65 -68
  63. {contentctl-4.4.6.dist-info → contentctl-5.0.0a0.dist-info}/WHEEL +1 -1
  64. contentctl/objects/event_source.py +0 -11
  65. contentctl/output/detection_writer.py +0 -28
  66. contentctl/output/new_content_yml_output.py +0 -56
  67. contentctl/output/yml_output.py +0 -66
  68. {contentctl-4.4.6.dist-info → contentctl-5.0.0a0.dist-info}/LICENSE.md +0 -0
  69. {contentctl-4.4.6.dist-info → contentctl-5.0.0a0.dist-info}/entry_points.txt +0 -0
@@ -1,246 +1,259 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ if TYPE_CHECKING:
4
+ from contentctl.objects.detection import Detection
5
+ from contentctl.objects.lookup import Lookup
6
+ from contentctl.objects.macro import Macro
7
+ from contentctl.objects.story import Story
8
+ from contentctl.objects.baseline import Baseline
9
+ from contentctl.objects.investigation import Investigation
10
+ from contentctl.objects.deployment import Deployment
11
+
1
12
  import os
2
- import json
3
13
  import pathlib
4
14
 
5
15
  from contentctl.output.json_writer import JsonWriter
6
- from contentctl.objects.enums import SecurityContentType
7
- from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
8
- SecurityContentObject_Abstract,
9
- )
10
16
 
11
17
 
12
18
 
13
19
  class ApiJsonOutput:
20
+ output_path: pathlib.Path
21
+ app_label: str
22
+
23
+ def __init__(self, output_path:pathlib.Path, app_label: str):
24
+ self.output_path = output_path
25
+ self.app_label = app_label
14
26
 
15
- def writeObjects(
27
+ def writeDetections(
16
28
  self,
17
- objects: list[SecurityContentObject_Abstract],
18
- output_path: pathlib.Path,
19
- app_label:str = "ESCU",
20
- contentType: SecurityContentType = None
29
+ objects: list[Detection],
21
30
  ) -> None:
22
- """#Serialize all objects
23
- try:
24
- for obj in objects:
25
-
26
- serialized_objects.append(obj.model_dump())
27
- except Exception as e:
28
- raise Exception(f"Error serializing object with name '{obj.name}' and type '{type(obj).__name__}': '{str(e)}'")
29
- """
30
-
31
- if contentType == SecurityContentType.detections:
32
- detections = [
33
- detection.model_dump(
34
- include=set(
35
- [
36
- "name",
37
- "author",
38
- "date",
39
- "version",
40
- "id",
41
- "description",
42
- "tags",
43
- "search",
44
- "how_to_implement",
45
- "known_false_positives",
46
- "references",
47
- "datamodel",
48
- "macros",
49
- "lookups",
50
- "source",
51
- "nes_fields",
52
- ]
53
- )
31
+ detections = [
32
+ detection.model_dump(
33
+ include=set(
34
+ [
35
+ "name",
36
+ "author",
37
+ "date",
38
+ "version",
39
+ "id",
40
+ "description",
41
+ "tags",
42
+ "search",
43
+ "how_to_implement",
44
+ "known_false_positives",
45
+ "references",
46
+ "datamodel",
47
+ "macros",
48
+ "lookups",
49
+ "source",
50
+ "nes_fields",
51
+ ]
54
52
  )
55
- for detection in objects
56
- ]
57
- #Only a subset of macro fields are required:
58
- # for detection in detections:
59
- # new_macros = []
60
- # for macro in detection.get("macros",[]):
61
- # new_macro_fields = {}
62
- # new_macro_fields["name"] = macro.get("name")
63
- # new_macro_fields["definition"] = macro.get("definition")
64
- # new_macro_fields["description"] = macro.get("description")
65
- # if len(macro.get("arguments", [])) > 0:
66
- # new_macro_fields["arguments"] = macro.get("arguments")
67
- # new_macros.append(new_macro_fields)
68
- # detection["macros"] = new_macros
69
- # del()
70
-
71
-
72
- JsonWriter.writeJsonObject(
73
- os.path.join(output_path, "detections.json"), "detections", detections
74
- )
75
-
76
- elif contentType == SecurityContentType.macros:
77
- macros = [
78
- macro.model_dump(include=set(["definition", "description", "name"]))
79
- for macro in objects
80
- ]
81
- for macro in macros:
82
- for k in ["author", "date","version","id","references"]:
83
- if k in macro:
84
- del(macro[k])
85
- JsonWriter.writeJsonObject(
86
- os.path.join(output_path, "macros.json"), "macros", macros
87
53
  )
88
-
89
- elif contentType == SecurityContentType.stories:
90
- stories = [
91
- story.model_dump(
92
- include=set(
93
- [
94
- "name",
95
- "author",
96
- "date",
97
- "version",
98
- "id",
99
- "description",
100
- "narrative",
101
- "references",
102
- "tags",
103
- "detections_names",
104
- "investigation_names",
105
- "baseline_names",
106
- "detections",
107
- ]
108
- )
109
- )
110
- for story in objects
111
- ]
112
- # Only get certain fields from detections
113
- for story in stories:
114
- # Only use a small subset of fields from the detection
115
- story["detections"] = [
116
- {
117
- "name": detection["name"],
118
- "source": detection["source"],
119
- "type": detection["type"],
120
- "tags": detection["tags"].get("mitre_attack_enrichments", []),
121
- }
122
- for detection in story["detections"]
123
- ]
124
- story["detection_names"] = [f"{app_label} - {name} - Rule" for name in story["detection_names"]]
54
+ for detection in objects
55
+ ]
56
+ #Only a subset of macro fields are required:
57
+ # for detection in detections:
58
+ # new_macros = []
59
+ # for macro in detection.get("macros",[]):
60
+ # new_macro_fields = {}
61
+ # new_macro_fields["name"] = macro.get("name")
62
+ # new_macro_fields["definition"] = macro.get("definition")
63
+ # new_macro_fields["description"] = macro.get("description")
64
+ # if len(macro.get("arguments", [])) > 0:
65
+ # new_macro_fields["arguments"] = macro.get("arguments")
66
+ # new_macros.append(new_macro_fields)
67
+ # detection["macros"] = new_macros
68
+ # del()
125
69
 
126
-
127
- JsonWriter.writeJsonObject(
128
- os.path.join(output_path, "stories.json"), "stories", stories
70
+
71
+ JsonWriter.writeJsonObject(
72
+ os.path.join(self.output_path, "detections.json"), "detections", detections
73
+ )
74
+
75
+ def writeMacros(
76
+ self,
77
+ objects: list[Macro],
78
+ ) -> None:
79
+ macros = [
80
+ macro.model_dump(include=set(["definition", "description", "name"]))
81
+ for macro in objects
82
+ ]
83
+ for macro in macros:
84
+ for k in ["author", "date","version","id","references"]:
85
+ if k in macro:
86
+ del(macro[k])
87
+ JsonWriter.writeJsonObject(
88
+ os.path.join(self.output_path, "macros.json"), "macros", macros
89
+ )
90
+
91
+ def writeStories(
92
+ self,
93
+ objects: list[Story],
94
+ ) -> None:
95
+ stories = [
96
+ story.model_dump(
97
+ include=set(
98
+ [
99
+ "name",
100
+ "author",
101
+ "date",
102
+ "version",
103
+ "id",
104
+ "description",
105
+ "narrative",
106
+ "references",
107
+ "tags",
108
+ "detections_names",
109
+ "investigation_names",
110
+ "baseline_names",
111
+ "detections",
112
+ ]
113
+ )
129
114
  )
115
+ for story in objects
116
+ ]
117
+ # Only get certain fields from detections
118
+ for story in stories:
119
+ # Only use a small subset of fields from the detection
120
+ story["detections"] = [
121
+ {
122
+ "name": detection["name"],
123
+ "source": detection["source"],
124
+ "type": detection["type"],
125
+ "tags": detection["tags"].get("mitre_attack_enrichments", []),
126
+ }
127
+ for detection in story["detections"]
128
+ ]
129
+ story["detection_names"] = [f"{self.app_label} - {name} - Rule" for name in story["detection_names"]]
130
+
130
131
 
131
- elif contentType == SecurityContentType.baselines:
132
- try:
133
- baselines = [
134
- baseline.model_dump(
135
- include=set(
136
- [
137
- "name",
138
- "author",
139
- "date",
140
- "version",
141
- "id",
142
- "description",
143
- "type",
144
- "datamodel",
145
- "search",
146
- "how_to_implement",
147
- "known_false_positives",
148
- "references",
149
- "tags",
150
- ]
151
- )
152
- )
153
- for baseline in objects
154
- ]
155
- except Exception as e:
156
- print(e)
157
- print('wait')
132
+ JsonWriter.writeJsonObject(
133
+ os.path.join(self.output_path, "stories.json"), "stories", stories
134
+ )
158
135
 
159
- JsonWriter.writeJsonObject(
160
- os.path.join(output_path, "baselines.json"), "baselines", baselines
136
+ def writeBaselines(
137
+ self,
138
+ objects: list[Baseline],
139
+ ) -> None:
140
+ baselines = [
141
+ baseline.model_dump(
142
+ include=set(
143
+ [
144
+ "name",
145
+ "author",
146
+ "date",
147
+ "version",
148
+ "id",
149
+ "description",
150
+ "type",
151
+ "datamodel",
152
+ "search",
153
+ "how_to_implement",
154
+ "known_false_positives",
155
+ "references",
156
+ "tags",
157
+ ]
161
158
  )
159
+ )
160
+ for baseline in objects
161
+ ]
162
+
163
+ JsonWriter.writeJsonObject(
164
+ os.path.join(self.output_path, "baselines.json"), "baselines", baselines
165
+ )
162
166
 
163
- elif contentType == SecurityContentType.investigations:
164
- investigations = [
165
- investigation.model_dump(
166
- include=set(
167
- [
168
- "name",
169
- "author",
170
- "date",
171
- "version",
172
- "id",
173
- "description",
174
- "type",
175
- "datamodel",
176
- "search",
177
- "how_to_implemnet",
178
- "known_false_positives",
179
- "references",
180
- "inputs",
181
- "tags",
182
- "lowercase_name",
183
- ]
184
- )
167
+ def writeInvestigations(
168
+ self,
169
+ objects: list[Investigation],
170
+ ) -> None:
171
+ investigations = [
172
+ investigation.model_dump(
173
+ include=set(
174
+ [
175
+ "name",
176
+ "author",
177
+ "date",
178
+ "version",
179
+ "id",
180
+ "description",
181
+ "type",
182
+ "datamodel",
183
+ "search",
184
+ "how_to_implemnet",
185
+ "known_false_positives",
186
+ "references",
187
+ "inputs",
188
+ "tags",
189
+ "lowercase_name",
190
+ ]
185
191
  )
186
- for investigation in objects
187
- ]
188
- JsonWriter.writeJsonObject(
189
- os.path.join(output_path, "response_tasks.json"),
190
- "response_tasks",
191
- investigations,
192
192
  )
193
+ for investigation in objects
194
+ ]
195
+ JsonWriter.writeJsonObject(
196
+ os.path.join(self.output_path, "response_tasks.json"),
197
+ "response_tasks",
198
+ investigations,
199
+ )
193
200
 
194
- elif contentType == SecurityContentType.lookups:
195
- lookups = [
196
- lookup.model_dump(
197
- include=set(
198
- [
199
- "name",
200
- "description",
201
- "collection",
202
- "fields_list",
203
- "filename",
204
- "default_match",
205
- "match_type",
206
- "min_matches",
207
- "case_sensitive_match",
208
- ]
209
- )
201
+ def writeLookups(
202
+ self,
203
+ objects: list[Lookup],
204
+ ) -> None:
205
+ lookups = [
206
+ lookup.model_dump(
207
+ include=set(
208
+ [
209
+ "name",
210
+ "description",
211
+ "collection",
212
+ "fields_list",
213
+ "filename",
214
+ "default_match",
215
+ "match_type",
216
+ "min_matches",
217
+ "case_sensitive_match",
218
+ ]
210
219
  )
211
- for lookup in objects
212
- ]
213
- for lookup in lookups:
214
- for k in ["author","date","version","id","references"]:
215
- if k in lookup:
216
- del(lookup[k])
217
- JsonWriter.writeJsonObject(
218
- os.path.join(output_path, "lookups.json"), "lookups", lookups
219
220
  )
221
+ for lookup in objects
222
+ ]
223
+ for lookup in lookups:
224
+ for k in ["author","date","version","id","references"]:
225
+ if k in lookup:
226
+ del(lookup[k])
227
+ JsonWriter.writeJsonObject(
228
+ os.path.join(self.output_path, "lookups.json"), "lookups", lookups
229
+ )
220
230
 
221
- elif contentType == SecurityContentType.deployments:
222
- deployments = [
223
- deployment.model_dump(
224
- include=set(
225
- [
226
- "name",
227
- "author",
228
- "date",
229
- "version",
230
- "id",
231
- "description",
232
- "scheduling",
233
- "rba",
234
- "tags"
235
- ]
236
- )
231
+ def writeDeployments(
232
+ self,
233
+ objects: list[Deployment],
234
+ ) -> None:
235
+ deployments = [
236
+ deployment.model_dump(
237
+ include=set(
238
+ [
239
+ "name",
240
+ "author",
241
+ "date",
242
+ "version",
243
+ "id",
244
+ "description",
245
+ "scheduling",
246
+ "rba",
247
+ "tags"
248
+ ]
237
249
  )
238
- for deployment in objects
239
- ]
240
- #references are not to be included, but have been deleted in the
241
- #model_serialization logic
242
- JsonWriter.writeJsonObject(
243
- os.path.join(output_path, "deployments.json"),
244
- "deployments",
245
- deployments,
246
- )
250
+ )
251
+ for deployment in objects
252
+ ]
253
+ #references are not to be included, but have been deleted in the
254
+ #model_serialization logic
255
+ JsonWriter.writeJsonObject(
256
+ os.path.join(self.output_path, "deployments.json"),
257
+ "deployments",
258
+ deployments,
259
+ )
@@ -1,22 +1,22 @@
1
- from dataclasses import dataclass
2
- import os
3
- import glob
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Callable
3
+ if TYPE_CHECKING:
4
+ from contentctl.objects.detection import Detection
5
+ from contentctl.objects.lookup import Lookup
6
+ from contentctl.objects.macro import Macro
7
+ from contentctl.objects.dashboard import Dashboard
8
+ from contentctl.objects.story import Story
9
+ from contentctl.objects.baseline import Baseline
10
+ from contentctl.objects.investigation import Investigation
11
+
12
+ from contentctl.objects.lookup import FileBackedLookup
4
13
  import shutil
5
- import sys
6
14
  import tarfile
7
- from typing import Union
8
- from pathlib import Path
9
15
  import pathlib
10
- import time
11
16
  import timeit
12
17
  import datetime
13
- import shutil
14
- import json
15
18
  from contentctl.output.conf_writer import ConfWriter
16
- from contentctl.objects.enums import SecurityContentType
17
19
  from contentctl.objects.config import build
18
- from requests import Session, post, get
19
- from requests.auth import HTTPBasicAuth
20
20
 
21
21
  class ConfOutput:
22
22
  config: build
@@ -80,25 +80,33 @@ class ConfOutput:
80
80
  return written_files
81
81
 
82
82
 
83
- def writeObjects(self, objects: list, type: SecurityContentType = None) -> set[pathlib.Path]:
83
+ def writeDetections(self, objects:list[Detection]) -> set[pathlib.Path]:
84
84
  written_files:set[pathlib.Path] = set()
85
- if type == SecurityContentType.detections:
86
- for output_app_path, template_name in [ ('default/savedsearches.conf', 'savedsearches_detections.j2'),
87
- ('default/analyticstories.conf', 'analyticstories_detections.j2')]:
88
- written_files.add(ConfWriter.writeConfFile(pathlib.Path(output_app_path),
89
- template_name, self.config, objects))
90
-
91
- elif type == SecurityContentType.stories:
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))
89
+ return written_files
90
+
91
+
92
+ def writeStories(self, objects:list[Story]) -> set[pathlib.Path]:
93
+ written_files:set[pathlib.Path] = set()
92
94
  written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/analyticstories.conf'),
93
95
  'analyticstories_stories.j2',
94
96
  self.config, objects))
97
+ return written_files
98
+
95
99
 
96
- elif type == SecurityContentType.baselines:
100
+ def writeBaselines(self, objects:list[Baseline]) -> set[pathlib.Path]:
101
+ written_files:set[pathlib.Path] = set()
97
102
  written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/savedsearches.conf'),
98
103
  'savedsearches_baselines.j2',
99
104
  self.config, objects))
105
+ return written_files
106
+
100
107
 
101
- elif type == SecurityContentType.investigations:
108
+ def writeInvestigations(self, objects:list[Investigation]) -> set[pathlib.Path]:
109
+ written_files:set[pathlib.Path] = set()
102
110
  for output_app_path, template_name in [ ('default/savedsearches.conf', 'savedsearches_investigations.j2'),
103
111
  ('default/analyticstories.conf', 'analyticstories_investigations.j2')]:
104
112
  ConfWriter.writeConfFile(pathlib.Path(output_app_path),
@@ -106,7 +114,7 @@ class ConfOutput:
106
114
  self.config,
107
115
  objects)
108
116
 
109
- workbench_panels = []
117
+ workbench_panels:list[Investigation] = []
110
118
  for investigation in objects:
111
119
  if investigation.inputs:
112
120
  response_file_name_xml = investigation.lowercase_name + "___response_task.xml"
@@ -128,8 +136,11 @@ class ConfOutput:
128
136
  template_name,
129
137
  self.config,
130
138
  workbench_panels))
139
+ return written_files
140
+
131
141
 
132
- elif type == SecurityContentType.lookups:
142
+ def writeLookups(self, objects:list[Lookup]) -> set[pathlib.Path]:
143
+ written_files:set[pathlib.Path] = set()
133
144
  for output_app_path, template_name in [ ('default/collections.conf', 'collections.j2'),
134
145
  ('default/transforms.conf', 'transforms.j2')]:
135
146
  written_files.add(ConfWriter.writeConfFile(pathlib.Path(output_app_path),
@@ -137,9 +148,7 @@ class ConfOutput:
137
148
  self.config,
138
149
  objects))
139
150
 
140
-
141
- #we want to copy all *.mlmodel files as well, not just csvs
142
- files = list(glob.iglob(str(self.config.path/ 'lookups/*.csv'))) + list(glob.iglob(str(self.config.path / 'lookups/*.mlmodel')))
151
+ #Get the path to the lookups folder
143
152
  lookup_folder = self.config.getPackageDirectoryPath()/"lookups"
144
153
 
145
154
  # Make the new folder for the lookups
@@ -147,26 +156,24 @@ class ConfOutput:
147
156
  lookup_folder.mkdir(exist_ok=True)
148
157
 
149
158
  #Copy each lookup into the folder
150
- for lookup_name in files:
151
- lookup_path = pathlib.Path(lookup_name)
152
- if lookup_path.is_file():
153
- shutil.copy(lookup_path, lookup_folder/lookup_path.name)
154
- else:
155
- raise(Exception(f"Error copying lookup/mlmodel file. Path {lookup_path} does not exist or is not a file."))
156
-
157
- elif type == SecurityContentType.macros:
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()
158
167
  written_files.add(ConfWriter.writeConfFile(pathlib.Path('default/macros.conf'),
159
168
  'macros.j2',
160
169
  self.config, objects))
161
-
162
- elif type == SecurityContentType.dashboards:
163
- written_files.update(ConfWriter.writeDashboardFiles(self.config, objects))
164
-
165
-
166
- return written_files
167
-
168
-
170
+ return written_files
171
+
169
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
170
177
 
171
178
 
172
179
  def packageAppTar(self) -> None:
@@ -202,7 +209,7 @@ class ConfOutput:
202
209
 
203
210
 
204
211
 
205
- def packageApp(self, method=packageAppTar)->None:
212
+ def packageApp(self, method: Callable[[ConfOutput],None]=packageAppTar)->None:
206
213
  return method(self)
207
214
 
208
215