contentctl 3.6.0__py3-none-any.whl → 4.0.2__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 +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import uuid
|
|
3
|
-
import questionary
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
from contentctl.objects.enums import SecurityContentType
|
|
8
|
-
from contentctl.input.new_content_questions import NewContentQuestions
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass(frozen=True)
|
|
12
|
-
class NewContentGeneratorInputDto:
|
|
13
|
-
type: SecurityContentType
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True)
|
|
17
|
-
class NewContentGeneratorOutputDto:
|
|
18
|
-
obj: dict
|
|
19
|
-
answers: dict
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class NewContentGenerator():
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def __init__(self, output_dto: NewContentGeneratorOutputDto) -> None:
|
|
26
|
-
self.output_dto = output_dto
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def execute(self, input_dto: NewContentGeneratorInputDto) -> None:
|
|
30
|
-
if input_dto.type == SecurityContentType.detections:
|
|
31
|
-
questions = NewContentQuestions.get_questions_detection()
|
|
32
|
-
answers = questionary.prompt(questions)
|
|
33
|
-
self.output_dto.answers.update(answers)
|
|
34
|
-
self.output_dto.obj['name'] = answers['detection_name']
|
|
35
|
-
self.output_dto.obj['id'] = str(uuid.uuid4())
|
|
36
|
-
self.output_dto.obj['version'] = 1
|
|
37
|
-
self.output_dto.obj['date'] = datetime.today().strftime('%Y-%m-%d')
|
|
38
|
-
self.output_dto.obj['author'] = answers['detection_author']
|
|
39
|
-
self.output_dto.obj['data_source'] = answers['data_source']
|
|
40
|
-
self.output_dto.obj['type'] = answers['detection_type']
|
|
41
|
-
self.output_dto.obj['status'] = "production" #start everything as production since that's what we INTEND the content to become
|
|
42
|
-
self.output_dto.obj['description'] = 'UPDATE_DESCRIPTION'
|
|
43
|
-
file_name = self.output_dto.obj['name'].replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
|
|
44
|
-
self.output_dto.obj['search'] = answers['detection_search'] + ' | `' + file_name + '_filter`'
|
|
45
|
-
self.output_dto.obj['how_to_implement'] = 'UPDATE_HOW_TO_IMPLEMENT'
|
|
46
|
-
self.output_dto.obj['known_false_positives'] = 'UPDATE_KNOWN_FALSE_POSITIVES'
|
|
47
|
-
self.output_dto.obj['references'] = ['REFERENCE']
|
|
48
|
-
self.output_dto.obj['tags'] = dict()
|
|
49
|
-
self.output_dto.obj['tags']['analytic_story'] = ['UPDATE_STORY_NAME']
|
|
50
|
-
self.output_dto.obj['tags']['asset_type'] = 'UPDATE asset_type'
|
|
51
|
-
self.output_dto.obj['tags']['confidence'] = 'UPDATE value between 1-100'
|
|
52
|
-
self.output_dto.obj['tags']['impact'] = 'UPDATE value between 1-100'
|
|
53
|
-
self.output_dto.obj['tags']['message'] = 'UPDATE message'
|
|
54
|
-
self.output_dto.obj['tags']['mitre_attack_id'] = [x.strip() for x in answers['mitre_attack_ids'].split(',')]
|
|
55
|
-
self.output_dto.obj['tags']['observable'] = [{'name': 'UPDATE', 'type': 'UPDATE', 'role': ['UPDATE']}]
|
|
56
|
-
self.output_dto.obj['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
|
|
57
|
-
self.output_dto.obj['tags']['required_fields'] = ['UPDATE']
|
|
58
|
-
self.output_dto.obj['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
|
|
59
|
-
self.output_dto.obj['tags']['security_domain'] = answers['security_domain']
|
|
60
|
-
self.output_dto.obj['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
|
|
61
|
-
|
|
62
|
-
#generate the tests section
|
|
63
|
-
self.output_dto.obj['tests'] = [
|
|
64
|
-
{
|
|
65
|
-
'name': "True Positive Test",
|
|
66
|
-
'attack_data': [
|
|
67
|
-
{
|
|
68
|
-
'data': "Enter URL for Dataset Here. This may also be a relative or absolute path on your local system for testing.",
|
|
69
|
-
"sourcetype": "UPDATE SOURCETYPE",
|
|
70
|
-
"source": "UPDATE SOURCE"
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
elif input_dto.type == SecurityContentType.stories:
|
|
79
|
-
questions = NewContentQuestions.get_questions_story()
|
|
80
|
-
answers = questionary.prompt(questions)
|
|
81
|
-
self.output_dto.answers.update(answers)
|
|
82
|
-
self.output_dto.obj['name'] = answers['story_name']
|
|
83
|
-
self.output_dto.obj['id'] = str(uuid.uuid4())
|
|
84
|
-
self.output_dto.obj['version'] = 1
|
|
85
|
-
self.output_dto.obj['date'] = datetime.today().strftime('%Y-%m-%d')
|
|
86
|
-
self.output_dto.obj['author'] = answers['story_author']
|
|
87
|
-
self.output_dto.obj['description'] = 'UPDATE_DESCRIPTION'
|
|
88
|
-
self.output_dto.obj['narrative'] = 'UPDATE_NARRATIVE'
|
|
89
|
-
self.output_dto.obj['references'] = []
|
|
90
|
-
self.output_dto.obj['tags'] = dict()
|
|
91
|
-
self.output_dto.obj['tags']['analytic_story'] = self.output_dto.obj['name']
|
|
92
|
-
self.output_dto.obj['tags']['category'] = answers['category']
|
|
93
|
-
self.output_dto.obj['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
|
|
94
|
-
self.output_dto.obj['tags']['usecase'] = answers['usecase']
|
|
95
|
-
self.output_dto.obj['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
import pathlib
|
|
5
|
-
from pydantic import ValidationError
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from contentctl.objects.playbook import Playbook
|
|
9
|
-
from contentctl.input.yml_reader import YmlReader
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class PlaybookBuilder():
|
|
13
|
-
playbook: Playbook
|
|
14
|
-
input_path: pathlib.Path
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def __init__(self, input_path: pathlib.Path):
|
|
18
|
-
self.input_path = input_path
|
|
19
|
-
|
|
20
|
-
def setObject(self, path: pathlib.Path) -> None:
|
|
21
|
-
yml_dict = YmlReader.load_file(path)
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
self.playbook = Playbook.parse_obj(yml_dict)
|
|
25
|
-
|
|
26
|
-
except ValidationError as e:
|
|
27
|
-
print('Validation Error for file ' + str(path))
|
|
28
|
-
print(e)
|
|
29
|
-
sys.exit(1)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def addDetections(self) -> None:
|
|
33
|
-
if self.playbook.tags.detections:
|
|
34
|
-
self.playbook.tags.detection_objects = []
|
|
35
|
-
for detection in self.playbook.tags.detections:
|
|
36
|
-
detection_object = {
|
|
37
|
-
"name": detection,
|
|
38
|
-
"lowercase_name": self.convertNameToFileName(detection),
|
|
39
|
-
"path": self.findDetectionPath(detection)
|
|
40
|
-
}
|
|
41
|
-
self.playbook.tags.detection_objects.append(detection_object)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def reset(self) -> None:
|
|
45
|
-
self.playbook = None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def getObject(self) -> Playbook:
|
|
49
|
-
return self.playbook
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def convertNameToFileName(self, name: str):
|
|
53
|
-
file_name = name \
|
|
54
|
-
.replace(' ', '_') \
|
|
55
|
-
.replace('-','_') \
|
|
56
|
-
.replace('.','_') \
|
|
57
|
-
.replace('/','_') \
|
|
58
|
-
.lower()
|
|
59
|
-
return file_name
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def findDetectionPath(self, detection_name: str) -> str:
|
|
63
|
-
for path in Path(os.path.join(self.input_path, 'detections')).rglob(self.convertNameToFileName(detection_name) + '.yml'):
|
|
64
|
-
normalized_path = os.path.normpath(path)
|
|
65
|
-
path_components = normalized_path.split(os.sep)
|
|
66
|
-
value_index = path_components.index('detections')
|
|
67
|
-
return "/".join(path_components[value_index:])
|
|
68
|
-
raise Exception(f"Failed to find detection path for playbook with name '{detection_name}'")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import sys
|
|
3
|
-
import pathlib
|
|
4
|
-
from pydantic import ValidationError
|
|
5
|
-
|
|
6
|
-
from contentctl.objects.story import Story
|
|
7
|
-
from contentctl.objects.enums import SecurityContentType
|
|
8
|
-
from contentctl.objects.config import Config
|
|
9
|
-
from contentctl.input.yml_reader import YmlReader
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class StoryBuilder():
|
|
13
|
-
story: Story
|
|
14
|
-
|
|
15
|
-
def setObject(self, path: pathlib.Path) -> None:
|
|
16
|
-
yml_dict = YmlReader.load_file(path)
|
|
17
|
-
yml_dict["tags"]["name"] = yml_dict["name"]
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
self.story = Story.parse_obj(yml_dict)
|
|
21
|
-
except ValidationError as e:
|
|
22
|
-
print('Validation Error for file ' + str(path))
|
|
23
|
-
print(e)
|
|
24
|
-
sys.exit(1)
|
|
25
|
-
|
|
26
|
-
def reset(self) -> None:
|
|
27
|
-
self.story = None
|
|
28
|
-
|
|
29
|
-
def getObject(self) -> Story:
|
|
30
|
-
return self.story
|
|
31
|
-
|
|
32
|
-
def addDetections(self, detections: list, config: Config) -> None:
|
|
33
|
-
matched_detection_names = []
|
|
34
|
-
matched_detections = []
|
|
35
|
-
mitre_attack_enrichments = []
|
|
36
|
-
mitre_attack_tactics = set()
|
|
37
|
-
datamodels = set()
|
|
38
|
-
kill_chain_phases = set()
|
|
39
|
-
|
|
40
|
-
for detection in detections:
|
|
41
|
-
if detection:
|
|
42
|
-
for detection_analytic_story in detection.tags.analytic_story:
|
|
43
|
-
if detection_analytic_story == self.story.name:
|
|
44
|
-
matched_detection_names.append(str(f'{config.build.prefix} - ' + detection.name + ' - Rule'))
|
|
45
|
-
mitre_attack_enrichments_list = []
|
|
46
|
-
if (detection.tags.mitre_attack_enrichments):
|
|
47
|
-
for attack in detection.tags.mitre_attack_enrichments:
|
|
48
|
-
mitre_attack_enrichments_list.append({"mitre_attack_technique": attack.mitre_attack_technique})
|
|
49
|
-
tags_obj = {"mitre_attack_enrichments": mitre_attack_enrichments_list}
|
|
50
|
-
matched_detections.append({
|
|
51
|
-
"name": detection.name,
|
|
52
|
-
"source": detection.source,
|
|
53
|
-
"type": detection.type,
|
|
54
|
-
"tags": tags_obj
|
|
55
|
-
})
|
|
56
|
-
datamodels.update(detection.datamodel)
|
|
57
|
-
if detection.tags.kill_chain_phases:
|
|
58
|
-
kill_chain_phases.update(detection.tags.kill_chain_phases)
|
|
59
|
-
|
|
60
|
-
if detection.tags.mitre_attack_enrichments:
|
|
61
|
-
for attack_enrichment in detection.tags.mitre_attack_enrichments:
|
|
62
|
-
mitre_attack_tactics.update(attack_enrichment.mitre_attack_tactics)
|
|
63
|
-
if attack_enrichment.mitre_attack_id not in [attack.mitre_attack_id for attack in mitre_attack_enrichments]:
|
|
64
|
-
mitre_attack_enrichments.append(attack_enrichment)
|
|
65
|
-
|
|
66
|
-
self.story.detection_names = matched_detection_names
|
|
67
|
-
self.story.detections = matched_detections
|
|
68
|
-
self.story.tags.datamodels = sorted(list(datamodels))
|
|
69
|
-
self.story.tags.kill_chain_phases = sorted(list(kill_chain_phases))
|
|
70
|
-
self.story.tags.mitre_attack_enrichments = mitre_attack_enrichments
|
|
71
|
-
self.story.tags.mitre_attack_tactics = sorted(list(mitre_attack_tactics))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def addBaselines(self, baselines: list) -> None:
|
|
75
|
-
matched_baseline_names = []
|
|
76
|
-
for baseline in baselines:
|
|
77
|
-
for baseline_analytic_story in baseline.tags.analytic_story:
|
|
78
|
-
if baseline_analytic_story == self.story.name:
|
|
79
|
-
matched_baseline_names.append(str(f'ESCU - ' + baseline.name))
|
|
80
|
-
|
|
81
|
-
self.story.baseline_names = matched_baseline_names
|
|
82
|
-
|
|
83
|
-
def addInvestigations(self, investigations: list) -> None:
|
|
84
|
-
matched_investigation_names = []
|
|
85
|
-
matched_investigations = []
|
|
86
|
-
for investigation in investigations:
|
|
87
|
-
for investigation_analytic_story in investigation.tags.analytic_story:
|
|
88
|
-
if investigation_analytic_story == self.story.name:
|
|
89
|
-
matched_investigation_names.append(str(f'ESCU - ' + investigation.name + ' - Response Task'))
|
|
90
|
-
matched_investigations.append(investigation)
|
|
91
|
-
|
|
92
|
-
self.story.investigation_names = matched_investigation_names
|
|
93
|
-
self.story.investigations = matched_investigations
|
|
94
|
-
|
|
95
|
-
def addAuthorCompanyName(self) -> None:
|
|
96
|
-
match_author = re.search(r'^([^,]+)', self.story.author)
|
|
97
|
-
if match_author is None:
|
|
98
|
-
self.story.author_name = 'no'
|
|
99
|
-
else:
|
|
100
|
-
self.story.author_name = match_author.group(1)
|
|
101
|
-
|
|
102
|
-
match_company = re.search(r',\s?(.*)$', self.story.author)
|
|
103
|
-
if match_company is None:
|
|
104
|
-
self.story.author_company = 'no'
|
|
105
|
-
else:
|
|
106
|
-
self.story.author_company = match_company.group(1)
|
contentctl/objects/app.py
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Needed for a staticmethod to be able to return an instance of the class it belongs to
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import pathlib
|
|
6
|
-
import re
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel, validator, ValidationError, Extra, Field
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from typing import Union
|
|
13
|
-
import validators
|
|
14
|
-
from contentctl.objects.security_content_object import SecurityContentObject
|
|
15
|
-
from contentctl.objects.enums import DataModel
|
|
16
|
-
from contentctl.helper.utils import Utils
|
|
17
|
-
import yaml
|
|
18
|
-
|
|
19
|
-
SPLUNKBASE_URL = "https://splunkbase.splunk.com/app/{uid}/release/{release}/download"
|
|
20
|
-
ENVIRONMENT_PATH_NOT_SET = "ENVIRONMENT_PATH_NOT_SET"
|
|
21
|
-
|
|
22
|
-
class App(BaseModel, extra=Extra.forbid):
|
|
23
|
-
|
|
24
|
-
# uid is a numeric identifier assigned by splunkbase, so
|
|
25
|
-
# homemade applications will not have this
|
|
26
|
-
uid: Union[int, None]
|
|
27
|
-
|
|
28
|
-
# appid is basically the internal name of your app
|
|
29
|
-
appid: str
|
|
30
|
-
|
|
31
|
-
# Title is the human readable name for your application
|
|
32
|
-
title: str
|
|
33
|
-
|
|
34
|
-
# Self explanatory
|
|
35
|
-
description: Union[str, None]
|
|
36
|
-
release: str
|
|
37
|
-
|
|
38
|
-
local_path: Union[str, None]
|
|
39
|
-
http_path: Union[str, None]
|
|
40
|
-
# Splunkbase path is made of the combination of uid and release fields
|
|
41
|
-
splunkbase_path: Union[str, None]
|
|
42
|
-
|
|
43
|
-
# Ultimate source of the app. Can be a local path or a Splunkbase Path.
|
|
44
|
-
# This will be set via a function call and should not be provided in the YML
|
|
45
|
-
# Note that this is the path relative to the container mount
|
|
46
|
-
environment_path: str = ENVIRONMENT_PATH_NOT_SET
|
|
47
|
-
force_local:bool = False
|
|
48
|
-
|
|
49
|
-
def configure_app_source_for_container(
|
|
50
|
-
self,
|
|
51
|
-
splunkbase_username: Union[str, None],
|
|
52
|
-
splunkbase_password: Union[str, None],
|
|
53
|
-
apps_directory: pathlib.Path,
|
|
54
|
-
container_mount_path: pathlib.Path,
|
|
55
|
-
):
|
|
56
|
-
|
|
57
|
-
splunkbase_creds_provided = (
|
|
58
|
-
splunkbase_username is not None and splunkbase_password is not None
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if splunkbase_creds_provided and self.splunkbase_path is not None and not self.force_local:
|
|
62
|
-
self.environment_path = self.splunkbase_path
|
|
63
|
-
|
|
64
|
-
elif self.local_path is not None:
|
|
65
|
-
# local path existence already validated
|
|
66
|
-
filename = pathlib.Path(self.local_path)
|
|
67
|
-
destination = str(apps_directory / filename.name)
|
|
68
|
-
Utils.copy_local_file(self.local_path, destination, verbose_print=True)
|
|
69
|
-
self.environment_path = str(container_mount_path / filename.name)
|
|
70
|
-
|
|
71
|
-
elif self.http_path is not None:
|
|
72
|
-
from urllib.parse import urlparse
|
|
73
|
-
|
|
74
|
-
path_on_server = str(urlparse(self.http_path).path)
|
|
75
|
-
filename = pathlib.Path(path_on_server)
|
|
76
|
-
download_path = str(apps_directory / filename.name)
|
|
77
|
-
Utils.download_file_from_http(self.http_path, download_path)
|
|
78
|
-
self.environment_path = str(container_mount_path / filename.name)
|
|
79
|
-
|
|
80
|
-
else:
|
|
81
|
-
raise (
|
|
82
|
-
Exception(
|
|
83
|
-
f"Unable to download app {self.title}:\n"
|
|
84
|
-
f"Splunkbase Path : {self.splunkbase_path}\n"
|
|
85
|
-
f"local_path : {self.local_path}\n"
|
|
86
|
-
f"http_path : {self.http_path}\n"
|
|
87
|
-
f"Splunkbase Creds: {splunkbase_creds_provided}\n"
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
@staticmethod
|
|
92
|
-
def validate_string_alphanumeric_with_underscores(input: str) -> bool:
|
|
93
|
-
if len(input) == 0:
|
|
94
|
-
raise (ValueError(f"String was length 0"))
|
|
95
|
-
|
|
96
|
-
for letter in input:
|
|
97
|
-
if not (letter.isalnum() or letter in "_-"):
|
|
98
|
-
raise (
|
|
99
|
-
ValueError(
|
|
100
|
-
f"String '{input}' can only contain alphanumeric characters, underscores, and hyphens."
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
return True
|
|
104
|
-
|
|
105
|
-
@validator("uid")
|
|
106
|
-
def validate_uid(cls, v):
|
|
107
|
-
return v
|
|
108
|
-
|
|
109
|
-
@validator("appid")
|
|
110
|
-
def validate_appid(cls, v):
|
|
111
|
-
# Called function raises exception on failure, so we don't need to raise it here
|
|
112
|
-
cls.validate_string_alphanumeric_with_underscores(v)
|
|
113
|
-
return v
|
|
114
|
-
|
|
115
|
-
@validator("title")
|
|
116
|
-
def validate_title(cls, v):
|
|
117
|
-
# Basically, a title can be any string
|
|
118
|
-
return v
|
|
119
|
-
|
|
120
|
-
@validator("description")
|
|
121
|
-
def validate_description(cls, v):
|
|
122
|
-
# description can be anything
|
|
123
|
-
return v
|
|
124
|
-
|
|
125
|
-
@validator("release")
|
|
126
|
-
def validate_release(cls, v):
|
|
127
|
-
# release can be any string
|
|
128
|
-
return v
|
|
129
|
-
|
|
130
|
-
@validator("local_path")
|
|
131
|
-
def validate_local_path(cls, v):
|
|
132
|
-
if v is not None:
|
|
133
|
-
p = pathlib.Path(v)
|
|
134
|
-
if not p.exists():
|
|
135
|
-
raise (ValueError(f"The path local_path {p} does not exist"))
|
|
136
|
-
elif not p.is_file():
|
|
137
|
-
raise (ValueError(f"The path local_path {p} exists, but is not a file"))
|
|
138
|
-
|
|
139
|
-
# release can be any string
|
|
140
|
-
return v
|
|
141
|
-
|
|
142
|
-
@validator("http_path")
|
|
143
|
-
def validate_http_path(cls, v, values):
|
|
144
|
-
if v is not None:
|
|
145
|
-
try:
|
|
146
|
-
if bool(validators.url(v)) == False:
|
|
147
|
-
raise ValueError(f"URL '{v}' is not a valid URL")
|
|
148
|
-
except Exception as e:
|
|
149
|
-
raise (ValueError(f"Error validating the http_path: {str(e)}"))
|
|
150
|
-
return v
|
|
151
|
-
|
|
152
|
-
@validator("splunkbase_path")
|
|
153
|
-
def validate_splunkbase_path(cls, v, values):
|
|
154
|
-
|
|
155
|
-
if v is not None:
|
|
156
|
-
try:
|
|
157
|
-
if bool(validators.url(v)) == False:
|
|
158
|
-
raise ValueError(f"splunkbase_url {v} is not a valid URL")
|
|
159
|
-
except Exception as e:
|
|
160
|
-
raise (ValueError(f"Error validating the splunkbase_url: {str(e)}"))
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
bool(
|
|
164
|
-
re.match(
|
|
165
|
-
"^https://splunkbase\.splunk\.com/app/\d+/release/.+/download$",
|
|
166
|
-
v,
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
== False
|
|
170
|
-
):
|
|
171
|
-
raise (
|
|
172
|
-
ValueError(
|
|
173
|
-
f"splunkbase_url {v} does not match the format {SPLUNKBASE_URL}"
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# Try to form the URL and error out if Splunkbase is the only place to get the app
|
|
178
|
-
if values["uid"] is None:
|
|
179
|
-
if values["must_download_from_splunkbase"]:
|
|
180
|
-
raise (
|
|
181
|
-
ValueError(
|
|
182
|
-
f"Error building splunkbase_url. Attempting to"
|
|
183
|
-
f" build the url for '{values['title']}', but no "
|
|
184
|
-
f"uid was supplied."
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
else:
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
|
-
if values["release"] is None:
|
|
191
|
-
if values["must_download_from_splunkbase"]:
|
|
192
|
-
raise (
|
|
193
|
-
ValueError(
|
|
194
|
-
f"Error building splunkbase_url. Attempting to"
|
|
195
|
-
f" build the url for '{values['title']}', but no "
|
|
196
|
-
f"release was supplied."
|
|
197
|
-
)
|
|
198
|
-
)
|
|
199
|
-
else:
|
|
200
|
-
return None
|
|
201
|
-
return SPLUNKBASE_URL.format(uid=values["uid"], release=values["release"])
|
|
202
|
-
|
|
203
|
-
@staticmethod
|
|
204
|
-
def get_default_apps() -> list[App]:
|
|
205
|
-
all_app_objs: list[App] = []
|
|
206
|
-
with open(
|
|
207
|
-
os.path.join(os.path.dirname(__file__), "../", "templates/app_default.yml"),
|
|
208
|
-
"r",
|
|
209
|
-
) as app_data:
|
|
210
|
-
all_apps_raw = yaml.safe_load(app_data)
|
|
211
|
-
for a in all_apps_raw:
|
|
212
|
-
app_obj = App.parse_obj(a)
|
|
213
|
-
all_app_objs.append(app_obj)
|
|
214
|
-
return all_app_objs
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import pathlib
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from pydantic import BaseModel, root_validator, validator, ValidationError, Extra, Field
|
|
7
|
-
from pydantic.main import ModelMetaclass
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import Union
|
|
11
|
-
|
|
12
|
-
import validators
|
|
13
|
-
|
|
14
|
-
from contentctl.objects.enums import SecurityContentProduct
|
|
15
|
-
|
|
16
|
-
from contentctl.helper.utils import Utils
|
|
17
|
-
|
|
18
|
-
from semantic_version import Version
|
|
19
|
-
|
|
20
|
-
import git
|
|
21
|
-
ALWAYS_PULL = True
|
|
22
|
-
|
|
23
|
-
SPLUNKBASE_URL = "https://splunkbase.splunk.com/app/{uid}/release/{release}/download"
|
|
24
|
-
|
|
25
|
-
class Manifest(BaseModel):
|
|
26
|
-
#Note that many of these fields are mirrored from App
|
|
27
|
-
|
|
28
|
-
#Some information about the developer of the app
|
|
29
|
-
author_name: str = Field(default=None, title="Enter the name of the app author")
|
|
30
|
-
author_email: str = Field(default=None, title="Enter a contact email for the develop(s) of the app")
|
|
31
|
-
author_company: str = Field(default=None, title="Enter the company who is developing the app")
|
|
32
|
-
|
|
33
|
-
#uid is a numeric identifier assigned by splunkbase, so
|
|
34
|
-
#homemade applications will not have this
|
|
35
|
-
uid: Union[int, None] = Field(default=None, title="Unique numeric identifier assigned by Splunkbase to identify your app. You can find it in the URL of your app's landing page. If you do not have one, leave this blank.")
|
|
36
|
-
|
|
37
|
-
#appid is basically the internal name of you app
|
|
38
|
-
appid: str = Field(default=None, title="Internal name of your app. Note that it MUST be alphanumeric with underscores, but no spaces or other special characters")
|
|
39
|
-
|
|
40
|
-
#Title is the human readable name for your application
|
|
41
|
-
title: str = Field(default=None, title="Human-Readable name for your app. This can include any characters you want")
|
|
42
|
-
|
|
43
|
-
#Self explanatory
|
|
44
|
-
description: Union[str,None] = Field(default=None, title="Provide a helpful description of the app.")
|
|
45
|
-
release: str = Field(default=None, title="Provide a name for the current release of the app. This MUST follow semantic version format MAJOR.MINOR.PATCH[-tag]")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@validator('author_email', always=True)
|
|
50
|
-
def validate_author_email(cls, v):
|
|
51
|
-
print("email is")
|
|
52
|
-
print(v)
|
|
53
|
-
if bool(validators.email(v)) == False:
|
|
54
|
-
raise(ValueError(f"Email address {v} is invalid"))
|
|
55
|
-
return v
|
|
56
|
-
|
|
57
|
-
@validator('release', always=True)
|
|
58
|
-
def validate_release(cls, v):
|
|
59
|
-
try:
|
|
60
|
-
Version(v)
|
|
61
|
-
except Exception as e:
|
|
62
|
-
raise(ValueError(f"The string '{v}' is not a valid Semantic Version. For more information on Semantic Versioning, please refer to https://semver.org/"))
|
|
63
|
-
|
|
64
|
-
return v
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class RepoConfig(BaseModel):
|
|
68
|
-
|
|
69
|
-
#Needs a manifest to be able to properly generate the app
|
|
70
|
-
manifest:Manifest = Field(default=None, title="Manifest Object")
|
|
71
|
-
repo_path: str = Field(default='.', title="Path to the root of your app")
|
|
72
|
-
repo_url: Union[str,None] = Field(default=None, title="HTTP(s) path to the repo for repo_path. If this field is blank, it will be inferred from the repo")
|
|
73
|
-
main_branch: str = Field(title="Main branch of the repo.")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
type: SecurityContentProduct = Field(default=SecurityContentProduct.SPLUNK_ENTERPRISE_APP, title=f"What type of product would you like to build. Choose one of {SecurityContentProduct._member_names_}")
|
|
79
|
-
skip_enrichment: bool = Field(default=True, title="Whether or not to skip the enrichment processes when validating the app. Enrichment increases the amount of time it takes to build an app significantly because it must hit a number of Web APIs.")
|
|
80
|
-
|
|
81
|
-
input_path: str = Field(default='.', title="Path to the root of your app")
|
|
82
|
-
output_path: str = Field(default='./dist', title="Path where 'generate' will write out your raw app")
|
|
83
|
-
#output_path: str = Field(default='./build', title="Path where 'build' will write out your custom app")
|
|
84
|
-
|
|
85
|
-
#test_config: TestConfig = Field(default=TestConfig, title="Test Configuration")
|
|
86
|
-
|
|
87
|
-
#@validator('manifest', always=True, pre=True)
|
|
88
|
-
'''
|
|
89
|
-
@root_validator(pre=True)
|
|
90
|
-
def validate_manifest(cls, values):
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
print(Manifest.parse_obj(values))
|
|
94
|
-
except Exception as e:
|
|
95
|
-
raise(ValueError(f"error validating manifest: {str(e)}"))
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return values
|
|
99
|
-
print("TWO")
|
|
100
|
-
#return {}
|
|
101
|
-
#return Manifest.parse_obj({"email":"invalid_email@gmail.com"})
|
|
102
|
-
'''
|
|
103
|
-
@validator('repo_path', always=True)
|
|
104
|
-
def validate_repo_path(cls,v):
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
path = pathlib.Path(v)
|
|
108
|
-
except Exception as e:
|
|
109
|
-
raise(ValueError(f"Error, the provided path is is not a valid path: '{v}'"))
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
r = git.Repo(path)
|
|
113
|
-
except Exception as e:
|
|
114
|
-
raise(ValueError(f"Error, the provided path is not a valid git repo: '{path}'"))
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
|
|
118
|
-
if ALWAYS_PULL:
|
|
119
|
-
r.remotes.origin.pull()
|
|
120
|
-
except Exception as e:
|
|
121
|
-
raise ValueError(f"Error pulling git repository {v}: {str(e)}")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return v
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@validator('repo_url')
|
|
128
|
-
def validate_repo_url(cls, v, values):
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
#First try to get the value from the repo
|
|
132
|
-
try:
|
|
133
|
-
remote_url_from_repo = git.Repo(values['repo_path']).remotes.origin.url
|
|
134
|
-
except Exception as e:
|
|
135
|
-
raise(ValueError(f"Error reading remote_url from the repo located at {values['repo_path']}"))
|
|
136
|
-
|
|
137
|
-
if v is not None and remote_url_from_repo != v:
|
|
138
|
-
raise(ValueError(f"The url of the remote repo supplied in the config file {v} does not "\
|
|
139
|
-
f"match the value read from the repository at {values['repo_path']}, {remote_url_from_repo}"))
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if v is None:
|
|
143
|
-
v = remote_url_from_repo
|
|
144
|
-
|
|
145
|
-
#Ensure that the url is the proper format
|
|
146
|
-
try:
|
|
147
|
-
if bool(validators.url(v)) == False:
|
|
148
|
-
raise(Exception)
|
|
149
|
-
except:
|
|
150
|
-
raise(ValueError(f"Error validating the repo_url. The url is not valid: {v}"))
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return v
|
|
154
|
-
|
|
155
|
-
@validator('main_branch')
|
|
156
|
-
def valid_main_branch(cls, v, values):
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
try:
|
|
160
|
-
Utils.validate_git_branch_name(values['repo_path'],values['repo_url'], v)
|
|
161
|
-
except Exception as e:
|
|
162
|
-
raise ValueError(f"Error validating main_branch: {str(e)}")
|
|
163
|
-
return v
|