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.
- contentctl/actions/build.py +1 -0
- contentctl/actions/detection_testing/GitService.py +10 -10
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +68 -38
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +5 -1
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +10 -8
- contentctl/actions/initialize.py +28 -12
- contentctl/actions/inspect.py +191 -91
- contentctl/actions/new_content.py +10 -2
- contentctl/actions/validate.py +3 -6
- contentctl/api.py +1 -1
- contentctl/contentctl.py +3 -0
- contentctl/enrichments/attack_enrichment.py +49 -81
- contentctl/enrichments/cve_enrichment.py +6 -7
- contentctl/helper/splunk_app.py +141 -10
- contentctl/input/director.py +19 -24
- contentctl/input/new_content_questions.py +9 -42
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +155 -13
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
- contentctl/objects/atomic.py +51 -77
- contentctl/objects/base_test_result.py +7 -7
- contentctl/objects/baseline.py +12 -18
- contentctl/objects/baseline_tags.py +2 -5
- contentctl/objects/config.py +154 -26
- contentctl/objects/constants.py +34 -1
- contentctl/objects/correlation_search.py +79 -114
- contentctl/objects/dashboard.py +100 -0
- contentctl/objects/deployment.py +20 -5
- contentctl/objects/detection_metadata.py +71 -0
- contentctl/objects/detection_stanza.py +79 -0
- contentctl/objects/detection_tags.py +28 -26
- contentctl/objects/drilldown.py +70 -0
- contentctl/objects/enums.py +26 -24
- contentctl/objects/errors.py +187 -0
- contentctl/objects/investigation.py +23 -15
- contentctl/objects/investigation_tags.py +4 -3
- contentctl/objects/lookup.py +8 -1
- contentctl/objects/macro.py +16 -7
- contentctl/objects/notable_event.py +6 -5
- contentctl/objects/risk_analysis_action.py +4 -4
- contentctl/objects/risk_event.py +8 -7
- contentctl/objects/savedsearches_conf.py +196 -0
- contentctl/objects/story.py +4 -16
- contentctl/objects/throttling.py +46 -0
- contentctl/output/conf_output.py +4 -0
- contentctl/output/conf_writer.py +24 -4
- contentctl/output/new_content_yml_output.py +4 -9
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_investigations.j2 +5 -5
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -3
- contentctl/output/templates/savedsearches_detections.j2 +12 -7
- contentctl/output/templates/savedsearches_investigations.j2 +3 -4
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
- {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/METADATA +6 -5
- {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/RECORD +58 -57
- {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/WHEEL +1 -1
- contentctl/objects/ssa_detection.py +0 -157
- contentctl/objects/ssa_detection_tags.py +0 -138
- contentctl/objects/unit_test_old.py +0 -10
- contentctl/objects/unit_test_ssa.py +0 -31
- contentctl/output/templates/finding_report.j2 +0 -30
- {contentctl-4.3.4.dist-info → contentctl-4.4.0.dist-info}/LICENSE.md +0 -0
- {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,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;
|
|
File without changes
|
|
File without changes
|