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.
Files changed (106) 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 +78 -50
  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 +250 -314
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
  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 +41 -30
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +29 -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 +41 -26
  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 +14 -8
  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 +45 -44
  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/transforms.j2 +2 -2
  98. contentctl/output/yml_writer.py +18 -24
  99. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  100. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  101. contentctl/actions/initialize_old.py +0 -245
  102. contentctl/objects/observable.py +0 -39
  103. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  104. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  105. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  106. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -2,10 +2,8 @@ from dataclasses import dataclass
2
2
  from typing import List
3
3
 
4
4
  from contentctl.objects.config import test_common, Selected, Changes
5
- from contentctl.objects.enums import DetectionTestingMode, DetectionStatus, AnalyticsType
6
5
  from contentctl.objects.detection import Detection
7
6
 
8
- from contentctl.input.director import DirectorOutputDto
9
7
 
10
8
  from contentctl.actions.detection_testing.DetectionTestingManager import (
11
9
  DetectionTestingManager,
@@ -41,7 +39,7 @@ MAXIMUM_CONFIGURATION_TIME_SECONDS = 600
41
39
  class TestInputDto:
42
40
  detections: List[Detection]
43
41
  config: test_common
44
-
42
+
45
43
 
46
44
  class Test:
47
45
  def filter_tests(self, input_dto: TestInputDto) -> None:
@@ -52,7 +50,7 @@ class Test:
52
50
  Args:
53
51
  input_dto (TestInputDto): A configuration of the test and all of the
54
52
  tests to be run.
55
- """
53
+ """
56
54
 
57
55
  if not input_dto.config.enable_integration_testing:
58
56
  # Skip all integraiton tests if integration testing is not enabled:
@@ -61,7 +59,6 @@ class Test:
61
59
  if isinstance(test, IntegrationTest):
62
60
  test.skip("TEST SKIPPED: Skipping all integration tests")
63
61
 
64
-
65
62
  def execute(self, input_dto: TestInputDto) -> bool:
66
63
  output_dto = DetectionTestingManagerOutputDto()
67
64
 
@@ -88,10 +85,21 @@ class Test:
88
85
  # detections were tested.
89
86
  file.stop()
90
87
  else:
91
- print(f"MODE: [{input_dto.config.mode.mode_name}] - Test [{len(input_dto.detections)}] detections")
92
- if isinstance(input_dto.config.mode, Selected) or isinstance(input_dto.config.mode, Changes):
93
- files_string = '\n- '.join(
94
- [str(pathlib.Path(detection.file_path).relative_to(input_dto.config.path)) for detection in input_dto.detections]
88
+ print(
89
+ f"MODE: [{input_dto.config.mode.mode_name}] - Test [{len(input_dto.detections)}] detections"
90
+ )
91
+ if isinstance(input_dto.config.mode, Selected) or isinstance(
92
+ input_dto.config.mode, Changes
93
+ ):
94
+ files_string = "\n- ".join(
95
+ [
96
+ str(
97
+ pathlib.Path(detection.file_path).relative_to(
98
+ input_dto.config.path
99
+ )
100
+ )
101
+ for detection in input_dto.detections
102
+ ]
95
103
  )
96
104
  print(f"Detections:\n- {files_string}")
97
105
 
@@ -102,43 +110,41 @@ class Test:
102
110
  summary_results = file.getSummaryObject()
103
111
  summary = summary_results.get("summary", {})
104
112
 
105
- print(f"Test Summary (mode: {summary.get('mode','Error')})")
106
- print(f"\tSuccess : {summary.get('success',False)}")
107
- print(
108
- f"\tSuccess Rate : {summary.get('success_rate','ERROR')}"
109
- )
113
+ print(f"Test Summary (mode: {summary.get('mode', 'Error')})")
114
+ print(f"\tSuccess : {summary.get('success', False)}")
110
115
  print(
111
- f"\tTotal Detections : {summary.get('total_detections','ERROR')}"
116
+ f"\tSuccess Rate : {summary.get('success_rate', 'ERROR')}"
112
117
  )
113
118
  print(
114
- f"\tTotal Tested Detections : {summary.get('total_tested_detections','ERROR')}"
119
+ f"\tTotal Detections : {summary.get('total_detections', 'ERROR')}"
115
120
  )
116
121
  print(
117
- f"\t Passed Detections : {summary.get('total_pass','ERROR')}"
122
+ f"\tTotal Tested Detections : {summary.get('total_tested_detections', 'ERROR')}"
118
123
  )
119
124
  print(
120
- f"\t Failed Detections : {summary.get('total_fail','ERROR')}"
125
+ f"\t Passed Detections : {summary.get('total_pass', 'ERROR')}"
121
126
  )
122
127
  print(
123
- f"\tSkipped Detections : {summary.get('total_skipped','ERROR')}"
128
+ f"\t Failed Detections : {summary.get('total_fail', 'ERROR')}"
124
129
  )
125
130
  print(
126
- "\tProduction Status :"
131
+ f"\tSkipped Detections : {summary.get('total_skipped', 'ERROR')}"
127
132
  )
133
+ print("\tProduction Status :")
128
134
  print(
129
- f"\t Production Detections : {summary.get('total_production','ERROR')}"
135
+ f"\t Production Detections : {summary.get('total_production', 'ERROR')}"
130
136
  )
131
137
  print(
132
- f"\t Experimental Detections : {summary.get('total_experimental','ERROR')}"
138
+ f"\t Experimental Detections : {summary.get('total_experimental', 'ERROR')}"
133
139
  )
134
140
  print(
135
- f"\t Deprecated Detections : {summary.get('total_deprecated','ERROR')}"
141
+ f"\t Deprecated Detections : {summary.get('total_deprecated', 'ERROR')}"
136
142
  )
137
143
  print(
138
- f"\tManually Tested Detections : {summary.get('total_manual','ERROR')}"
144
+ f"\tManually Tested Detections : {summary.get('total_manual', 'ERROR')}"
139
145
  )
140
146
  print(
141
- f"\tUntested Detections : {summary.get('total_untested','ERROR')}"
147
+ f"\tUntested Detections : {summary.get('total_untested', 'ERROR')}"
142
148
  )
143
149
  print(f"\tTest Results File : {file.getOutputFilePath()}")
144
150
  print(
@@ -1,4 +1,3 @@
1
-
2
1
  import pathlib
3
2
 
4
3
  from contentctl.input.director import Director, DirectorOutputDto
@@ -27,7 +26,7 @@ class Validate:
27
26
  [],
28
27
  [],
29
28
  [],
30
- []
29
+ [],
31
30
  )
32
31
 
33
32
  director = Director(director_output_dto)
@@ -35,51 +34,69 @@ class Validate:
35
34
  self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
36
35
  if input_dto.data_source_TA_validation:
37
36
  self.validate_latest_TA_information(director_output_dto.data_sources)
38
-
37
+
39
38
  return director_output_dto
40
39
 
41
-
42
- def ensure_no_orphaned_files_in_lookups(self, repo_path:pathlib.Path, director_output_dto:DirectorOutputDto):
40
+ def ensure_no_orphaned_files_in_lookups(
41
+ self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto
42
+ ):
43
43
  """
44
44
  This function ensures that only files which are relevant to lookups are included in the lookups folder.
45
45
  This means that a file must be either:
46
46
  1. A lookup YML (.yml)
47
47
  2. A lookup CSV (.csv) which is referenced by a YML
48
48
  3. A lookup MLMODEL (.mlmodel) which is referenced by a YML.
49
-
49
+
50
50
  All other files, includes CSV and MLMODEL files which are NOT
51
51
  referenced by a YML, will generate an exception from this function.
52
-
52
+
53
53
  Args:
54
54
  repo_path (pathlib.Path): path to the root of the app
55
55
  director_output_dto (DirectorOutputDto): director object with all constructed content
56
56
 
57
57
  Raises:
58
- Exception: An Exception will be raised if there are any non .yml, .csv, or .mlmodel
59
- files in this directory. Additionally, an exception will be raised if there
60
- exists one or more .csv or .mlmodel files that are not referenced by at least 1
61
- detection .yml file in this directory.
58
+ Exception: An Exception will be raised if there are any non .yml, .csv, or .mlmodel
59
+ files in this directory. Additionally, an exception will be raised if there
60
+ exists one or more .csv or .mlmodel files that are not referenced by at least 1
61
+ detection .yml file in this directory.
62
62
  This avoids having additional, unused files in this directory that may be copied into
63
63
  the app when it is built (which can cause appinspect errors or larger app size.)
64
- """
65
- lookupsDirectory = repo_path/"lookups"
66
-
64
+ """
65
+ lookupsDirectory = repo_path / "lookups"
66
+
67
67
  # Get all of the files referneced by Lookups
68
- usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if isinstance(lookup, FileBackedLookup)] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
68
+ usedLookupFiles: list[pathlib.Path] = [
69
+ lookup.filename
70
+ for lookup in director_output_dto.lookups
71
+ if isinstance(lookup, FileBackedLookup)
72
+ ] + [
73
+ lookup.file_path
74
+ for lookup in director_output_dto.lookups
75
+ if lookup.file_path is not None
76
+ ]
69
77
 
70
78
  # Get all of the mlmodel and csv files in the lookups directory
71
- csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(lookupsDirectory, allowedFileExtensions=[".yml",".csv",".mlmodel"], fileExtensionsToReturn=[".csv",".mlmodel"])
72
-
79
+ csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(
80
+ lookupsDirectory,
81
+ allowedFileExtensions=[".yml", ".csv", ".mlmodel"],
82
+ fileExtensionsToReturn=[".csv", ".mlmodel"],
83
+ )
84
+
73
85
  # Generate an exception of any csv or mlmodel files exist but are not used
74
- unusedLookupFiles:list[pathlib.Path] = [testFile for testFile in csvAndMlmodelFiles if testFile not in usedLookupFiles]
86
+ unusedLookupFiles: list[pathlib.Path] = [
87
+ testFile
88
+ for testFile in csvAndMlmodelFiles
89
+ if testFile not in usedLookupFiles
90
+ ]
75
91
  if len(unusedLookupFiles) > 0:
76
- raise Exception(f"The following .csv or .mlmodel files exist in '{lookupsDirectory}', but are not referenced by a lookup file: {[str(path) for path in unusedLookupFiles]}")
92
+ raise Exception(
93
+ f"The following .csv or .mlmodel files exist in '{lookupsDirectory}', but are not referenced by a lookup file: {[str(path) for path in unusedLookupFiles]}"
94
+ )
77
95
  return
78
-
79
96
 
80
97
  def validate_latest_TA_information(self, data_sources: list[DataSource]) -> None:
81
98
  validated_TAs: list[tuple[str, str]] = []
82
- errors:list[str] = []
99
+ errors: list[str] = []
83
100
  print("----------------------")
84
101
  print("Validating latest TA:")
85
102
  print("----------------------")
@@ -90,22 +107,25 @@ class Validate:
90
107
  continue
91
108
  if supported_TA.url is not None:
92
109
  validated_TAs.append(ta_identifier)
93
- uid = int(str(supported_TA.url).rstrip('/').split("/")[-1])
110
+ uid = int(str(supported_TA.url).rstrip("/").split("/")[-1])
94
111
  try:
95
112
  splunk_app = SplunkApp(app_uid=uid)
96
113
  if splunk_app.latest_version != supported_TA.version:
97
- errors.append(f"Version mismatch in '{data_source.file_path}' supported TA '{supported_TA.name}'"
98
- f"\n Latest version on Splunkbase : {splunk_app.latest_version}"
99
- f"\n Version specified in data source: {supported_TA.version}")
114
+ errors.append(
115
+ f"Version mismatch in '{data_source.file_path}' supported TA '{supported_TA.name}'"
116
+ f"\n Latest version on Splunkbase : {splunk_app.latest_version}"
117
+ f"\n Version specified in data source: {supported_TA.version}"
118
+ )
100
119
  except Exception as e:
101
- errors.append(f"Error processing checking version of TA {supported_TA.name}: {str(e)}")
102
-
120
+ errors.append(
121
+ f"Error processing checking version of TA {supported_TA.name}: {str(e)}"
122
+ )
123
+
103
124
  if len(errors) > 0:
104
- errorString = '\n\n'.join(errors)
105
- raise Exception(f"[{len(errors)}] or more TA versions are out of date or have other errors."
106
- f"Please update the following data sources with the latest versions of "
107
- f"their supported tas:\n\n{errorString}")
125
+ errorString = "\n\n".join(errors)
126
+ raise Exception(
127
+ f"[{len(errors)}] or more TA versions are out of date or have other errors."
128
+ f"Please update the following data sources with the latest versions of "
129
+ f"their supported tas:\n\n{errorString}"
130
+ )
108
131
  print("All TA versions are up to date.")
109
-
110
-
111
-
contentctl/api.py CHANGED
@@ -5,42 +5,48 @@ from contentctl.objects.config import test_common, test, test_servers
5
5
  from contentctl.objects.security_content_object import SecurityContentObject
6
6
  from contentctl.input.director import DirectorOutputDto
7
7
 
8
- def config_from_file(path:Path=Path("contentctl.yml"), config: dict[str,Any]={},
9
- configType:Type[Union[test,test_servers]]=test)->test_common:
10
-
8
+
9
+ def config_from_file(
10
+ path: Path = Path("contentctl.yml"),
11
+ config: dict[str, Any] = {},
12
+ configType: Type[Union[test, test_servers]] = test,
13
+ ) -> test_common:
11
14
  """
12
15
  Fetch a configuration object that can be used for a number of different contentctl
13
- operations including validate, build, inspect, test, and test_servers. A file will
16
+ operations including validate, build, inspect, test, and test_servers. A file will
14
17
  be used as the basis for constructing the configuration.
15
18
 
16
19
  Args:
17
- path (Path, optional): Relative or absolute path to a contentctl config file.
20
+ path (Path, optional): Relative or absolute path to a contentctl config file.
18
21
  Defaults to Path("contentctl.yml"), which is the default name and location (in the current directory)
19
22
  of the configuration files which are automatically generated for contentctl.
20
23
  config (dict[], optional): Dictionary of values to override values read from the YML
21
24
  path passed as the first argument. Defaults to {}, an empty dict meaning that nothing
22
- will be overwritten
23
- configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
25
+ will be overwritten
26
+ configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
24
27
  This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
25
28
  Returns:
26
29
  test_common: Returns a complete contentctl test_common configuration. Note that this configuration
27
30
  will have all applicable field for validate and build as well, but can also be used for easily
28
- construction a test or test_servers object.
29
- """
31
+ construction a test or test_servers object.
32
+ """
30
33
 
31
34
  try:
32
35
  yml_dict = YmlReader.load_file(path, add_fields=False)
33
-
34
-
36
+
35
37
  except Exception as e:
36
- raise Exception(f"Failed to load contentctl configuration from file '{path}': {str(e)}")
37
-
38
+ raise Exception(
39
+ f"Failed to load contentctl configuration from file '{path}': {str(e)}"
40
+ )
41
+
38
42
  # Apply settings that have been overridden from the ones in the file
39
43
  try:
40
44
  yml_dict.update(config)
41
45
  except Exception as e:
42
- raise Exception(f"Failed updating dictionary of values read from file '{path}'"
43
- f" with the dictionary of arguments passed: {str(e)}")
46
+ raise Exception(
47
+ f"Failed updating dictionary of values read from file '{path}'"
48
+ f" with the dictionary of arguments passed: {str(e)}"
49
+ )
44
50
 
45
51
  # The function below will throw its own descriptive exception if it fails
46
52
  configObject = config_from_dict(yml_dict, configType=configType)
@@ -48,13 +54,12 @@ def config_from_file(path:Path=Path("contentctl.yml"), config: dict[str,Any]={},
48
54
  return configObject
49
55
 
50
56
 
51
-
52
-
53
- def config_from_dict(config: dict[str,Any]={},
54
- configType:Type[Union[test,test_servers]]=test)->test_common:
57
+ def config_from_dict(
58
+ config: dict[str, Any] = {}, configType: Type[Union[test, test_servers]] = test
59
+ ) -> test_common:
55
60
  """
56
61
  Fetch a configuration object that can be used for a number of different contentctl
57
- operations including validate, build, inspect, test, and test_servers. A dict will
62
+ operations including validate, build, inspect, test, and test_servers. A dict will
58
63
  be used as the basis for constructing the configuration.
59
64
 
60
65
  Args:
@@ -63,29 +68,30 @@ def config_from_dict(config: dict[str,Any]={},
63
68
  values. Note that based on default values in the contentctl/objects/config.py
64
69
  file, this may raise an exception. If so, please set appropriate default values
65
70
  in the file above or supply those values via this argument.
66
- configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
71
+ configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
67
72
  This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
68
73
  Returns:
69
74
  test_common: Returns a complete contentctl test_common configuration. Note that this configuration
70
75
  will have all applicable field for validate and build as well, but can also be used for easily
71
- construction a test or test_servers object.
72
- """
76
+ construction a test or test_servers object.
77
+ """
73
78
  try:
74
79
  test_object = configType.model_validate(config)
75
80
  except Exception as e:
76
81
  raise Exception(f"Failed to load contentctl configuration from dict:\n{str(e)}")
77
-
82
+
78
83
  return test_object
79
84
 
80
85
 
81
- def update_config(config:Union[test,test_servers], **key_value_updates:dict[str,Any])->test_common:
82
-
86
+ def update_config(
87
+ config: Union[test, test_servers], **key_value_updates: dict[str, Any]
88
+ ) -> test_common:
83
89
  """Update any relevant keys in a config file with the specified values.
84
90
  Full validation will be performed after this update and descriptive errors
85
91
  will be produced
86
92
 
87
93
  Args:
88
- config (test_common): A previously-constructed test_common object. This can be
94
+ config (test_common): A previously-constructed test_common object. This can be
89
95
  build using the configFromDict or configFromFile functions.
90
96
  key_value_updates (kwargs, optional): Additional keyword/argument pairs to update
91
97
  arbitrary fields in the configuration.
@@ -101,37 +107,40 @@ def update_config(config:Union[test,test_servers], **key_value_updates:dict[str,
101
107
 
102
108
  # Force validation of assignment since doing so via arbitrary dict can be error prone
103
109
  # Also, ensure that we do not try to add fields that are not part of the model
104
- config_copy.model_config.update({'validate_assignment': True, 'extra': 'forbid'})
110
+ config_copy.model_config.update({"validate_assignment": True, "extra": "forbid"})
105
111
 
106
-
107
-
108
112
  # Collect any errors that may occur
109
- errors:list[Exception] = []
110
-
111
- # We need to do this one by one because the extra:forbid argument does not appear to
113
+ errors: list[Exception] = []
114
+
115
+ # We need to do this one by one because the extra:forbid argument does not appear to
112
116
  # be respected at this time.
113
117
  for key, value in key_value_updates.items():
114
118
  try:
115
- setattr(config_copy,key,value)
119
+ setattr(config_copy, key, value)
116
120
  except Exception as e:
117
121
  errors.append(e)
118
122
  if len(errors) > 0:
119
- errors_string = '\n'.join([str(e) for e in errors])
123
+ errors_string = "\n".join([str(e) for e in errors])
120
124
  raise Exception(f"Error(s) updaitng configuration:\n{errors_string}")
121
-
125
+
122
126
  return config_copy
123
-
124
127
 
125
128
 
126
- def content_to_dict(director:DirectorOutputDto)->dict[str,list[dict[str,Any]]]:
127
- output_dict:dict[str,list[dict[str,Any]]] = {}
128
- for contentType in ['detections','stories','baselines','investigations',
129
- 'playbooks','macros','lookups','deployments',]:
130
-
129
+ def content_to_dict(director: DirectorOutputDto) -> dict[str, list[dict[str, Any]]]:
130
+ output_dict: dict[str, list[dict[str, Any]]] = {}
131
+ for contentType in [
132
+ "detections",
133
+ "stories",
134
+ "baselines",
135
+ "investigations",
136
+ "playbooks",
137
+ "macros",
138
+ "lookups",
139
+ "deployments",
140
+ ]:
131
141
  output_dict[contentType] = []
132
- t:list[SecurityContentObject] = getattr(director,contentType)
133
-
142
+ t: list[SecurityContentObject] = getattr(director, contentType)
143
+
134
144
  for item in t:
135
145
  output_dict[contentType].append(item.model_dump())
136
146
  return output_dict
137
-
contentctl/contentctl.py CHANGED
@@ -113,7 +113,7 @@ def deploy_acs_func(config: deploy_acs):
113
113
 
114
114
 
115
115
  def test_common_func(config: test_common):
116
- if type(config) == test:
116
+ if type(config) is test:
117
117
  # construct the container Infrastructure objects
118
118
  config.getContainerInfrastructureObjects()
119
119
  # otherwise, they have already been passed as servers
@@ -220,25 +220,25 @@ def main():
220
220
  with warnings.catch_warnings(action="ignore"):
221
221
  config = tyro.cli(models)
222
222
 
223
- if type(config) == init:
223
+ if type(config) is init:
224
224
  t.__dict__.update(config.__dict__)
225
225
  init_func(t)
226
- elif type(config) == validate:
226
+ elif type(config) is validate:
227
227
  validate_func(config)
228
- elif type(config) == report:
228
+ elif type(config) is report:
229
229
  report_func(config)
230
- elif type(config) == build:
230
+ elif type(config) is build:
231
231
  build_func(config)
232
- elif type(config) == new:
232
+ elif type(config) is new:
233
233
  new_func(config)
234
- elif type(config) == inspect:
234
+ elif type(config) is inspect:
235
235
  inspect_func(config)
236
- elif type(config) == release_notes:
236
+ elif type(config) is release_notes:
237
237
  release_notes_func(config)
238
- elif type(config) == deploy_acs:
238
+ elif type(config) is deploy_acs:
239
239
  updated_config = deploy_acs.model_validate(config)
240
240
  deploy_acs_func(updated_config)
241
- elif type(config) == test or type(config) == test_servers:
241
+ elif type(config) is test or type(config) is test_servers:
242
242
  test_common_func(config)
243
243
  else:
244
244
  raise Exception(f"Unknown command line type '{type(config).__name__}'")