psengine 2.1.1__tar.gz → 2.2.0__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.2.0/PKG-INFO +103 -0
- psengine-2.2.0/README.md +49 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/_version.py +1 -1
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/models.py +7 -3
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/note_mgr.py +9 -9
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/classic_alert_mgr.py +6 -4
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/markdown/markdown.py +11 -8
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/collective_insights.py +0 -5
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/insight.py +8 -3
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/detection_mgr.py +5 -6
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/models.py +8 -5
- {psengine-2.1.1 → psengine-2.2.0}/psengine/endpoints.py +1 -1
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/constants.py +14 -17
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/lookup.py +18 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/lookup_mgr.py +3 -1
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/soar_mgr.py +2 -2
- {psengine-2.1.1 → psengine-2.2.0}/psengine/helpers/helpers.py +12 -5
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/__init__.py +1 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +2 -3
- {psengine-2.1.1 → psengine-2.2.0}/psengine/rf_client.py +1 -1
- psengine-2.2.0/psengine.egg-info/PKG-INFO +103 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/SOURCES.txt +1 -1
- {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/requires.txt +8 -4
- {psengine-2.1.1 → psengine-2.2.0}/pyproject.toml +10 -6
- psengine-2.1.1/PKG-INFO +0 -196
- psengine-2.1.1/README.rst +0 -146
- psengine-2.1.1/psengine.egg-info/PKG-INFO +0 -196
- {psengine-2.1.1 → psengine-2.2.0}/LICENSE +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/_sdk_id.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/helpers.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/markdown.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/note.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/base_http_client.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/classic_alert.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/helpers.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/common_models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/config/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/config/config.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/config/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/detection_rule.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/helpers.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/lookup.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/soar.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/soar.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/entity_list.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/entity_match.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/entity_match_mgr.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/helpers/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/identity.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/identity_mgr.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/common_models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/detections.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/incident_report.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/lookup.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/rf_logger.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/markdown.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/helpers.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/mappings.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/common_models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/pa_category.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/py.typed +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/models.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/risklist_mgr.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/__init__.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/base_stix_entity.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/complex_entity.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/constants.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/enriched_indicator.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/errors.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/helpers.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/rf_bundle.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/simple_entity.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/util.py +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/dependency_links.txt +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/top_level.txt +0 -0
- {psengine-2.1.1 → psengine-2.2.0}/setup.cfg +0 -0
psengine-2.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: psengine
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: psengine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
|
|
5
|
+
Author-email: Moise Medici <moise.medici@recordedfuture.com>, Patrick Kinsella <patrick.kinsella@recordedfuture.com>, Ernest Bartosevic <ernest.bartosevic@recordedfuture.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/RecordedFuture-ProfessionalServices/psengine
|
|
8
|
+
Project-URL: Changelog, https://recordedfuture-professionalservices.github.io/psengine/CHANGELOG/
|
|
9
|
+
Keywords: API,Recorded Future,Cyber Security Engineering,Threat Intelligence
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: <3.14,>=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests>=2.27.1
|
|
24
|
+
Requires-Dist: jsonpath_ng<=1.6.1,>=1.5.3
|
|
25
|
+
Requires-Dist: stix2~=3.0.1
|
|
26
|
+
Requires-Dist: python-dateutil>=2.7.0
|
|
27
|
+
Requires-Dist: more-itertools<=10.2.0,>=9.0.0
|
|
28
|
+
Requires-Dist: pydantic<3.0.0,>=2.7
|
|
29
|
+
Requires-Dist: pydantic-settings[toml]<2.11.0,>=2.5.2
|
|
30
|
+
Requires-Dist: markdown-strings==3.4.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest==8.3.4; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov==6.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-mock==3.14.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-md==0.2.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-random-order==1.1.1; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-httpdbg==0.9.0; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff~=0.11.0; extra == "dev"
|
|
39
|
+
Requires-Dist: mimesis>=12.1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: build==1.3.0; extra == "dev"
|
|
41
|
+
Requires-Dist: wheel==0.45.1; extra == "dev"
|
|
42
|
+
Requires-Dist: setuptools==80.9.0; extra == "dev"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: ruff~=0.11.0; extra == "docs"
|
|
45
|
+
Requires-Dist: mike~=2.1.3; extra == "docs"
|
|
46
|
+
Requires-Dist: mkdocs~=1.6.1; extra == "docs"
|
|
47
|
+
Requires-Dist: mkdocs-material~=9.6.18; extra == "docs"
|
|
48
|
+
Requires-Dist: mkdocstrings[python]>=0.18; extra == "docs"
|
|
49
|
+
Requires-Dist: griffe-typingdoc~=0.2.8; extra == "docs"
|
|
50
|
+
Requires-Dist: mkdocs-codeinclude-plugin~=0.2.1; extra == "docs"
|
|
51
|
+
Requires-Dist: markdown-include~=0.8.1; extra == "docs"
|
|
52
|
+
Requires-Dist: mkdocs-exclude~=1.0.2; extra == "docs"
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
**Documentation**: <https://recordedfuture-professionalservices.github.io/psengine/>
|
|
56
|
+
|
|
57
|
+
**Github**: <https://github.com/RecordedFuture-ProfessionalServices/psengine>
|
|
58
|
+
|
|
59
|
+
**PyPi**: <https://pypi.org/project/psengine/>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
PSEngine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
|
|
64
|
+
|
|
65
|
+
PSEngine allows you to interact with the Recorded Future API extremely easily. There’s no need to manually build the URLs and query parameters, just use the modules dedicated to individual API endpoints.
|
|
66
|
+
|
|
67
|
+
PSEngine is a Python package solely built and maintained by the Cyber Security Engineering team powering a number of high profile integrations, such as: Elasticsearch, QRadar, Anomali, Jira, TheHive, etc.
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
PSEngine is a Python package that can be installed using `pip`. To install PSengine, run the following command:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install psengine
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
PSEngine officially supports Python >= 3.9, < 3.14.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Supported Features & Best Practices
|
|
81
|
+
|
|
82
|
+
PSEngine is ready for the demands of building robust and reliable integrations.
|
|
83
|
+
|
|
84
|
+
It can easily interact with the following Recorded Future datasets:
|
|
85
|
+
|
|
86
|
+
* Analyst Notes
|
|
87
|
+
* Collective Insights
|
|
88
|
+
* Classic & Playbook Alerts
|
|
89
|
+
* Detection Rules
|
|
90
|
+
- Identity Exposures management
|
|
91
|
+
* List management
|
|
92
|
+
* On demand IOC enrichment
|
|
93
|
+
* Risklists
|
|
94
|
+
- STIX conversion
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
And facilitate the development with features like:
|
|
98
|
+
|
|
99
|
+
* Built-in logging
|
|
100
|
+
* Easy configuration management
|
|
101
|
+
- Markdown creation from certain data types
|
|
102
|
+
* Proxy support
|
|
103
|
+
|
psengine-2.2.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
**Documentation**: <https://recordedfuture-professionalservices.github.io/psengine/>
|
|
2
|
+
|
|
3
|
+
**Github**: <https://github.com/RecordedFuture-ProfessionalServices/psengine>
|
|
4
|
+
|
|
5
|
+
**PyPi**: <https://pypi.org/project/psengine/>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
PSEngine is a simple, yet elegant, library for rapid development of integrations with Recorded Future.
|
|
10
|
+
|
|
11
|
+
PSEngine allows you to interact with the Recorded Future API extremely easily. There’s no need to manually build the URLs and query parameters, just use the modules dedicated to individual API endpoints.
|
|
12
|
+
|
|
13
|
+
PSEngine is a Python package solely built and maintained by the Cyber Security Engineering team powering a number of high profile integrations, such as: Elasticsearch, QRadar, Anomali, Jira, TheHive, etc.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
PSEngine is a Python package that can be installed using `pip`. To install PSengine, run the following command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install psengine
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
PSEngine officially supports Python >= 3.9, < 3.14.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Supported Features & Best Practices
|
|
27
|
+
|
|
28
|
+
PSEngine is ready for the demands of building robust and reliable integrations.
|
|
29
|
+
|
|
30
|
+
It can easily interact with the following Recorded Future datasets:
|
|
31
|
+
|
|
32
|
+
* Analyst Notes
|
|
33
|
+
* Collective Insights
|
|
34
|
+
* Classic & Playbook Alerts
|
|
35
|
+
* Detection Rules
|
|
36
|
+
- Identity Exposures management
|
|
37
|
+
* List management
|
|
38
|
+
* On demand IOC enrichment
|
|
39
|
+
* Risklists
|
|
40
|
+
- STIX conversion
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
And facilitate the development with features like:
|
|
44
|
+
|
|
45
|
+
* Built-in logging
|
|
46
|
+
* Easy configuration management
|
|
47
|
+
- Markdown creation from certain data types
|
|
48
|
+
* Proxy support
|
|
49
|
+
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
15
|
from datetime import datetime
|
|
16
|
-
from typing import Any, Optional, Union
|
|
16
|
+
from typing import Annotated, Any, Optional, Union
|
|
17
17
|
|
|
18
|
-
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
18
|
+
from pydantic import BeforeValidator, Field, ValidationError, field_validator, model_validator
|
|
19
19
|
|
|
20
20
|
from ..common_models import IdNameType, IdNameTypeDescription, RFBaseModel
|
|
21
|
+
from ..helpers import Validators
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class DiamondModel(RFBaseModel):
|
|
@@ -198,7 +199,10 @@ class PreviewAttributesIn(RFBaseModel):
|
|
|
198
199
|
text: str
|
|
199
200
|
note_entities: Optional[list[str]] = []
|
|
200
201
|
context_entities: Optional[list[str]] = []
|
|
201
|
-
topic:
|
|
202
|
+
topic: Annotated[
|
|
203
|
+
Union[list[str], str, None],
|
|
204
|
+
BeforeValidator(Validators.convert_str_to_list),
|
|
205
|
+
] = []
|
|
202
206
|
labels: Optional[list[str]] = []
|
|
203
207
|
validation_urls: Optional[list[str]] = []
|
|
204
208
|
|
|
@@ -210,9 +210,6 @@ class AnalystNoteMgr:
|
|
|
210
210
|
ValidationError: If any supplied parameter is of incorrect type.
|
|
211
211
|
AnalystNotePreviewRequest: If connection error occurs.
|
|
212
212
|
"""
|
|
213
|
-
if topic:
|
|
214
|
-
topic = topic if isinstance(topic, list) else [topic]
|
|
215
|
-
|
|
216
213
|
data = {
|
|
217
214
|
'attributes': {
|
|
218
215
|
'title': title,
|
|
@@ -262,9 +259,6 @@ class AnalystNoteMgr:
|
|
|
262
259
|
ValidationError: If any supplied parameter is of incorrect type.
|
|
263
260
|
AnalystNotePublishError: If connection error occurs.
|
|
264
261
|
"""
|
|
265
|
-
if topic:
|
|
266
|
-
topic = topic if isinstance(topic, list) else [topic]
|
|
267
|
-
|
|
268
262
|
data = {
|
|
269
263
|
'attributes': {
|
|
270
264
|
'title': title,
|
|
@@ -308,15 +302,21 @@ class AnalystNoteMgr:
|
|
|
308
302
|
Fetch and save an attachment from an analyst note:
|
|
309
303
|
|
|
310
304
|
```python
|
|
311
|
-
from
|
|
305
|
+
from pathlib import Path
|
|
306
|
+
|
|
307
|
+
from psengine.analyst_notes import AnalystNoteMgr, save_attachment
|
|
308
|
+
|
|
309
|
+
OUTPUT_DIR = Path(__file__).parent / 'attachments'
|
|
310
|
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
312
311
|
|
|
313
312
|
# Note with PDF attachment
|
|
314
313
|
attachment, extension = note_mgr.fetch_attachment('tPtLVw')
|
|
315
|
-
save_attachment('tPtLVw', attachment, extension)
|
|
314
|
+
save_attachment('tPtLVw', attachment, extension, OUTPUT_DIR)
|
|
316
315
|
|
|
317
316
|
# Note with YAR attachment
|
|
318
317
|
attachment, extension = note_mgr.fetch_attachment('oJeqDP')
|
|
319
|
-
save_attachment('oJeqDP', attachment, extension)
|
|
318
|
+
save_attachment('oJeqDP', attachment, extension, OUTPUT_DIR)
|
|
319
|
+
|
|
320
320
|
```
|
|
321
321
|
|
|
322
322
|
Raises:
|
|
@@ -157,10 +157,11 @@ class ClassicAlertMgr:
|
|
|
157
157
|
Optional[list[str]],
|
|
158
158
|
Doc(
|
|
159
159
|
"""
|
|
160
|
-
Fields to include in the
|
|
160
|
+
Fields to include in the fetch result.
|
|
161
161
|
|
|
162
162
|
**Note:**
|
|
163
|
-
|
|
163
|
+
All fields are collected by default. Specify the fields needed, however the fields
|
|
164
|
+
`id`, `log`, `title`, `rule` are always retrieved.
|
|
164
165
|
Any provided fields are added to these."
|
|
165
166
|
"""
|
|
166
167
|
),
|
|
@@ -216,10 +217,11 @@ class ClassicAlertMgr:
|
|
|
216
217
|
Optional[list[str]],
|
|
217
218
|
Doc(
|
|
218
219
|
"""
|
|
219
|
-
Fields to include in the
|
|
220
|
+
Fields to include in the fetch result.
|
|
220
221
|
|
|
221
222
|
**Note:**
|
|
222
|
-
|
|
223
|
+
All fields are collected by default. Specify the fields needed, however the fields
|
|
224
|
+
`id`, `log`, `title`, `rule` are always retrieved.
|
|
223
225
|
Any provided fields are added to these."
|
|
224
226
|
"""
|
|
225
227
|
),
|
|
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
|
|
|
18
18
|
from markdown_strings import blockquote, bold, esc_format, link
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
from
|
|
21
|
+
from ...classic_alerts.classic_alert import ClassicAlert
|
|
22
22
|
|
|
23
23
|
from ...constants import TIMESTAMP_STR, TRUNCATE_COMMENT
|
|
24
24
|
from ...markdown import (
|
|
@@ -132,9 +132,9 @@ def _process_entities(entities, hit) -> list[list[str]]:
|
|
|
132
132
|
def _hits_markdown(
|
|
133
133
|
classic_alert: 'ClassicAlert',
|
|
134
134
|
hits,
|
|
135
|
-
include_fragment_entities: bool
|
|
136
|
-
include_triggered_by: bool
|
|
137
|
-
html_tags: bool
|
|
135
|
+
include_fragment_entities: bool,
|
|
136
|
+
include_triggered_by: bool,
|
|
137
|
+
html_tags: bool,
|
|
138
138
|
) -> list:
|
|
139
139
|
sections = []
|
|
140
140
|
for idx, hit in enumerate(hits):
|
|
@@ -202,7 +202,9 @@ def _enriched_entities_markdown(classic_alert: 'ClassicAlert') -> list:
|
|
|
202
202
|
return results
|
|
203
203
|
|
|
204
204
|
|
|
205
|
-
def _target_entities_markdown(
|
|
205
|
+
def _target_entities_markdown(
|
|
206
|
+
classic_alert: 'ClassicAlert', triggered_by: bool, html_tags: bool = False
|
|
207
|
+
) -> list:
|
|
206
208
|
results = []
|
|
207
209
|
for entity in classic_alert.enriched_entities:
|
|
208
210
|
result = {'title': f'Target {entity.entity.name}'}
|
|
@@ -210,6 +212,7 @@ def _target_entities_markdown(classic_alert: 'ClassicAlert', html_tags: bool = F
|
|
|
210
212
|
classic_alert,
|
|
211
213
|
hits=entity.references,
|
|
212
214
|
include_fragment_entities=False,
|
|
215
|
+
include_triggered_by=triggered_by,
|
|
213
216
|
html_tags=html_tags,
|
|
214
217
|
)
|
|
215
218
|
if len(result['content']):
|
|
@@ -268,7 +271,7 @@ def _add_ai_insights_section(
|
|
|
268
271
|
|
|
269
272
|
|
|
270
273
|
def _add_enriched_entities_sections(
|
|
271
|
-
md_maker: MarkdownMaker, classic_alert: 'ClassicAlert', html_tags: bool
|
|
274
|
+
md_maker: MarkdownMaker, classic_alert: 'ClassicAlert', triggered_by: bool, html_tags: bool
|
|
272
275
|
) -> None:
|
|
273
276
|
"""Adds sections related to enriched entities (evidence and references)."""
|
|
274
277
|
if any(x.evidence for x in classic_alert.enriched_entities):
|
|
@@ -278,7 +281,7 @@ def _add_enriched_entities_sections(
|
|
|
278
281
|
if any(x.references for x in classic_alert.enriched_entities):
|
|
279
282
|
md_maker.add_section(
|
|
280
283
|
'Target Entities',
|
|
281
|
-
_target_entities_markdown(classic_alert, html_tags),
|
|
284
|
+
_target_entities_markdown(classic_alert, triggered_by, html_tags),
|
|
282
285
|
)
|
|
283
286
|
|
|
284
287
|
|
|
@@ -350,7 +353,7 @@ def _markdown_alert(
|
|
|
350
353
|
_add_ai_insights_section(md_maker, classic_alert, ai_insights)
|
|
351
354
|
|
|
352
355
|
if classic_alert.enriched_entities:
|
|
353
|
-
_add_enriched_entities_sections(md_maker, classic_alert, html_tags)
|
|
356
|
+
_add_enriched_entities_sections(md_maker, classic_alert, triggered_by, html_tags)
|
|
354
357
|
else:
|
|
355
358
|
_add_hits_section_if_no_enriched_entities(
|
|
356
359
|
md_maker, classic_alert, fragment_entities, triggered_by, html_tags
|
|
@@ -67,11 +67,6 @@ class CollectiveInsights:
|
|
|
67
67
|
Raises:
|
|
68
68
|
ValidationError: If any supplied parameter is of incorrect type.
|
|
69
69
|
"""
|
|
70
|
-
malwares = malwares if isinstance(malwares, list) else [malwares] if malwares else None
|
|
71
|
-
mitre_codes = (
|
|
72
|
-
mitre_codes if isinstance(mitre_codes, list) else [mitre_codes] if mitre_codes else None
|
|
73
|
-
)
|
|
74
|
-
|
|
75
70
|
incident = {'id': incident_id, 'type': incident_type, 'name': incident_name}
|
|
76
71
|
detection = {
|
|
77
72
|
'id': detection_id,
|
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
from functools import total_ordering
|
|
16
|
-
from typing import Optional
|
|
16
|
+
from typing import Annotated, Optional
|
|
17
|
+
|
|
18
|
+
from pydantic import BeforeValidator
|
|
17
19
|
|
|
18
20
|
from ..common_models import IdNameType, RFBaseModel
|
|
19
21
|
from ..constants import TIMESTAMP_STR
|
|
22
|
+
from ..helpers import Validators
|
|
20
23
|
from .models import (
|
|
21
24
|
RequestDetection,
|
|
22
25
|
RequestIOC,
|
|
@@ -59,8 +62,10 @@ class Insight(RFBaseModel):
|
|
|
59
62
|
timestamp: datetime
|
|
60
63
|
ioc: RequestIOC
|
|
61
64
|
incident: Optional[IdNameType] = None
|
|
62
|
-
mitre_codes: Optional[list[str]] =
|
|
63
|
-
|
|
65
|
+
mitre_codes: Annotated[Optional[list[str]], BeforeValidator(Validators.convert_str_to_list)] = (
|
|
66
|
+
None
|
|
67
|
+
)
|
|
68
|
+
malwares: Annotated[Optional[list[str]], BeforeValidator(Validators.convert_str_to_list)] = None
|
|
64
69
|
detection: RequestDetection
|
|
65
70
|
|
|
66
71
|
def __hash__(self):
|
|
@@ -45,22 +45,22 @@ class DetectionMgr:
|
|
|
45
45
|
def search(
|
|
46
46
|
self,
|
|
47
47
|
detection_rule: Annotated[
|
|
48
|
-
Union[list[str],
|
|
48
|
+
Union[str, list[str], None], Doc('Types of detection rules to search for.')
|
|
49
49
|
] = None,
|
|
50
50
|
entities: Annotated[
|
|
51
51
|
Optional[list[str]], Doc('List of entities to filter the search.')
|
|
52
52
|
] = None,
|
|
53
53
|
created_before: Annotated[
|
|
54
|
-
Optional[str], Doc('Filter for rules created before this date.')
|
|
54
|
+
Optional[str], Doc('Filter for rules created before this date or relative date.')
|
|
55
55
|
] = None,
|
|
56
56
|
created_after: Annotated[
|
|
57
|
-
Optional[str], Doc('Filter for rules created after this date.')
|
|
57
|
+
Optional[str], Doc('Filter for rules created after this date or relative date.')
|
|
58
58
|
] = None,
|
|
59
59
|
updated_before: Annotated[
|
|
60
|
-
Optional[str], Doc('Filter for rules updated before this date.')
|
|
60
|
+
Optional[str], Doc('Filter for rules updated before this date or relative date.')
|
|
61
61
|
] = None,
|
|
62
62
|
updated_after: Annotated[
|
|
63
|
-
Optional[str], Doc('Filter for rules updated after this date.')
|
|
63
|
+
Optional[str], Doc('Filter for rules updated after this date or relative date.')
|
|
64
64
|
] = None,
|
|
65
65
|
doc_id: Annotated[Optional[str], Doc('Filter by document ID.')] = None,
|
|
66
66
|
title: Annotated[Optional[str], Doc('Filter by title.')] = None,
|
|
@@ -82,7 +82,6 @@ class DetectionMgr:
|
|
|
82
82
|
ValidationError: If any supplied parameter is of incorrect type.
|
|
83
83
|
DetectionRuleSearchError: If connection error occurs.
|
|
84
84
|
"""
|
|
85
|
-
detection_rule = [detection_rule] if isinstance(detection_rule, str) else detection_rule
|
|
86
85
|
filters = {
|
|
87
86
|
'types': detection_rule,
|
|
88
87
|
'entities': entities,
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
##############################################################################################
|
|
13
13
|
|
|
14
14
|
from datetime import datetime
|
|
15
|
-
from typing import Optional
|
|
15
|
+
from typing import Annotated, Optional
|
|
16
16
|
|
|
17
|
-
from pydantic import Field
|
|
17
|
+
from pydantic import BeforeValidator, Field
|
|
18
18
|
|
|
19
19
|
from ..common_models import DetectionRuleType, RFBaseModel
|
|
20
|
+
from ..helpers import Validators
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class Entity(RFBaseModel):
|
|
@@ -34,12 +35,14 @@ class RuleContext(RFBaseModel):
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class TimeRange(RFBaseModel):
|
|
37
|
-
after: Optional[datetime] = None
|
|
38
|
-
before: Optional[datetime] = None
|
|
38
|
+
after: Annotated[Optional[datetime], BeforeValidator(Validators.convert_relative_time)] = None
|
|
39
|
+
before: Annotated[Optional[datetime], BeforeValidator(Validators.convert_relative_time)] = None
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class SearchFilter(RFBaseModel):
|
|
42
|
-
types:
|
|
43
|
+
types: Annotated[
|
|
44
|
+
Optional[list[DetectionRuleType]], BeforeValidator(Validators.convert_str_to_list)
|
|
45
|
+
] = None
|
|
43
46
|
entities: Optional[list[str]] = None
|
|
44
47
|
created: Optional[TimeRange] = None
|
|
45
48
|
updated: Optional[TimeRange] = None
|
|
@@ -75,7 +75,7 @@ EP_SEARCH_LIST = EP_LIST + '/search'
|
|
|
75
75
|
###############################################################################
|
|
76
76
|
# SOAR Endpoints
|
|
77
77
|
###############################################################################
|
|
78
|
-
EP_SOAR_ENRICHMENT =
|
|
78
|
+
EP_SOAR_ENRICHMENT = BASE_URL + '/soar/v3/enrichment'
|
|
79
79
|
|
|
80
80
|
###############################################################################
|
|
81
81
|
# Detection Rules API Endpoints
|
|
@@ -23,40 +23,37 @@ from ..enrich import (
|
|
|
23
23
|
EnrichedVulnerability,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
SOAR_POST_ROWS =
|
|
26
|
+
SOAR_POST_ROWS = 1000
|
|
27
27
|
|
|
28
28
|
ALLOWED_ENTITIES = Literal[
|
|
29
|
+
'Company',
|
|
29
30
|
'company',
|
|
30
31
|
'company_by_domain',
|
|
31
32
|
'company/by_domain',
|
|
32
|
-
'domain',
|
|
33
|
-
'hash',
|
|
34
|
-
'ip',
|
|
35
|
-
'malware',
|
|
36
|
-
'url',
|
|
37
|
-
'vulnerability',
|
|
38
|
-
'Company',
|
|
39
33
|
'Organization',
|
|
40
|
-
'
|
|
34
|
+
'organization',
|
|
35
|
+
'hash',
|
|
41
36
|
'Hash',
|
|
37
|
+
'InternetDomainName',
|
|
38
|
+
'domain',
|
|
39
|
+
'ip',
|
|
42
40
|
'IpAddress',
|
|
43
41
|
'Malware',
|
|
42
|
+
'malware',
|
|
44
43
|
'URL',
|
|
44
|
+
'url',
|
|
45
45
|
'CyberVulnerability',
|
|
46
|
+
'vulnerability',
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
ENTITY_FIELDS = ['entity', 'risk', 'timestamps']
|
|
49
50
|
MALWARE_FIELDS = ['entity', 'timestamps']
|
|
50
51
|
TYPE_MAPPING = {
|
|
51
52
|
'company/by_domain': 'company_by_domain',
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'CyberVulnerability': 'vulnerability',
|
|
57
|
-
'URL': 'url',
|
|
58
|
-
'Malware': 'malware',
|
|
59
|
-
'Hash': 'hash',
|
|
53
|
+
'organization': 'company',
|
|
54
|
+
'ipaddress': 'ip',
|
|
55
|
+
'internetdomainname': 'domain',
|
|
56
|
+
'cybervulnerability': 'vulnerability',
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
|
|
@@ -291,3 +291,21 @@ class EnrichmentData(RFBaseModel):
|
|
|
291
291
|
f'Risk Score: {self.content.risk.score}, '
|
|
292
292
|
f'Last Seen: {self.content.timestamps.last_seen.strftime(TIMESTAMP_STR)}'
|
|
293
293
|
)
|
|
294
|
+
|
|
295
|
+
def links(self, from_section: str, entity_type: str) -> list[str]:
|
|
296
|
+
"""Retrieve a list of entities from the links attribute of the specific type and section."""
|
|
297
|
+
results = []
|
|
298
|
+
if not hasattr(self.content, 'links'):
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
results.extend(
|
|
302
|
+
entity.name
|
|
303
|
+
for hit in self.content.links.hits
|
|
304
|
+
for section in hit.sections
|
|
305
|
+
if section.section_id and section.section_id.name == from_section
|
|
306
|
+
for lst in section.lists
|
|
307
|
+
for entity in lst.entities
|
|
308
|
+
if entity.type_ == entity_type
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return results
|
|
@@ -72,6 +72,7 @@ class LookupMgr:
|
|
|
72
72
|
- `malware`
|
|
73
73
|
- `Malware`
|
|
74
74
|
- `Organization`
|
|
75
|
+
- `organization`
|
|
75
76
|
- `url`
|
|
76
77
|
- `URL`
|
|
77
78
|
- `vulnerability`
|
|
@@ -181,6 +182,7 @@ class LookupMgr:
|
|
|
181
182
|
- `malware`
|
|
182
183
|
- `Malware`
|
|
183
184
|
- `Organization`
|
|
185
|
+
- `organization`
|
|
184
186
|
- `url`
|
|
185
187
|
- `URL`
|
|
186
188
|
- `vulnerability`
|
|
@@ -273,7 +275,7 @@ class LookupMgr:
|
|
|
273
275
|
entity_type: str,
|
|
274
276
|
fields: list,
|
|
275
277
|
):
|
|
276
|
-
entity_type = TYPE_MAPPING.get(entity_type, entity_type)
|
|
278
|
+
entity_type = TYPE_MAPPING.get(entity_type.lower(), entity_type.lower())
|
|
277
279
|
|
|
278
280
|
enriched = self._fetch_data(
|
|
279
281
|
entity=entity,
|
|
@@ -61,7 +61,7 @@ class SoarMgr:
|
|
|
61
61
|
vulnerabilities, URLs, and company domains. Uses multithreading if `max_workers` > 0.
|
|
62
62
|
|
|
63
63
|
Endpoint:
|
|
64
|
-
`
|
|
64
|
+
`soar/v3/enrichment`
|
|
65
65
|
|
|
66
66
|
Example:
|
|
67
67
|
Simple bulk enrichment:
|
|
@@ -81,7 +81,7 @@ class SoarMgr:
|
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Save enriched results to file:
|
|
84
|
-
```
|
|
84
|
+
```python
|
|
85
85
|
from pathlib import Path
|
|
86
86
|
from json import dumps
|
|
87
87
|
from psengine.enrich import SoarMgr
|
|
@@ -46,17 +46,22 @@ VALID_TIME_REGEX = r'^(-?)([1-9]?[0-9]+[dDhH])$'
|
|
|
46
46
|
IDS = ['ip:', 'idn:', 'url:', 'hash:', 'id:']
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# Warning: this cannot be annotated with `Doc` since it breaks the IDE autocomplete.
|
|
49
50
|
def connection_exceptions(
|
|
50
51
|
ignore_status_code: list[int], exception_to_raise: RecordedFutureError, on_ignore_return=None
|
|
51
52
|
):
|
|
52
53
|
"""Decorator for handling HTTP related errors.
|
|
53
54
|
|
|
55
|
+
!!! warning:
|
|
56
|
+
|
|
57
|
+
This decorator should not be used in user code. It is meant for internal PSEngine methods.
|
|
58
|
+
|
|
54
59
|
Args:
|
|
55
60
|
ignore_status_code (List[int]): list of status codes to be ignored - dont raise exception
|
|
56
61
|
exception_to_raise (Exception): exception to raise in case of error. It should be based on
|
|
57
62
|
the function that is decorated
|
|
58
63
|
on_ignore_return (Any): whatever it is needed to be returned if the ignore_status happens.
|
|
59
|
-
|
|
64
|
+
Defaults to None.
|
|
60
65
|
|
|
61
66
|
Raises:
|
|
62
67
|
exception_to_raise
|
|
@@ -456,11 +461,13 @@ class Validators:
|
|
|
456
461
|
|
|
457
462
|
@staticmethod
|
|
458
463
|
def convert_str_to_list(
|
|
459
|
-
value: Annotated[Union[str, list], Doc('String or list to convert.')],
|
|
460
|
-
) -> Annotated[list, Doc('Converted list with None values removed.')]:
|
|
464
|
+
value: Annotated[Union[str, list, None], Doc('String or list to convert.')],
|
|
465
|
+
) -> Annotated[Union[list, None], Doc('Converted list with None values removed.')]:
|
|
461
466
|
"""Convert value from str to list and remove None values."""
|
|
462
|
-
|
|
463
|
-
|
|
467
|
+
if value:
|
|
468
|
+
value = value if isinstance(value, list) else [value]
|
|
469
|
+
return [v for v in value if v is not None]
|
|
470
|
+
return value
|
|
464
471
|
|
|
465
472
|
@staticmethod
|
|
466
473
|
def convert_relative_time(
|
{psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py
RENAMED
|
@@ -70,11 +70,10 @@ def _add_images(pba: 'PBA_GeopoliticsFacility', md_maker: MarkdownMaker) -> None
|
|
|
70
70
|
|
|
71
71
|
def _add_events(pba: 'PBA_GeopoliticsFacility', md_maker: MarkdownMaker) -> None:
|
|
72
72
|
result = []
|
|
73
|
-
for event in pba.
|
|
73
|
+
for event in pba.panel_evidence_summary.events:
|
|
74
74
|
section = [
|
|
75
75
|
f'{bold("When:")} {_format_timestamp(event.time)} ',
|
|
76
76
|
f'{bold("Source:")} {event.source} - {event.url} ',
|
|
77
|
-
f'{event.translated_text} ',
|
|
78
77
|
]
|
|
79
78
|
title = ', '.join(assessment.name for assessment in event.assessments)
|
|
80
79
|
result.append(bold(title))
|
|
@@ -96,7 +95,7 @@ def _geopolitics_facility_markdown(pba: 'PBA_GeopoliticsFacility', md_maker: Mar
|
|
|
96
95
|
if pba.panel_overview.ai_insights:
|
|
97
96
|
md_maker.add_section('AI Insights', f'{pba.panel_overview.ai_insights} ')
|
|
98
97
|
|
|
99
|
-
if pba.
|
|
98
|
+
if pba.panel_evidence_summary.events:
|
|
100
99
|
_add_events(pba, md_maker)
|
|
101
100
|
|
|
102
101
|
if not md_maker.character_limit:
|
|
@@ -84,7 +84,7 @@ class RFClient(BaseHTTPClient):
|
|
|
84
84
|
raise ValueError('Missing Recorded Future API token.')
|
|
85
85
|
if not is_api_token_format_valid(self._api_token):
|
|
86
86
|
raise ValueError(
|
|
87
|
-
f'Invalid Recorded Future API token
|
|
87
|
+
f'Invalid Recorded Future API token: must match regex {RF_TOKEN_VALIDATION_REGEX}'
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
@debug_call
|