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
psengine/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from ._version import __version__ as version
|
|
17
|
+
from .base_http_client import BaseHTTPClient
|
|
18
|
+
from .errors import ReadFileError, RecordedFutureError, WriteFileError
|
|
19
|
+
from .rf_client import RFClient
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger('psengine')
|
|
22
|
+
log.addHandler(logging.NullHandler())
|
psengine/_sdk_id.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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 ._version import __version__
|
|
15
|
+
|
|
16
|
+
SDK_ID = f'psengine-py/{__version__}'
|
psengine/_version.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
__version__ = '2.0.4'
|
|
@@ -0,0 +1,32 @@
|
|
|
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 (
|
|
15
|
+
AnalystNoteAttachmentError,
|
|
16
|
+
AnalystNoteDeleteError,
|
|
17
|
+
AnalystNoteError,
|
|
18
|
+
AnalystNoteLookupError,
|
|
19
|
+
AnalystNotePreviewError,
|
|
20
|
+
AnalystNotePublishError,
|
|
21
|
+
AnalystNoteSearchError,
|
|
22
|
+
)
|
|
23
|
+
from .helpers import save_attachment, save_note
|
|
24
|
+
from .note import (
|
|
25
|
+
AnalystNote,
|
|
26
|
+
AnalystNotePreviewIn,
|
|
27
|
+
AnalystNotePreviewOut,
|
|
28
|
+
AnalystNotePublishIn,
|
|
29
|
+
AnalystNotePublishOut,
|
|
30
|
+
AnalystNoteSearchIn,
|
|
31
|
+
)
|
|
32
|
+
from .note_mgr import AnalystNoteMgr
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
URL_TO_PORTAL = 'https://app.recordedfuture.com/portal/analyst-note/shared/true/{}'
|
|
15
|
+
NOTES_PER_PAGE = 20
|
|
@@ -0,0 +1,42 @@
|
|
|
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 AnalystNoteError(RecordedFutureError):
|
|
18
|
+
"""Error raise when the init of AnalystNote is failing."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AnalystNoteLookupError(RecordedFutureError):
|
|
22
|
+
"""Error raise when cannot lookup an analyst note."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnalystNoteSearchError(RecordedFutureError):
|
|
26
|
+
"""Error raise when cannot search analyst notes."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AnalystNoteAttachmentError(RecordedFutureError):
|
|
30
|
+
"""Error raise when cannot lookup an analyst note."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AnalystNoteDeleteError(RecordedFutureError):
|
|
34
|
+
"""Error raise when cannot delete an analyst note."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AnalystNotePreviewError(RecordedFutureError):
|
|
38
|
+
"""Error raise when cannot post to preview endpoint."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AnalystNotePublishError(RecordedFutureError):
|
|
42
|
+
"""Error raise when cannot post to publish endpoint."""
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
from pathlib import Path
|
|
17
|
+
from typing import Union
|
|
18
|
+
|
|
19
|
+
from pydantic import validate_call
|
|
20
|
+
|
|
21
|
+
from ..errors import WriteFileError
|
|
22
|
+
from ..helpers import OSHelpers, debug_call
|
|
23
|
+
from .note import AnalystNote
|
|
24
|
+
|
|
25
|
+
LOG = logging.getLogger('psengine.analyst_notes.helpers')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@debug_call
|
|
29
|
+
@validate_call
|
|
30
|
+
def save_attachment(
|
|
31
|
+
note_id: str, data: Union[bytes, str], ext: str, output_directory: Union[str, Path]
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Save yara, sigma, snort or pdf to file. The file will use the extension provided and the
|
|
34
|
+
``note_id`` to create the filename.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
note_id (str): ``AnalystNote`` id
|
|
38
|
+
data (Union[bytes, str]): data, returned from ``fetch_attachment``
|
|
39
|
+
ext (str): extension of the attachment, returned by ``fetch_attachment``
|
|
40
|
+
output_directory (str, Path): the directory to save the file into
|
|
41
|
+
"""
|
|
42
|
+
output_directory = (
|
|
43
|
+
output_directory if isinstance(output_directory, str) else output_directory.as_posix()
|
|
44
|
+
)
|
|
45
|
+
_save_attachment(note_id, data, ext, output_directory)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@debug_call
|
|
49
|
+
@validate_call
|
|
50
|
+
def save_note(note: AnalystNote, output_directory: Union[str, Path]) -> None:
|
|
51
|
+
"""Save AnalystNote object on file. The file will have the name of the note id.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
note (AnalystNote): note to save.
|
|
55
|
+
output_directory (str, Path): the directory to save the file into
|
|
56
|
+
"""
|
|
57
|
+
output_directory = (
|
|
58
|
+
output_directory if isinstance(output_directory, str) else output_directory.as_posix()
|
|
59
|
+
)
|
|
60
|
+
_save_attachment(
|
|
61
|
+
note_id=note.id_,
|
|
62
|
+
data=json.dumps(note.json(), indent=4),
|
|
63
|
+
ext='json',
|
|
64
|
+
output_directory=output_directory,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _save_attachment(
|
|
69
|
+
note_id: str, data: Union[bytes, str], ext: str, output_directory: str
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Save attachment from bytes or note itself from json.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
note_id (str): id of the note, will be the filename
|
|
75
|
+
data (bytes | str): content to save
|
|
76
|
+
ext (str): extension of the file.
|
|
77
|
+
output_directory (str): the directory to save the file into
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
WriteFileError: if saving to file fails
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
note_id = note_id.removeprefix('doc:')
|
|
84
|
+
LOG.debug(f"Saving file related to '{note_id}' to disk")
|
|
85
|
+
|
|
86
|
+
dir_path = OSHelpers.mkdir(output_directory)
|
|
87
|
+
note_path = Path(dir_path) / f'{note_id}.{ext}'
|
|
88
|
+
note_path.write_bytes(data) if isinstance(data, bytes) else note_path.write_text(data)
|
|
89
|
+
except (FileNotFoundError, IsADirectoryError, PermissionError, OSError) as err:
|
|
90
|
+
raise WriteFileError(f'Failed to save file to disk. Cause: {err.args}') from err
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Optional, Union
|
|
17
|
+
|
|
18
|
+
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
19
|
+
|
|
20
|
+
from ..common_models import IdNameType, IdNameTypeDescription, RFBaseModel
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DiamondModel(RFBaseModel):
|
|
24
|
+
start: Optional[datetime] = None
|
|
25
|
+
stop: Optional[datetime] = None
|
|
26
|
+
malicious_infrastructure: Optional[list[IdNameTypeDescription]] = []
|
|
27
|
+
capabilities: Optional[list[IdNameTypeDescription]] = []
|
|
28
|
+
adversary: Optional[list[IdNameTypeDescription]] = []
|
|
29
|
+
target: Optional[list[IdNameTypeDescription]] = []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Query(RFBaseModel):
|
|
33
|
+
title: str
|
|
34
|
+
url: Optional[IdNameTypeDescription] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Position(RFBaseModel):
|
|
38
|
+
longitude: float
|
|
39
|
+
latitude: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PositionEvent(RFBaseModel):
|
|
43
|
+
start: datetime
|
|
44
|
+
stop: datetime
|
|
45
|
+
location: Optional[list[IdNameTypeDescription]] = []
|
|
46
|
+
event_positions: Optional[list[Position]] = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CyberAttackEvent(RFBaseModel):
|
|
50
|
+
start: datetime
|
|
51
|
+
stop: datetime
|
|
52
|
+
adversary: Optional[list[IdNameTypeDescription]] = []
|
|
53
|
+
target: Optional[list[IdNameTypeDescription]] = []
|
|
54
|
+
capabilities: list[IdNameTypeDescription] = []
|
|
55
|
+
malicious_infrastructure: Optional[list[IdNameTypeDescription]] = []
|
|
56
|
+
operation: Optional[list[IdNameTypeDescription]] = []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ArmedConflictEvent(PositionEvent):
|
|
60
|
+
attacker: Optional[list[IdNameTypeDescription]] = []
|
|
61
|
+
target: Optional[list[IdNameTypeDescription]] = []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ArmsPurchaseSaleEvent(RFBaseModel):
|
|
65
|
+
start: datetime
|
|
66
|
+
stop: datetime
|
|
67
|
+
arms_seller: Optional[list[IdNameTypeDescription]] = []
|
|
68
|
+
arms_purchaser: Optional[list[IdNameTypeDescription]] = []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DiseaseOutbreakEvent(PositionEvent):
|
|
72
|
+
disease: Optional[list[IdNameTypeDescription]] = []
|
|
73
|
+
facility: Optional[list[IdNameTypeDescription]] = []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EnvironmentalIssueEvent(PositionEvent):
|
|
77
|
+
environmental_issue: list[str]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ManMadeDisasterEvent(PositionEvent):
|
|
81
|
+
facility: list[IdNameTypeDescription]
|
|
82
|
+
manmade_disaster: Union[list[IdNameTypeDescription], list[str]]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MilitaryManeuverEvent(PositionEvent):
|
|
86
|
+
actors: Optional[list[IdNameTypeDescription]] = []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class NaturalDisasterEvent(PositionEvent):
|
|
90
|
+
natural_disaster: list[IdNameTypeDescription]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class NuclearMaterialTransactionEvent(PositionEvent):
|
|
94
|
+
material: list[str]
|
|
95
|
+
location_origin: Optional[list[str]] = []
|
|
96
|
+
location_destination: Optional[list[str]] = []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PersonThreatEvent(RFBaseModel):
|
|
100
|
+
start: datetime
|
|
101
|
+
stop: datetime
|
|
102
|
+
threatened: list[IdNameTypeDescription]
|
|
103
|
+
actor: Optional[list[IdNameTypeDescription]] = []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ProtestEvent(RFBaseModel):
|
|
107
|
+
protest_target: Optional[list[IdNameTypeDescription]] = []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class MalwareAnalysisEvent(RFBaseModel):
|
|
111
|
+
start: datetime
|
|
112
|
+
stop: datetime
|
|
113
|
+
malware: list[IdNameTypeDescription]
|
|
114
|
+
attacker: Optional[list[IdNameTypeDescription]] = []
|
|
115
|
+
malicious_infrastructure: Optional[list[IdNameTypeDescription]] = []
|
|
116
|
+
ttp: Optional[list[IdNameTypeDescription]] = []
|
|
117
|
+
target: Optional[list[IdNameTypeDescription]] = []
|
|
118
|
+
exploit: Optional[list[IdNameTypeDescription]] = []
|
|
119
|
+
hash_: Optional[list[IdNameTypeDescription]] = Field(alias='hash', default=[])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
ATTRIBUTES_MAPPING = {
|
|
123
|
+
'ArmedConflict': ArmedConflictEvent,
|
|
124
|
+
'ArmsPurchaseSale': ArmsPurchaseSaleEvent,
|
|
125
|
+
'Coup': PositionEvent,
|
|
126
|
+
'CyberAttack': CyberAttackEvent,
|
|
127
|
+
'DiseaseOutbreak': DiseaseOutbreakEvent,
|
|
128
|
+
'Election': PositionEvent,
|
|
129
|
+
'EnvironmentalIssue': EnvironmentalIssueEvent,
|
|
130
|
+
'MalwareAnalysis': MalwareAnalysisEvent,
|
|
131
|
+
'ManMadeDisaster': ManMadeDisasterEvent,
|
|
132
|
+
'MilitaryManeuver': MilitaryManeuverEvent,
|
|
133
|
+
'NaturalDisaster': NaturalDisasterEvent,
|
|
134
|
+
'NuclearMaterialTransaction': NuclearMaterialTransactionEvent,
|
|
135
|
+
'PersonThreat': PersonThreatEvent,
|
|
136
|
+
'PoliticalEvent': PositionEvent,
|
|
137
|
+
'PublicSafetyWarning': PositionEvent,
|
|
138
|
+
'RFEVEArmedAssault': PositionEvent,
|
|
139
|
+
'RFEVEProtest': ProtestEvent,
|
|
140
|
+
'TerrorIncident': PositionEvent,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class NoteEvent(RFBaseModel):
|
|
145
|
+
type_: Optional[str] = Field(alias='type', default=None)
|
|
146
|
+
attributes: Optional[Any] = None
|
|
147
|
+
|
|
148
|
+
@model_validator(mode='before')
|
|
149
|
+
@classmethod
|
|
150
|
+
def validate_attribute(cls, values):
|
|
151
|
+
"""Validate note event attributes."""
|
|
152
|
+
if not values.get('type') or not values.get('attributes'):
|
|
153
|
+
raise ValueError('Missing type or attributes from note event')
|
|
154
|
+
|
|
155
|
+
type_ = values['type']
|
|
156
|
+
validator = ATTRIBUTES_MAPPING.get(type_)
|
|
157
|
+
if not validator:
|
|
158
|
+
log = logging.getLogger(__name__)
|
|
159
|
+
log.warning(f'Unknown validator for Analyst Note with event type {type_}')
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
attributes = validator.model_validate(values['attributes'])
|
|
164
|
+
except ValidationError as e:
|
|
165
|
+
log = logging.getLogger(__name__)
|
|
166
|
+
log.warning(f'Failed to validate note event of type {type_}. Error {e}')
|
|
167
|
+
log.warning(values)
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
return {'type': type_, 'attributes': attributes}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Attributes(RFBaseModel):
|
|
174
|
+
title: str
|
|
175
|
+
text: str
|
|
176
|
+
published: datetime
|
|
177
|
+
attachment: Optional[str] = None
|
|
178
|
+
events: Optional[list[NoteEvent]] = []
|
|
179
|
+
validated_on: Optional[datetime] = None
|
|
180
|
+
note_entities: Optional[list[IdNameTypeDescription]] = []
|
|
181
|
+
context_entities: Optional[list[IdNameTypeDescription]] = []
|
|
182
|
+
topic: Optional[Union[list[IdNameTypeDescription], IdNameTypeDescription]] = []
|
|
183
|
+
labels: Optional[list[IdNameTypeDescription]] = []
|
|
184
|
+
validation_urls: Optional[list[IdNameTypeDescription]] = []
|
|
185
|
+
diamond_model: Optional[list[DiamondModel]] = []
|
|
186
|
+
recommended_queries: Optional[list[Query]] = []
|
|
187
|
+
header_image: Optional[IdNameType] = None
|
|
188
|
+
|
|
189
|
+
@field_validator('events', mode='after')
|
|
190
|
+
@classmethod
|
|
191
|
+
def remove_empty_events(cls, values):
|
|
192
|
+
"""Remove empty events when ``NoteEvent`` skip the validation."""
|
|
193
|
+
return [v for v in values if v.type_ and v.attributes]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class PreviewAttributesIn(RFBaseModel):
|
|
197
|
+
title: str
|
|
198
|
+
text: str
|
|
199
|
+
note_entities: Optional[list[str]] = []
|
|
200
|
+
context_entities: Optional[list[str]] = []
|
|
201
|
+
topic: Union[list[str], str, None] = []
|
|
202
|
+
labels: Optional[list[str]] = []
|
|
203
|
+
validation_urls: Optional[list[str]] = []
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class PreviewAttributesOut(RFBaseModel):
|
|
207
|
+
title: str
|
|
208
|
+
text: str
|
|
209
|
+
note_entities: Optional[list[IdNameTypeDescription]] = []
|
|
210
|
+
context_entities: Optional[list[IdNameTypeDescription]] = []
|
|
211
|
+
topic: Optional[list[IdNameTypeDescription]] = []
|
|
212
|
+
labels: Optional[list[IdNameTypeDescription]] = []
|
|
213
|
+
validation_urls: Optional[list[IdNameTypeDescription]] = []
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class RequestAttachment(RFBaseModel):
|
|
217
|
+
content_type: str
|
|
218
|
+
encoding: str
|
|
219
|
+
content: str
|
|
@@ -0,0 +1,149 @@
|
|
|
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 functools import total_ordering
|
|
15
|
+
from typing import Optional, Union
|
|
16
|
+
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
from ..common_models import IdNameTypeDescription, RFBaseModel
|
|
20
|
+
from ..constants import TIMESTAMP_STR
|
|
21
|
+
from .constants import NOTES_PER_PAGE, URL_TO_PORTAL
|
|
22
|
+
from .models import Attributes, PreviewAttributesIn, PreviewAttributesOut, RequestAttachment
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# AnalystNote also used by BaseEnrichedEntity
|
|
26
|
+
@total_ordering
|
|
27
|
+
class AnalystNote(RFBaseModel):
|
|
28
|
+
"""Validate data received from ``/search``, ``/analystnote/{note}`` endpoints.
|
|
29
|
+
|
|
30
|
+
Methods:
|
|
31
|
+
__hash__:
|
|
32
|
+
Returns a hash value based on note ``id_``.
|
|
33
|
+
|
|
34
|
+
__eq__:
|
|
35
|
+
Checks equality between two AnalystNote instances based on ``id_`` and published time.
|
|
36
|
+
|
|
37
|
+
__gt__:
|
|
38
|
+
Defines a greater-than comparison between two AnalystNote instances based published time
|
|
39
|
+
and the ``id_``
|
|
40
|
+
|
|
41
|
+
__str__:
|
|
42
|
+
Returns a string representation of the AnalystNote instance with:
|
|
43
|
+
``id_``, ``title``, and published timestamp.
|
|
44
|
+
|
|
45
|
+
.. code-block:: python
|
|
46
|
+
|
|
47
|
+
>>> print(analyst_note)
|
|
48
|
+
Analyst Note ID: 12345, Title: Cyber Vuln, Published: 2024-05-21 10:42:30AM
|
|
49
|
+
|
|
50
|
+
Total Ordering:
|
|
51
|
+
The ordering of AnalystNote instances is determined primarily by the published timestamp in
|
|
52
|
+
the attributes. If two instances have the same published timestamp, the note id is used as
|
|
53
|
+
a secondary criterion for ordering.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
external_id: Optional[str] = None
|
|
57
|
+
source: IdNameTypeDescription
|
|
58
|
+
attributes: Attributes
|
|
59
|
+
id_: str = Field(alias='id')
|
|
60
|
+
|
|
61
|
+
def __hash__(self):
|
|
62
|
+
return hash((self.id_, self.attributes.published))
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: 'AnalystNote'):
|
|
65
|
+
return (self.id_, self.attributes.published) == (other.id_, other.attributes.published)
|
|
66
|
+
|
|
67
|
+
def __gt__(self, other: 'AnalystNote'):
|
|
68
|
+
return (self.attributes.published, self.id_) > (other.attributes.published, other.id_)
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
return (
|
|
72
|
+
f'Analyst Note ID: {self.id_}, Title: {self.attributes.title}, '
|
|
73
|
+
f'Published: {self.attributes.published.strftime(TIMESTAMP_STR)}'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def detection_rule_type(self) -> Optional[str]:
|
|
78
|
+
"""Returns the attachment type if present, else None. It checks for specific types like
|
|
79
|
+
'sigma rule', 'yara rule', and 'snort rule' in the topics of the note.
|
|
80
|
+
"""
|
|
81
|
+
topics_type = ('sigma rule', 'yara rule', 'snort rule')
|
|
82
|
+
|
|
83
|
+
topics = (
|
|
84
|
+
{topic.name.lower() for topic in self.attributes.topic if topic.name}
|
|
85
|
+
if isinstance(self.attributes.topic, list)
|
|
86
|
+
else [self.attributes.topic.name]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return next(
|
|
90
|
+
(topic_type.split()[0] for topic_type in topics_type if topic_type in topics),
|
|
91
|
+
None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def portal_url(self) -> str:
|
|
96
|
+
"""Get the link to portal."""
|
|
97
|
+
if self.id_.startswith('doc:'):
|
|
98
|
+
return URL_TO_PORTAL.format(self.id_)
|
|
99
|
+
return URL_TO_PORTAL.format(f'doc:{self.id_}')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AnalystNotePreviewIn(RFBaseModel):
|
|
103
|
+
"""Validate data sent to ``/preview`` endpoint."""
|
|
104
|
+
|
|
105
|
+
attributes: PreviewAttributesIn
|
|
106
|
+
source: Optional[str]
|
|
107
|
+
tagged_text: bool = False
|
|
108
|
+
serialization: str = 'full'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AnalystNotePreviewOut(RFBaseModel):
|
|
112
|
+
"""Validate data received from ``/preview`` endpoint."""
|
|
113
|
+
|
|
114
|
+
attributes: PreviewAttributesOut
|
|
115
|
+
source: IdNameTypeDescription
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AnalystNotePublishIn(AnalystNotePreviewIn):
|
|
119
|
+
"""Validate data sent to ``/publish`` endpoint."""
|
|
120
|
+
|
|
121
|
+
attributes: PreviewAttributesIn
|
|
122
|
+
source: Optional[str] = None
|
|
123
|
+
tagged_text: bool = False
|
|
124
|
+
serialization: str = 'full'
|
|
125
|
+
note_id: Optional[str] = None
|
|
126
|
+
resolve_entities: bool = True
|
|
127
|
+
attachment_content_details: Optional[RequestAttachment] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class AnalystNotePublishOut(RFBaseModel):
|
|
131
|
+
"""Validate data received from ``/publish`` endpoint."""
|
|
132
|
+
|
|
133
|
+
note_id: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AnalystNoteSearchIn(RFBaseModel):
|
|
137
|
+
"""Validate data sent to ``/search`` endpoint."""
|
|
138
|
+
|
|
139
|
+
published: Optional[str] = None
|
|
140
|
+
entity: Optional[str] = None
|
|
141
|
+
author: Optional[str] = None
|
|
142
|
+
title: Optional[str] = None
|
|
143
|
+
topic: Union[list[str], str, None] = []
|
|
144
|
+
label: Optional[str] = None
|
|
145
|
+
source: Optional[str] = None
|
|
146
|
+
serialization: str = None
|
|
147
|
+
tagged_text: bool = None
|
|
148
|
+
limit: int = NOTES_PER_PAGE
|
|
149
|
+
from_: Optional[str] = Field(alias='from', default=None)
|