contentctl 3.6.0__py3-none-any.whl → 4.0.2__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 (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
@@ -1,67 +1,205 @@
1
+ from typing import Any
1
2
  import datetime
3
+ import re
2
4
  import os
5
+ import json
6
+ import configparser
3
7
  from xmlrpc.client import APPLICATION_ERROR
4
8
  from jinja2 import Environment, FileSystemLoader, StrictUndefined
5
9
  import pathlib
6
10
  from contentctl.objects.security_content_object import SecurityContentObject
7
- from contentctl.objects.config import Config
11
+ from contentctl.objects.config import build
12
+ import xml.etree.ElementTree as ET
8
13
 
9
14
  class ConfWriter():
10
15
 
11
16
  @staticmethod
12
- def writeConfFileHeader(output_path:pathlib.Path, config: Config) -> None:
13
- utc_time = datetime.datetime.utcnow().replace(microsecond=0).isoformat()
17
+ def custom_jinja2_enrichment_filter(string:str, object:SecurityContentObject):
18
+ substitutions = re.findall(r"%[^%]*%", string)
19
+ updated_string = string
20
+ for sub in substitutions:
21
+ sub_without_percents = sub.replace("%","")
22
+ if hasattr(object, sub_without_percents):
23
+ updated_string = updated_string.replace(sub, str(getattr(object, sub_without_percents)))
24
+ elif hasattr(object,'tags') and hasattr(object.tags, sub_without_percents):
25
+ updated_string = updated_string.replace(sub, str(getattr(object.tags, sub_without_percents)))
26
+ else:
27
+ raise Exception(f"Unable to find field {sub} in object {object.name}")
28
+
29
+ return updated_string
30
+
31
+ @staticmethod
32
+ def escapeNewlines(obj:Any):
33
+ # Ensure that any newlines that occur in a string are escaped with a \.
34
+ # Failing to do so will result in an improperly formatted conf files that
35
+ # cannot be parsed
36
+ if isinstance(obj,str):
37
+ return obj.replace(f"\n"," \\\n")
38
+ else:
39
+ return obj
40
+
41
+
42
+ @staticmethod
43
+ def writeConfFileHeader(app_output_path:pathlib.Path, config: build) -> pathlib.Path:
44
+ output = ConfWriter.writeFileHeader(app_output_path, config)
45
+
46
+ output_path = config.getPackageDirectoryPath()/app_output_path
47
+ output_path.parent.mkdir(parents=True, exist_ok=True)
48
+ with open(output_path, 'w') as f:
49
+ output = output.encode('utf-8', 'ignore').decode('utf-8')
50
+ f.write(output)
51
+
52
+ #Ensure that the conf file we just generated/update is syntactically valid
53
+ ConfWriter.validateConfFile(output_path)
54
+ return output_path
55
+
56
+ @staticmethod
57
+ def writeManifestFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> pathlib.Path:
58
+ j2_env = ConfWriter.getJ2Environment()
59
+ template = j2_env.get_template(template_name)
60
+
61
+ output = template.render(objects=objects, APP_NAME=config.app.label, currentDate=datetime.datetime.now(datetime.UTC).date().isoformat())
62
+
63
+ output_path = config.getPackageDirectoryPath()/app_output_path
64
+ output_path.parent.mkdir(parents=True, exist_ok=True)
65
+ with open(output_path, 'w') as f:
66
+ output = output.encode('utf-8', 'ignore').decode('utf-8')
67
+ f.write(output)
68
+ return output_path
69
+
70
+
71
+ @staticmethod
72
+ def writeFileHeader(app_output_path:pathlib.Path, config: build) -> str:
73
+ #Do not output microseconds or +00:000 at the end of the datetime string
74
+ utc_time = datetime.datetime.now(datetime.UTC).replace(microsecond=0,tzinfo=None).isoformat()
75
+
14
76
  j2_env = Environment(
15
77
  loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
16
78
  trim_blocks=True)
17
79
 
18
80
  template = j2_env.get_template('header.j2')
19
- output = template.render(time=utc_time, author=' - '.join([config.build.author_name,config.build.author_company]), author_email=config.build.author_email)
81
+ output = template.render(time=utc_time, author=' - '.join([config.app.author_name,config.app.author_company]), author_email=config.app.author_email)
82
+
83
+ return output
84
+
85
+
86
+
87
+ @staticmethod
88
+ def writeXmlFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> None:
89
+
90
+
91
+ j2_env = ConfWriter.getJ2Environment()
92
+ template = j2_env.get_template(template_name)
93
+
94
+ output = template.render(objects=objects, APP_NAME=config.app.label)
95
+
96
+ output_path = config.getPackageDirectoryPath()/app_output_path
20
97
  output_path.parent.mkdir(parents=True, exist_ok=True)
21
- with open(output_path, 'w') as f:
22
- output = output.encode('ascii', 'ignore').decode('ascii')
98
+ with open(output_path, 'a') as f:
99
+ output = output.encode('utf-8', 'ignore').decode('utf-8')
23
100
  f.write(output)
101
+
102
+ #Ensure that the conf file we just generated/update is syntactically valid
103
+ ConfWriter.validateXmlFile(output_path)
104
+
105
+
24
106
 
25
107
 
26
108
  @staticmethod
27
- def writeConfFileHeaderEmpty(output_path:pathlib.Path, config: Config) -> None:
109
+ def writeXmlFileHeader(app_output_path:pathlib.Path, config: build) -> None:
110
+ output = ConfWriter.writeFileHeader(app_output_path, config)
111
+ output_with_xml_comment = f"<!--\n{output}-->\n"
112
+
113
+ output_path = config.getPackageDirectoryPath()/app_output_path
28
114
  output_path.parent.mkdir(parents=True, exist_ok=True)
29
115
  with open(output_path, 'w') as f:
30
- f.write('')
116
+ output_with_xml_comment = output_with_xml_comment.encode('utf-8', 'ignore').decode('utf-8')
117
+ f.write(output_with_xml_comment)
118
+
119
+ # We INTENTIONALLY do not validate the comment we wrote to the header. This is because right now,
120
+ # the file is an empty XML document (besides the commented header). This means that it will FAIL validation
31
121
 
32
122
 
33
123
  @staticmethod
34
- def writeConfFile(output_path:pathlib.Path, template_name : str, config: Config, objects : list) -> None:
35
- def custom_jinja2_enrichment_filter(string, object):
36
- customized_string = string
37
-
38
- for key in dir(object):
39
- if type(key) is not str:
40
- key = key.decode()
41
- if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object, key)):
42
- if hasattr(object, key):
43
- customized_string = customized_string.replace("%" + key + "%", str(getattr(object, key)))
44
-
45
- for key in dir(object.tags):
46
- if type(key) is not str:
47
- key = key.decode()
48
- if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object.tags, key)):
49
- if hasattr(object.tags, key):
50
- customized_string = customized_string.replace("%" + key + "%", str(getattr(object.tags, key)))
51
-
52
- return customized_string
53
-
124
+ def getJ2Environment()->Environment:
54
125
  j2_env = Environment(
55
126
  loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
56
127
  trim_blocks=True,
57
128
  undefined=StrictUndefined)
129
+ j2_env.globals.update(objectListToNameList=SecurityContentObject.objectListToNameList)
130
+
58
131
 
132
+ j2_env.filters['custom_jinja2_enrichment_filter'] = ConfWriter.custom_jinja2_enrichment_filter
133
+ j2_env.filters['escapeNewlines'] = ConfWriter.escapeNewlines
134
+ return j2_env
59
135
 
60
- j2_env.filters['custom_jinja2_enrichment_filter'] = custom_jinja2_enrichment_filter
136
+ @staticmethod
137
+ def writeConfFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> pathlib.Path:
138
+ output_path = config.getPackageDirectoryPath()/app_output_path
139
+ j2_env = ConfWriter.getJ2Environment()
140
+
61
141
  template = j2_env.get_template(template_name)
62
- output = template.render(objects=objects, APP_NAME=config.build.prefix)
142
+ output = template.render(objects=objects, APP_NAME=config.app.label)
143
+
63
144
  output_path.parent.mkdir(parents=True, exist_ok=True)
64
145
  with open(output_path, 'a') as f:
65
- output = output.encode('ascii', 'ignore').decode('ascii')
146
+ output = output.encode('utf-8', 'ignore').decode('utf-8')
66
147
  f.write(output)
148
+ return output_path
149
+
150
+
151
+ @staticmethod
152
+ def validateConfFile(path:pathlib.Path):
153
+ """Ensure that the conf file is valid. We will do this by reading back
154
+ the conf using RawConfigParser to ensure that it does not throw any parsing errors.
155
+ This is particularly relevant because newlines contained in string fields may
156
+ break the formatting of the conf file if they have been incorrectly escaped with
157
+ the 'ConfWriter.escapeNewlines()' function.
158
+
159
+ If a conf file failes validation, we will throw an exception
160
+
161
+ Args:
162
+ path (pathlib.Path): path to the conf file to validate
163
+ """
164
+ return
165
+ if path.suffix != ".conf":
166
+ #there may be some other files built, so just ignore them
167
+ return
168
+ try:
169
+ _ = configparser.RawConfigParser().read(path)
170
+ except Exception as e:
171
+ raise Exception(f"Failed to validate .conf file {str(path)}: {str(e)}")
172
+
173
+ @staticmethod
174
+ def validateXmlFile(path:pathlib.Path):
175
+ """Ensure that the XML file is valid XML.
176
+
177
+ Args:
178
+ path (pathlib.Path): path to the xml file to validate
179
+ """
180
+
181
+ try:
182
+ with open(path, 'r') as xmlFile:
183
+ _ = ET.fromstring(xmlFile.read())
184
+ except Exception as e:
185
+ raise Exception(f"Failed to validate .xml file {str(path)}: {str(e)}")
186
+
187
+
188
+ @staticmethod
189
+ def validateManifestFile(path:pathlib.Path):
190
+ """Ensure that the Manifest file is valid JSON.
191
+
192
+ Args:
193
+ path (pathlib.Path): path to the manifest JSON file to validate
194
+ """
195
+ return
196
+ try:
197
+ with open(path, 'r') as manifestFile:
198
+ _ = json.load(manifestFile)
199
+ except Exception as e:
200
+ raise Exception(f"Failed to validate .manifest file {str(path)} (Note that .manifest files should contain only valid JSON-formatted data): {str(e)}")
201
+
202
+
203
+
204
+
67
205
 
@@ -1,5 +1,5 @@
1
1
  import os
2
-
2
+ from typing import Any
3
3
  from jinja2 import Environment, FileSystemLoader
4
4
 
5
5
 
@@ -20,7 +20,7 @@ class JinjaWriter:
20
20
 
21
21
 
22
22
  @staticmethod
23
- def writeObject(template_name : str, output_path : str, object : dict) -> None:
23
+ def writeObject(template_name : str, output_path : str, object: dict[str,Any]) -> None:
24
24
 
25
25
  j2_env = Environment(
26
26
  loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
@@ -1,9 +1,21 @@
1
1
  import json
2
-
3
-
2
+ from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract
3
+ from typing import List
4
+ from io import TextIOWrapper
4
5
  class JsonWriter():
5
6
 
6
7
  @staticmethod
7
- def writeJsonObject(file_path : str, obj) -> None:
8
- with open(file_path, 'w') as outfile:
9
- json.dump(obj, outfile, ensure_ascii=False)
8
+ def writeJsonObject(file_path : str, object_name: str, objs: List[dict],readable_output=False) -> None:
9
+ try:
10
+ with open(file_path, 'w') as outfile:
11
+ if readable_output:
12
+ # At the cost of slightly larger filesize, improve the redability significantly
13
+ # by sorting and indenting keys/values
14
+ sorted_objs = sorted(objs, key=lambda o: o['name'])
15
+ json.dump({object_name:sorted_objs}, outfile, ensure_ascii=False, indent=2)
16
+ else:
17
+ json.dump({object_name:objs}, outfile, ensure_ascii=False)
18
+
19
+ except Exception as e:
20
+ raise Exception(f"Error serializing object to Json File '{file_path}': {str(e)}")
21
+
@@ -2,17 +2,18 @@ import os
2
2
  import pathlib
3
3
  from contentctl.objects.enums import SecurityContentType
4
4
  from contentctl.output.yml_writer import YmlWriter
5
-
6
-
5
+ import pathlib
6
+ from contentctl.objects.config import NewContentType
7
7
  class NewContentYmlOutput():
8
- output_path: str
8
+ output_path: pathlib.Path
9
9
 
10
- def __init__(self, output_path:str):
10
+ def __init__(self, output_path:pathlib.Path):
11
11
  self.output_path = output_path
12
12
 
13
13
 
14
- def writeObjectNewContent(self, object: dict, subdirectory_name: str, type: SecurityContentType) -> None:
15
- if type == SecurityContentType.detections:
14
+ def writeObjectNewContent(self, object: dict, subdirectory_name: str, type: NewContentType) -> None:
15
+ if type == NewContentType.detection:
16
+
16
17
  file_path = os.path.join(self.output_path, 'detections', subdirectory_name, self.convertNameToFileName(object['name'], object['tags']['product']))
17
18
  output_folder = pathlib.Path(self.output_path)/'detections'/subdirectory_name
18
19
  #make sure the output folder exists for this detection
@@ -21,7 +22,7 @@ class NewContentYmlOutput():
21
22
  YmlWriter.writeYmlFile(file_path, object)
22
23
  print("Successfully created detection " + file_path)
23
24
 
24
- elif type == SecurityContentType.stories:
25
+ elif type == NewContentType.story:
25
26
  file_path = os.path.join(self.output_path, 'stories', self.convertNameToFileName(object['name'], object['tags']['product']))
26
27
  YmlWriter.writeYmlFile(file_path, object)
27
28
  print("Successfully created story " + file_path)
@@ -1,15 +1,18 @@
1
1
  import os
2
2
  import pathlib
3
+ from typing import List, Any
3
4
 
4
5
  from contentctl.objects.enums import SecurityContentType
5
6
  from contentctl.output.jinja_writer import JinjaWriter
6
- from contentctl.objects.config import Config
7
7
  from contentctl.objects.enums import DetectionStatus
8
+ from contentctl.objects.detection import Detection
8
9
  class SvgOutput():
9
10
 
10
- def get_badge_dict(self, name:str, total_detections:list, these_detections:list):
11
- obj = dict()
11
+
12
+ def get_badge_dict(self, name:str, total_detections:List[Detection], these_detections:List[Detection])->dict[str,Any]:
13
+ obj:dict[str,Any] = {}
12
14
  obj['name'] = name
15
+
13
16
  if name == "Production":
14
17
  obj['color'] = "Green"
15
18
  elif name == "Detections":
@@ -26,40 +29,27 @@ class SvgOutput():
26
29
  obj['coverage'] = len(these_detections) / obj['count']
27
30
  obj['coverage'] = "{:.0%}".format(obj['coverage'])
28
31
  return obj
29
-
30
- def writeObjects(self, objects: list, path: str, type: SecurityContentType = None) -> None:
32
+
33
+ def writeObjects(self, detections: List[Detection], output_path: pathlib.Path, type: SecurityContentType = None) -> None:
31
34
 
32
- detections_tmp = objects
33
-
34
- output_path = pathlib.Path(path)
35
-
36
- production_detections = []
37
- deprecated_detections = []
38
- experimental_detections = []
39
- obj = dict()
40
35
 
41
- for detection in detections_tmp:
42
- if detection.status == DetectionStatus.production.value:
43
- production_detections.append(detection)
44
- if detection.status == DetectionStatus.deprecated.value:
45
- deprecated_detections.append(detection)
46
- elif detection.status == DetectionStatus.experimental.value:
47
- experimental_detections.append(detection)
48
36
 
37
+ total_dict:dict[str,Any] = self.get_badge_dict("Detections", detections, detections)
38
+ production_dict:dict[str,Any] = self.get_badge_dict("% Production", detections, [detection for detection in detections if detection.status == DetectionStatus.production.value])
39
+ #deprecated_dict = self.get_badge_dict("Deprecated", detections, [detection for detection in detections if detection.status == DetectionStatus.deprecated])
40
+ #experimental_dict = self.get_badge_dict("Experimental", detections, [detection for detection in detections if detection.status == DetectionStatus.experimental])
49
41
 
50
- total_detections = production_detections + deprecated_detections + experimental_detections
51
- total_dict = self.get_badge_dict("Detections", total_detections, production_detections)
52
- production_dict = self.get_badge_dict("Production", total_detections, production_detections)
53
- deprecated_dict = self.get_badge_dict("Deprecated", total_detections, deprecated_detections)
54
- experimental_dict = self.get_badge_dict("Experimental", total_detections, experimental_detections)
55
42
 
56
43
 
57
- JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'detection_count.svg'), total_dict)
44
+
45
+ #Total number of detections
46
+ JinjaWriter.writeObject('detection_count.j2', output_path /'detection_count.svg', total_dict)
58
47
  #JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'production_count.svg'), production_dict)
59
48
  #JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'deprecated_count.svg'), deprecated_dict)
60
49
  #JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'experimental_count.svg'), experimental_dict)
61
50
 
62
- JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), total_dict)
51
+ #Percentage of detections that are production
52
+ JinjaWriter.writeObject('detection_coverage.j2', output_path/'detection_coverage.svg', production_dict)
63
53
  #JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), deprecated_dict)
64
54
  #JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), experimental_dict)
65
55
 
@@ -5,17 +5,21 @@
5
5
  {% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %}
6
6
  [savedsearch://{{APP_NAME}} - {{ detection.name }} - Rule]
7
7
  type = detection
8
- asset_type = {{ detection.tags.asset_type }}
8
+ asset_type = {{ detection.tags.asset_type.value }}
9
9
  confidence = medium
10
- explanation = {{ detection.description }}
10
+ explanation = {{ detection.description | escapeNewlines() }}
11
11
  {% if detection.how_to_implement is defined %}
12
- how_to_implement = {{ detection.how_to_implement }}
12
+ how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
13
13
  {% else %}
14
14
  how_to_implement = none
15
15
  {% endif %}
16
16
  annotations = {{ detection.mappings | tojson }}
17
- known_false_positives = {{ detection.known_false_positives }}
17
+ known_false_positives = {{ detection.known_false_positives | escapeNewlines() }}
18
+ {% if detection.providing_technologies | length > 0 %}
18
19
  providing_technologies = {{ detection.providing_technologies | tojson }}
20
+ {% else %}
21
+ providing_technologies = null
22
+ {% endif %}
19
23
 
20
24
  {% endif %}
21
25
  {% endfor %}
@@ -7,7 +7,7 @@
7
7
  type = investigation
8
8
  explanation = none
9
9
  {% if detection.how_to_implement is defined %}
10
- how_to_implement = {{ detection.how_to_implement }}
10
+ how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
11
11
  {% else %}
12
12
  how_to_implement = none
13
13
  {% endif %}
@@ -4,16 +4,16 @@
4
4
 
5
5
  {% for story in objects %}
6
6
  [analytic_story://{{ story.name }}]
7
- category = {{ story.tags.category[0].value }}
7
+ category = {{ story.tags.getCategory_conf() }}
8
8
  last_updated = {{ story.date }}
9
9
  version = {{ story.version }}
10
- references = {{ story.references | tojson }}
11
- maintainers = [{"company": "{{ story.author_company }}", "email": "-", "name": "{{ story.author_name }}"}]
10
+ references = {{ story.getReferencesListForJson() | tojson }}
11
+ maintainers = [{"company": "{{ story.author_company }}", "email": "{{ story.author_email }}", "name": "{{ story.author_name }}"}]
12
12
  spec_version = 3
13
- searches = {{ (story.detection_names + story.investigation_names) | tojson }}
14
- description = {{ story.description }}
13
+ searches = {{ story.storyAndInvestigationNamesWithApp(APP_NAME) | tojson }}
14
+ description = {{ story.description | escapeNewlines() }}
15
15
  {% if story.narrative is defined %}
16
- narrative = {{ story.narrative }}
16
+ narrative = {{ story.narrative | escapeNewlines() }}
17
17
  {% endif %}
18
18
 
19
19
  {% endfor %}
@@ -21,14 +21,14 @@ reload.es_investigations = simple
21
21
  [launcher]
22
22
  author = {{ objects[0].author_company }}
23
23
  version = {{ objects[0].version }}
24
- description = {{ objects[0].description }}
24
+ description = {{ objects[0].description | escapeNewlines() }}
25
25
 
26
26
  [ui]
27
27
  is_visible = true
28
28
  label = {{ objects[0].title }}
29
29
 
30
30
  [package]
31
- id = {{ objects[0].name }}
31
+ id = {{ objects[0].appid }}
32
32
 
33
33
 
34
34
 
@@ -4,7 +4,7 @@
4
4
  "title": "{{ objects[0].title }}",
5
5
  "id": {
6
6
  "group": null,
7
- "name": "{{ objects[0].name }}",
7
+ "name": "{{ objects[0].appid }}",
8
8
  "version": "{{ objects[0].version }}"
9
9
  },
10
10
  "author": [
@@ -14,7 +14,7 @@
14
14
  "company": "{{ objects[0].author_company }}"
15
15
  }
16
16
  ],
17
- "releaseDate": null,
17
+ "releaseDate": "{{ currentDate }}",
18
18
  "description": "{{ objects[0].description }}",
19
19
  "classification": {
20
20
  "intendedAudience": null,
@@ -1,18 +1,16 @@
1
1
  <?xml version="1.0"?>
2
- <svg xmlns="http://www.w3.org/2000/svg" width="100" height="20">
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="130" height="20">
3
3
  <linearGradient id="a" x2="0" y2="100%">
4
4
  <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
5
5
  <stop offset="2" stop-opacity=".1"/>
6
6
  </linearGradient>
7
7
 
8
- <rect rx="3" width="60" height="20" fill="#555"/> <!-- Comment -->
9
- <rect rx="3" x="60" width="40" height="20" fill="#4c1"/>
10
-
11
- <path fill="#4c1" d="M58 0h4v20h-4z"/>
8
+ <rect rx="3" width="90" height="20" fill="#555"/> <!-- Comment -->
9
+ <rect rx="3" x="90" width="40" height="20" fill="#4c1"/>
12
10
 
13
11
  <rect rx="3" width="100" height="20" fill="url(#a)"/>
14
- <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
- <text x="30" y="14">coverage</text>
16
- <text x="80" y="14">{{ object.coverage }}</text>
12
+ <g fill="#fff" text-anchor="left" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
13
+ <text x="5" y="14">% Production</text>
14
+ <text x="100" y="14">{{object.coverage}}</text>
17
15
  </g>
18
16
  </svg>
@@ -12,8 +12,8 @@ sidebar:
12
12
  | -------------- | --------------- | --------------- |
13
13
  {%- for detection in objects -%}
14
14
  {% if detection.tags.mitre_attack_enrichments %}
15
- | [{{ detection.name }}](/{{ detection.source }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% for attack in detection.tags.mitre_attack_enrichments -%} [{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last -%}, {% endif -%}{%- endfor %} | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
15
+ | [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% for attack in detection.tags.mitre_attack_enrichments -%} [{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last -%}, {% endif -%}{%- endfor %} | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
16
16
  {%- else %}
17
- | [{{ detection.name }}](/{{ detection.source }}/{{ detection.name | lower | replace(' ', '_') }}/) | None | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
17
+ | [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | None | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
18
18
  {%- endif -%}
19
19
  {%- endfor -%}
@@ -5,7 +5,7 @@ excerpt: "{% if object.tags.mitre_attack_enrichments %}{% for attack in object.t
5
5
  {% if not loop.last -%}, {% endif -%}
6
6
  {% endfor %}{% endif -%}"
7
7
  categories:
8
- - {{object.source|capitalize}}
8
+ - {{object.getSource()|capitalize}}
9
9
  last_modified_at: {{object.date}}
10
10
  toc: true
11
11
  toc_label: ""
@@ -207,4 +207,4 @@ Alternatively you can replay a dataset into a [Splunk Attack Range](https://gith
207
207
  {% endfor %}
208
208
  {% endif %}
209
209
 
210
- [*source*](https://github.com/splunk/security_content/tree/develop/detections/{% if object.experimental is sameas true -%}experimental/{%- endif -%}{{object.source}}/{{ object.name | lower | replace (" ", "_") | replace("-", "_") }}.yml) \| *version*: **{{object.version}}**
210
+ [*source*](https://github.com/splunk/security_content/tree/develop/detections/{% if object.experimental is sameas true -%}experimental/{%- endif -%}{{object.getSource()}}/{{ object.name | lower | replace (" ", "_") | replace("-", "_") }}.yml) \| *version*: **{{object.version}}**
@@ -37,7 +37,7 @@ tags:
37
37
  | ----------- | ----------- |--------------|
38
38
  {%- if object.detections %}
39
39
  {%- for detection in object.detections %}
40
- | [{{ detection.name }}](/{{ detection.source }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% if detection.tags.mitre_attack_enrichments %}{% for attack in detection.tags.mitre_attack_enrichments -%}[{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last %}, {% endif %}{%- endfor %}{% else %}None{%- endif -%} | {{ detection.type }} |
40
+ | [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% if detection.tags.mitre_attack_enrichments %}{% for attack in detection.tags.mitre_attack_enrichments -%}[{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last %}, {% endif %}{%- endfor %}{% else %}None{%- endif -%} | {{ detection.type }} |
41
41
  {%- endfor %}
42
42
  {%- endif %}
43
43
 
@@ -2,7 +2,7 @@
2
2
  {% for response_task in objects %}
3
3
  [panel://workbench_panel_{{ response_task.lowercase_name }}___response_task]
4
4
  label = {{ response_task.name }}
5
- description = {{ response_task.description }}
5
+ description = {{ response_task.description | escapeNewlines() }}
6
6
  disabled = 0
7
7
  tokens = {\
8
8
  {% for token in response_task.inputs %}
@@ -2,7 +2,7 @@
2
2
  {% for story in objects %}
3
3
  [panel_group://workbench_panel_group_{{ story.lowercase_name}}]
4
4
  label = {{ story.name }}
5
- description = {{ story.description }}
5
+ description = {{ story.description | escapeNewlines() }}
6
6
  disabled = 0
7
7
 
8
8
  {% if story.workbench_panels is defined %}
@@ -1,5 +1,6 @@
1
1
  #############
2
- # Automatically generated by generator.py in splunk/security_content
2
+ # Automatically generated by 'contentctl build' from
3
+ # https://github.com/splunk/contentctl
3
4
  # On Date: {{ time }} UTC
4
5
  # Author: {{ author }}
5
6
  # Contact: {{ author_email }}
@@ -1,15 +1,11 @@
1
1
 
2
2
  {% for macro in objects %}
3
- [{{ macro.name }}{% if macro.arguments is not none %}({{ macro.arguments|length }}){% endif %}]
4
- {% if macro.arguments is not none %}
5
- args = {% for arg in macro.arguments %}{{ arg }}{{ ", " if not loop.last }}
6
- {% endfor %}
7
- {% endif %}
8
- {% if macro.definition is not none %}
9
- definition = {{ macro.definition }}
10
- {% else %}
11
- definition =
3
+ [{{ macro.name }}{% if macro.arguments | length > 0 %}({{ macro.arguments|length }}){% endif %}]
4
+ {% if macro.arguments | length > 0 %}
5
+ args = {% for arg in macro.arguments %}{{ arg }}{{ ", " if not loop.last }}{% endfor %}
6
+
12
7
  {% endif %}
13
- description = {{ macro.description }}
8
+ definition = {{ macro.definition | escapeNewlines() }}
9
+ description = {{ macro.description | escapeNewlines() }}
14
10
 
15
11
  {% endfor %}
@@ -9,11 +9,11 @@ action.escu = 0
9
9
  action.escu.enabled = 1
10
10
  action.escu.search_type = support
11
11
  action.escu.full_search_name = {{APP_NAME}} - {{ detection.name }}
12
- description = {{ detection.description }}
12
+ description = {{ detection.description | escapeNewlines() }}
13
13
  action.escu.creation_date = {{ detection.date }}
14
14
  action.escu.modification_date = {{ detection.date }}
15
15
  {% if detection.tags.analytic_story is defined %}
16
- action.escu.analytic_story = {{ detection.tags.analytic_story | tojson }}
16
+ action.escu.analytic_story = {{ objectListToNameList(detection.tags.analytic_story) | tojson }}
17
17
  {% else %}
18
18
  action.escu.analytic_story = []
19
19
  {% endif %}
@@ -30,9 +30,9 @@ action.escu.providing_technologies = {{ detection.providing_technologies | tojso
30
30
  {% else %}
31
31
  action.escu.providing_technologies = []
32
32
  {% endif %}
33
- action.escu.eli5 = {{ detection.description }}
33
+ action.escu.eli5 = {{ detection.description | escapeNewlines() }}
34
34
  {% if detection.how_to_implement is defined %}
35
- action.escu.how_to_implement = {{ detection.how_to_implement }}
35
+ action.escu.how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
36
36
  {% else %}
37
37
  action.escu.how_to_implement = none
38
38
  {% endif %}
@@ -42,7 +42,7 @@ disabled = false
42
42
  disabled = true
43
43
  {% endif %}
44
44
  is_visible = false
45
- search = {{ detection.search }}
45
+ search = {{ detection.search | escapeNewlines() }}
46
46
 
47
47
  {% endif %}
48
48
  {% endfor %}