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.
Files changed (140) hide show
  1. psengine-2.2.0/PKG-INFO +103 -0
  2. psengine-2.2.0/README.md +49 -0
  3. {psengine-2.1.1 → psengine-2.2.0}/psengine/_version.py +1 -1
  4. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/models.py +7 -3
  5. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/note_mgr.py +9 -9
  6. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/classic_alert_mgr.py +6 -4
  7. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/markdown/markdown.py +11 -8
  8. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/collective_insights.py +0 -5
  9. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/insight.py +8 -3
  10. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/detection_mgr.py +5 -6
  11. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/models.py +8 -5
  12. {psengine-2.1.1 → psengine-2.2.0}/psengine/endpoints.py +1 -1
  13. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/constants.py +14 -17
  14. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/lookup.py +18 -0
  15. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/lookup_mgr.py +3 -1
  16. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/soar_mgr.py +2 -2
  17. {psengine-2.1.1 → psengine-2.2.0}/psengine/helpers/helpers.py +12 -5
  18. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/__init__.py +1 -0
  19. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_geopolitics_facility.py +2 -3
  20. {psengine-2.1.1 → psengine-2.2.0}/psengine/rf_client.py +1 -1
  21. psengine-2.2.0/psengine.egg-info/PKG-INFO +103 -0
  22. {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/SOURCES.txt +1 -1
  23. {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/requires.txt +8 -4
  24. {psengine-2.1.1 → psengine-2.2.0}/pyproject.toml +10 -6
  25. psengine-2.1.1/PKG-INFO +0 -196
  26. psengine-2.1.1/README.rst +0 -146
  27. psengine-2.1.1/psengine.egg-info/PKG-INFO +0 -196
  28. {psengine-2.1.1 → psengine-2.2.0}/LICENSE +0 -0
  29. {psengine-2.1.1 → psengine-2.2.0}/psengine/__init__.py +0 -0
  30. {psengine-2.1.1 → psengine-2.2.0}/psengine/_sdk_id.py +0 -0
  31. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/__init__.py +0 -0
  32. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/constants.py +0 -0
  33. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/errors.py +0 -0
  34. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/helpers.py +0 -0
  35. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/markdown.py +0 -0
  36. {psengine-2.1.1 → psengine-2.2.0}/psengine/analyst_notes/note.py +0 -0
  37. {psengine-2.1.1 → psengine-2.2.0}/psengine/base_http_client.py +0 -0
  38. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/__init__.py +0 -0
  39. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/classic_alert.py +0 -0
  40. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/constants.py +0 -0
  41. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/errors.py +0 -0
  42. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/helpers.py +0 -0
  43. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/markdown/__init__.py +0 -0
  44. {psengine-2.1.1 → psengine-2.2.0}/psengine/classic_alerts/models.py +0 -0
  45. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/__init__.py +0 -0
  46. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/constants.py +0 -0
  47. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/errors.py +0 -0
  48. {psengine-2.1.1 → psengine-2.2.0}/psengine/collective_insights/models.py +0 -0
  49. {psengine-2.1.1 → psengine-2.2.0}/psengine/common_models.py +0 -0
  50. {psengine-2.1.1 → psengine-2.2.0}/psengine/config/__init__.py +0 -0
  51. {psengine-2.1.1 → psengine-2.2.0}/psengine/config/config.py +0 -0
  52. {psengine-2.1.1 → psengine-2.2.0}/psengine/config/errors.py +0 -0
  53. {psengine-2.1.1 → psengine-2.2.0}/psengine/constants.py +0 -0
  54. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/__init__.py +0 -0
  55. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/detection_rule.py +0 -0
  56. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/errors.py +0 -0
  57. {psengine-2.1.1 → psengine-2.2.0}/psengine/detection/helpers.py +0 -0
  58. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/__init__.py +0 -0
  59. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/errors.py +0 -0
  60. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/__init__.py +0 -0
  61. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/base_enriched_entity.py +0 -0
  62. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/lookup.py +0 -0
  63. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/models/soar.py +0 -0
  64. {psengine-2.1.1 → psengine-2.2.0}/psengine/enrich/soar.py +0 -0
  65. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/__init__.py +0 -0
  66. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/constants.py +0 -0
  67. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/entity_list.py +0 -0
  68. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/entity_list_mgr.py +0 -0
  69. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/errors.py +0 -0
  70. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_lists/models.py +0 -0
  71. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/__init__.py +0 -0
  72. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/entity_match.py +0 -0
  73. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/entity_match_mgr.py +0 -0
  74. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/errors.py +0 -0
  75. {psengine-2.1.1 → psengine-2.2.0}/psengine/entity_match/models.py +0 -0
  76. {psengine-2.1.1 → psengine-2.2.0}/psengine/errors.py +0 -0
  77. {psengine-2.1.1 → psengine-2.2.0}/psengine/helpers/__init__.py +0 -0
  78. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/__init__.py +0 -0
  79. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/constants.py +0 -0
  80. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/errors.py +0 -0
  81. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/identity.py +0 -0
  82. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/identity_mgr.py +0 -0
  83. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/__init__.py +0 -0
  84. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/common_models.py +0 -0
  85. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/detections.py +0 -0
  86. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/incident_report.py +0 -0
  87. {psengine-2.1.1 → psengine-2.2.0}/psengine/identity/models/lookup.py +0 -0
  88. {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/__init__.py +0 -0
  89. {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/constants.py +0 -0
  90. {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/errors.py +0 -0
  91. {psengine-2.1.1 → psengine-2.2.0}/psengine/logger/rf_logger.py +0 -0
  92. {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/__init__.py +0 -0
  93. {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/markdown.py +0 -0
  94. {psengine-2.1.1 → psengine-2.2.0}/psengine/markdown/models.py +0 -0
  95. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/constants.py +0 -0
  96. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/errors.py +0 -0
  97. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/helpers.py +0 -0
  98. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/mappings.py +0 -0
  99. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/__init__.py +0 -0
  100. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown.py +0 -0
  101. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_code_repo.py +0 -0
  102. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_cyber_vulnerability.py +0 -0
  103. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_domain_abuse.py +0 -0
  104. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_identity_exposure.py +0 -0
  105. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_malware_report.py +0 -0
  106. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/markdown/markdown_third_party_risk.py +0 -0
  107. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/__init__.py +0 -0
  108. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/common_models.py +0 -0
  109. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/panel_log.py +0 -0
  110. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/panel_status.py +0 -0
  111. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_code_repo_leak.py +0 -0
  112. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_cyber_vulnerability.py +0 -0
  113. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_domain_abuse.py +0 -0
  114. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_geopolitics_facility.py +0 -0
  115. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_identity_exposures.py +0 -0
  116. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_malware_report.py +0 -0
  117. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/pba_third_party_risk.py +0 -0
  118. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/models/search_endpoint.py +0 -0
  119. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/pa_category.py +0 -0
  120. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/playbook_alert_mgr.py +0 -0
  121. {psengine-2.1.1 → psengine-2.2.0}/psengine/playbook_alerts/playbook_alerts.py +0 -0
  122. {psengine-2.1.1 → psengine-2.2.0}/psengine/py.typed +0 -0
  123. {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/__init__.py +0 -0
  124. {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/constants.py +0 -0
  125. {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/errors.py +0 -0
  126. {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/models.py +0 -0
  127. {psengine-2.1.1 → psengine-2.2.0}/psengine/risklists/risklist_mgr.py +0 -0
  128. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/__init__.py +0 -0
  129. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/base_stix_entity.py +0 -0
  130. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/complex_entity.py +0 -0
  131. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/constants.py +0 -0
  132. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/enriched_indicator.py +0 -0
  133. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/errors.py +0 -0
  134. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/helpers.py +0 -0
  135. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/rf_bundle.py +0 -0
  136. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/simple_entity.py +0 -0
  137. {psengine-2.1.1 → psengine-2.2.0}/psengine/stix2/util.py +0 -0
  138. {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/dependency_links.txt +0 -0
  139. {psengine-2.1.1 → psengine-2.2.0}/psengine.egg-info/top_level.txt +0 -0
  140. {psengine-2.1.1 → psengine-2.2.0}/setup.cfg +0 -0
@@ -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
+
@@ -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
+
@@ -11,4 +11,4 @@
11
11
  # accessed from any third party API. #
12
12
  ##############################################################################################
13
13
 
14
- __version__ = '2.1.1'
14
+ __version__ = '2.2.0'
@@ -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: Union[list[str], str, None] = []
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 psengine.analyst_notes import save_attachment
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 search result.
160
+ Fields to include in the fetch result.
161
161
 
162
162
  **Note:**
163
- Defaults fields are `id`, `log`, `title`, `rule` which are always retrieved.
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 search result.
220
+ Fields to include in the fetch result.
220
221
 
221
222
  **Note:**
222
- Defaults fields are `id`, `log`, `title`, `rule` which are always retrieved.
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 ..classic_alerts.classic_alert import ClassicAlert
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 = True,
136
- include_triggered_by: bool = True,
137
- html_tags: bool = False,
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(classic_alert: 'ClassicAlert', html_tags: bool = False) -> list:
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]] = None
63
- malwares: Optional[list[str]] = None
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], str, None], Doc('Types of detection rules to search for.')
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: Optional[list[DetectionRuleType]] = None
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 = CONNECT_API_BASE_URL + '/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 = 2000
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
- 'InternetDomainName',
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
- 'Organization': 'company',
53
- 'Company': 'company',
54
- 'IpAddress': 'ip',
55
- 'InternetDomainName': 'domain',
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
- `v2/soar/enrichment`
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
- Defaults to None.
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
- value = value if isinstance(value, list) else [value]
463
- return [v for v in value if v is not None]
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(
@@ -28,6 +28,7 @@ from .playbook_alerts import (
28
28
  PBA_CyberVulnerability,
29
29
  PBA_DomainAbuse,
30
30
  PBA_Generic,
31
+ PBA_GeopoliticsFacility,
31
32
  PBA_IdentityNovelExposure,
32
33
  PBA_MalwareReport,
33
34
  PBA_ThirdPartyRisk,
@@ -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.panel_events_summary.events:
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.panel_events_summary.events:
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.must match regex {RF_TOKEN_VALIDATION_REGEX}'
87
+ f'Invalid Recorded Future API token: must match regex {RF_TOKEN_VALIDATION_REGEX}'
88
88
  )
89
89
 
90
90
  @debug_call