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.
Files changed (115) hide show
  1. psengine/__init__.py +22 -0
  2. psengine/_sdk_id.py +16 -0
  3. psengine/_version.py +14 -0
  4. psengine/analyst_notes/__init__.py +32 -0
  5. psengine/analyst_notes/constants.py +15 -0
  6. psengine/analyst_notes/errors.py +42 -0
  7. psengine/analyst_notes/helpers.py +90 -0
  8. psengine/analyst_notes/models.py +219 -0
  9. psengine/analyst_notes/note.py +149 -0
  10. psengine/analyst_notes/note_mgr.py +400 -0
  11. psengine/base_http_client.py +285 -0
  12. psengine/classic_alerts/__init__.py +24 -0
  13. psengine/classic_alerts/classic_alert.py +275 -0
  14. psengine/classic_alerts/classic_alert_mgr.py +507 -0
  15. psengine/classic_alerts/constants.py +31 -0
  16. psengine/classic_alerts/errors.py +38 -0
  17. psengine/classic_alerts/helpers.py +87 -0
  18. psengine/classic_alerts/markdown/__init__.py +13 -0
  19. psengine/classic_alerts/markdown/markdown.py +359 -0
  20. psengine/classic_alerts/models.py +141 -0
  21. psengine/collective_insights/__init__.py +29 -0
  22. psengine/collective_insights/collective_insights.py +164 -0
  23. psengine/collective_insights/constants.py +44 -0
  24. psengine/collective_insights/errors.py +18 -0
  25. psengine/collective_insights/insight.py +89 -0
  26. psengine/collective_insights/models.py +81 -0
  27. psengine/common_models.py +89 -0
  28. psengine/config/__init__.py +15 -0
  29. psengine/config/config.py +284 -0
  30. psengine/config/errors.py +18 -0
  31. psengine/constants.py +63 -0
  32. psengine/detection/__init__.py +17 -0
  33. psengine/detection/detection_mgr.py +135 -0
  34. psengine/detection/detection_rule.py +85 -0
  35. psengine/detection/errors.py +26 -0
  36. psengine/detection/helpers.py +56 -0
  37. psengine/detection/models.py +47 -0
  38. psengine/endpoints.py +98 -0
  39. psengine/enrich/__init__.py +28 -0
  40. psengine/enrich/constants.py +73 -0
  41. psengine/enrich/errors.py +26 -0
  42. psengine/enrich/lookup.py +299 -0
  43. psengine/enrich/lookup_mgr.py +341 -0
  44. psengine/enrich/models/__init__.py +13 -0
  45. psengine/enrich/models/base_enriched_entity.py +43 -0
  46. psengine/enrich/models/lookup.py +271 -0
  47. psengine/enrich/models/soar.py +138 -0
  48. psengine/enrich/soar.py +89 -0
  49. psengine/enrich/soar_mgr.py +176 -0
  50. psengine/entity_lists/__init__.py +16 -0
  51. psengine/entity_lists/constants.py +19 -0
  52. psengine/entity_lists/entity_list.py +435 -0
  53. psengine/entity_lists/entity_list_mgr.py +185 -0
  54. psengine/entity_lists/errors.py +26 -0
  55. psengine/entity_lists/models.py +87 -0
  56. psengine/entity_match/__init__.py +16 -0
  57. psengine/entity_match/entity_match.py +90 -0
  58. psengine/entity_match/entity_match_mgr.py +235 -0
  59. psengine/entity_match/errors.py +18 -0
  60. psengine/entity_match/models.py +22 -0
  61. psengine/errors.py +41 -0
  62. psengine/helpers/__init__.py +23 -0
  63. psengine/helpers/helpers.py +471 -0
  64. psengine/logger/__init__.py +15 -0
  65. psengine/logger/constants.py +39 -0
  66. psengine/logger/errors.py +18 -0
  67. psengine/logger/rf_logger.py +148 -0
  68. psengine/markdown/__init__.py +21 -0
  69. psengine/markdown/markdown.py +169 -0
  70. psengine/markdown/models.py +22 -0
  71. psengine/playbook_alerts/__init__.py +34 -0
  72. psengine/playbook_alerts/constants.py +35 -0
  73. psengine/playbook_alerts/errors.py +35 -0
  74. psengine/playbook_alerts/helpers.py +80 -0
  75. psengine/playbook_alerts/mappings.py +44 -0
  76. psengine/playbook_alerts/markdown/__init__.py +13 -0
  77. psengine/playbook_alerts/markdown/markdown.py +98 -0
  78. psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
  79. psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
  80. psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
  81. psengine/playbook_alerts/models/__init__.py +36 -0
  82. psengine/playbook_alerts/models/common_models.py +18 -0
  83. psengine/playbook_alerts/models/panel_log.py +329 -0
  84. psengine/playbook_alerts/models/panel_status.py +70 -0
  85. psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
  86. psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
  87. psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
  88. psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
  89. psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
  90. psengine/playbook_alerts/models/search_endpoint.py +68 -0
  91. psengine/playbook_alerts/pa_category.py +37 -0
  92. psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
  93. psengine/playbook_alerts/playbook_alerts.py +393 -0
  94. psengine/rf_client.py +430 -0
  95. psengine/risklists/__init__.py +17 -0
  96. psengine/risklists/constants.py +15 -0
  97. psengine/risklists/errors.py +20 -0
  98. psengine/risklists/models.py +65 -0
  99. psengine/risklists/risklist_mgr.py +156 -0
  100. psengine/stix2/__init__.py +21 -0
  101. psengine/stix2/base_stix_entity.py +62 -0
  102. psengine/stix2/complex_entity.py +372 -0
  103. psengine/stix2/constants.py +81 -0
  104. psengine/stix2/enriched_indicator.py +261 -0
  105. psengine/stix2/errors.py +22 -0
  106. psengine/stix2/helpers.py +68 -0
  107. psengine/stix2/rf_bundle.py +240 -0
  108. psengine/stix2/simple_entity.py +145 -0
  109. psengine/stix2/util.py +53 -0
  110. psengine-2.0.4.dist-info/METADATA +189 -0
  111. psengine-2.0.4.dist-info/RECORD +115 -0
  112. psengine-2.0.4.dist-info/WHEEL +5 -0
  113. psengine-2.0.4.dist-info/entry_points.txt +2 -0
  114. psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
  115. 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)
@@ -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