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.
- contentctl/__init__.py +1 -1
- contentctl/actions/build.py +88 -55
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
- contentctl/actions/detection_testing/GitService.py +2 -4
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +3 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +78 -50
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +31 -25
- contentctl/actions/validate.py +54 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +10 -10
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -39
- contentctl/input/director.py +69 -37
- contentctl/input/new_content_questions.py +26 -34
- contentctl/input/yml_reader.py +22 -17
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
- contentctl/objects/alert_action.py +8 -8
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +64 -54
- contentctl/objects/base_test.py +2 -1
- contentctl/objects/base_test_result.py +16 -8
- contentctl/objects/baseline.py +41 -30
- contentctl/objects/baseline_tags.py +29 -22
- contentctl/objects/config.py +1 -1
- contentctl/objects/constants.py +29 -58
- contentctl/objects/correlation_search.py +75 -55
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +13 -13
- contentctl/objects/deployment.py +44 -37
- contentctl/objects/deployment_email.py +1 -1
- contentctl/objects/deployment_notable.py +2 -1
- contentctl/objects/deployment_phantom.py +5 -5
- contentctl/objects/deployment_rba.py +1 -1
- contentctl/objects/deployment_scheduling.py +1 -1
- contentctl/objects/deployment_slack.py +1 -1
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +54 -64
- contentctl/objects/drilldown.py +66 -35
- contentctl/objects/enums.py +61 -43
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +41 -26
- contentctl/objects/investigation_tags.py +29 -17
- contentctl/objects/lookup.py +234 -113
- contentctl/objects/macro.py +55 -38
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +22 -16
- contentctl/objects/rba.py +14 -8
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +27 -20
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +45 -44
- contentctl/objects/story_tags.py +56 -44
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +4 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +22 -22
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +230 -174
- contentctl/output/data_source_writer.py +38 -25
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +20 -8
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/transforms.j2 +2 -2
- contentctl/output/yml_writer.py +18 -24
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
- contentctl-5.0.0a3.dist-info/RECORD +168 -0
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/observable.py +0 -39
- contentctl-5.0.0a2.dist-info/RECORD +0 -170
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
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(
|
|
27
|
+
|
|
28
|
+
def getEnrichmentByMitreID(
|
|
29
|
+
self, mitre_id: MITRE_ATTACK_ID_TYPE
|
|
30
|
+
) -> MitreAttackEnrichment:
|
|
27
31
|
if not self.use_enrichment:
|
|
28
|
-
raise Exception(
|
|
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(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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[
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
73
|
-
|
|
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 (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
95
|
-
|
|
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 (
|
|
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[
|
|
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
|
|
109
|
-
for tactic in technique[
|
|
110
|
-
tactics.append(tactic.replace(
|
|
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[
|
|
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
|
|
4
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
raise Exception(
|
|
49
|
+
f"Error setting CVE_SEARCH API to: {CVESSEARCH_API_URL}: {str(e)}"
|
|
50
|
+
)
|
|
56
51
|
|
|
57
|
-
|
|
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(
|
|
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(
|
|
64
|
-
|
|
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=
|
|
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
|
|
33
|
-
raise(
|
|
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(
|
|
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[
|
|
59
|
-
splunk_app_enriched[
|
|
60
|
-
# grab out the splunkbase url
|
|
61
|
-
if
|
|
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[
|
|
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[
|
|
73
|
-
splunk_app_enriched[
|
|
74
|
+
splunk_app_enriched["name"] = splunk_ta
|
|
75
|
+
splunk_app_enriched["url"] = ""
|
|
74
76
|
except Exception as e:
|
|
75
|
-
print(
|
|
76
|
-
|
|
77
|
-
|
|
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[
|
|
84
|
-
url = response_dict[
|
|
85
|
-
results = response_dict[
|
|
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[
|
|
88
|
-
results = response_dict[
|
|
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
|
-
|