contentctl 5.2.0__py3-none-any.whl → 5.3.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 (43) hide show
  1. contentctl/actions/build.py +5 -43
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +64 -24
  3. contentctl/actions/detection_testing/GitService.py +4 -1
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +146 -42
  5. contentctl/actions/detection_testing/views/DetectionTestingView.py +5 -6
  6. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +2 -0
  7. contentctl/actions/initialize.py +35 -9
  8. contentctl/actions/release_notes.py +14 -12
  9. contentctl/actions/test.py +16 -20
  10. contentctl/actions/validate.py +9 -16
  11. contentctl/helper/utils.py +69 -20
  12. contentctl/input/director.py +147 -119
  13. contentctl/input/yml_reader.py +39 -27
  14. contentctl/objects/abstract_security_content_objects/detection_abstract.py +95 -21
  15. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +548 -8
  16. contentctl/objects/baseline.py +24 -6
  17. contentctl/objects/config.py +32 -8
  18. contentctl/objects/content_versioning_service.py +508 -0
  19. contentctl/objects/correlation_search.py +53 -63
  20. contentctl/objects/dashboard.py +15 -1
  21. contentctl/objects/data_source.py +13 -1
  22. contentctl/objects/deployment.py +23 -9
  23. contentctl/objects/detection.py +2 -0
  24. contentctl/objects/enums.py +28 -18
  25. contentctl/objects/investigation.py +40 -20
  26. contentctl/objects/lookup.py +62 -6
  27. contentctl/objects/macro.py +19 -4
  28. contentctl/objects/playbook.py +16 -2
  29. contentctl/objects/rba.py +1 -33
  30. contentctl/objects/removed_security_content_object.py +50 -0
  31. contentctl/objects/security_content_object.py +1 -0
  32. contentctl/objects/story.py +37 -5
  33. contentctl/output/api_json_output.py +5 -3
  34. contentctl/output/conf_output.py +9 -1
  35. contentctl/output/runtime_csv_writer.py +111 -0
  36. contentctl/output/svg_output.py +4 -5
  37. contentctl/output/templates/savedsearches_detections.j2 +2 -6
  38. {contentctl-5.2.0.dist-info → contentctl-5.3.1.dist-info}/METADATA +4 -3
  39. {contentctl-5.2.0.dist-info → contentctl-5.3.1.dist-info}/RECORD +42 -40
  40. {contentctl-5.2.0.dist-info → contentctl-5.3.1.dist-info}/WHEEL +1 -1
  41. contentctl/output/data_source_writer.py +0 -52
  42. {contentctl-5.2.0.dist-info → contentctl-5.3.1.dist-info}/LICENSE.md +0 -0
  43. {contentctl-5.2.0.dist-info → contentctl-5.3.1.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,14 @@
1
+ import datetime
2
+ import json
3
+ import pathlib
1
4
  import shutil
2
-
3
5
  from dataclasses import dataclass
4
6
 
5
7
  from contentctl.input.director import DirectorOutputDto
8
+ from contentctl.objects.config import build
9
+ from contentctl.output.api_json_output import ApiJsonOutput
6
10
  from contentctl.output.conf_output import ConfOutput
7
11
  from contentctl.output.conf_writer import ConfWriter
8
- from contentctl.output.api_json_output import ApiJsonOutput
9
- from contentctl.output.data_source_writer import DataSourceWriter
10
- from contentctl.objects.lookup import CSVLookup, Lookup_Type
11
- import pathlib
12
- import json
13
- import datetime
14
- import uuid
15
-
16
- from contentctl.objects.config import build
17
12
 
18
13
 
19
14
  @dataclass(frozen=True)
@@ -28,39 +23,6 @@ class Build:
28
23
  updated_conf_files: set[pathlib.Path] = set()
29
24
  conf_output = ConfOutput(input_dto.config)
30
25
 
31
- # Construct a path to a YML that does not actually exist.
32
- # We mock this "fake" path since the YML does not exist.
33
- # This ensures the checking for the existence of the CSV is correct
34
- data_sources_fake_yml_path = (
35
- input_dto.config.getPackageDirectoryPath()
36
- / "lookups"
37
- / "data_sources.yml"
38
- )
39
-
40
- # Construct a special lookup whose CSV is created at runtime and
41
- # written directly into the lookups folder. We will delete this after a build,
42
- # assuming that it is successful.
43
- data_sources_lookup_csv_path = (
44
- input_dto.config.getPackageDirectoryPath()
45
- / "lookups"
46
- / "data_sources.csv"
47
- )
48
-
49
- DataSourceWriter.writeDataSourceCsv(
50
- input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path
51
- )
52
- input_dto.director_output_dto.addContentToDictMappings(
53
- CSVLookup.model_construct(
54
- name="data_sources",
55
- id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
56
- version=1,
57
- author=input_dto.config.app.author_name,
58
- date=datetime.date.today(),
59
- description="A lookup file that will contain the data source objects for detections.",
60
- lookup_type=Lookup_Type.csv,
61
- file_path=data_sources_fake_yml_path,
62
- )
63
- )
64
26
  updated_conf_files.update(conf_output.writeHeaders())
65
27
  updated_conf_files.update(
66
28
  conf_output.writeLookups(input_dto.director_output_dto.lookups)
@@ -1,7 +1,16 @@
1
+ import concurrent.futures
2
+ import datetime
3
+ import signal
4
+ import traceback
5
+ from dataclasses import dataclass
1
6
  from typing import List, Union
2
- from contentctl.objects.config import test, test_servers, Container, Infrastructure
7
+
8
+ import docker
9
+ from pydantic import BaseModel
10
+
3
11
  from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
4
12
  DetectionTestingInfrastructure,
13
+ DetectionTestingManagerOutputDto,
5
14
  )
6
15
  from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureContainer import (
7
16
  DetectionTestingInfrastructureContainer,
@@ -9,24 +18,12 @@ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfras
9
18
  from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureServer import (
10
19
  DetectionTestingInfrastructureServer,
11
20
  )
12
- import signal
13
- import datetime
14
-
15
- # from queue import Queue
16
- from dataclasses import dataclass
17
-
18
- # import threading
19
- from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
20
- DetectionTestingManagerOutputDto,
21
- )
22
21
  from contentctl.actions.detection_testing.views.DetectionTestingView import (
23
22
  DetectionTestingView,
24
23
  )
25
- from contentctl.objects.enums import PostTestBehavior
26
- from pydantic import BaseModel
24
+ from contentctl.objects.config import Container, Infrastructure, test, test_servers
27
25
  from contentctl.objects.detection import Detection
28
- import concurrent.futures
29
- import docker
26
+ from contentctl.objects.enums import PostTestBehavior
30
27
 
31
28
 
32
29
  @dataclass(frozen=False)
@@ -63,12 +60,14 @@ class DetectionTestingManager(BaseModel):
63
60
  # a newline '\r\n' which will cause that wait to stop
64
61
  print("*******************************")
65
62
  print(
66
- "If testing is paused and you are debugging a detection, you MUST hit CTRL-D at the prompt to complete shutdown."
63
+ "If testing is paused and you are debugging a detection, you MUST hit CTRL-D "
64
+ "at the prompt to complete shutdown."
67
65
  )
68
66
  print("*******************************")
69
67
 
70
68
  signal.signal(signal.SIGINT, sigint_handler)
71
69
 
70
+ # TODO (#337): futures can be hard to maintain/debug; let's consider alternatives
72
71
  with (
73
72
  concurrent.futures.ThreadPoolExecutor(
74
73
  max_workers=len(self.input_dto.config.test_instances),
@@ -80,10 +79,19 @@ class DetectionTestingManager(BaseModel):
80
79
  max_workers=len(self.input_dto.config.test_instances),
81
80
  ) as view_shutdowner,
82
81
  ):
82
+ # Capture any errors for reporting at the end after all threads have been gathered
83
+ errors: dict[str, list[Exception]] = {
84
+ "INSTANCE SETUP ERRORS": [],
85
+ "TESTING ERRORS": [],
86
+ "ERRORS DURING VIEW SHUTDOWN": [],
87
+ "ERRORS DURING VIEW EXECUTION": [],
88
+ }
89
+
83
90
  # Start all the views
84
91
  future_views = {
85
92
  view_runner.submit(view.setup): view for view in self.input_dto.views
86
93
  }
94
+
87
95
  # Configure all the instances
88
96
  future_instances_setup = {
89
97
  instance_pool.submit(instance.setup): instance
@@ -96,7 +104,11 @@ class DetectionTestingManager(BaseModel):
96
104
  future.result()
97
105
  except Exception as e:
98
106
  self.output_dto.terminate = True
99
- print(f"Error setting up container: {str(e)}")
107
+ # Output the traceback if we encounter errors in verbose mode
108
+ if self.input_dto.config.verbose:
109
+ tb = traceback.format_exc()
110
+ print(tb)
111
+ errors["INSTANCE SETUP ERRORS"].append(e)
100
112
 
101
113
  # Start and wait for all tests to run
102
114
  if not self.output_dto.terminate:
@@ -111,7 +123,11 @@ class DetectionTestingManager(BaseModel):
111
123
  future.result()
112
124
  except Exception as e:
113
125
  self.output_dto.terminate = True
114
- print(f"Error running in container: {str(e)}")
126
+ # Output the traceback if we encounter errors in verbose mode
127
+ if self.input_dto.config.verbose:
128
+ tb = traceback.format_exc()
129
+ print(tb)
130
+ errors["TESTING ERRORS"].append(e)
115
131
 
116
132
  self.output_dto.terminate = True
117
133
 
@@ -123,14 +139,34 @@ class DetectionTestingManager(BaseModel):
123
139
  try:
124
140
  future.result()
125
141
  except Exception as e:
126
- print(f"Error stopping view: {str(e)}")
142
+ # Output the traceback if we encounter errors in verbose mode
143
+ if self.input_dto.config.verbose:
144
+ tb = traceback.format_exc()
145
+ print(tb)
146
+ errors["ERRORS DURING VIEW SHUTDOWN"].append(e)
127
147
 
128
148
  # Wait for original view-related threads to complete
129
149
  for future in concurrent.futures.as_completed(future_views):
130
150
  try:
131
151
  future.result()
132
152
  except Exception as e:
133
- print(f"Error running container: {str(e)}")
153
+ # Output the traceback if we encounter errors in verbose mode
154
+ if self.input_dto.config.verbose:
155
+ tb = traceback.format_exc()
156
+ print(tb)
157
+ errors["ERRORS DURING VIEW EXECUTION"].append(e)
158
+
159
+ # Log any errors
160
+ for error_type in errors:
161
+ if len(errors[error_type]) > 0:
162
+ print()
163
+ print(f"[{error_type}]:")
164
+ for error in errors[error_type]:
165
+ print(f"\t❌ {str(error)}")
166
+ if isinstance(error, ExceptionGroup):
167
+ for suberror in error.exceptions: # type: ignore
168
+ print(f"\t\t❌ {str(suberror)}") # type: ignore
169
+ print()
134
170
 
135
171
  return self.output_dto
136
172
 
@@ -154,12 +190,15 @@ class DetectionTestingManager(BaseModel):
154
190
  )
155
191
  if len(parts) != 2:
156
192
  raise Exception(
157
- f"Expected to find a name:tag in {self.input_dto.config.container_settings.full_image_path}, "
158
- f"but instead found {parts}. Note that this path MUST include the tag, which is separated by ':'"
193
+ "Expected to find a name:tag in "
194
+ f"{self.input_dto.config.container_settings.full_image_path}, "
195
+ f"but instead found {parts}. Note that this path MUST include the "
196
+ "tag, which is separated by ':'"
159
197
  )
160
198
 
161
199
  print(
162
- f"Getting the latest version of the container image [{self.input_dto.config.container_settings.full_image_path}]...",
200
+ "Getting the latest version of the container image "
201
+ f"[{self.input_dto.config.container_settings.full_image_path}]...",
163
202
  end="",
164
203
  flush=True,
165
204
  )
@@ -168,7 +207,8 @@ class DetectionTestingManager(BaseModel):
168
207
  break
169
208
  except Exception as e:
170
209
  raise Exception(
171
- f"Failed to pull docker container image [{self.input_dto.config.container_settings.full_image_path}]: {str(e)}"
210
+ "Failed to pull docker container image "
211
+ f"[{self.input_dto.config.container_settings.full_image_path}]: {str(e)}"
172
212
  )
173
213
 
174
214
  already_staged_container_files = False
@@ -14,7 +14,7 @@ from contentctl.input.director import DirectorOutputDto
14
14
  from contentctl.objects.config import All, Changes, Selected, test_common
15
15
  from contentctl.objects.data_source import DataSource
16
16
  from contentctl.objects.detection import Detection
17
- from contentctl.objects.lookup import CSVLookup, Lookup
17
+ from contentctl.objects.lookup import CSVLookup, Lookup, RuntimeCSV
18
18
  from contentctl.objects.macro import Macro
19
19
  from contentctl.objects.security_content_object import SecurityContentObject
20
20
 
@@ -148,6 +148,9 @@ class GitService(BaseModel):
148
148
  matched = list(
149
149
  filter(
150
150
  lambda x: isinstance(x, CSVLookup)
151
+ and not isinstance(
152
+ x, RuntimeCSV
153
+ ) # RuntimeCSV is not used directly by any content
151
154
  and x.filename == decoded_path,
152
155
  self.director.lookups,
153
156
  )
@@ -11,13 +11,20 @@ from shutil import copyfile
11
11
  from ssl import SSLEOFError, SSLZeroReturnError
12
12
  from sys import stdout
13
13
  from tempfile import TemporaryDirectory, mktemp
14
- from typing import Optional, Union
14
+ from typing import Callable, Optional, Union
15
15
 
16
16
  import requests # type: ignore
17
17
  import splunklib.client as client # type: ignore
18
- import splunklib.results
19
18
  import tqdm # type: ignore
20
- from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, dataclasses
19
+ from pydantic import (
20
+ BaseModel,
21
+ ConfigDict,
22
+ Field,
23
+ PrivateAttr,
24
+ computed_field,
25
+ dataclasses,
26
+ )
27
+ from semantic_version import Version
21
28
  from splunklib.binding import HTTPError # type: ignore
22
29
  from splunklib.results import JSONResultsReader, Message # type: ignore
23
30
  from urllib3 import disable_warnings
@@ -31,7 +38,8 @@ from contentctl.actions.detection_testing.progress_bar import (
31
38
  from contentctl.helper.utils import Utils
32
39
  from contentctl.objects.base_test import BaseTest
33
40
  from contentctl.objects.base_test_result import TestResultStatus
34
- from contentctl.objects.config import Infrastructure, test_common
41
+ from contentctl.objects.config import All, Infrastructure, test_common
42
+ from contentctl.objects.content_versioning_service import ContentVersioningService
35
43
  from contentctl.objects.correlation_search import CorrelationSearch, PbarData
36
44
  from contentctl.objects.detection import Detection
37
45
  from contentctl.objects.enums import AnalyticsType, PostTestBehavior
@@ -42,6 +50,9 @@ from contentctl.objects.test_group import TestGroup
42
50
  from contentctl.objects.unit_test import UnitTest
43
51
  from contentctl.objects.unit_test_result import UnitTestResult
44
52
 
53
+ # The app name of ES; needed to check ES version
54
+ ES_APP_NAME = "SplunkEnterpriseSecuritySuite"
55
+
45
56
 
46
57
  class SetupTestGroupResults(BaseModel):
47
58
  exception: Union[Exception, None] = None
@@ -136,21 +147,27 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
136
147
  )
137
148
 
138
149
  self.start_time = time.time()
150
+
151
+ # Init the list of setup functions we always need
152
+ primary_setup_functions: list[
153
+ tuple[Callable[[], None | client.Service], str]
154
+ ] = [
155
+ (self.start, "Starting"),
156
+ (self.get_conn, "Waiting for App Installation"),
157
+ (self.configure_conf_file_datamodels, "Configuring Datamodels"),
158
+ (self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
159
+ (self.get_all_indexes, "Getting all indexes from server"),
160
+ (self.check_for_es_install, "Checking for ES Install"),
161
+ (self.configure_imported_roles, "Configuring Roles"),
162
+ (self.configure_delete_indexes, "Configuring Indexes"),
163
+ (self.configure_hec, "Configuring HEC"),
164
+ (self.wait_for_ui_ready, "Finishing Primary Setup"),
165
+ ]
166
+
167
+ # Execute and report on each setup function
139
168
  try:
140
- for func, msg in [
141
- (self.start, "Starting"),
142
- (self.get_conn, "Waiting for App Installation"),
143
- (self.configure_conf_file_datamodels, "Configuring Datamodels"),
144
- (
145
- self.create_replay_index,
146
- f"Create index '{self.sync_obj.replay_index}'",
147
- ),
148
- (self.get_all_indexes, "Getting all indexes from server"),
149
- (self.configure_imported_roles, "Configuring Roles"),
150
- (self.configure_delete_indexes, "Configuring Indexes"),
151
- (self.configure_hec, "Configuring HEC"),
152
- (self.wait_for_ui_ready, "Finishing Setup"),
153
- ]:
169
+ # Run the primary setup functions
170
+ for func, msg in primary_setup_functions:
154
171
  self.format_pbar_string(
155
172
  TestReportingType.SETUP,
156
173
  self.get_name(),
@@ -160,18 +177,114 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
160
177
  func()
161
178
  self.check_for_teardown()
162
179
 
180
+ # Run any setup functions only applicable to content versioning validation
181
+ if self.should_test_content_versioning:
182
+ self.pbar.write(
183
+ self.format_pbar_string(
184
+ TestReportingType.SETUP,
185
+ self.get_name(),
186
+ "Beginning Content Versioning Validation...",
187
+ set_pbar=False,
188
+ )
189
+ )
190
+ for func, msg in self.content_versioning_service.setup_functions:
191
+ self.format_pbar_string(
192
+ TestReportingType.SETUP,
193
+ self.get_name(),
194
+ msg,
195
+ update_sync_status=True,
196
+ )
197
+ func()
198
+ self.check_for_teardown()
199
+
163
200
  except Exception as e:
164
- self.pbar.write(str(e))
201
+ msg = f"[{self.get_name()}]: {str(e)}"
165
202
  self.finish()
166
- return
203
+ if isinstance(e, ExceptionGroup):
204
+ raise ExceptionGroup(msg, e.exceptions) from e # type: ignore
205
+ raise Exception(msg) from e
167
206
 
168
- self.format_pbar_string(
169
- TestReportingType.SETUP, self.get_name(), "Finished Setup!"
207
+ self.pbar.write(
208
+ self.format_pbar_string(
209
+ TestReportingType.SETUP,
210
+ self.get_name(),
211
+ "Finished Setup!",
212
+ set_pbar=False,
213
+ )
170
214
  )
171
215
 
172
216
  def wait_for_ui_ready(self):
173
217
  self.get_conn()
174
218
 
219
+ @computed_field
220
+ @property
221
+ def content_versioning_service(self) -> ContentVersioningService:
222
+ """
223
+ A computed field returning a handle to the content versioning service, used by ES to
224
+ version detections. We use this model to validate that all detections have been installed
225
+ compatibly with ES versioning.
226
+
227
+ :return: a handle to the content versioning service on the instance
228
+ :rtype: :class:`contentctl.objects.content_versioning_service.ContentVersioningService`
229
+ """
230
+ return ContentVersioningService(
231
+ global_config=self.global_config,
232
+ infrastructure=self.infrastructure,
233
+ service=self.get_conn(),
234
+ detections=self.sync_obj.inputQueue,
235
+ )
236
+
237
+ @property
238
+ def should_test_content_versioning(self) -> bool:
239
+ """
240
+ Indicates whether we should test content versioning. Content versioning
241
+ should be tested when integration testing is enabled, the mode is all, and ES is at least
242
+ version 8.0.0.
243
+
244
+ :return: a bool indicating whether we should test content versioning
245
+ :rtype: bool
246
+ """
247
+ es_version = self.es_version
248
+ return (
249
+ self.global_config.enable_integration_testing
250
+ and isinstance(self.global_config.mode, All)
251
+ and es_version is not None
252
+ and es_version >= Version("8.0.0")
253
+ )
254
+
255
+ @property
256
+ def es_version(self) -> Version | None:
257
+ """
258
+ Returns the version of Enterprise Security installed on the instance; None if not installed.
259
+
260
+ :return: the version of ES, as a semver aware object
261
+ :rtype: :class:`semantic_version.Version`
262
+ """
263
+ if not self.es_installed:
264
+ return None
265
+ return Version(self.get_conn().apps[ES_APP_NAME]["version"]) # type: ignore
266
+
267
+ @property
268
+ def es_installed(self) -> bool:
269
+ """
270
+ Indicates whether ES is installed on the instance.
271
+
272
+ :return: a bool indicating whether ES is installed or not
273
+ :rtype: bool
274
+ """
275
+ return ES_APP_NAME in self.get_conn().apps
276
+
277
+ def check_for_es_install(self) -> None:
278
+ """
279
+ Validating function which raises an error if Enterprise Security is not installed and
280
+ integration testing is enabled.
281
+ """
282
+ if not self.es_installed and self.global_config.enable_integration_testing:
283
+ raise Exception(
284
+ "Enterprise Security does not appear to be installed on this instance and "
285
+ "integration testing is enabled."
286
+ )
287
+
175
288
  def configure_hec(self):
176
289
  self.hec_channel = str(uuid.uuid4())
177
290
  try:
@@ -298,14 +411,14 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
298
411
  imported_roles: list[str] = ["user", "power", "can_delete"],
299
412
  enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
300
413
  ):
301
- try:
302
- # Set which roles should be configured. For Enterprise Security/Integration Testing,
303
- # we must add some extra foles.
304
- if self.global_config.enable_integration_testing:
305
- roles = imported_roles + enterprise_security_roles
306
- else:
307
- roles = imported_roles
414
+ # Set which roles should be configured. For Enterprise Security/Integration Testing,
415
+ # we must add some extra foles.
416
+ if self.global_config.enable_integration_testing:
417
+ roles = imported_roles + enterprise_security_roles
418
+ else:
419
+ roles = imported_roles
308
420
 
421
+ try:
309
422
  self.get_conn().roles.post(
310
423
  self.infrastructure.splunk_app_username,
311
424
  imported_roles=roles,
@@ -314,16 +427,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
314
427
  )
315
428
  return
316
429
  except Exception as e:
317
- self.pbar.write(
318
- f"The following role(s) do not exist:'{enterprise_security_roles}: {str(e)}"
319
- )
320
-
321
- self.get_conn().roles.post(
322
- self.infrastructure.splunk_app_username,
323
- imported_roles=imported_roles,
324
- srchIndexesAllowed=";".join(self.all_indexes_on_server),
325
- srchIndexesDefault=self.sync_obj.replay_index,
326
- )
430
+ msg = f"Error configuring roles: {str(e)}"
431
+ self.pbar.write(msg)
432
+ raise Exception(msg) from e
327
433
 
328
434
  def configure_delete_indexes(self):
329
435
  endpoint = "/services/properties/authorize/default/deleteIndexesAllowed"
@@ -1170,8 +1276,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1170
1276
  # on a field. In this case, the field will appear but will not contain any values
1171
1277
  current_empty_fields: set[str] = set()
1172
1278
 
1173
- # TODO (cmcginley): @ljstella is this something we're keeping for testing as
1174
- # well?
1175
1279
  for field in full_rba_field_set:
1176
1280
  if result.get(field, "null") == "null":
1177
1281
  if field in risk_object_fields_set:
@@ -1257,7 +1361,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1257
1361
  job = self.get_conn().jobs.create(splunk_search, **kwargs)
1258
1362
  results_stream = job.results(output_mode="json")
1259
1363
  # TODO: should we be doing something w/ this reader?
1260
- _ = splunklib.results.JSONResultsReader(results_stream)
1364
+ _ = JSONResultsReader(results_stream)
1261
1365
 
1262
1366
  except Exception as e:
1263
1367
  raise (
@@ -4,14 +4,13 @@ from typing import Any
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
- from contentctl.objects.config import test_common
8
-
9
7
  from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
10
8
  DetectionTestingManagerOutputDto,
11
9
  )
12
10
  from contentctl.helper.utils import Utils
13
- from contentctl.objects.enums import DetectionStatus
14
11
  from contentctl.objects.base_test_result import TestResultStatus
12
+ from contentctl.objects.config import test_common
13
+ from contentctl.objects.enums import ContentStatus
15
14
 
16
15
 
17
16
  class DetectionTestingView(BaseModel, abc.ABC):
@@ -117,11 +116,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
117
116
  total_skipped += 1
118
117
 
119
118
  # Aggregate production status metrics
120
- if detection.status == DetectionStatus.production:
119
+ if detection.status == ContentStatus.production:
121
120
  total_production += 1
122
- elif detection.status == DetectionStatus.experimental:
121
+ elif detection.status == ContentStatus.experimental:
123
122
  total_experimental += 1
124
- elif detection.status == DetectionStatus.deprecated:
123
+ elif detection.status == ContentStatus.deprecated:
125
124
  total_deprecated += 1
126
125
 
127
126
  # Check if the detection is manual_test
@@ -47,6 +47,8 @@ class DetectionTestingViewCLI(DetectionTestingView, arbitrary_types_allowed=True
47
47
  while True:
48
48
  summary = self.getSummaryObject()
49
49
 
50
+ # TODO (#338): there's a 1-off error here I think (we show one more than we
51
+ # actually have during testing)
50
52
  total = len(
51
53
  summary.get("tested_detections", [])
52
54
  + summary.get("untested_detections", [])
@@ -1,7 +1,21 @@
1
- import shutil
2
1
  import os
3
2
  import pathlib
3
+ import shutil
4
+
5
+ from contentctl.objects.baseline import Baseline
4
6
  from contentctl.objects.config import test
7
+ from contentctl.objects.dashboard import Dashboard
8
+ from contentctl.objects.data_source import DataSource
9
+ from contentctl.objects.deployment import Deployment
10
+ from contentctl.objects.detection import Detection
11
+ from contentctl.objects.investigation import Investigation
12
+ from contentctl.objects.lookup import Lookup
13
+ from contentctl.objects.macro import Macro
14
+ from contentctl.objects.playbook import Playbook
15
+ from contentctl.objects.removed_security_content_object import (
16
+ RemovedSecurityContentObject,
17
+ )
18
+ from contentctl.objects.story import Story
5
19
  from contentctl.output.yml_writer import YmlWriter
6
20
 
7
21
 
@@ -13,21 +27,33 @@ class Initialize:
13
27
 
14
28
  YmlWriter.writeYmlFile(str(config.path / "contentctl.yml"), config.model_dump())
15
29
 
16
- # Create the following empty directories:
30
+ # Create the following empty directories. Each type of content,
31
+ # even if you don't have any of that type of content, need its own directory to exist.
32
+ for contentType in [
33
+ Detection,
34
+ Playbook,
35
+ Story,
36
+ DataSource,
37
+ Investigation,
38
+ Macro,
39
+ Lookup,
40
+ Dashboard,
41
+ Baseline,
42
+ Deployment,
43
+ RemovedSecurityContentObject,
44
+ ]:
45
+ contentType.containing_folder().mkdir(exist_ok=False, parents=True)
46
+
47
+ # Some other directories that do not map directly to a piece of content also must exist
48
+
17
49
  for emptyDir in [
18
- "lookups",
19
- "baselines",
20
- "data_sources",
21
50
  "docs",
22
51
  "reporting",
23
- "investigations",
24
52
  "detections/application",
25
53
  "detections/cloud",
26
54
  "detections/endpoint",
27
55
  "detections/network",
28
56
  "detections/web",
29
- "macros",
30
- "stories",
31
57
  ]:
32
58
  # Throw an error if this directory already exists
33
59
  (config.path / emptyDir).mkdir(exist_ok=False, parents=True)
@@ -60,7 +86,7 @@ class Initialize:
60
86
  source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
61
87
  target_directory = config.path / targetDir
62
88
  # Throw an exception if the target exists
63
- shutil.copytree(source_directory, target_directory, dirs_exist_ok=False)
89
+ shutil.copytree(source_directory, target_directory, dirs_exist_ok=True)
64
90
 
65
91
  # Create a README.md file. Note that this is the README.md for the repository, not the
66
92
  # one which will actually be packaged into the app. That is located in the app_template folder.
@@ -1,10 +1,12 @@
1
- from contentctl.objects.config import release_notes
2
- from git import Repo
3
- import re
4
- import yaml
5
1
  import pathlib
2
+ import re
6
3
  from typing import List, Union
7
4
 
5
+ import yaml
6
+ from git import Repo
7
+
8
+ from contentctl.objects.config import release_notes
9
+
8
10
 
9
11
  class ReleaseNotes:
10
12
  def create_notes(
@@ -171,13 +173,13 @@ class ReleaseNotes:
171
173
 
172
174
  def release_notes(self, config: release_notes) -> None:
173
175
  ### Remove hard coded path
174
- directories = [
175
- "detections/",
176
- "stories/",
177
- "macros/",
178
- "lookups/",
179
- "playbooks/",
180
- "ssa_detections/",
176
+ directories: list[pathlib.Path] = [
177
+ config.path / "detections",
178
+ config.path / "stories",
179
+ config.path / "macros",
180
+ config.path / "lookups",
181
+ config.path / "playbooks",
182
+ config.path / "ssa_detections",
181
183
  ]
182
184
 
183
185
  repo = Repo(config.path)
@@ -229,7 +231,7 @@ class ReleaseNotes:
229
231
  file_path = pathlib.Path(diff.a_path)
230
232
 
231
233
  # Check if the file is in the specified directories
232
- if any(str(file_path).startswith(directory) for directory in directories):
234
+ if any(file_path.is_relative_to(directory) for directory in directories):
233
235
  # Check if a file is Modified
234
236
  if diff.change_type == "M":
235
237
  modified_files.append(file_path)