psengine 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psengine/__init__.py +22 -0
- psengine/_sdk_id.py +16 -0
- psengine/_version.py +14 -0
- psengine/analyst_notes/__init__.py +32 -0
- psengine/analyst_notes/constants.py +15 -0
- psengine/analyst_notes/errors.py +42 -0
- psengine/analyst_notes/helpers.py +90 -0
- psengine/analyst_notes/models.py +219 -0
- psengine/analyst_notes/note.py +149 -0
- psengine/analyst_notes/note_mgr.py +400 -0
- psengine/base_http_client.py +285 -0
- psengine/classic_alerts/__init__.py +24 -0
- psengine/classic_alerts/classic_alert.py +275 -0
- psengine/classic_alerts/classic_alert_mgr.py +507 -0
- psengine/classic_alerts/constants.py +31 -0
- psengine/classic_alerts/errors.py +38 -0
- psengine/classic_alerts/helpers.py +87 -0
- psengine/classic_alerts/markdown/__init__.py +13 -0
- psengine/classic_alerts/markdown/markdown.py +359 -0
- psengine/classic_alerts/models.py +141 -0
- psengine/collective_insights/__init__.py +29 -0
- psengine/collective_insights/collective_insights.py +164 -0
- psengine/collective_insights/constants.py +44 -0
- psengine/collective_insights/errors.py +18 -0
- psengine/collective_insights/insight.py +89 -0
- psengine/collective_insights/models.py +81 -0
- psengine/common_models.py +89 -0
- psengine/config/__init__.py +15 -0
- psengine/config/config.py +284 -0
- psengine/config/errors.py +18 -0
- psengine/constants.py +63 -0
- psengine/detection/__init__.py +17 -0
- psengine/detection/detection_mgr.py +135 -0
- psengine/detection/detection_rule.py +85 -0
- psengine/detection/errors.py +26 -0
- psengine/detection/helpers.py +56 -0
- psengine/detection/models.py +47 -0
- psengine/endpoints.py +98 -0
- psengine/enrich/__init__.py +28 -0
- psengine/enrich/constants.py +73 -0
- psengine/enrich/errors.py +26 -0
- psengine/enrich/lookup.py +299 -0
- psengine/enrich/lookup_mgr.py +341 -0
- psengine/enrich/models/__init__.py +13 -0
- psengine/enrich/models/base_enriched_entity.py +43 -0
- psengine/enrich/models/lookup.py +271 -0
- psengine/enrich/models/soar.py +138 -0
- psengine/enrich/soar.py +89 -0
- psengine/enrich/soar_mgr.py +176 -0
- psengine/entity_lists/__init__.py +16 -0
- psengine/entity_lists/constants.py +19 -0
- psengine/entity_lists/entity_list.py +435 -0
- psengine/entity_lists/entity_list_mgr.py +185 -0
- psengine/entity_lists/errors.py +26 -0
- psengine/entity_lists/models.py +87 -0
- psengine/entity_match/__init__.py +16 -0
- psengine/entity_match/entity_match.py +90 -0
- psengine/entity_match/entity_match_mgr.py +235 -0
- psengine/entity_match/errors.py +18 -0
- psengine/entity_match/models.py +22 -0
- psengine/errors.py +41 -0
- psengine/helpers/__init__.py +23 -0
- psengine/helpers/helpers.py +471 -0
- psengine/logger/__init__.py +15 -0
- psengine/logger/constants.py +39 -0
- psengine/logger/errors.py +18 -0
- psengine/logger/rf_logger.py +148 -0
- psengine/markdown/__init__.py +21 -0
- psengine/markdown/markdown.py +169 -0
- psengine/markdown/models.py +22 -0
- psengine/playbook_alerts/__init__.py +34 -0
- psengine/playbook_alerts/constants.py +35 -0
- psengine/playbook_alerts/errors.py +35 -0
- psengine/playbook_alerts/helpers.py +80 -0
- psengine/playbook_alerts/mappings.py +44 -0
- psengine/playbook_alerts/markdown/__init__.py +13 -0
- psengine/playbook_alerts/markdown/markdown.py +98 -0
- psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
- psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
- psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
- psengine/playbook_alerts/models/__init__.py +36 -0
- psengine/playbook_alerts/models/common_models.py +18 -0
- psengine/playbook_alerts/models/panel_log.py +329 -0
- psengine/playbook_alerts/models/panel_status.py +70 -0
- psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
- psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
- psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
- psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
- psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
- psengine/playbook_alerts/models/search_endpoint.py +68 -0
- psengine/playbook_alerts/pa_category.py +37 -0
- psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
- psengine/playbook_alerts/playbook_alerts.py +393 -0
- psengine/rf_client.py +430 -0
- psengine/risklists/__init__.py +17 -0
- psengine/risklists/constants.py +15 -0
- psengine/risklists/errors.py +20 -0
- psengine/risklists/models.py +65 -0
- psengine/risklists/risklist_mgr.py +156 -0
- psengine/stix2/__init__.py +21 -0
- psengine/stix2/base_stix_entity.py +62 -0
- psengine/stix2/complex_entity.py +372 -0
- psengine/stix2/constants.py +81 -0
- psengine/stix2/enriched_indicator.py +261 -0
- psengine/stix2/errors.py +22 -0
- psengine/stix2/helpers.py +68 -0
- psengine/stix2/rf_bundle.py +240 -0
- psengine/stix2/simple_entity.py +145 -0
- psengine/stix2/util.py +53 -0
- psengine-2.0.4.dist-info/METADATA +189 -0
- psengine-2.0.4.dist-info/RECORD +115 -0
- psengine-2.0.4.dist-info/WHEEL +5 -0
- psengine-2.0.4.dist-info/entry_points.txt +2 -0
- psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
- psengine-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Optional, Union
|
|
16
|
+
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
from ...common_models import IdNameType, IdNameTypeDescription, RFBaseModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
###########################################################
|
|
23
|
+
# Enterprise Lists
|
|
24
|
+
###########################################################
|
|
25
|
+
class EnterpriseList(RFBaseModel):
|
|
26
|
+
added: Optional[datetime]
|
|
27
|
+
list_: IdNameTypeDescription = Field(alias='list')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
###########################################################
|
|
31
|
+
class RiskyCIDRPIP(RFBaseModel):
|
|
32
|
+
score: int
|
|
33
|
+
ip: IdNameType
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AIInsights(RFBaseModel):
|
|
37
|
+
comment: Optional[str] = None
|
|
38
|
+
text: Optional[str] = None
|
|
39
|
+
number_of_references: Optional[int] = Field(alias='numberOfReferences', default=None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EvidenceDetails(RFBaseModel):
|
|
43
|
+
mitigation_string: str = Field(alias='mitigationString')
|
|
44
|
+
evidence_string: str = Field(alias='evidenceString')
|
|
45
|
+
rule: str
|
|
46
|
+
criticality: int
|
|
47
|
+
timestamp: datetime
|
|
48
|
+
criticality_label: str = Field(alias='criticalityLabel')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EntityRisk(RFBaseModel):
|
|
52
|
+
criticality_label: str = Field(alias='criticalityLabel')
|
|
53
|
+
risk_string: str = Field(alias='riskString')
|
|
54
|
+
rules: int
|
|
55
|
+
criticality: int
|
|
56
|
+
risk_summary: str = Field(alias='riskSummary')
|
|
57
|
+
score: int
|
|
58
|
+
evidence_details: list[EvidenceDetails] = Field(alias='evidenceDetails')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Sighting(RFBaseModel):
|
|
62
|
+
source: str
|
|
63
|
+
url: str
|
|
64
|
+
published: datetime
|
|
65
|
+
fragment: str
|
|
66
|
+
title: str
|
|
67
|
+
type_: str = Field(alias='type')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RiskMappingCategory(RFBaseModel):
|
|
71
|
+
framework: str
|
|
72
|
+
name: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RiskMapping(RFBaseModel):
|
|
76
|
+
rule: str
|
|
77
|
+
categories: Optional[list[RiskMappingCategory]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RelatedEntity(RFBaseModel):
|
|
81
|
+
count: int
|
|
82
|
+
entity: IdNameTypeDescription
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RelatedEntities(RFBaseModel):
|
|
86
|
+
entities: list[RelatedEntity]
|
|
87
|
+
type_: str = Field(alias='type')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GeoLocation(RFBaseModel):
|
|
91
|
+
continent: Optional[str] = None
|
|
92
|
+
country: Optional[str] = None
|
|
93
|
+
city: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class IPLocation(RFBaseModel):
|
|
97
|
+
organization: Optional[str]
|
|
98
|
+
cidr: IdNameType
|
|
99
|
+
location: GeoLocation
|
|
100
|
+
asn: Optional[str] = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Timestamps(RFBaseModel):
|
|
104
|
+
last_seen: datetime = Field(alias='lastSeen')
|
|
105
|
+
first_seen: datetime = Field(alias='firstSeen')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ReferenceCount(RFBaseModel):
|
|
109
|
+
date: datetime
|
|
110
|
+
count: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Metric(RFBaseModel):
|
|
114
|
+
type_: str = Field(alias='type')
|
|
115
|
+
value: Union[int, float]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
###########################################################
|
|
119
|
+
# Links
|
|
120
|
+
###########################################################
|
|
121
|
+
class LinksCounts(RFBaseModel):
|
|
122
|
+
count: int
|
|
123
|
+
type_: IdNameTypeDescription = Field(alias='type')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class LinksList(RFBaseModel):
|
|
127
|
+
entities: list[IdNameTypeDescription]
|
|
128
|
+
total_count: int
|
|
129
|
+
type_: IdNameTypeDescription = Field(alias='type')
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class SectionHits(RFBaseModel):
|
|
133
|
+
section_id: IdNameType
|
|
134
|
+
total_count: int
|
|
135
|
+
lists: Optional[list[LinksList]] = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Hits(RFBaseModel):
|
|
139
|
+
sections: list[SectionHits]
|
|
140
|
+
start_date: datetime
|
|
141
|
+
stop_date: datetime
|
|
142
|
+
total_count: int
|
|
143
|
+
sample_reference_ids: list[str]
|
|
144
|
+
counts: list[LinksCounts]
|
|
145
|
+
event_count: int
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class MethodAggregate(RFBaseModel):
|
|
149
|
+
count: int
|
|
150
|
+
type_: str = Field(alias='type')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Links(RFBaseModel):
|
|
154
|
+
hits: list[Hits]
|
|
155
|
+
method_aggregates: list[MethodAggregate]
|
|
156
|
+
counts: list[LinksCounts]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
###########################################################
|
|
160
|
+
# Linked Malware
|
|
161
|
+
###########################################################
|
|
162
|
+
class LinkedMalware(RFBaseModel):
|
|
163
|
+
entities: list[IdNameType]
|
|
164
|
+
total_count: int
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
###########################################################
|
|
168
|
+
# CVSS
|
|
169
|
+
###########################################################
|
|
170
|
+
class CVSS(RFBaseModel):
|
|
171
|
+
access_vector: Optional[str] = Field(alias='accessVector', default=None)
|
|
172
|
+
last_modified: Optional[datetime] = Field(alias='lastModified', default=None)
|
|
173
|
+
published: Optional[datetime] = None
|
|
174
|
+
score: Optional[float] = None
|
|
175
|
+
availability: Optional[str] = None
|
|
176
|
+
authentication: Optional[str] = None
|
|
177
|
+
access_complexity: Optional[str] = Field(alias='accessComplexity', default=None)
|
|
178
|
+
integrity: Optional[str] = None
|
|
179
|
+
confidentiality: Optional[str] = None
|
|
180
|
+
version: Optional[str] = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class CVSSRating(RFBaseModel):
|
|
184
|
+
score: float
|
|
185
|
+
modified: datetime
|
|
186
|
+
version: str
|
|
187
|
+
type_: str = Field(alias='type')
|
|
188
|
+
created: datetime
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CVSSV3(RFBaseModel):
|
|
192
|
+
scope: Optional[str] = None
|
|
193
|
+
exploitability_score: Optional[float] = Field(alias='exploitabilityScore', default=None)
|
|
194
|
+
modified: Optional[datetime] = None
|
|
195
|
+
base_severity: Optional[str] = Field(alias='baseSeverity', default=None)
|
|
196
|
+
base_score: Optional[float] = Field(alias='baseScore', default=None)
|
|
197
|
+
privileges_required: Optional[str] = Field(alias='privilegesRequired', default=None)
|
|
198
|
+
user_interaction: Optional[str] = Field(alias='userInteraction', default=None)
|
|
199
|
+
impact_score: Optional[float] = Field(alias='impactScore', default=None)
|
|
200
|
+
attack_vector: Optional[str] = Field(alias='attackVector', default=None)
|
|
201
|
+
integrity_impact: Optional[str] = Field(alias='integrityImpact', default=None)
|
|
202
|
+
confidentiality_impact: Optional[str] = Field(alias='confidentialityImpact', default=None)
|
|
203
|
+
vector_string: Optional[str] = Field(alias='vectorString', default=None)
|
|
204
|
+
version: Optional[str] = None
|
|
205
|
+
attack_complexity: Optional[str] = Field(alias='attackComplexity', default=None)
|
|
206
|
+
created: Optional[datetime] = None
|
|
207
|
+
availability_impact: Optional[str] = Field(alias='availabilityImpact', default=None)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
###########################################################
|
|
211
|
+
# Raw Risk
|
|
212
|
+
###########################################################
|
|
213
|
+
class RawRisk(RFBaseModel):
|
|
214
|
+
rule: str
|
|
215
|
+
timestamp: datetime
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
###########################################################
|
|
219
|
+
# DNS Port Cert
|
|
220
|
+
###########################################################
|
|
221
|
+
class Validity(RFBaseModel):
|
|
222
|
+
valid_from: datetime = Field(alias='validFrom')
|
|
223
|
+
valid_to: datetime = Field(alias='validTo')
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Issuer(RFBaseModel):
|
|
227
|
+
organization: Optional[str] = None
|
|
228
|
+
location: Optional[str] = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Certificate(RFBaseModel):
|
|
232
|
+
subject: Optional[str] = None
|
|
233
|
+
validity: Validity
|
|
234
|
+
issuer: Issuer
|
|
235
|
+
seen_on_port: list[int] = Field(alias='seenOnPort')
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ForwardDNS(RFBaseModel):
|
|
239
|
+
hostname: Optional[str] = None
|
|
240
|
+
last_seen: Union[datetime, None] = Field(alias='lastSeen')
|
|
241
|
+
first_seen: Union[datetime, None] = Field(alias='firstSeen')
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class DNS(RFBaseModel):
|
|
245
|
+
forward_dns: list[ForwardDNS] = Field(alias='forwardDns')
|
|
246
|
+
reverse_dns: Optional[str] = Field(alias='reverseDns', default=None)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Port(RFBaseModel):
|
|
250
|
+
name: str
|
|
251
|
+
version: Union[str, None]
|
|
252
|
+
port: int
|
|
253
|
+
extra_info: Union[str, None] = Field(alias='extraInfo')
|
|
254
|
+
protocol: str
|
|
255
|
+
product: Union[str, None]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class DnsPortCert(RFBaseModel):
|
|
259
|
+
certificates: Optional[list[Certificate]] = None
|
|
260
|
+
dns: Optional[DNS] = None
|
|
261
|
+
ports: Optional[list[Port]] = None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
###########################################################
|
|
265
|
+
# NVD
|
|
266
|
+
###########################################################
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class NvdReference(RFBaseModel):
|
|
270
|
+
url: str
|
|
271
|
+
tags: list[str]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from pydantic import Field, model_validator
|
|
18
|
+
|
|
19
|
+
from ...common_models import RFBaseModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScoreCount(RFBaseModel):
|
|
23
|
+
count: Optional[int] = None
|
|
24
|
+
max_count: int = Field(alias='maxCount')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ScoreRule(RFBaseModel):
|
|
28
|
+
score: int
|
|
29
|
+
rule: ScoreCount
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Public(ScoreRule):
|
|
33
|
+
summary: list
|
|
34
|
+
most_critical_rule: str = Field(alias='mostCriticalRule')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Evidence(RFBaseModel):
|
|
38
|
+
count: int
|
|
39
|
+
timestamp: datetime
|
|
40
|
+
description: str
|
|
41
|
+
rule: str
|
|
42
|
+
# TODO - temp fix until API team fixes/confirms behaviour of sightings
|
|
43
|
+
sightings: int = 0
|
|
44
|
+
mitigation: str
|
|
45
|
+
level: int
|
|
46
|
+
type_: str = Field(alias='type')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RiskRule(ScoreCount):
|
|
50
|
+
score: Optional[int] = None
|
|
51
|
+
summary: list
|
|
52
|
+
most_critical: str = Field(alias='mostCritical')
|
|
53
|
+
evidence: Optional[list[Evidence]] = None
|
|
54
|
+
|
|
55
|
+
@model_validator(mode='before')
|
|
56
|
+
@classmethod
|
|
57
|
+
def evidence_transform(cls, data: dict) -> dict:
|
|
58
|
+
"""Transforms the evidence field into a list of dicts, each with a 'type' key.
|
|
59
|
+
|
|
60
|
+
From:
|
|
61
|
+
|
|
62
|
+
.. code-block::
|
|
63
|
+
|
|
64
|
+
"evidence": {
|
|
65
|
+
"recentValidatedCnc": {
|
|
66
|
+
"count": 1,
|
|
67
|
+
"timestamp": "2024-03-25T07:18:35.000Z",
|
|
68
|
+
"description": "xyz",
|
|
69
|
+
"rule": "Validated C&C Server",
|
|
70
|
+
"sightings": 41,
|
|
71
|
+
"mitigation": "",
|
|
72
|
+
"level": 4
|
|
73
|
+
},
|
|
74
|
+
"recentSuspectedCnc": {
|
|
75
|
+
"count": 1,
|
|
76
|
+
"timestamp": "2024-03-24T16:05:31.634Z",
|
|
77
|
+
"description": "xyz",
|
|
78
|
+
"rule": "Recent Suspected C&C Server",
|
|
79
|
+
"sightings": 5,
|
|
80
|
+
"mitigation": "",
|
|
81
|
+
"level": 2
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
To:
|
|
86
|
+
|
|
87
|
+
.. code-block::
|
|
88
|
+
|
|
89
|
+
"evidence": [
|
|
90
|
+
{
|
|
91
|
+
"count": 1,
|
|
92
|
+
"timestamp": "2023-12-11T19:25:25.892000Z",
|
|
93
|
+
"description": "xyz",
|
|
94
|
+
"sightings": 1,
|
|
95
|
+
"mitigation": "",
|
|
96
|
+
"level": 3,
|
|
97
|
+
"type": "recentReportedCnc"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"count": 2,
|
|
101
|
+
"timestamp": "2023-12-25T22:09:55.398000Z",
|
|
102
|
+
"description": "xyz",
|
|
103
|
+
"rule": "Historical Suspected C&C Server",
|
|
104
|
+
"sightings": 2,
|
|
105
|
+
"mitigation": "",
|
|
106
|
+
"level": 1,
|
|
107
|
+
"type": "suspectedCnc"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
data (dict): The data to be validated.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: The transformed data.
|
|
116
|
+
"""
|
|
117
|
+
if 'evidence' not in data:
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
evidence = data.pop('evidence')
|
|
121
|
+
|
|
122
|
+
data['evidence'] = [{**ev, 'type': key} for key, ev in evidence.items()]
|
|
123
|
+
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Context(RFBaseModel):
|
|
128
|
+
phishing: Optional[ScoreRule] = None
|
|
129
|
+
public: Optional[Public] = None
|
|
130
|
+
c2: Optional[ScoreRule] = None
|
|
131
|
+
malware: Optional[ScoreRule] = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Risk(RFBaseModel):
|
|
135
|
+
score: int
|
|
136
|
+
level: int
|
|
137
|
+
context: Context
|
|
138
|
+
rule: RiskRule
|
psengine/enrich/soar.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from functools import total_ordering
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
from ..common_models import IdNameTypeDescription, RFBaseModel
|
|
20
|
+
from .models.soar import Risk
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@total_ordering
|
|
24
|
+
class SOAREnrichedEntity(RFBaseModel):
|
|
25
|
+
"""Model used for validating returned data from SOAR enrichment endpoint for bulk enrichment.
|
|
26
|
+
|
|
27
|
+
Methods:
|
|
28
|
+
__hash__:
|
|
29
|
+
Returns a hash value based on the entity ``id_`` and the risk score.
|
|
30
|
+
|
|
31
|
+
__eq__:
|
|
32
|
+
Checks equality between two ``SOAREnrichedEntity`` instances based on their entity name
|
|
33
|
+
and the risk score.
|
|
34
|
+
|
|
35
|
+
__gt__:
|
|
36
|
+
Defines a greater-than comparison between two ``SOAREnrichedEntity`` instances based on
|
|
37
|
+
their risk score and entity name.
|
|
38
|
+
|
|
39
|
+
__str__:
|
|
40
|
+
Returns a string representation of the ``SOAREnrichedEntity`` instance with:
|
|
41
|
+
enriched entity name, risk score, and most critical rule.
|
|
42
|
+
|
|
43
|
+
.. code-block:: python
|
|
44
|
+
|
|
45
|
+
>>> print(soar_enriched_entity)
|
|
46
|
+
Enriched Entity: 1.1.1.1, Risk Score: 95, Most Critical Rule: C&C Server
|
|
47
|
+
|
|
48
|
+
Total Ordering:
|
|
49
|
+
The ordering of ``SOAREnrichedEntity`` instances is determined primarily by the risk score.
|
|
50
|
+
If two instances have the same risk score, their entity name is used as a secondary
|
|
51
|
+
criterion for ordering.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
risk: Risk
|
|
55
|
+
entity: IdNameTypeDescription
|
|
56
|
+
|
|
57
|
+
def __hash__(self):
|
|
58
|
+
return hash((self.entity.id_, self.risk.score))
|
|
59
|
+
|
|
60
|
+
def __eq__(self, other: 'SOAREnrichedEntity'):
|
|
61
|
+
return (self.entity.name, self.risk.score) == (other.entity.name, other.risk.score)
|
|
62
|
+
|
|
63
|
+
def __gt__(self, other: 'SOAREnrichedEntity'):
|
|
64
|
+
return (self.risk.score, self.entity.name) > (other.risk.score, other.entity.name)
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return (
|
|
68
|
+
f'Enriched Entity: {self.entity.name}, Risk Score: {self.risk.score}, '
|
|
69
|
+
f'Most Critical Rule: {self.risk.rule.most_critical}'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SOAREnrichIn(RFBaseModel):
|
|
74
|
+
"""Model used to validate payload sent to SOAR enrichment endpoint."""
|
|
75
|
+
|
|
76
|
+
ip: Optional[list[str]] = None
|
|
77
|
+
domain: Optional[list[str]] = None
|
|
78
|
+
url: Optional[list[str]] = None
|
|
79
|
+
hash_: Optional[list[str]] = Field(alias='hash', default=None)
|
|
80
|
+
vulnerability: Optional[list[str]] = None
|
|
81
|
+
companybydomain: Optional[list[str]] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SOAREnrichOut(RFBaseModel):
|
|
85
|
+
"""Model used for collecting all the data returned in a SOAR call."""
|
|
86
|
+
|
|
87
|
+
entity: str
|
|
88
|
+
is_enriched: bool
|
|
89
|
+
content: Optional[SOAREnrichedEntity] = None
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from itertools import chain
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from pydantic import validate_call
|
|
19
|
+
|
|
20
|
+
from ..endpoints import EP_SOAR_ENRICHMENT
|
|
21
|
+
from ..helpers import MultiThreadingHelper, connection_exceptions, debug_call
|
|
22
|
+
from ..rf_client import RFClient
|
|
23
|
+
from .constants import SOAR_POST_ROWS
|
|
24
|
+
from .errors import EnrichmentSoarError
|
|
25
|
+
from .soar import SOAREnrichedEntity, SOAREnrichIn, SOAREnrichOut
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SoarMgr:
|
|
29
|
+
"""Perform SOAR enrichment of entities."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, rf_token: str = None):
|
|
32
|
+
"""Initializes the SoarMgr object.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
rf_token (str, optional): Recorded Future API token. Defaults to None
|
|
36
|
+
"""
|
|
37
|
+
self.log = logging.getLogger(__name__)
|
|
38
|
+
self.rf_client = RFClient(api_token=rf_token) if rf_token else RFClient()
|
|
39
|
+
|
|
40
|
+
@validate_call
|
|
41
|
+
@debug_call
|
|
42
|
+
def soar(
|
|
43
|
+
self,
|
|
44
|
+
ip: Optional[list[str]] = None,
|
|
45
|
+
domain: Optional[list[str]] = None,
|
|
46
|
+
hash_: Optional[list[str]] = None,
|
|
47
|
+
vulnerability: Optional[list[str]] = None,
|
|
48
|
+
url: Optional[list[str]] = None,
|
|
49
|
+
companybydomain: Optional[list[str]] = None,
|
|
50
|
+
max_workers: Optional[int] = 0,
|
|
51
|
+
) -> list[SOAREnrichOut]:
|
|
52
|
+
"""Enrich multiple types of IOCs via the SOAR API.
|
|
53
|
+
|
|
54
|
+
Endpoint:
|
|
55
|
+
``v2/soar/enrichment``
|
|
56
|
+
|
|
57
|
+
This method allows for batch processing of various IOC types, including IP addresses,
|
|
58
|
+
domains, file hashes, vulnerabilities, URLs, and company domains. It utilizes either
|
|
59
|
+
multi-threading or sequential processing based on the `max_workers` parameter.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
Simple SOAR enrichment:
|
|
63
|
+
|
|
64
|
+
.. code-block:: python
|
|
65
|
+
:linenos:
|
|
66
|
+
|
|
67
|
+
mgr.soar(ip=['1.1.1.1'])
|
|
68
|
+
|
|
69
|
+
Multithreaded example:
|
|
70
|
+
|
|
71
|
+
.. code-block:: python
|
|
72
|
+
:linenos:
|
|
73
|
+
|
|
74
|
+
mgr.soar(ip=['1.1.1.1'], max_workers=10)
|
|
75
|
+
|
|
76
|
+
To write an enriched object to file:
|
|
77
|
+
|
|
78
|
+
.. code-block:: python
|
|
79
|
+
:linenos:
|
|
80
|
+
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
from json import dump
|
|
83
|
+
|
|
84
|
+
mgr = SoarMgr()
|
|
85
|
+
OUTPUT_DIR = Path('your' / 'path')
|
|
86
|
+
OUTPUT_DIR.mkdir(exists_ok=True)
|
|
87
|
+
data = mgr.soar(ip=['1.1.1.1', '8.8.8.8'])
|
|
88
|
+
for ip in data:
|
|
89
|
+
(OUTPUT_DIR / f'{ip.entity}.json').write_text(dumps(ip.json(), indent=2))
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
ip (List[str], optional): List of IP addresses to enrich.
|
|
93
|
+
domain (List[str], optional): List of domains to enrich.
|
|
94
|
+
hash_ (List[str], optional): List of file hashes to enrich.
|
|
95
|
+
vulnerability (List[str], optional): List of vulnerabilities to enrich.
|
|
96
|
+
url (List[str], optional): List of URLs to enrich.
|
|
97
|
+
companybydomain (List[str], optional): List of company domains to enrich.
|
|
98
|
+
max_workers (int, optional): number of workers to multithread requests.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List[SOAREnrichOut]: A list of enriched data for the provided IOCs.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If no parameters are provided or all provided lists are empty.
|
|
105
|
+
ValidationError If the arguments have incorrect types or if the data returned
|
|
106
|
+
by the API is malformed.
|
|
107
|
+
EnrichmentSoarError: If an HTTP or JSON decoding error occurs during enrichment.
|
|
108
|
+
"""
|
|
109
|
+
iocs = {
|
|
110
|
+
'ip': ip,
|
|
111
|
+
'domain': domain,
|
|
112
|
+
'hash': hash_,
|
|
113
|
+
'vulnerability': vulnerability,
|
|
114
|
+
'url': url,
|
|
115
|
+
'companybydomain': companybydomain,
|
|
116
|
+
}
|
|
117
|
+
iocs = {k: v for k, v in iocs.items() if v}
|
|
118
|
+
if not iocs:
|
|
119
|
+
raise ValueError('At least one parameter must be used')
|
|
120
|
+
|
|
121
|
+
results = []
|
|
122
|
+
if max_workers:
|
|
123
|
+
results.append(
|
|
124
|
+
chain.from_iterable(
|
|
125
|
+
MultiThreadingHelper.multithread_it(
|
|
126
|
+
max_workers,
|
|
127
|
+
self._fetch_data,
|
|
128
|
+
iterator=self._batched_cross_entity(iocs, SOAR_POST_ROWS),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
results = [
|
|
134
|
+
self._fetch_data(batched_iocs)
|
|
135
|
+
for batched_iocs in self._batched_cross_entity(iocs, SOAR_POST_ROWS)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return list(chain.from_iterable(results))
|
|
139
|
+
|
|
140
|
+
def _batched_cross_entity(self, iocs, batch_size):
|
|
141
|
+
"""Batches the SOAR data in dict of maximum SOAR_POST_ROWS elements in total.
|
|
142
|
+
It always return a list of SoarRequest compatible object as dict.
|
|
143
|
+
"""
|
|
144
|
+
batches = []
|
|
145
|
+
current_batch = {k: [] for k in iocs}
|
|
146
|
+
current_count = 0
|
|
147
|
+
|
|
148
|
+
for k, vals in iocs.items():
|
|
149
|
+
for v in vals:
|
|
150
|
+
if current_count >= batch_size:
|
|
151
|
+
batches.append({k: v for k, v in current_batch.items() if v})
|
|
152
|
+
current_batch = {k: [] for k in iocs}
|
|
153
|
+
current_count = 0
|
|
154
|
+
|
|
155
|
+
current_batch[k].append(v)
|
|
156
|
+
current_count += 1
|
|
157
|
+
|
|
158
|
+
batches.append({k: v for k, v in current_batch.items() if v})
|
|
159
|
+
return batches
|
|
160
|
+
|
|
161
|
+
@connection_exceptions(ignore_status_code=[], exception_to_raise=EnrichmentSoarError)
|
|
162
|
+
@debug_call
|
|
163
|
+
def _fetch_data(self, data: dict) -> list[SOAREnrichOut]:
|
|
164
|
+
"""Perform soar post request and raises EnrichementSoarError if something goes wrong."""
|
|
165
|
+
data = SOAREnrichIn.model_validate(data)
|
|
166
|
+
res = self.rf_client.request('post', EP_SOAR_ENRICHMENT, data=data.json()).json()['data'][
|
|
167
|
+
'results'
|
|
168
|
+
]
|
|
169
|
+
result = []
|
|
170
|
+
|
|
171
|
+
for d in res:
|
|
172
|
+
content = SOAREnrichedEntity.model_validate(d)
|
|
173
|
+
entity = content.entity.name
|
|
174
|
+
result.append(SOAREnrichOut(entity=entity, is_enriched=True, content=content))
|
|
175
|
+
|
|
176
|
+
return result
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
##################################### TERMS OF USE ###########################################
|
|
2
|
+
# The following code is provided for demonstration purpose only, and should not be used #
|
|
3
|
+
# without independent verification. Recorded Future makes no representations or warranties, #
|
|
4
|
+
# express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
|
|
5
|
+
# information it may retrieve, and provides it both strictly “as-is” and without assuming #
|
|
6
|
+
# responsibility for any information it may retrieve. Recorded Future shall not be liable #
|
|
7
|
+
# for, and you assume all risk of using, the foregoing. By using this code, Customer #
|
|
8
|
+
# represents that it is solely responsible for having all necessary licenses, permissions, #
|
|
9
|
+
# rights, and/or consents to connect to third party APIs, and that it is solely responsible #
|
|
10
|
+
# for having all necessary licenses, permissions, rights, and/or consents to any data #
|
|
11
|
+
# accessed from any third party API. #
|
|
12
|
+
##############################################################################################
|
|
13
|
+
|
|
14
|
+
from .entity_list import EntityList, ListEntity, ListInfoOut, ListStatusOut
|
|
15
|
+
from .entity_list_mgr import EntityListMgr
|
|
16
|
+
from .errors import ListApiError, ListResolutionError, ListStateError
|