contentctl 4.3.4__py3-none-any.whl → 4.4.0__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 (63) 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/initialize.py +28 -12
  7. contentctl/actions/inspect.py +191 -91
  8. contentctl/actions/new_content.py +10 -2
  9. contentctl/actions/validate.py +3 -6
  10. contentctl/api.py +1 -1
  11. contentctl/contentctl.py +3 -0
  12. contentctl/enrichments/attack_enrichment.py +49 -81
  13. contentctl/enrichments/cve_enrichment.py +6 -7
  14. contentctl/helper/splunk_app.py +141 -10
  15. contentctl/input/director.py +19 -24
  16. contentctl/input/new_content_questions.py +9 -42
  17. contentctl/objects/abstract_security_content_objects/detection_abstract.py +155 -13
  18. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
  19. contentctl/objects/atomic.py +51 -77
  20. contentctl/objects/base_test_result.py +7 -7
  21. contentctl/objects/baseline.py +12 -18
  22. contentctl/objects/baseline_tags.py +2 -5
  23. contentctl/objects/config.py +154 -26
  24. contentctl/objects/constants.py +34 -1
  25. contentctl/objects/correlation_search.py +79 -114
  26. contentctl/objects/dashboard.py +100 -0
  27. contentctl/objects/deployment.py +20 -5
  28. contentctl/objects/detection_metadata.py +71 -0
  29. contentctl/objects/detection_stanza.py +79 -0
  30. contentctl/objects/detection_tags.py +28 -26
  31. contentctl/objects/drilldown.py +70 -0
  32. contentctl/objects/enums.py +26 -24
  33. contentctl/objects/errors.py +187 -0
  34. contentctl/objects/investigation.py +23 -15
  35. contentctl/objects/investigation_tags.py +4 -3
  36. contentctl/objects/lookup.py +8 -1
  37. contentctl/objects/macro.py +16 -7
  38. contentctl/objects/notable_event.py +6 -5
  39. contentctl/objects/risk_analysis_action.py +4 -4
  40. contentctl/objects/risk_event.py +8 -7
  41. contentctl/objects/savedsearches_conf.py +196 -0
  42. contentctl/objects/story.py +4 -16
  43. contentctl/objects/throttling.py +46 -0
  44. contentctl/output/conf_output.py +4 -0
  45. contentctl/output/conf_writer.py +24 -4
  46. contentctl/output/new_content_yml_output.py +4 -9
  47. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  48. contentctl/output/templates/analyticstories_investigations.j2 +5 -5
  49. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  50. contentctl/output/templates/savedsearches_baselines.j2 +2 -3
  51. contentctl/output/templates/savedsearches_detections.j2 +12 -7
  52. contentctl/output/templates/savedsearches_investigations.j2 +3 -4
  53. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
  54. {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/METADATA +6 -5
  55. {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/RECORD +58 -57
  56. {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/WHEEL +1 -1
  57. contentctl/objects/ssa_detection.py +0 -157
  58. contentctl/objects/ssa_detection_tags.py +0 -138
  59. contentctl/objects/unit_test_old.py +0 -10
  60. contentctl/objects/unit_test_ssa.py +0 -31
  61. contentctl/output/templates/finding_report.j2 +0 -30
  62. {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/LICENSE.md +0 -0
  63. {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,157 +0,0 @@
1
- from __future__ import annotations
2
- import uuid
3
- import string
4
- import requests
5
- import time
6
- from pydantic import BaseModel, validator, root_validator
7
- from dataclasses import dataclass
8
- from datetime import datetime
9
- from typing import Union
10
- import re
11
-
12
- from contentctl.objects.abstract_security_content_objects.detection_abstract import Detection_Abstract
13
- from contentctl.objects.enums import AnalyticsType
14
- from contentctl.objects.enums import DataModel
15
- from contentctl.objects.enums import DetectionStatus
16
- from contentctl.objects.deployment import Deployment
17
- from contentctl.objects.ssa_detection_tags import SSADetectionTags
18
- from contentctl.objects.unit_test_ssa import UnitTestSSA
19
- from contentctl.objects.unit_test_old import UnitTestOld
20
- from contentctl.objects.macro import Macro
21
- from contentctl.objects.lookup import Lookup
22
- from contentctl.objects.baseline import Baseline
23
- from contentctl.objects.playbook import Playbook
24
- from contentctl.helper.link_validator import LinkValidator
25
- from contentctl.objects.enums import SecurityContentType
26
-
27
- class SSADetection(BaseModel):
28
- # detection spec
29
- name: str
30
- id: str
31
- version: int
32
- date: str
33
- author: str
34
- type: AnalyticsType = ...
35
- status: DetectionStatus = ...
36
- detection_type: str = None
37
- description: str
38
- data_source: list[str]
39
- search: Union[str, dict]
40
- how_to_implement: str
41
- known_false_positives: str
42
- references: list
43
- tags: SSADetectionTags
44
- tests: list[UnitTestSSA] = None
45
-
46
- # enrichments
47
- annotations: dict = None
48
- risk: list = None
49
- mappings: dict = None
50
- file_path: str = None
51
- source: str = None
52
- test: Union[UnitTestSSA, dict, UnitTestOld] = None
53
- runtime: str = None
54
- internalVersion: int = None
55
-
56
- # @validator('name')v
57
- # def name_max_length(cls, v, values):
58
- # if len(v) > 67:
59
- # raise ValueError('name is longer then 67 chars: ' + v)
60
- # return v
61
-
62
- # TODO (#266): disable the use_enum_values configuration
63
- class Config:
64
- use_enum_values = True
65
-
66
- '''
67
- @validator("name")
68
- def name_invalid_chars(cls, v):
69
- invalidChars = set(string.punctuation.replace("-", ""))
70
- if any(char in invalidChars for char in v):
71
- raise ValueError("invalid chars used in name: " + v)
72
- return v
73
-
74
- @validator("id")
75
- def id_check(cls, v, values):
76
- try:
77
- uuid.UUID(str(v))
78
- except:
79
- raise ValueError("uuid is not valid: " + values["name"])
80
- return v
81
-
82
- @validator("date")
83
- def date_valid(cls, v, values):
84
- try:
85
- datetime.strptime(v, "%Y-%m-%d")
86
- except:
87
- raise ValueError("date is not in format YYYY-MM-DD: " + values["name"])
88
- return v
89
-
90
- # @validator("type")
91
- # def type_valid(cls, v, values):
92
- # if v.lower() not in [el.name.lower() for el in AnalyticsType]:
93
- # raise ValueError("not valid analytics type: " + values["name"])
94
- # return v
95
-
96
- @validator("description", "how_to_implement")
97
- def encode_error(cls, v, values, field):
98
- try:
99
- v.encode("ascii")
100
- except UnicodeEncodeError:
101
- raise ValueError("encoding error in " + field.name + ": " + values["name"])
102
- return v
103
-
104
- # @root_validator
105
- # def search_validation(cls, values):
106
- # if 'ssa_' not in values['file_path']:
107
- # if not '_filter' in values['search']:
108
- # raise ValueError('filter macro missing in: ' + values["name"])
109
- # if any(x in values['search'] for x in ['eventtype=', 'sourcetype=', ' source=', 'index=']):
110
- # if not 'index=_internal' in values['search']:
111
- # raise ValueError('Use source macro instead of eventtype, sourcetype, source or index in detection: ' + values["name"])
112
- # return values
113
-
114
- @root_validator
115
- def name_max_length(cls, values):
116
- # Check max length only for ESCU searches, SSA does not have that constraint
117
- if "ssa_" not in values["file_path"]:
118
- if len(values["name"]) > 67:
119
- raise ValueError("name is longer then 67 chars: " + values["name"])
120
- return values
121
-
122
-
123
- @root_validator
124
- def new_line_check(cls, values):
125
- # Check if there is a new line in description and how to implement that is not escaped
126
- pattern = r'(?<!\\)\n'
127
- if re.search(pattern, values["description"]):
128
- match_obj = re.search(pattern,values["description"])
129
- words = values["description"][:match_obj.span()[0]].split()[-10:]
130
- newline_context = ' '.join(words)
131
- raise ValueError(f"Field named 'description' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
132
- if re.search(pattern, values["how_to_implement"]):
133
- match_obj = re.search(pattern,values["how_to_implement"])
134
- words = values["how_to_implement"][:match_obj.span()[0]].split()[-10:]
135
- newline_context = ' '.join(words)
136
- raise ValueError(f"Field named 'how_to_implement' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
137
- return values
138
-
139
- # @validator('references')
140
- # def references_check(cls, v, values):
141
- # return LinkValidator.SecurityContentObject_validate_references(v, values)
142
-
143
-
144
- @validator("search")
145
- def search_validate(cls, v, values):
146
- # write search validator
147
- return v
148
-
149
- @validator("tests")
150
- def tests_validate(cls, v, values):
151
- if (values.get("status","") in [DetectionStatus.production.value, DetectionStatus.validation.value]) and not v:
152
- raise ValueError(
153
- "At least one test is required for a production or validation detection: " + values["name"]
154
- )
155
- return v
156
-
157
- '''
@@ -1,138 +0,0 @@
1
- from __future__ import annotations
2
- import re
3
- from typing import List
4
- from pydantic import BaseModel, validator, ValidationError, model_validator, Field
5
-
6
- from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
7
- from contentctl.objects.constants import *
8
- from contentctl.objects.enums import SecurityContentProductName
9
-
10
- class SSADetectionTags(BaseModel):
11
- # detection spec
12
- #name: str
13
- analytic_story: list
14
- asset_type: str
15
- automated_detection_testing: str = None
16
- cis20: list = None
17
- confidence: int
18
- impact: int
19
- kill_chain_phases: list = None
20
- message: str
21
- mitre_attack_id: list = None
22
- nist: list = None
23
- observable: list
24
- product: List[SecurityContentProductName] = Field(...,min_length=1)
25
- required_fields: list
26
- risk_score: int
27
- security_domain: str
28
- risk_severity: str = None
29
- cve: list = None
30
- supported_tas: list = None
31
- atomic_guid: list = None
32
- drilldown_search: str = None
33
- manual_test: str = None
34
-
35
-
36
- # enrichment
37
- mitre_attack_enrichments: list[MitreAttackEnrichment] = []
38
- confidence_id: int = None
39
- impact_id: int = None
40
- context_ids: list = None
41
- risk_level_id: int = None
42
- risk_level: str = None
43
- observable_str: str = None
44
- evidence_str: str = None
45
- analytics_story_str: str = None
46
- kill_chain_phases_id:dict = None
47
- kill_chain_phases_str:str = None
48
- research_site_url: str = None
49
- event_schema: str = None
50
- mappings: list = None
51
- annotations: dict = None
52
-
53
-
54
- @validator('cis20')
55
- def tags_cis20(cls, v, values):
56
- pattern = r'^CIS ([\d|1\d|20)$' #DO NOT match leading zeroes and ensure no extra characters before or after the string
57
- for value in v:
58
- if not re.match(pattern, value):
59
- raise ValueError(f"CIS control '{value}' is not a valid Control ('CIS 1' -> 'CIS 20'): {values['name']}")
60
- return v
61
-
62
- @validator('nist')
63
- def tags_nist(cls, v, values):
64
- # Sourced Courtest of NIST: https://www.nist.gov/system/files/documents/cyberframework/cybersecurity-framework-021214.pdf (Page 19)
65
- IDENTIFY = [f'ID.{category}' for category in ["AM", "BE", "GV", "RA", "RM"] ]
66
- PROTECT = [f'PR.{category}' for category in ["AC", "AT", "DS", "IP", "MA", "PT"]]
67
- DETECT = [f'DE.{category}' for category in ["AE", "CM", "DP"] ]
68
- RESPOND = [f'RS.{category}' for category in ["RP", "CO", "AN", "MI", "IM"] ]
69
- RECOVER = [f'RC.{category}' for category in ["RP", "IM", "CO"] ]
70
- ALL_NIST_CATEGORIES = IDENTIFY + PROTECT + DETECT + RESPOND + RECOVER
71
-
72
-
73
- for value in v:
74
- if not value in ALL_NIST_CATEGORIES:
75
- raise ValueError(f"NIST Category '{value}' is not a valid category")
76
- return v
77
-
78
- @validator('confidence')
79
- def tags_confidence(cls, v, values):
80
- v = int(v)
81
- if not (v > 0 and v <= 100):
82
- raise ValueError('confidence score is out of range 1-100.' )
83
- else:
84
- return v
85
-
86
-
87
- @validator('impact')
88
- def tags_impact(cls, v, values):
89
- if not (v > 0 and v <= 100):
90
- raise ValueError('impact score is out of range 1-100.')
91
- else:
92
- return v
93
-
94
- @validator('kill_chain_phases')
95
- def tags_kill_chain_phases(cls, v, values):
96
- valid_kill_chain_phases = SES_KILL_CHAIN_MAPPINGS.keys()
97
- for value in v:
98
- if value not in valid_kill_chain_phases:
99
- raise ValueError('kill chain phase not valid. Valid options are ' + str(valid_kill_chain_phases))
100
- return v
101
-
102
- @validator('mitre_attack_id')
103
- def tags_mitre_attack_id(cls, v, values):
104
- pattern = 'T[0-9]{4}'
105
- for value in v:
106
- if not re.match(pattern, value):
107
- raise ValueError('Mitre Attack ID are not following the pattern Txxxx:' )
108
- return v
109
-
110
-
111
-
112
- @validator('risk_score')
113
- def tags_calculate_risk_score(cls, v, values):
114
- calculated_risk_score = round(values['impact'] * values['confidence'] / 100)
115
- if calculated_risk_score != int(v):
116
- raise ValueError(f"Risk Score must be calculated as round(confidence * impact / 100)"
117
- f"\n Expected risk_score={calculated_risk_score}, found risk_score={int(v)}: {values['name']}")
118
- return v
119
-
120
-
121
- @model_validator(mode="after")
122
- def tags_observable(self):
123
- valid_roles = SES_OBSERVABLE_ROLE_MAPPING.keys()
124
- valid_types = SES_OBSERVABLE_TYPE_MAPPING.keys()
125
-
126
- for value in self.observable:
127
- if value['type'] in valid_types:
128
- if 'Splunk Behavioral Analytics' in self.product:
129
- continue
130
-
131
- if 'role' not in value:
132
- raise ValueError('Observable role is missing')
133
- for role in value['role']:
134
- if role not in valid_roles:
135
- raise ValueError(f'Observable role ' + role + ' not valid. Valid options are {str(valid_roles)}')
136
- else:
137
- raise ValueError(f'Observable type ' + value['type'] + ' not valid. Valid options are {str(valid_types)}')
138
- return self
@@ -1,10 +0,0 @@
1
- from __future__ import annotations
2
- from pydantic import BaseModel
3
-
4
-
5
- from contentctl.objects.unit_test_ssa import UnitTestSSA
6
-
7
-
8
- class UnitTestOld(BaseModel):
9
- name: str
10
- tests: list[UnitTestSSA]
@@ -1,31 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Optional
3
- from pydantic import BaseModel, Field
4
- from pydantic import Field
5
-
6
-
7
- class UnitTestAttackDataSSA(BaseModel):
8
- file_name:Optional[str] = None
9
- data: str = Field(...)
10
- # TODO - should source and sourcetype should be mapped to a list
11
- # of supported source and sourcetypes in a given environment?
12
- source: str = Field(...)
13
-
14
- sourcetype: Optional[str] = None
15
-
16
-
17
- class UnitTestSSA(BaseModel):
18
- """
19
- A unit test for a detection
20
- """
21
- name: str
22
-
23
- # The attack data to be ingested for the unit test
24
- attack_data: list[UnitTestAttackDataSSA] = Field(...)
25
-
26
-
27
-
28
-
29
-
30
-
31
-
@@ -1,30 +0,0 @@
1
-
2
- | eval devices = [{"hostname": device_hostname, "type_id": 0, "uuid": device.uuid}],
3
- time = timestamp,
4
- evidence = {{ detection.tags.evidence_str }},
5
- message = "{{ detection.name }} has been triggered on " + device_hostname + " by " + {{ actor_user_name }} + ".",
6
- users = [{"name": {{ actor_user_name }}, "uuid": actor_user.uuid, "uid": actor_user.uid}],
7
- activity_id = 1,
8
- cis_csc = [{"control": "CIS 10", "version": 8}],
9
- analytic_stories = {{ detection.tags.analytics_story_str }},
10
- class_name = "Detection Report",
11
- confidence = {{ detection.tags.confidence }},
12
- confidence_id = {{ detection.tags.confidence_id }},
13
- duration = 0,
14
- impact = {{ detection.tags.impact }},
15
- impact_id = {{ detection.tags.impact_id }},
16
- kill_chain = {{ detection.tags.kill_chain_phases_str }},
17
- nist = ["DE.AE"],
18
- risk_level = "{{ detection.tags.risk_level }}",
19
- category_uid = 2,
20
- class_uid = 102001,
21
- risk_level_id = {{ detection.tags.risk_level_id }},
22
- risk_score = {{ detection.tags.risk_score }},
23
- severity_id = 0,
24
- rule = {"name": "{{ detection.name }}", "uid": "{{ detection.id }}", "type": "Streaming"},
25
- metadata = {"customer_uid": metadata.customer_uid, "product": {"name": "Behavior Analytics", "vendor_name": "Splunk"}, "version": "1.0.0-rc.2", "logged_time": time()},
26
- type_uid = 10200101,
27
- start_time = timestamp,
28
- end_time = timestamp
29
- | fields metadata, rule, activity_id, analytic_stories, cis_csc, category_uid, class_name, class_uid, confidence, confidence_id, devices, duration, time, evidence, impact, impact_id, kill_chain, message, nist, observables, risk_level, risk_level_id, risk_score, severity_id, type_uid, users, start_time, end_time
30
- | into sink;