contentctl 4.3.5__py3-none-any.whl → 4.4.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 (49) hide show
  1. contentctl/actions/build.py +1 -0
  2. contentctl/actions/detection_testing/GitService.py +10 -10
  3. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +68 -38
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +5 -1
  5. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +10 -8
  6. contentctl/actions/inspect.py +6 -4
  7. contentctl/actions/new_content.py +10 -2
  8. contentctl/actions/release_notes.py +5 -3
  9. contentctl/actions/validate.py +2 -1
  10. contentctl/enrichments/cve_enrichment.py +6 -7
  11. contentctl/input/director.py +14 -12
  12. contentctl/input/new_content_questions.py +9 -42
  13. contentctl/objects/abstract_security_content_objects/detection_abstract.py +147 -7
  14. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
  15. contentctl/objects/base_test_result.py +7 -7
  16. contentctl/objects/baseline.py +12 -18
  17. contentctl/objects/baseline_tags.py +2 -5
  18. contentctl/objects/config.py +15 -9
  19. contentctl/objects/constants.py +30 -0
  20. contentctl/objects/correlation_search.py +79 -114
  21. contentctl/objects/dashboard.py +100 -0
  22. contentctl/objects/deployment.py +20 -5
  23. contentctl/objects/detection_tags.py +22 -20
  24. contentctl/objects/drilldown.py +70 -0
  25. contentctl/objects/enums.py +26 -22
  26. contentctl/objects/investigation.py +23 -15
  27. contentctl/objects/investigation_tags.py +4 -3
  28. contentctl/objects/lookup.py +8 -1
  29. contentctl/objects/macro.py +16 -7
  30. contentctl/objects/notable_event.py +6 -5
  31. contentctl/objects/risk_analysis_action.py +4 -4
  32. contentctl/objects/risk_event.py +8 -7
  33. contentctl/objects/story.py +4 -16
  34. contentctl/objects/throttling.py +46 -0
  35. contentctl/output/conf_output.py +4 -0
  36. contentctl/output/conf_writer.py +20 -3
  37. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  38. contentctl/output/templates/analyticstories_investigations.j2 +5 -5
  39. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  40. contentctl/output/templates/savedsearches_baselines.j2 +2 -3
  41. contentctl/output/templates/savedsearches_detections.j2 +12 -7
  42. contentctl/output/templates/savedsearches_investigations.j2 +3 -4
  43. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
  44. {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/METADATA +3 -2
  45. {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/RECORD +48 -46
  46. {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/WHEEL +1 -1
  47. contentctl/output/templates/finding_report.j2 +0 -30
  48. {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/LICENSE.md +0 -0
  49. {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,11 @@
1
+ from typing import Any
2
+ from contentctl.objects.enums import DataSource
3
+
4
+
1
5
  class NewContentQuestions:
2
6
 
3
7
  @classmethod
4
- def get_questions_detection(self) -> list:
8
+ def get_questions_detection(cls) -> list[dict[str,Any]]:
5
9
  questions = [
6
10
  {
7
11
  "type": "text",
@@ -45,46 +49,9 @@ class NewContentQuestions:
45
49
  'type': 'checkbox',
46
50
  'message': 'Your data source',
47
51
  'name': 'data_source',
48
- 'choices': [
49
- "OSQuery ES Process Events",
50
- "Powershell 4104",
51
- "Sysmon Event ID 1",
52
- "Sysmon Event ID 3",
53
- "Sysmon Event ID 5",
54
- "Sysmon Event ID 6",
55
- "Sysmon Event ID 7",
56
- "Sysmon Event ID 8",
57
- "Sysmon Event ID 9",
58
- "Sysmon Event ID 10",
59
- "Sysmon Event ID 11",
60
- "Sysmon Event ID 13",
61
- "Sysmon Event ID 15",
62
- "Sysmon Event ID 20",
63
- "Sysmon Event ID 21",
64
- "Sysmon Event ID 22",
65
- "Sysmon Event ID 23",
66
- "Windows Security 4624",
67
- "Windows Security 4625",
68
- "Windows Security 4648",
69
- "Windows Security 4663",
70
- "Windows Security 4688",
71
- "Windows Security 4698",
72
- "Windows Security 4703",
73
- "Windows Security 4720",
74
- "Windows Security 4732",
75
- "Windows Security 4738",
76
- "Windows Security 4741",
77
- "Windows Security 4742",
78
- "Windows Security 4768",
79
- "Windows Security 4769",
80
- "Windows Security 4771",
81
- "Windows Security 4776",
82
- "Windows Security 4781",
83
- "Windows Security 4798",
84
- "Windows Security 5136",
85
- "Windows Security 5145",
86
- "Windows System 7045"
87
- ]
52
+ #In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
53
+ 'choices': sorted(DataSource._value2member_map_ )
54
+
88
55
  },
89
56
  {
90
57
  "type": "text",
@@ -116,7 +83,7 @@ class NewContentQuestions:
116
83
  return questions
117
84
 
118
85
  @classmethod
119
- def get_questions_story(self) -> list:
86
+ def get_questions_story(cls)-> list[dict[str,Any]]:
120
87
  questions = [
121
88
  {
122
89
  "type": "text",
@@ -20,7 +20,8 @@ from contentctl.objects.lookup import Lookup
20
20
  if TYPE_CHECKING:
21
21
  from contentctl.input.director import DirectorOutputDto
22
22
  from contentctl.objects.baseline import Baseline
23
-
23
+ from contentctl.objects.config import CustomApp
24
+
24
25
  from contentctl.objects.security_content_object import SecurityContentObject
25
26
  from contentctl.objects.enums import AnalyticsType
26
27
  from contentctl.objects.enums import DataModel
@@ -35,11 +36,17 @@ from contentctl.objects.test_group import TestGroup
35
36
  from contentctl.objects.integration_test import IntegrationTest
36
37
  from contentctl.objects.data_source import DataSource
37
38
  from contentctl.objects.base_test_result import TestResultStatus
38
-
39
- # from contentctl.objects.playbook import Playbook
39
+ from contentctl.objects.drilldown import Drilldown, DRILLDOWN_SEARCH_PLACEHOLDER
40
40
  from contentctl.objects.enums import ProvidingTechnology
41
41
  from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
42
42
  import datetime
43
+ from contentctl.objects.constants import (
44
+ ES_MAX_STANZA_LENGTH,
45
+ ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE,
46
+ CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
47
+ CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE
48
+ )
49
+
43
50
  MISSING_SOURCES: set[str] = set()
44
51
 
45
52
  # Those AnalyticsTypes that we do not test via contentctl
@@ -51,8 +58,8 @@ SKIPPED_ANALYTICS_TYPES: set[str] = {
51
58
  # TODO (#266): disable the use_enum_values configuration
52
59
  class Detection_Abstract(SecurityContentObject):
53
60
  model_config = ConfigDict(use_enum_values=True)
54
-
55
- # contentType: SecurityContentType = SecurityContentType.detections
61
+ name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
62
+ #contentType: SecurityContentType = SecurityContentType.detections
56
63
  type: AnalyticsType = Field(...)
57
64
  status: DetectionStatus = Field(...)
58
65
  data_source: list[str] = []
@@ -60,6 +67,16 @@ class Detection_Abstract(SecurityContentObject):
60
67
  search: str = Field(...)
61
68
  how_to_implement: str = Field(..., min_length=4)
62
69
  known_false_positives: str = Field(..., min_length=4)
70
+ explanation: None | str = Field(
71
+ default=None,
72
+ exclude=True, #Don't serialize this value when dumping the object
73
+ description="Provide an explanation to be included "
74
+ "in the 'Explanation' field of the Detection in "
75
+ "the Use Case Library. If this field is not "
76
+ "defined in the YML, it will default to the "
77
+ "value of the 'description' field when "
78
+ "serialized in analyticstories_detections.j2",
79
+ )
63
80
 
64
81
  enabled_by_default: bool = False
65
82
  file_path: FilePath = Field(...)
@@ -70,9 +87,30 @@ class Detection_Abstract(SecurityContentObject):
70
87
  # https://github.com/pydantic/pydantic/issues/9101#issuecomment-2019032541
71
88
  tests: List[Annotated[Union[UnitTest, IntegrationTest, ManualTest], Field(union_mode='left_to_right')]] = []
72
89
  # A list of groups of tests, relying on the same data
73
- test_groups: Union[list[TestGroup], None] = Field(None, validate_default=True)
90
+ test_groups: list[TestGroup] = []
74
91
 
75
92
  data_source_objects: list[DataSource] = []
93
+ drilldown_searches: list[Drilldown] = Field(default=[], description="A list of Drilldowns that should be included with this search")
94
+
95
+ def get_conf_stanza_name(self, app:CustomApp)->str:
96
+ stanza_name = CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
97
+ self.check_conf_stanza_max_length(stanza_name)
98
+ return stanza_name
99
+
100
+
101
+ def get_action_dot_correlationsearch_dot_label(self, app:CustomApp, max_stanza_length:int=ES_MAX_STANZA_LENGTH)->str:
102
+ stanza_name = self.get_conf_stanza_name(app)
103
+ stanza_name_after_saving_in_es = ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(
104
+ security_domain_value = self.tags.security_domain.value,
105
+ search_name = stanza_name
106
+ )
107
+
108
+
109
+ if len(stanza_name_after_saving_in_es) > max_stanza_length:
110
+ raise ValueError(f"label may only be {max_stanza_length} characters to allow updating in-product, "
111
+ f"but stanza was actually {len(stanza_name_after_saving_in_es)} characters: '{stanza_name_after_saving_in_es}' ")
112
+
113
+ return stanza_name
76
114
 
77
115
  @field_validator("search", mode="before")
78
116
  @classmethod
@@ -130,6 +168,7 @@ class Detection_Abstract(SecurityContentObject):
130
168
  the model from the list of unit tests. Also, preemptively skips all manual tests, as well as
131
169
  tests for experimental/deprecated detections and Correlation type detections.
132
170
  """
171
+
133
172
  # Since ManualTest and UnitTest are not differentiable without looking at the manual_test
134
173
  # tag, Pydantic builds all tests as UnitTest objects. If we see the manual_test flag, we
135
174
  # convert these to ManualTest
@@ -248,6 +287,7 @@ class Detection_Abstract(SecurityContentObject):
248
287
  annotations_dict["cve"] = self.tags.cve
249
288
  annotations_dict["impact"] = self.tags.impact
250
289
  annotations_dict["type"] = self.type
290
+ annotations_dict["type_list"] = [self.type]
251
291
  # annotations_dict["version"] = self.version
252
292
 
253
293
  annotations_dict["data_source"] = self.data_source
@@ -518,13 +558,53 @@ class Detection_Abstract(SecurityContentObject):
518
558
  self.data_source_objects = matched_data_sources
519
559
 
520
560
  for story in self.tags.analytic_story:
521
- story.detections.append(self)
561
+ story.detections.append(self)
522
562
 
523
563
  self.cve_enrichment_func(__context)
524
564
 
525
565
  # Derive TestGroups and IntegrationTests, adjust for ManualTests, skip as needed
526
566
  self.adjust_tests_and_groups()
527
567
 
568
+ # Ensure that if there is at least 1 drilldown, at least
569
+ # 1 of the drilldowns contains the string Drilldown.SEARCH_PLACEHOLDER.
570
+ # This is presently a requirement when 1 or more drilldowns are added to a detection.
571
+ # Note that this is only required for production searches that are not hunting
572
+
573
+ if self.type == AnalyticsType.Hunting.value or self.status != DetectionStatus.production.value:
574
+ #No additional check need to happen on the potential drilldowns.
575
+ pass
576
+ else:
577
+ found_placeholder = False
578
+ if len(self.drilldown_searches) < 2:
579
+ raise ValueError(f"This detection is required to have 2 drilldown_searches, but only has [{len(self.drilldown_searches)}]")
580
+ for drilldown in self.drilldown_searches:
581
+ if DRILLDOWN_SEARCH_PLACEHOLDER in drilldown.search:
582
+ found_placeholder = True
583
+ if not found_placeholder:
584
+ raise ValueError("Detection has one or more drilldown_searches, but none of them "
585
+ f"contained '{DRILLDOWN_SEARCH_PLACEHOLDER}. This is a requirement "
586
+ "if drilldown_searches are defined.'")
587
+
588
+ # Update the search fields with the original search, if required
589
+ for drilldown in self.drilldown_searches:
590
+ drilldown.perform_search_substitutions(self)
591
+
592
+ #For experimental purposes, add the default drilldowns
593
+ #self.drilldown_searches.extend(Drilldown.constructDrilldownsFromDetection(self))
594
+
595
+ @property
596
+ def drilldowns_in_JSON(self) -> list[dict[str,str]]:
597
+ """This function is required for proper JSON
598
+ serializiation of drilldowns to occur in savedsearches.conf.
599
+ It returns the list[Drilldown] as a list[dict].
600
+ Without this function, the jinja template is unable
601
+ to convert list[Drilldown] to JSON
602
+
603
+ Returns:
604
+ list[dict[str,str]]: List of Drilldowns dumped to dict format
605
+ """
606
+ return [drilldown.model_dump() for drilldown in self.drilldown_searches]
607
+
528
608
  @field_validator('lookups', mode="before")
529
609
  @classmethod
530
610
  def getDetectionLookups(cls, v:list[str], info:ValidationInfo) -> list[Lookup]:
@@ -653,6 +733,27 @@ class Detection_Abstract(SecurityContentObject):
653
733
  else:
654
734
  self.tags.nist = [NistCategory.DE_AE]
655
735
  return self
736
+
737
+
738
+ @model_validator(mode="after")
739
+ def ensureThrottlingFieldsExist(self):
740
+ '''
741
+ For throttling to work properly, the fields to throttle on MUST
742
+ exist in the search itself. If not, then we cannot apply the throttling
743
+ '''
744
+ if self.tags.throttling is None:
745
+ # No throttling configured for this detection
746
+ return self
747
+
748
+ missing_fields:list[str] = [field for field in self.tags.throttling.fields if field not in self.search]
749
+ if len(missing_fields) > 0:
750
+ raise ValueError(f"The following throttle fields were missing from the search: {missing_fields}")
751
+
752
+ else:
753
+ # All throttling fields present in search
754
+ return self
755
+
756
+
656
757
 
657
758
  @model_validator(mode="after")
658
759
  def ensureProperObservablesExist(self):
@@ -730,6 +831,45 @@ class Detection_Abstract(SecurityContentObject):
730
831
  # Found everything
731
832
  return self
732
833
 
834
+ @field_validator("tests", mode="before")
835
+ def ensure_yml_test_is_unittest(cls, v:list[dict]):
836
+ """The typing for the tests field allows it to be one of
837
+ a number of different types of tests. However, ONLY
838
+ UnitTest should be allowed to be defined in the YML
839
+ file. If part of the UnitTest defined in the YML
840
+ is incorrect, such as the attack_data file, then
841
+ it will FAIL to be instantiated as a UnitTest and
842
+ may instead be instantiated as a different type of
843
+ test, such as IntegrationTest (since that requires
844
+ less fields) which is incorrect. Ensure that any
845
+ raw data read from the YML can actually construct
846
+ a valid UnitTest and, if not, return errors right
847
+ away instead of letting Pydantic try to construct
848
+ it into a different type of test
849
+
850
+ Args:
851
+ v (list[dict]): list of dicts read from the yml.
852
+ Each one SHOULD be a valid UnitTest. If we cannot
853
+ construct a valid unitTest from it, a ValueError should be raised
854
+
855
+ Returns:
856
+ _type_: The input of the function, assuming no
857
+ ValueError is raised.
858
+ """
859
+ valueErrors:list[ValueError] = []
860
+ for unitTest in v:
861
+ #This raises a ValueError on a failed UnitTest.
862
+ try:
863
+ UnitTest.model_validate(unitTest)
864
+ except ValueError as e:
865
+ valueErrors.append(e)
866
+ if len(valueErrors):
867
+ raise ValueError(valueErrors)
868
+ # All of these can be constructred as UnitTests with no
869
+ # Exceptions, so let the normal flow continue
870
+ return v
871
+
872
+
733
873
  @field_validator("tests")
734
874
  def tests_validate(
735
875
  cls,
@@ -5,8 +5,10 @@ if TYPE_CHECKING:
5
5
  from contentctl.objects.deployment import Deployment
6
6
  from contentctl.objects.security_content_object import SecurityContentObject
7
7
  from contentctl.input.director import DirectorOutputDto
8
+ from contentctl.objects.config import CustomApp
8
9
 
9
10
  from contentctl.objects.enums import AnalyticsType
11
+ from contentctl.objects.constants import CONTENTCTL_MAX_STANZA_LENGTH
10
12
  import abc
11
13
  import uuid
12
14
  import datetime
@@ -31,14 +33,14 @@ NO_FILE_NAME = "NO_FILE_NAME"
31
33
 
32
34
  # TODO (#266): disable the use_enum_values configuration
33
35
  class SecurityContentObject_Abstract(BaseModel, abc.ABC):
34
- model_config = ConfigDict(use_enum_values=True, validate_default=True)
35
-
36
- name: str = Field(...)
37
- author: str = Field("Content Author", max_length=255)
38
- date: datetime.date = Field(datetime.date.today())
39
- version: NonNegativeInt = 1
40
- id: uuid.UUID = Field(default_factory=uuid.uuid4) # we set a default here until all content has a uuid
41
- description: str = Field("Enter Description Here", max_length=10000)
36
+ model_config = ConfigDict(use_enum_values=True,validate_default=True)
37
+
38
+ name: str = Field(...,max_length=99)
39
+ author: str = Field(...,max_length=255)
40
+ date: datetime.date = Field(...)
41
+ version: NonNegativeInt = Field(...)
42
+ id: uuid.UUID = Field(...) #we set a default here until all content has a uuid
43
+ description: str = Field(...,max_length=10000)
42
44
  file_path: Optional[FilePath] = None
43
45
  references: Optional[List[HttpUrl]] = None
44
46
 
@@ -56,7 +58,13 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
56
58
  "description": self.description,
57
59
  "references": [str(url) for url in self.references or []]
58
60
  }
59
-
61
+
62
+
63
+ def check_conf_stanza_max_length(self, stanza_name:str, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH) -> None:
64
+ if len(stanza_name) > max_stanza_length:
65
+ raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
66
+ f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
67
+
60
68
  @staticmethod
61
69
  def objectListToNameList(objects: list[SecurityContentObject]) -> list[str]:
62
70
  return [object.getName() for object in objects]
@@ -1,8 +1,8 @@
1
1
  from typing import Union, Any
2
2
  from enum import Enum
3
3
 
4
- from pydantic import BaseModel
5
- from splunklib.data import Record
4
+ from pydantic import ConfigDict, BaseModel
5
+ from splunklib.data import Record # type: ignore
6
6
 
7
7
  from contentctl.helper.utils import Utils
8
8
 
@@ -53,11 +53,11 @@ class BaseTestResult(BaseModel):
53
53
  # The Splunk endpoint URL
54
54
  sid_link: Union[None, str] = None
55
55
 
56
- class Config:
57
- validate_assignment = True
58
-
59
- # Needed to allow for embedding of Exceptions in the model
60
- arbitrary_types_allowed = True
56
+ # Needed to allow for embedding of Exceptions in the model
57
+ model_config = ConfigDict(
58
+ validate_assignment=True,
59
+ arbitrary_types_allowed=True
60
+ )
61
61
 
62
62
  @property
63
63
  def passed(self) -> bool:
@@ -1,33 +1,21 @@
1
1
 
2
2
  from __future__ import annotations
3
- from typing import TYPE_CHECKING, Annotated, Optional, List,Any
3
+ from typing import Annotated, Optional, List,Any
4
4
  from pydantic import field_validator, ValidationInfo, Field, model_serializer
5
- if TYPE_CHECKING:
6
- from contentctl.input.director import DirectorOutputDto
7
-
8
5
  from contentctl.objects.deployment import Deployment
9
6
  from contentctl.objects.security_content_object import SecurityContentObject
10
- from contentctl.objects.enums import DataModel, AnalyticsType
7
+ from contentctl.objects.enums import DataModel
11
8
  from contentctl.objects.baseline_tags import BaselineTags
12
- from contentctl.objects.enums import DeploymentType
13
- #from contentctl.objects.deployment import Deployment
14
9
 
15
- # from typing import TYPE_CHECKING
16
- # if TYPE_CHECKING:
17
- # from contentctl.input.director import DirectorOutputDto
10
+ from contentctl.objects.config import CustomApp
11
+
18
12
 
13
+ from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH,CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE
19
14
 
20
15
  class Baseline(SecurityContentObject):
21
- # baseline spec
22
- #name: str
23
- #id: str
24
- #version: int
25
- #date: str
26
- #author: str
27
- #contentType: SecurityContentType = SecurityContentType.baselines
16
+ name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
28
17
  type: Annotated[str,Field(pattern="^Baseline$")] = Field(...)
29
18
  datamodel: Optional[List[DataModel]] = None
30
- #description: str
31
19
  search: str = Field(..., min_length=4)
32
20
  how_to_implement: str = Field(..., min_length=4)
33
21
  known_false_positives: str = Field(..., min_length=4)
@@ -35,6 +23,12 @@ class Baseline(SecurityContentObject):
35
23
 
36
24
  # enrichment
37
25
  deployment: Deployment = Field({})
26
+
27
+
28
+ def get_conf_stanza_name(self, app:CustomApp)->str:
29
+ stanza_name = CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
30
+ self.check_conf_stanza_max_length(stanza_name)
31
+ return stanza_name
38
32
 
39
33
  @field_validator("deployment", mode="before")
40
34
  def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:
@@ -1,15 +1,12 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
3
2
  from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_serializer
4
3
  from typing import List, Any, Union
5
4
 
6
5
  from contentctl.objects.story import Story
7
- from contentctl.objects.deployment import Deployment
8
6
  from contentctl.objects.detection import Detection
9
7
  from contentctl.objects.enums import SecurityContentProductName
10
8
  from contentctl.objects.enums import SecurityDomain
11
- if TYPE_CHECKING:
12
- from contentctl.input.director import DirectorOutputDto
9
+
13
10
 
14
11
 
15
12
 
@@ -19,7 +16,7 @@ class BaselineTags(BaseModel):
19
16
  #deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
20
17
  # TODO (#223): can we remove str from the possible types here?
21
18
  detections: List[Union[Detection,str]] = Field(...)
22
- product: list[SecurityContentProductName] = Field(...,min_length=1)
19
+ product: List[SecurityContentProductName] = Field(...,min_length=1)
23
20
  required_fields: List[str] = Field(...,min_length=1)
24
21
  security_domain: SecurityDomain = Field(...)
25
22
 
@@ -158,8 +158,7 @@ class CustomApp(App_Base):
158
158
  str(destination),
159
159
  verbose_print=True)
160
160
  return str(destination)
161
-
162
-
161
+
163
162
  # TODO (#266): disable the use_enum_values configuration
164
163
  class Config_Base(BaseModel):
165
164
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
@@ -287,7 +286,6 @@ class build(validate):
287
286
 
288
287
  def getAppTemplatePath(self)->pathlib.Path:
289
288
  return self.path/"app_template"
290
-
291
289
 
292
290
 
293
291
  class StackType(StrEnum):
@@ -310,6 +308,16 @@ class inspect(build):
310
308
  "should be enabled."
311
309
  )
312
310
  )
311
+ suppress_missing_content_exceptions: bool = Field(
312
+ default=False,
313
+ description=(
314
+ "Suppress exceptions during metadata validation if a detection that existed in "
315
+ "the previous build does not exist in this build. This is to ensure that content "
316
+ "is not accidentally removed. In order to support testing both public and private "
317
+ "content, this warning can be suppressed. If it is suppressed, it will still be "
318
+ "printed out as a warning."
319
+ )
320
+ )
313
321
  enrichments: bool = Field(
314
322
  default=True,
315
323
  description=(
@@ -951,15 +959,15 @@ class test_servers(test_common):
951
959
  index+=1
952
960
 
953
961
 
954
-
955
962
  class release_notes(Config_Base):
956
963
  old_tag:Optional[str] = Field(None, description="Name of the tag to diff against to find new content. "
957
964
  "If it is not supplied, then it will be inferred as the "
958
965
  "second newest tag at runtime.")
959
966
  new_tag:Optional[str] = Field(None, description="Name of the tag containing new content. If it is not supplied,"
960
967
  " then it will be inferred as the newest tag at runtime.")
961
- latest_branch:Optional[str] = Field(None, description="Branch for which we are generating release notes")
962
-
968
+ latest_branch:Optional[str] = Field(None, description="Branch name for which we are generating release notes for")
969
+ compare_against:Optional[str] = Field(default="develop", description="Branch name for which we are comparing the files changes against")
970
+
963
971
  def releaseNotesFilename(self, filename:str)->pathlib.Path:
964
972
  #Assume that notes are written to dist/. This does not respect build_dir since that is
965
973
  #only a member of build
@@ -1033,6 +1041,4 @@ class release_notes(Config_Base):
1033
1041
  # raise ValueError("The latest_branch '{self.latest_branch}' was not found in the repository")
1034
1042
 
1035
1043
 
1036
- # return self
1037
-
1038
-
1044
+ # return self
@@ -1,3 +1,5 @@
1
+ # Use for calculation of maximum length of name field
2
+ from contentctl.objects.enums import SecurityDomain
1
3
 
2
4
  ATTACK_TACTICS_KILLCHAIN_MAPPING = {
3
5
  "Reconnaissance": "Reconnaissance",
@@ -140,3 +142,31 @@ RBA_OBSERVABLE_ROLE_MAPPING = {
140
142
 
141
143
  # The relative path to the directory where any apps/packages will be downloaded
142
144
  DOWNLOADS_DIRECTORY = "downloads"
145
+
146
+ # Maximum length of the name field for a search.
147
+ # This number is derived from a limitation that exists in
148
+ # ESCU where a search cannot be edited, due to validation
149
+ # errors, if its name is longer than 99 characters.
150
+ # When an saved search is cloned in Enterprise Security User Interface,
151
+ # it is wrapped in the following:
152
+ # {Detection.tags.security_domain.value} - {SEARCH_STANZA_NAME} - Rule
153
+ # Similarly, when we generate the search stanza name in contentctl, it
154
+ # is app.label - detection.name - Rule
155
+ # However, in product the search name is:
156
+ # {CustomApp.label} - {detection.name} - Rule,
157
+ # or in ESCU:
158
+ # ESCU - {detection.name} - Rule,
159
+ # this gives us a maximum length below.
160
+ # When an ESCU search is cloned, it will
161
+ # have a full name like (the following is NOT a typo):
162
+ # Endpoint - ESCU - Name of Search From YML File - Rule - Rule
163
+ # The math below accounts for all these caveats
164
+ ES_MAX_STANZA_LENGTH = 99
165
+ CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Rule"
166
+ CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name}"
167
+ CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Response Task"
168
+
169
+ ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE = "{security_domain_value} - {search_name} - Rule"
170
+ SECURITY_DOMAIN_MAX_LENGTH = max([len(SecurityDomain[value]) for value in SecurityDomain._member_map_])
171
+ CONTENTCTL_MAX_STANZA_LENGTH = ES_MAX_STANZA_LENGTH - len(ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(security_domain_value="X"*SECURITY_DOMAIN_MAX_LENGTH,search_name=""))
172
+ CONTENTCTL_MAX_SEARCH_NAME_LENGTH = CONTENTCTL_MAX_STANZA_LENGTH - len(CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label="ESCU", detection_name=""))