diff --git a/content/response_integrations/third_party/partner/silverfort/.python-version b/content/response_integrations/third_party/partner/silverfort/.python-version new file mode 100644 index 000000000..902b2c90c --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/.python-version @@ -0,0 +1 @@ +3.11 \ No newline at end of file diff --git a/content/response_integrations/third_party/partner/silverfort/__init__.py b/content/response_integrations/third_party/partner/silverfort/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/content/response_integrations/third_party/partner/silverfort/actions/__init__.py b/content/response_integrations/third_party/partner/silverfort/actions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.py b/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.py new file mode 100644 index 000000000..64efc997d --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.py @@ -0,0 +1,74 @@ +"""Change Policy State action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import CHANGE_POLICY_STATE_SCRIPT_NAME + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully {action} policy: {policy_id}" +ERROR_MESSAGE: str = "Failed to change policy state!" + + +class ChangePolicyState(SilverfortAction): + """Action to enable or disable a policy in Silverfort.""" + + def __init__(self) -> None: + """Initialize the Change Policy State action.""" + super().__init__(CHANGE_POLICY_STATE_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.policy_id = extract_action_param( + self.soar_action, + param_name="Policy ID", + is_mandatory=True, + print_value=True, + ) + self.params.enabled = extract_action_param( + self.soar_action, + param_name="Enable Policy", + is_mandatory=True, + input_type=bool, + print_value=True, + ) + + def _perform_action(self, _=None) -> None: + """Perform the change policy state action.""" + client = self._get_policy_client() + + client.change_policy_state( + policy_id=self.params.policy_id, + state=self.params.enabled, + ) + + action = "enabled" if self.params.enabled else "disabled" + + self.json_results = { + "policy_id": self.params.policy_id, + "enabled": self.params.enabled, + "status": action, + } + + self.output_message = SUCCESS_MESSAGE.format( + action=action, + policy_id=self.params.policy_id, + ) + + +def main() -> NoReturn: + """Main entry point for the Change Policy State action.""" + ChangePolicyState().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.yaml b/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.yaml new file mode 100644 index 000000000..1cf3a185f --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/change_policy_state.yaml @@ -0,0 +1,21 @@ +creator: admin +description: Enable or disable an authentication policy in Silverfort. This is a + quick way to toggle a policy's active state without modifying its configuration. +dynamic_results_metadata: +- result_example_path: resources/change_policy_state_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Change Policy State +parameters: +- default_value: '' + description: The ID of the policy to enable or disable. + is_mandatory: true + name: Policy ID + type: string +- default_value: true + description: Set to true to enable the policy, false to disable it. + is_mandatory: true + name: Enable Policy + type: boolean +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.py b/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.py new file mode 100644 index 000000000..2ebc142a3 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.py @@ -0,0 +1,73 @@ +"""Get Entity Risk action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import GET_ENTITY_RISK_SCRIPT_NAME +from ..core.exceptions import SilverfortInvalidParameterError + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully retrieved risk information for: {entity}" +ERROR_MESSAGE: str = "Failed to get entity risk information!" + + +class GetEntityRisk(SilverfortAction): + """Action to get risk information for a user or resource.""" + + def __init__(self) -> None: + """Initialize the Get Entity Risk action.""" + super().__init__(GET_ENTITY_RISK_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.user_principal_name = extract_action_param( + self.soar_action, + param_name="User Principal Name", + print_value=True, + ) + self.params.resource_name = extract_action_param( + self.soar_action, + param_name="Resource Name", + print_value=True, + ) + + def _validate_params(self) -> None: + """Validate action parameters.""" + if not self.params.user_principal_name and not self.params.resource_name: + raise SilverfortInvalidParameterError( + "Either 'User Principal Name' or 'Resource Name' must be provided." + ) + + def _perform_action(self, _=None) -> None: + """Perform the get entity risk action.""" + client = self._get_risk_client() + + entity_risk = client.get_entity_risk( + user_principal_name=self.params.user_principal_name, + resource_name=self.params.resource_name, + ) + + # Set JSON result + self.json_results = entity_risk.to_json() + + # Determine entity identifier for message + entity = self.params.user_principal_name or self.params.resource_name + self.output_message = SUCCESS_MESSAGE.format(entity=entity) + + +def main() -> NoReturn: + """Main entry point for the Get Entity Risk action.""" + GetEntityRisk().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.yaml b/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.yaml new file mode 100644 index 000000000..9193d26da --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_entity_risk.yaml @@ -0,0 +1,24 @@ +creator: admin +description: Get risk information for a user or resource from Silverfort. Returns + the current risk score, severity, and risk factors. You must provide either the + User Principal Name (for users) or Resource Name (for resources). +dynamic_results_metadata: +- result_example_path: resources/get_entity_risk_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Get Entity Risk +parameters: +- default_value: '' + description: The user principal name (e.g., user@domain.com). Either this or Resource + Name must be provided. + is_mandatory: false + name: User Principal Name + type: string +- default_value: '' + description: The resource name for non-user entities. Either this or User Principal + Name must be provided. + is_mandatory: false + name: Resource Name + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_policy.py b/content/response_integrations/third_party/partner/silverfort/actions/get_policy.py new file mode 100644 index 000000000..15a35c890 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_policy.py @@ -0,0 +1,60 @@ +"""Get Policy action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import GET_POLICY_SCRIPT_NAME + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully retrieved policy: {policy_name} (ID: {policy_id})" +ERROR_MESSAGE: str = "Failed to get policy information!" + + +class GetPolicy(SilverfortAction): + """Action to get policy details from Silverfort.""" + + def __init__(self) -> None: + """Initialize the Get Policy action.""" + super().__init__(GET_POLICY_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.policy_id = extract_action_param( + self.soar_action, + param_name="Policy ID", + is_mandatory=True, + print_value=True, + ) + + def _perform_action(self, _=None) -> None: + """Perform the get policy action.""" + client = self._get_policy_client() + + policy = client.get_policy(self.params.policy_id) + + # Set JSON result + self.json_results = policy.to_json() + + policy_name = policy.policy_name or f"Policy {self.params.policy_id}" + self.output_message = SUCCESS_MESSAGE.format( + policy_name=policy_name, + policy_id=self.params.policy_id, + ) + + +def main() -> NoReturn: + """Main entry point for the Get Policy action.""" + GetPolicy().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_policy.yaml b/content/response_integrations/third_party/partner/silverfort/actions/get_policy.yaml new file mode 100644 index 000000000..fd0b9e5dd --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_policy.yaml @@ -0,0 +1,17 @@ +creator: admin +description: Get detailed information about a specific authentication policy from + Silverfort by its ID. Returns the policy configuration including users, groups, + sources, destinations, and action settings. +dynamic_results_metadata: +- result_example_path: resources/get_policy_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Get Policy +parameters: +- default_value: '' + description: The ID of the policy to retrieve. + is_mandatory: true + name: Policy ID + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.py b/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.py new file mode 100644 index 000000000..aa49bcc0f --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.py @@ -0,0 +1,60 @@ +"""Get Service Account action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import GET_SERVICE_ACCOUNT_SCRIPT_NAME + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully retrieved service account: {display_name} ({guid})" +ERROR_MESSAGE: str = "Failed to get service account information!" + + +class GetServiceAccount(SilverfortAction): + """Action to get service account details from Silverfort.""" + + def __init__(self) -> None: + """Initialize the Get Service Account action.""" + super().__init__(GET_SERVICE_ACCOUNT_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.guid = extract_action_param( + self.soar_action, + param_name="Service Account GUID", + is_mandatory=True, + print_value=True, + ) + + def _perform_action(self, _=None) -> None: + """Perform the get service account action.""" + client = self._get_service_account_client() + + service_account = client.get_service_account(self.params.guid) + + # Set JSON result + self.json_results = service_account.to_json() + + display_name = service_account.display_name or service_account.upn or self.params.guid + self.output_message = SUCCESS_MESSAGE.format( + display_name=display_name, + guid=self.params.guid, + ) + + +def main() -> NoReturn: + """Main entry point for the Get Service Account action.""" + GetServiceAccount().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.yaml b/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.yaml new file mode 100644 index 000000000..1b3e06099 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/get_service_account.yaml @@ -0,0 +1,17 @@ +creator: admin +description: Get detailed information about a specific service account from Silverfort + by its GUID. Returns the service account's attributes including risk, predictability, + protection status, and more. +dynamic_results_metadata: +- result_example_path: resources/get_service_account_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Get Service Account +parameters: +- default_value: '' + description: The GUID of the service account to retrieve. + is_mandatory: true + name: Service Account GUID + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/list_policies.py b/content/response_integrations/third_party/partner/silverfort/actions/list_policies.py new file mode 100644 index 000000000..6fad8dd9e --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/list_policies.py @@ -0,0 +1,68 @@ +"""List Policies action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import LIST_POLICIES_SCRIPT_NAME, POLICY_INDEX_FIELDS + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully retrieved {count} policies." +ERROR_MESSAGE: str = "Failed to list policies!" + + +class ListPolicies(SilverfortAction): + """Action to list policies from Silverfort.""" + + def __init__(self) -> None: + """Initialize the List Policies action.""" + super().__init__(LIST_POLICIES_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.fields = extract_action_param( + self.soar_action, + param_name="Fields", + print_value=True, + ) + + def _perform_action(self, _=None) -> None: + """Perform the list policies action.""" + client = self._get_policy_client() + + # Parse fields if provided + fields = None + if self.params.fields: + fields = [f.strip() for f in self.params.fields.split(",")] + # Validate fields + invalid_fields = [f for f in fields if f not in POLICY_INDEX_FIELDS] + if invalid_fields: + self.logger.warning( + f"Invalid fields specified (will be ignored): {invalid_fields}. " + f"Valid fields are: {POLICY_INDEX_FIELDS}" + ) + fields = [f for f in fields if f in POLICY_INDEX_FIELDS] + + result = client.list_policies(fields=fields) + + # Set JSON result + self.json_results = result.to_json() + + self.output_message = SUCCESS_MESSAGE.format(count=len(result.policies)) + + +def main() -> NoReturn: + """Main entry point for the List Policies action.""" + ListPolicies().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/list_policies.yaml b/content/response_integrations/third_party/partner/silverfort/actions/list_policies.yaml new file mode 100644 index 000000000..87306baa7 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/list_policies.yaml @@ -0,0 +1,20 @@ +creator: admin +description: List all authentication policies from Silverfort with optional field + filtering. Returns a list of policies with their configurations. +dynamic_results_metadata: +- result_example_path: resources/list_policies_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: List Policies +parameters: +- default_value: '' + description: 'Comma-separated list of fields to include in the response. Available + fields: enabled, policyName, authType, protocols, policyType, allUsersAndGroups, + usersAndGroups, allDevices, sources, allDestinations, destinations, action, + MFAPrompt, all, bridgeType. If not specified, all fields are returned.' + is_mandatory: false + + name: Fields + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.py b/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.py new file mode 100644 index 000000000..61f90645a --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.py @@ -0,0 +1,94 @@ +"""List Service Accounts action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import ( + DEFAULT_PAGE_NUMBER, + DEFAULT_PAGE_SIZE, + LIST_SERVICE_ACCOUNTS_SCRIPT_NAME, + SA_INDEX_FIELDS, +) + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully retrieved {count} service accounts (page {page})." +ERROR_MESSAGE: str = "Failed to list service accounts!" + + +class ListServiceAccounts(SilverfortAction): + """Action to list service accounts from Silverfort.""" + + def __init__(self) -> None: + """Initialize the List Service Accounts action.""" + super().__init__(LIST_SERVICE_ACCOUNTS_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.page_size = extract_action_param( + self.soar_action, + param_name="Page Size", + default_value=DEFAULT_PAGE_SIZE, + input_type=int, + print_value=True, + ) + self.params.page_number = extract_action_param( + self.soar_action, + param_name="Page Number", + default_value=DEFAULT_PAGE_NUMBER, + input_type=int, + print_value=True, + ) + self.params.fields = extract_action_param( + self.soar_action, + param_name="Fields", + print_value=True, + ) + + def _perform_action(self, _=None) -> None: + """Perform the list service accounts action.""" + client = self._get_service_account_client() + + # Parse fields if provided + fields = None + if self.params.fields: + fields = [f.strip() for f in self.params.fields.split(",")] + # Validate fields + invalid_fields = [f for f in fields if f not in SA_INDEX_FIELDS] + if invalid_fields: + self.logger.warning( + f"Invalid fields specified (will be ignored): {invalid_fields}. " + f"Valid fields are: {SA_INDEX_FIELDS}" + ) + fields = [f for f in fields if f in SA_INDEX_FIELDS] + + result = client.list_service_accounts( + page_size=self.params.page_size, + page_number=self.params.page_number, + fields=fields, + ) + + # Set JSON result + self.json_results = result.to_json() + + self.output_message = SUCCESS_MESSAGE.format( + count=len(result.service_accounts), + page=self.params.page_number, + ) + + +def main() -> NoReturn: + """Main entry point for the List Service Accounts action.""" + ListServiceAccounts().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.yaml b/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.yaml new file mode 100644 index 000000000..c1e74acd6 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/list_service_accounts.yaml @@ -0,0 +1,31 @@ +creator: admin +description: List service accounts from Silverfort with optional pagination and field + filtering. Returns a list of service accounts with their attributes. +dynamic_results_metadata: +- result_example_path: resources/list_service_accounts_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: List Service Accounts +parameters: +- default_value: '50' + description: Number of results per page (1-100). + is_mandatory: false + name: Page Size + type: string +- default_value: '1' + description: Page number to retrieve. + is_mandatory: false + name: Page Number + type: string +- default_value: '' + description: 'Comma-separated list of fields to include in the response. Available + fields: guid, display_name, sources_count, destinations_count, number_of_authentications, + risk, predictability, protected, upn, dn, spn, comment, owner, type, domain, + category, creation_date, highly_privileged, interactive_login, broadly_used, + suspected_brute_force, repetitive_behavior. If not specified, all fields are + returned.' + is_mandatory: false + name: Fields + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/ping.py b/content/response_integrations/third_party/partner/silverfort/actions/ping.py new file mode 100644 index 000000000..01d2f4d30 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/ping.py @@ -0,0 +1,97 @@ +"""Ping action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..core.auth import get_configured_api_types +from ..core.base_action import SilverfortAction +from ..core.constants import PING_SCRIPT_NAME, ApiType +from ..core.exceptions import SilverfortError + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully connected to Silverfort API with the provided credentials!" +PARTIAL_SUCCESS_MESSAGE: str = ( + "Successfully connected to Silverfort API. " + "Connected APIs: {connected_apis}. " + "Failed APIs: {failed_apis}." +) +NO_CREDENTIALS_MESSAGE: str = ( + "No API credentials configured. Please configure at least one set of API credentials " + "(Risk, Service Accounts, or Policies) in the integration settings." +) +ERROR_MESSAGE: str = "Failed to connect to Silverfort API!" + + +class Ping(SilverfortAction): + """Ping action to test connectivity to Silverfort API.""" + + def __init__(self) -> None: + """Initialize the Ping action.""" + super().__init__(PING_SCRIPT_NAME) + self.output_message: str = SUCCESS_MESSAGE + self.error_output_message: str = ERROR_MESSAGE + + def _perform_action(self, _=None) -> None: + """Perform the ping action to test API connectivity.""" + params = self._get_integration_params() + configured_apis = get_configured_api_types(params) + + if not configured_apis: + raise SilverfortError(NO_CREDENTIALS_MESSAGE) + + connected_apis: list[str] = [] + failed_apis: list[str] = [] + + for api_type in configured_apis: + try: + self._test_api_connectivity(api_type) + connected_apis.append(api_type.value) + except Exception as e: + self.logger.error(f"Failed to connect to {api_type.value} API: {e}") + failed_apis.append(api_type.value) + + if not connected_apis: + raise SilverfortError( + f"Failed to connect to any Silverfort API. Failed APIs: {', '.join(failed_apis)}" + ) + + if failed_apis: + self.output_message = PARTIAL_SUCCESS_MESSAGE.format( + connected_apis=", ".join(connected_apis), + failed_apis=", ".join(failed_apis), + ) + else: + self.output_message = SUCCESS_MESSAGE + f" Connected APIs: {', '.join(connected_apis)}" + + def _test_api_connectivity(self, api_type: ApiType) -> bool: + """Test connectivity to a specific API. + + Args: + api_type: Type of API to test. + + Returns: + True if connectivity test succeeds. + """ + if api_type == ApiType.RISK: + client = self._get_risk_client() + return client.test_connectivity() + elif api_type == ApiType.SERVICE_ACCOUNTS: + client = self._get_service_account_client() + return client.test_connectivity() + elif api_type == ApiType.POLICIES: + client = self._get_policy_client() + return client.test_connectivity() + return False + + +def main() -> NoReturn: + """Main entry point for the Ping action.""" + Ping().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/ping.yaml b/content/response_integrations/third_party/partner/silverfort/actions/ping.yaml new file mode 100644 index 000000000..3a26f20dd --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/ping.yaml @@ -0,0 +1,9 @@ +creator: admin +description: Use the Ping action to test connectivity to Silverfort API. This action + validates that the External API Key is working and tests connectivity for each + configured API (Risk, Service Accounts, Policies) using their respective credentials. +dynamic_results_metadata: [] +integration_identifier: Silverfort +name: Ping +parameters: [] +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.py b/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.py new file mode 100644 index 000000000..9c5ded4cc --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.py @@ -0,0 +1,124 @@ +"""Update Entity Risk action for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import UPDATE_ENTITY_RISK_SCRIPT_NAME, RiskSeverity, RiskType +from ..core.data_models import RiskUpdate +from ..core.exceptions import SilverfortInvalidParameterError + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully updated risk for: {user_principal_name}" +ERROR_MESSAGE: str = "Failed to update entity risk!" + + +class UpdateEntityRisk(SilverfortAction): + """Action to update risk information for a user entity.""" + + def __init__(self) -> None: + """Initialize the Update Entity Risk action.""" + super().__init__(UPDATE_ENTITY_RISK_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.user_principal_name = extract_action_param( + self.soar_action, + param_name="User Principal Name", + is_mandatory=True, + print_value=True, + ) + self.params.risk_type = extract_action_param( + self.soar_action, + param_name="Risk Type", + is_mandatory=True, + print_value=True, + ) + self.params.severity = extract_action_param( + self.soar_action, + param_name="Severity", + is_mandatory=True, + print_value=True, + ) + self.params.valid_for = extract_action_param( + self.soar_action, + param_name="Valid For Hours", + is_mandatory=True, + input_type=int, + print_value=True, + ) + self.params.description = extract_action_param( + self.soar_action, + param_name="Description", + is_mandatory=True, + print_value=True, + ) + + def _validate_params(self) -> None: + """Validate action parameters.""" + # Validate risk type + valid_risk_types = [rt.value for rt in RiskType] + if self.params.risk_type.lower() not in valid_risk_types: + raise SilverfortInvalidParameterError( + f"Invalid risk type: {self.params.risk_type}. " + f"Valid values are: {', '.join(valid_risk_types)}" + ) + + # Validate severity + valid_severities = [s.value for s in RiskSeverity] + if self.params.severity.lower() not in valid_severities: + raise SilverfortInvalidParameterError( + f"Invalid severity: {self.params.severity}. " + f"Valid values are: {', '.join(valid_severities)}" + ) + + # Validate valid_for + if self.params.valid_for <= 0: + raise SilverfortInvalidParameterError("'Valid For Hours' must be a positive integer.") + + def _perform_action(self, _=None) -> None: + """Perform the update entity risk action.""" + client = self._get_risk_client() + + risk_update = RiskUpdate( + severity=self.params.severity.lower(), + valid_for=self.params.valid_for, + description=self.params.description, + ) + + risks = {self.params.risk_type.lower(): risk_update} + + client.update_entity_risk( + user_principal_name=self.params.user_principal_name, + risks=risks, + ) + + self.json_results = { + "user_principal_name": self.params.user_principal_name, + "risk_type": self.params.risk_type, + "severity": self.params.severity, + "valid_for": self.params.valid_for, + "description": self.params.description, + "status": "updated", + } + + self.output_message = SUCCESS_MESSAGE.format( + user_principal_name=self.params.user_principal_name + ) + + +def main() -> NoReturn: + """Main entry point for the Update Entity Risk action.""" + UpdateEntityRisk().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.yaml b/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.yaml new file mode 100644 index 000000000..63a5479f5 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_entity_risk.yaml @@ -0,0 +1,47 @@ +creator: admin +description: Update risk information for a user in Silverfort. This allows you to + set a specific risk level for a user based on external threat intelligence or + security events. +dynamic_results_metadata: +- result_example_path: resources/update_entity_risk_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Update Entity Risk +parameters: +- default_value: '' + description: The user principal name (e.g., user@domain.com) to update risk for. + is_mandatory: true + name: User Principal Name + type: string +- default_value: activity_risk + description: The type of risk to update. + is_mandatory: true + name: Risk Type + optional_values: + - activity_risk + - malware_risk + - data_breach_risk + - custom_risk + type: ddl +- default_value: medium + description: The severity level of the risk. + is_mandatory: true + name: Severity + optional_values: + - low + - medium + - high + - critical + type: ddl +- default_value: '24' + description: How long (in hours) the risk indicator should be valid. + is_mandatory: true + name: Valid For Hours + type: string +- default_value: '' + description: Description of the risk indicator. + is_mandatory: true + name: Description + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_policy.py b/content/response_integrations/third_party/partner/silverfort/actions/update_policy.py new file mode 100644 index 000000000..9efa2d621 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_policy.py @@ -0,0 +1,157 @@ +"""Update Policy action for Silverfort integration.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import UPDATE_POLICY_SCRIPT_NAME +from ..core.data_models import PolicyDestination, PolicyIdentifier +from ..core.exceptions import SilverfortInvalidParameterError + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully updated policy: {policy_id}" +ERROR_MESSAGE: str = "Failed to update policy!" + + +class UpdatePolicy(SilverfortAction): + """Action to update an authentication policy in Silverfort.""" + + def __init__(self) -> None: + """Initialize the Update Policy action.""" + super().__init__(UPDATE_POLICY_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.policy_id = extract_action_param( + self.soar_action, + param_name="Policy ID", + is_mandatory=True, + print_value=True, + ) + self.params.enabled = extract_action_param( + self.soar_action, + param_name="Enabled", + input_type=bool, + print_value=True, + ) + self.params.add_users_and_groups = extract_action_param( + self.soar_action, + param_name="Add Users and Groups", + print_value=True, + ) + self.params.remove_users_and_groups = extract_action_param( + self.soar_action, + param_name="Remove Users and Groups", + print_value=True, + ) + self.params.add_sources = extract_action_param( + self.soar_action, + param_name="Add Sources", + print_value=True, + ) + self.params.remove_sources = extract_action_param( + self.soar_action, + param_name="Remove Sources", + print_value=True, + ) + self.params.add_destinations = extract_action_param( + self.soar_action, + param_name="Add Destinations", + print_value=True, + ) + self.params.remove_destinations = extract_action_param( + self.soar_action, + param_name="Remove Destinations", + print_value=True, + ) + + def _parse_identifiers(self, identifiers_json: str | None) -> list[PolicyIdentifier] | None: + """Parse JSON string of identifiers into PolicyIdentifier objects. + + Args: + identifiers_json: JSON string of identifiers. + + Returns: + List of PolicyIdentifier objects or None. + """ + if not identifiers_json: + return None + + try: + identifiers_data = json.loads(identifiers_json) + if not isinstance(identifiers_data, list): + raise SilverfortInvalidParameterError( + "Identifiers must be a JSON array of objects." + ) + return [PolicyIdentifier.from_json(i) for i in identifiers_data] + except json.JSONDecodeError as e: + raise SilverfortInvalidParameterError(f"Invalid JSON for identifiers: {e}") + + def _parse_destinations(self, destinations_json: str | None) -> list[PolicyDestination] | None: + """Parse JSON string of destinations into PolicyDestination objects. + + Args: + destinations_json: JSON string of destinations. + + Returns: + List of PolicyDestination objects or None. + """ + if not destinations_json: + return None + + try: + destinations_data = json.loads(destinations_json) + if not isinstance(destinations_data, list): + raise SilverfortInvalidParameterError( + "Destinations must be a JSON array of objects." + ) + return [PolicyDestination.from_json(d) for d in destinations_data] + except json.JSONDecodeError as e: + raise SilverfortInvalidParameterError(f"Invalid JSON for destinations: {e}") + + def _perform_action(self, _=None) -> None: + """Perform the update policy action.""" + client = self._get_policy_client() + + add_users_groups = self._parse_identifiers(self.params.add_users_and_groups) + remove_users_groups = self._parse_identifiers(self.params.remove_users_and_groups) + add_sources = self._parse_identifiers(self.params.add_sources) + remove_sources = self._parse_identifiers(self.params.remove_sources) + add_destinations = self._parse_destinations(self.params.add_destinations) + remove_destinations = self._parse_destinations(self.params.remove_destinations) + + client.update_policy( + policy_id=self.params.policy_id, + enabled=self.params.enabled, + add_users_and_groups=add_users_groups, + remove_users_and_groups=remove_users_groups, + add_sources=add_sources, + remove_sources=remove_sources, + add_destinations=add_destinations, + remove_destinations=remove_destinations, + ) + + self.json_results = { + "policy_id": self.params.policy_id, + "status": "updated", + } + + self.output_message = SUCCESS_MESSAGE.format(policy_id=self.params.policy_id) + + +def main() -> NoReturn: + """Main entry point for the Update Policy action.""" + UpdatePolicy().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_policy.yaml b/content/response_integrations/third_party/partner/silverfort/actions/update_policy.yaml new file mode 100644 index 000000000..bc666c81e --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_policy.yaml @@ -0,0 +1,61 @@ +creator: admin +description: Update an authentication policy in Silverfort. Allows modifying the + policy's enabled state and adding/removing users, groups, sources, and destinations. +dynamic_results_metadata: +- result_example_path: resources/update_policy_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Update Policy +parameters: +- default_value: '' + description: The ID of the policy to update. + is_mandatory: true + name: Policy ID + type: string +- default_value: '' + description: Enable or disable the policy. + is_mandatory: false + name: Enabled + type: boolean +- default_value: '' + description: 'JSON array of users/groups to add to the policy. Format: [{"identifierType": + "upn", "identifier": "user@domain.com", "displayName": "User Name", "domain": + "domain.com"}]' + is_mandatory: false + name: Add Users and Groups + type: string +- default_value: '' + description: 'JSON array of users/groups to remove from the policy. Format: [{"identifierType": + "upn", "identifier": "user@domain.com", "displayName": "User Name", "domain": + "domain.com"}]' + is_mandatory: false + name: Remove Users and Groups + type: string +- default_value: '' + description: 'JSON array of sources to add to the policy. Format: [{"identifierType": + "ip", "identifier": "10.0.0.1", "displayName": "Server"}]' + is_mandatory: false + name: Add Sources + type: string +- default_value: '' + description: 'JSON array of sources to remove from the policy. Format: [{"identifierType": + "ip", "identifier": "10.0.0.1", "displayName": "Server"}]' + is_mandatory: false + name: Remove Sources + type: string +- default_value: '' + description: 'JSON array of destinations to add to the policy. Format: [{"identifierType": + "hostname", "identifier": "server.domain.com", "displayName": "Server", "domain": + "domain.com", "services": ["rdp"]}]' + is_mandatory: false + name: Add Destinations + type: string +- default_value: '' + description: 'JSON array of destinations to remove from the policy. Format: [{"identifierType": + "hostname", "identifier": "server.domain.com", "displayName": "Server", "domain": + "domain.com", "services": ["rdp"]}]' + is_mandatory: false + name: Remove Destinations + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.py b/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.py new file mode 100644 index 000000000..758081749 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.py @@ -0,0 +1,193 @@ +"""Update Service Account Policy action for Silverfort integration.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +from TIPCommon.extraction import extract_action_param + +from ..core.base_action import SilverfortAction +from ..core.constants import UPDATE_SA_POLICY_SCRIPT_NAME, SAPolicyRiskLevel, SAProtocol +from ..core.data_models import AllowedEndpoint +from ..core.exceptions import SilverfortInvalidParameterError + +if TYPE_CHECKING: + from typing import NoReturn + + +SUCCESS_MESSAGE: str = "Successfully updated service account policy for GUID: {guid}" +ERROR_MESSAGE: str = "Failed to update service account policy!" + + +class UpdateSAPolicy(SilverfortAction): + """Action to update service account policy in Silverfort.""" + + def __init__(self) -> None: + """Initialize the Update SA Policy action.""" + super().__init__(UPDATE_SA_POLICY_SCRIPT_NAME) + self.output_message: str = "" + self.error_output_message: str = ERROR_MESSAGE + + def _extract_action_parameters(self) -> None: + """Extract action parameters.""" + self.params.guid = extract_action_param( + self.soar_action, + param_name="Service Account GUID", + is_mandatory=True, + print_value=True, + ) + self.params.enabled = extract_action_param( + self.soar_action, + param_name="Enabled", + input_type=bool, + print_value=True, + ) + self.params.block = extract_action_param( + self.soar_action, + param_name="Block", + input_type=bool, + print_value=True, + ) + self.params.send_to_siem = extract_action_param( + self.soar_action, + param_name="Send to SIEM", + input_type=bool, + print_value=True, + ) + self.params.risk_level = extract_action_param( + self.soar_action, + param_name="Risk Level", + print_value=True, + ) + self.params.allow_all_sources = extract_action_param( + self.soar_action, + param_name="Allow All Sources", + input_type=bool, + print_value=True, + ) + self.params.allow_all_destinations = extract_action_param( + self.soar_action, + param_name="Allow All Destinations", + input_type=bool, + print_value=True, + ) + self.params.protocols = extract_action_param( + self.soar_action, + param_name="Protocols", + print_value=True, + ) + self.params.add_allowed_sources = extract_action_param( + self.soar_action, + param_name="Add Allowed Sources", + print_value=True, + ) + self.params.remove_allowed_sources = extract_action_param( + self.soar_action, + param_name="Remove Allowed Sources", + print_value=True, + ) + self.params.add_allowed_destinations = extract_action_param( + self.soar_action, + param_name="Add Allowed Destinations", + print_value=True, + ) + self.params.remove_allowed_destinations = extract_action_param( + self.soar_action, + param_name="Remove Allowed Destinations", + print_value=True, + ) + + def _validate_params(self) -> None: + """Validate action parameters.""" + if self.params.risk_level: + valid_levels = [level.value for level in SAPolicyRiskLevel] + if self.params.risk_level.lower() not in valid_levels: + raise SilverfortInvalidParameterError( + f"Invalid risk level: {self.params.risk_level}. " + f"Valid values are: {', '.join(valid_levels)}" + ) + + if self.params.protocols: + protocols = [p.strip() for p in self.params.protocols.split(",")] + valid_protocols = [p.value for p in SAProtocol] + invalid = [p for p in protocols if p not in valid_protocols] + if invalid: + raise SilverfortInvalidParameterError( + f"Invalid protocols: {invalid}. Valid values are: {', '.join(valid_protocols)}" + ) + + def _parse_endpoints(self, endpoints_json: str | None) -> list[AllowedEndpoint] | None: + """Parse JSON string of endpoints into AllowedEndpoint objects. + + Args: + endpoints_json: JSON string like [{"key": "10.0.0.1", "key_type": "ip"}] + + Returns: + List of AllowedEndpoint objects or None. + """ + if not endpoints_json: + return None + + try: + endpoints_data = json.loads(endpoints_json) + if not isinstance(endpoints_data, list): + raise SilverfortInvalidParameterError( + "Endpoints must be a JSON array of objects with 'key' and 'key_type' fields." + ) + return [ + AllowedEndpoint(key=ep["key"], key_type=ep["key_type"]) for ep in endpoints_data + ] + except json.JSONDecodeError as e: + raise SilverfortInvalidParameterError(f"Invalid JSON for endpoints: {e}") + except KeyError as e: + raise SilverfortInvalidParameterError( + f"Missing required field in endpoint: {e}. " + "Each endpoint must have 'key' and 'key_type' fields." + ) + + def _perform_action(self, _=None) -> None: + """Perform the update service account policy action.""" + client = self._get_service_account_client() + + # Parse protocols + protocols = None + if self.params.protocols: + protocols = [p.strip() for p in self.params.protocols.split(",")] + + # Parse endpoints + add_sources = self._parse_endpoints(self.params.add_allowed_sources) + remove_sources = self._parse_endpoints(self.params.remove_allowed_sources) + add_destinations = self._parse_endpoints(self.params.add_allowed_destinations) + remove_destinations = self._parse_endpoints(self.params.remove_allowed_destinations) + + client.update_service_account_policy( + guid=self.params.guid, + enabled=self.params.enabled, + block=self.params.block, + send_to_siem=self.params.send_to_siem, + risk_level=self.params.risk_level.lower() if self.params.risk_level else None, + allow_all_sources=self.params.allow_all_sources, + allow_all_destinations=self.params.allow_all_destinations, + protocols=protocols, + add_allowed_sources=add_sources, + remove_allowed_sources=remove_sources, + add_allowed_destinations=add_destinations, + remove_allowed_destinations=remove_destinations, + ) + + self.json_results = { + "guid": self.params.guid, + "status": "updated", + } + + self.output_message = SUCCESS_MESSAGE.format(guid=self.params.guid) + + +def main() -> NoReturn: + """Main entry point for the Update SA Policy action.""" + UpdateSAPolicy().run() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.yaml b/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.yaml new file mode 100644 index 000000000..a27874865 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/actions/update_sa_policy.yaml @@ -0,0 +1,81 @@ +creator: admin +description: Update the protection policy for a service account in Silverfort. Allows + configuring blocking, SIEM logging, risk level thresholds, protocols, and allowed + sources/destinations. +dynamic_results_metadata: +- result_example_path: resources/update_sa_policy_JsonResult_example.json + result_name: JsonResult + show_result: true +integration_identifier: Silverfort +name: Update SA Policy +parameters: +- default_value: '' + description: The GUID of the service account whose policy to update. + is_mandatory: true + name: Service Account GUID + type: string +- default_value: '' + description: Enable or disable the policy. + is_mandatory: false + name: Enabled + type: boolean +- default_value: '' + description: Enable or disable blocking for policy violations. + is_mandatory: false + name: Block + type: boolean +- default_value: '' + description: Enable or disable SIEM logging. + is_mandatory: false + name: Send to SIEM + type: boolean +- default_value: '' + description: Risk level threshold for the policy. + is_mandatory: false + name: Risk Level + optional_values: + - low + - medium + - high + type: ddl +- default_value: '' + description: Allow all sources (if true, ignores specific allowed sources list). + is_mandatory: false + name: Allow All Sources + type: boolean +- default_value: '' + description: Allow all destinations (if true, ignores specific allowed destinations + list). + is_mandatory: false + name: Allow All Destinations + type: boolean +- default_value: '' + description: 'Comma-separated list of protocols to allow (Kerberos, ldap, ntlm).' + is_mandatory: false + name: Protocols + type: string +- default_value: '' + description: 'JSON array of sources to add to the allowlist. Format: [{"key": + "10.0.0.1", "key_type": "ip"}]' + is_mandatory: false + name: Add Allowed Sources + type: string +- default_value: '' + description: 'JSON array of sources to remove from the allowlist. Format: [{"key": + "10.0.0.1", "key_type": "ip"}]' + is_mandatory: false + name: Remove Allowed Sources + type: string +- default_value: '' + description: 'JSON array of destinations to add to the allowlist. Format: [{"key": + "10.0.0.1", "key_type": "ip"}]' + is_mandatory: false + name: Add Allowed Destinations + type: string +- default_value: '' + description: 'JSON array of destinations to remove from the allowlist. Format: + [{"key": "10.0.0.1", "key_type": "ip"}]' + is_mandatory: false + name: Remove Allowed Destinations + type: string +script_result_name: is_success diff --git a/content/response_integrations/third_party/partner/silverfort/core/__init__.py b/content/response_integrations/third_party/partner/silverfort/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/content/response_integrations/third_party/partner/silverfort/core/api_utils.py b/content/response_integrations/third_party/partner/silverfort/core/api_utils.py new file mode 100644 index 000000000..95ea88ab9 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/api_utils.py @@ -0,0 +1,109 @@ +"""API utility functions for Silverfort integration.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING +from urllib.parse import urljoin + +import requests + +from .constants import ENDPOINTS +from .exceptions import SilverfortAPIError, SilverfortHTTPError + +if TYPE_CHECKING: + from collections.abc import Mapping + + +def get_full_url( + api_root: str, + endpoint_id: str, + endpoints: Mapping[str, str] | None = None, + **kwargs, +) -> str: + """Construct the full URL using a URL identifier and optional variables. + + Args: + api_root: The root of the API endpoint. + endpoint_id: The identifier for the specific URL. + endpoints: Optional endpoints dictionary (defaults to ENDPOINTS). + **kwargs: Variables passed for string formatting. + + Returns: + The full URL constructed from API root, endpoint identifier and variables. + """ + endpoints = endpoints or ENDPOINTS + endpoint = endpoints[endpoint_id].format(**kwargs) + return urljoin(api_root + "/", endpoint.lstrip("/")) + + +def validate_response( + response: requests.Response, + error_msg: str = "An error occurred", +) -> None: + """Validate API response and raise appropriate exceptions on errors. + + Args: + response: Response to validate. + error_msg: Default message to display on error. + + Raises: + SilverfortAPIError: If there is an API-specific error. + SilverfortHTTPError: If there is an HTTP error. + """ + try: + if response.status_code == 422: + try: + error_data = response.json() + detail = error_data.get("detail", error_data.get("message", "Unknown Error")) + raise SilverfortAPIError( + f"Invalid request parameters: {detail}", + error_code="VALIDATION_ERROR", + details=error_data, + ) + except json.JSONDecodeError: + raise SilverfortAPIError( + f"Invalid request parameters: {response.text}", + error_code="VALIDATION_ERROR", + ) + + if response.status_code == 401: + raise SilverfortHTTPError( + "Authentication failed. Please verify your API credentials.", + status_code=401, + ) + + if response.status_code == 403: + raise SilverfortHTTPError( + "Access forbidden. Please verify your API permissions.", + status_code=403, + ) + + if response.status_code == 404: + try: + error_data = response.json() + detail = error_data.get("detail", error_data.get("message", "Resource not found")) + raise SilverfortAPIError( + detail, + error_code="NOT_FOUND", + details=error_data, + ) + except json.JSONDecodeError: + raise SilverfortAPIError("Resource not found", error_code="NOT_FOUND") + + response.raise_for_status() + + except requests.HTTPError as error: + msg = f"{error_msg}: {error}" + if error.response is not None: + try: + error_content = error.response.json() + msg = f"{error_msg}: {error_content}" + except json.JSONDecodeError: + content = error.response.content.decode("utf-8", errors="ignore") + msg = f"{error_msg}: {error} - {content}" + + raise SilverfortHTTPError( + msg, + status_code=error.response.status_code if error.response else None, + ) from error diff --git a/content/response_integrations/third_party/partner/silverfort/core/auth.py b/content/response_integrations/third_party/partner/silverfort/core/auth.py new file mode 100644 index 000000000..d56f91f23 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/auth.py @@ -0,0 +1,282 @@ +"""Authentication module for Silverfort integration.""" + +from __future__ import annotations + +import dataclasses +import time +from typing import TYPE_CHECKING + +import jwt +from requests import Session +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyConnectors import SiemplifyConnectorExecution +from soar_sdk.SiemplifyJob import SiemplifyJob +from TIPCommon.base.interfaces import Authable +from TIPCommon.base.utils import CreateSession +from TIPCommon.extraction import extract_script_param + +from .constants import ( + API_KEY_HEADER, + AUTHORIZATION_HEADER, + DEFAULT_VERIFY_SSL, + INTEGRATION_IDENTIFIER, + JWT_ALGORITHM, + JWT_EXPIRATION_SECONDS, + ApiType, +) +from .data_models import AppCredentials, IntegrationParameters +from .exceptions import SilverfortConfigurationError, SilverfortCredentialsNotConfiguredError + +if TYPE_CHECKING: + from TIPCommon.types import ChronicleSOAR + + +@dataclasses.dataclass(slots=True) +class SessionAuthenticationParameters: + """Parameters for session authentication.""" + + api_root: str + external_api_key: str + verify_ssl: bool + app_credentials: AppCredentials | None = None + api_type: ApiType | None = None + + +class JWTAuthenticator: + """JWT token generator for Silverfort API authentication.""" + + def __init__(self, app_user_id: str, app_user_secret: str) -> None: + """Initialize the JWT authenticator. + + Args: + app_user_id: The App User ID (used as issuer claim). + app_user_secret: The App User Secret (used as signing key). + """ + self.app_user_id = app_user_id + self.app_user_secret = app_user_secret + + def generate_token(self) -> str: + """Generate a JWT token for API authentication. + + Returns: + JWT token string. + """ + current_time = int(time.time()) + payload = { + "issuer": self.app_user_id, + "iat": current_time, + "exp": current_time + JWT_EXPIRATION_SECONDS, + } + return jwt.encode(payload, self.app_user_secret, algorithm=JWT_ALGORITHM) + + +def build_auth_params(soar_sdk_object: ChronicleSOAR) -> IntegrationParameters: + """Extract authentication parameters from SOAR SDK object. + + Args: + soar_sdk_object: ChronicleSOAR SDK object (Action, Connector, or Job). + + Returns: + IntegrationParameters object with extracted credentials. + + Raises: + SilverfortConfigurationError: If SDK object type is not supported. + """ + sdk_class = type(soar_sdk_object).__name__ + + if sdk_class == SiemplifyAction.__name__: + input_dictionary = soar_sdk_object.get_configuration(INTEGRATION_IDENTIFIER) + elif sdk_class in (SiemplifyConnectorExecution.__name__, SiemplifyJob.__name__): + input_dictionary = soar_sdk_object.parameters + else: + raise SilverfortConfigurationError( + f"Provided SOAR instance is not supported! type: {sdk_class}." + ) + + api_root = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="API Root", + is_mandatory=True, + print_value=True, + ) + + external_api_key = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="External API Key", + is_mandatory=True, + ) + + verify_ssl = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Verify SSL", + default_value=DEFAULT_VERIFY_SSL, + input_type=bool, + is_mandatory=False, + print_value=True, + ) + + # Risk API credentials (optional) + risk_app_user_id = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Risk App User ID", + is_mandatory=False, + ) + risk_app_user_secret = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Risk App User Secret", + is_mandatory=False, + ) + + # Service Accounts API credentials (optional) + service_accounts_app_user_id = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Service Accounts App User ID", + is_mandatory=False, + ) + service_accounts_app_user_secret = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Service Accounts App User Secret", + is_mandatory=False, + ) + + # Policies API credentials (optional) + policies_app_user_id = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Policies App User ID", + is_mandatory=False, + ) + policies_app_user_secret = extract_script_param( + soar_sdk_object, + input_dictionary=input_dictionary, + param_name="Policies App User Secret", + is_mandatory=False, + ) + + return IntegrationParameters( + api_root=api_root.rstrip("/"), + external_api_key=external_api_key, + verify_ssl=verify_ssl, + risk_app_user_id=risk_app_user_id, + risk_app_user_secret=risk_app_user_secret, + service_accounts_app_user_id=service_accounts_app_user_id, + service_accounts_app_user_secret=service_accounts_app_user_secret, + policies_app_user_id=policies_app_user_id, + policies_app_user_secret=policies_app_user_secret, + ) + + +def get_app_credentials( + params: IntegrationParameters, + api_type: ApiType, +) -> AppCredentials: + """Get app credentials for a specific API type. + + Args: + params: Integration parameters containing all credentials. + api_type: Type of API to get credentials for. + + Returns: + AppCredentials for the specified API type. + + Raises: + SilverfortCredentialsNotConfiguredError: If credentials are not configured. + """ + credential_map = { + ApiType.RISK: (params.risk_app_user_id, params.risk_app_user_secret), + ApiType.SERVICE_ACCOUNTS: ( + params.service_accounts_app_user_id, + params.service_accounts_app_user_secret, + ), + ApiType.POLICIES: (params.policies_app_user_id, params.policies_app_user_secret), + } + + user_id, user_secret = credential_map.get(api_type, (None, None)) + + if not user_id or not user_secret: + raise SilverfortCredentialsNotConfiguredError(api_type.value.replace("_", " ").title()) + + return AppCredentials(app_user_id=user_id, app_user_secret=user_secret) + + +def get_configured_api_types(params: IntegrationParameters) -> list[ApiType]: + """Get list of API types that have credentials configured. + + Args: + params: Integration parameters containing all credentials. + + Returns: + List of ApiType enums for which credentials are configured. + """ + configured = [] + + if params.risk_app_user_id and params.risk_app_user_secret: + configured.append(ApiType.RISK) + if params.service_accounts_app_user_id and params.service_accounts_app_user_secret: + configured.append(ApiType.SERVICE_ACCOUNTS) + if params.policies_app_user_id and params.policies_app_user_secret: + configured.append(ApiType.POLICIES) + + return configured + + +class AuthenticatedSession(Authable): + """Authenticated session for Silverfort API requests.""" + + def authenticate_session(self, params: SessionAuthenticationParameters) -> None: + """Authenticate the session with Silverfort credentials. + + Args: + params: Session authentication parameters. + """ + self.session = get_authenticated_session(session_parameters=params) + + +def get_authenticated_session(session_parameters: SessionAuthenticationParameters) -> Session: + """Get an authenticated session for API requests. + + Args: + session_parameters: Authentication parameters. + + Returns: + Authenticated requests Session object. + """ + session: Session = CreateSession.create_session() + _authenticate_session(session, session_parameters=session_parameters) + return session + + +def _authenticate_session( + session: Session, + session_parameters: SessionAuthenticationParameters, +) -> None: + """Configure session with authentication headers. + + Args: + session: Requests session to configure. + session_parameters: Authentication parameters. + """ + session.verify = session_parameters.verify_ssl + + # Add External API Key header (always required) + session.headers.update({ + API_KEY_HEADER: session_parameters.external_api_key, + }) + + # Add JWT Bearer token if app credentials are provided + if session_parameters.app_credentials: + jwt_authenticator = JWTAuthenticator( + app_user_id=session_parameters.app_credentials.app_user_id, + app_user_secret=session_parameters.app_credentials.app_user_secret, + ) + jwt_token = jwt_authenticator.generate_token() + session.headers.update({ + AUTHORIZATION_HEADER: f"Bearer {jwt_token}", + }) diff --git a/content/response_integrations/third_party/partner/silverfort/core/base_action.py b/content/response_integrations/third_party/partner/silverfort/core/base_action.py new file mode 100644 index 000000000..c57c3774c --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/base_action.py @@ -0,0 +1,131 @@ +"""Base action class for Silverfort integration.""" + +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING + +from TIPCommon.base.action import Action + +from .auth import ( + AuthenticatedSession, + SessionAuthenticationParameters, + build_auth_params, + get_app_credentials, +) +from .constants import ApiType +from .policy_client import PolicyApiClient, PolicyApiParameters +from .risk_client import RiskApiClient, RiskApiParameters +from .service_account_client import ServiceAccountApiClient, ServiceAccountApiParameters + +if TYPE_CHECKING: + import requests + + from .data_models import IntegrationParameters + + +class SilverfortAction(Action, ABC): + """Base action class for Silverfort integration.""" + + def _init_api_clients(self) -> None: + """Initialize API clients placeholder. + + Note: This returns None as Silverfort requires different clients + for different APIs. Use specific initialization methods instead. + """ + return None + + def _get_integration_params(self) -> IntegrationParameters: + """Get integration parameters from SOAR configuration. + + Returns: + IntegrationParameters with configuration values. + """ + if not hasattr(self, "_integration_params"): + self._integration_params = build_auth_params(self.soar_action) + return self._integration_params + + def _get_authenticated_session( + self, + api_type: ApiType | None = None, + ) -> requests.Session: + """Get an authenticated session for API requests. + + Args: + api_type: Type of API to authenticate for. If None, only uses External API Key. + + Returns: + Authenticated requests Session. + """ + params = self._get_integration_params() + + app_credentials = None + if api_type: + app_credentials = get_app_credentials(params, api_type) + + session_params = SessionAuthenticationParameters( + api_root=params.api_root, + external_api_key=params.external_api_key, + verify_ssl=params.verify_ssl, + app_credentials=app_credentials, + api_type=api_type, + ) + + authenticator = AuthenticatedSession() + authenticator.authenticate_session(session_params) + return authenticator.session + + def _get_risk_client(self) -> RiskApiClient: + """Get a Risk API client. + + Returns: + Configured RiskApiClient instance. + """ + params = self._get_integration_params() + session = self._get_authenticated_session(ApiType.RISK) + + return RiskApiClient( + authenticated_session=session, + configuration=RiskApiParameters(api_root=params.api_root), + logger=self.logger, + ) + + def _get_service_account_client(self) -> ServiceAccountApiClient: + """Get a Service Account API client. + + Returns: + Configured ServiceAccountApiClient instance. + """ + params = self._get_integration_params() + session = self._get_authenticated_session(ApiType.SERVICE_ACCOUNTS) + + return ServiceAccountApiClient( + authenticated_session=session, + configuration=ServiceAccountApiParameters(api_root=params.api_root), + logger=self.logger, + ) + + def _get_policy_client(self) -> PolicyApiClient: + """Get a Policy API client. + + Returns: + Configured PolicyApiClient instance. + """ + params = self._get_integration_params() + session = self._get_authenticated_session(ApiType.POLICIES) + + return PolicyApiClient( + authenticated_session=session, + configuration=PolicyApiParameters(api_root=params.api_root), + logger=self.logger, + ) + + @property + def result_value(self) -> bool: + """Get the result value.""" + return self._result_value + + @result_value.setter + def result_value(self, value: bool) -> None: + """Set the result value.""" + self._result_value = value diff --git a/content/response_integrations/third_party/partner/silverfort/core/constants.py b/content/response_integrations/third_party/partner/silverfort/core/constants.py new file mode 100644 index 000000000..f8de2dbf4 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/constants.py @@ -0,0 +1,163 @@ +"""Constants for Silverfort integration.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, Mapping + +if TYPE_CHECKING: + pass + +# Integration Identifiers +INTEGRATION_IDENTIFIER: str = "Silverfort" +INTEGRATION_DISPLAY_NAME: str = "Silverfort Identity Security" + +# Script Identifiers +PING_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Ping" +GET_ENTITY_RISK_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Get Entity Risk" +UPDATE_ENTITY_RISK_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Update Entity Risk" +GET_SERVICE_ACCOUNT_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Get Service Account" +LIST_SERVICE_ACCOUNTS_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - List Service Accounts" +UPDATE_SA_POLICY_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Update SA Policy" +GET_POLICY_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Get Policy" +UPDATE_POLICY_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Update Policy" +CHANGE_POLICY_STATE_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - Change Policy State" +LIST_POLICIES_SCRIPT_NAME: str = f"{INTEGRATION_IDENTIFIER} - List Policies" + +# Default Configuration Parameter Values +DEFAULT_VERIFY_SSL: bool = True +DEFAULT_PAGE_SIZE: int = 50 +DEFAULT_PAGE_NUMBER: int = 1 + +# API Constants +API_KEY_HEADER: str = "X-Console-API-Key" +AUTHORIZATION_HEADER: str = "Authorization" +CONTENT_TYPE_HEADER: str = "Content-Type" +APPLICATION_JSON: str = "application/json" + +# JWT Configuration +JWT_ALGORITHM: str = "HS256" +JWT_EXPIRATION_SECONDS: int = 3600 # 1 hour + +# API Endpoints +ENDPOINTS: Mapping[str, str] = { + # Risk API + "get_entity_risk": "/v1/public/getEntityRisk", + "update_entity_risk": "/v1/public/updateEntityRisk", + # Service Accounts API + "get_service_account": "/v1/public/serviceAccounts/{guid}", + "add_service_account": "/v1/public/serviceAccounts/{guid}", + "update_service_account": "/v1/public/serviceAccounts/update/{guid}", + "remove_service_account": "/v1/public/serviceAccounts/remove/{guid}", + "get_sa_policy": "/v1/public/serviceAccounts/policy/{guid}", + "update_sa_policy": "/v1/public/serviceAccounts/policy/{guid}", + "list_service_accounts": "/v1/public/serviceAccounts/index", + "get_sa_guids": "/v1/public/serviceAccounts/guids", + # Policies API v1 + "change_policy_state": "/v1/public/changePolicyState", + "get_rules_name_and_status": "/v1/public/getRulesNameAndStatus", + "get_rules_names_and_ids": "/v1/public/getRulesNamesAndIds", + # Policies API v2 + "get_policy": "/v2/public/policies/{policy_id}", + "update_policy": "/v2/public/policies/{policy_id}", + "list_policies": "/v2/public/policies/index", +} + +# Timeouts +REQUEST_TIMEOUT: int = 30 + + +class ApiType(str, Enum): + """Enum for Silverfort API types.""" + + RISK = "risk" + SERVICE_ACCOUNTS = "service_accounts" + POLICIES = "policies" + + +class RiskSeverity(str, Enum): + """Enum for risk severity levels.""" + + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +class RiskType(str, Enum): + """Enum for risk types that can be updated.""" + + ACTIVITY_RISK = "activity_risk" + MALWARE_RISK = "malware_risk" + DATA_BREACH_RISK = "data_breach_risk" + CUSTOM_RISK = "custom_risk" + + +class ServiceAccountCategory(str, Enum): + """Enum for service account categories.""" + + MACHINE_TO_MACHINE = "machine_to_machine" + INTERACTIVE = "interactive" + UNKNOWN = "unknown" + + +class SAPolicyRiskLevel(str, Enum): + """Enum for service account policy risk levels.""" + + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + + +class SAProtocol(str, Enum): + """Enum for service account protocols.""" + + KERBEROS = "Kerberos" + LDAP = "ldap" + NTLM = "ntlm" + + +# Service Account Index Fields +SA_INDEX_FIELDS: list[str] = [ + "guid", + "display_name", + "sources_count", + "destinations_count", + "number_of_authentications", + "risk", + "predictability", + "protected", + "upn", + "dn", + "spn", + "comment", + "owner", + "type", + "domain", + "category", + "creation_date", + "highly_privileged", + "interactive_login", + "broadly_used", + "suspected_brute_force", + "repetitive_behavior", +] + +# Policy Index Fields +POLICY_INDEX_FIELDS: list[str] = [ + "enabled", + "policyName", + "authType", + "protocols", + "policyType", + "allUsersAndGroups", + "usersAndGroups", + "allDevices", + "sources", + "allDestinations", + "destinations", + "action", + "MFAPrompt", + "all", + "bridgeType", +] diff --git a/content/response_integrations/third_party/partner/silverfort/core/data_models.py b/content/response_integrations/third_party/partner/silverfort/core/data_models.py new file mode 100644 index 000000000..2f1562941 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/data_models.py @@ -0,0 +1,399 @@ +"""Data models for Silverfort integration.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from typing import TYPE_CHECKING, NamedTuple + +if TYPE_CHECKING: + from TIPCommon.types import SingleJson + + +class IntegrationParameters(NamedTuple): + """Integration configuration parameters.""" + + api_root: str + external_api_key: str + verify_ssl: bool + risk_app_user_id: str | None = None + risk_app_user_secret: str | None = None + service_accounts_app_user_id: str | None = None + service_accounts_app_user_secret: str | None = None + policies_app_user_id: str | None = None + policies_app_user_secret: str | None = None + + +class AppCredentials(NamedTuple): + """App User credentials for a specific API type.""" + + app_user_id: str + app_user_secret: str + + +@dataclass(frozen=True, slots=True) +class EntityRisk: + """Risk information for an entity (user or resource).""" + + user_principal_name: str | None = None + resource_name: str | None = None + risk_score: float | None = None + severity: str | None = None + risk_factors: list[dict] = field(default_factory=list) + last_updated: str | None = None + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = {} + if self.user_principal_name: + result["user_principal_name"] = self.user_principal_name + if self.resource_name: + result["resource_name"] = self.resource_name + if self.risk_score is not None: + result["risk_score"] = self.risk_score + if self.severity: + result["severity"] = self.severity + if self.risk_factors: + result["risk_factors"] = self.risk_factors + if self.last_updated: + result["last_updated"] = self.last_updated + return result + + @classmethod + def from_json(cls, data: SingleJson) -> EntityRisk: + """Create from JSON response.""" + return cls( + user_principal_name=data.get("user_principal_name"), + resource_name=data.get("resource_name"), + risk_score=data.get("risk_score"), + severity=data.get("severity"), + risk_factors=data.get("risk_factors", []), + last_updated=data.get("last_updated"), + ) + + +@dataclass(frozen=True, slots=True) +class RiskUpdate: + """Risk update information.""" + + severity: str + valid_for: int # Hours + description: str + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + return { + "severity": self.severity, + "valid_for": self.valid_for, + "description": self.description, + } + + +@dataclass(frozen=True, slots=True) +class ServiceAccount: + """Service account information.""" + + guid: str + display_name: str | None = None + upn: str | None = None + dn: str | None = None + spn: str | None = None + domain: str | None = None + category: str | None = None + risk: str | None = None + predictability: str | None = None + protected: bool | None = None + owner: str | None = None + comment: str | None = None + sources_count: int | None = None + destinations_count: int | None = None + number_of_authentications: int | None = None + creation_date: str | None = None + highly_privileged: bool | None = None + interactive_login: bool | None = None + broadly_used: bool | None = None + suspected_brute_force: bool | None = None + repetitive_behavior: bool | None = None + account_type: str | None = None + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + return {k: v for k, v in asdict(self).items() if v is not None} + + @classmethod + def from_json(cls, data: SingleJson) -> ServiceAccount: + """Create from JSON response.""" + return cls( + guid=data.get("guid", ""), + display_name=data.get("display_name"), + upn=data.get("upn"), + dn=data.get("dn"), + spn=data.get("spn"), + domain=data.get("domain"), + category=data.get("category"), + risk=data.get("risk"), + predictability=data.get("predictability"), + protected=data.get("protected"), + owner=data.get("owner"), + comment=data.get("comment"), + sources_count=data.get("sources_count"), + destinations_count=data.get("destinations_count"), + number_of_authentications=data.get("number_of_authentications"), + creation_date=data.get("creation_date"), + highly_privileged=data.get("highly_privileged"), + interactive_login=data.get("interactive_login"), + broadly_used=data.get("broadly_used"), + suspected_brute_force=data.get("suspected_brute_force"), + repetitive_behavior=data.get("repetitive_behavior"), + account_type=data.get("type"), + ) + + +@dataclass(frozen=True, slots=True) +class AllowedEndpoint: + """Allowed source or destination endpoint.""" + + key: str + key_type: str # ip, hostname, dn, etc. + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + return {"key": self.key, "key_type": self.key_type} + + @classmethod + def from_json(cls, data: SingleJson) -> AllowedEndpoint: + """Create from JSON response.""" + return cls(key=data.get("key", ""), key_type=data.get("key_type", "")) + + +@dataclass(slots=True) +class ServiceAccountPolicy: + """Service account policy configuration.""" + + guid: str + enabled: bool = False + block: bool = False + send_to_siem: bool = False + risk_level: str | None = None + allow_all_sources: bool = False + allow_all_destinations: bool = False + protocols: list[str] = field(default_factory=list) + allowed_sources: list[AllowedEndpoint] = field(default_factory=list) + allowed_destinations: list[AllowedEndpoint] = field(default_factory=list) + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = { + "enabled": self.enabled, + "block": self.block, + "send_to_siem": self.send_to_siem, + "allow_all_sources": self.allow_all_sources, + "allow_all_destinations": self.allow_all_destinations, + } + if self.risk_level: + result["risk_level"] = self.risk_level + if self.protocols: + result["protocols"] = self.protocols + return result + + @classmethod + def from_json(cls, data: SingleJson, guid: str = "") -> ServiceAccountPolicy: + """Create from JSON response.""" + allowed_sources = [ + AllowedEndpoint.from_json(src) for src in data.get("allowed_sources", []) + ] + allowed_destinations = [ + AllowedEndpoint.from_json(dst) for dst in data.get("allowed_destinations", []) + ] + return cls( + guid=guid, + enabled=data.get("enabled", False), + block=data.get("block", False), + send_to_siem=data.get("send_to_siem", False), + risk_level=data.get("risk_level"), + allow_all_sources=data.get("allow_all_sources", False), + allow_all_destinations=data.get("allow_all_destinations", False), + protocols=data.get("protocols", []), + allowed_sources=allowed_sources, + allowed_destinations=allowed_destinations, + ) + + +@dataclass(frozen=True, slots=True) +class PolicyIdentifier: + """Policy user or group identifier.""" + + identifier_type: str + identifier: str + display_name: str | None = None + domain: str | None = None + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = { + "identifierType": self.identifier_type, + "identifier": self.identifier, + } + if self.display_name: + result["displayName"] = self.display_name + if self.domain: + result["domain"] = self.domain + return result + + @classmethod + def from_json(cls, data: SingleJson) -> PolicyIdentifier: + """Create from JSON response.""" + return cls( + identifier_type=data.get("identifierType", ""), + identifier=data.get("identifier", ""), + display_name=data.get("displayName"), + domain=data.get("domain"), + ) + + +@dataclass(frozen=True, slots=True) +class PolicyDestination: + """Policy destination configuration.""" + + identifier_type: str + identifier: str + display_name: str | None = None + domain: str | None = None + services: list[str] = field(default_factory=list) + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = { + "identifierType": self.identifier_type, + "identifier": self.identifier, + } + if self.display_name: + result["displayName"] = self.display_name + if self.domain: + result["domain"] = self.domain + if self.services: + result["services"] = self.services + return result + + @classmethod + def from_json(cls, data: SingleJson) -> PolicyDestination: + """Create from JSON response.""" + return cls( + identifier_type=data.get("identifierType", ""), + identifier=data.get("identifier", ""), + display_name=data.get("displayName"), + domain=data.get("domain"), + services=data.get("services", []), + ) + + +@dataclass(slots=True) +class Policy: + """Authentication policy configuration.""" + + policy_id: str + policy_name: str | None = None + enabled: bool = False + policy_type: str | None = None + auth_type: str | None = None + protocols: list[str] = field(default_factory=list) + action: str | None = None + mfa_prompt: str | None = None + all_users_and_groups: bool = False + users_and_groups: list[PolicyIdentifier] = field(default_factory=list) + all_devices: bool = False + sources: list[PolicyIdentifier] = field(default_factory=list) + all_destinations: bool = False + destinations: list[PolicyDestination] = field(default_factory=list) + bridge_type: str | None = None + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = { + "policyId": self.policy_id, + "enabled": self.enabled, + } + if self.policy_name: + result["policyName"] = self.policy_name + if self.policy_type: + result["policyType"] = self.policy_type + if self.auth_type: + result["authType"] = self.auth_type + if self.protocols: + result["protocols"] = self.protocols + if self.action: + result["action"] = self.action + if self.mfa_prompt: + result["MFAPrompt"] = self.mfa_prompt + result["allUsersAndGroups"] = self.all_users_and_groups + if self.users_and_groups: + result["usersAndGroups"] = [ug.to_json() for ug in self.users_and_groups] + result["allDevices"] = self.all_devices + if self.sources: + result["sources"] = [src.to_json() for src in self.sources] + result["allDestinations"] = self.all_destinations + if self.destinations: + result["destinations"] = [dst.to_json() for dst in self.destinations] + if self.bridge_type: + result["bridgeType"] = self.bridge_type + return result + + @classmethod + def from_json(cls, data: SingleJson) -> Policy: + """Create from JSON response.""" + users_and_groups = [PolicyIdentifier.from_json(ug) for ug in data.get("usersAndGroups", [])] + sources = [PolicyIdentifier.from_json(src) for src in data.get("sources", [])] + destinations = [PolicyDestination.from_json(dst) for dst in data.get("destinations", [])] + + return cls( + policy_id=str(data.get("policyId", data.get("id", ""))), + policy_name=data.get("policyName"), + enabled=data.get("enabled", False), + policy_type=data.get("policyType"), + auth_type=data.get("authType"), + protocols=data.get("protocols", []), + action=data.get("action"), + mfa_prompt=data.get("MFAPrompt"), + all_users_and_groups=data.get("allUsersAndGroups", False), + users_and_groups=users_and_groups, + all_devices=data.get("allDevices", False), + sources=sources, + all_destinations=data.get("allDestinations", False), + destinations=destinations, + bridge_type=data.get("bridgeType"), + ) + + +@dataclass(frozen=True, slots=True) +class ServiceAccountsListResult: + """Result of listing service accounts.""" + + service_accounts: list[ServiceAccount] + total_count: int | None = None + page_number: int | None = None + page_size: int | None = None + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + result: SingleJson = { + "service_accounts": [sa.to_json() for sa in self.service_accounts], + } + if self.total_count is not None: + result["total_count"] = self.total_count + if self.page_number is not None: + result["page_number"] = self.page_number + if self.page_size is not None: + result["page_size"] = self.page_size + return result + + +@dataclass(frozen=True, slots=True) +class PoliciesListResult: + """Result of listing policies.""" + + policies: list[Policy] + + def to_json(self) -> SingleJson: + """Convert to JSON representation.""" + return { + "policies": [policy.to_json() for policy in self.policies], + } diff --git a/content/response_integrations/third_party/partner/silverfort/core/exceptions.py b/content/response_integrations/third_party/partner/silverfort/core/exceptions.py new file mode 100644 index 000000000..d85695926 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/exceptions.py @@ -0,0 +1,78 @@ +"""Custom exceptions for Silverfort integration.""" + +from __future__ import annotations + + +class SilverfortError(Exception): + """Base exception for Silverfort integration.""" + + +class SilverfortConfigurationError(SilverfortError): + """Exception raised when integration configuration is invalid or missing.""" + + +class SilverfortAuthenticationError(SilverfortError): + """Exception raised when authentication fails.""" + + +class SilverfortHTTPError(SilverfortError): + """Exception raised for HTTP errors.""" + + def __init__(self, message: str, *args, status_code: int | None = None) -> None: + """Initialize the exception. + + Args: + message: Error message. + *args: Additional positional arguments. + status_code: HTTP status code. + """ + super().__init__(message, *args) + self.status_code = status_code + + +class SilverfortAPIError(SilverfortError): + """Exception raised for API-specific errors.""" + + def __init__( + self, + message: str, + *args, + error_code: str | None = None, + details: dict | None = None, + ) -> None: + """Initialize the exception. + + Args: + message: Error message. + *args: Additional positional arguments. + error_code: Silverfort-specific error code. + details: Additional error details. + """ + super().__init__(message, *args) + self.error_code = error_code + self.details = details or {} + + +class SilverfortInvalidParameterError(SilverfortError): + """Exception raised when action parameters are invalid.""" + + +class SilverfortEntityNotFoundError(SilverfortError): + """Exception raised when a requested entity is not found.""" + + +class SilverfortCredentialsNotConfiguredError(SilverfortConfigurationError): + """Exception raised when required API credentials are not configured.""" + + def __init__(self, api_type: str) -> None: + """Initialize the exception. + + Args: + api_type: Type of API whose credentials are missing. + """ + self.api_type = api_type + super().__init__( + f"{api_type} API credentials are not configured. " + f"Please configure the {api_type} App User ID and {api_type} App User Secret " + "in the integration settings." + ) diff --git a/content/response_integrations/third_party/partner/silverfort/core/policy_client.py b/content/response_integrations/third_party/partner/silverfort/core/policy_client.py new file mode 100644 index 000000000..4e70ec3ff --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/policy_client.py @@ -0,0 +1,213 @@ +"""Policy API client for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple + +from TIPCommon.base.interfaces import Apiable + +from .api_utils import get_full_url, validate_response +from .constants import POLICY_INDEX_FIELDS, REQUEST_TIMEOUT +from .data_models import ( + PoliciesListResult, + Policy, + PolicyDestination, + PolicyIdentifier, +) + +if TYPE_CHECKING: + from requests import Response, Session + from TIPCommon.base.interfaces.logger import ScriptLogger + from TIPCommon.types import SingleJson + + +class PolicyApiParameters(NamedTuple): + """Parameters for Policy API client.""" + + api_root: str + + +class PolicyApiClient(Apiable): + """Client for Silverfort Policy API.""" + + def __init__( + self, + authenticated_session: Session, + configuration: PolicyApiParameters, + logger: ScriptLogger, + ) -> None: + """Initialize the Policy API client. + + Args: + authenticated_session: Authenticated requests session. + configuration: API configuration parameters. + logger: Logger instance. + """ + super().__init__( + authenticated_session=authenticated_session, + configuration=configuration, + ) + self.logger: ScriptLogger = logger + self.api_root: str = configuration.api_root + + def test_connectivity(self) -> bool: + """Test connectivity to the Policy API. + + Returns: + True if connectivity test succeeds. + """ + # Try to get rules names and IDs - lightweight operation + url: str = get_full_url(self.api_root, "get_rules_names_and_ids") + response: Response = self.session.get(url, timeout=REQUEST_TIMEOUT) + validate_response(response, "Policy API connectivity test failed") + return True + + def get_policy(self, policy_id: str) -> Policy: + """Get policy details by ID. + + Args: + policy_id: The ID of the policy. + + Returns: + Policy object with policy configuration. + """ + url: str = get_full_url(self.api_root, "get_policy", policy_id=policy_id) + + self.logger.info(f"Getting policy: {policy_id}") + response: Response = self.session.get(url, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to get policy: {policy_id}") + + data: SingleJson = response.json() + self.logger.info(f"Successfully retrieved policy: {policy_id}") + + return Policy.from_json(data) + + def list_policies( + self, + fields: list[str] | None = None, + ) -> PoliciesListResult: + """List all policies. + + Args: + fields: List of fields to include in response. If None, includes all fields. + + Returns: + PoliciesListResult with list of policies. + """ + url: str = get_full_url(self.api_root, "list_policies") + + # Use all fields if not specified + if fields is None: + fields = POLICY_INDEX_FIELDS + + payload: SingleJson = {"fields": fields} + + self.logger.info("Listing policies") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, "Failed to list policies") + + data: SingleJson = response.json() + self.logger.info("Successfully retrieved policies list") + + # Parse the response - may be a list or dict with policies key + if isinstance(data, list): + policies_data = data + else: + policies_data = data.get("policies", data.get("data", [])) + + policies = [Policy.from_json(p) for p in policies_data] + + return PoliciesListResult(policies=policies) + + def update_policy( + self, + policy_id: str, + enabled: bool | None = None, + add_users_and_groups: list[PolicyIdentifier] | None = None, + remove_users_and_groups: list[PolicyIdentifier] | None = None, + add_sources: list[PolicyIdentifier] | None = None, + remove_sources: list[PolicyIdentifier] | None = None, + add_destinations: list[PolicyDestination] | None = None, + remove_destinations: list[PolicyDestination] | None = None, + ) -> bool: + """Update a policy. + + Args: + policy_id: The ID of the policy to update. + enabled: Enable or disable the policy. + add_users_and_groups: Users/groups to add to the policy. + remove_users_and_groups: Users/groups to remove from the policy. + add_sources: Sources to add to the policy. + remove_sources: Sources to remove from the policy. + add_destinations: Destinations to add to the policy. + remove_destinations: Destinations to remove from the policy. + + Returns: + True if the policy was successfully updated. + """ + url: str = get_full_url(self.api_root, "update_policy", policy_id=policy_id) + + payload: SingleJson = {} + + if enabled is not None: + payload["enabled"] = enabled + if add_users_and_groups: + payload["addUsersAndGroups"] = [ug.to_json() for ug in add_users_and_groups] + if remove_users_and_groups: + payload["removeUsersAndGroups"] = [ug.to_json() for ug in remove_users_and_groups] + if add_sources: + payload["addSources"] = [src.to_json() for src in add_sources] + if remove_sources: + payload["removeSources"] = [src.to_json() for src in remove_sources] + if add_destinations: + payload["addDestinations"] = [dst.to_json() for dst in add_destinations] + if remove_destinations: + payload["removeDestinations"] = [dst.to_json() for dst in remove_destinations] + + self.logger.info(f"Updating policy: {policy_id}") + response: Response = self.session.patch(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to update policy: {policy_id}") + + self.logger.info(f"Successfully updated policy: {policy_id}") + return True + + def change_policy_state(self, policy_id: str, state: bool) -> bool: + """Change the state (enabled/disabled) of a policy. + + Args: + policy_id: The ID of the policy. + state: True to enable, False to disable. + + Returns: + True if the state was successfully changed. + """ + url: str = get_full_url(self.api_root, "change_policy_state") + + payload: SingleJson = { + "policy_id": str(policy_id), + "state": str(state).lower(), + } + + self.logger.info(f"Changing policy state: {policy_id} -> {state}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to change policy state: {policy_id}") + + self.logger.info(f"Successfully changed policy state: {policy_id} -> {state}") + return True + + def get_rules_names_and_ids(self) -> list[dict]: + """Get all policy rules names and IDs. + + Returns: + List of dictionaries with policy names and IDs. + """ + url: str = get_full_url(self.api_root, "get_rules_names_and_ids") + + self.logger.info("Getting policy rules names and IDs") + response: Response = self.session.get(url, timeout=REQUEST_TIMEOUT) + validate_response(response, "Failed to get policy rules names and IDs") + + data = response.json() + self.logger.info("Successfully retrieved policy rules names and IDs") + + return data if isinstance(data, list) else [] diff --git a/content/response_integrations/third_party/partner/silverfort/core/risk_client.py b/content/response_integrations/third_party/partner/silverfort/core/risk_client.py new file mode 100644 index 000000000..2cf3009e5 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/risk_client.py @@ -0,0 +1,143 @@ +"""Risk API client for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple + +from TIPCommon.base.interfaces import Apiable + +from .api_utils import get_full_url, validate_response +from .constants import REQUEST_TIMEOUT +from .data_models import EntityRisk, RiskUpdate + +if TYPE_CHECKING: + from requests import Response, Session + from TIPCommon.base.interfaces.logger import ScriptLogger + from TIPCommon.types import SingleJson + + +class RiskApiParameters(NamedTuple): + """Parameters for Risk API client.""" + + api_root: str + + +class RiskApiClient(Apiable): + """Client for Silverfort Risk API.""" + + def __init__( + self, + authenticated_session: Session, + configuration: RiskApiParameters, + logger: ScriptLogger, + ) -> None: + """Initialize the Risk API client. + + Args: + authenticated_session: Authenticated requests session. + configuration: API configuration parameters. + logger: Logger instance. + """ + super().__init__( + authenticated_session=authenticated_session, + configuration=configuration, + ) + self.logger: ScriptLogger = logger + self.api_root: str = configuration.api_root + + def test_connectivity(self) -> bool: + """Test connectivity to the Risk API. + + Returns: + True if connectivity test succeeds. + """ + # Try to get risk for a test entity - this will fail if auth is incorrect + # We don't care about the result, just that the API responds + url: str = get_full_url(self.api_root, "get_entity_risk") + params: dict[str, str] = {"user_principal_name": "test@test.com"} + response: Response = self.session.get(url, params=params, timeout=REQUEST_TIMEOUT) + # Accept both success (200) and not found (404/400) as valid connectivity + if response.status_code in (200, 400, 404): + return True + validate_response(response, "Risk API connectivity test failed") + return True + + def get_entity_risk( + self, + user_principal_name: str | None = None, + resource_name: str | None = None, + ) -> EntityRisk: + """Get risk information for a user or resource. + + Args: + user_principal_name: The user principal name (e.g., user@domain.com). + resource_name: The resource name (for non-user entities). + + Returns: + EntityRisk object with risk information. + + Raises: + ValueError: If neither user_principal_name nor resource_name is provided. + """ + if not user_principal_name and not resource_name: + raise ValueError("Either user_principal_name or resource_name must be provided") + + url: str = get_full_url(self.api_root, "get_entity_risk") + params: dict[str, str] = {} + + if user_principal_name: + params["user_principal_name"] = user_principal_name + if resource_name: + params["resource_name"] = resource_name + + self.logger.info(f"Getting entity risk for: {params}") + response: Response = self.session.get(url, params=params, timeout=REQUEST_TIMEOUT) + validate_response(response, "Failed to get entity risk") + + data: SingleJson = response.json() + self.logger.info(f"Successfully retrieved entity risk: {data}") + + return EntityRisk.from_json(data) + + def update_entity_risk( + self, + user_principal_name: str, + risks: dict[str, RiskUpdate], + ) -> bool: + """Update risk information for a user entity. + + Args: + user_principal_name: The user principal name to update. + risks: Dictionary mapping risk types to RiskUpdate objects. + + Returns: + True if update was successful. + + Example: + risks = { + "activity_risk": RiskUpdate( + severity="medium", + valid_for=24, + description="Suspicious activity detected" + ), + "malware_risk": RiskUpdate( + severity="high", + valid_for=48, + description="Malware indicator found" + ) + } + client.update_entity_risk("user@domain.com", risks) + """ + url: str = get_full_url(self.api_root, "update_entity_risk") + + payload: SingleJson = { + "user_principal_name": user_principal_name, + "risks": {risk_type: risk.to_json() for risk_type, risk in risks.items()}, + } + + self.logger.info(f"Updating entity risk for: {user_principal_name}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, "Failed to update entity risk") + + self.logger.info(f"Successfully updated entity risk for: {user_principal_name}") + return True diff --git a/content/response_integrations/third_party/partner/silverfort/core/service_account_client.py b/content/response_integrations/third_party/partner/silverfort/core/service_account_client.py new file mode 100644 index 000000000..83423159b --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/core/service_account_client.py @@ -0,0 +1,285 @@ +"""Service Account API client for Silverfort integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple + +from TIPCommon.base.interfaces import Apiable + +from .api_utils import get_full_url, validate_response +from .constants import DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE, REQUEST_TIMEOUT, SA_INDEX_FIELDS +from .data_models import ( + AllowedEndpoint, + ServiceAccount, + ServiceAccountPolicy, + ServiceAccountsListResult, +) + +if TYPE_CHECKING: + from requests import Response, Session + from TIPCommon.base.interfaces.logger import ScriptLogger + from TIPCommon.types import SingleJson + + +class ServiceAccountApiParameters(NamedTuple): + """Parameters for Service Account API client.""" + + api_root: str + + +class ServiceAccountApiClient(Apiable): + """Client for Silverfort Service Accounts API.""" + + def __init__( + self, + authenticated_session: Session, + configuration: ServiceAccountApiParameters, + logger: ScriptLogger, + ) -> None: + """Initialize the Service Account API client. + + Args: + authenticated_session: Authenticated requests session. + configuration: API configuration parameters. + logger: Logger instance. + """ + super().__init__( + authenticated_session=authenticated_session, + configuration=configuration, + ) + self.logger: ScriptLogger = logger + self.api_root: str = configuration.api_root + + def test_connectivity(self) -> bool: + """Test connectivity to the Service Accounts API. + + Returns: + True if connectivity test succeeds. + """ + # Try to list service accounts with minimal data + url: str = get_full_url(self.api_root, "list_service_accounts") + payload: SingleJson = { + "page_size": 1, + "page_number": 1, + "fields": ["guid"], + } + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, "Service Accounts API connectivity test failed") + return True + + def get_service_account(self, guid: str) -> ServiceAccount: + """Get service account details by GUID. + + Args: + guid: The GUID of the service account. + + Returns: + ServiceAccount object with account details. + """ + url: str = get_full_url(self.api_root, "get_service_account", guid=guid) + + self.logger.info(f"Getting service account: {guid}") + response: Response = self.session.get(url, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to get service account: {guid}") + + data: SingleJson = response.json() + self.logger.info(f"Successfully retrieved service account: {guid}") + + return ServiceAccount.from_json(data) + + def list_service_accounts( + self, + page_size: int = DEFAULT_PAGE_SIZE, + page_number: int = DEFAULT_PAGE_NUMBER, + fields: list[str] | None = None, + ) -> ServiceAccountsListResult: + """List service accounts with pagination. + + Args: + page_size: Number of results per page (default: 50). + page_number: Page number to retrieve (default: 1). + fields: List of fields to include in response. If None, includes all fields. + + Returns: + ServiceAccountsListResult with list of service accounts. + """ + url: str = get_full_url(self.api_root, "list_service_accounts") + + # Use all fields if not specified + if fields is None: + fields = SA_INDEX_FIELDS + + payload: SingleJson = { + "page_size": page_size, + "page_number": page_number, + "fields": fields, + } + + self.logger.info(f"Listing service accounts: page={page_number}, size={page_size}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, "Failed to list service accounts") + + data: SingleJson = response.json() + self.logger.info("Successfully retrieved service accounts list") + + # Parse the response + accounts_data = data.get("service_accounts", data.get("data", [])) + service_accounts = [ServiceAccount.from_json(sa) for sa in accounts_data] + + return ServiceAccountsListResult( + service_accounts=service_accounts, + total_count=data.get("total_count"), + page_number=page_number, + page_size=page_size, + ) + + def add_service_account( + self, + guid: str, + category: str = "machine_to_machine", + ) -> bool: + """Add a service account to protection. + + Args: + guid: The GUID of the service account. + category: The category (machine_to_machine, interactive, unknown). + + Returns: + True if the account was successfully added. + """ + url: str = get_full_url(self.api_root, "add_service_account", guid=guid) + payload: SingleJson = {"category": category} + + self.logger.info(f"Adding service account to protection: {guid}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to add service account: {guid}") + + self.logger.info(f"Successfully added service account: {guid}") + return True + + def get_service_account_policy(self, guid: str) -> ServiceAccountPolicy: + """Get the policy for a service account. + + Args: + guid: The GUID of the service account. + + Returns: + ServiceAccountPolicy object with policy configuration. + """ + url: str = get_full_url(self.api_root, "get_sa_policy", guid=guid) + + self.logger.info(f"Getting service account policy: {guid}") + response: Response = self.session.get(url, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to get service account policy: {guid}") + + data: SingleJson = response.json() + self.logger.info(f"Successfully retrieved service account policy: {guid}") + + return ServiceAccountPolicy.from_json(data, guid=guid) + + def update_service_account_policy( + self, + guid: str, + enabled: bool | None = None, + block: bool | None = None, + send_to_siem: bool | None = None, + risk_level: str | None = None, + allow_all_sources: bool | None = None, + allow_all_destinations: bool | None = None, + protocols: list[str] | None = None, + add_allowed_sources: list[AllowedEndpoint] | None = None, + remove_allowed_sources: list[AllowedEndpoint] | None = None, + add_allowed_destinations: list[AllowedEndpoint] | None = None, + remove_allowed_destinations: list[AllowedEndpoint] | None = None, + ) -> bool: + """Update the policy for a service account. + + Args: + guid: The GUID of the service account. + enabled: Enable or disable the policy. + block: Enable or disable blocking. + send_to_siem: Enable or disable SIEM logging. + risk_level: Risk level threshold (low, medium, high). + allow_all_sources: Allow all sources. + allow_all_destinations: Allow all destinations. + protocols: List of protocols (Kerberos, ldap, ntlm). + add_allowed_sources: Sources to add to allowlist. + remove_allowed_sources: Sources to remove from allowlist. + add_allowed_destinations: Destinations to add to allowlist. + remove_allowed_destinations: Destinations to remove from allowlist. + + Returns: + True if the policy was successfully updated. + """ + url: str = get_full_url(self.api_root, "update_sa_policy", guid=guid) + + payload: SingleJson = {} + + if enabled is not None: + payload["enabled"] = enabled + if block is not None: + payload["block"] = block + if send_to_siem is not None: + payload["send_to_siem"] = send_to_siem + if risk_level is not None: + payload["risk_level"] = risk_level + if allow_all_sources is not None: + payload["allow_all_sources"] = allow_all_sources + if allow_all_destinations is not None: + payload["allow_all_destinations"] = allow_all_destinations + if protocols is not None: + payload["protocols"] = protocols + if add_allowed_sources: + payload["add_allowed_sources"] = [src.to_json() for src in add_allowed_sources] + if remove_allowed_sources: + payload["remove_allowed_sources"] = [src.to_json() for src in remove_allowed_sources] + if add_allowed_destinations: + payload["add_allowed_destinations"] = [ + dst.to_json() for dst in add_allowed_destinations + ] + if remove_allowed_destinations: + payload["remove_allowed_destinations"] = [ + dst.to_json() for dst in remove_allowed_destinations + ] + + self.logger.info(f"Updating service account policy: {guid}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to update service account policy: {guid}") + + self.logger.info(f"Successfully updated service account policy: {guid}") + return True + + def update_service_account( + self, + guid: str, + category: str | None = None, + owner: str | None = None, + comment: str | None = None, + ) -> bool: + """Update service account attributes. + + Args: + guid: The GUID of the service account. + category: The category (machine_to_machine, interactive, unknown). + owner: The owner GUID. + comment: Comment for the service account. + + Returns: + True if the account was successfully updated. + """ + url: str = get_full_url(self.api_root, "update_service_account", guid=guid) + + payload: SingleJson = {} + if category is not None: + payload["category"] = category + if owner is not None: + payload["owner"] = owner + if comment is not None: + payload["comment"] = comment + + self.logger.info(f"Updating service account: {guid}") + response: Response = self.session.post(url, json=payload, timeout=REQUEST_TIMEOUT) + validate_response(response, f"Failed to update service account: {guid}") + + self.logger.info(f"Successfully updated service account: {guid}") + return True diff --git a/content/response_integrations/third_party/partner/silverfort/definition.yaml b/content/response_integrations/third_party/partner/silverfort/definition.yaml new file mode 100644 index 000000000..5e5e5037e --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/definition.yaml @@ -0,0 +1,61 @@ +identifier: Silverfort +name: Silverfort Identity Security +parameters: + - name: API Root + default_value: https://your-silverfort-instance.com + type: string + description: Base URL for Silverfort API (e.g., https://your-instance.silverfort.io) + is_mandatory: true + integration_identifier: Silverfort + - name: External API Key + default_value: "" + type: password + description: External API Key from Silverfort Admin Console (X-Console-API-Key header) + is_mandatory: true + integration_identifier: Silverfort + - name: Risk App User ID + default_value: "" + type: string + description: App User ID for Risk API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Risk App User Secret + default_value: "" + type: password + description: App User Secret for Risk API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Service Accounts App User ID + default_value: "" + type: string + description: App User ID for Service Accounts API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Service Accounts App User Secret + default_value: "" + type: password + description: App User Secret for Service Accounts API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Policies App User ID + default_value: "" + type: string + description: App User ID for Policies API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Policies App User Secret + default_value: "" + type: password + description: App User Secret for Policies API credentials + is_mandatory: false + integration_identifier: Silverfort + - name: Verify SSL + default_value: true + type: boolean + description: If selected, the integration validates the SSL certificate when connecting to Silverfort API + is_mandatory: false + integration_identifier: Silverfort +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/Silverfort +categories: ["Identity Security", "Access Management"] +svg_logo_path: resources/logo.svg +image_path: resources/image.png diff --git a/content/response_integrations/third_party/partner/silverfort/pyproject.toml b/content/response_integrations/third_party/partner/silverfort/pyproject.toml new file mode 100644 index 000000000..76fe430a9 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "Silverfort" +version = "1.0" +description = "Silverfort identity security platform integration for Google SecOps.\n\nIn case of any queries, please reach out to support@silverfort.com." +requires-python = ">=3.11,<3.12" +dependencies = [ + "environmentcommon", + "pyjwt>=2.8.0", + "requests>=2.32.4", + "tipcommon", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "soar-sdk", + "integration-testing", + "pytest-json-report>=1.5.0", +] + +[tool.pytest.ini_options] +pythonpath = "." + +[tool.uv.sources] +soar-sdk = { git = "https://github.com/chronicle/soar-sdk" } +tipcommon = { path = "../../../../../packages/tipcommon/TIPCommon-2.2.10/TIPCommon-2.2.10-py2.py3-none-any.whl" } +environmentcommon = { path = "../../../../../packages/envcommon/EnvironmentCommon-1.0.2/EnvironmentCommon-1.0.2-py2.py3-none-any.whl" } +integration-testing = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.10-py3-none-any.whl" } + +[[tool.uv.index]] +url = "https://pypi.org/simple" +default = true diff --git a/content/response_integrations/third_party/partner/silverfort/release_notes.yaml b/content/response_integrations/third_party/partner/silverfort/release_notes.yaml new file mode 100644 index 000000000..5f83a5582 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/release_notes.yaml @@ -0,0 +1,85 @@ +- description: New Integration Added - Silverfort Identity Security. Provides user + risk management, service account protection, and authentication policy management + capabilities. + integration_version: 1.0 + item_name: Silverfort + item_type: Integration + publish_time: '2026-01-13' + new: true + +- description: Added Ping action to test connectivity to Silverfort API with all configured + API endpoints. + integration_version: 1.0 + item_name: Ping + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Get Entity Risk action to retrieve risk information for users + and resources. + integration_version: 1.0 + item_name: Get Entity Risk + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Update Entity Risk action to set risk levels for users based + on external threat intelligence. + integration_version: 1.0 + item_name: Update Entity Risk + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Get Service Account action to retrieve detailed information about + service accounts. + integration_version: 1.0 + item_name: Get Service Account + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added List Service Accounts action to list all service accounts with + pagination support. + integration_version: 1.0 + item_name: List Service Accounts + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Update SA Policy action to configure protection policies for + service accounts. + integration_version: 1.0 + item_name: Update SA Policy + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Get Policy action to retrieve authentication policy configurations. + integration_version: 1.0 + item_name: Get Policy + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added List Policies action to list all authentication policies. + integration_version: 1.0 + item_name: List Policies + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Update Policy action to modify authentication policy settings. + integration_version: 1.0 + item_name: Update Policy + item_type: Action + publish_time: '2026-01-13' + new: true + +- description: Added Change Policy State action to enable or disable authentication + policies. + integration_version: 1.0 + item_name: Change Policy State + item_type: Action + publish_time: '2026-01-13' + new: true diff --git a/content/response_integrations/third_party/partner/silverfort/resources/change_policy_state_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/change_policy_state_JsonResult_example.json new file mode 100644 index 000000000..df123686b --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/change_policy_state_JsonResult_example.json @@ -0,0 +1,5 @@ +{ + "policy_id": "42", + "enabled": true, + "status": "enabled" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/get_entity_risk_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/get_entity_risk_JsonResult_example.json new file mode 100644 index 000000000..8e1160d48 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/get_entity_risk_JsonResult_example.json @@ -0,0 +1,18 @@ +{ + "user_principal_name": "user@example.com", + "risk_score": 65.0, + "severity": "medium", + "risk_factors": [ + { + "type": "activity_risk", + "severity": "medium", + "description": "Suspicious login activity detected" + }, + { + "type": "malware_risk", + "severity": "low", + "description": "Potential malware indicator" + } + ], + "last_updated": "2026-01-13T10:30:00Z" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/get_policy_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/get_policy_JsonResult_example.json new file mode 100644 index 000000000..e12b98d30 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/get_policy_JsonResult_example.json @@ -0,0 +1,39 @@ +{ + "policyId": "42", + "policyName": "Critical Server MFA", + "enabled": true, + "policyType": "authentication", + "authType": "mfa", + "protocols": ["Kerberos", "NTLM"], + "action": "mfa", + "MFAPrompt": "always", + "allUsersAndGroups": false, + "usersAndGroups": [ + { + "identifierType": "group", + "identifier": "CN=IT-Admins,CN=Groups,DC=example,DC=com", + "displayName": "IT Admins", + "domain": "example.com" + } + ], + "allDevices": false, + "sources": [], + "allDestinations": false, + "destinations": [ + { + "identifierType": "hostname", + "identifier": "dc01.example.com", + "displayName": "Domain Controller 1", + "domain": "example.com", + "services": ["ldap", "kerberos"] + }, + { + "identifierType": "hostname", + "identifier": "dc02.example.com", + "displayName": "Domain Controller 2", + "domain": "example.com", + "services": ["ldap", "kerberos"] + } + ], + "bridgeType": "ad" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/get_service_account_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/get_service_account_JsonResult_example.json new file mode 100644 index 000000000..1dc77b5c3 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/get_service_account_JsonResult_example.json @@ -0,0 +1,23 @@ +{ + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "display_name": "svc_backup", + "upn": "svc_backup@ad.example.com", + "dn": "CN=svc_backup,CN=Service Accounts,DC=ad,DC=example,DC=com", + "domain": "ad.example.com", + "category": "machine_to_machine", + "risk": "low", + "predictability": "high", + "protected": true, + "owner": "626be9e4-7557-3749-b8bc-e03420c3ed78", + "comment": "Backup service account for nightly jobs", + "sources_count": 5, + "destinations_count": 10, + "number_of_authentications": 15420, + "creation_date": "2024-06-15T08:00:00Z", + "highly_privileged": false, + "interactive_login": false, + "broadly_used": false, + "suspected_brute_force": false, + "repetitive_behavior": true, + "account_type": "user" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/image.png b/content/response_integrations/third_party/partner/silverfort/resources/image.png new file mode 100644 index 000000000..b11403cc2 Binary files /dev/null and b/content/response_integrations/third_party/partner/silverfort/resources/image.png differ diff --git a/content/response_integrations/third_party/partner/silverfort/resources/list_policies_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/list_policies_JsonResult_example.json new file mode 100644 index 000000000..d9382c1b2 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/list_policies_JsonResult_example.json @@ -0,0 +1,25 @@ +{ + "policies": [ + { + "policyId": "1", + "policyName": "Block Compromised Users", + "enabled": true, + "policyType": "block", + "action": "block" + }, + { + "policyId": "42", + "policyName": "Critical Server MFA", + "enabled": true, + "policyType": "authentication", + "action": "mfa" + }, + { + "policyId": "100", + "policyName": "VPN Access Policy", + "enabled": false, + "policyType": "authentication", + "action": "mfa" + } + ] +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/list_service_accounts_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/list_service_accounts_JsonResult_example.json new file mode 100644 index 000000000..9f07fa8ed --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/list_service_accounts_JsonResult_example.json @@ -0,0 +1,28 @@ +{ + "service_accounts": [ + { + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "display_name": "svc_backup", + "risk": "low", + "predictability": "high", + "protected": true + }, + { + "guid": "4e9f7309-5a48-f542-8f2b-334f72cf2c89", + "display_name": "svc_monitoring", + "risk": "medium", + "predictability": "medium", + "protected": true + }, + { + "guid": "aa123456-1234-5678-9abc-def012345678", + "display_name": "svc_deploy", + "risk": "high", + "predictability": "low", + "protected": false + } + ], + "total_count": 3, + "page_number": 1, + "page_size": 50 +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/logo.svg b/content/response_integrations/third_party/partner/silverfort/resources/logo.svg new file mode 100644 index 000000000..ca0770d6e --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/response_integrations/third_party/partner/silverfort/resources/update_entity_risk_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/update_entity_risk_JsonResult_example.json new file mode 100644 index 000000000..a16896e35 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/update_entity_risk_JsonResult_example.json @@ -0,0 +1,8 @@ +{ + "user_principal_name": "user@example.com", + "risk_type": "activity_risk", + "severity": "high", + "valid_for": 24, + "description": "Suspicious activity detected by SIEM", + "status": "updated" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/update_policy_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/update_policy_JsonResult_example.json new file mode 100644 index 000000000..21d865555 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/update_policy_JsonResult_example.json @@ -0,0 +1,4 @@ +{ + "policy_id": "42", + "status": "updated" +} diff --git a/content/response_integrations/third_party/partner/silverfort/resources/update_sa_policy_JsonResult_example.json b/content/response_integrations/third_party/partner/silverfort/resources/update_sa_policy_JsonResult_example.json new file mode 100644 index 000000000..fd979f908 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/resources/update_sa_policy_JsonResult_example.json @@ -0,0 +1,4 @@ +{ + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "status": "updated" +} diff --git a/content/response_integrations/third_party/partner/silverfort/tests/__init__.py b/content/response_integrations/third_party/partner/silverfort/tests/__init__.py new file mode 100644 index 000000000..f681be9d8 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/__init__.py @@ -0,0 +1 @@ +"""Tests package for Silverfort integration.""" diff --git a/content/response_integrations/third_party/partner/silverfort/tests/common.py b/content/response_integrations/third_party/partner/silverfort/tests/common.py new file mode 100644 index 000000000..ec8f72a9f --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/common.py @@ -0,0 +1,135 @@ +"""Common utilities and constants for Silverfort tests.""" + +from __future__ import annotations + +import json +import pathlib +from typing import TYPE_CHECKING + +from integration_testing.common import get_def_file_content + +if TYPE_CHECKING: + from TIPCommon.types import SingleJson + + +INTEGRATION_PATH: pathlib.Path = pathlib.Path(__file__).parent.parent +CONFIG_PATH = pathlib.Path.joinpath(INTEGRATION_PATH, "tests", "config.json") +CONFIG: SingleJson = get_def_file_content(CONFIG_PATH) +MOCKS_PATH = pathlib.Path.joinpath(INTEGRATION_PATH, "tests", "mocks") + +# Load mock responses +MOCK_RESPONSES_FILE = pathlib.Path.joinpath(MOCKS_PATH, "responses.json") + + +def load_mock_responses() -> SingleJson: + """Load mock responses from JSON file.""" + if MOCK_RESPONSES_FILE.exists(): + return json.loads(MOCK_RESPONSES_FILE.read_text(encoding="utf-8")) + return {} + + +MOCK_DATA: SingleJson = load_mock_responses() if MOCK_RESPONSES_FILE.exists() else {} + +# Default mock responses +MOCK_ENTITY_RISK: SingleJson = { + "user_principal_name": "test.user@example.com", + "risk_score": 65.0, + "severity": "medium", + "risk_factors": [ + {"type": "activity_risk", "severity": "medium", "description": "Suspicious activity"}, + ], + "last_updated": "2026-01-13T10:00:00Z", +} + +MOCK_SERVICE_ACCOUNT: SingleJson = { + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "display_name": "svc_test", + "upn": "svctest@ad.example.com", + "dn": "CN=svc_test,CN=Users,DC=ad,DC=example,DC=com", + "domain": "ad.example.com", + "category": "machine_to_machine", + "risk": "low", + "predictability": "high", + "protected": True, + "sources_count": 5, + "destinations_count": 3, + "number_of_authentications": 1500, +} + +MOCK_SERVICE_ACCOUNTS_LIST: SingleJson = { + "service_accounts": [ + { + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "display_name": "svc_test", + "risk": "low", + }, + { + "guid": "4e9f7309-5a48-f542-8f2b-334f72cf2c89", + "display_name": "svc_app", + "risk": "medium", + }, + ], + "total_count": 2, +} + +MOCK_SA_POLICY: SingleJson = { + "enabled": True, + "block": False, + "send_to_siem": True, + "risk_level": "medium", + "allow_all_sources": False, + "allow_all_destinations": False, + "protocols": ["Kerberos", "ldap"], + "allowed_sources": [{"key": "10.0.0.1", "key_type": "ip"}], + "allowed_destinations": [{"key": "server.example.com", "key_type": "hostname"}], +} + +MOCK_POLICY: SingleJson = { + "policyId": "1", + "policyName": "Test Policy", + "enabled": True, + "policyType": "authentication", + "authType": "mfa", + "protocols": ["Kerberos", "NTLM"], + "action": "mfa", + "MFAPrompt": "always", + "allUsersAndGroups": False, + "usersAndGroups": [ + { + "identifierType": "upn", + "identifier": "user@example.com", + "displayName": "Test User", + "domain": "example.com", + } + ], + "allDevices": False, + "sources": [], + "allDestinations": False, + "destinations": [ + { + "identifierType": "hostname", + "identifier": "server.example.com", + "displayName": "Test Server", + "domain": "example.com", + "services": ["rdp"], + } + ], +} + +MOCK_POLICIES_LIST: SingleJson = [ + { + "policyId": "1", + "policyName": "Test Policy 1", + "enabled": True, + }, + { + "policyId": "2", + "policyName": "Test Policy 2", + "enabled": False, + }, +] + +MOCK_RULES_NAMES_IDS: list[dict] = [ + {"id": "1", "name": "Test Policy 1"}, + {"id": "2", "name": "Test Policy 2"}, +] diff --git a/content/response_integrations/third_party/partner/silverfort/tests/config.json b/content/response_integrations/third_party/partner/silverfort/tests/config.json new file mode 100644 index 000000000..145b0f278 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/config.json @@ -0,0 +1,11 @@ +{ + "API Root": "https://mock-silverfort.local", + "External API Key": "test-external-api-key", + "Risk App User ID": "test-risk-app-user", + "Risk App User Secret": "test-risk-app-secret", + "Service Accounts App User ID": "test-sa-app-user", + "Service Accounts App User Secret": "test-sa-app-secret", + "Policies App User ID": "test-policies-app-user", + "Policies App User Secret": "test-policies-app-secret", + "Verify SSL": true +} diff --git a/content/response_integrations/third_party/partner/silverfort/tests/conftest.py b/content/response_integrations/third_party/partner/silverfort/tests/conftest.py new file mode 100644 index 000000000..7241f4e5b --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/conftest.py @@ -0,0 +1,48 @@ +"""Pytest configuration and fixtures for Silverfort integration tests.""" + +from __future__ import annotations + +import pytest +from integration_testing.common import use_live_api +from soar_sdk.SiemplifyBase import SiemplifyBase +from TIPCommon.base.utils import CreateSession + +from silverfort.tests.core.product import MockSilverfort +from silverfort.tests.core.session import SilverfortSession + +pytest_plugins = ("integration_testing.conftest",) + + +@pytest.fixture +def silverfort() -> MockSilverfort: + """Create a mock Silverfort instance.""" + return MockSilverfort() + + +@pytest.fixture(autouse=True) +def script_session( + monkeypatch: pytest.MonkeyPatch, + silverfort: MockSilverfort, +) -> SilverfortSession: + """Mock Silverfort scripts' session and get back an object to view request history.""" + session: SilverfortSession = SilverfortSession(silverfort) + + if not use_live_api(): + monkeypatch.setattr(CreateSession, "create_session", lambda: session) + monkeypatch.setattr("requests.Session", lambda: session) + + return session + + +@pytest.fixture(autouse=True) +def sdk_session( + monkeypatch: pytest.MonkeyPatch, + silverfort: MockSilverfort, +) -> SilverfortSession: + """Mock the SDK sessions and get it back to view request and response history.""" + session: SilverfortSession = SilverfortSession(silverfort) + + if not use_live_api(): + monkeypatch.setattr(SiemplifyBase, "create_session", lambda *_: session) + + return session diff --git a/content/response_integrations/third_party/partner/silverfort/tests/core/__init__.py b/content/response_integrations/third_party/partner/silverfort/tests/core/__init__.py new file mode 100644 index 000000000..b6211c8f6 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/core/__init__.py @@ -0,0 +1,6 @@ +"""Test core module for Silverfort integration tests.""" + +from .product import MockSilverfort +from .session import SilverfortSession + +__all__ = ["MockSilverfort", "SilverfortSession"] diff --git a/content/response_integrations/third_party/partner/silverfort/tests/core/product.py b/content/response_integrations/third_party/partner/silverfort/tests/core/product.py new file mode 100644 index 000000000..905ea9c12 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/core/product.py @@ -0,0 +1,148 @@ +"""Mock Silverfort product for testing.""" + +from __future__ import annotations + +import dataclasses +from typing import TYPE_CHECKING + +from silverfort.tests.common import ( + MOCK_ENTITY_RISK, + MOCK_POLICIES_LIST, + MOCK_POLICY, + MOCK_RULES_NAMES_IDS, + MOCK_SA_POLICY, + MOCK_SERVICE_ACCOUNT, + MOCK_SERVICE_ACCOUNTS_LIST, +) + +if TYPE_CHECKING: + from TIPCommon.types import SingleJson + + +@dataclasses.dataclass(slots=True) +class MockSilverfort: + """Mock Silverfort API for testing.""" + + # Risk API data + entity_risks: dict[str, SingleJson] = dataclasses.field(default_factory=dict) + + # Service Account API data + service_accounts: dict[str, SingleJson] = dataclasses.field(default_factory=dict) + sa_policies: dict[str, SingleJson] = dataclasses.field(default_factory=dict) + + # Policy API data + policies: dict[str, SingleJson] = dataclasses.field(default_factory=dict) + rules_names_ids: list[dict] = dataclasses.field(default_factory=list) + + def __post_init__(self) -> None: + """Initialize with default mock data.""" + # Set default entity risk + self.entity_risks["test.user@example.com"] = MOCK_ENTITY_RISK + + # Set default service accounts + self.service_accounts["82132169-b41b-8b47-ba4b-494814500785"] = MOCK_SERVICE_ACCOUNT + + # Set default SA policies + self.sa_policies["82132169-b41b-8b47-ba4b-494814500785"] = MOCK_SA_POLICY + + # Set default policies + self.policies["1"] = MOCK_POLICY + + # Set default rules names/ids + self.rules_names_ids = MOCK_RULES_NAMES_IDS + + # Risk API methods + def get_entity_risk( + self, + user_principal_name: str | None = None, + resource_name: str | None = None, + ) -> SingleJson: + """Get entity risk.""" + identifier = user_principal_name or resource_name + if identifier and identifier in self.entity_risks: + return self.entity_risks[identifier] + # Return empty risk for unknown entities + return { + "user_principal_name": user_principal_name, + "resource_name": resource_name, + "risk_score": 0, + "severity": "low", + "risk_factors": [], + } + + def update_entity_risk( + self, + user_principal_name: str, + risks: SingleJson, + ) -> SingleJson: + """Update entity risk.""" + if user_principal_name not in self.entity_risks: + self.entity_risks[user_principal_name] = { + "user_principal_name": user_principal_name, + "risk_factors": [], + } + # Update risk factors + for risk_type, risk_data in risks.items(): + factor = { + "type": risk_type, + "severity": risk_data.get("severity"), + "description": risk_data.get("description"), + } + self.entity_risks[user_principal_name]["risk_factors"].append(factor) + return {"status": "success"} + + # Service Account API methods + def get_service_account(self, guid: str) -> SingleJson: + """Get service account.""" + if guid in self.service_accounts: + return self.service_accounts[guid] + raise ValueError(f"Service account not found: {guid}") + + def list_service_accounts( + self, + page_size: int = 50, + page_number: int = 1, + fields: list[str] | None = None, + ) -> SingleJson: + """List service accounts.""" + return MOCK_SERVICE_ACCOUNTS_LIST + + def get_sa_policy(self, guid: str) -> SingleJson: + """Get service account policy.""" + if guid in self.sa_policies: + return self.sa_policies[guid] + return MOCK_SA_POLICY + + def update_sa_policy(self, guid: str, policy_data: SingleJson) -> SingleJson: + """Update service account policy.""" + if guid not in self.sa_policies: + self.sa_policies[guid] = {} + self.sa_policies[guid].update(policy_data) + return {"status": "success"} + + # Policy API methods + def get_policy(self, policy_id: str) -> SingleJson: + """Get policy.""" + if policy_id in self.policies: + return self.policies[policy_id] + raise ValueError(f"Policy not found: {policy_id}") + + def list_policies(self, fields: list[str] | None = None) -> list[SingleJson]: + """List policies.""" + return MOCK_POLICIES_LIST + + def update_policy(self, policy_id: str, policy_data: SingleJson) -> SingleJson: + """Update policy.""" + if policy_id in self.policies: + self.policies[policy_id].update(policy_data) + return {"status": "success"} + + def change_policy_state(self, policy_id: str, state: bool) -> SingleJson: + """Change policy state.""" + if policy_id in self.policies: + self.policies[policy_id]["enabled"] = state + return {"status": "success"} + + def get_rules_names_and_ids(self) -> list[dict]: + """Get rules names and IDs.""" + return self.rules_names_ids diff --git a/content/response_integrations/third_party/partner/silverfort/tests/core/session.py b/content/response_integrations/third_party/partner/silverfort/tests/core/session.py new file mode 100644 index 000000000..77299fd06 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/core/session.py @@ -0,0 +1,174 @@ +"""Mock session for Silverfort integration tests.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable + +from integration_testing import router +from integration_testing.common import get_request_payload +from integration_testing.request import MockRequest +from integration_testing.requests.response import MockResponse +from integration_testing.requests.session import MockSession, Response, RouteFunction + +from silverfort.tests.core.product import MockSilverfort + +if TYPE_CHECKING: + from TIPCommon.types import SingleJson + + +class SilverfortSession(MockSession[MockRequest, MockResponse, MockSilverfort]): + """Mock session for Silverfort API requests.""" + + def get_routed_functions(self) -> Iterable[RouteFunction[Response]]: + """Get all routed functions for this session.""" + return [ + # Risk API + self.get_entity_risk, + self.update_entity_risk, + # Service Account API + self.get_service_account, + self.list_service_accounts, + self.get_sa_policy, + self.update_sa_policy, + # Policy API + self.get_policy, + self.list_policies, + self.update_policy, + self.change_policy_state, + self.get_rules_names_and_ids, + ] + + # Risk API routes + @router.get(r"/v1/public/getEntityRisk") + def get_entity_risk(self, request: MockRequest) -> MockResponse: + """Handle get entity risk requests.""" + try: + params: SingleJson = get_request_payload(request) + user_principal_name = params.get("user_principal_name") + resource_name = params.get("resource_name") + + result = self._product.get_entity_risk(user_principal_name, resource_name) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.post(r"/v1/public/updateEntityRisk") + def update_entity_risk(self, request: MockRequest) -> MockResponse: + """Handle update entity risk requests.""" + try: + payload: SingleJson = get_request_payload(request) + user_principal_name = payload.get("user_principal_name", "") + risks = payload.get("risks", {}) + + result = self._product.update_entity_risk(user_principal_name, risks) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + # Service Account API routes + @router.get(r"/v1/public/serviceAccounts/[a-f0-9-]+$") + def get_service_account(self, request: MockRequest) -> MockResponse: + """Handle get service account requests.""" + try: + guid = request.url.path.split("/")[-1] + result = self._product.get_service_account(guid) + return MockResponse(content=result, status_code=200) + except ValueError as e: + return MockResponse(content={"error": str(e)}, status_code=404) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.post(r"/v1/public/serviceAccounts/index") + def list_service_accounts(self, request: MockRequest) -> MockResponse: + """Handle list service accounts requests.""" + try: + payload: SingleJson = get_request_payload(request) + page_size = payload.get("page_size", 50) + page_number = payload.get("page_number", 1) + fields = payload.get("fields") + + result = self._product.list_service_accounts(page_size, page_number, fields) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.get(r"/v1/public/serviceAccounts/policy/[a-f0-9-]+") + def get_sa_policy(self, request: MockRequest) -> MockResponse: + """Handle get service account policy requests.""" + try: + guid = request.url.path.split("/")[-1] + result = self._product.get_sa_policy(guid) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.post(r"/v1/public/serviceAccounts/policy/[a-f0-9-]+") + def update_sa_policy(self, request: MockRequest) -> MockResponse: + """Handle update service account policy requests.""" + try: + guid = request.url.path.split("/")[-1] + payload: SingleJson = get_request_payload(request) + + result = self._product.update_sa_policy(guid, payload) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + # Policy API routes + @router.get(r"/v2/public/policies/\d+") + def get_policy(self, request: MockRequest) -> MockResponse: + """Handle get policy requests.""" + try: + policy_id = request.url.path.split("/")[-1] + result = self._product.get_policy(policy_id) + return MockResponse(content=result, status_code=200) + except ValueError as e: + return MockResponse(content={"error": str(e)}, status_code=404) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.post(r"/v2/public/policies/index") + def list_policies(self, request: MockRequest) -> MockResponse: + """Handle list policies requests.""" + try: + payload: SingleJson = get_request_payload(request) + fields = payload.get("fields") + + result = self._product.list_policies(fields) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.patch(r"/v2/public/policies/\d+") + def update_policy(self, request: MockRequest) -> MockResponse: + """Handle update policy requests.""" + try: + policy_id = request.url.path.split("/")[-1] + payload: SingleJson = get_request_payload(request) + + result = self._product.update_policy(policy_id, payload) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.post(r"/v1/public/changePolicyState") + def change_policy_state(self, request: MockRequest) -> MockResponse: + """Handle change policy state requests.""" + try: + payload: SingleJson = get_request_payload(request) + policy_id = payload.get("policy_id", "") + state = payload.get("state", "").lower() == "true" + + result = self._product.change_policy_state(policy_id, state) + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) + + @router.get(r"/v1/public/getRulesNamesAndIds") + def get_rules_names_and_ids(self, request: MockRequest) -> MockResponse: + """Handle get rules names and IDs requests.""" + try: + result = self._product.get_rules_names_and_ids() + return MockResponse(content=result, status_code=200) + except Exception as e: + return MockResponse(content={"error": str(e)}, status_code=400) diff --git a/content/response_integrations/third_party/partner/silverfort/tests/mocks/responses.json b/content/response_integrations/third_party/partner/silverfort/tests/mocks/responses.json new file mode 100644 index 000000000..73a8328c9 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/mocks/responses.json @@ -0,0 +1,75 @@ +{ + "entity_risk": { + "user_principal_name": "test.user@example.com", + "risk_score": 65.0, + "severity": "medium", + "risk_factors": [ + { + "type": "activity_risk", + "severity": "medium", + "description": "Suspicious activity" + } + ], + "last_updated": "2026-01-13T10:00:00Z" + }, + "service_account": { + "guid": "82132169-b41b-8b47-ba4b-494814500785", + "display_name": "svc_test", + "upn": "svctest@ad.example.com", + "dn": "CN=svc_test,CN=Users,DC=ad,DC=example,DC=com", + "domain": "ad.example.com", + "category": "machine_to_machine", + "risk": "low", + "predictability": "high", + "protected": true, + "sources_count": 5, + "destinations_count": 3, + "number_of_authentications": 1500 + }, + "sa_policy": { + "enabled": true, + "block": false, + "send_to_siem": true, + "risk_level": "medium", + "allow_all_sources": false, + "allow_all_destinations": false, + "protocols": ["Kerberos", "ldap"], + "allowed_sources": [ + {"key": "10.0.0.1", "key_type": "ip"} + ], + "allowed_destinations": [ + {"key": "server.example.com", "key_type": "hostname"} + ] + }, + "policy": { + "policyId": "1", + "policyName": "Test Policy", + "enabled": true, + "policyType": "authentication", + "authType": "mfa", + "protocols": ["Kerberos", "NTLM"], + "action": "mfa", + "MFAPrompt": "always", + "allUsersAndGroups": false, + "usersAndGroups": [ + { + "identifierType": "upn", + "identifier": "user@example.com", + "displayName": "Test User", + "domain": "example.com" + } + ], + "allDevices": false, + "sources": [], + "allDestinations": false, + "destinations": [ + { + "identifierType": "hostname", + "identifier": "server.example.com", + "displayName": "Test Server", + "domain": "example.com", + "services": ["rdp"] + } + ] + } +} diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_actions/__init__.py b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/__init__.py new file mode 100644 index 000000000..39e1e027f --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/__init__.py @@ -0,0 +1 @@ +"""Test actions package for Silverfort integration.""" diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_ping.py b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_ping.py new file mode 100644 index 000000000..1a4dd3820 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_ping.py @@ -0,0 +1,60 @@ +"""Tests for Ping action.""" + +from __future__ import annotations + +from integration_testing.platform.script_output import MockActionOutput +from integration_testing.set_meta import set_metadata +from TIPCommon.base.action import ExecutionState + +from silverfort.actions import ping +from silverfort.tests.common import CONFIG_PATH +from silverfort.tests.core.product import MockSilverfort +from silverfort.tests.core.session import SilverfortSession + + +class TestPing: + """Tests for the Ping action.""" + + @set_metadata(integration_config_file_path=CONFIG_PATH) + def test_ping_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful ping with all APIs configured.""" + ping.main() + + # Verify requests were made + assert len(script_session.request_history) >= 1 + + # Verify successful output + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully connected" in action_output.results.output_message + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + integration_config={ + "API Root": "https://mock-silverfort.local", + "External API Key": "test-external-api-key", + "Risk App User ID": "", + "Risk App User Secret": "", + "Service Accounts App User ID": "", + "Service Accounts App User Secret": "", + "Policies App User ID": "", + "Policies App User Secret": "", + "Verify SSL": True, + }, + ) + def test_ping_no_credentials( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test ping fails when no API credentials configured.""" + ping.main() + + # Verify failure + assert action_output.results.execution_state == ExecutionState.FAILED + assert "No API credentials configured" in action_output.results.output_message diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_policy_actions.py b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_policy_actions.py new file mode 100644 index 000000000..cf5d6c3ec --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_policy_actions.py @@ -0,0 +1,122 @@ +"""Tests for Policy actions.""" + +from __future__ import annotations + +from integration_testing.platform.script_output import MockActionOutput +from integration_testing.set_meta import set_metadata +from TIPCommon.base.action import ExecutionState + +from silverfort.actions import change_policy_state, get_policy, list_policies, update_policy +from silverfort.tests.common import CONFIG_PATH +from silverfort.tests.core.product import MockSilverfort +from silverfort.tests.core.session import SilverfortSession + + +class TestGetPolicy: + """Tests for the Get Policy action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Policy ID": "1"}, + ) + def test_get_policy_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful get policy.""" + get_policy.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "policies" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully retrieved policy" in action_output.results.output_message + + +class TestListPolicies: + """Tests for the List Policies action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={}, + ) + def test_list_policies_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful list policies.""" + list_policies.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "policies/index" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully retrieved" in action_output.results.output_message + + +class TestUpdatePolicy: + """Tests for the Update Policy action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Policy ID": "1", "Enabled": "true"}, + ) + def test_update_policy_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful update policy.""" + update_policy.main() + + assert len(script_session.request_history) >= 1 + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully updated policy" in action_output.results.output_message + + +class TestChangePolicyState: + """Tests for the Change Policy State action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Policy ID": "1", "Enable Policy": "true"}, + ) + def test_change_policy_state_enable( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful enable policy.""" + change_policy_state.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "changePolicyState" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "enabled" in action_output.results.output_message + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Policy ID": "1", "Enable Policy": "false"}, + ) + def test_change_policy_state_disable( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful disable policy.""" + change_policy_state.main() + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "disabled" in action_output.results.output_message diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_risk_actions.py b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_risk_actions.py new file mode 100644 index 000000000..d5a73bca6 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_risk_actions.py @@ -0,0 +1,104 @@ +"""Tests for Risk actions.""" + +from __future__ import annotations + +from integration_testing.platform.script_output import MockActionOutput +from integration_testing.set_meta import set_metadata +from TIPCommon.base.action import ExecutionState + +from silverfort.actions import get_entity_risk, update_entity_risk +from silverfort.tests.common import CONFIG_PATH +from silverfort.tests.core.product import MockSilverfort +from silverfort.tests.core.session import SilverfortSession + + +class TestGetEntityRisk: + """Tests for the Get Entity Risk action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"User Principal Name": "test.user@example.com"}, + ) + def test_get_entity_risk_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful get entity risk.""" + get_entity_risk.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "getEntityRisk" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully retrieved risk" in action_output.results.output_message + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"User Principal Name": "", "Resource Name": ""}, + ) + def test_get_entity_risk_no_identifier( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test get entity risk fails without identifier.""" + get_entity_risk.main() + + assert action_output.results.execution_state == ExecutionState.FAILED + assert "must be provided" in action_output.results.output_message + + +class TestUpdateEntityRisk: + """Tests for the Update Entity Risk action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={ + "User Principal Name": "test.user@example.com", + "Risk Type": "activity_risk", + "Severity": "high", + "Valid For Hours": "24", + "Description": "Test risk update", + }, + ) + def test_update_entity_risk_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful update entity risk.""" + update_entity_risk.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "updateEntityRisk" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully updated risk" in action_output.results.output_message + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={ + "User Principal Name": "test.user@example.com", + "Risk Type": "invalid_type", + "Severity": "high", + "Valid For Hours": "24", + "Description": "Test risk update", + }, + ) + def test_update_entity_risk_invalid_type( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test update entity risk fails with invalid risk type.""" + update_entity_risk.main() + + assert action_output.results.execution_state == ExecutionState.FAILED + assert "Invalid risk type" in action_output.results.output_message diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_sa_actions.py b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_sa_actions.py new file mode 100644 index 000000000..0a125a1e8 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_actions/test_sa_actions.py @@ -0,0 +1,103 @@ +"""Tests for Service Account actions.""" + +from __future__ import annotations + +from integration_testing.platform.script_output import MockActionOutput +from integration_testing.set_meta import set_metadata +from TIPCommon.base.action import ExecutionState + +from silverfort.actions import get_service_account, list_service_accounts, update_sa_policy +from silverfort.tests.common import CONFIG_PATH +from silverfort.tests.core.product import MockSilverfort +from silverfort.tests.core.session import SilverfortSession + + +class TestGetServiceAccount: + """Tests for the Get Service Account action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Service Account GUID": "82132169-b41b-8b47-ba4b-494814500785"}, + ) + def test_get_service_account_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful get service account.""" + get_service_account.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "serviceAccounts" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully retrieved service account" in action_output.results.output_message + + +class TestListServiceAccounts: + """Tests for the List Service Accounts action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Page Size": "50", "Page Number": "1"}, + ) + def test_list_service_accounts_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful list service accounts.""" + list_service_accounts.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "serviceAccounts/index" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully retrieved" in action_output.results.output_message + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={"Page Size": "10", "Page Number": "1", "Fields": "guid,display_name,risk"}, + ) + def test_list_service_accounts_with_fields( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test list service accounts with specific fields.""" + list_service_accounts.main() + + assert action_output.results.execution_state == ExecutionState.COMPLETED + + +class TestUpdateSAPolicy: + """Tests for the Update SA Policy action.""" + + @set_metadata( + integration_config_file_path=CONFIG_PATH, + parameters={ + "Service Account GUID": "82132169-b41b-8b47-ba4b-494814500785", + "Enabled": "true", + "Block": "false", + }, + ) + def test_update_sa_policy_success( + self, + script_session: SilverfortSession, + action_output: MockActionOutput, + silverfort: MockSilverfort, + ) -> None: + """Test successful update SA policy.""" + update_sa_policy.main() + + assert len(script_session.request_history) >= 1 + request = script_session.request_history[0].request + assert "serviceAccounts/policy" in request.url.path + + assert action_output.results.execution_state == ExecutionState.COMPLETED + assert "Successfully updated" in action_output.results.output_message diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_defaults/__init__.py b/content/response_integrations/third_party/partner/silverfort/tests/test_defaults/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/content/response_integrations/third_party/partner/silverfort/tests/test_defaults/test_imports.py b/content/response_integrations/third_party/partner/silverfort/tests/test_defaults/test_imports.py new file mode 100644 index 000000000..910a495e5 --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/tests/test_defaults/test_imports.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from integration_testing.default_tests.import_test import import_all_integration_modules + +from .. import common + + +def test_imports() -> None: + import_all_integration_modules(common.INTEGRATION_PATH) diff --git a/content/response_integrations/third_party/partner/silverfort/uv.lock b/content/response_integrations/third_party/partner/silverfort/uv.lock new file mode 100644 index 000000000..f85bce60e --- /dev/null +++ b/content/response_integrations/third_party/partner/silverfort/uv.lock @@ -0,0 +1,871 @@ +version = 1 +revision = 3 +requires-python = "==3.11.*" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "environmentcommon" +version = "1.0.2" +source = { path = "../../../../../packages/envcommon/EnvironmentCommon-1.0.2/EnvironmentCommon-1.0.2-py2.py3-none-any.whl" } +wheels = [ + { filename = "environmentcommon-1.0.2-py2.py3-none-any.whl", hash = "sha256:a31d611ddf539fb081cfe9423a8d506d198fb5aefdb225c5534904ef9d80aaaa" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.187.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/83/60cdacf139d768dd7f0fcbe8d95b418299810068093fdf8228c6af89bb70/google_api_python_client-2.187.0.tar.gz", hash = "sha256:e98e8e8f49e1b5048c2f8276473d6485febc76c9c47892a8b4d1afa2c9ec8278", size = 14068154, upload-time = "2025-11-06T01:48:53.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/58/c1e716be1b055b504d80db2c8413f6c6a890a6ae218a65f178b63bc30356/google_api_python_client-2.187.0-py3-none-any.whl", hash = "sha256:d8d0f6d85d7d1d10bdab32e642312ed572bdc98919f72f831b44b9a9cebba32f", size = 14641434, upload-time = "2025-11-06T01:48:50.763Z" }, +] + +[[package]] +name = "google-auth" +version = "2.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "integration-testing" +version = "2.2.10" +source = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.10-py3-none-any.whl" } +dependencies = [ + { name = "aiohttp" }, + { name = "environmentcommon" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "soar-sdk" }, + { name = "tipcommon" }, + { name = "yarl" }, +] +wheels = [ + { filename = "integration_testing-2.2.10-py3-none-any.whl", hash = "sha256:98f0a8fb3ff1889d275b307e1dfafa7e3851393e9ae017d7d1db9e45b4912137" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.12.13" }, + { name = "environmentcommon" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "soar-sdk" }, + { name = "tipcommon" }, + { name = "yarl", specifier = ">=1.20.1" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158, upload-time = "2025-12-16T13:46:25.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205, upload-time = "2025-12-16T13:46:24.76Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, + { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-json-report" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/d3/765dae9712fcd68d820338908c1337e077d5fdadccd5cacf95b9b0bea278/pytest-json-report-1.5.0.tar.gz", hash = "sha256:2dde3c647851a19b5f3700729e8310a6e66efb2077d674f27ddea3d34dc615de", size = 21241, upload-time = "2022-03-15T21:03:10.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/35/d07400c715bf8a88aa0c1ee9c9eb6050ca7fe5b39981f0eea773feeb0681/pytest_json_report-1.5.0-py3-none-any.whl", hash = "sha256:9897b68c910b12a2e48dd849f9a284b2c79a732a8a9cb398452ddd23d3c8c325", size = 13222, upload-time = "2022-03-15T21:03:08.65Z" }, +] + +[[package]] +name = "pytest-metadata" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "silverfort" +version = "1.0" +source = { virtual = "." } +dependencies = [ + { name = "environmentcommon" }, + { name = "pyjwt" }, + { name = "requests" }, + { name = "tipcommon" }, +] + +[package.dev-dependencies] +dev = [ + { name = "integration-testing" }, + { name = "pytest" }, + { name = "pytest-json-report" }, + { name = "soar-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "environmentcommon", path = "../../../../../packages/envcommon/EnvironmentCommon-1.0.2/EnvironmentCommon-1.0.2-py2.py3-none-any.whl" }, + { name = "pyjwt", specifier = ">=2.8.0" }, + { name = "requests", specifier = ">=2.32.4" }, + { name = "tipcommon", path = "../../../../../packages/tipcommon/TIPCommon-2.2.10/TIPCommon-2.2.10-py2.py3-none-any.whl" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "integration-testing", path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.10-py3-none-any.whl" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-json-report", specifier = ">=1.5.0" }, + { name = "soar-sdk", git = "https://github.com/chronicle/soar-sdk" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soar-sdk" +version = "0.2.0" +source = { git = "https://github.com/chronicle/soar-sdk#5c563da488afa729eeba2195d3569de1370ab106" } +dependencies = [ + { name = "arrow" }, + { name = "chardet" }, + { name = "cryptography" }, + { name = "google-auth" }, + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "six" }, +] + +[[package]] +name = "tipcommon" +version = "2.2.10" +source = { path = "../../../../../packages/tipcommon/TIPCommon-2.2.10/TIPCommon-2.2.10-py2.py3-none-any.whl" } +dependencies = [ + { name = "google-api-python-client" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httpx" }, + { name = "pycryptodome" }, + { name = "requests" }, +] +wheels = [ + { filename = "tipcommon-2.2.10-py2.py3-none-any.whl", hash = "sha256:9bb9ba3654b26d6e9dae6344c80db5011443a1616a9efde0d610035f9c4f44fc" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-api-python-client" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httpx" }, + { name = "pycryptodome" }, + { name = "requests" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +]