psengine 2.0.5__tar.gz → 2.0.6__tar.gz
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-2.0.5 → psengine-2.0.6}/PKG-INFO +3 -3
- {psengine-2.0.5 → psengine-2.0.6}/psengine/_version.py +1 -1
- psengine-2.0.6/psengine/analyst_notes/markdown.py +216 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/note.py +32 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/base_http_client.py +9 -2
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/classic_alert_mgr.py +7 -10
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/markdown/__init__.py +0 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/markdown/markdown.py +40 -34
- {psengine-2.0.5 → psengine-2.0.6}/psengine/constants.py +2 -2
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/detection_mgr.py +2 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/helpers.py +2 -2
- {psengine-2.0.5 → psengine-2.0.6}/psengine/endpoints.py +5 -3
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/lookup.py +5 -5
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/lookup_mgr.py +2 -2
- {psengine-2.0.5/psengine/playbook_alerts/markdown → psengine-2.0.6/psengine/enrich/models}/__init__.py +0 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/entity_list.py +3 -6
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/entity_list_mgr.py +1 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/helpers/helpers.py +11 -12
- {psengine-2.0.5 → psengine-2.0.6}/psengine/markdown/markdown.py +19 -15
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/constants.py +4 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/mappings.py +8 -0
- {psengine-2.0.5/psengine/enrich/models → psengine-2.0.6/psengine/playbook_alerts/markdown}/__init__.py +0 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/markdown/markdown.py +18 -11
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/markdown/markdown_code_repo.py +24 -9
- psengine-2.0.6/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +200 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +26 -13
- psengine-2.0.6/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +105 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +27 -17
- psengine-2.0.6/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +317 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/__init__.py +14 -7
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/common_models.py +13 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/pba_code_repo_leak.py +1 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +3 -12
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/pba_domain_abuse.py +3 -3
- psengine-2.0.6/psengine/playbook_alerts/models/pba_geopolitics_facility.py +86 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/pba_identity_exposures.py +1 -1
- psengine-2.0.6/psengine/playbook_alerts/models/pba_malware_report.py +55 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/pba_third_party_risk.py +5 -13
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/pa_category.py +2 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/playbook_alert_mgr.py +39 -20
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/playbook_alerts.py +252 -46
- {psengine-2.0.5 → psengine-2.0.6}/psengine/rf_client.py +1 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine/risklists/models.py +1 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/risklists/risklist_mgr.py +2 -2
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/complex_entity.py +11 -12
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/helpers.py +4 -5
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/rf_bundle.py +5 -7
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/util.py +1 -1
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/PKG-INFO +3 -3
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/SOURCES.txt +6 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/requires.txt +1 -1
- {psengine-2.0.5 → psengine-2.0.6}/pyproject.toml +3 -3
- {psengine-2.0.5 → psengine-2.0.6}/LICENSE +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/README.rst +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/_sdk_id.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/helpers.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/analyst_notes/note_mgr.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/classic_alert.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/helpers.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/classic_alerts/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/collective_insights.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/insight.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/collective_insights/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/common_models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/config/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/config/config.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/config/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/detection_rule.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/detection/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/models/base_enriched_entity.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/models/lookup.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/models/soar.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/soar.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/enrich/soar_mgr.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_lists/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_match/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_match/entity_match.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_match/entity_match_mgr.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_match/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/entity_match/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/helpers/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/logger/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/logger/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/logger/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/logger/rf_logger.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/markdown/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/markdown/models.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/helpers.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/panel_log.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/panel_status.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/risklists/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/risklists/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/risklists/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/__init__.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/base_stix_entity.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/constants.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/enriched_indicator.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/errors.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine/stix2/simple_entity.py +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/dependency_links.txt +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/entry_points.txt +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/psengine.egg-info/top_level.txt +0 -0
- {psengine-2.0.5 → psengine-2.0.6}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psengine
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.6
|
|
4
4
|
Summary: psengine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
|
|
5
5
|
Author-email: Moise Medici <moise.medici@recordedfuture.com>, Patrick Kinsella <patrick.kinsella@recordedfuture.com>, Ernest Bartosevic <ernest.bartosevic@recordedfuture.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/RecordedFuture-ProfessionalServices/psengine
|
|
8
|
-
Project-URL: Changelog, https://github.com/RecordedFuture-ProfessionalServices/psengine/CHANGELOG.rst
|
|
8
|
+
Project-URL: Changelog, https://github.com/RecordedFuture-ProfessionalServices/psengine/blob/main/CHANGELOG.rst
|
|
9
9
|
Keywords: API,Recorded Future,Cyber Security Engineering,Threat Intelligence
|
|
10
10
|
Requires-Python: <3.14,>=3.9
|
|
11
11
|
Description-Content-Type: text/x-rst
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pytest-random-order==1.1.1; extra == "dev"
|
|
|
28
28
|
Requires-Dist: pytest-vcr==1.0.2; extra == "dev"
|
|
29
29
|
Requires-Dist: pytest-watch==4.2.0; extra == "dev"
|
|
30
30
|
Requires-Dist: requests==2.29.0; extra == "dev"
|
|
31
|
-
Requires-Dist: ruff~=0.
|
|
31
|
+
Requires-Dist: ruff~=0.11.0; extra == "dev"
|
|
32
32
|
Requires-Dist: wheel==0.37.1; extra == "dev"
|
|
33
33
|
Requires-Dist: setuptools==61.0.0; extra == "dev"
|
|
34
34
|
Requires-Dist: Sphinx==7.1.2; extra == "dev"
|
|
@@ -0,0 +1,216 @@
|
|
|
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 re
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from itertools import chain
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from markdown_strings import bold, unordered_list
|
|
20
|
+
|
|
21
|
+
from ..constants import TIMESTAMP_STR
|
|
22
|
+
from ..endpoints import EP_ANALYST_NOTE_LOOKUP
|
|
23
|
+
from ..markdown.markdown import MarkdownMaker, divider, html_collapsible
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..analyst_notes import AnalystNote
|
|
27
|
+
|
|
28
|
+
EXTRACTED_KEYS = {
|
|
29
|
+
'AttackVector': 'Attack Vector',
|
|
30
|
+
'Malware': 'Malware',
|
|
31
|
+
'MalwareCategory': 'Malware Category',
|
|
32
|
+
'WinRegKey': 'Windows Registry Key',
|
|
33
|
+
'Hash': 'Hash',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cleanup_insikt_note_text(note_text: str) -> str:
|
|
38
|
+
"""Clean up insikt note text to avoid markdown rendering issues."""
|
|
39
|
+
translation = {
|
|
40
|
+
r'\•': '+ ',
|
|
41
|
+
r'--+': '',
|
|
42
|
+
r'>>+': '',
|
|
43
|
+
r'<<+': '',
|
|
44
|
+
r'\*\*': '••',
|
|
45
|
+
r'#': '\\#',
|
|
46
|
+
r'_': '\\_',
|
|
47
|
+
}
|
|
48
|
+
for k, v in translation.items():
|
|
49
|
+
note_text = re.sub(k, v, note_text)
|
|
50
|
+
|
|
51
|
+
return note_text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _entity_by_type_list(entities: list, entity_type: str) -> list:
|
|
55
|
+
"""Extract all the related entities by type."""
|
|
56
|
+
return sorted({entity.name for entity in entities if entity.type_ == entity_type})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _vuln_list(entities: list) -> list:
|
|
60
|
+
"""Extract all the vulnerabilities and add the description if present."""
|
|
61
|
+
vulns = [entity for entity in entities if entity.type_ == 'CyberVulnerability']
|
|
62
|
+
|
|
63
|
+
texts = []
|
|
64
|
+
for vuln in sorted(vulns, key=lambda x: x.name):
|
|
65
|
+
text = bold(vuln.name)
|
|
66
|
+
if vuln.description:
|
|
67
|
+
descr = vuln.description.replace('\n', ' ')
|
|
68
|
+
text = f'{text}: {descr}'
|
|
69
|
+
texts.append(text)
|
|
70
|
+
|
|
71
|
+
return texts
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _add_extra_entities(note: 'AnalystNote', html_tags: bool, md_maker: MarkdownMaker):
|
|
75
|
+
"""Add the Entities Extracted block for EXTRACTED_KEYS types."""
|
|
76
|
+
data = defaultdict(list)
|
|
77
|
+
entities = list(chain(note.attributes.note_entities, note.attributes.context_entities))
|
|
78
|
+
|
|
79
|
+
for entity_type, clean_entity_type in EXTRACTED_KEYS.items():
|
|
80
|
+
if extracted := _entity_by_type_list(entities, entity_type):
|
|
81
|
+
data[clean_entity_type].extend(extracted)
|
|
82
|
+
|
|
83
|
+
if vulns := _vuln_list(entities):
|
|
84
|
+
data['Vulnerability'].extend(vulns)
|
|
85
|
+
|
|
86
|
+
data = dict(sorted(data.items()))
|
|
87
|
+
if data:
|
|
88
|
+
if html_tags:
|
|
89
|
+
md_maker.add_section(
|
|
90
|
+
'Entities',
|
|
91
|
+
[
|
|
92
|
+
html_collapsible(f'<b>{k}</b>', '\n\n' + unordered_list(v, esc=False)) + '\n'
|
|
93
|
+
if len(v) > 5
|
|
94
|
+
else f'{bold(k)}\n{unordered_list(v, esc=False)}\n\n'
|
|
95
|
+
for k, v in data.items()
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
md_maker.add_section(
|
|
100
|
+
'Entities',
|
|
101
|
+
[
|
|
102
|
+
'\n'.join([bold(f'{k}:'), unordered_list(v, esc=False) + '\n'])
|
|
103
|
+
for k, v in data.items()
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _add_diamond_model(
|
|
109
|
+
note: 'AnalystNote',
|
|
110
|
+
html_tags: bool,
|
|
111
|
+
defang_malicious_infrastructure: bool,
|
|
112
|
+
md_maker: MarkdownMaker,
|
|
113
|
+
):
|
|
114
|
+
"""Add all the Diamond Models if present.
|
|
115
|
+
|
|
116
|
+
Since a note can have more than one Diamond model for different targets/methods all will be
|
|
117
|
+
displayed.
|
|
118
|
+
The collapsible is only applied to the title (ie. Diamond Model 1) and each section that has
|
|
119
|
+
more than 5 entries.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
html_title = '<h4>Cyber Attack'
|
|
123
|
+
diamond_models, data = [], []
|
|
124
|
+
for diamond_model in note.attributes.diamond_model:
|
|
125
|
+
model = {
|
|
126
|
+
'Malicious Infrastructure': [e.name for e in diamond_model.malicious_infrastructure],
|
|
127
|
+
'Capabilities': [e.name for e in diamond_model.capabilities],
|
|
128
|
+
'Adversary': [e.name for e in diamond_model.adversary],
|
|
129
|
+
'Target': [f'{e.name} ({e.type_})' for e in diamond_model.target],
|
|
130
|
+
}
|
|
131
|
+
if defang_malicious_infrastructure:
|
|
132
|
+
iocs = [ioc.replace('.', '[.]') for ioc in model['Malicious Infrastructure']]
|
|
133
|
+
model['Malicious Infrastructure'] = iocs
|
|
134
|
+
|
|
135
|
+
diamond_models.append({k: v for k, v in model.items() if v})
|
|
136
|
+
|
|
137
|
+
if html_tags:
|
|
138
|
+
data = [
|
|
139
|
+
html_collapsible(
|
|
140
|
+
f'{html_title} {i}{": " + model["Adversary"][0] if model.get("Adversary") else ""}',
|
|
141
|
+
'\n\n'
|
|
142
|
+
+ ''.join(
|
|
143
|
+
html_collapsible(f'<b>{section}</b>', f'\n\n{unordered_list(sorted(entities))}')
|
|
144
|
+
+ '\n'
|
|
145
|
+
if len(entities) > 5
|
|
146
|
+
else f'\n{bold(section)}\n\n{unordered_list(sorted(entities))}\n\n'
|
|
147
|
+
for section, entities in model.items()
|
|
148
|
+
)
|
|
149
|
+
+ f'\n{divider()}',
|
|
150
|
+
)
|
|
151
|
+
+ '\n'
|
|
152
|
+
for i, model in enumerate(diamond_models, 1)
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
else:
|
|
156
|
+
data = [
|
|
157
|
+
'\n'
|
|
158
|
+
+ bold(
|
|
159
|
+
f'Cyber Attack {i}{": " + model["Adversary"][0] if model.get("Adversary") else ""}'
|
|
160
|
+
)
|
|
161
|
+
+ '\n'
|
|
162
|
+
+ '\n\n'.join(
|
|
163
|
+
[
|
|
164
|
+
'\n' + section + ':\n' + unordered_list(sorted(entities))
|
|
165
|
+
for section, entities in model.items()
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
+ f'\n{divider()}\n'
|
|
169
|
+
for i, model in enumerate(diamond_models, 1)
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
if data:
|
|
173
|
+
md_maker.add_section('Diamond Models', data)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _markdown(
|
|
177
|
+
note: 'AnalystNote',
|
|
178
|
+
extract_entities: bool,
|
|
179
|
+
diamond_model: bool,
|
|
180
|
+
html_tags: bool,
|
|
181
|
+
defang_malicious_infrastructure: bool,
|
|
182
|
+
character_limit: int,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Main markdown function."""
|
|
185
|
+
md_maker = MarkdownMaker()
|
|
186
|
+
topic = (
|
|
187
|
+
note.attributes.topic
|
|
188
|
+
if isinstance(note.attributes.topic, list)
|
|
189
|
+
else [note.attributes.topic]
|
|
190
|
+
)
|
|
191
|
+
topic_str = f'{bold("Topic:")} {{}}' if len(topic) == 1 else f'{bold("Topics:")} {{}}'
|
|
192
|
+
intro = [
|
|
193
|
+
f'{bold("ID:")} {note.id_} ',
|
|
194
|
+
f'{bold("Published:")} {note.attributes.published.strftime(TIMESTAMP_STR)} ',
|
|
195
|
+
f'{bold("Source:")} {note.source.name} ',
|
|
196
|
+
topic_str.format(', '.join(t.name for t in topic) + ' '),
|
|
197
|
+
]
|
|
198
|
+
if validation_urls := '\n- '.join(url.name for url in note.attributes.validation_urls):
|
|
199
|
+
validation_urls = f'\n- {validation_urls}\n'
|
|
200
|
+
intro.append(f'{bold("Validation URLs:")} {validation_urls}')
|
|
201
|
+
|
|
202
|
+
intro.append(f'[API]({EP_ANALYST_NOTE_LOOKUP.format(note.id_)}) | [Portal]({note.portal_url})')
|
|
203
|
+
|
|
204
|
+
md_maker.add_title(note.attributes.title)
|
|
205
|
+
md_maker.add_section('Summary', intro)
|
|
206
|
+
|
|
207
|
+
if extract_entities:
|
|
208
|
+
_add_extra_entities(note, html_tags, md_maker)
|
|
209
|
+
|
|
210
|
+
if diamond_model:
|
|
211
|
+
_add_diamond_model(note, html_tags, defang_malicious_infrastructure, md_maker)
|
|
212
|
+
|
|
213
|
+
md_maker.add_section('Note', _cleanup_insikt_note_text(note.attributes.text))
|
|
214
|
+
|
|
215
|
+
output = md_maker.format_output()
|
|
216
|
+
return output if not character_limit else output[:character_limit]
|
|
@@ -19,6 +19,7 @@ from pydantic import Field
|
|
|
19
19
|
from ..common_models import IdNameTypeDescription, RFBaseModel
|
|
20
20
|
from ..constants import TIMESTAMP_STR
|
|
21
21
|
from .constants import NOTES_PER_PAGE, URL_TO_PORTAL
|
|
22
|
+
from .markdown import _markdown
|
|
22
23
|
from .models import Attributes, PreviewAttributesIn, PreviewAttributesOut, RequestAttachment
|
|
23
24
|
|
|
24
25
|
|
|
@@ -98,6 +99,37 @@ class AnalystNote(RFBaseModel):
|
|
|
98
99
|
return URL_TO_PORTAL.format(self.id_)
|
|
99
100
|
return URL_TO_PORTAL.format(f'doc:{self.id_}')
|
|
100
101
|
|
|
102
|
+
def markdown(
|
|
103
|
+
self,
|
|
104
|
+
extract_entities: bool = True,
|
|
105
|
+
diamond_model: bool = True,
|
|
106
|
+
html_tags: bool = False,
|
|
107
|
+
defang_malicious_infrastructure: bool = False,
|
|
108
|
+
character_limit: int = None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Return the markdown representation of the Note.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
extract_entities (bool): Extract and include entities in the markdown. Defaults to True.
|
|
114
|
+
diamond_model (bool): Include a diamond model visualization. Defaults to True.
|
|
115
|
+
html_tags (bool): Include HTML tags in the output. Defaults to False.
|
|
116
|
+
defang_malicious_infrastructure (bool): Defang URLs or other malicious indicators.
|
|
117
|
+
Defaults to False.
|
|
118
|
+
character_limit (int, optional): Limit the output to a specified number of characters.
|
|
119
|
+
Defaults to None.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: The generated markdown string.
|
|
123
|
+
"""
|
|
124
|
+
return _markdown(
|
|
125
|
+
self,
|
|
126
|
+
extract_entities=extract_entities,
|
|
127
|
+
diamond_model=diamond_model,
|
|
128
|
+
html_tags=html_tags,
|
|
129
|
+
defang_malicious_infrastructure=defang_malicious_infrastructure,
|
|
130
|
+
character_limit=character_limit,
|
|
131
|
+
)
|
|
132
|
+
|
|
101
133
|
|
|
102
134
|
class AnalystNotePreviewIn(RFBaseModel):
|
|
103
135
|
"""Validate data sent to ``/preview`` endpoint."""
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
9
|
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
10
|
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
-
# accessed from any third party API.
|
|
11
|
+
# accessed from any third party API. #
|
|
12
12
|
##############################################################################################
|
|
13
13
|
|
|
14
14
|
import json
|
|
@@ -16,7 +16,14 @@ import logging
|
|
|
16
16
|
from typing import Union
|
|
17
17
|
|
|
18
18
|
from pydantic import validate_call
|
|
19
|
-
from requests import
|
|
19
|
+
from requests import (
|
|
20
|
+
ConnectionError, # noqa: A004
|
|
21
|
+
ConnectTimeout,
|
|
22
|
+
HTTPError,
|
|
23
|
+
ReadTimeout,
|
|
24
|
+
Session,
|
|
25
|
+
adapters,
|
|
26
|
+
)
|
|
20
27
|
from requests.adapters import HTTPAdapter, Retry
|
|
21
28
|
from requests.exceptions import JSONDecodeError
|
|
22
29
|
from requests.models import Response
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
15
|
from itertools import chain
|
|
16
|
-
from typing import Optional, Union
|
|
16
|
+
from typing import Annotated, Optional, Union
|
|
17
17
|
|
|
18
18
|
from pydantic import Field, validate_call
|
|
19
19
|
|
|
@@ -26,8 +26,7 @@ from ..endpoints import (
|
|
|
26
26
|
EP_CLASSIC_ALERTS_SEARCH,
|
|
27
27
|
EP_CLASSIC_ALERTS_UPDATE,
|
|
28
28
|
)
|
|
29
|
-
from ..helpers import MultiThreadingHelper, debug_call
|
|
30
|
-
from ..helpers.helpers import connection_exceptions
|
|
29
|
+
from ..helpers import MultiThreadingHelper, connection_exceptions, debug_call
|
|
31
30
|
from ..rf_client import RFClient
|
|
32
31
|
from .classic_alert import AlertRuleOut, ClassicAlert, ClassicAlertHit
|
|
33
32
|
from .constants import ALERTS_PER_PAGE, ALL_CA_FIELDS, REQUIRED_CA_FIELDS
|
|
@@ -126,20 +125,19 @@ class ClassicAlertMgr:
|
|
|
126
125
|
)
|
|
127
126
|
)
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
if isinstance(rule_id, list):
|
|
130
129
|
return list(chain.from_iterable(self._search(rule, **params) for rule in rule_id))
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
if isinstance(rule_id, str):
|
|
133
132
|
return self._search(rule_id, **params)
|
|
134
|
-
|
|
135
|
-
return self._search(**params)
|
|
133
|
+
return self._search(**params)
|
|
136
134
|
|
|
137
135
|
@debug_call
|
|
138
136
|
@validate_call
|
|
139
137
|
@connection_exceptions(ignore_status_code=[], exception_to_raise=AlertFetchError)
|
|
140
138
|
def fetch(
|
|
141
139
|
self,
|
|
142
|
-
id_: str,
|
|
140
|
+
id_: Annotated[str, Field(min_length=4)],
|
|
143
141
|
fields: Optional[list[str]] = ALL_CA_FIELDS,
|
|
144
142
|
tagged_text: Optional[bool] = None,
|
|
145
143
|
) -> ClassicAlert:
|
|
@@ -418,8 +416,7 @@ class ClassicAlertMgr:
|
|
|
418
416
|
JSON response
|
|
419
417
|
"""
|
|
420
418
|
self.log.info(f'Updating alerts: {updates}')
|
|
421
|
-
|
|
422
|
-
return response
|
|
419
|
+
return self.rf_client.request('post', url=EP_CLASSIC_ALERTS_UPDATE, data=updates).json()
|
|
423
420
|
|
|
424
421
|
@debug_call
|
|
425
422
|
@validate_call
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
15
|
from itertools import chain
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
16
17
|
|
|
17
18
|
from markdown_strings import blockquote, bold, esc_format, link
|
|
18
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..classic_alerts.classic_alert import ClassicAlert
|
|
22
|
+
|
|
19
23
|
from ...constants import TIMESTAMP_STR, TRUNCATE_COMMENT
|
|
20
24
|
from ...markdown import (
|
|
21
25
|
MarkdownMaker,
|
|
@@ -44,7 +48,7 @@ def _clean_title(alert_title: str) -> str:
|
|
|
44
48
|
return re.sub(expression, '', alert_title).strip()
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
def _owner_org_markdown(classic_alert) -> list[str]:
|
|
51
|
+
def _owner_org_markdown(classic_alert: 'ClassicAlert') -> list[str]:
|
|
48
52
|
results = []
|
|
49
53
|
details = classic_alert.owner_organisation_details
|
|
50
54
|
|
|
@@ -52,9 +56,9 @@ def _owner_org_markdown(classic_alert) -> list[str]:
|
|
|
52
56
|
return []
|
|
53
57
|
|
|
54
58
|
if details.enterprise_name:
|
|
55
|
-
results.append(f
|
|
59
|
+
results.append(f'{bold("Enterprise:")} {details.enterprise_name} ')
|
|
56
60
|
if details.owner_name:
|
|
57
|
-
results.append(f
|
|
61
|
+
results.append(f'{bold("Owner:")} {details.owner_name} ')
|
|
58
62
|
|
|
59
63
|
orgs = [[org.organisation_id, org.organisation_name] for org in details.organisations]
|
|
60
64
|
orgs.insert(0, ['Organisation ID', 'Organisation Name'])
|
|
@@ -64,24 +68,24 @@ def _owner_org_markdown(classic_alert) -> list[str]:
|
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def _process_hit_fragment(
|
|
67
|
-
hit, include_triggered_by: bool, html_tags: bool, classic_alert
|
|
71
|
+
hit, include_triggered_by: bool, html_tags: bool, classic_alert: 'ClassicAlert'
|
|
68
72
|
) -> tuple[str, str]:
|
|
69
73
|
content = []
|
|
70
74
|
authors = ', '.join(author.name for author in hit.document.authors)
|
|
71
75
|
|
|
72
76
|
title_line = f' From {hit.document.source.name}'
|
|
73
77
|
if authors:
|
|
74
|
-
content.append(f
|
|
78
|
+
content.append(f'{bold("Author(s):")} {authors}\n')
|
|
75
79
|
|
|
76
80
|
if hit.document.title and hit.fragment:
|
|
77
81
|
first_half_title = hit.document.title[: (len(hit.document.title) // 2)]
|
|
78
82
|
if not hit.fragment.lower().startswith(first_half_title.lower()):
|
|
79
|
-
content.append(f
|
|
83
|
+
content.append(f'{bold("Title:")} {clean_text(hit.document.title)}\n')
|
|
80
84
|
elif hit.document.title and not hit.fragment:
|
|
81
|
-
content.append(f
|
|
85
|
+
content.append(f'{bold("Title:")} {clean_text(hit.document.title)}\n')
|
|
82
86
|
|
|
83
87
|
if hit.document.url:
|
|
84
|
-
content.append(f
|
|
88
|
+
content.append(f'{bold("URL:")} {hit.document.url}\n')
|
|
85
89
|
|
|
86
90
|
if hit.fragment:
|
|
87
91
|
fragment = (
|
|
@@ -90,7 +94,7 @@ def _process_hit_fragment(
|
|
|
90
94
|
content.append(f'{blockquote(fragment)}\n')
|
|
91
95
|
else:
|
|
92
96
|
content.append(
|
|
93
|
-
f
|
|
97
|
+
f'_Reference text is missing, check the Recorded Future {link("Portal", str(classic_alert.url.portal))} for more information._\n' # noqa: E501
|
|
94
98
|
)
|
|
95
99
|
|
|
96
100
|
if include_triggered_by:
|
|
@@ -101,7 +105,7 @@ def _process_hit_fragment(
|
|
|
101
105
|
triggered_by = TRIGGERED_BY_HTML.format(triggered_by)
|
|
102
106
|
content.append(triggered_by)
|
|
103
107
|
else:
|
|
104
|
-
content.append(f
|
|
108
|
+
content.append(f'{bold("Triggered By:")}\n+ {triggered_by}\n')
|
|
105
109
|
|
|
106
110
|
return title_line, content
|
|
107
111
|
|
|
@@ -125,7 +129,7 @@ def _process_entities(entities, hit) -> list[list[str]]:
|
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
def _hits_markdown(
|
|
128
|
-
classic_alert,
|
|
132
|
+
classic_alert: 'ClassicAlert',
|
|
129
133
|
hits,
|
|
130
134
|
include_fragment_entities: bool = True,
|
|
131
135
|
include_triggered_by: bool = True,
|
|
@@ -167,7 +171,7 @@ def _hits_markdown(
|
|
|
167
171
|
return sections
|
|
168
172
|
|
|
169
173
|
|
|
170
|
-
def _enriched_entities_markdown(classic_alert) -> list:
|
|
174
|
+
def _enriched_entities_markdown(classic_alert: 'ClassicAlert') -> list:
|
|
171
175
|
results = []
|
|
172
176
|
for entity in classic_alert.enriched_entities:
|
|
173
177
|
if not entity.evidence:
|
|
@@ -175,10 +179,10 @@ def _enriched_entities_markdown(classic_alert) -> list:
|
|
|
175
179
|
|
|
176
180
|
criticality = entity.criticality
|
|
177
181
|
contents = [
|
|
178
|
-
f
|
|
179
|
-
f
|
|
180
|
-
f
|
|
181
|
-
f
|
|
182
|
+
f'{bold("Risk Score:")} {criticality.score}',
|
|
183
|
+
f'{bold("Criticality:")} {criticality.name}',
|
|
184
|
+
f'{bold("Triggered:")} {criticality.triggered.strftime(TIMESTAMP_STR)}',
|
|
185
|
+
f'{bold("Last Triggered:")} {criticality.last_triggered.strftime(TIMESTAMP_STR)} \n\n',
|
|
182
186
|
]
|
|
183
187
|
|
|
184
188
|
evidences = []
|
|
@@ -197,7 +201,7 @@ def _enriched_entities_markdown(classic_alert) -> list:
|
|
|
197
201
|
return results
|
|
198
202
|
|
|
199
203
|
|
|
200
|
-
def _target_entities_markdown(classic_alert, html_tags: bool = False) -> list:
|
|
204
|
+
def _target_entities_markdown(classic_alert: 'ClassicAlert', html_tags: bool = False) -> list:
|
|
201
205
|
results = []
|
|
202
206
|
for entity in classic_alert.enriched_entities:
|
|
203
207
|
result = {'title': f'Target {entity.entity.name}'}
|
|
@@ -213,17 +217,16 @@ def _target_entities_markdown(classic_alert, html_tags: bool = False) -> list:
|
|
|
213
217
|
return results
|
|
214
218
|
|
|
215
219
|
|
|
216
|
-
def _create_summary_section(ca) -> None:
|
|
217
|
-
|
|
218
|
-
f
|
|
219
|
-
f
|
|
220
|
-
f
|
|
221
|
-
f
|
|
220
|
+
def _create_summary_section(ca: 'ClassicAlert') -> None:
|
|
221
|
+
return [
|
|
222
|
+
f'{bold("ID:")} {ca.id_} ',
|
|
223
|
+
f'{bold("Triggered:")} {ca.log.triggered.strftime(TIMESTAMP_STR)} ',
|
|
224
|
+
f'{bold("Alerting Rule:")} {ca.rule.name} ',
|
|
225
|
+
f'{link("API", str(ca.url.api))} | {link("Portal", str(ca.url.portal))}',
|
|
222
226
|
]
|
|
223
|
-
return summary_content
|
|
224
227
|
|
|
225
228
|
|
|
226
|
-
def _get_entities_to_defang(classic_alert) -> set:
|
|
229
|
+
def _get_entities_to_defang(classic_alert: 'ClassicAlert') -> set:
|
|
227
230
|
"""Return a set of IOC entities to defang from the classic_alert hits."""
|
|
228
231
|
if not classic_alert.hits:
|
|
229
232
|
return set()
|
|
@@ -233,35 +236,38 @@ def _get_entities_to_defang(classic_alert) -> set:
|
|
|
233
236
|
for entity in chain.from_iterable(h.entities for h in classic_alert.hits)
|
|
234
237
|
if entity.type_ in MARKDOWN_ENTITY_TYPES_TO_DEFANG
|
|
235
238
|
}
|
|
236
|
-
|
|
237
|
-
return defanged_entities
|
|
239
|
+
return raw_entities.union({esc_format(ent, esc=True) for ent in raw_entities})
|
|
238
240
|
|
|
239
241
|
|
|
240
|
-
def _add_summary_section(md_maker: MarkdownMaker, classic_alert) -> None:
|
|
242
|
+
def _add_summary_section(md_maker: MarkdownMaker, classic_alert: 'ClassicAlert') -> None:
|
|
241
243
|
"""Adds the 'Summary' section to the markdown builder."""
|
|
242
244
|
md_maker.add_title(_clean_title(classic_alert.title))
|
|
243
245
|
md_maker.add_section('Summary', _create_summary_section(classic_alert))
|
|
244
246
|
|
|
245
247
|
|
|
246
|
-
def _add_owner_org_section(
|
|
248
|
+
def _add_owner_org_section(
|
|
249
|
+
md_maker: MarkdownMaker, classic_alert: 'ClassicAlert', owner_org: bool
|
|
250
|
+
) -> None:
|
|
247
251
|
"""Adds 'Owner Organisation Details' section if owner_org is True and details are present."""
|
|
248
252
|
if owner_org and classic_alert.owner_organisation_details:
|
|
249
253
|
md_maker.add_section('Owner Organisation Details', _owner_org_markdown(classic_alert))
|
|
250
254
|
|
|
251
255
|
|
|
252
|
-
def _add_ai_insights_section(
|
|
256
|
+
def _add_ai_insights_section(
|
|
257
|
+
md_maker: MarkdownMaker, classic_alert: 'ClassicAlert', ai_insights: bool
|
|
258
|
+
) -> None:
|
|
253
259
|
"""Adds the 'AI Insights' sections if ai_insights is True and data is present."""
|
|
254
260
|
if ai_insights and classic_alert.ai_insights:
|
|
255
261
|
if classic_alert.ai_insights.text:
|
|
256
262
|
md_maker.add_section('AI Insights', [classic_alert.ai_insights.text])
|
|
257
263
|
if classic_alert.ai_insights.comment:
|
|
258
264
|
md_maker.add_section(
|
|
259
|
-
'AI Insights', [f
|
|
265
|
+
'AI Insights', [f'{bold("Comment:")} {classic_alert.ai_insights.comment}']
|
|
260
266
|
)
|
|
261
267
|
|
|
262
268
|
|
|
263
269
|
def _add_enriched_entities_sections(
|
|
264
|
-
md_maker: MarkdownMaker, classic_alert, html_tags: bool
|
|
270
|
+
md_maker: MarkdownMaker, classic_alert: 'ClassicAlert', html_tags: bool
|
|
265
271
|
) -> None:
|
|
266
272
|
"""Adds sections related to enriched entities (evidence and references)."""
|
|
267
273
|
if any(x.evidence for x in classic_alert.enriched_entities):
|
|
@@ -277,7 +283,7 @@ def _add_enriched_entities_sections(
|
|
|
277
283
|
|
|
278
284
|
def _add_hits_section_if_no_enriched_entities(
|
|
279
285
|
md_maker: MarkdownMaker,
|
|
280
|
-
classic_alert,
|
|
286
|
+
classic_alert: 'ClassicAlert',
|
|
281
287
|
fragment_entities: bool,
|
|
282
288
|
triggered_by: bool,
|
|
283
289
|
html_tags: bool,
|
|
@@ -297,7 +303,7 @@ def _add_hits_section_if_no_enriched_entities(
|
|
|
297
303
|
|
|
298
304
|
|
|
299
305
|
def _markdown_alert(
|
|
300
|
-
classic_alert,
|
|
306
|
+
classic_alert: 'ClassicAlert',
|
|
301
307
|
owner_org: bool = False,
|
|
302
308
|
ai_insights: bool = True,
|
|
303
309
|
fragment_entities: bool = True,
|
|
@@ -22,8 +22,8 @@ DEFAULT_MAX_WORKERS = 10
|
|
|
22
22
|
#####################
|
|
23
23
|
# Recorded Future API
|
|
24
24
|
#####################
|
|
25
|
-
RF_TOKEN_ENV_VAR = 'RF_TOKEN'
|
|
26
|
-
RF_TOKEN_VALIDATION_REGEX = r'^[a-f0-9]{32}$'
|
|
25
|
+
RF_TOKEN_ENV_VAR = 'RF_TOKEN' # noqa: S105
|
|
26
|
+
RF_TOKEN_VALIDATION_REGEX = r'^[a-f0-9]{32}$' # noqa: S105
|
|
27
27
|
|
|
28
28
|
#####################
|
|
29
29
|
# Recorded Future Portal
|
|
@@ -47,9 +47,9 @@ def save_rule(rule: DetectionRule, output_directory: Union[str, Path] = None):
|
|
|
47
47
|
output_directory = Path(output_directory).absolute() if output_directory else Path().cwd()
|
|
48
48
|
OSHelpers.mkdir(output_directory)
|
|
49
49
|
|
|
50
|
-
for data in rule.rules:
|
|
50
|
+
for i, data in enumerate(rule.rules):
|
|
51
51
|
try:
|
|
52
|
-
full_path = output_directory / data.file_name
|
|
52
|
+
full_path = output_directory / (data.file_name or f'{rule.id_.replace(":", "_")}_{i}')
|
|
53
53
|
full_path.write_text(data.content)
|
|
54
54
|
LOG.info(f'Wrote: {full_path}')
|
|
55
55
|
except (FileNotFoundError, IsADirectoryError, PermissionError, OSError) as err: # noqa: PERF203
|
|
@@ -51,11 +51,13 @@ EP_FUSION_FILES = CONNECT_API_BASE_URL + '/fusion/files'
|
|
|
51
51
|
EP_PLAYBOOK_ALERT = BASE_URL + '/playbook-alert'
|
|
52
52
|
EP_PLAYBOOK_ALERT_SEARCH = EP_PLAYBOOK_ALERT + '/search'
|
|
53
53
|
EP_PLAYBOOK_ALERT_COMMON = EP_PLAYBOOK_ALERT + '/common'
|
|
54
|
-
EP_PLAYBOOK_ALERT_DOMAIN_ABUSE = EP_PLAYBOOK_ALERT + '/domain_abuse'
|
|
55
|
-
EP_PLAYBOOK_ALERT_CYBER_VULNERABILITY = EP_PLAYBOOK_ALERT + '/vulnerability'
|
|
56
54
|
EP_PLAYBOOK_ALERT_CODE_REPO_LEAKAGE = EP_PLAYBOOK_ALERT + '/code_repo_leakage'
|
|
57
|
-
|
|
55
|
+
EP_PLAYBOOK_ALERT_CYBER_VULNERABILITY = EP_PLAYBOOK_ALERT + '/vulnerability'
|
|
56
|
+
EP_PLAYBOOK_ALERT_DOMAIN_ABUSE = EP_PLAYBOOK_ALERT + '/domain_abuse'
|
|
57
|
+
EP_PLAYBOOK_ALERT_GEOPOLITICS_FACILITY = EP_PLAYBOOK_ALERT + '/geopolitics_facility'
|
|
58
58
|
EP_PLAYBOOK_ALERT_IDENTITY_NOVEL_EXPOSURES = EP_PLAYBOOK_ALERT + '/identity_novel_exposures'
|
|
59
|
+
EP_PLAYBOOK_ALERT_THIRD_PARTY_RISK = EP_PLAYBOOK_ALERT + '/third_party_risk'
|
|
60
|
+
EP_PLAYBOOK_ALERT_MALWARE_REPORT = EP_PLAYBOOK_ALERT + '/malware_report'
|
|
59
61
|
|
|
60
62
|
###############################################################################
|
|
61
63
|
# Entity Match Endpoint
|