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
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)