psengine 2.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. psengine/__init__.py +22 -0
  2. psengine/_sdk_id.py +16 -0
  3. psengine/_version.py +14 -0
  4. psengine/analyst_notes/__init__.py +32 -0
  5. psengine/analyst_notes/constants.py +15 -0
  6. psengine/analyst_notes/errors.py +42 -0
  7. psengine/analyst_notes/helpers.py +90 -0
  8. psengine/analyst_notes/models.py +219 -0
  9. psengine/analyst_notes/note.py +149 -0
  10. psengine/analyst_notes/note_mgr.py +400 -0
  11. psengine/base_http_client.py +285 -0
  12. psengine/classic_alerts/__init__.py +24 -0
  13. psengine/classic_alerts/classic_alert.py +275 -0
  14. psengine/classic_alerts/classic_alert_mgr.py +507 -0
  15. psengine/classic_alerts/constants.py +31 -0
  16. psengine/classic_alerts/errors.py +38 -0
  17. psengine/classic_alerts/helpers.py +87 -0
  18. psengine/classic_alerts/markdown/__init__.py +13 -0
  19. psengine/classic_alerts/markdown/markdown.py +359 -0
  20. psengine/classic_alerts/models.py +141 -0
  21. psengine/collective_insights/__init__.py +29 -0
  22. psengine/collective_insights/collective_insights.py +164 -0
  23. psengine/collective_insights/constants.py +44 -0
  24. psengine/collective_insights/errors.py +18 -0
  25. psengine/collective_insights/insight.py +89 -0
  26. psengine/collective_insights/models.py +81 -0
  27. psengine/common_models.py +89 -0
  28. psengine/config/__init__.py +15 -0
  29. psengine/config/config.py +284 -0
  30. psengine/config/errors.py +18 -0
  31. psengine/constants.py +63 -0
  32. psengine/detection/__init__.py +17 -0
  33. psengine/detection/detection_mgr.py +135 -0
  34. psengine/detection/detection_rule.py +85 -0
  35. psengine/detection/errors.py +26 -0
  36. psengine/detection/helpers.py +56 -0
  37. psengine/detection/models.py +47 -0
  38. psengine/endpoints.py +98 -0
  39. psengine/enrich/__init__.py +28 -0
  40. psengine/enrich/constants.py +73 -0
  41. psengine/enrich/errors.py +26 -0
  42. psengine/enrich/lookup.py +299 -0
  43. psengine/enrich/lookup_mgr.py +341 -0
  44. psengine/enrich/models/__init__.py +13 -0
  45. psengine/enrich/models/base_enriched_entity.py +43 -0
  46. psengine/enrich/models/lookup.py +271 -0
  47. psengine/enrich/models/soar.py +138 -0
  48. psengine/enrich/soar.py +89 -0
  49. psengine/enrich/soar_mgr.py +176 -0
  50. psengine/entity_lists/__init__.py +16 -0
  51. psengine/entity_lists/constants.py +19 -0
  52. psengine/entity_lists/entity_list.py +435 -0
  53. psengine/entity_lists/entity_list_mgr.py +185 -0
  54. psengine/entity_lists/errors.py +26 -0
  55. psengine/entity_lists/models.py +87 -0
  56. psengine/entity_match/__init__.py +16 -0
  57. psengine/entity_match/entity_match.py +90 -0
  58. psengine/entity_match/entity_match_mgr.py +235 -0
  59. psengine/entity_match/errors.py +18 -0
  60. psengine/entity_match/models.py +22 -0
  61. psengine/errors.py +41 -0
  62. psengine/helpers/__init__.py +23 -0
  63. psengine/helpers/helpers.py +471 -0
  64. psengine/logger/__init__.py +15 -0
  65. psengine/logger/constants.py +39 -0
  66. psengine/logger/errors.py +18 -0
  67. psengine/logger/rf_logger.py +148 -0
  68. psengine/markdown/__init__.py +21 -0
  69. psengine/markdown/markdown.py +169 -0
  70. psengine/markdown/models.py +22 -0
  71. psengine/playbook_alerts/__init__.py +34 -0
  72. psengine/playbook_alerts/constants.py +35 -0
  73. psengine/playbook_alerts/errors.py +35 -0
  74. psengine/playbook_alerts/helpers.py +80 -0
  75. psengine/playbook_alerts/mappings.py +44 -0
  76. psengine/playbook_alerts/markdown/__init__.py +13 -0
  77. psengine/playbook_alerts/markdown/markdown.py +98 -0
  78. psengine/playbook_alerts/markdown/markdown_code_repo.py +64 -0
  79. psengine/playbook_alerts/markdown/markdown_domain_abuse.py +118 -0
  80. psengine/playbook_alerts/markdown/markdown_identity_exposure.py +158 -0
  81. psengine/playbook_alerts/models/__init__.py +36 -0
  82. psengine/playbook_alerts/models/common_models.py +18 -0
  83. psengine/playbook_alerts/models/panel_log.py +329 -0
  84. psengine/playbook_alerts/models/panel_status.py +70 -0
  85. psengine/playbook_alerts/models/pba_code_repo_leak.py +52 -0
  86. psengine/playbook_alerts/models/pba_cyber_vulnerability.py +53 -0
  87. psengine/playbook_alerts/models/pba_domain_abuse.py +139 -0
  88. psengine/playbook_alerts/models/pba_identity_exposures.py +93 -0
  89. psengine/playbook_alerts/models/pba_third_party_risk.py +103 -0
  90. psengine/playbook_alerts/models/search_endpoint.py +68 -0
  91. psengine/playbook_alerts/pa_category.py +37 -0
  92. psengine/playbook_alerts/playbook_alert_mgr.py +593 -0
  93. psengine/playbook_alerts/playbook_alerts.py +393 -0
  94. psengine/rf_client.py +430 -0
  95. psengine/risklists/__init__.py +17 -0
  96. psengine/risklists/constants.py +15 -0
  97. psengine/risklists/errors.py +20 -0
  98. psengine/risklists/models.py +65 -0
  99. psengine/risklists/risklist_mgr.py +156 -0
  100. psengine/stix2/__init__.py +21 -0
  101. psengine/stix2/base_stix_entity.py +62 -0
  102. psengine/stix2/complex_entity.py +372 -0
  103. psengine/stix2/constants.py +81 -0
  104. psengine/stix2/enriched_indicator.py +261 -0
  105. psengine/stix2/errors.py +22 -0
  106. psengine/stix2/helpers.py +68 -0
  107. psengine/stix2/rf_bundle.py +240 -0
  108. psengine/stix2/simple_entity.py +145 -0
  109. psengine/stix2/util.py +53 -0
  110. psengine-2.0.4.dist-info/METADATA +189 -0
  111. psengine-2.0.4.dist-info/RECORD +115 -0
  112. psengine-2.0.4.dist-info/WHEEL +5 -0
  113. psengine-2.0.4.dist-info/entry_points.txt +2 -0
  114. psengine-2.0.4.dist-info/licenses/LICENSE +21 -0
  115. psengine-2.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,118 @@
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 base64
15
+
16
+ from markdown_strings import bold
17
+
18
+ from ...constants import TIMESTAMP_STR
19
+ from ...helpers import FormattingHelpers
20
+ from ...markdown import MarkdownMaker
21
+ from ...markdown.markdown import divider, table_from_rows
22
+ from ..models.pba_domain_abuse import ValueServer
23
+
24
+
25
+ def _add_screenshots(pba, md_maker: MarkdownMaker):
26
+ screenshots = [f'{bold("Screenshot Count:")} {len(pba.panel_evidence_summary.screenshots)}']
27
+ for screenshot in pba.panel_evidence_summary.screenshots:
28
+ screenshots.append(f'{bold("Created:")} {screenshot.created.strftime(TIMESTAMP_STR)}')
29
+
30
+ image = base64.b64encode(pba.images[screenshot.image_id]['image_bytes']).decode('utf-8')
31
+
32
+ for mentions in pba.panel_evidence_summary.screenshot_mentions:
33
+ if mentions.screenshot == screenshot.image_id and mentions.url:
34
+ url = FormattingHelpers.cleanup_rf_id(mentions.url)
35
+ md_maker.iocs_to_defang.append(url)
36
+ screenshots.append(f'{bold("Screenshot URL:")} {"".join(url)}')
37
+
38
+ screenshots.append(f'![img](data:image/png;base64,{image})')
39
+ screenshots.append(divider())
40
+
41
+ md_maker.add_section('Screenshots', screenshots)
42
+
43
+
44
+ def _add_whois(pba, md_maker: MarkdownMaker):
45
+ whois_body = [
46
+ body for body in pba.panel_evidence_whois.body if isinstance(body.value, ValueServer)
47
+ ]
48
+
49
+ whois_data = []
50
+ for whois in whois_body:
51
+ entity = FormattingHelpers.cleanup_rf_id(whois.entity)
52
+ md_maker.iocs_to_defang.append(entity)
53
+ whois_entity = f"{bold('Entity:')} {entity}"
54
+
55
+ if (v := whois.value) and v.name_servers:
56
+ created_dt, updated_dt, expires_dt = '', '', ''
57
+ name_servers = [FormattingHelpers.cleanup_rf_id(s) for s in v.name_servers]
58
+ servers = f"{bold('Name servers:')} {', '.join(name_servers)}"
59
+ md_maker.iocs_to_defang.extend(name_servers)
60
+
61
+ if v.created_date:
62
+ created_dt = f'{bold("Creation Date:")} {v.created_date.strftime(TIMESTAMP_STR)}'
63
+
64
+ if v.updated_date:
65
+ updated_dt = f'{bold("Update Date:")} {v.updated_date.strftime(TIMESTAMP_STR)}'
66
+
67
+ if v.expires_date:
68
+ expires_dt = f'{bold("Expiration Date:")} {v.expires_date.strftime(TIMESTAMP_STR)}'
69
+
70
+ registrar = f'{bold("Registrar:")} {v.registrar_name}' if v.registrar_name else ''
71
+
72
+ whois_data.extend(
73
+ [whois_entity, servers, created_dt, updated_dt, expires_dt, registrar]
74
+ )
75
+
76
+ if whois_data:
77
+ md_maker.add_section('WHOIS Details', whois_data)
78
+
79
+
80
+ def _add_dns_records(pba, md_maker: MarkdownMaker):
81
+ records = [
82
+ [
83
+ FormattingHelpers.cleanup_rf_id(record.entity),
84
+ record.risk_score,
85
+ record.criticality,
86
+ record.record_type,
87
+ ', '.join(c.context for c in record.context_list if c),
88
+ ]
89
+ for record in pba.panel_evidence_summary.resolved_record_list
90
+ ]
91
+ md_maker.iocs_to_defang.extend(list(zip(*records))[0])
92
+
93
+ records.sort(key=lambda x: x[1], reverse=True)
94
+ records.insert(0, ['Entity', 'Risk Score', 'Criticality', 'Record Type', 'Context'])
95
+ evidence_summary = [
96
+ f'{bold("Reason:")} {pba.panel_evidence_summary.explanation}\n',
97
+ f'{table_from_rows(records)}',
98
+ ]
99
+
100
+ md_maker.add_section('DNS Records', evidence_summary)
101
+
102
+
103
+ def _domain_abuse_markdown(pba, md_maker: MarkdownMaker, *args) -> str: # noqa: ARG001
104
+ if targets := pba.panel_status.targets:
105
+ targets = [FormattingHelpers.cleanup_rf_id(t) for t in targets]
106
+ md_maker.iocs_to_defang.extend(targets)
107
+ md_maker.add_section('Targets', targets)
108
+
109
+ if pba.panel_evidence_summary.resolved_record_list:
110
+ _add_dns_records(pba, md_maker)
111
+
112
+ if pba.panel_evidence_whois:
113
+ _add_whois(pba, md_maker)
114
+
115
+ if pba.images and not md_maker.character_limit:
116
+ _add_screenshots(pba, md_maker)
117
+
118
+ return md_maker.format_output()
@@ -0,0 +1,158 @@
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 markdown_strings import bold, unordered_list
15
+
16
+ from ...constants import TIMESTAMP_STR
17
+ from ...markdown import MarkdownMaker
18
+ from ...markdown.markdown import divider
19
+
20
+ DOMAIN_CONFIG_URL = 'https://app.recordedfuture.com/portal/identity/domain-configuration'
21
+ PORTAL_URL = 'https://app.recordedfuture.com/portal/playbook-alerts/{}'
22
+
23
+
24
+ def _get_compromised_host(pba) -> str:
25
+ host_data = []
26
+ summ = pba.panel_evidence_summary
27
+
28
+ if not (
29
+ summ.compromised_host
30
+ and summ.compromised_host.os_username
31
+ and summ.compromised_host.computer_name
32
+ ):
33
+ return ''
34
+
35
+ comprom = summ.compromised_host
36
+ os = _format_field('Operating System', comprom.os)
37
+ username = _format_field('OS Username', comprom.os_username)
38
+ file_path = _format_field('File Path', comprom.malware_file)
39
+ timezone = _format_field('Time Zone', comprom.timezone)
40
+ uac = _format_field('User Account Control Setting', comprom.uac)
41
+ av = _format_field('Antivirus', comprom.antivirus)
42
+ machine = _format_field('Machine Name', comprom.computer_name)
43
+
44
+ host_data = [x for x in [os, username, file_path, timezone, machine, uac, av] if x]
45
+
46
+ return f"\n{bold('Compromised Host:')}\n\n{unordered_list(host_data, esc=False)}"
47
+
48
+
49
+ def _get_technology(pba) -> str:
50
+ items = []
51
+ for tech in pba.panel_evidence_summary.technologies:
52
+ label = 'Category:' if tech.category else 'Technology:'
53
+ items.append(f'{bold(label)} {tech.name}')
54
+
55
+ if not items:
56
+ return ''
57
+
58
+ return f"\n{bold('Technology:')}\n\n{unordered_list(items, esc=False)}"
59
+
60
+
61
+ def _add_exposure(pba, md_maker: MarkdownMaker):
62
+ summ = pba.panel_evidence_summary
63
+ result = []
64
+
65
+ result.append(_format_field('Identity', pba.panel_status.entity_name))
66
+ result.append(_format_password(summ.exposed_secret.details))
67
+ result.append(_format_assessments(summ.assessments))
68
+ if summ.compromised_host.exfiltration_date:
69
+ result.append(
70
+ _format_field(
71
+ 'Exfiltration Date', summ.compromised_host.exfiltration_date.strftime(TIMESTAMP_STR)
72
+ )
73
+ )
74
+
75
+ result.append(_format_field('Authorization URL', summ.authorization_url))
76
+ result.append(_format_field('IP Address', summ.infrastructure.ip))
77
+ result.append(_format_field('Properties', summ.exposed_secret.details.properties))
78
+
79
+ result.append(_format_hashes(summ.exposed_secret.hashes))
80
+ result.append(_format_source(summ.dump))
81
+ result.append(_get_compromised_host(pba))
82
+
83
+ result.append('\n' + _format_field('Malware Family', summ.malware_family.name))
84
+
85
+ result.append(_get_technology(pba))
86
+ result.append(divider())
87
+
88
+ md_maker.add_section('Exposure', result)
89
+
90
+
91
+ def _format_field(label, value) -> str:
92
+ """Generic helper for fields that can be single strings or lists.
93
+ Returns an empty string if there's no value.
94
+ """
95
+ if not value:
96
+ return ''
97
+ if isinstance(value, list):
98
+ value = ', '.join(value)
99
+ return f'{bold(label + ":")} {value}'
100
+
101
+
102
+ def _format_password(secret_details):
103
+ password = ''
104
+ if secret_details.clear_text_hint:
105
+ # Hides all but the password hint
106
+ password = (
107
+ f"{bold('Password:')} {secret_details.clear_text_hint:•<8} [ⓘ] ({DOMAIN_CONFIG_URL})"
108
+ )
109
+ if secret_details.clear_text_value:
110
+ # If support has enabled clear text, it takes precedence
111
+ password = f"{bold('Password:')} {secret_details.clear_text_value}"
112
+ return password
113
+
114
+
115
+ def _format_assessments(assessments) -> str:
116
+ if assessments:
117
+ names = ', '.join(ass.name for ass in assessments)
118
+ return f"{bold('Assessment:')} {names}"
119
+ return ''
120
+
121
+
122
+ def _format_hashes(hashes) -> str:
123
+ hash_values = [f'{bold(h.algorithm)} {h.hash_}' for h in hashes if h.hash_]
124
+ if hash_values:
125
+ formatted_hashes = '\n\n' + unordered_list(hash_values, esc=False)
126
+ return f"\n{bold('Hashes:')} {formatted_hashes}\n"
127
+ return ''
128
+
129
+
130
+ def _format_source(dump) -> str:
131
+ source_data = []
132
+ if dump.name:
133
+ source_data.append(f"{bold('Name:')} {dump.name}")
134
+ if dump.description:
135
+ source_data.append(f"{bold('Description:')} {dump.description}")
136
+ if source_data:
137
+ return f"\n{bold('Source:')}\n\n{unordered_list(source_data, esc=False)}"
138
+ return ''
139
+
140
+
141
+ def _add_actions_to_consider(pba, md_maker: MarkdownMaker):
142
+ actions = []
143
+ actions.append(f'- [Check Incident Report] ({PORTAL_URL.format(pba.playbook_alert_id)})')
144
+ actions.append('- Enforce Password Reset')
145
+ actions.append('- Initiate MFA Challenge')
146
+ actions.append('- Request Compromised Host Incident Response')
147
+ actions.append('- Review Malware Hunting Packages')
148
+
149
+ md_maker.add_section('Actions to Consider', actions)
150
+
151
+
152
+ def _identity_exposure_markdown(pba, md_maker: MarkdownMaker, *args) -> str: # noqa: ARG001
153
+ if pba.panel_evidence_summary:
154
+ _add_exposure(pba, md_maker)
155
+
156
+ _add_actions_to_consider(pba, md_maker)
157
+
158
+ return md_maker.format_output()
@@ -0,0 +1,36 @@
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 .common_models import ResolvedEntity
15
+ from .panel_log import PanelLogV2
16
+ from .panel_status import PanelAction
17
+ from .pba_code_repo_leak import (
18
+ CodeRepoEvidencePanel,
19
+ CodeRepoPanelStatus,
20
+ )
21
+ from .pba_cyber_vulnerability import (
22
+ CyberVulnerabilityEvidencePanel,
23
+ CyberVulnerabilityPanelStatus,
24
+ )
25
+ from .pba_domain_abuse import (
26
+ DomainAbuseEvidenceDns,
27
+ DomainAbuseEvidenceSummary,
28
+ DomainAbuseEvidenceWhois,
29
+ DomainAbusePanelStatus,
30
+ )
31
+ from .pba_identity_exposures import (
32
+ IdentityEvidencePanel,
33
+ IdentityPanelStatus,
34
+ )
35
+ from .pba_third_party_risk import TPREvidencePanel, TPRPanelStatus
36
+ from .search_endpoint import DatetimeRange, SearchCounts, SearchData, SearchResponse, SearchStatus
@@ -0,0 +1,18 @@
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 ...common_models import RFBaseModel
15
+
16
+
17
+ class ResolvedEntity(RFBaseModel):
18
+ name: str
@@ -0,0 +1,329 @@
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, HttpUrl, model_validator
18
+
19
+ from ...common_models import IdOptionalNameType, RFBaseModel
20
+
21
+
22
+ class ChangeType(RFBaseModel):
23
+ type_: str = Field(alias='type')
24
+
25
+
26
+ class PriorityChange(ChangeType):
27
+ old: str
28
+ new: str
29
+
30
+
31
+ class StatusChange(ChangeType):
32
+ old: str
33
+ new: str
34
+ actions_taken: list
35
+
36
+
37
+ class OldNewOptionalType(ChangeType):
38
+ """This is valid for the following Panel Log types.
39
+
40
+ - ``ExternalIdChange``,
41
+ - ``DescriptionChange``,
42
+ - ``TitleChange``,
43
+ - ``ReopenStrategyChange``
44
+
45
+ """
46
+
47
+ old: Optional[str] = None
48
+ new: Optional[str] = None
49
+
50
+
51
+ class AddedRemovedTypeEntities(ChangeType):
52
+ """This is valid for the following Panel Log types.
53
+
54
+ - ``EntityChangeV2``,
55
+ - ``RelatedEntityChangeV2``
56
+ """
57
+
58
+ removed: Optional[list[IdOptionalNameType]] = []
59
+ added: Optional[list[IdOptionalNameType]] = []
60
+
61
+
62
+ class AddedRemovedList(ChangeType):
63
+ removed: Optional[list[str]] = []
64
+ added: Optional[list[str]] = []
65
+
66
+
67
+ class CommentChange(ChangeType):
68
+ comment: str
69
+
70
+
71
+ class Assignee(RFBaseModel):
72
+ id_: str = Field(alias='id')
73
+ name: str
74
+
75
+
76
+ class AssigneeChange(ChangeType):
77
+ old: Optional[Assignee] = None
78
+ new: Optional[Assignee] = None
79
+
80
+
81
+ class DnsRecord(RFBaseModel):
82
+ type_: Optional[str] = Field(alias='type', default=None)
83
+ entity: Optional[IdOptionalNameType] = None
84
+
85
+
86
+ class DomainAbuseDnsChange(ChangeType):
87
+ domain: str
88
+ removed: list[DnsRecord]
89
+ added: list[DnsRecord]
90
+
91
+
92
+ class WhoisRecord(RFBaseModel):
93
+ status: Optional[str] = None
94
+ registrar_name: Optional[str] = None
95
+ private_registration: Optional[bool] = None
96
+ name_servers: Optional[list[str]] = []
97
+ contact_email: Optional[str] = None
98
+ created: Optional[datetime] = None
99
+
100
+
101
+ class WhoisContactRecord(ChangeType):
102
+ telephone: Optional[str] = None
103
+ street1: Optional[str] = None
104
+ state: Optional[str] = None
105
+ postal_code: Optional[str] = None
106
+ organization: Optional[str] = None
107
+ name: Optional[str] = None
108
+ fax: Optional[str] = None
109
+ email: Optional[str] = None
110
+ country_code: Optional[str] = None
111
+ country: Optional[str] = None
112
+ city: Optional[str] = None
113
+ created: Optional[datetime] = None
114
+
115
+
116
+ class DomainAbuseWhoisChange(ChangeType):
117
+ domain: str
118
+ old_record: Optional[WhoisRecord] = None
119
+ new_record: Optional[WhoisRecord] = None
120
+ removed_contacts: list[WhoisContactRecord]
121
+ added_contacts: list[WhoisContactRecord]
122
+
123
+
124
+ class LogotypeInScreenshot(RFBaseModel):
125
+ logotype_id: Optional[str] = None
126
+ screenshot_id: Optional[str] = None
127
+ url: HttpUrl
128
+
129
+
130
+ class DomainAbuseLogoTypeChange(ChangeType):
131
+ domain: str
132
+ removed: Optional[list[LogotypeInScreenshot]] = []
133
+ added: Optional[list[LogotypeInScreenshot]] = []
134
+
135
+
136
+ class MaliciousAssessment(RFBaseModel):
137
+ id_: str = Field(alias='id')
138
+ level: int
139
+ title: Optional[str] = None
140
+
141
+
142
+ class MaliciousDnsRecord(RFBaseModel):
143
+ id_: Optional[str] = Field(alias='id', default=None)
144
+ assessments: list[MaliciousAssessment]
145
+
146
+
147
+ class DomainAbuseMaliciousDnsChange(ChangeType):
148
+ domain: str
149
+ removed: Optional[list[MaliciousDnsRecord]] = []
150
+ added: Optional[list[MaliciousDnsRecord]] = []
151
+
152
+
153
+ class ReregistrationRecord(RFBaseModel):
154
+ registrar: Optional[str] = None
155
+ registrar_name: Optional[str] = None
156
+ iana_id: Optional[int] = None
157
+ expiration: Optional[datetime] = None
158
+
159
+
160
+ class DomainAbuseReregistrationRecordChange(ChangeType):
161
+ domain: str
162
+ removed: Optional[ReregistrationRecord] = None
163
+ added: Optional[ReregistrationRecord] = None
164
+
165
+
166
+ class Source(RFBaseModel):
167
+ id_: str = Field(alias='id')
168
+ name: str
169
+
170
+
171
+ class UrlAssessment(MaliciousAssessment):
172
+ source: Source
173
+
174
+
175
+ class MaliciousUrlRecord(RFBaseModel):
176
+ url: Optional[HttpUrl] = None
177
+ assessments: list[UrlAssessment]
178
+
179
+
180
+ class DomainAbuseMaliciousUrlChange(ChangeType):
181
+ domain: str
182
+ removed: Optional[list[MaliciousUrlRecord]] = []
183
+ added: Optional[list[MaliciousUrlRecord]] = []
184
+
185
+
186
+ class MentionedEntity(RFBaseModel):
187
+ entity: IdOptionalNameType
188
+ reference: Optional[str] = None
189
+ fragment: Optional[str] = None
190
+
191
+
192
+ class ScreenshotMention(RFBaseModel):
193
+ url: HttpUrl
194
+ screenshot_id: str
195
+ document: str
196
+ analyzed: datetime
197
+ mentioned_entities: list[MentionedEntity]
198
+
199
+
200
+ class DomainAbuseScreenshotMentions(ChangeType):
201
+ domain: str
202
+ added: list[ScreenshotMention]
203
+
204
+
205
+ class VulnerabilityAssessment(RFBaseModel):
206
+ id_: str = Field(alias='id')
207
+ level: int
208
+ title: Optional[str] = None
209
+
210
+
211
+ class TriggeredRiskRule(RFBaseModel):
212
+ id_: str = Field(alias='id')
213
+ name: Optional[str] = None
214
+ description: Optional[str] = None
215
+ evidence_string: Optional[str] = None
216
+ machine_name: Optional[str] = None
217
+ timestamp: Optional[datetime] = None
218
+
219
+
220
+ class VulnerabilityLifecycleChange(ChangeType):
221
+ added: Optional[VulnerabilityAssessment] = None
222
+ removed: Optional[VulnerabilityAssessment] = None
223
+ triggered_by_risk_rule: Optional[TriggeredRiskRule] = None
224
+
225
+
226
+ class Document(RFBaseModel):
227
+ id_: str = Field(alias='id')
228
+ content: str
229
+ owner_id: str
230
+ owner_name: Optional[str] = None
231
+ published: datetime
232
+
233
+
234
+ class WatchList(RFBaseModel):
235
+ id_: str = Field(alias='id')
236
+ name: Optional[str] = None
237
+
238
+
239
+ class RepoAssessment(RFBaseModel):
240
+ id_: str = Field(alias='id')
241
+ level: int
242
+ title: Optional[str] = None
243
+ text_indicator: Optional[str] = None
244
+ entity: Optional[IdOptionalNameType] = None
245
+
246
+
247
+ class CodeRepoLeakageEvidence(RFBaseModel):
248
+ assessments: list[RepoAssessment]
249
+ document: Document
250
+ target_entities: list[IdOptionalNameType]
251
+ watch_lists: list[WatchList]
252
+
253
+
254
+ class CodeRepoLeakageEvidenceChange(ChangeType):
255
+ added: list[CodeRepoLeakageEvidence]
256
+
257
+
258
+ class TPRRiskEvidence(RFBaseModel):
259
+ level: int
260
+ evidence_string: Optional[str] = None
261
+ timestamp: Optional[datetime] = None
262
+
263
+
264
+ class ThirdPartyAssessmentChange(ChangeType):
265
+ risk_attribute: str
266
+ added: Optional[TPRRiskEvidence] = None
267
+ removed: Optional[TPRRiskEvidence] = None
268
+
269
+
270
+ class Assessment(RFBaseModel):
271
+ level: int
272
+ evidence_string: str
273
+ timestamp: datetime
274
+
275
+
276
+ class AssessmentChange(ChangeType):
277
+ risk_attribute: str
278
+ removed: Optional[Assessment] = None
279
+ added: Optional[Assessment] = None
280
+
281
+
282
+ TYPE_MAPPING = {
283
+ 'assignee_change': AssigneeChange,
284
+ 'status_change': StatusChange,
285
+ 'priority_change': PriorityChange,
286
+ 'reopen_strategy_change': OldNewOptionalType,
287
+ 'title_change': OldNewOptionalType,
288
+ 'entities_change': AddedRemovedTypeEntities,
289
+ 'related_entities_change': AddedRemovedTypeEntities,
290
+ 'description_change': OldNewOptionalType,
291
+ 'external_id_change': OldNewOptionalType,
292
+ 'comment_change': CommentChange,
293
+ 'action_change': AddedRemovedList,
294
+ 'assessment_ids_change': AddedRemovedList,
295
+ 'dns_change': DomainAbuseDnsChange,
296
+ 'whois_change': DomainAbuseWhoisChange,
297
+ 'logotype_in_screenshot_change': DomainAbuseLogoTypeChange,
298
+ 'malicious_dns_change': DomainAbuseMaliciousDnsChange,
299
+ 'reregistration_change': DomainAbuseReregistrationRecordChange,
300
+ 'malicious_url_change': DomainAbuseMaliciousUrlChange,
301
+ 'screenshot_mentions_change': DomainAbuseScreenshotMentions,
302
+ 'lifecycle_in_cve_change': VulnerabilityLifecycleChange,
303
+ 'evidence_change': CodeRepoLeakageEvidenceChange,
304
+ 'tpr_assessment_change': ThirdPartyAssessmentChange,
305
+ 'assessment_change': AssessmentChange,
306
+ }
307
+
308
+
309
+ class PanelLogV2(RFBaseModel):
310
+ id_: str = Field(alias='id')
311
+ author_id: Optional[str] = None
312
+ author_name: Optional[str] = None
313
+ created: datetime
314
+ changes: list
315
+
316
+ @model_validator(mode='before')
317
+ @classmethod
318
+ def validate_changes(cls, data):
319
+ """Validate each panel_log_v2 changes based on the supported changes.
320
+
321
+ The list of changes is in ``TYPE_MAPPING``. Skip unsupported changes.
322
+ """
323
+ new_changes = [
324
+ model_type.model_validate(change)
325
+ for change in data.get('changes', [])
326
+ if (change_type := change.get('type')) and (model_type := TYPE_MAPPING.get(change_type))
327
+ ]
328
+ data['changes'] = new_changes
329
+ return data