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.
Files changed (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {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