contentctl 4.3.2__py3-none-any.whl → 4.3.4__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 (34) hide show
  1. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +35 -27
  2. contentctl/actions/detection_testing/views/DetectionTestingView.py +64 -38
  3. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -0
  4. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +3 -5
  5. contentctl/actions/test.py +55 -32
  6. contentctl/contentctl.py +3 -6
  7. contentctl/enrichments/attack_enrichment.py +2 -1
  8. contentctl/enrichments/cve_enrichment.py +2 -2
  9. contentctl/objects/abstract_security_content_objects/detection_abstract.py +183 -90
  10. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +1 -0
  11. contentctl/objects/annotated_types.py +6 -0
  12. contentctl/objects/base_test.py +1 -0
  13. contentctl/objects/base_test_result.py +1 -0
  14. contentctl/objects/config.py +27 -12
  15. contentctl/objects/correlation_search.py +35 -28
  16. contentctl/objects/detection_tags.py +8 -3
  17. contentctl/objects/integration_test.py +3 -5
  18. contentctl/objects/integration_test_result.py +1 -5
  19. contentctl/objects/investigation.py +1 -0
  20. contentctl/objects/manual_test.py +32 -0
  21. contentctl/objects/manual_test_result.py +8 -0
  22. contentctl/objects/mitre_attack_enrichment.py +3 -1
  23. contentctl/objects/risk_event.py +94 -76
  24. contentctl/objects/ssa_detection.py +1 -0
  25. contentctl/objects/story_tags.py +5 -3
  26. contentctl/objects/{unit_test_attack_data.py → test_attack_data.py} +4 -5
  27. contentctl/objects/test_group.py +3 -3
  28. contentctl/objects/unit_test.py +4 -11
  29. contentctl/output/templates/savedsearches_detections.j2 +1 -1
  30. {contentctl-4.3.2.dist-info → contentctl-4.3.4.dist-info}/METADATA +8 -8
  31. {contentctl-4.3.2.dist-info → contentctl-4.3.4.dist-info}/RECORD +34 -31
  32. {contentctl-4.3.2.dist-info → contentctl-4.3.4.dist-info}/LICENSE.md +0 -0
  33. {contentctl-4.3.2.dist-info → contentctl-4.3.4.dist-info}/WHEEL +0 -0
  34. {contentctl-4.3.2.dist-info → contentctl-4.3.4.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
  from typing import TYPE_CHECKING, Union, Optional, List, Any, Annotated
3
3
  import re
4
4
  import pathlib
5
+ from enum import Enum
6
+
5
7
  from pydantic import (
6
8
  field_validator,
7
9
  model_validator,
@@ -12,6 +14,7 @@ from pydantic import (
12
14
  ConfigDict,
13
15
  FilePath
14
16
  )
17
+
15
18
  from contentctl.objects.macro import Macro
16
19
  from contentctl.objects.lookup import Lookup
17
20
  if TYPE_CHECKING:
@@ -27,17 +30,25 @@ from contentctl.objects.enums import NistCategory
27
30
  from contentctl.objects.detection_tags import DetectionTags
28
31
  from contentctl.objects.deployment import Deployment
29
32
  from contentctl.objects.unit_test import UnitTest
33
+ from contentctl.objects.manual_test import ManualTest
30
34
  from contentctl.objects.test_group import TestGroup
31
35
  from contentctl.objects.integration_test import IntegrationTest
32
36
  from contentctl.objects.data_source import DataSource
37
+ from contentctl.objects.base_test_result import TestResultStatus
33
38
 
34
39
  # from contentctl.objects.playbook import Playbook
35
40
  from contentctl.objects.enums import ProvidingTechnology
36
41
  from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
37
-
42
+ import datetime
38
43
  MISSING_SOURCES: set[str] = set()
39
44
 
45
+ # Those AnalyticsTypes that we do not test via contentctl
46
+ SKIPPED_ANALYTICS_TYPES: set[str] = {
47
+ AnalyticsType.Correlation.value
48
+ }
40
49
 
50
+
51
+ # TODO (#266): disable the use_enum_values configuration
41
52
  class Detection_Abstract(SecurityContentObject):
42
53
  model_config = ConfigDict(use_enum_values=True)
43
54
 
@@ -57,7 +68,7 @@ class Detection_Abstract(SecurityContentObject):
57
68
  # default mode, 'smart'
58
69
  # https://docs.pydantic.dev/latest/concepts/unions/#left-to-right-mode
59
70
  # https://github.com/pydantic/pydantic/issues/9101#issuecomment-2019032541
60
- tests: List[Annotated[Union[UnitTest, IntegrationTest], Field(union_mode='left_to_right')]] = []
71
+ tests: List[Annotated[Union[UnitTest, IntegrationTest, ManualTest], Field(union_mode='left_to_right')]] = []
61
72
  # A list of groups of tests, relying on the same data
62
73
  test_groups: Union[list[TestGroup], None] = Field(None, validate_default=True)
63
74
 
@@ -115,35 +126,102 @@ class Detection_Abstract(SecurityContentObject):
115
126
 
116
127
  return value
117
128
 
118
- @field_validator("test_groups")
119
- @classmethod
120
- def validate_test_groups(
121
- cls,
122
- value: Union[None, List[TestGroup]],
123
- info: ValidationInfo
124
- ) -> Union[List[TestGroup], None]:
129
+ def adjust_tests_and_groups(self) -> None:
125
130
  """
126
- Validates the `test_groups` field and constructs the model from the list of unit tests
127
- if no explicit construct was provided
128
- :param value: the value of the field `test_groups`
129
- :param values: a dict of the other fields in the Detection model
131
+ Converts UnitTest to ManualTest as needed, B=builds the `test_groups` field, constructing
132
+ the model from the list of unit tests. Also, preemptively skips all manual tests, as well as
133
+ tests for experimental/deprecated detections and Correlation type detections.
130
134
  """
131
- # if the value was not the None default, do nothing
132
- if value is not None:
133
- return value
135
+ # Since ManualTest and UnitTest are not differentiable without looking at the manual_test
136
+ # tag, Pydantic builds all tests as UnitTest objects. If we see the manual_test flag, we
137
+ # convert these to ManualTest
138
+ tmp: list[UnitTest | IntegrationTest | ManualTest] = []
139
+ if self.tags.manual_test is not None:
140
+ for test in self.tests:
141
+ if not isinstance(test, UnitTest):
142
+ raise ValueError(
143
+ "At this point of intialization, tests should only be UnitTest objects, "
144
+ f"but encountered a {type(test)}."
145
+ )
146
+ # Create the manual test and skip it upon creation (cannot test via contentctl)
147
+ manual_test = ManualTest(
148
+ name=test.name,
149
+ attack_data=test.attack_data
150
+ )
151
+ tmp.append(manual_test)
152
+ self.tests = tmp
134
153
 
135
- # iterate over the unit tests and create a TestGroup (and as a result, an IntegrationTest) for each
136
- test_groups: list[TestGroup] = []
137
- tests: list[UnitTest | IntegrationTest] = info.data.get("tests") # type: ignore
138
- unit_test: UnitTest
139
- for unit_test in tests: # type: ignore
140
- test_group = TestGroup.derive_from_unit_test(unit_test, info.data.get("name")) # type: ignore
141
- test_groups.append(test_group)
154
+ # iterate over the tests and create a TestGroup (and as a result, an IntegrationTest) for
155
+ # each unit test
156
+ self.test_groups = []
157
+ for test in self.tests:
158
+ # We only derive TestGroups from UnitTests (ManualTest is ignored and IntegrationTests
159
+ # have not been created yet)
160
+ if isinstance(test, UnitTest):
161
+ test_group = TestGroup.derive_from_unit_test(test, self.name)
162
+ self.test_groups.append(test_group)
142
163
 
143
164
  # now add each integration test to the list of tests
144
- for test_group in test_groups:
145
- tests.append(test_group.integration_test)
146
- return test_groups
165
+ for test_group in self.test_groups:
166
+ self.tests.append(test_group.integration_test)
167
+
168
+ # Skip all manual tests
169
+ self.skip_manual_tests()
170
+
171
+ # NOTE: we ignore the type error around self.status because we are using Pydantic's
172
+ # use_enum_values configuration
173
+ # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
174
+
175
+ # Skip tests for non-production detections
176
+ if self.status != DetectionStatus.production.value: # type: ignore
177
+ self.skip_all_tests(f"TEST SKIPPED: Detection is non-production ({self.status})")
178
+
179
+ # Skip tests for detecton types like Correlation which are not supported via contentctl
180
+ if self.type in SKIPPED_ANALYTICS_TYPES:
181
+ self.skip_all_tests(
182
+ f"TEST SKIPPED: Detection type {self.type} cannot be tested by contentctl"
183
+ )
184
+
185
+ @property
186
+ def test_status(self) -> TestResultStatus | None:
187
+ """
188
+ Returns the collective status of the detections tests. If any test status has yet to be set,
189
+ None is returned.If any test failed or errored, FAIL is returned. If all tests were skipped,
190
+ SKIP is returned. If at least one test passed and the rest passed or skipped, PASS is
191
+ returned.
192
+ """
193
+ # If the detection has no tests, we consider it to have been skipped (only non-production,
194
+ # non-manual, non-correlation detections are allowed to have no tests defined)
195
+ if len(self.tests) == 0:
196
+ return TestResultStatus.SKIP
197
+
198
+ passed = 0
199
+ skipped = 0
200
+ for test in self.tests:
201
+ # If the result/status of any test has not yet been set, return None
202
+ if test.result is None or test.result.status is None:
203
+ return None
204
+ elif test.result.status == TestResultStatus.ERROR or test.result.status == TestResultStatus.FAIL:
205
+ # If any test failed or errored, return fail (we don't return the error state at
206
+ # the aggregate detection level)
207
+ return TestResultStatus.FAIL
208
+ elif test.result.status == TestResultStatus.SKIP:
209
+ skipped += 1
210
+ elif test.result.status == TestResultStatus.PASS:
211
+ passed += 1
212
+ else:
213
+ raise ValueError(
214
+ f"Undefined test status for test ({test.name}) in detection ({self.name})"
215
+ )
216
+
217
+ # If at least one of the tests passed and the rest passed or skipped, report pass
218
+ if passed > 0 and (passed + skipped) == len(self.tests):
219
+ return TestResultStatus.PASS
220
+ elif skipped == len(self.tests):
221
+ # If all tests skipped, return skip
222
+ return TestResultStatus.SKIP
223
+
224
+ raise ValueError(f"Undefined overall test status for detection: {self.name}")
147
225
 
148
226
  @computed_field
149
227
  @property
@@ -244,12 +322,13 @@ class Detection_Abstract(SecurityContentObject):
244
322
  @property
245
323
  def providing_technologies(self) -> List[ProvidingTechnology]:
246
324
  return ProvidingTechnology.getProvidingTechFromSearch(self.search)
247
-
248
-
325
+
326
+ # TODO (#247): Refactor the risk property of detection_abstract
249
327
  @computed_field
250
328
  @property
251
329
  def risk(self) -> list[dict[str, Any]]:
252
330
  risk_objects: list[dict[str, str | int]] = []
331
+ # TODO (#246): "User Name" type should map to a "user" risk object and not "other"
253
332
  risk_object_user_types = {'user', 'username', 'email address'}
254
333
  risk_object_system_types = {'device', 'endpoint', 'hostname', 'ip address'}
255
334
  process_threat_object_types = {'process name', 'process'}
@@ -307,14 +386,16 @@ class Detection_Abstract(SecurityContentObject):
307
386
 
308
387
  @computed_field
309
388
  @property
310
- def metadata(self) -> dict[str, str]:
389
+ def metadata(self) -> dict[str, str|float]:
311
390
  # NOTE: we ignore the type error around self.status because we are using Pydantic's
312
391
  # use_enum_values configuration
313
392
  # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
393
+
314
394
  return {
315
395
  'detection_id': str(self.id),
316
396
  'deprecated': '1' if self.status == DetectionStatus.deprecated.value else '0', # type: ignore
317
- 'detection_version': str(self.version)
397
+ 'detection_version': str(self.version),
398
+ 'publish_time': datetime.datetime(self.date.year,self.date.month,self.date.day,0,0,0,0,tzinfo=datetime.timezone.utc).timestamp()
318
399
  }
319
400
 
320
401
  @model_serializer
@@ -439,6 +520,9 @@ class Detection_Abstract(SecurityContentObject):
439
520
 
440
521
  self.cve_enrichment_func(__context)
441
522
 
523
+ # Derive TestGroups and IntegrationTests, adjust for ManualTests, skip as needed
524
+ self.adjust_tests_and_groups()
525
+
442
526
  @field_validator('lookups', mode="before")
443
527
  @classmethod
444
528
  def getDetectionLookups(cls, v:list[str], info:ValidationInfo) -> list[Lookup]:
@@ -644,68 +728,65 @@ class Detection_Abstract(SecurityContentObject):
644
728
  # Found everything
645
729
  return self
646
730
 
647
- @model_validator(mode='after')
648
- def ensurePresenceOfRequiredTests(self):
649
- # NOTE: we ignore the type error around self.status because we are using Pydantic's
650
- # use_enum_values configuration
651
- # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
652
-
653
- # Only production analytics require tests
654
- if self.status != DetectionStatus.production.value: # type: ignore
655
- return self
656
-
657
- # All types EXCEPT Correlation MUST have test(s). Any other type, including newly defined types, requires them.
658
- # Accordingly, we do not need to do additional checks if the type is Correlation
659
- if self.type in set([AnalyticsType.Correlation.value]):
660
- return self
661
-
662
- if self.tags.manual_test is not None:
663
- for test in self.tests:
664
- test.skip(
665
- f"TEST SKIPPED: Detection marked as 'manual_test' with explanation: '{self.tags.manual_test}'"
666
- )
667
-
668
- if len(self.tests) == 0:
669
- raise ValueError(f"At least one test is REQUIRED for production detection: {self.name}")
670
-
671
- return self
672
-
673
731
  @field_validator("tests")
674
732
  def tests_validate(
675
733
  cls,
676
- v: list[UnitTest | IntegrationTest],
734
+ v: list[UnitTest | IntegrationTest | ManualTest],
677
735
  info: ValidationInfo
678
- ) -> list[UnitTest | IntegrationTest]:
736
+ ) -> list[UnitTest | IntegrationTest | ManualTest]:
679
737
  # Only production analytics require tests
680
738
  if info.data.get("status", "") != DetectionStatus.production.value:
681
739
  return v
682
740
 
683
- # All types EXCEPT Correlation MUST have test(s). Any other type, including newly defined types, requires them.
684
- # Accordingly, we do not need to do additional checks if the type is Correlation
685
- if info.data.get("type", "") in set([AnalyticsType.Correlation.value]):
741
+ # All types EXCEPT Correlation MUST have test(s). Any other type, including newly defined
742
+ # types, requires them. Accordingly, we do not need to do additional checks if the type is
743
+ # Correlation
744
+ if info.data.get("type", "") in SKIPPED_ANALYTICS_TYPES:
745
+ return v
746
+
747
+ # Manually tested detections are not required to have tests defined
748
+ tags: DetectionTags | None = info.data.get("tags", None)
749
+ if tags is not None and tags.manual_test is not None:
686
750
  return v
687
751
 
688
752
  # Ensure that there is at least 1 test
689
753
  if len(v) == 0:
690
- if info.data.get("tags", None) and info.data.get("tags").manual_test is not None: # type: ignore
691
- # Detections that are manual_test MAY have detections, but it is not required. If they
692
- # do not have one, then create one which will be a placeholder.
693
- # Note that this fake UnitTest (and by extension, Integration Test) will NOT be generated
694
- # if there ARE test(s) defined for a Detection.
695
- placeholder_test = UnitTest( # type: ignore
696
- name="PLACEHOLDER FOR DETECTION TAGGED MANUAL_TEST WITH NO TESTS SPECIFIED IN YML FILE",
697
- attack_data=[]
698
- )
699
- return [placeholder_test]
700
-
701
- else:
702
- raise ValueError(
703
- "At least one test is REQUIRED for production detection: " + info.data.get("name", "NO NAME FOUND")
704
- )
754
+ raise ValueError(
755
+ "At least one test is REQUIRED for production detection: " + info.data.get("name", "NO NAME FOUND")
756
+ )
705
757
 
706
758
  # No issues - at least one test provided for production type requiring testing
707
759
  return v
708
760
 
761
+ def skip_all_tests(self, message: str = "TEST SKIPPED") -> None:
762
+ """
763
+ Given a message, skip all tests for this detection.
764
+ :param message: the message to set in the test result
765
+ """
766
+ for test in self.tests:
767
+ test.skip(message=message)
768
+
769
+ def skip_manual_tests(self) -> None:
770
+ """
771
+ Skips all ManualTests, if the manual_test flag is set; also raises an error if any other
772
+ test types are found for a manual_test detection
773
+ """
774
+ # Skip all ManualTest
775
+ if self.tags.manual_test is not None:
776
+ for test in self.tests:
777
+ if isinstance(test, ManualTest):
778
+ test.skip(
779
+ message=(
780
+ "TEST SKIPPED (MANUAL): Detection marked as 'manual_test' with "
781
+ f"explanation: {self.tags.manual_test}"
782
+ )
783
+ )
784
+ else:
785
+ raise ValueError(
786
+ "A detection with the manual_test flag should only have tests of type "
787
+ "ManualTest"
788
+ )
789
+
709
790
  def all_tests_successful(self) -> bool:
710
791
  """
711
792
  Checks that all tests in the detection succeeded. If no tests are defined, consider that a
@@ -715,9 +796,11 @@ class Detection_Abstract(SecurityContentObject):
715
796
  :returns: bool where True indicates all tests succeeded (they existed, complete and were
716
797
  PASS/SKIP)
717
798
  """
718
- # If no tests are defined, we consider it a failure for the detection
799
+ # If no tests are defined, we consider it a success for the detection (this detection was
800
+ # skipped for testing). Note that the existence of at least one test is enforced by Pydantic
801
+ # validation already, with a few specific exceptions
719
802
  if len(self.tests) == 0:
720
- return False
803
+ return True
721
804
 
722
805
  # Iterate over tests
723
806
  for test in self.tests:
@@ -740,7 +823,13 @@ class Detection_Abstract(SecurityContentObject):
740
823
 
741
824
  def get_summary(
742
825
  self,
743
- detection_fields: list[str] = ["name", "search"],
826
+ detection_fields: list[str] = [
827
+ "name", "type", "status", "test_status", "source", "data_source", "search", "file_path"
828
+ ],
829
+ detection_field_aliases: dict[str, str] = {
830
+ "status": "production_status", "test_status": "status", "source": "source_category"
831
+ },
832
+ tags_fields: list[str] = ["manual_test"],
744
833
  test_result_fields: list[str] = ["success", "message", "exception", "status", "duration", "wait_duration"],
745
834
  test_job_fields: list[str] = ["resultCount", "runDuration"],
746
835
  ) -> dict[str, Any]:
@@ -756,7 +845,21 @@ class Detection_Abstract(SecurityContentObject):
756
845
 
757
846
  # Grab the top level detection fields
758
847
  for field in detection_fields:
759
- summary_dict[field] = getattr(self, field)
848
+ value = getattr(self, field)
849
+
850
+ # Enums and Path objects cannot be serialized directly, so we convert it to a string
851
+ if isinstance(value, Enum) or isinstance(value, pathlib.Path):
852
+ value = str(value)
853
+
854
+ # Alias any fields as needed
855
+ if field in detection_field_aliases:
856
+ summary_dict[detection_field_aliases[field]] = value
857
+ else:
858
+ summary_dict[field] = value
859
+
860
+ # Grab fields from the tags
861
+ for field in tags_fields:
862
+ summary_dict[field] = getattr(self.tags, field)
760
863
 
761
864
  # Set success based on whether all tests passed
762
865
  summary_dict["success"] = self.all_tests_successful()
@@ -789,13 +892,3 @@ class Detection_Abstract(SecurityContentObject):
789
892
  # Return the summary
790
893
 
791
894
  return summary_dict
792
-
793
- def getMetadata(self) -> dict[str, str]:
794
- # NOTE: we ignore the type error around self.status because we are using Pydantic's
795
- # use_enum_values configuration
796
- # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
797
- return {
798
- 'detection_id': str(self.id),
799
- 'deprecated': '1' if self.status == DetectionStatus.deprecated.value else '0', # type: ignore
800
- 'detection_version': str(self.version)
801
- }
@@ -29,6 +29,7 @@ import pathlib
29
29
  NO_FILE_NAME = "NO_FILE_NAME"
30
30
 
31
31
 
32
+ # TODO (#266): disable the use_enum_values configuration
32
33
  class SecurityContentObject_Abstract(BaseModel, abc.ABC):
33
34
  model_config = ConfigDict(use_enum_values=True, validate_default=True)
34
35
 
@@ -0,0 +1,6 @@
1
+ from pydantic import Field
2
+ from typing import Annotated
3
+
4
+ CVE_TYPE = Annotated[str, Field(pattern=r"^CVE-[1|2]\d{3}-\d+$")]
5
+ MITRE_ATTACK_ID_TYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]
6
+ APPID_TYPE = Annotated[str,Field(pattern="^[a-zA-Z0-9_-]+$")]
@@ -13,6 +13,7 @@ class TestType(str, Enum):
13
13
  """
14
14
  UNIT = "unit"
15
15
  INTEGRATION = "integration"
16
+ MANUAL = "manual"
16
17
 
17
18
  def __str__(self) -> str:
18
19
  return self.value
@@ -7,6 +7,7 @@ from splunklib.data import Record
7
7
  from contentctl.helper.utils import Utils
8
8
 
9
9
 
10
+ # TODO (#267): Align test reporting more closely w/ status enums (as it relates to "untested")
10
11
  # TODO (PEX-432): add status "UNSET" so that we can make sure the result is always of this enum
11
12
  # type; remove mypy ignores associated w/ these typing issues once we do
12
13
  class TestResultStatus(str, Enum):
@@ -16,9 +16,9 @@ import pathlib
16
16
  from contentctl.helper.utils import Utils
17
17
  from urllib.parse import urlparse
18
18
  from abc import ABC, abstractmethod
19
- from contentctl.objects.enums import PostTestBehavior
19
+ from contentctl.objects.enums import PostTestBehavior, DetectionTestingMode
20
20
  from contentctl.objects.detection import Detection
21
-
21
+ from contentctl.objects.annotated_types import APPID_TYPE
22
22
  import tqdm
23
23
  from functools import partialmethod
24
24
 
@@ -27,11 +27,13 @@ COMMON_INFORMATION_MODEL_UID = 1621
27
27
 
28
28
  SPLUNKBASE_URL = "https://splunkbase.splunk.com/app/{uid}/release/{version}/download"
29
29
 
30
+
31
+ # TODO (#266): disable the use_enum_values configuration
30
32
  class App_Base(BaseModel,ABC):
31
33
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
32
34
  uid: Optional[int] = Field(default=None)
33
35
  title: str = Field(description="Human-readable name used by the app. This can have special characters.")
34
- appid: Optional[Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]]= Field(default=None,description="Internal name used by your app. "
36
+ appid: Optional[APPID_TYPE]= Field(default=None,description="Internal name used by your app. "
35
37
  "It may ONLY have characters, numbers, and underscores. No other characters are allowed.")
36
38
  version: str = Field(description="The version of your Content Pack. This must follow semantic versioning guidelines.")
37
39
  description: Optional[str] = Field(default="description of app",description="Free text description of the Content Pack.")
@@ -51,6 +53,8 @@ class App_Base(BaseModel,ABC):
51
53
  if not config.getLocalAppDir().exists():
52
54
  config.getLocalAppDir().mkdir(parents=True)
53
55
 
56
+
57
+ # TODO (#266): disable the use_enum_values configuration
54
58
  class TestApp(App_Base):
55
59
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
56
60
  hardcoded_path: Optional[Union[FilePath,HttpUrl]] = Field(default=None, description="This may be a relative or absolute link to a file OR an HTTP URL linking to your app.")
@@ -89,13 +93,15 @@ class TestApp(App_Base):
89
93
 
90
94
  return str(destination)
91
95
 
96
+
97
+ # TODO (#266): disable the use_enum_values configuration
92
98
  class CustomApp(App_Base):
93
99
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
94
100
  # Fields required for app.conf based on
95
101
  # https://docs.splunk.com/Documentation/Splunk/9.0.4/Admin/Appconf
96
102
  uid: int = Field(ge=2, lt=100000, default_factory=lambda:random.randint(20000,100000))
97
103
  title: str = Field(default="Content Pack",description="Human-readable name used by the app. This can have special characters.")
98
- appid: Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]= Field(default="ContentPack",description="Internal name used by your app. "
104
+ appid: APPID_TYPE = Field(default="ContentPack",description="Internal name used by your app. "
99
105
  "It may ONLY have characters, numbers, and underscores. No other characters are allowed.")
100
106
  version: str = Field(default="0.0.1",description="The version of your Content Pack. This must follow semantic versioning guidelines.", validate_default=True)
101
107
 
@@ -149,6 +155,7 @@ class CustomApp(App_Base):
149
155
  return str(destination)
150
156
 
151
157
 
158
+ # TODO (#266): disable the use_enum_values configuration
152
159
  class Config_Base(BaseModel):
153
160
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
154
161
 
@@ -167,6 +174,7 @@ class init(Config_Base):
167
174
  pass
168
175
 
169
176
 
177
+ # TODO (#266): disable the use_enum_values configuration
170
178
  class validate(Config_Base):
171
179
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
172
180
  enrichments: bool = Field(default=False, description="Enable MITRE, APP, and CVE Enrichments. "\
@@ -186,7 +194,7 @@ class report(validate):
186
194
  return self.path/"reporting/"
187
195
 
188
196
 
189
-
197
+ # TODO (#266): disable the use_enum_values configuration
190
198
  class build(validate):
191
199
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
192
200
  build_path: DirectoryPath = Field(default=DirectoryPath("dist/"), title="Target path for all build outputs")
@@ -255,6 +263,7 @@ class new(Config_Base):
255
263
  type: NewContentType = Field(default=NewContentType.detection, description="Specify the type of content you would like to create.")
256
264
 
257
265
 
266
+ # TODO (#266): disable the use_enum_values configuration
258
267
  class deploy_acs(inspect):
259
268
  model_config = ConfigDict(use_enum_values=True,validate_default=False, arbitrary_types_allowed=True)
260
269
  #ignore linter error
@@ -262,6 +271,7 @@ class deploy_acs(inspect):
262
271
  splunk_cloud_stack: str = Field(description="The name of your Splunk Cloud Stack")
263
272
 
264
273
 
274
+ # TODO (#266): disable the use_enum_values configuration
265
275
  class Infrastructure(BaseModel):
266
276
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
267
277
  splunk_app_username:str = Field(default="admin", description="Username for logging in to your Splunk Server")
@@ -273,11 +283,13 @@ class Infrastructure(BaseModel):
273
283
  instance_name: str = Field(...)
274
284
 
275
285
 
286
+ # TODO (#266): disable the use_enum_values configuration
276
287
  class Container(Infrastructure):
277
288
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
278
289
  instance_address:str = Field(default="localhost", description="Address of your splunk server.")
279
290
 
280
291
 
292
+ # TODO (#266): disable the use_enum_values configuration
281
293
  class ContainerSettings(BaseModel):
282
294
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
283
295
  leave_running: bool = Field(default=True, description="Leave container running after it is first "
@@ -302,11 +314,14 @@ class All(BaseModel):
302
314
  #Doesn't need any extra logic
303
315
  pass
304
316
 
317
+
318
+ # TODO (#266): disable the use_enum_values configuration
305
319
  class Changes(BaseModel):
306
320
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
307
321
  target_branch:str = Field(...,description="The target branch to diff against. Note that this includes uncommitted changes in the working directory as well.")
308
322
 
309
323
 
324
+ # TODO (#266): disable the use_enum_values configuration
310
325
  class Selected(BaseModel):
311
326
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
312
327
  files:List[FilePath] = Field(...,description="List of detection files to test, separated by spaces.")
@@ -672,17 +687,14 @@ class test_common(build):
672
687
 
673
688
  def getModeName(self)->str:
674
689
  if isinstance(self.mode, All):
675
- return "All"
690
+ return DetectionTestingMode.all.value
676
691
  elif isinstance(self.mode, Changes):
677
- return "Changes"
692
+ return DetectionTestingMode.changes.value
678
693
  else:
679
- return "Selected"
680
-
681
-
682
-
683
-
694
+ return DetectionTestingMode.selected.value
684
695
 
685
696
 
697
+ # TODO (#266): disable the use_enum_values configuration
686
698
  class test(test_common):
687
699
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
688
700
  container_settings:ContainerSettings = ContainerSettings()
@@ -747,6 +759,9 @@ class test(test_common):
747
759
 
748
760
 
749
761
  TEST_ARGS_ENV = "CONTENTCTL_TEST_INFRASTRUCTURES"
762
+
763
+
764
+ # TODO (#266): disable the use_enum_values configuration
750
765
  class test_servers(test_common):
751
766
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
752
767
  test_instances:List[Infrastructure] = Field([],description="Test against one or more preconfigured servers.", validate_default=True)