contentctl 5.0.0a2__py3-none-any.whl → 5.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +2 -4
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +83 -53
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +10 -10
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +255 -323
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +111 -46
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +47 -35
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +32 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +53 -31
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +22 -16
  74. contentctl/objects/rba.py +68 -11
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +54 -49
  81. contentctl/objects/story_tags.py +56 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  98. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  99. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  100. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  101. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  102. contentctl/output/templates/savedsearches_detections.j2 +2 -8
  103. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  104. contentctl/output/templates/transforms.j2 +2 -4
  105. contentctl/output/yml_writer.py +18 -24
  106. contentctl/templates/stories/cobalt_strike.yml +1 -0
  107. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/METADATA +1 -1
  108. contentctl-5.0.1.dist-info/RECORD +168 -0
  109. contentctl/actions/initialize_old.py +0 -245
  110. contentctl/objects/observable.py +0 -39
  111. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  112. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/LICENSE.md +0 -0
  113. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/WHEEL +0 -0
  114. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  from pydantic import BaseModel, model_validator
2
2
  from typing import Union, Callable, Any
3
3
  import requests
4
- import urllib3, urllib3.exceptions
4
+ import urllib3
5
+ import urllib3.exceptions
5
6
  import time
6
7
  import abc
7
8
 
@@ -10,88 +11,96 @@ import shelve
10
11
 
11
12
  DEFAULT_USER_AGENT_STRING = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36"
12
13
  ALLOWED_HTTP_CODES = [200]
13
- class LinkStats(BaseModel):
14
14
 
15
- #Static Values
15
+
16
+ class LinkStats(BaseModel):
17
+ # Static Values
16
18
  method: Callable = requests.get
17
- allowed_http_codes: list[int] = ALLOWED_HTTP_CODES
18
- access_count: int = 1 #when constructor is called, it has been accessed once!
19
+ allowed_http_codes: list[int] = ALLOWED_HTTP_CODES
20
+ access_count: int = 1 # when constructor is called, it has been accessed once!
19
21
  timeout_seconds: int = 15
20
22
  allow_redirects: bool = True
21
23
  headers: dict = {"User-Agent": DEFAULT_USER_AGENT_STRING}
22
24
  verify_ssl: bool = False
23
25
  if verify_ssl is False:
24
26
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
25
-
26
- #Values generated at runtime.
27
- #We need to assign these some default values to get the
28
- #validation working since ComputedField has not yet been
29
- #introduced to Pydantic
27
+
28
+ # Values generated at runtime.
29
+ # We need to assign these some default values to get the
30
+ # validation working since ComputedField has not yet been
31
+ # introduced to Pydantic
30
32
  reference: str
31
33
  referencing_files: set[str]
32
- redirect: Union[str,None] = None
34
+ redirect: Union[str, None] = None
33
35
  status_code: int = 0
34
36
  valid: bool = False
35
- resolution_time: float = 0
36
-
37
-
38
- def is_link_valid(self, referencing_file:str)->bool:
37
+ resolution_time: float = 0
38
+
39
+ def is_link_valid(self, referencing_file: str) -> bool:
39
40
  self.access_count += 1
40
41
  self.referencing_files.add(referencing_file)
41
42
  return self.valid
42
-
43
+
43
44
  @model_validator(mode="before")
44
- def check_reference(cls, data:Any)->Any:
45
+ def check_reference(cls, data: Any) -> Any:
45
46
  start_time = time.time()
46
- #Get out all the fields names to make them easier to reference
47
- method = data['method']
48
- reference = data['reference']
49
- timeout_seconds = data['timeout_seconds']
50
- headers = data['headers']
51
- allow_redirects = data['allow_redirects']
52
- verify_ssl = data['verify_ssl']
53
- allowed_http_codes = data['allowed_http_codes']
47
+ # Get out all the fields names to make them easier to reference
48
+ method = data["method"]
49
+ reference = data["reference"]
50
+ timeout_seconds = data["timeout_seconds"]
51
+ headers = data["headers"]
52
+ allow_redirects = data["allow_redirects"]
53
+ verify_ssl = data["verify_ssl"]
54
+ allowed_http_codes = data["allowed_http_codes"]
54
55
  if not (reference.startswith("http://") or reference.startswith("https://")):
55
- raise(ValueError(f"Reference {reference} does not begin with http(s). Only http(s) references are supported"))
56
-
56
+ raise (
57
+ ValueError(
58
+ f"Reference {reference} does not begin with http(s). Only http(s) references are supported"
59
+ )
60
+ )
61
+
57
62
  try:
58
- get = method(reference, timeout=timeout_seconds,
59
- headers = headers,
60
- allow_redirects=allow_redirects, verify=verify_ssl)
63
+ get = method(
64
+ reference,
65
+ timeout=timeout_seconds,
66
+ headers=headers,
67
+ allow_redirects=allow_redirects,
68
+ verify=verify_ssl,
69
+ )
61
70
  resolution_time = time.time() - start_time
62
- data['status_code'] = get.status_code
63
- data['resolution_time'] = resolution_time
71
+ data["status_code"] = get.status_code
72
+ data["resolution_time"] = resolution_time
64
73
  if reference != get.url:
65
- data['redirect'] = get.url
74
+ data["redirect"] = get.url
66
75
  else:
67
- data['redirect'] = None #None is also already the default
76
+ data["redirect"] = None # None is also already the default
68
77
 
69
- #Returns the updated values and sets them for the object
78
+ # Returns the updated values and sets them for the object
70
79
  if get.status_code in allowed_http_codes:
71
- data['valid'] = True
80
+ data["valid"] = True
72
81
  else:
73
- #print(f"Unacceptable HTTP Status Code {get.status_code} received for {reference}")
74
- data['valid'] = False
75
- return data
82
+ # print(f"Unacceptable HTTP Status Code {get.status_code} received for {reference}")
83
+ data["valid"] = False
84
+ return data
76
85
 
77
- except Exception as e:
86
+ except Exception:
78
87
  resolution_time = time.time() - start_time
79
- #print(f"Reference {reference} was not reachable after {resolution_time:.2f} seconds")
80
- data['status_code'] = 0
81
- data['valid'] = False
82
- data['redirect'] = None
83
- data['resolution_time'] = resolution_time
88
+ # print(f"Reference {reference} was not reachable after {resolution_time:.2f} seconds")
89
+ data["status_code"] = 0
90
+ data["valid"] = False
91
+ data["redirect"] = None
92
+ data["resolution_time"] = resolution_time
84
93
  return data
85
94
 
86
95
 
87
96
  class LinkValidator(abc.ABC):
88
- cache: Union[dict[str,LinkStats], shelve.Shelf] = {}
97
+ cache: Union[dict[str, LinkStats], shelve.Shelf] = {}
89
98
  uncached_checks: int = 0
90
99
  total_checks: int = 0
91
- #cache: dict[str,LinkStats] = {}
100
+ # cache: dict[str,LinkStats] = {}
92
101
 
93
102
  use_file_cache: bool = False
94
- reference_cache_file: str ="lookups/REFERENCE_CACHE.db"
103
+ reference_cache_file: str = "lookups/REFERENCE_CACHE.db"
95
104
 
96
105
  @staticmethod
97
106
  def initialize_cache(use_file_cache: bool = False):
@@ -99,74 +108,88 @@ class LinkValidator(abc.ABC):
99
108
  if use_file_cache is False:
100
109
  return
101
110
  if not os.path.exists(LinkValidator.reference_cache_file):
102
- print(f"Cache at {LinkValidator.reference_cache_file} not found - Creating it.")
103
-
111
+ print(
112
+ f"Cache at {LinkValidator.reference_cache_file} not found - Creating it."
113
+ )
114
+
104
115
  try:
105
- LinkValidator.cache = shelve.open(LinkValidator.reference_cache_file, flag='c', writeback=True)
106
- except:
107
- print(f"Failed to create the cache file {LinkValidator.reference_cache_file}. Reference info will not be cached.")
116
+ LinkValidator.cache = shelve.open(
117
+ LinkValidator.reference_cache_file, flag="c", writeback=True
118
+ )
119
+ except Exception:
120
+ print(
121
+ f"Failed to create the cache file {LinkValidator.reference_cache_file}. Reference info will not be cached."
122
+ )
108
123
  LinkValidator.cache = {}
109
124
 
110
- #Remove all of the failures to force those resources to be resolved again
125
+ # Remove all of the failures to force those resources to be resolved again
111
126
  failed_refs = []
112
127
  for ref in LinkValidator.cache.keys():
113
128
  if LinkValidator.cache[ref].status_code not in ALLOWED_HTTP_CODES:
114
129
  failed_refs.append(ref)
115
- #can't remove it here because this will throw an error:
116
- #cannot change size of dictionary while iterating over it
130
+ # can't remove it here because this will throw an error:
131
+ # cannot change size of dictionary while iterating over it
117
132
  else:
118
- #Set the reference count to 0 and referencing files to empty set
133
+ # Set the reference count to 0 and referencing files to empty set
119
134
  LinkValidator.cache[ref].access_count = 0
120
135
  LinkValidator.cache[ref].referencing_files = set()
121
-
122
- for ref in failed_refs:
123
- del(LinkValidator.cache[ref])
124
136
 
137
+ for ref in failed_refs:
138
+ del LinkValidator.cache[ref]
125
139
 
126
-
127
-
128
140
  @staticmethod
129
141
  def close_cache():
130
142
  if LinkValidator.use_file_cache:
131
143
  LinkValidator.cache.close()
132
144
 
133
145
  @staticmethod
134
- def validate_reference(reference: str, referencing_file:str, raise_exception_if_failure: bool = False) -> bool:
146
+ def validate_reference(
147
+ reference: str, referencing_file: str, raise_exception_if_failure: bool = False
148
+ ) -> bool:
135
149
  LinkValidator.total_checks += 1
136
150
  if reference not in LinkValidator.cache:
137
151
  LinkValidator.uncached_checks += 1
138
- LinkValidator.cache[reference] = LinkStats(reference=reference, referencing_files = set([referencing_file]))
152
+ LinkValidator.cache[reference] = LinkStats(
153
+ reference=reference, referencing_files=set([referencing_file])
154
+ )
139
155
  result = LinkValidator.cache[reference].is_link_valid(referencing_file)
140
156
 
141
- #print(f"Total Checks: {LinkValidator.total_checks}, Percent Cached: {100*(1 - LinkValidator.uncached_checks / LinkValidator.total_checks):.2f}")
157
+ # print(f"Total Checks: {LinkValidator.total_checks}, Percent Cached: {100*(1 - LinkValidator.uncached_checks / LinkValidator.total_checks):.2f}")
142
158
 
143
159
  if result is True:
144
160
  return True
145
161
  elif raise_exception_if_failure is True:
146
- raise(Exception(f"Reference Link Failed: {reference}"))
162
+ raise (Exception(f"Reference Link Failed: {reference}"))
147
163
  else:
148
164
  return False
165
+
149
166
  @staticmethod
150
167
  def print_link_validation_errors():
151
- failures = [LinkValidator.cache[k] for k in LinkValidator.cache if LinkValidator.cache[k].valid is False]
168
+ failures = [
169
+ LinkValidator.cache[k]
170
+ for k in LinkValidator.cache
171
+ if LinkValidator.cache[k].valid is False
172
+ ]
152
173
  failures.sort(key=lambda d: d.status_code)
153
174
  for failure in failures:
154
- print(f"Link {failure.reference} invalid with HTTP Status Code [{failure.status_code}] and referenced by the following files:")
175
+ print(
176
+ f"Link {failure.reference} invalid with HTTP Status Code [{failure.status_code}] and referenced by the following files:"
177
+ )
155
178
  for ref in failure.referencing_files:
156
179
  print(f"\t* {ref}")
157
180
 
158
181
  @staticmethod
159
- def SecurityContentObject_validate_references(v:list, values: dict)->list:
160
- if 'check_references' not in values:
161
- raise(Exception("Member 'check_references' missing from Baseline!"))
162
- elif values['check_references'] is False:
163
- #Reference checking is enabled
182
+ def SecurityContentObject_validate_references(v: list, values: dict) -> list:
183
+ if "check_references" not in values:
184
+ raise (Exception("Member 'check_references' missing from Baseline!"))
185
+ elif values["check_references"] is False:
186
+ # Reference checking is enabled
164
187
  pass
165
- elif values['check_references'] is True:
188
+ elif values["check_references"] is True:
166
189
  for reference in v:
167
- LinkValidator.validate_reference(reference, values['name'])
168
- #Remove the check_references key from the values dict so that it is not
169
- #output by the serialization code
170
- del values['check_references']
190
+ LinkValidator.validate_reference(reference, values["name"])
191
+ # Remove the check_references key from the values dict so that it is not
192
+ # output by the serialization code
193
+ del values["check_references"]
171
194
 
172
195
  return v
@@ -39,6 +39,7 @@ class RetryConstant:
39
39
 
40
40
  class SplunkBaseError(requests.HTTPError):
41
41
  """An error raise in communicating with Splunkbase"""
42
+
42
43
  pass
43
44
 
44
45
 
@@ -50,6 +51,7 @@ class SplunkApp:
50
51
 
51
52
  class InitializationError(Exception):
52
53
  """An initialization error during SplunkApp setup"""
54
+
53
55
  pass
54
56
 
55
57
  @staticmethod
@@ -68,16 +70,16 @@ class SplunkApp:
68
70
  status_forcelist=status_forcelist,
69
71
  )
70
72
  adapter = HTTPAdapter(max_retries=retry)
71
- session.mount('http://', adapter)
72
- session.mount('https://', adapter)
73
+ session.mount("http://", adapter)
74
+ session.mount("https://", adapter)
73
75
  return session
74
76
 
75
77
  def __init__(
76
- self,
77
- app_uid: Optional[int] = None,
78
- app_name_id: Optional[str] = None,
79
- manual_setup: bool = False,
80
- ) -> None:
78
+ self,
79
+ app_uid: Optional[int] = None,
80
+ app_name_id: Optional[str] = None,
81
+ manual_setup: bool = False,
82
+ ) -> None:
81
83
  if app_uid is None and app_name_id is None:
82
84
  raise SplunkApp.InitializationError(
83
85
  "Either app_uid (the numeric app UID e.g. 742) or app_name_id (the app name "
@@ -123,18 +125,22 @@ class SplunkApp:
123
125
  if self._app_info_cache is not None:
124
126
  return self._app_info_cache
125
127
  elif self.app_uid is None:
126
- raise SplunkApp.InitializationError("app_uid must be set in order to fetch app info")
128
+ raise SplunkApp.InitializationError(
129
+ "app_uid must be set in order to fetch app info"
130
+ )
127
131
 
128
132
  # NOTE: auth not required
129
133
  # Get app info by uid
130
134
  try:
131
135
  response = self.requests_retry_session().get(
132
136
  APIEndPoint.SPLUNK_BASE_APP_INFO.format(app_uid=self.app_uid),
133
- timeout=RetryConstant.RETRY_INTERVAL
137
+ timeout=RetryConstant.RETRY_INTERVAL,
134
138
  )
135
139
  response.raise_for_status()
136
140
  except requests.exceptions.RequestException as e:
137
- raise SplunkBaseError(f"Error fetching app info for app_uid {self.app_uid}: {str(e)}")
141
+ raise SplunkBaseError(
142
+ f"Error fetching app info for app_uid {self.app_uid}: {str(e)}"
143
+ )
138
144
 
139
145
  # parse JSON and set cache
140
146
  self._app_info_cache: dict = json.loads(response.content)
@@ -156,7 +162,9 @@ class SplunkApp:
156
162
  if "appid" in app_info:
157
163
  self.app_name_id = app_info["appid"]
158
164
  else:
159
- raise SplunkBaseError(f"Invalid response from Splunkbase; missing key 'appid': {app_info}")
165
+ raise SplunkBaseError(
166
+ f"Invalid response from Splunkbase; missing key 'appid': {app_info}"
167
+ )
160
168
 
161
169
  def set_app_uid(self) -> None:
162
170
  """
@@ -166,19 +174,25 @@ class SplunkApp:
166
174
  if self.app_uid is not None:
167
175
  return
168
176
  elif self.app_name_id is None:
169
- raise SplunkApp.InitializationError("app_name_id must be set in order to fetch app_uid")
177
+ raise SplunkApp.InitializationError(
178
+ "app_name_id must be set in order to fetch app_uid"
179
+ )
170
180
 
171
181
  # NOTE: auth not required
172
182
  # Get app_uid by app_name_id via a redirect
173
183
  try:
174
184
  response = self.requests_retry_session().get(
175
- APIEndPoint.SPLUNK_BASE_GET_UID_REDIRECT.format(app_name_id=self.app_name_id),
185
+ APIEndPoint.SPLUNK_BASE_GET_UID_REDIRECT.format(
186
+ app_name_id=self.app_name_id
187
+ ),
176
188
  allow_redirects=False,
177
- timeout=RetryConstant.RETRY_INTERVAL
189
+ timeout=RetryConstant.RETRY_INTERVAL,
178
190
  )
179
191
  response.raise_for_status()
180
192
  except requests.exceptions.RequestException as e:
181
- raise SplunkBaseError(f"Error fetching app_uid for app_name_id '{self.app_name_id}': {str(e)}")
193
+ raise SplunkBaseError(
194
+ f"Error fetching app_uid for app_name_id '{self.app_name_id}': {str(e)}"
195
+ )
182
196
 
183
197
  # Extract the app_uid from the redirect path
184
198
  if "Location" in response.headers:
@@ -199,7 +213,9 @@ class SplunkApp:
199
213
  if "title" in app_info:
200
214
  self.app_title = app_info["title"]
201
215
  else:
202
- raise SplunkBaseError(f"Invalid response from Splunkbase; missing key 'title': {app_info}")
216
+ raise SplunkBaseError(
217
+ f"Invalid response from Splunkbase; missing key 'title': {app_info}"
218
+ )
203
219
 
204
220
  def __fetch_url_latest_version_info(self) -> str:
205
221
  """
@@ -209,12 +225,16 @@ class SplunkApp:
209
225
  # retrieve app entries using the app_name_id
210
226
  try:
211
227
  response = self.requests_retry_session().get(
212
- APIEndPoint.SPLUNK_BASE_FETCH_APP_BY_ENTRY_ID.format(app_name_id=self.app_name_id),
213
- timeout=RetryConstant.RETRY_INTERVAL
228
+ APIEndPoint.SPLUNK_BASE_FETCH_APP_BY_ENTRY_ID.format(
229
+ app_name_id=self.app_name_id
230
+ ),
231
+ timeout=RetryConstant.RETRY_INTERVAL,
214
232
  )
215
233
  response.raise_for_status()
216
234
  except requests.exceptions.RequestException as e:
217
- raise SplunkBaseError(f"Error fetching app entries for app_name_id '{self.app_name_id}': {str(e)}")
235
+ raise SplunkBaseError(
236
+ f"Error fetching app entries for app_name_id '{self.app_name_id}': {str(e)}"
237
+ )
218
238
 
219
239
  # parse xml
220
240
  app_xml = xmltodict.parse(response.content)
@@ -231,7 +251,9 @@ class SplunkApp:
231
251
  return entry.get("link").get("@href")
232
252
 
233
253
  # raise if no entry was found
234
- raise SplunkBaseError(f"No app entry found with 'islatest' tag set to True: {self.app_name_id}")
254
+ raise SplunkBaseError(
255
+ f"No app entry found with 'islatest' tag set to True: {self.app_name_id}"
256
+ )
235
257
 
236
258
  def __fetch_url_latest_version_download(self, info_url: str) -> str:
237
259
  """
@@ -241,10 +263,14 @@ class SplunkApp:
241
263
  """
242
264
  # fetch download info
243
265
  try:
244
- response = self.requests_retry_session().get(info_url, timeout=RetryConstant.RETRY_INTERVAL)
266
+ response = self.requests_retry_session().get(
267
+ info_url, timeout=RetryConstant.RETRY_INTERVAL
268
+ )
245
269
  response.raise_for_status()
246
270
  except requests.exceptions.RequestException as e:
247
- raise SplunkBaseError(f"Error fetching download info for app_name_id '{self.app_name_id}': {str(e)}")
271
+ raise SplunkBaseError(
272
+ f"Error fetching download info for app_name_id '{self.app_name_id}': {str(e)}"
273
+ )
248
274
 
249
275
  # parse XML and extract download URL
250
276
  build_xml = xmltodict.parse(response.content)
@@ -254,14 +280,18 @@ class SplunkApp:
254
280
  def set_latest_version_info(self) -> None:
255
281
  # raise if app_name_id not set
256
282
  if self.app_name_id is None:
257
- raise SplunkApp.InitializationError("app_name_id must be set in order to fetch latest version info")
283
+ raise SplunkApp.InitializationError(
284
+ "app_name_id must be set in order to fetch latest version info"
285
+ )
258
286
 
259
287
  # fetch the info URL
260
288
  info_url = self.__fetch_url_latest_version_info()
261
289
 
262
290
  # parse out the version number and fetch the download URL
263
291
  self.latest_version = info_url.split("/")[-1]
264
- self.latest_version_download_url = self.__fetch_url_latest_version_download(info_url)
292
+ self.latest_version_download_url = self.__fetch_url_latest_version_download(
293
+ info_url
294
+ )
265
295
 
266
296
  def __get_splunk_base_session_token(self, username: str, password: str) -> str:
267
297
  """
@@ -309,12 +339,12 @@ class SplunkApp:
309
339
  return token_value
310
340
 
311
341
  def download(
312
- self,
313
- out: Path,
314
- username: str,
315
- password: str,
316
- is_dir: bool = False,
317
- overwrite: bool = False
342
+ self,
343
+ out: Path,
344
+ username: str,
345
+ password: str,
346
+ is_dir: bool = False,
347
+ overwrite: bool = False,
318
348
  ) -> Path:
319
349
  """
320
350
  Given an output path, download the app to the specified location
@@ -336,11 +366,7 @@ class SplunkApp:
336
366
  # Get the Splunkbase session token
337
367
  token = self.__get_splunk_base_session_token(username, password)
338
368
  response = requests.request(
339
- "GET",
340
- self.latest_version_download_url,
341
- cookies={
342
- "sessionid": token
343
- }
369
+ "GET", self.latest_version_download_url, cookies={"sessionid": token}
344
370
  )
345
371
 
346
372
  # If the provided output path was a directory we need to try and pull the filename from the
@@ -348,17 +374,21 @@ class SplunkApp:
348
374
  if is_dir:
349
375
  try:
350
376
  # Pull 'Content-Disposition' from the headers
351
- content_disposition: str = response.headers['Content-Disposition']
377
+ content_disposition: str = response.headers["Content-Disposition"]
352
378
 
353
379
  # Attempt to parse the filename as a KV
354
380
  key, value = content_disposition.strip().split("=")
355
381
  if key != "attachment;filename":
356
- raise ValueError(f"Unexpected key in 'Content-Disposition' KV pair: {key}")
382
+ raise ValueError(
383
+ f"Unexpected key in 'Content-Disposition' KV pair: {key}"
384
+ )
357
385
 
358
386
  # Validate the filename is the expected .tgz file
359
387
  filename = Path(value.strip().strip('"'))
360
388
  if filename.suffixes != [".tgz"]:
361
- raise ValueError(f"Filename has unexpected extension(s): {filename.suffixes}")
389
+ raise ValueError(
390
+ f"Filename has unexpected extension(s): {filename.suffixes}"
391
+ )
362
392
  out = Path(out, filename)
363
393
  except KeyError as e:
364
394
  raise KeyError(
@@ -371,9 +401,7 @@ class SplunkApp:
371
401
 
372
402
  # Ensure the output path is not already occupied
373
403
  if out.exists() and not overwrite:
374
- msg = (
375
- f"File already exists at {out}, cannot download the app."
376
- )
404
+ msg = f"File already exists at {out}, cannot download the app."
377
405
  raise Exception(msg)
378
406
 
379
407
  # Make any parent directories as needed