contentctl 5.0.0a2__py3-none-any.whl → 5.0.0a3__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 (106) 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 +78 -50
  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 +250 -314
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
  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 +41 -30
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +29 -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 +41 -26
  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 +14 -8
  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 +45 -44
  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/transforms.j2 +2 -2
  98. contentctl/output/yml_writer.py +18 -24
  99. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  100. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  101. contentctl/actions/initialize_old.py +0 -245
  102. contentctl/objects/observable.py +0 -39
  103. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  104. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  105. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  106. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -1,121 +1,161 @@
1
-
2
1
  from __future__ import annotations
3
- import sys
4
2
  from attackcti import attack_client
5
3
  import logging
6
4
  from pydantic import BaseModel
7
5
  from dataclasses import field
8
6
  from typing import Any
9
7
  from pathlib import Path
10
- from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment, MitreTactics
8
+ from contentctl.objects.mitre_attack_enrichment import (
9
+ MitreAttackEnrichment,
10
+ MitreTactics,
11
+ )
11
12
  from contentctl.objects.config import validate
12
13
  from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
13
- logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
14
+
15
+ logging.getLogger("taxii2client").setLevel(logging.CRITICAL)
14
16
 
15
17
 
16
18
  class AttackEnrichment(BaseModel):
17
19
  data: dict[str, MitreAttackEnrichment] = field(default_factory=dict)
18
- use_enrichment:bool = True
19
-
20
+ use_enrichment: bool = True
21
+
20
22
  @staticmethod
21
- def getAttackEnrichment(config:validate)->AttackEnrichment:
23
+ def getAttackEnrichment(config: validate) -> AttackEnrichment:
22
24
  enrichment = AttackEnrichment(use_enrichment=config.enrichments)
23
25
  _ = enrichment.get_attack_lookup(config.mitre_cti_repo_path, config.enrichments)
24
26
  return enrichment
25
-
26
- def getEnrichmentByMitreID(self, mitre_id:MITRE_ATTACK_ID_TYPE)->MitreAttackEnrichment:
27
+
28
+ def getEnrichmentByMitreID(
29
+ self, mitre_id: MITRE_ATTACK_ID_TYPE
30
+ ) -> MitreAttackEnrichment:
27
31
  if not self.use_enrichment:
28
- raise Exception("Error, trying to add Mitre Enrichment, but use_enrichment was set to False")
29
-
32
+ raise Exception(
33
+ "Error, trying to add Mitre Enrichment, but use_enrichment was set to False"
34
+ )
35
+
30
36
  enrichment = self.data.get(mitre_id, None)
31
37
  if enrichment is not None:
32
38
  return enrichment
33
39
  else:
34
- raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
35
-
36
- def addMitreIDViaGroupNames(self, technique:dict[str,Any], tactics:list[str], groupNames:list[str])->None:
37
- technique_id = technique['technique_id']
38
- technique_obj = technique['technique']
40
+ raise Exception(
41
+ f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}"
42
+ )
43
+
44
+ def addMitreIDViaGroupNames(
45
+ self, technique: dict[str, Any], tactics: list[str], groupNames: list[str]
46
+ ) -> None:
47
+ technique_id = technique["technique_id"]
48
+ technique_obj = technique["technique"]
39
49
  tactics.sort()
40
-
50
+
41
51
  if technique_id in self.data:
42
52
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
43
- self.data[technique_id] = MitreAttackEnrichment.model_validate({'mitre_attack_id':technique_id,
44
- 'mitre_attack_technique':technique_obj,
45
- 'mitre_attack_tactics':tactics,
46
- 'mitre_attack_groups':groupNames,
47
- 'mitre_attack_group_objects':[]})
48
-
49
- def addMitreIDViaGroupObjects(self, technique:dict[str,Any], tactics:list[MitreTactics], groupDicts:list[dict[str,Any]])->None:
50
- technique_id = technique['technique_id']
51
- technique_obj = technique['technique']
53
+ self.data[technique_id] = MitreAttackEnrichment.model_validate(
54
+ {
55
+ "mitre_attack_id": technique_id,
56
+ "mitre_attack_technique": technique_obj,
57
+ "mitre_attack_tactics": tactics,
58
+ "mitre_attack_groups": groupNames,
59
+ "mitre_attack_group_objects": [],
60
+ }
61
+ )
62
+
63
+ def addMitreIDViaGroupObjects(
64
+ self,
65
+ technique: dict[str, Any],
66
+ tactics: list[MitreTactics],
67
+ groupDicts: list[dict[str, Any]],
68
+ ) -> None:
69
+ technique_id = technique["technique_id"]
70
+ technique_obj = technique["technique"]
52
71
  tactics.sort()
53
-
54
- groupNames:list[str] = sorted([group['group'] for group in groupDicts])
55
-
72
+
73
+ groupNames: list[str] = sorted([group["group"] for group in groupDicts])
74
+
56
75
  if technique_id in self.data:
57
76
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
58
-
59
- self.data[technique_id] = MitreAttackEnrichment.model_validate({'mitre_attack_id': technique_id,
60
- 'mitre_attack_technique': technique_obj,
61
- 'mitre_attack_tactics': tactics,
62
- 'mitre_attack_groups': groupNames,
63
- 'mitre_attack_group_objects': groupDicts})
64
-
65
-
66
- def get_attack_lookup(self, input_path: Path, enrichments:bool = False) -> dict[str,MitreAttackEnrichment]:
67
- attack_lookup:dict[str,MitreAttackEnrichment] = {}
77
+
78
+ self.data[technique_id] = MitreAttackEnrichment.model_validate(
79
+ {
80
+ "mitre_attack_id": technique_id,
81
+ "mitre_attack_technique": technique_obj,
82
+ "mitre_attack_tactics": tactics,
83
+ "mitre_attack_groups": groupNames,
84
+ "mitre_attack_group_objects": groupDicts,
85
+ }
86
+ )
87
+
88
+ def get_attack_lookup(
89
+ self, input_path: Path, enrichments: bool = False
90
+ ) -> dict[str, MitreAttackEnrichment]:
91
+ attack_lookup: dict[str, MitreAttackEnrichment] = {}
68
92
  if not enrichments:
69
93
  return attack_lookup
70
-
94
+
71
95
  try:
72
- print(f"Performing MITRE Enrichment using the repository at {input_path}...",end="", flush=True)
73
- # The existence of the input_path is validated during cli argument validation, but it is
96
+ print(
97
+ f"Performing MITRE Enrichment using the repository at {input_path}...",
98
+ end="",
99
+ flush=True,
100
+ )
101
+ # The existence of the input_path is validated during cli argument validation, but it is
74
102
  # possible that the repo is in the wrong format. If the following directories do not
75
- # exist, then attack_client will fall back to resolving via REST API. We do not
76
- # want this as it is slow and error prone, so we will force an exception to
103
+ # exist, then attack_client will fall back to resolving via REST API. We do not
104
+ # want this as it is slow and error prone, so we will force an exception to
77
105
  # be generated.
78
- enterprise_path = input_path/"enterprise-attack"
79
- mobile_path = input_path/"ics-attack"
80
- ics_path = input_path/"mobile-attack"
81
- if not (enterprise_path.is_dir() and mobile_path.is_dir() and ics_path.is_dir()):
82
- raise FileNotFoundError("One or more of the following paths does not exist: "
83
- f"{[str(enterprise_path),str(mobile_path),str(ics_path)]}. "
84
- f"Please ensure that the {input_path} directory "
85
- "has been git cloned correctly.")
106
+ enterprise_path = input_path / "enterprise-attack"
107
+ mobile_path = input_path / "ics-attack"
108
+ ics_path = input_path / "mobile-attack"
109
+ if not (
110
+ enterprise_path.is_dir() and mobile_path.is_dir() and ics_path.is_dir()
111
+ ):
112
+ raise FileNotFoundError(
113
+ "One or more of the following paths does not exist: "
114
+ f"{[str(enterprise_path), str(mobile_path), str(ics_path)]}. "
115
+ f"Please ensure that the {input_path} directory "
116
+ "has been git cloned correctly."
117
+ )
86
118
  lift = attack_client(
87
- local_paths= {
88
- "enterprise":str(enterprise_path),
89
- "mobile":str(mobile_path),
90
- "ics":str(ics_path)
119
+ local_paths={
120
+ "enterprise": str(enterprise_path),
121
+ "mobile": str(mobile_path),
122
+ "ics": str(ics_path),
91
123
  }
92
124
  )
93
-
94
- all_enterprise_techniques = lift.get_enterprise_techniques(stix_format=False)
95
- enterprise_relationships = lift.get_enterprise_relationships(stix_format=False)
125
+
126
+ all_enterprise_techniques = lift.get_enterprise_techniques(
127
+ stix_format=False
128
+ )
129
+ enterprise_relationships = lift.get_enterprise_relationships(
130
+ stix_format=False
131
+ )
96
132
  enterprise_groups = lift.get_enterprise_groups(stix_format=False)
97
-
133
+
98
134
  for technique in all_enterprise_techniques:
99
- apt_groups:list[dict[str,Any]] = []
135
+ apt_groups: list[dict[str, Any]] = []
100
136
  for relationship in enterprise_relationships:
101
- if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
137
+ if (
138
+ relationship["target_object"] == technique["id"]
139
+ ) and relationship["source_object"].startswith("intrusion-set"):
102
140
  for group in enterprise_groups:
103
- if relationship['source_object'] == group['id']:
141
+ if relationship["source_object"] == group["id"]:
104
142
  apt_groups.append(group)
105
- #apt_groups.append(group['group'])
143
+ # apt_groups.append(group['group'])
106
144
 
107
145
  tactics = []
108
- if ('tactic' in technique):
109
- for tactic in technique['tactic']:
110
- tactics.append(tactic.replace('-',' ').title())
146
+ if "tactic" in technique:
147
+ for tactic in technique["tactic"]:
148
+ tactics.append(tactic.replace("-", " ").title())
111
149
 
112
150
  self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
113
- attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
114
-
151
+ attack_lookup[technique["technique_id"]] = {
152
+ "technique": technique["technique"],
153
+ "tactics": tactics,
154
+ "groups": apt_groups,
155
+ }
115
156
 
116
-
117
157
  except Exception as err:
118
158
  raise Exception(f"Error getting MITRE Enrichment: {str(err)}")
119
-
159
+
120
160
  print("Done!")
121
- return attack_lookup
161
+ return attack_lookup
@@ -1,64 +1,70 @@
1
1
  from __future__ import annotations
2
2
  from pycvesearch import CVESearch
3
- import functools
4
- import os
5
- import shelve
6
- import time
7
- from typing import Annotated, Any, Union, TYPE_CHECKING
8
- from pydantic import ConfigDict, BaseModel,Field, computed_field
3
+ from typing import Annotated, Union, TYPE_CHECKING
4
+ from pydantic import ConfigDict, BaseModel, Field, computed_field
9
5
  from decimal import Decimal
10
- from requests.exceptions import ReadTimeout
11
6
  from contentctl.objects.annotated_types import CVE_TYPE
7
+
12
8
  if TYPE_CHECKING:
13
9
  from contentctl.objects.config import validate
14
10
 
15
11
 
16
-
17
- CVESSEARCH_API_URL = 'https://cve.circl.lu'
12
+ CVESSEARCH_API_URL = "https://cve.circl.lu"
18
13
 
19
14
 
20
15
  class CveEnrichmentObj(BaseModel):
21
16
  id: CVE_TYPE
22
- cvss: Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
17
+ cvss: Annotated[Decimal, Field(ge=0.1, le=10, decimal_places=1)]
23
18
  summary: str
24
-
19
+
25
20
  @computed_field
26
21
  @property
27
- def url(self)->str:
22
+ def url(self) -> str:
28
23
  BASE_NVD_URL = "https://nvd.nist.gov/vuln/detail/"
29
24
  return f"{BASE_NVD_URL}{self.id}"
30
25
 
31
26
 
32
27
  class CveEnrichment(BaseModel):
33
28
  use_enrichment: bool = True
34
- cve_api_obj: Union[CVESearch,None] = None
29
+ cve_api_obj: Union[CVESearch, None] = None
35
30
 
36
31
  # Arbitrary_types are allowed to let us use the CVESearch Object
37
- model_config = ConfigDict(
38
- arbitrary_types_allowed=True,
39
- frozen=True
40
- )
32
+ model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
41
33
 
42
34
  @staticmethod
43
- def getCveEnrichment(config:validate, timeout_seconds:int=10, force_disable_enrichment:bool=True)->CveEnrichment:
35
+ def getCveEnrichment(
36
+ config: validate,
37
+ timeout_seconds: int = 10,
38
+ force_disable_enrichment: bool = True,
39
+ ) -> CveEnrichment:
44
40
  if force_disable_enrichment:
45
- return CveEnrichment(use_enrichment=False, cve_api_obj=None)
46
-
41
+ return CveEnrichment(use_enrichment=False, cve_api_obj=None)
42
+
47
43
  if config.enrichments:
48
44
  try:
49
45
  cve_api_obj = CVESearch(CVESSEARCH_API_URL, timeout=timeout_seconds)
50
46
  return CveEnrichment(use_enrichment=True, cve_api_obj=cve_api_obj)
51
47
  except Exception as e:
52
- raise Exception(f"Error setting CVE_SEARCH API to: {CVESSEARCH_API_URL}: {str(e)}")
53
-
54
- return CveEnrichment(use_enrichment=False, cve_api_obj=None)
55
-
48
+ raise Exception(
49
+ f"Error setting CVE_SEARCH API to: {CVESSEARCH_API_URL}: {str(e)}"
50
+ )
56
51
 
57
- def enrich_cve(self, cve_id:str, raise_exception_on_failure:bool=True)->CveEnrichmentObj:
52
+ return CveEnrichment(use_enrichment=False, cve_api_obj=None)
58
53
 
54
+ def enrich_cve(
55
+ self, cve_id: str, raise_exception_on_failure: bool = True
56
+ ) -> CveEnrichmentObj:
59
57
  if not self.use_enrichment:
60
- return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
58
+ return CveEnrichmentObj(
59
+ id=cve_id,
60
+ cvss=Decimal(5.0),
61
+ summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME",
62
+ )
61
63
  else:
62
64
  print("WARNING - Dynamic enrichment not supported at this time.")
63
- return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
64
- # Depending on needs, we may add dynamic enrichment functionality back to the tool
65
+ return CveEnrichmentObj(
66
+ id=cve_id,
67
+ cvss=Decimal(5.0),
68
+ summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME",
69
+ )
70
+ # Depending on needs, we may add dynamic enrichment functionality back to the tool
@@ -1,11 +1,8 @@
1
1
  import requests
2
2
  import xmltodict
3
- import json
4
3
  import functools
5
- import pickle
6
4
  import shelve
7
5
  import os
8
- import time
9
6
 
10
7
  SPLUNKBASE_API_URL = "https://apps.splunk.com/api/apps/entriesbyid/"
11
8
 
@@ -13,15 +10,16 @@ APP_ENRICHMENT_CACHE_FILENAME = "lookups/APP_ENRICHMENT_CACHE.db"
13
10
 
14
11
  NON_PERSISTENT_CACHE = {}
15
12
 
13
+
16
14
  @functools.cache
17
- def requests_get_helper(url:str, force_cached_or_offline:bool = False)->bytes:
15
+ def requests_get_helper(url: str, force_cached_or_offline: bool = False) -> bytes:
18
16
  if force_cached_or_offline:
19
17
  if not os.path.exists(APP_ENRICHMENT_CACHE_FILENAME):
20
18
  print(f"Cache at {APP_ENRICHMENT_CACHE_FILENAME} not found - Creating it.")
21
- cache = shelve.open(APP_ENRICHMENT_CACHE_FILENAME, flag='c', writeback=True)
19
+ cache = shelve.open(APP_ENRICHMENT_CACHE_FILENAME, flag="c", writeback=True)
22
20
  else:
23
21
  cache = NON_PERSISTENT_CACHE
24
-
22
+
25
23
  if url in cache:
26
24
  req_content = cache[url]
27
25
  else:
@@ -29,62 +27,66 @@ def requests_get_helper(url:str, force_cached_or_offline:bool = False)->bytes:
29
27
  req = requests.get(url)
30
28
  req_content = req.content
31
29
  cache[url] = req_content
32
- except Exception as e:
33
- raise(Exception(f"ERROR - Failed to get Splunk App Enrichment at {SPLUNKBASE_API_URL}"))
34
-
30
+ except Exception:
31
+ raise (
32
+ Exception(
33
+ f"ERROR - Failed to get Splunk App Enrichment at {SPLUNKBASE_API_URL}"
34
+ )
35
+ )
36
+
35
37
  if isinstance(cache, shelve.Shelf):
36
- #close the cache if it is a shelf
38
+ # close the cache if it is a shelf
37
39
  cache.close()
38
-
40
+
39
41
  return req_content
40
42
 
41
- class SplunkAppEnrichment():
42
43
 
44
+ class SplunkAppEnrichment:
43
45
  @classmethod
44
- def enrich_splunk_app(self, splunk_ta: str, force_cached_or_offline: bool = False) -> dict:
45
-
46
+ def enrich_splunk_app(
47
+ self, splunk_ta: str, force_cached_or_offline: bool = False
48
+ ) -> dict:
46
49
  appurl = SPLUNKBASE_API_URL + splunk_ta
47
50
  splunk_app_enriched = dict()
48
-
51
+
49
52
  try:
50
-
51
53
  content = requests_get_helper(appurl, force_cached_or_offline)
52
54
  response_dict = xmltodict.parse(content)
53
-
55
+
54
56
  # check if list since data changes depending on answer
55
57
  url, results = self._parse_splunkbase_response(response_dict)
56
58
  # grab the app name
57
59
  for i in results:
58
- if i['@name'] == 'appName':
59
- splunk_app_enriched['name'] = i['#text']
60
- # grab out the splunkbase url
61
- if 'entriesbyid' in url:
60
+ if i["@name"] == "appName":
61
+ splunk_app_enriched["name"] = i["#text"]
62
+ # grab out the splunkbase url
63
+ if "entriesbyid" in url:
62
64
  content = requests_get_helper(url, force_cached_or_offline)
63
65
  response_dict = xmltodict.parse(content)
64
-
65
- #print(json.dumps(response_dict, indent=2))
66
+
67
+ # print(json.dumps(response_dict, indent=2))
66
68
  url, results = self._parse_splunkbase_response(response_dict)
67
69
  # chop the url so we grab the splunkbase portion but not direct download
68
- splunk_app_enriched['url'] = url.rsplit('/', 4)[0]
70
+ splunk_app_enriched["url"] = url.rsplit("/", 4)[0]
69
71
  except requests.exceptions.ConnectionError as connErr:
70
72
  print(f"There was a connErr for ta {splunk_ta}: {connErr}")
71
73
  # there was a connection error lets just capture the name
72
- splunk_app_enriched['name'] = splunk_ta
73
- splunk_app_enriched['url'] = ''
74
+ splunk_app_enriched["name"] = splunk_ta
75
+ splunk_app_enriched["url"] = ""
74
76
  except Exception as e:
75
- print(f"There was an unknown error enriching the Splunk TA [{splunk_ta}]: {str(e)}")
76
- splunk_app_enriched['name'] = splunk_ta
77
- splunk_app_enriched['url'] = ''
78
-
77
+ print(
78
+ f"There was an unknown error enriching the Splunk TA [{splunk_ta}]: {str(e)}"
79
+ )
80
+ splunk_app_enriched["name"] = splunk_ta
81
+ splunk_app_enriched["url"] = ""
79
82
 
80
83
  return splunk_app_enriched
81
84
 
82
85
  def _parse_splunkbase_response(response_dict):
83
- if isinstance(response_dict['feed']['entry'], list):
84
- url = response_dict['feed']['entry'][0]['link']['@href']
85
- results = response_dict['feed']['entry'][0]['content']['s:dict']['s:key']
86
+ if isinstance(response_dict["feed"]["entry"], list):
87
+ url = response_dict["feed"]["entry"][0]["link"]["@href"]
88
+ results = response_dict["feed"]["entry"][0]["content"]["s:dict"]["s:key"]
86
89
  else:
87
- url = response_dict['feed']['entry']['link']['@href']
88
- results = response_dict['feed']['entry']['content']['s:dict']['s:key']
90
+ url = response_dict["feed"]["entry"]["link"]["@href"]
91
+ results = response_dict["feed"]["entry"]["content"]["s:dict"]["s:key"]
89
92
  return url, results
90
-