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,156 @@
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 csv
15
+ import logging
16
+ from collections.abc import Generator
17
+ from typing import Any, Optional, Union
18
+
19
+ from pydantic import BaseModel, validate_call
20
+ from requests.exceptions import (
21
+ ConnectionError,
22
+ ConnectTimeout,
23
+ HTTPError,
24
+ JSONDecodeError,
25
+ ReadTimeout,
26
+ SSLError,
27
+ )
28
+
29
+ from ..endpoints import EP_FUSION_FILES, EP_RISKLIST
30
+ from ..helpers import debug_call
31
+ from ..rf_client import RFClient
32
+ from .constants import DEFAULT_RISKLIST_FORMAT
33
+ from .errors import RiskListNotAvailableError
34
+
35
+
36
+ class RisklistMgr:
37
+ """Manages requests for Recorded Future risk lists."""
38
+
39
+ def __init__(self, rf_token: str = None):
40
+ """Initializes the RiskListMgr object.
41
+
42
+ Args:
43
+ rf_token (str, optional): Recorded Future API token. Defaults to None
44
+ """
45
+ self.log = logging.getLogger(__name__)
46
+ self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
47
+
48
+ @debug_call
49
+ @validate_call
50
+ def fetch_risklist(
51
+ self,
52
+ list: str, # noqa: A002
53
+ entity_type: str = None,
54
+ format: str = None, # noqa: A002
55
+ headers: bool = True,
56
+ validate: Optional[Any] = None,
57
+ ) -> Generator[Union[dict, list[str], BaseModel], None, None]:
58
+ """Get a Recorded Future RiskList. Specify a fusion_path to get a custom
59
+ risklist instead - format field is ignored when custom risklists are used.
60
+
61
+ Gotchas:
62
+
63
+ - If a specified list does not exist, RF API returns the default risklist
64
+ - An empty risklist can be returned.
65
+ - If ``validate`` is None: If it has headers, they will be returned.
66
+ - If ``validate`` is not None, an empty list will be returned.
67
+
68
+ Args:
69
+ list (str): name of the risklist to download.
70
+ entity_type (str, optional): Type of entity to get risklist for.
71
+ format (str, optional): Format of the risklist.
72
+ headers (bool, optional): Whether headers are included in the CSV.
73
+ validate (BaseModel, optional): Validation model to use. Has to be subclass of pydantic
74
+ BaseModel.
75
+
76
+ Raises:
77
+ RisklistNotAvailableError: if HTTP error occurs on risklist fetch
78
+ ValidationError if any supplied parameter is of incorrect type
79
+
80
+ Returns:
81
+ Generator: Yields risklist rows or validated risklist models.
82
+ """
83
+ if validate and not issubclass(validate, BaseModel):
84
+ raise ValueError('`validate` should be a subclass of Pydantic BaseModel or None')
85
+
86
+ format = format if format else DEFAULT_RISKLIST_FORMAT # noqa: A001
87
+ risklist_type, url, params = self._get_risklist_url_and_params(list, entity_type, format)
88
+
89
+ if risklist_type == 'fusion' and list.endswith('json'):
90
+ return self._fetch_json_risklist(url, params, validate)
91
+ return self._fetch_csv_risklist(url, params, validate, headers)
92
+
93
+ @debug_call
94
+ def _fetch_csv_risklist(
95
+ self, url, params, validate, headers
96
+ ) -> Generator[Union[dict, BaseModel, list[str]], None, None]:
97
+ try:
98
+ response = self.rf_client.request('get', url, params=params)
99
+ response.raise_for_status()
100
+ except (
101
+ HTTPError,
102
+ ConnectTimeout,
103
+ ConnectionError,
104
+ ReadTimeout,
105
+ OSError,
106
+ SSLError,
107
+ KeyError,
108
+ ) as e:
109
+ raise RiskListNotAvailableError(message=str(e)) from e
110
+
111
+ lines = response.iter_lines(decode_unicode=True)
112
+ if headers:
113
+ reader = csv.DictReader(lines)
114
+ for row in reader:
115
+ if validate:
116
+ yield validate(**row)
117
+ else:
118
+ yield row
119
+ else:
120
+ reader = csv.reader(lines)
121
+ yield from reader
122
+
123
+ @debug_call
124
+ def _fetch_json_risklist(
125
+ self, url, params, validate
126
+ ) -> Generator[Union[dict, BaseModel], None, None]:
127
+ try:
128
+ response = self.rf_client.request('get', url, params=params)
129
+ response.raise_for_status()
130
+ response = response.json()
131
+ except (
132
+ HTTPError,
133
+ ConnectTimeout,
134
+ ConnectionError,
135
+ ReadTimeout,
136
+ OSError,
137
+ SSLError,
138
+ JSONDecodeError,
139
+ ) as e:
140
+ raise RiskListNotAvailableError(message=str(e)) from e
141
+
142
+ if validate:
143
+ for row in response:
144
+ yield validate(**row)
145
+ else:
146
+ yield from response
147
+
148
+ def _get_risklist_url_and_params(self, filename: str, entity_type: str, format_type: str):
149
+ """Helper function to determine URL and parameters based on entity type."""
150
+ if entity_type:
151
+ return (
152
+ 'risklist',
153
+ EP_RISKLIST.format(entity_type),
154
+ {'format': format_type, 'list': filename},
155
+ )
156
+ return 'fusion', EP_FUSION_FILES, {'path': filename}
@@ -0,0 +1,21 @@
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 .base_stix_entity import BaseStixEntity
15
+ from .complex_entity import DetectionRuleEntity, Grouping, IndicatorEntity, NoteEntity, Relationship
16
+ from .constants import ENTITY_TYPE_MAP
17
+ from .enriched_indicator import EnrichedIndicator
18
+ from .errors import STIX2TransformError
19
+ from .helpers import convert_entity
20
+ from .rf_bundle import RFBundle
21
+ from .simple_entity import TTP, Identity, IntrusionSet, Malware, ThreatActor, Vulnerability
@@ -0,0 +1,62 @@
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 stix2
15
+
16
+ from .util import create_rf_author, generate_uuid
17
+
18
+
19
+ class BaseStixEntity:
20
+ """Base STIX entity class for Recorded Future entities."""
21
+
22
+ def __init__(self, name: str, author: stix2.Identity = None) -> None:
23
+ """Initializes base STIX entity.
24
+
25
+ Args:
26
+ name (str): Name of entity
27
+ author (stix2.Identity): Recorded Future Identity object
28
+
29
+ """
30
+ self.name = name
31
+ if not author:
32
+ author = self._create_author()
33
+ self.author = author
34
+ self.stix_obj = None
35
+ self.create_stix_object()
36
+
37
+ def __str__(self) -> str:
38
+ """String representation of entity.
39
+
40
+ Returns:
41
+ str: String representation of entity
42
+ """
43
+ return f'Base STIX Entity: {self.name}, Author Name: {self.author.name}'
44
+
45
+ def create_stix_object(self) -> None:
46
+ """Creates STIX objects from object attributes."""
47
+
48
+ def _create_author(self) -> stix2.Identity:
49
+ """Creates author object if it doesn't already exist."""
50
+ return create_rf_author()
51
+
52
+ def __eq__(self, other) -> bool:
53
+ """Verify if two STIX Objects are the same."""
54
+ return self._generate_id() == other._generate_id()
55
+
56
+ def __hash__(self) -> int:
57
+ """Hash for set function."""
58
+ return hash(self._generate_id())
59
+
60
+ def _generate_id(self) -> str:
61
+ """Generates an ID."""
62
+ return 'invalid-prefix--' + generate_uuid(name=self.name)
@@ -0,0 +1,372 @@
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
+ from datetime import datetime
14
+ from typing import Union
15
+
16
+ import stix2
17
+
18
+ from ..constants import INDICATOR_INTEL_CARD_URL
19
+ from .base_stix_entity import BaseStixEntity
20
+ from .constants import (
21
+ CONVERTED_TYPES,
22
+ INDICATOR_TYPE_TO_RF_PORTAL_MAP,
23
+ INDICATOR_TYPES,
24
+ SUPPORTED_HUNTING_RULES,
25
+ TLP_MAP,
26
+ )
27
+ from .errors import STIX2TransformError
28
+ from .util import generate_uuid
29
+
30
+
31
+ class DetectionRuleEntity(BaseStixEntity):
32
+ """Represents a Yara or SNORT rule."""
33
+
34
+ def __init__(
35
+ self,
36
+ name: str,
37
+ type_: str,
38
+ content: str,
39
+ description: str = None,
40
+ author: stix2.Identity = None,
41
+ ) -> None:
42
+ """Detection Rule.
43
+
44
+ Args:
45
+ name (str): Name of Detection Rule
46
+ type_ (str): Detection rule type (Yara or Sigma)
47
+ content (str): Hunting rule itself, usually either yara, snort or sigma
48
+ description (str, optional): Description of Detection Rule
49
+ author (stix2.Identity): Recorded Future author
50
+
51
+ Raises:
52
+ STIX2TransformError: Description
53
+ """
54
+ self.name = name.split('.')[0]
55
+ self.type = type_
56
+ self.content = content
57
+ self.description = description
58
+ self.stix_obj = None
59
+
60
+ if self.type not in SUPPORTED_HUNTING_RULES:
61
+ msg = f'Detection rule of type {self.type} is not supported'
62
+ raise STIX2TransformError(msg)
63
+ super().__init__(name, author)
64
+
65
+ def create_stix_object(self) -> None:
66
+ """Creates STIX objects from object attributes."""
67
+ self.stix_obj = stix2.Indicator(
68
+ id=self._generate_id(),
69
+ name=self.name,
70
+ description=self.description,
71
+ pattern_type=self.type,
72
+ pattern=self.content,
73
+ valid_from=datetime.now(),
74
+ created_by_ref=self.author.id,
75
+ )
76
+
77
+ def _generate_id(self) -> str:
78
+ """Generates an ID."""
79
+ return 'indicator--' + generate_uuid(name=self.name, content=self.content, type=self.type)
80
+
81
+
82
+ class Grouping(BaseStixEntity):
83
+ """Explicitly asserts that the referenced STIX Objects
84
+ have a shared context, unlike a STIX Bundle (which explicitly
85
+ conveys no context).
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ name: str,
91
+ description: str = None,
92
+ is_malware: bool = False,
93
+ is_suspicious=False,
94
+ object_refs: list = None,
95
+ author: stix2.Identity = None,
96
+ ):
97
+ """Grouping of STIX2 objects. Usually as part of the same event.
98
+
99
+ Args:
100
+ name (str): Name of the event. Should be unique
101
+ description (str, optional): Description, Usually empty
102
+ is_malware (bool, optional):
103
+ flag to determine if malware-analysis context should be used
104
+ is_suspicious (bool, optional):
105
+ Flag to determine is suspicious-activity flag should be used
106
+ object_refs (list, optional): List of objects to group together
107
+ author (stix2.Identity, optional): Recorded Future Identity
108
+ """
109
+ self.name = name
110
+ self.description = description
111
+ if is_malware:
112
+ self.context = 'malware-analysis'
113
+ elif is_suspicious:
114
+ self.context = 'suspicious-activity'
115
+ else:
116
+ self.context = 'unspecified'
117
+ self.object_refs = object_refs or []
118
+ self.stix_obj = None
119
+ super().__init__(name, author)
120
+
121
+ def create_stix_object(self) -> None:
122
+ """Creates STIX objects from object attributes."""
123
+ self.stix_obj = stix2.Grouping(
124
+ id=self._generate_id(),
125
+ name=self.name,
126
+ description=self.description,
127
+ context=self.context,
128
+ object_refs=self.object_refs,
129
+ created_by_ref=self.author.id,
130
+ )
131
+
132
+ def _generate_id(self) -> str:
133
+ """Generates an ID."""
134
+ desc = self.description if self.description is not None else ''
135
+ return 'grouping--' + generate_uuid(name=self.name, description=desc)
136
+
137
+
138
+ class Relationship(BaseStixEntity):
139
+ """Represents Relationship SDO."""
140
+
141
+ def __init__(self, source: str, target: str, type_: str, author: stix2.Identity = None) -> None:
142
+ """Relationship.
143
+
144
+ Args:
145
+ source (str): source of relationship
146
+ target (str): target of relationship
147
+ type_ (str): how the source relates to target
148
+ author (stix2.Identity, optional): Recorded Future Identity
149
+ """
150
+ self.source = source
151
+ self.target = target
152
+ self.type_ = type_
153
+ self.stix_obj = None
154
+ super().__init__(None, author)
155
+
156
+ def create_stix_object(self) -> None:
157
+ """Creates the Relationship object."""
158
+ self.stix_obj = stix2.Relationship(
159
+ id=self._generate_id(),
160
+ relationship_type=self.type_,
161
+ source_ref=self.source,
162
+ target_ref=self.target,
163
+ created_by_ref=self.author.id,
164
+ )
165
+
166
+ def _generate_id(self) -> str:
167
+ """Generates an ID."""
168
+ return 'relationship--' + generate_uuid(
169
+ source=self.source,
170
+ target=self.target,
171
+ type=self.type_,
172
+ )
173
+
174
+
175
+ class NoteEntity(BaseStixEntity):
176
+ """Note SDO."""
177
+
178
+ def __init__(
179
+ self,
180
+ name: str,
181
+ content: str,
182
+ object_refs: list,
183
+ author: stix2.Identity = None,
184
+ ) -> None:
185
+ """Note Entity.
186
+
187
+ Args:
188
+ name (str): Title of Note
189
+ content (str): Content/text of note
190
+ object_refs (list[str]): List of SDO IDs note should be attached to.
191
+ author (stix2.Identity, optional): Recorded Future Identity
192
+ """
193
+ self.content = content
194
+ self.object_refs = object_refs
195
+ self.stix_obj = None
196
+ super().__init__(name, author)
197
+
198
+ def create_stix_object(self) -> None:
199
+ """Creates the Note object."""
200
+ self.stix_obj = stix2.Note(
201
+ id=self._generate_id(),
202
+ abstract=self.name,
203
+ content=self.content,
204
+ object_refs=self.object_refs,
205
+ created_by_ref=self.author.id,
206
+ )
207
+
208
+ def _generate_id(self) -> str:
209
+ """Generates an ID."""
210
+ return 'note--' + generate_uuid(name=self.name, content=self.content)
211
+
212
+
213
+ class IndicatorEntity(BaseStixEntity):
214
+ """Indicator SDO."""
215
+
216
+ def __init__(
217
+ self,
218
+ name: str,
219
+ type_: str,
220
+ description: str = None,
221
+ author: stix2.Identity = None,
222
+ create_indicator: bool = True,
223
+ create_obs: bool = True,
224
+ confidence: int = None,
225
+ labels: list = None,
226
+ tlp_marking='amber',
227
+ ):
228
+ """Indicator container. Containers Indicator, observable, and relationship between them.
229
+
230
+ Args:
231
+ name (str): Indicator value
232
+ type_ (str): Recorded Future type of indicator. Options: 'IpAddress',
233
+ 'InternetDomainName', 'URL', 'FileHash'.
234
+ description (str, optional): Description of Indicator. Usually an AI Insight
235
+ author (stix2.Identity, optional): Recorded Future Identity
236
+ create_indicator (bool, optional): flag that governs if indicator should be created
237
+ create_obs (bool, optional): flag that governs if observable should be created
238
+ confidence (int, optional): Confidence score of indicator
239
+ labels (list, optional): labels applied to Indicator. Often risk rules
240
+ tlp_marking (str, optional): the TLP level. Default to amber
241
+
242
+
243
+ Raises:
244
+ STIX2TransformError: If indicator type is not supported
245
+ """
246
+ if not create_indicator and not create_obs:
247
+ raise STIX2TransformError(
248
+ 'Inidcator must create at least one of "Observable" or "Indicator"',
249
+ )
250
+
251
+ type_ = CONVERTED_TYPES.get(type_, type_)
252
+
253
+ if type_ not in INDICATOR_TYPES:
254
+ raise STIX2TransformError(
255
+ f'Indicator {name} of type {type_} not one of: {", ".join(INDICATOR_TYPES)}'
256
+ )
257
+
258
+ self.type = type_
259
+ if not author:
260
+ author = self._create_author()
261
+ self.author = author
262
+ self.name = name
263
+ self.confidence = confidence
264
+ self.description = description
265
+ self.labels = labels
266
+ self.tlp = tlp_marking
267
+ self.indicator = None
268
+ self.observable = None
269
+ self.relationship = None
270
+ self.stix_objects = []
271
+ if create_indicator:
272
+ self.indicator = self._generate_indicator()
273
+ self.stix_objects.append(self.indicator)
274
+ if create_obs:
275
+ self.observable = self._generate_observable()
276
+ self.stix_objects.append(self.observable)
277
+ if self.indicator and self.observable:
278
+ self.relationship = self._generate_relationship()
279
+ self.stix_objects.append(self.relationship)
280
+
281
+ def _generate_indicator(self) -> stix2.Indicator:
282
+ """Creates STIX2 Indicator Object."""
283
+ return stix2.Indicator(
284
+ id=self._generate_indicator_id(),
285
+ name=self.name,
286
+ confidence=self.confidence,
287
+ pattern_type='stix',
288
+ pattern=self._generate_pattern(),
289
+ created_by_ref=self.author.id,
290
+ labels=self.labels,
291
+ description=self.description,
292
+ object_marking_refs=TLP_MAP.get(self.tlp),
293
+ external_references=self._generate_external_references(),
294
+ )
295
+
296
+ def _generate_indicator_id(self) -> str:
297
+ """Creates an indicator ID string."""
298
+ return 'indicator--' + generate_uuid(name=self.name)
299
+
300
+ def _generate_pattern(self) -> str:
301
+ """Generates a stix2 pattern for indicators."""
302
+ if self.type == 'IpAddress':
303
+ if ':' in self.name:
304
+ return f"[ipv6-addr:value = '{self.name}']"
305
+ else:
306
+ return f"[ipv4-addr:value = '{self.name}']"
307
+ elif self.type == 'InternetDomainName':
308
+ return f"[domain-name:value = '{self.name}']"
309
+ elif self.type == 'URL':
310
+ ioc = self.name.replace('\\', '\\\\')
311
+ ioc = ioc.replace("'", "\\'")
312
+ return f"[url:value = '{ioc}']"
313
+ elif self.type == 'FileHash':
314
+ return f"[file:hashes.'{self._determine_algorithm()}' = '{self.name}']"
315
+
316
+ def _determine_algorithm(self) -> str:
317
+ """Determines Hash Algorithm."""
318
+ if len(self.name) == 64:
319
+ return 'SHA-256'
320
+ elif len(self.name) == 40:
321
+ return 'SHA-1'
322
+ elif len(self.name) == 32:
323
+ return 'MD5'
324
+ msg = (
325
+ f'Could not determine hash type for {self.name}. Only MD5, SHA1'
326
+ ' and SHA256 hashes are supported'
327
+ )
328
+ raise STIX2TransformError(msg)
329
+
330
+ def _generate_external_references(self):
331
+ external_references = []
332
+ intel_card_url = INDICATOR_INTEL_CARD_URL.format(
333
+ INDICATOR_TYPE_TO_RF_PORTAL_MAP[self.type],
334
+ self.name,
335
+ )
336
+ external_references.append(
337
+ {
338
+ 'source_name': 'View Intel Card in Recorded Future',
339
+ 'url': intel_card_url,
340
+ },
341
+ )
342
+
343
+ return external_references
344
+
345
+ def _generate_observable(
346
+ self,
347
+ ) -> Union[stix2.IPv6Address, stix2.IPv4Address, stix2.DomainName, stix2.File, stix2.URL]:
348
+ """Creates stix2 observable."""
349
+ uuid = generate_uuid(name=self.name)
350
+ if self.type == 'IpAddress':
351
+ if ':' in self.name:
352
+ return stix2.IPv6Address(id='ipv6-addr--' + uuid, value=self.name)
353
+ else:
354
+ return stix2.IPv4Address(id='ipv4-addr--' + uuid, value=self.name)
355
+ elif self.type == 'InternetDomainName':
356
+ return stix2.DomainName(id='domain-name--' + uuid, value=self.name)
357
+ elif self.type == 'URL':
358
+ return stix2.URL(id='url--' + uuid, value=self.name)
359
+ elif self.type == 'FileHash':
360
+ algo = self._determine_algorithm()
361
+ return stix2.File(id='file--' + uuid, hashes={algo: self.name})
362
+ raise STIX2TransformError('')
363
+
364
+ def _generate_relationship(self) -> stix2.Relationship:
365
+ return stix2.Relationship(
366
+ id='relationship--'
367
+ + generate_uuid(source=self.indicator.id, target=self.observable.id, type='based-on'),
368
+ relationship_type='based-on',
369
+ source_ref=self.indicator.id,
370
+ target_ref=self.observable.id,
371
+ created_by_ref=self.author.id,
372
+ )
@@ -0,0 +1,81 @@
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
+ import stix2
14
+
15
+ ENTITY_TYPE_MAP = {
16
+ 'ip': 'IpAddress',
17
+ 'domain': 'InternetDomainName',
18
+ 'url': 'URL',
19
+ 'hash': 'FileHash',
20
+ }
21
+
22
+ INDICATOR_TYPE_TO_RF_PORTAL_MAP = {
23
+ 'IpAddress': 'ip',
24
+ 'InternetDomainName': 'idn',
25
+ 'URL': 'url',
26
+ 'FileHash': 'hash',
27
+ }
28
+
29
+ IDENTITY_TYPE_TO_CLASS = {
30
+ 'Company': 'organization',
31
+ 'Organization': 'organization',
32
+ 'Person': 'individual',
33
+ }
34
+
35
+ CONVERT_ENTITY_KWARGS = 'description'
36
+ SUPPORTED_HUNTING_RULES = ('yara', 'snort', 'sigma')
37
+
38
+ # maps Insikt Report types to STIX2 report types
39
+ REPORT_TYPE_MAPPER = {
40
+ 'Actor Profile': 'Threat-Actor',
41
+ 'Analyst On-Demand Report': 'Threat-Report',
42
+ 'Cyber Threat Analysis': 'Threat-Report',
43
+ 'Flash Report': 'Threat-Report',
44
+ 'Geopolitical Flash Event': 'Threat-Report',
45
+ 'Geopolitical Intelligence Summary': 'Threat-Report',
46
+ 'Geopolitical Profile': 'Threat-Actor',
47
+ 'Geopolitical Threat Forecast': 'Threat-Actor',
48
+ 'Geopolitical Validated Event': 'Observed-Data',
49
+ 'Hunting Package': 'Attack-Pattern',
50
+ 'Indicator': 'Indicator',
51
+ 'Informational': 'Threat-Report',
52
+ 'Insikt Research Lead': 'Intrusion-Set',
53
+ 'Malware/Tool Profile': 'Malware',
54
+ 'Regular Vendor Vulnerability Disclosures': 'Vulnerability',
55
+ 'Sigma Rule': 'Attack-Pattern',
56
+ 'SNORT Rule': 'Indicator',
57
+ 'Source Profile': 'Observed-Data',
58
+ 'The Record by Recorded Future': 'Threat-Report',
59
+ 'Threat Lead': 'Threat-Actor',
60
+ 'TTP Instance': 'Attack-Pattern',
61
+ 'Validated Intelligence Event': 'Observed-Data',
62
+ 'Weekly Threat Landscape': 'Threat-Report',
63
+ 'YARA Rule': 'Indicator',
64
+ }
65
+
66
+ TLP_MAP = {
67
+ 'white': stix2.TLP_WHITE,
68
+ 'green': stix2.TLP_GREEN,
69
+ 'amber': stix2.TLP_AMBER,
70
+ 'red': stix2.TLP_RED,
71
+ }
72
+
73
+ RF_IDENTITY_UUID = 'identity--509cdfd1-b97f-5329-9e27-a841f8b2dbce'
74
+ RF_NAMESPACE = '7fb92aa3-456a-406a-ad7e-1400307c46b1'
75
+ INDICATOR_TYPES = ['IpAddress', 'InternetDomainName', 'URL', 'FileHash']
76
+ CONVERTED_TYPES = {
77
+ 'ip': 'IpAddress',
78
+ 'domain': 'InternetDomainName',
79
+ 'url': 'URL',
80
+ 'hash': 'FileHash',
81
+ }