psengine 2.0.4__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.
- psengine/__init__.py +22 -0
- psengine/_sdk_id.py +16 -0
- psengine/_version.py +14 -0
- psengine/analyst_notes/__init__.py +32 -0
- psengine/analyst_notes/constants.py +15 -0
- psengine/analyst_notes/errors.py +42 -0
- psengine/analyst_notes/helpers.py +90 -0
- psengine/analyst_notes/models.py +219 -0
- psengine/analyst_notes/note.py +149 -0
- psengine/analyst_notes/note_mgr.py +400 -0
- psengine/base_http_client.py +285 -0
- psengine/classic_alerts/__init__.py +24 -0
- psengine/classic_alerts/classic_alert.py +275 -0
- psengine/classic_alerts/classic_alert_mgr.py +507 -0
- psengine/classic_alerts/constants.py +31 -0
- psengine/classic_alerts/errors.py +38 -0
- psengine/classic_alerts/helpers.py +87 -0
- psengine/classic_alerts/markdown/__init__.py +13 -0
- psengine/classic_alerts/markdown/markdown.py +359 -0
- psengine/classic_alerts/models.py +141 -0
- psengine/collective_insights/__init__.py +29 -0
- psengine/collective_insights/collective_insights.py +164 -0
- psengine/collective_insights/constants.py +44 -0
- psengine/collective_insights/errors.py +18 -0
- psengine/collective_insights/insight.py +89 -0
- psengine/collective_insights/models.py +81 -0
- psengine/common_models.py +89 -0
- psengine/config/__init__.py +15 -0
- psengine/config/config.py +284 -0
- psengine/config/errors.py +18 -0
- psengine/constants.py +63 -0
- psengine/detection/__init__.py +17 -0
- psengine/detection/detection_mgr.py +135 -0
- psengine/detection/detection_rule.py +85 -0
- psengine/detection/errors.py +26 -0
- psengine/detection/helpers.py +56 -0
- psengine/detection/models.py +47 -0
- psengine/endpoints.py +98 -0
- psengine/enrich/__init__.py +28 -0
- psengine/enrich/constants.py +73 -0
- psengine/enrich/errors.py +26 -0
- psengine/enrich/lookup.py +299 -0
- psengine/enrich/lookup_mgr.py +341 -0
- psengine/enrich/models/__init__.py +13 -0
- psengine/enrich/models/base_enriched_entity.py +43 -0
- psengine/enrich/models/lookup.py +271 -0
- psengine/enrich/models/soar.py +138 -0
- psengine/enrich/soar.py +89 -0
- psengine/enrich/soar_mgr.py +176 -0
- psengine/entity_lists/__init__.py +16 -0
- psengine/entity_lists/constants.py +19 -0
- psengine/entity_lists/entity_list.py +435 -0
- psengine/entity_lists/entity_list_mgr.py +185 -0
- psengine/entity_lists/errors.py +26 -0
- psengine/entity_lists/models.py +87 -0
- psengine/entity_match/__init__.py +16 -0
- psengine/entity_match/entity_match.py +90 -0
- psengine/entity_match/entity_match_mgr.py +235 -0
- psengine/entity_match/errors.py +18 -0
- psengine/entity_match/models.py +22 -0
- psengine/errors.py +41 -0
- psengine/helpers/__init__.py +23 -0
- psengine/helpers/helpers.py +471 -0
- psengine/logger/__init__.py +15 -0
- psengine/logger/constants.py +39 -0
- psengine/logger/errors.py +18 -0
- psengine/logger/rf_logger.py +148 -0
- psengine/markdown/__init__.py +21 -0
- psengine/markdown/markdown.py +169 -0
- psengine/markdown/models.py +22 -0
- psengine/playbook_alerts/__init__.py +34 -0
- psengine/playbook_alerts/constants.py +35 -0
- psengine/playbook_alerts/errors.py +35 -0
- psengine/playbook_alerts/helpers.py +80 -0
- psengine/playbook_alerts/mappings.py +44 -0
- psengine/playbook_alerts/markdown/__init__.py +13 -0
- psengine/playbook_alerts/markdown/markdown.py +98 -0
- psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
- psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
- psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
- psengine/playbook_alerts/models/__init__.py +36 -0
- psengine/playbook_alerts/models/common_models.py +18 -0
- psengine/playbook_alerts/models/panel_log.py +329 -0
- psengine/playbook_alerts/models/panel_status.py +70 -0
- psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
- psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
- psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
- psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
- psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
- psengine/playbook_alerts/models/search_endpoint.py +68 -0
- psengine/playbook_alerts/pa_category.py +37 -0
- psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
- psengine/playbook_alerts/playbook_alerts.py +393 -0
- psengine/rf_client.py +430 -0
- psengine/risklists/__init__.py +17 -0
- psengine/risklists/constants.py +15 -0
- psengine/risklists/errors.py +20 -0
- psengine/risklists/models.py +65 -0
- psengine/risklists/risklist_mgr.py +156 -0
- psengine/stix2/__init__.py +21 -0
- psengine/stix2/base_stix_entity.py +62 -0
- psengine/stix2/complex_entity.py +372 -0
- psengine/stix2/constants.py +81 -0
- psengine/stix2/enriched_indicator.py +261 -0
- psengine/stix2/errors.py +22 -0
- psengine/stix2/helpers.py +68 -0
- psengine/stix2/rf_bundle.py +240 -0
- psengine/stix2/simple_entity.py +145 -0
- psengine/stix2/util.py +53 -0
- psengine-2.0.4.dist-info/METADATA +189 -0
- psengine-2.0.4.dist-info/RECORD +115 -0
- psengine-2.0.4.dist-info/WHEEL +5 -0
- psengine-2.0.4.dist-info/entry_points.txt +2 -0
- psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
- psengine-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
import stix2
|
|
17
|
+
|
|
18
|
+
from ..helpers import FormattingHelpers
|
|
19
|
+
from .base_stix_entity import BaseStixEntity
|
|
20
|
+
from .complex_entity import IndicatorEntity, NoteEntity, Relationship
|
|
21
|
+
from .errors import STIX2TransformError, UnsupportedConversionTypeError
|
|
22
|
+
from .helpers import convert_entity
|
|
23
|
+
from .simple_entity import TTP, ThreatActor
|
|
24
|
+
|
|
25
|
+
LOG = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EnrichedIndicator(IndicatorEntity):
|
|
29
|
+
"""Class for converting Indicator + risk score + links to OpenCTI bundle."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
type_: str,
|
|
35
|
+
evidence_details: list,
|
|
36
|
+
link_hits: list = None,
|
|
37
|
+
risk_mapping: list = None,
|
|
38
|
+
ai_insights: dict = None,
|
|
39
|
+
author: stix2.Identity = None,
|
|
40
|
+
confidence: int = None,
|
|
41
|
+
create_indicator: bool = True,
|
|
42
|
+
create_obs: bool = True,
|
|
43
|
+
tlp_marking='amber',
|
|
44
|
+
):
|
|
45
|
+
"""Indicator container. Containers Indicator, observable, and relationship between them.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name (str): Indicator value
|
|
49
|
+
type_ (str): Recorded Future type of indicator
|
|
50
|
+
evidence_details (list): risk rules + evidence details
|
|
51
|
+
link_hits (list, optional): list of lists
|
|
52
|
+
risk_mapping (list, optional): Risk rule to TTP mapping
|
|
53
|
+
ai_insights (dict, optional): AI insights for IOC in Recorded Future
|
|
54
|
+
author (stix2.Identity, optional): Recorded Future Identity
|
|
55
|
+
confidence (int, optional): Confidence score of indicator
|
|
56
|
+
create_indicator (bool, optional): flag that governs if indicator should be created
|
|
57
|
+
create_obs (bool, optional): flag that governs if observable should be created
|
|
58
|
+
tlp_marking (str, optional): the TLP level. Default to amber
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
STIX2TransformError: if transformation fails
|
|
62
|
+
"""
|
|
63
|
+
link_hits = link_hits or []
|
|
64
|
+
risk_mapping = risk_mapping or []
|
|
65
|
+
ai_insights = ai_insights or {}
|
|
66
|
+
evidence_details = [e if isinstance(e, dict) else e.json() for e in evidence_details]
|
|
67
|
+
labels = ['rf:' + (rule.get('Rule') or rule.get('rule')) for rule in evidence_details]
|
|
68
|
+
|
|
69
|
+
description = self._format_description(ai_insights)
|
|
70
|
+
super().__init__(
|
|
71
|
+
name=name,
|
|
72
|
+
type_=type_,
|
|
73
|
+
confidence=confidence,
|
|
74
|
+
create_indicator=create_indicator,
|
|
75
|
+
create_obs=create_obs,
|
|
76
|
+
labels=labels,
|
|
77
|
+
description=description,
|
|
78
|
+
tlp_marking=tlp_marking,
|
|
79
|
+
author=author,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self._add_risk_score_as_note(confidence)
|
|
83
|
+
|
|
84
|
+
for ttp in risk_mapping:
|
|
85
|
+
self._add_ttp(ttp)
|
|
86
|
+
for rule in evidence_details:
|
|
87
|
+
self._add_rule(rule)
|
|
88
|
+
|
|
89
|
+
for hit in link_hits:
|
|
90
|
+
self._add_link(hit)
|
|
91
|
+
|
|
92
|
+
def _format_description(self, ai_insights: dict):
|
|
93
|
+
"""If AI insights are available then:
|
|
94
|
+
- use it as a description
|
|
95
|
+
- otherwise use a default string value.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
ai_insights (dict): string from RF API
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
str: Indicator description
|
|
102
|
+
"""
|
|
103
|
+
ai_insights = ai_insights if isinstance(ai_insights, dict) else ai_insights.json()
|
|
104
|
+
description = 'Indicator from Recorded Future'
|
|
105
|
+
if ai_insights.get('text') is not None:
|
|
106
|
+
description = '### Recorded Future AI Insights\n\n{}'.format(
|
|
107
|
+
FormattingHelpers.cleanup_ai_insights(ai_insights.get('text')),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return description
|
|
111
|
+
|
|
112
|
+
def _add_link(self, hit) -> None:
|
|
113
|
+
hit = hit if isinstance(hit, dict) else hit.json()
|
|
114
|
+
for section in hit['sections']:
|
|
115
|
+
for list_ in section['lists']:
|
|
116
|
+
if list_['type']['name'] == 'Threat Actor':
|
|
117
|
+
for entity in list_['entities']:
|
|
118
|
+
stix_entity = ThreatActor(entity['name'])
|
|
119
|
+
self.stix_objects.append(stix_entity.stix_obj)
|
|
120
|
+
self._relate(stix_entity)
|
|
121
|
+
else:
|
|
122
|
+
for entity in list_['entities']:
|
|
123
|
+
try:
|
|
124
|
+
stix_entity = convert_entity(entity['name'], entity['type'])
|
|
125
|
+
except UnsupportedConversionTypeError as err:
|
|
126
|
+
LOG.warning(str(err) + '. Skipping...')
|
|
127
|
+
continue
|
|
128
|
+
if isinstance(stix_entity, IndicatorEntity):
|
|
129
|
+
self.stix_objects.extend(stix_entity.stix_objects)
|
|
130
|
+
self._relate(stix_entity)
|
|
131
|
+
else:
|
|
132
|
+
self.stix_objects.append(stix_entity.stix_obj)
|
|
133
|
+
self._relate(stix_entity)
|
|
134
|
+
|
|
135
|
+
def _relate(self, obj: BaseStixEntity) -> None:
|
|
136
|
+
"""Creates relationship between object and indicator/observabe.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
obj (RFBaseStixEntity): Stix object we're linking to
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
STIX2TransformError: Generic transofmr error
|
|
143
|
+
"""
|
|
144
|
+
if isinstance(obj, IndicatorEntity):
|
|
145
|
+
sources = []
|
|
146
|
+
targets = []
|
|
147
|
+
if self.indicator:
|
|
148
|
+
sources.append(self.indicator)
|
|
149
|
+
if self.observable:
|
|
150
|
+
sources.append(self.observable)
|
|
151
|
+
if obj.indicator:
|
|
152
|
+
targets.append(obj.indicator)
|
|
153
|
+
if obj.observable:
|
|
154
|
+
targets.append(obj.observable)
|
|
155
|
+
self._append_indicator_relationships(sources, targets)
|
|
156
|
+
elif isinstance(obj, BaseStixEntity):
|
|
157
|
+
if self.indicator:
|
|
158
|
+
self.stix_objects.append(
|
|
159
|
+
Relationship(
|
|
160
|
+
source=self.indicator.id,
|
|
161
|
+
target=obj.stix_obj.id,
|
|
162
|
+
type_='indicates',
|
|
163
|
+
author=self.author,
|
|
164
|
+
).stix_obj,
|
|
165
|
+
)
|
|
166
|
+
if self.observable:
|
|
167
|
+
self.stix_objects.append(
|
|
168
|
+
Relationship(
|
|
169
|
+
source=self.observable.id,
|
|
170
|
+
target=obj.stix_obj.id,
|
|
171
|
+
type_='related-to',
|
|
172
|
+
author=self.author,
|
|
173
|
+
).stix_obj,
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
raise STIX2TransformError('Cannot transform, entity is not of correct type')
|
|
177
|
+
|
|
178
|
+
def _append_indicator_relationships(self, sources, targets) -> None:
|
|
179
|
+
for source in sources:
|
|
180
|
+
for target in targets:
|
|
181
|
+
self.stix_objects.append(
|
|
182
|
+
Relationship(
|
|
183
|
+
source=source.id,
|
|
184
|
+
target=target.id,
|
|
185
|
+
type_='related-to',
|
|
186
|
+
author=self.author,
|
|
187
|
+
).stix_obj,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _add_ttp(self, ttp: dict) -> None:
|
|
191
|
+
"""Adds a TTP from risk mapping.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
ttp (dict): maps a risk rule to one or more T codes
|
|
195
|
+
"""
|
|
196
|
+
ttp = ttp if isinstance(ttp, dict) else ttp.json()
|
|
197
|
+
for cat in ttp.get('categories', []):
|
|
198
|
+
if cat['framework'] == 'MITRE':
|
|
199
|
+
obj = TTP(cat['name'])
|
|
200
|
+
self.stix_objects.append(obj.stix_obj)
|
|
201
|
+
self._relate(obj)
|
|
202
|
+
|
|
203
|
+
def _add_rule(self, rule: dict) -> None:
|
|
204
|
+
"""Convert risk rule + evidence detail to notes.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
rule (dict): Risk rule + evidence details json blobs
|
|
208
|
+
"""
|
|
209
|
+
rule = rule if isinstance(rule, dict) else rule.json()
|
|
210
|
+
refs = []
|
|
211
|
+
if self.indicator:
|
|
212
|
+
refs.append(self.indicator.id)
|
|
213
|
+
if self.observable:
|
|
214
|
+
refs.append(self.observable.id)
|
|
215
|
+
|
|
216
|
+
# in risklists and enrichment 'rule' has different capitalization
|
|
217
|
+
rule_name = rule.get('Rule') or rule.get('rule')
|
|
218
|
+
|
|
219
|
+
if not rule_name:
|
|
220
|
+
raise STIX2TransformError('Cannot transform, rule name is missing')
|
|
221
|
+
|
|
222
|
+
self.stix_objects.append(
|
|
223
|
+
NoteEntity(
|
|
224
|
+
name=rule_name,
|
|
225
|
+
content=(rule.get('evidenceString') or rule.get('EvidenceString')),
|
|
226
|
+
object_refs=refs,
|
|
227
|
+
author=self.author,
|
|
228
|
+
).stix_obj,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _add_risk_score_as_note(self, risk_score: int) -> None:
|
|
232
|
+
"""Add Confidende/Risk Score as a note.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
risk_score (int): Confidence score
|
|
236
|
+
"""
|
|
237
|
+
if not risk_score:
|
|
238
|
+
raise STIX2TransformError('Cannot transform, confidence is missing')
|
|
239
|
+
object_refs = []
|
|
240
|
+
if self.indicator:
|
|
241
|
+
object_refs.append(self.indicator.id)
|
|
242
|
+
if self.observable:
|
|
243
|
+
object_refs.append(self.observable.id)
|
|
244
|
+
self.stix_objects.append(
|
|
245
|
+
NoteEntity(
|
|
246
|
+
name='Recorded Future Risk Score',
|
|
247
|
+
content=f'{risk_score}/99',
|
|
248
|
+
object_refs=object_refs,
|
|
249
|
+
author=self.author,
|
|
250
|
+
).stix_obj,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def bundle(self) -> stix2.v21.Bundle:
|
|
255
|
+
"""Creates STIX2 bundle.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
stix2.v21.Bundle: Bundle
|
|
259
|
+
"""
|
|
260
|
+
self.stix_objects.append(self.author)
|
|
261
|
+
return stix2.v21.Bundle(objects=self.stix_objects, allow_custom=True)
|
psengine/stix2/errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from ..errors import RecordedFutureError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class STIX2TransformError(RecordedFutureError):
|
|
18
|
+
"""Error raised when invalid parameters are passed to the STIX2 module."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnsupportedConversionTypeError(STIX2TransformError):
|
|
22
|
+
"""Error raised when client tries to convert an Recorded FUture type that is not supported."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from .complex_entity import IndicatorEntity
|
|
15
|
+
from .constants import CONVERT_ENTITY_KWARGS, INDICATOR_TYPES
|
|
16
|
+
from .errors import STIX2TransformError, UnsupportedConversionTypeError
|
|
17
|
+
from .simple_entity import TTP, Identity, Malware, Vulnerability
|
|
18
|
+
|
|
19
|
+
SIMPLE_ENTITY_MAP = {
|
|
20
|
+
'MitreAttackIdentifier': TTP,
|
|
21
|
+
'Company': Identity,
|
|
22
|
+
'Person': Identity,
|
|
23
|
+
'Organization': Identity,
|
|
24
|
+
'Malware': Malware,
|
|
25
|
+
'CyberVulnerability': Vulnerability,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def convert_entity(
|
|
30
|
+
name: str,
|
|
31
|
+
entity_type: str,
|
|
32
|
+
create_indicator: bool = True,
|
|
33
|
+
create_obs: bool = False,
|
|
34
|
+
**kwargs,
|
|
35
|
+
):
|
|
36
|
+
"""Converts RF entity to STIX2.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name (str): Name of entity
|
|
40
|
+
entity_type (str): Recorded Future type of entity
|
|
41
|
+
create_indicator (bool, optional): flag to determine if create indicator
|
|
42
|
+
create_obs (bool, optional): flag to determine if create observable
|
|
43
|
+
**kwargs: Any other fields that can be used in an entity.
|
|
44
|
+
Must be one of CONVERT_ENTITY_KWARGS
|
|
45
|
+
|
|
46
|
+
No Longer Returned:
|
|
47
|
+
A subclass of RFBaseEntity.
|
|
48
|
+
"""
|
|
49
|
+
for key in kwargs:
|
|
50
|
+
if key not in CONVERT_ENTITY_KWARGS:
|
|
51
|
+
msg = f'{key} is invalid keyword argument for convert_entity. Only {CONVERT_ENTITY_KWARGS} are accepted' # noqa: E501
|
|
52
|
+
raise STIX2TransformError(msg)
|
|
53
|
+
if entity_type in INDICATOR_TYPES:
|
|
54
|
+
return IndicatorEntity(
|
|
55
|
+
name=name,
|
|
56
|
+
type_=entity_type,
|
|
57
|
+
create_indicator=create_indicator,
|
|
58
|
+
create_obs=create_obs,
|
|
59
|
+
)
|
|
60
|
+
elif entity_type in SIMPLE_ENTITY_MAP:
|
|
61
|
+
ent = SIMPLE_ENTITY_MAP[entity_type]
|
|
62
|
+
if ent == Identity:
|
|
63
|
+
return ent(name, rf_type=entity_type)
|
|
64
|
+
return SIMPLE_ENTITY_MAP[entity_type](name, **kwargs)
|
|
65
|
+
else:
|
|
66
|
+
raise UnsupportedConversionTypeError(
|
|
67
|
+
f'Could not convert entity {name} because type {entity_type} is not supported',
|
|
68
|
+
)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from stix2 import Bundle, Identity, Report
|
|
18
|
+
from stix2.exceptions import (
|
|
19
|
+
ExtraPropertiesError,
|
|
20
|
+
InvalidValueError,
|
|
21
|
+
MissingPropertiesError,
|
|
22
|
+
STIXError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from ..analyst_notes import AnalystNote
|
|
26
|
+
from ..risklists.models import DefaultRiskList
|
|
27
|
+
from .complex_entity import DetectionRuleEntity, IndicatorEntity
|
|
28
|
+
from .constants import ENTITY_TYPE_MAP, REPORT_TYPE_MAPPER, SUPPORTED_HUNTING_RULES, TLP_MAP
|
|
29
|
+
from .enriched_indicator import EnrichedIndicator
|
|
30
|
+
from .errors import STIX2TransformError, UnsupportedConversionTypeError
|
|
31
|
+
from .helpers import convert_entity
|
|
32
|
+
from .util import create_rf_author
|
|
33
|
+
|
|
34
|
+
LOG = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RFBundle:
|
|
38
|
+
"""Class for creating STIX2 bundles from Recorded Future objects."""
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_default_risklist(
|
|
42
|
+
cls,
|
|
43
|
+
risklist: list[DefaultRiskList],
|
|
44
|
+
entity_type: str,
|
|
45
|
+
identity: Identity = None,
|
|
46
|
+
) -> Bundle:
|
|
47
|
+
"""Creates STIX2 bundle from a Recorded Future default risklist.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
risklist (str): Recorded Future default risklist (contains the standard 5 columns)
|
|
51
|
+
entity_type (str): entity type
|
|
52
|
+
identity (_type_, optional): Author identity. Defaults to Recorded Future
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
STIX2TransformError: if the risklist is not valid
|
|
56
|
+
STIX2TransformError: if EvidenceDetails is not valid JSON
|
|
57
|
+
STIX2TransformError: if the bundle cannot be created
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Bundle: STIX2 bundle
|
|
61
|
+
"""
|
|
62
|
+
if not identity:
|
|
63
|
+
identity = create_rf_author()
|
|
64
|
+
objects = [identity]
|
|
65
|
+
LOG.info(f'Creating STIX2 bundle from {entity_type} risklist')
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
enriched_indicators = []
|
|
69
|
+
for ioc in risklist:
|
|
70
|
+
indicator = EnrichedIndicator(
|
|
71
|
+
name=ioc.ioc,
|
|
72
|
+
type_=ENTITY_TYPE_MAP[entity_type],
|
|
73
|
+
confidence=ioc.risk_score,
|
|
74
|
+
evidence_details=ioc.evidence_details,
|
|
75
|
+
)
|
|
76
|
+
enriched_indicators.append(indicator)
|
|
77
|
+
|
|
78
|
+
except KeyError as ke:
|
|
79
|
+
raise STIX2TransformError(f'Risklist missing header: {ke}') from ke
|
|
80
|
+
|
|
81
|
+
except json.JSONDecodeError as jse:
|
|
82
|
+
raise STIX2TransformError(f'EvidenceDetails is not valid JSON: {jse}') from jse
|
|
83
|
+
|
|
84
|
+
for i in enriched_indicators:
|
|
85
|
+
objects.extend(i.stix_objects)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
bundle = Bundle(objects=objects, allow_custom=True)
|
|
89
|
+
except (
|
|
90
|
+
ValueError,
|
|
91
|
+
ExtraPropertiesError,
|
|
92
|
+
InvalidValueError,
|
|
93
|
+
MissingPropertiesError,
|
|
94
|
+
STIXError,
|
|
95
|
+
) as e:
|
|
96
|
+
raise STIX2TransformError(f'Failed to create STIX2 bundle from risklist. {e}') from e
|
|
97
|
+
|
|
98
|
+
return bundle
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_analyst_note(
|
|
102
|
+
cls,
|
|
103
|
+
note: AnalystNote,
|
|
104
|
+
attachment: bytes = None,
|
|
105
|
+
split_snort: bool = False,
|
|
106
|
+
identity: Identity = None,
|
|
107
|
+
) -> Bundle:
|
|
108
|
+
"""Creates a STIX2 bundle from a Recorded Future analyst note.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
note (AnalystNote): Recorded Future analyst note
|
|
112
|
+
attachment (bytes, optional): Note attachment. Defaults to None.
|
|
113
|
+
split_snort (bool, optional): Split snort rules into separate DetectionRule objects.
|
|
114
|
+
Defaults to False.
|
|
115
|
+
identity (stix2.Identity, optional): Author. Defaults to Recorded Future.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Bundle: STIX2 bundle
|
|
119
|
+
"""
|
|
120
|
+
LOG.info(f'Creating STIX2 bundle from analyst note {note.id_}')
|
|
121
|
+
|
|
122
|
+
if not identity:
|
|
123
|
+
identity = create_rf_author()
|
|
124
|
+
objects = [identity]
|
|
125
|
+
topics = [topic.name for topic in note.attributes.topic]
|
|
126
|
+
report_types = _create_report_types(topics)
|
|
127
|
+
for entity in note.attributes.note_entities:
|
|
128
|
+
try:
|
|
129
|
+
stix_entity = convert_entity(entity.name, entity.type_)
|
|
130
|
+
if isinstance(stix_entity, IndicatorEntity):
|
|
131
|
+
objects.extend(stix_entity.stix_objects)
|
|
132
|
+
else:
|
|
133
|
+
objects.append(stix_entity.stix_obj)
|
|
134
|
+
|
|
135
|
+
except UnsupportedConversionTypeError as err: # noqa: PERF203
|
|
136
|
+
LOG.warning(str(err) + '. Skipping...')
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if attachment and note.detection_rule_type in SUPPORTED_HUNTING_RULES:
|
|
140
|
+
# This is a workaround for OpenCTI
|
|
141
|
+
# OpenCTI does not support multiple Snort rules within a single DetectionRule object
|
|
142
|
+
# so we split them into separate objects (split_snort = True)
|
|
143
|
+
if note.detection_rule_type == 'snort' and split_snort is True:
|
|
144
|
+
objects.extend(_split_snort_rules(note, str(attachment, 'UTF-8')))
|
|
145
|
+
else:
|
|
146
|
+
rule = DetectionRuleEntity(
|
|
147
|
+
name=note.attributes.title,
|
|
148
|
+
type_=note.detection_rule_type,
|
|
149
|
+
content=str(attachment, 'UTF-8'),
|
|
150
|
+
)
|
|
151
|
+
objects.append(rule.stix_obj)
|
|
152
|
+
|
|
153
|
+
external_references = _generate_external_references(note.attributes.validation_urls)
|
|
154
|
+
external_references.append(
|
|
155
|
+
{
|
|
156
|
+
'source_name': 'Recorded Future',
|
|
157
|
+
'url': note.portal_url,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
report = Report(
|
|
162
|
+
name=note.attributes.title,
|
|
163
|
+
created_by_ref=identity.id,
|
|
164
|
+
description=note.attributes.text,
|
|
165
|
+
published=note.attributes.published,
|
|
166
|
+
labels=topics,
|
|
167
|
+
report_types=report_types,
|
|
168
|
+
object_refs=[obj.id for obj in objects],
|
|
169
|
+
object_marking_refs=TLP_MAP['amber'],
|
|
170
|
+
external_references=external_references,
|
|
171
|
+
)
|
|
172
|
+
objects.append(report)
|
|
173
|
+
|
|
174
|
+
bundle = Bundle(objects=objects, allow_custom=True)
|
|
175
|
+
|
|
176
|
+
return bundle
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Helpers for bundle creation
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _split_snort_rules(note: AnalystNote, attachment: str) -> list:
|
|
183
|
+
"""Splits snort rules into multiple DetectionRule objects.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
note (AnalystNote): AnalystNote object
|
|
187
|
+
attachment (str): snort rule payload
|
|
188
|
+
"""
|
|
189
|
+
rules = []
|
|
190
|
+
ctr = 1
|
|
191
|
+
temp_description = []
|
|
192
|
+
for line in attachment.split('\n'):
|
|
193
|
+
line = line.strip()
|
|
194
|
+
|
|
195
|
+
# skip comments and empty lines
|
|
196
|
+
if line.startswith('#') or not line:
|
|
197
|
+
temp_description.append(line[1:].strip())
|
|
198
|
+
continue
|
|
199
|
+
rules.append(
|
|
200
|
+
DetectionRuleEntity(
|
|
201
|
+
name=note.attributes.title + f', Rule {ctr}',
|
|
202
|
+
type_=note.detection_rule_type,
|
|
203
|
+
content=line,
|
|
204
|
+
description='\n'.join(temp_description),
|
|
205
|
+
).stix_obj,
|
|
206
|
+
)
|
|
207
|
+
# reset description for next rule
|
|
208
|
+
temp_description = []
|
|
209
|
+
ctr += 1
|
|
210
|
+
return rules
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _create_report_types(topics: list) -> list:
|
|
214
|
+
"""Map topics to STIX2 report types.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
topics (list): List of topics to map
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
list: List of STIX2 report types
|
|
221
|
+
"""
|
|
222
|
+
ret = set()
|
|
223
|
+
for topic in topics:
|
|
224
|
+
if topic not in REPORT_TYPE_MAPPER:
|
|
225
|
+
LOG.warning(f'Could not map a report type for type {topic}')
|
|
226
|
+
continue
|
|
227
|
+
ret.add(REPORT_TYPE_MAPPER[topic])
|
|
228
|
+
return list(ret)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _generate_external_references(urls: list) -> list:
|
|
232
|
+
"""Generate External references from validation urls."""
|
|
233
|
+
refs = []
|
|
234
|
+
if urls is None:
|
|
235
|
+
return refs
|
|
236
|
+
|
|
237
|
+
for url in urls:
|
|
238
|
+
source_name = url.name.split('/')[2].split('.')[-2].capitalize()
|
|
239
|
+
refs.append({'source_name': source_name, 'url': url})
|
|
240
|
+
return refs
|