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,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
|
+
}
|