diff --git a/content/response_integrations/google/lastline/.python-version b/content/response_integrations/google/lastline/.python-version new file mode 100644 index 000000000..902b2c90c --- /dev/null +++ b/content/response_integrations/google/lastline/.python-version @@ -0,0 +1 @@ +3.11 \ No newline at end of file diff --git a/content/response_integrations/google/lastline/actions/GetAnalysisResults.py b/content/response_integrations/google/lastline/actions/GetAnalysisResults.py new file mode 100644 index 000000000..0d1fd7dc4 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/GetAnalysisResults.py @@ -0,0 +1,273 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import validators +from TIPCommon import extract_configuration_param, extract_action_param, construct_csv + +from soar_sdk.ScriptResult import EXECUTION_STATE_COMPLETED, EXECUTION_STATE_FAILED +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyUtils import output_handler, convert_dict_to_json_result_dict +from soar_sdk.SiemplifyDataModel import EntityTypes +from ..core.LastlineManager import LastlineManager +from ..core.consts import ( + INTEGRATION_NAME, + GET_ANALYSIS_RESULTS, + FILE, + URL, + ANALYSIS_RESULTS, + THRESHOLD, + DEFAULT_X_LAST_SCANS_GET_RESULTS, +) +from ..core.exceptions import LastlineAuthenticationException, LastlineInvalidParamException +from ..core.utils import get_file_hash + + +@output_handler +def main(): + siemplify = SiemplifyAction() + siemplify.script_name = f"{INTEGRATION_NAME} - {GET_ANALYSIS_RESULTS}" + siemplify.LOGGER.info("================= Main - Param Init =================") + + api_root = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Api Root", + is_mandatory=True, + print_value=True, + ) + + username = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Username", + is_mandatory=True, + print_value=True, + ) + + password = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Password", + is_mandatory=True, + ) + + verify_ssl = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + input_type=bool, + default_value=True, + is_mandatory=False, + print_value=True, + ) + + create_insight = extract_action_param( + siemplify, + param_name="Create Insight?", + is_mandatory=False, + print_value=True, + input_type=bool, + ) + + siemplify.LOGGER.info("----------------- Main - Started -----------------") + + result_value = False + output_message = "" + try: + threshold = extract_action_param( + siemplify, + param_name="Threshold", + is_mandatory=True, + print_value=True, + input_type=int, + default_value=THRESHOLD, + ) + + search_in_last_x_scans = extract_action_param( + siemplify, + param_name="Search in last x scans", + is_mandatory=True, + print_value=True, + input_type=int, + default_value=DEFAULT_X_LAST_SCANS_GET_RESULTS, + ) + + manager = LastlineManager( + api_root=api_root, + username=username, + password=password, + verify_ssl=verify_ssl, + ) + + enriched_entities = [] + json_results = {} + for entity in siemplify.target_entities: + # Check if the entity is URL or file hash + + siemplify.LOGGER.info(f"Started processing entity {entity.identifier}") + try: + submission_type = URL if entity.entity_type == EntityTypes.URL else FILE + + url = None + if entity.entity_type == EntityTypes.URL: + url = entity.identifier + if not validators.url(url): + raise LastlineInvalidParamException("Invalid URL!") + + file_sha1 = None + file_md5 = None + if entity.entity_type == EntityTypes.FILEHASH: + file_sha1, file_md5 = get_file_hash(entity.identifier) + + if entity.entity_type not in (EntityTypes.URL, EntityTypes.FILEHASH): + output_message += ( + f"Entity type {entity.entity_type} is not supported by the action, only URL " + f"or Filehash are supported, skipping this entity type\n" + ) + continue + + siemplify.LOGGER.info(f"Fetching analysis of url: {entity.identifier}") + analysis = manager.search_analysis_history( + submission_type=submission_type, + search_in_last_x_scans=search_in_last_x_scans, + url=url, + file_md5=file_md5, + file_sha1=file_sha1, + ) + + if not analysis.data: + siemplify.LOGGER.info( + f"There is no successful analysis that have been made for: {entity.identifier}" + ) + continue + + siemplify.LOGGER.info( + f"Successfully fetched analysis of url: {entity.identifier}" + ) + + task_uuid = analysis.data[0].task_uuid + + siemplify.LOGGER.info(f"Fetching report of url: {entity.identifier}") + submission_task_report = manager.get_result( + uuid=task_uuid, is_get_process=False + ) + siemplify.LOGGER.info( + f"Successfully fetched report of url: {entity.identifier}" + ) + + # Insight + if create_insight: + siemplify.LOGGER.info( + f"Creating insight for entity: {entity.identifier}" + ) + entity_type = URL if submission_type == URL else "File" + + siemplify.add_entity_insight( + entity, + submission_task_report.as_insight( + entity.identifier, entity_type + ), + triggered_by=INTEGRATION_NAME, + ) + siemplify.LOGGER.info( + f"Created insight for entity: {entity.identifier}" + ) + + siemplify.LOGGER.info( + f"Creating JSON results for entity: {entity.identifier}" + ) + json_results[entity.identifier] = submission_task_report.as_json() + siemplify.LOGGER.info( + f"Created JSON results for entity: {entity.identifier}" + ) + + siemplify.LOGGER.info( + f"Creating CSV results for entity: {entity.identifier}" + ) + siemplify.result.add_data_table( + ANALYSIS_RESULTS.format(entity.identifier), + construct_csv([submission_task_report.as_table(submission_type)]), + ) + siemplify.LOGGER.info( + f"Created CSV results for entity: {entity.identifier}" + ) + + siemplify.LOGGER.info(f"Enriching entity: {entity.identifier}") + # Enrich entity + if submission_task_report.data.score > threshold: + siemplify.LOGGER.info("The entity will be mark as suspicious") + entity.is_suspicious = True + + enrichment_data = submission_task_report.as_table( + submission_type, is_enrichment=True + ) + entity.additional_properties.update(enrichment_data) + entity.is_enriched = True + enriched_entities.append(entity) + + siemplify.LOGGER.info(f"Enriched entity: {entity.identifier}") + + output_message += ( + f"Successfully fetched the analysis results for the {submission_type} " + f"{entity.identifier}\n" + ) + + except LastlineInvalidParamException as error: + siemplify.LOGGER.error(error) + output_message += ( + f"Failed to fetch the analysis results for the {submission_type} " + f"{entity.identifier}\n" + ) + + if enriched_entities: + result_value = True + siemplify.update_entities(enriched_entities) + siemplify.result.add_result_json( + convert_dict_to_json_result_dict(json_results) + ) + + else: + output_message += "No previously completed analysis tasks were found based on the provided entities\n" + + status = EXECUTION_STATE_COMPLETED + + except LastlineAuthenticationException as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to connect to the {INTEGRATION_NAME} service with the provided account. Please " + f"check your configuration. Error is: {error}\n" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except Exception as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Error executing action: {GET_ANALYSIS_RESULTS}. Reason: {error}." + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + siemplify.LOGGER.info("----------------- Main - Finished -----------------") + siemplify.LOGGER.info(f"Status: {status}:") + siemplify.LOGGER.info(f"Result Value: {result_value}") + siemplify.LOGGER.info(f"Output Message: {output_message}") + siemplify.end(output_message, result_value, status) + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/google/lastline/actions/GetAnalysisResults.yaml b/content/response_integrations/google/lastline/actions/GetAnalysisResults.yaml new file mode 100644 index 000000000..d5b5d1fa3 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/GetAnalysisResults.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Get Analysis Results +description: 'Enrich Siemplify FileHash or URL entities with the previously completed + analysis tasks results. Note: Action supports filehash entity in md-5 and sha-1 + formats. Note 2: Action always fetches the latest analysis available for the provided + entity in Lastline.' +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline#get_analysis_results +integration_identifier: Lastline +parameters: +- name: Threshold + default_value: 70 + type: string + description: Mark entity as suspicious if the score value for the entity is above + the specified threshold. + is_mandatory: true +- name: Search in last x scans + default_value: 25 + type: string + description: Search for report for provided entity in last x analyses executed + in Lastline. + is_mandatory: true +- name: Create Insight? + default_value: 'False' + type: boolean + description: Specify whether to create insight based on the report data. + is_mandatory: false +dynamic_results_metadata: +- result_example_path: resources/get_analysis_results_JsonResult_example.json + result_name: JsonResult + show_result: true +creator: Admin +simulation_data_json: '{"Entities": ["FILEHASH", "DestinationURL"]}' diff --git a/content/response_integrations/google/lastline/actions/Ping.py b/content/response_integrations/google/lastline/actions/Ping.py new file mode 100644 index 000000000..f46581e53 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/Ping.py @@ -0,0 +1,112 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +from TIPCommon import extract_configuration_param + +from soar_sdk.ScriptResult import EXECUTION_STATE_COMPLETED, EXECUTION_STATE_FAILED +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyUtils import output_handler +from ..core.LastlineManager import LastlineManager +from ..core.consts import INTEGRATION_NAME, PING +from ..core.exceptions import LastlineAuthenticationException + + +@output_handler +def main(): + siemplify = SiemplifyAction() + siemplify.script_name = f"{INTEGRATION_NAME} - {PING}" + siemplify.LOGGER.info("================= Main - Param Init =================") + + api_root = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Api Root", + is_mandatory=True, + print_value=True, + ) + + username = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Username", + is_mandatory=True, + print_value=True, + ) + + password = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Password", + is_mandatory=True, + ) + + verify_ssl = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + input_type=bool, + default_value=True, + is_mandatory=False, + print_value=True, + ) + + siemplify.LOGGER.info("----------------- Main - Started -----------------") + + try: + manager = LastlineManager( + api_root=api_root, + username=username, + password=password, + verify_ssl=verify_ssl, + ) + + siemplify.LOGGER.info(f"Connecting to {INTEGRATION_NAME}") + manager.test_connectivity() + output_message = ( + f"Successfully connected to the {INTEGRATION_NAME} service with the provided connection " + f"parameters!" + ) + + result_value = True + status = EXECUTION_STATE_COMPLETED + + except LastlineAuthenticationException as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to connect to the {INTEGRATION_NAME} service with the provided account. Please " + f"check your configuration. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except Exception as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to connect to the {INTEGRATION_NAME} server! Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + siemplify.LOGGER.info("----------------- Main - Finished -----------------") + siemplify.LOGGER.info(f"Status: {status}:") + siemplify.LOGGER.info(f"Result Value: {result_value}") + siemplify.LOGGER.info(f"Output Message: {output_message}") + siemplify.end(output_message, result_value, status) + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/google/lastline/actions/Ping.yaml b/content/response_integrations/google/lastline/actions/Ping.yaml new file mode 100644 index 000000000..d1d993df6 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/Ping.yaml @@ -0,0 +1,23 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Ping +description: Test connectivity to the Lastline service with parameters provided at + the integration configuration page on the Marketplace tab. +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline#ping +integration_identifier: Lastline +parameters: [] +dynamic_results_metadata: [] +creator: Admin +simulation_data_json: '{"Entities":[]}' diff --git a/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.py b/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.py new file mode 100644 index 000000000..3ec7a9b6f --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.py @@ -0,0 +1,197 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import validators +from TIPCommon import extract_configuration_param, extract_action_param, construct_csv + +from soar_sdk.ScriptResult import EXECUTION_STATE_COMPLETED, EXECUTION_STATE_FAILED +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyUtils import output_handler +from ..core.LastlineManager import LastlineManager +from ..core.consts import ( + INTEGRATION_NAME, + SEARCH_ANALYSIS_HISTORY, + DEFAULT_MAX_HOURS_BACKWARDS, + DEFAULT_X_LAST_SCANS, + DEFAULT_SKIP_X_FIRST_SCANS, + SUBMISSION_TYPE_MAPPER, + NOT_SPECIFIED, + FILE, + URL, + SEARCH_RESULTS, +) +from ..core.utils import get_max_hours_backwards_as_date, get_file_hash +from ..core.exceptions import LastlineInvalidParamException + + +@output_handler +def main(): + siemplify = SiemplifyAction() + siemplify.script_name = f"{INTEGRATION_NAME} - {SEARCH_ANALYSIS_HISTORY}" + siemplify.LOGGER.info("================= Main - Param Init =================") + + api_root = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Api Root", + is_mandatory=True, + print_value=True, + ) + + username = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Username", + is_mandatory=True, + print_value=True, + ) + + password = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Password", + is_mandatory=True, + ) + + verify_ssl = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + input_type=bool, + default_value=True, + is_mandatory=False, + print_value=True, + ) + + submission_name = extract_action_param( + siemplify, param_name="Submission Name", is_mandatory=False, print_value=True + ) + + submission_type = extract_action_param( + siemplify, param_name="Submission Type", is_mandatory=False, print_value=True + ) + + if submission_type != NOT_SPECIFIED: + submission_type = SUBMISSION_TYPE_MAPPER.get(submission_type, NOT_SPECIFIED) + + siemplify.LOGGER.info("----------------- Main - Started -----------------") + + try: + max_hours_backwards = extract_action_param( + siemplify, + param_name="Max Hours Backwards", + is_mandatory=False, + print_value=True, + input_type=int, + default_value=DEFAULT_MAX_HOURS_BACKWARDS, + ) + + if max_hours_backwards is not None: + max_hours_backwards = get_max_hours_backwards_as_date( + hours_backwards=max_hours_backwards + ) + + last_x_scans = extract_action_param( + siemplify, + param_name="Search in last x scans", + is_mandatory=True, + print_value=True, + input_type=int, + default_value=DEFAULT_X_LAST_SCANS, + ) + + first_x_scans = extract_action_param( + siemplify, + param_name="Skip first x scans", + is_mandatory=True, + print_value=True, + input_type=int, + default_value=DEFAULT_SKIP_X_FIRST_SCANS, + ) + + manager = LastlineManager( + api_root=api_root, + username=username, + password=password, + verify_ssl=verify_ssl, + ) + + if submission_type == NOT_SPECIFIED and submission_name: + submission_type = URL if validators.url(submission_name) else FILE + elif submission_type == NOT_SPECIFIED and not submission_name: + submission_type = None + url = submission_name if submission_type == URL else None + + file_sha1 = None + file_md5 = None + if submission_type == FILE and submission_name: + file_sha1, file_md5 = get_file_hash(submission_name) + + siemplify.LOGGER.info(f"Fetching analysis history from {INTEGRATION_NAME}.") + analysis = manager.search_analysis_history( + submission_type=submission_type, + start_time=max_hours_backwards, + search_in_last_x_scans=last_x_scans, + skip_first_x_scans=first_x_scans, + url=url, + file_sha1=file_sha1, + file_md5=file_md5, + ) + siemplify.LOGGER.info( + f"Successfully fetched analysis history from {INTEGRATION_NAME}" + ) + + if analysis.data: + # JSON + siemplify.result.add_result_json(analysis.as_json()) + + # CSV + csv_table = analysis.as_csv() + siemplify.result.add_data_table(SEARCH_RESULTS, construct_csv(csv_table)) + + output_message = f"Found {INTEGRATION_NAME} completed analysis tasks for the provided search parameters" + result_value = True + + else: + output_message = "No Lastline reports were found." + result_value = False + + status = EXECUTION_STATE_COMPLETED + + except LastlineInvalidParamException as error: + siemplify.LOGGER.error(error) + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = f"No {INTEGRATION_NAME} reports were found." + + except Exception as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to find completed analysis tasks for the provided search parameters. Error is: " + f"{error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + siemplify.LOGGER.info("----------------- Main - Finished -----------------") + siemplify.LOGGER.info(f"Status: {status}:") + siemplify.LOGGER.info(f"Result Value: {result_value}") + siemplify.LOGGER.info(f"Output Message: {output_message}") + siemplify.end(output_message, result_value, status) + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.yaml b/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.yaml new file mode 100644 index 000000000..12db73f4f --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SearchAnalysisHistory.yaml @@ -0,0 +1,58 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Search Analysis History +description: 'Search Lastline completed analysis tasks history. For submission either + URL or Filehash in a format of md5 or sha1 can be provided. Note: Action is not + working with Siemplify entities, only action input parameters are used.' +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline#search_analysis_history +integration_identifier: Lastline +parameters: +- name: Submission Name + default_value: '' + type: string + description: Submission name to search for. Can be either URL or Filehash in a + format of MD5 and SHA1. + is_mandatory: false +- name: Submission Type + default_value: Not specified + type: ddl + optional_values: + - Not specified + - URL + - FileHash + description: Optionally specify a submission type to search for, either URL or + FileHash. + is_mandatory: false +- name: Max Hours Backwards + default_value: 24 + type: string + description: Time frame for which to search for completed analysis tasks. + is_mandatory: false +- name: Search in last x scans + default_value: 100 + type: string + description: Search for report in last x analyses executed in Lastline. + is_mandatory: true +- name: Skip first x scans + default_value: 0 + type: string + description: Skip first x scans returned by Lastline API. + is_mandatory: false +dynamic_results_metadata: +- result_example_path: resources/search_analysis_history_JsonResult_example.json + result_name: JsonResult + show_result: true +creator: Admin +simulation_data_json: '{"Entities":[]}' diff --git a/content/response_integrations/google/lastline/actions/SubmitFile.py b/content/response_integrations/google/lastline/actions/SubmitFile.py new file mode 100644 index 000000000..a3f1c7035 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SubmitFile.py @@ -0,0 +1,279 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import sys +import json +from TIPCommon import extract_configuration_param, extract_action_param, construct_csv + +from soar_sdk.ScriptResult import ( + EXECUTION_STATE_COMPLETED, + EXECUTION_STATE_FAILED, + EXECUTION_STATE_INPROGRESS, +) +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyUtils import output_handler +from ..core.LastlineManager import LastlineManager +from ..core.consts import INTEGRATION_NAME, SUBMIT_FILE +from ..core.datamodels import SubmissionTask +from ..core.exceptions import ( + LastlineInvalidParamException, + LastlinePermissionException, + LastlineManyRequestsException, + LastlineAuthenticationException, +) + + +def start_operation(siemplify, manager, file_path: str, wait_for_report: bool): + """ + If the action runs for the first time, this function will be called and will create a submission task. + :param siemplify: {SiemplifyAction} Siemplify service + :param manager: {LastlineManager} Lastline manager + :param file_path: {str} The file_path of the file to analyze + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + status = EXECUTION_STATE_INPROGRESS + + siemplify.LOGGER.info(f"Submitting file to {INTEGRATION_NAME}: {file_path}") + submission_task = manager.submit_file(file_path=file_path) + siemplify.LOGGER.info(f"Successfully submitted file: {file_path}") + + if ( + wait_for_report and submission_task.data.raw_data.get("reports") + ) or not wait_for_report: + return finish_operation( + siemplify=siemplify, + submission_task=submission_task, + file_path=file_path, + wait_for_report=wait_for_report, + ) + + else: + result_value = json.dumps( + {"task_uuid": submission_task.data.task_uuid, "file_path": file_path} + ) + output_message = f"Waiting for the analysis results for the file: {file_path}" + + return output_message, result_value, status + + +def query_operation_status( + siemplify, manager, task_uuid: str, file_path: str, wait_for_report: bool +): + """ + Starting from the second time that the action will run, this function will be called and check if the submission + task completed create a submission task. + :param siemplify: {SiemplifyAction} Siemplify service + :param manager: {LastlineManager} Lastline manager + :param task_uuid: {str} Task identifier + :param file_path: {str} The file path of the file to analyze + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + + siemplify.LOGGER.info(f"Checking the status of analysis task.") + submission_task = manager.get_progress(uuid=task_uuid) + siemplify.LOGGER.info(f"Successfully checked the status of analysis task.") + + if submission_task.data.completed: + completed_submission_task = manager.get_result(uuid=task_uuid) + output_message, result_value, status = finish_operation( + siemplify=siemplify, + submission_task=completed_submission_task, + file_path=file_path, + wait_for_report=wait_for_report, + ) + + else: + status = EXECUTION_STATE_INPROGRESS + result_value = json.dumps({"task_uuid": task_uuid, "file_path": file_path}) + output_message = f"Waiting for the analysis results for the file: {file_path}" + + return output_message, result_value, status + + +def finish_operation( + siemplify, submission_task: SubmissionTask, file_path: str, wait_for_report: bool +): + """ + Finalizing results + :param siemplify: {SiemplifyAction} Siemplify service + :param submission_task: {SubmissionTask} Submission Task data model + :param file_path: {str} file path of the analyzed file + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + siemplify.LOGGER.info(f"Finalizing submission results for: {file_path}") + + # JSON + json_results = submission_task.as_json() + siemplify.result.add_result_json(json_results) + + # CSV output message and attachment + if wait_for_report: + # CSV + csv_table = [submission_task.as_csv()] + csv_table_name = f"{file_path} Analysis Results" + siemplify.result.add_data_table(csv_table_name, construct_csv(csv_table)) + + output_message = ( + f"Successfully fetched the analysis results for the file {file_path}" + ) + + else: + output_message = f"Successfully created analysis task for the file {file_path}" + + result_value = True + status = EXECUTION_STATE_COMPLETED + + return output_message, result_value, status + + +@output_handler +def main(is_first_run): + siemplify = SiemplifyAction() + siemplify.script_name = f"{INTEGRATION_NAME} - {SUBMIT_FILE}" + siemplify.LOGGER.info("================= Main - Param Init =================") + + api_root = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Api Root", + is_mandatory=True, + print_value=True, + ) + + username = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Username", + is_mandatory=True, + print_value=True, + ) + + password = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Password", + is_mandatory=True, + ) + + verify_ssl = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + input_type=bool, + default_value=True, + is_mandatory=False, + print_value=True, + ) + + file_path = extract_action_param( + siemplify, param_name="File Path", is_mandatory=False, print_value=True + ) + + wait_for_report = extract_action_param( + siemplify, + param_name="Wait for the report?", + is_mandatory=False, + print_value=True, + input_type=bool, + ) + + mode = "Main" if is_first_run else {SUBMIT_FILE} + siemplify.LOGGER.info(f"----------------- {mode} - Started -----------------") + + try: + manager = LastlineManager( + api_root=api_root, + username=username, + password=password, + verify_ssl=verify_ssl, + ) + + if is_first_run: + output_message, result_value, status = start_operation( + siemplify=siemplify, + manager=manager, + file_path=file_path, + wait_for_report=wait_for_report, + ) + + else: + task_uuid = json.loads( + siemplify.extract_action_param("additional_data") + ).get("task_uuid") + file_path = json.loads( + siemplify.extract_action_param("additional_data") + ).get("file_path") + output_message, result_value, status = query_operation_status( + siemplify=siemplify, + manager=manager, + task_uuid=task_uuid, + file_path=file_path, + wait_for_report=wait_for_report, + ) + + except FileNotFoundError as error: + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = f"Failed to create analysis task because the provided file path {file_path} is incorrect." + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except LastlineInvalidParamException as error: + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = f"Failed to create analysis task because the provided file path {file_path} is incorrect." + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except (LastlinePermissionException, LastlineManyRequestsException) as error: + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = f"Failed to create analysis task for the provided file path {file_path}. Error is: {error}" + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except LastlineAuthenticationException as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to connect to the {INTEGRATION_NAME} service with the provided account. Please " + f"check your configuration. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except Exception as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to create analysis task for the provided file path" + f"{(' ' + file_path) if file_path else ''}. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + siemplify.LOGGER.info("----------------- Main - Finished -----------------") + siemplify.LOGGER.info(f"Status: {status}:") + siemplify.LOGGER.info(f"Result Value: {result_value}") + siemplify.LOGGER.info(f"Output Message: {output_message}") + siemplify.end(output_message, result_value, status) + + +if __name__ == "__main__": + is_first_run = len(sys.argv) < 3 or sys.argv[2] == "True" + main(is_first_run) diff --git a/content/response_integrations/google/lastline/actions/SubmitFile.yaml b/content/response_integrations/google/lastline/actions/SubmitFile.yaml new file mode 100644 index 000000000..d3191f886 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SubmitFile.yaml @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Submit File +description: 'Submit analysis task for the provided URL. Note: Action is not working + with Siemplify entities, full path to file to analyze should be provided as action + input parameter.' +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline#submit_file +integration_identifier: Lastline +parameters: +- name: File Path + default_value: '' + type: string + description: Specify full path to file to analyze. + is_mandatory: true +- name: Wait for the report? + default_value: 'True' + type: boolean + description: Specify whether the action should wait for the report creation. Report + also can be obtained later with Get Analysis Results action once scan is completed. + is_mandatory: false +dynamic_results_metadata: +- result_example_path: resources/submit_file_JsonResult_example.json + result_name: JsonResult + show_result: true +creator: Admin +is_async: true +simulation_data_json: '{"Entities":[]}' diff --git a/content/response_integrations/google/lastline/actions/SubmitURL.py b/content/response_integrations/google/lastline/actions/SubmitURL.py new file mode 100644 index 000000000..b16ad99a2 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SubmitURL.py @@ -0,0 +1,270 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import sys +import json +from TIPCommon import extract_configuration_param, extract_action_param, construct_csv + +from soar_sdk.ScriptResult import ( + EXECUTION_STATE_COMPLETED, + EXECUTION_STATE_FAILED, + EXECUTION_STATE_INPROGRESS, +) +from soar_sdk.SiemplifyAction import SiemplifyAction +from soar_sdk.SiemplifyUtils import output_handler +from ..core.LastlineManager import LastlineManager +from ..core.consts import INTEGRATION_NAME, SUBMIT_URL +from ..core.datamodels import SubmissionTask +from ..core.exceptions import ( + LastlineInvalidParamException, + LastlinePermissionException, + LastlineManyRequestsException, + LastlineAuthenticationException, +) + + +def start_operation(siemplify, manager, url: str, wait_for_report: bool): + """ + If the action runs for the first time, this function will be called and will create a submission task. + :param siemplify: {SiemplifyAction} Siemplify service + :param manager: {LastlineManager} Lastline manager + :param url: {str} The url to analyze + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + status = EXECUTION_STATE_INPROGRESS + output_message = "" + + siemplify.LOGGER.info(f"Submitting url to {INTEGRATION_NAME}: {url}") + submission_task = manager.submit_url(url_for_analysis=url) + siemplify.LOGGER.info(f"Successfully submitted url: {url}") + + if ( + wait_for_report and submission_task.data.raw_data.get("reports") + ) or not wait_for_report: + return finish_operation( + siemplify=siemplify, + submission_task=submission_task, + url=url, + wait_for_report=wait_for_report, + ) + + else: + result_value = json.dumps( + {"task_uuid": submission_task.data.task_uuid, "url": url} + ) + output_message = f"Waiting for the analysis results for the url: {url}" + + return output_message, result_value, status + + +def query_operation_status( + siemplify, manager, task_uuid: str, url: str, wait_for_report: bool +): + """ + Starting from the second time that the action will run, this function will be called and check if the submission + task completed create a submission task. + :param siemplify: {SiemplifyAction} Siemplify service + :param manager: {LastlineManager} Lastline manager + :param task_uuid: {str} Task identifier + :param url: {str} The url to analyze + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + + siemplify.LOGGER.info(f"Checking the status of analysis task.") + submission_task = manager.get_progress(uuid=task_uuid) + siemplify.LOGGER.info(f"Successfully checked the status of analysis task.") + + if submission_task.data.completed: + completed_submission_task = manager.get_result(uuid=task_uuid) + output_message, result_value, status = finish_operation( + siemplify=siemplify, + submission_task=completed_submission_task, + url=url, + wait_for_report=wait_for_report, + ) + + else: + status = EXECUTION_STATE_INPROGRESS + result_value = json.dumps({"task_uuid": task_uuid, "url": url}) + output_message = f"Waiting for the analysis results for the url: {url}" + + return output_message, result_value, status + + +def finish_operation( + siemplify, submission_task: SubmissionTask, url: str, wait_for_report: bool +): + """ + Finalizing results + :param siemplify: {SiemplifyAction} Siemplify service + :param submission_task: {SubmissionTask} Submission Task data model + :param url: {str} Analyzed URL + :param wait_for_report: {bool} Decide if should wait to submission reports or not + :return: (output_message, result_value, status) + """ + siemplify.LOGGER.info(f"Finalizing submission results for: {url}") + + # JSON + json_results = submission_task.as_json() + siemplify.result.add_result_json(json_results) + + # CSV and output message + if wait_for_report: + csv_table = [submission_task.as_csv()] + csv_table_name = f"{url} Analysis Results" + siemplify.result.add_data_table(csv_table_name, construct_csv(csv_table)) + output_message = f"Successfully fetched the analysis results for the url {url}" + + else: + output_message = f"Successfully created analysis task for the url {url}" + + result_value = True + status = EXECUTION_STATE_COMPLETED + + return output_message, result_value, status + + +@output_handler +def main(is_first_run): + siemplify = SiemplifyAction() + siemplify.script_name = f"{INTEGRATION_NAME} - {SUBMIT_URL}" + siemplify.LOGGER.info("================= Main - Param Init =================") + + api_root = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Api Root", + is_mandatory=True, + print_value=True, + ) + + username = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Username", + is_mandatory=True, + print_value=True, + ) + + password = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Password", + is_mandatory=True, + ) + + verify_ssl = extract_configuration_param( + siemplify, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + input_type=bool, + default_value=True, + is_mandatory=False, + print_value=True, + ) + + url = extract_action_param( + siemplify, param_name="URL For Analysis", is_mandatory=False, print_value=True + ) + + wait_for_report = extract_action_param( + siemplify, + param_name="Wait for the report?", + is_mandatory=False, + print_value=True, + input_type=bool, + ) + + mode = "Main" if is_first_run else {SUBMIT_URL} + siemplify.LOGGER.info(f"----------------- {mode} - Started -----------------") + + try: + manager = LastlineManager( + api_root=api_root, + username=username, + password=password, + verify_ssl=verify_ssl, + ) + + if is_first_run: + output_message, result_value, status = start_operation( + siemplify=siemplify, + manager=manager, + url=url, + wait_for_report=wait_for_report, + ) + + else: + task_uuid = json.loads( + siemplify.extract_action_param("additional_data") + ).get("task_uuid") + url = json.loads(siemplify.extract_action_param("additional_data")).get( + "url" + ) + output_message, result_value, status = query_operation_status( + siemplify=siemplify, + manager=manager, + task_uuid=task_uuid, + url=url, + wait_for_report=wait_for_report, + ) + + except LastlineInvalidParamException as error: + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = f"Failed to create analysis task because the provided url {url} is incorrect." + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except (LastlinePermissionException, LastlineManyRequestsException) as error: + result_value = False + status = EXECUTION_STATE_COMPLETED + output_message = ( + f"Failed to create analysis task for the url {url}. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except LastlineAuthenticationException as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to connect to the {INTEGRATION_NAME} service with the provided account. Please " + f"check your configuration. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + except Exception as error: + result_value = False + status = EXECUTION_STATE_FAILED + output_message = ( + f"Failed to create analysis task for the url {url}. Error is: {error}" + ) + siemplify.LOGGER.error(output_message) + siemplify.LOGGER.exception(error) + + siemplify.LOGGER.info("----------------- Main - Finished -----------------") + siemplify.LOGGER.info(f"Status: {status}:") + siemplify.LOGGER.info(f"Result Value: {result_value}") + siemplify.LOGGER.info(f"Output Message: {output_message}") + siemplify.end(output_message, result_value, status) + + +if __name__ == "__main__": + is_first_run = len(sys.argv) < 3 or sys.argv[2] == "True" + main(is_first_run) diff --git a/content/response_integrations/google/lastline/actions/SubmitURL.yaml b/content/response_integrations/google/lastline/actions/SubmitURL.yaml new file mode 100644 index 000000000..a3e590418 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/SubmitURL.yaml @@ -0,0 +1,38 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Submit URL +description: 'Submit analysis task for the provided URL. Note: Action is not working + with Siemplify entities, URL to analyze should be provided as action input parameter.' +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline#submit_url +integration_identifier: Lastline +parameters: +- name: URL For Analysis + default_value: '' + type: string + description: 'Specify URL to analyze. ' + is_mandatory: true +- name: Wait for the report? + default_value: 'True' + type: boolean + description: Specify whether the action should wait for the report creation. Report + also can be obtained later with Get Analysis Results action once scan is completed. + is_mandatory: false +dynamic_results_metadata: +- result_example_path: resources/submit_url_JsonResult_example.json + result_name: JsonResult + show_result: true +creator: Admin +is_async: true +simulation_data_json: '{"Entities":[]}' diff --git a/content/response_integrations/google/lastline/actions/__init__.py b/content/response_integrations/google/lastline/actions/__init__.py new file mode 100644 index 000000000..9f71a2dc3 --- /dev/null +++ b/content/response_integrations/google/lastline/actions/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/content/response_integrations/google/lastline/core/LastlineManager.py b/content/response_integrations/google/lastline/core/LastlineManager.py new file mode 100644 index 000000000..662a0d9ab --- /dev/null +++ b/content/response_integrations/google/lastline/core/LastlineManager.py @@ -0,0 +1,289 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ============================================================================# +# title :LastlineManager.py +# description :This Module contain all Lastline operations functionality +# author :amit.levizky@siemplify.co +# date :18-03-2021 +# python_version :3.7 +# product_version :1.0 +# ============================================================================# + +# ============================= IMPORTS ===================================== # + +from __future__ import annotations +from urllib.parse import urljoin +from .exceptions import ( + LastlineAPIException, + LastlineAuthenticationException, + LastlinePermissionException, + LastlineInvalidParamException, + LastlineManyRequestsException, +) +from .consts import ( + SUCCESS_CODE, + AUTHENTICATION_ERROR, + PERMISSION_DENIED, + INVALID_PARAMETER_ERROR, + TOO_MANY_REQUESTS_ERROR, +) +from .datamodels import SubmissionTask, Analysis +from .utils import remove_empty_kwargs + +from .LastlineParser import LastlineParser + +import requests + +HEADERS = {"Content-Type": "multipart/form-data"} + +ENDPOINTS = { + "login": "/papi/login", + "submit_url": "/papi/analysis/submit_url", + "submit_file": "/papi/analysis/submit_file", + "get_progress": "/papi/analysis/get_progress", + "get_result": "/papi/analysis/get_result", + "search_analysis_history": "/papi/analysis/get_history", +} + + +class LastlineManager: + def __init__( + self, api_root: str, username: str, password: str, verify_ssl: bool = True + ): + self.api_root = api_root[:-1] if api_root.endswith("/") else api_root + self.username = username + self.password = password + + self.session = requests.Session() + self.session.verify = verify_ssl + self._obtain_session_cookie() + self.session.headers = HEADERS + + self.parser = LastlineParser() + + def _obtain_session_cookie(self): + """ + Obtain cookie to make API calls + """ + request_url = self._get_full_url("login") + response = self.session.get( + request_url, data={"username": self.username, "password": self.password} + ) + self.session.cookies.update(response.cookies) + + @staticmethod + def validate_response(response, error_msg="An error occurred"): + """ + Validate a response + :param response: {requests.Response} The response + :param error_msg: {str} The error message to display on failure + """ + try: + response.raise_for_status() + + except requests.HTTPError as error: + try: + response.json() + except: + # Not a JSON - return content + raise LastlineAPIException( + f"{error_msg}: {error} - {error.response.content}" + ) + raise LastlineAPIException( + f"{error_msg}: {error} {response.json().get('message')}" + ) + + def _validate_api_errors(self, response, error_msg="An error occurred"): + """ + In case of api with specific Lastline API errors, raise the appropriate error. + :param response: {requests.Response} Response from the API call. + :param error_msg: {str} Error message + :return: raise Exception if failed to validate response + """ + if response.json().get("success") != SUCCESS_CODE: + self.raise_api_error(response.json(), error_msg) + + @staticmethod + def raise_api_error(response_json, error_msg): + """ + Raise the appropriate error + :param response_json: {Dict} Response error dictionary {success, error_code, error} + :param error_msg: {str} Error message + :return: raise Exception if failed to validate response + """ + if response_json.get("error_code") == AUTHENTICATION_ERROR: + raise LastlineAuthenticationException( + f"{error_msg}: {response_json.get('error')}" + ) + elif response_json.get("error_code") == PERMISSION_DENIED: + raise LastlinePermissionException( + f"{error_msg}: {response_json.get('error')}" + ) + elif response_json.get("error_code") == INVALID_PARAMETER_ERROR: + raise LastlineInvalidParamException( + f"{error_msg}: {response_json.get('error')}" + ) + elif response_json.get("error_code") == TOO_MANY_REQUESTS_ERROR: + raise LastlineManyRequestsException( + f"{error_msg}: {response_json.get('error')}" + ) + + def _get_full_url(self, url_key, **kwargs) -> str: + """ + Get full url from url key. + :param url_id: {str} The key of url + :param kwargs: {dict} Variables passed for string formatting + :return: {str} The full url + """ + return urljoin(self.api_root, ENDPOINTS[url_key].format(**kwargs)) + + def test_connectivity(self): + """ + Test connectivity to the Lastline service with parameters provided at the integration configuration page on + the Marketplace tab. + :return: raise Exception if failed to validate response + """ + request_url = self._get_full_url("login") + payload = {"username": self.username, "password": self.password} + + response = self.session.get(request_url, data=payload) + self.validate_response(response, "Unable to login") + self._validate_api_errors(response, "Unable to login") + + def submit_url(self, url_for_analysis: str, is_get_process=False) -> SubmissionTask: + """ + Create submission task for URL if this url submission does not exists. + Returns the summary of the task + should be provided as action input parameter. + :param url_for_analysis: {str} Specify URL to analyze. + :param is_get_process: {bool} Weather if this an status update request or submission data request + :return: {datamodels.SubmissionTask} SubmissionTask data model + raise Exception if failed to validate response + """ + request_url = self._get_full_url("submit_url") + payload = {"url": url_for_analysis} + + response = self.session.post(request_url, params=payload) + self.validate_response(response, "Unable to submit url") + self._validate_api_errors(response, "Unable to submit url") + + return self.parser.build_submission_task_obj( + response, is_get_process=is_get_process + ) + + def get_progress(self, uuid: str, is_get_process=True): + """ + Get progress for a previously submitted analysis task. + :param uuid: {str} The unique identifier of the submitted task. + :param is_get_process: {bool} Weather if this an status update request or submission data request + :return: {datamodels.SubmissionTask} SubmissionTask data model + raise Exception if failed to validate response + """ + request_url = self._get_full_url("get_progress") + payload = {"uuid": uuid} + + response = self.session.get(request_url, params=payload) + self.validate_response( + response, "Unable to get progress details on submission task" + ) + self._validate_api_errors( + response, "Unable to get progress details on submission task" + ) + + return self.parser.build_submission_task_obj(response, is_get_process) + + def submit_file(self, file_path: str, is_get_process=False) -> SubmissionTask: + """ + Submit analysis task for the provided URL. + :param file_path: {str} Specify File Path to analyze. + :param is_get_process: {bool} Weather if this an status update request or submission data request + :return: {datamodels.SubmissionTask} SubmissionTask data model + raise Exception if failed to validate response + """ + self.session.headers.pop("Content-Type") + request_url = self._get_full_url("submit_file") + payload = {} + + files = [("file", (open(file_path, "rb")))] + + response = self.session.post(request_url, data=payload, files=files) + self.validate_response(response, "Unable to submit file") + self._validate_api_errors(response, "Unable to submit file") + + return self.parser.build_submission_task_obj( + response, is_get_process=is_get_process + ) + + def get_result( + self, uuid: str, full_report_score: int = -1, is_get_process: bool = False + ): + """ + Get results for a previously submitted analysis task. + :param uuid: {str} The unique identifier of the submitted task. + :param is_get_process: {bool} Weather if this an status update request or submission data request + :param full_report_score: {int} Minimum score that causes detailed analysis reports to be served; -1 indicates + “never return full report”; 0 indicates “return full report at all times”. If report_uuid is specified, + this parameter is ignored. + :return: {datamodels.SubmissionTask} SubmissionTask data model + raise Exception if failed to validate response + """ + request_url = self._get_full_url("get_result") + payload = {"uuid": uuid, "full_report_score": full_report_score} + + response = self.session.get(request_url, params=payload) + self.validate_response(response, "Unable to get results") + self._validate_api_errors(response, "Unable to get results") + + return self.parser.build_submission_task_obj( + response, is_get_process=is_get_process + ) + + def search_analysis_history( + self, + submission_type: str = None, + start_time: str = None, + search_in_last_x_scans: int = None, + skip_first_x_scans: int = None, + url: str = None, + file_md5: str = None, + file_sha1: str = None, + ) -> Analysis: + """ + Search Lastline completed analysis tasks history. + :param submission_type: {str} submission type to search for, either URL or FileHash. + :param start_time: {int} Time frame for which to search for completed analysis tasks + :param search_in_last_x_scans: {int} Search for report in last x analyses executed in any run. + :param skip_first_x_scans: {int} Skip first x scans returned by any run. + :param url: {str} Limits the results to those with the corresponding url + :return: {datamodels.Analysis} Analysis data model + :param file_md5: {str} File hash in MD5 format + :param file_sha1: {str} File hash in SHA1 format + """ + request_url = self._get_full_url("search_analysis_history") + payload = { + "limit": search_in_last_x_scans, + "limit_offset": skip_first_x_scans, + "start_time": start_time, + "submission_type": submission_type, + "file_md5": file_md5, + "file_sha1": file_sha1, + "url": url, + } + + response = self.session.get(request_url, params=remove_empty_kwargs(**payload)) + self.validate_response(response, "Unable to get analysis history") + self._validate_api_errors(response, "Unable to get analysis history") + + return self.parser.build_analysis_obj(response) diff --git a/content/response_integrations/google/lastline/core/LastlineParser.py b/content/response_integrations/google/lastline/core/LastlineParser.py new file mode 100644 index 000000000..3a7a04711 --- /dev/null +++ b/content/response_integrations/google/lastline/core/LastlineParser.py @@ -0,0 +1,127 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +from .datamodels import ( + SubmissionTask, + SubmissionTaskData, + SubmissionTaskProcessData, + SubmissionTaskAnalysisSubject, + Analysis, + AnalysisData, +) + + +class LastlineParser: + """ + Lastline Parser + """ + + @staticmethod + def build_submission_task_obj(response, is_get_process: bool) -> SubmissionTask: + response_json = response.json() + if is_get_process: + data = LastlineParser.build_submission_process_data_obj( + response_json.get("data", {}) + ) + else: + data = LastlineParser.build_submission_task_data_obj( + response_json.get("data", {}) + ) + + return SubmissionTask( + raw_data=response_json, + success=response_json.get("success") or "", + data=data, + ) + + @staticmethod + def build_submission_task_data_obj(raw_data) -> SubmissionTaskData: + analysis_subject = ( + LastlineParser.build_submission_task_analysis_subject_obj( + raw_data.get("analysis_subject") + ) + if raw_data.get("analysis_subject") + else None + ) + + md5 = analysis_subject.md5 if analysis_subject and analysis_subject.md5 else "" + sha1 = ( + analysis_subject.sha1 if analysis_subject and analysis_subject.sha1 else "" + ) + sha256 = ( + analysis_subject.sha256 + if analysis_subject and analysis_subject.sha256 + else "" + ) + mime_type = ( + analysis_subject.mime_type + if analysis_subject and analysis_subject.mime_type + else "" + ) + + malicious_activity = raw_data.get("malicious_activity", {}) + # malicious_activity_str = '' + # if malicious_activity: + # for item in malicious_activity: + # malicious_activity_str += f"{item}\n" + + return SubmissionTaskData( + raw_data=raw_data, + submission=raw_data.get("submission") or "", + expires=raw_data.get("expires") or "", + task_uuid=raw_data.get("task_uuid") or "", + score=raw_data.get("score") if raw_data.get("score") is not None else "", + analysis_subject=analysis_subject or "", + malicious_activity=malicious_activity, + md5=md5, + sha1=sha1, + sha256=sha256, + mime_type=mime_type, + ) + + @staticmethod + def build_submission_process_data_obj(raw_data) -> SubmissionTaskProcessData: + return SubmissionTaskProcessData( + raw_data=raw_data, + progress=raw_data.get("progress") or "", + completed=raw_data.get("completed") or "", + score=raw_data.get("score") if raw_data.get("score") is not None else "", + ) + + @staticmethod + def build_submission_task_analysis_subject_obj( + raw_data, + ) -> SubmissionTaskAnalysisSubject: + return SubmissionTaskAnalysisSubject( + raw_data=raw_data, + sha1=raw_data.get("sha1") or "", + sha256=raw_data.get("sha256") or "", + mime_type=raw_data.get("mime_type") or "", + md5=raw_data.get("md5") or "", + ) + + @staticmethod + def build_analysis_obj(response) -> Analysis: + data = [ + LastlineParser.build_analysis_data_obj(analysis_data) + for analysis_data in response.json().get("data", []) + ] + return Analysis( + raw_data=response.json(), success=response.json().get("success"), data=data + ) + + @staticmethod + def build_analysis_data_obj(raw_data) -> AnalysisData: + return AnalysisData(raw_data=raw_data, **raw_data) diff --git a/content/response_integrations/google/lastline/core/__init__.py b/content/response_integrations/google/lastline/core/__init__.py new file mode 100644 index 000000000..9f71a2dc3 --- /dev/null +++ b/content/response_integrations/google/lastline/core/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/content/response_integrations/google/lastline/core/consts.py b/content/response_integrations/google/lastline/core/consts.py new file mode 100644 index 000000000..31c089ea9 --- /dev/null +++ b/content/response_integrations/google/lastline/core/consts.py @@ -0,0 +1,53 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +INTEGRATION_NAME = "Lastline" + +# Actions: +PING = "Ping" +SUBMIT_URL = "Submit URL" +SUBMIT_FILE = "Submit File" +SEARCH_ANALYSIS_HISTORY = "Search Analysis History" +GET_ANALYSIS_RESULTS = "Get Analysis Results" + +SUCCESS_CODE = 1 + +# API Error codes: +PERMISSION_DENIED = 3001 +AUTHENTICATION_ERROR = 3004 +INVALID_PARAMETER_ERROR = 3005 +NO_SUCH_ENTITY = 3007 +NOT_YET_AVAILABLE = 3013 +TOO_MANY_REQUESTS_ERROR = 3014 +HOSTED_BACKEND_UNVAILABLE = 3015 + +DEFAULT_MAX_HOURS_BACKWARDS = 24 +DEFAULT_X_LAST_SCANS = 100 +DEFAULT_X_LAST_SCANS_GET_RESULTS = 25 +DEFAULT_SKIP_X_FIRST_SCANS = 0 +THRESHOLD = 70 + +MD5_LENGTH = 32 +SHA1_LENGTH = 40 + +SUBMISSION_TYPE_MAPPER = {"URL": "URL", "FileHash": "FILE"} + +NOT_SPECIFIED = "Not specified" +FILE = "FILE" +URL = "URL" + +ANALYSIS_REPORT = "Lastline File Analysis Results" +SEARCH_RESULTS = "Search Results" +ANALYSIS_RESULTS = "{0} Analysis Results" diff --git a/content/response_integrations/google/lastline/core/datamodels.py b/content/response_integrations/google/lastline/core/datamodels.py new file mode 100644 index 000000000..10e61dc63 --- /dev/null +++ b/content/response_integrations/google/lastline/core/datamodels.py @@ -0,0 +1,236 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +from typing import Dict, List +from .consts import FILE, INTEGRATION_NAME, URL +from TIPCommon import add_prefix_to_dict, dict_to_flat + + +class SubmissionTask: + """ + SubmissionTask Data Model + """ + + def __init__(self, raw_data, data, success: int = None): + self.raw_data = raw_data + self.success = success + self.data = data + + def as_json(self) -> Dict: + return {"success": self.success, "data": self.data.as_json()} + + def as_csv(self): + return self.data.as_csv() + + def as_insight(self, entity_identifier: str, entity_type: str): + return self.data.as_insight(entity_identifier, entity_type) + + def as_table(self, entity_type: str, is_enrichment=False): + if not is_enrichment: + return self.data.as_csv() + flat_dict = dict_to_flat(self.data.as_table(entity_type)) + enrichment_data = add_prefix_to_dict(flat_dict, f"{INTEGRATION_NAME}") + return enrichment_data if is_enrichment else self.data.as_table(entity_type) + + +class SubmissionTaskData: + """ + SubmissionTaskData Data Model + """ + + def __init__( + self, + raw_data, + submission: str = None, + expires: str = None, + task_uuid: str = None, + score: int = None, + malicious_activity=None, + md5: str = None, + sha1: str = None, + sha256: str = None, + mime_type: str = None, + analysis_subject=None, + ): + self.raw_data = raw_data + self.submission = submission + self.expires = expires + self.task_uuid = task_uuid + self.score = score + self.malicious_activity = malicious_activity + self.analysis_subject = analysis_subject + self.md5 = md5 + self.sha1 = sha1 + self.sha256 = sha256 + self.mime_type = mime_type + + def as_json(self) -> Dict: + return self.raw_data + + def as_csv(self) -> Dict: + malicious_activity_str = ",".join(self.malicious_activity) + csv_dict = { + "Submission_Timestamp": self.submission, + "Latest_Submission_Timestamp": self.raw_data.get( + "last_submission_timestamp" + ) + or "", + "Results_Expiry_Timestamp": self.expires, + "Analysis_Task_UUID": self.task_uuid, + "Score": self.score, + "Malicious_Activity": malicious_activity_str, + } + + if self.analysis_subject and self.analysis_subject.sha1: + csv_dict.update(self.analysis_subject.as_csv()) + + return csv_dict + + def as_insight(self, entity_identifier, entity_type: str): + malicious_activity_str = ",\n".join(self.malicious_activity) + return f""" + Entity: {entity_identifier} + Score: {self.raw_data.get('score') if not None else ''} + Malicious Activity Observed:\n {f"{malicious_activity_str}" or 'N/A'} + """ + + def as_table(self, entity_type: str = URL) -> Dict: + csv_dict = { + "Submission_Timestamp": self.submission, + "Latest_Submission_Timestamp": self.raw_data.get( + "last_submission_timestamp" + ) + or "", + "Results_Expiry_Timestamp": self.expires, + "Analysis_Task_UUID": self.task_uuid, + "Score": self.score, + "Malicious_Activity": self.malicious_activity, + } + + if entity_type == FILE: + csv_dict["md5"] = self.md5 + csv_dict["sha1"] = self.sha1 + csv_dict["sha256"] = self.sha256 + csv_dict["mime_type"] = self.mime_type + + return csv_dict + + +class SubmissionTaskProcessData: + """ + SubmissionTaskProcessData Data Model + """ + + def __init__( + self, raw_data, progress: int = None, completed: int = None, score: int = None + ): + self.raw_data = raw_data + self.progress = progress + self.completed = completed + self.score = score + + +class SubmissionTaskAnalysisSubject: + """ + Submission Task Analysis Subject Data Model + """ + + def __init__( + self, + raw_data, + sha256: str = None, + sha1: str = None, + mime_type: str = None, + md5: str = None, + ): + self.raw_data = raw_data + self.sha1 = sha1 + self.sha256 = sha256 + self.mime_type = mime_type + self.md5 = md5 + + def as_csv(self): + return { + "md5_hash": self.md5, + "sha1_hash": self.sha1, + "sha256_hash": self.sha256, + "mime_type": self.mime_type, + } + + +class Analysis: + """ + Analysis Model + """ + + def __init__(self, raw_data, success: int = None, data: List = None): + self.raw_data = raw_data + self.success = success + self.data = data + + def as_json(self): + return self.raw_data + + def as_csv(self): + return [data_analysis.as_csv() for data_analysis in self.data] + + +class AnalysisData: + """ + Analysis Data Model + """ + + def __init__( + self, + raw_data, + username: str = None, + status: str = None, + task_subject_filename: str = None, + task_subject_sha1: str = None, + task_uuid: str = None, + task_subject_md5: str = None, + task_subject_url: str = None, + task_start_time: str = None, + analysis_history_id: int = None, + title: str = None, + score: int = None, + **kwargs, + ): + self.raw_data = raw_data + self.username = username + self.status = status + self.task_subject_filename = task_subject_filename + self.task_subject_sha1 = task_subject_sha1 + self.task_uuid = task_uuid + self.task_subject_md5 = task_subject_md5 + self.task_subject_url = task_subject_url + self.task_start_time = task_start_time + self.analysis_history_id = analysis_history_id + self.title = title + self.score = score + + def as_json(self): + return self.raw_data + + def as_csv(self): + return { + "Task UUID": self.task_uuid, + "md5": self.task_subject_md5, + "sha1": self.task_subject_sha1, + "Url": self.task_subject_url, + "Status": self.status, + "Submitted by (username)": self.username, + "Submitted at": self.task_start_time, + } diff --git a/content/response_integrations/google/lastline/core/exceptions.py b/content/response_integrations/google/lastline/core/exceptions.py new file mode 100644 index 000000000..c66824685 --- /dev/null +++ b/content/response_integrations/google/lastline/core/exceptions.py @@ -0,0 +1,53 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +class LastlineAPIException(Exception): + """ + Lastline API exception + """ + + pass + + +class LastlineAuthenticationException(Exception): + """ + Lastline Authentication exception + """ + + pass + + +class LastlinePermissionException(Exception): + """ + Lastline Authentication exception + """ + + pass + + +class LastlineInvalidParamException(Exception): + """ + Lastline Invalid Parameter exception + """ + + pass + + +class LastlineManyRequestsException(Exception): + """ + Lastline Too Many Requests exception + """ + + pass diff --git a/content/response_integrations/google/lastline/core/utils.py b/content/response_integrations/google/lastline/core/utils.py new file mode 100644 index 000000000..2d3b189e5 --- /dev/null +++ b/content/response_integrations/google/lastline/core/utils.py @@ -0,0 +1,87 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import re + +from datetime import datetime, timedelta +from .consts import SHA1_LENGTH, MD5_LENGTH +from .exceptions import LastlineInvalidParamException +from typing import Tuple + + +def remove_empty_kwargs(**kwargs) -> dict: + """ + Remove keys from dictionary that has the value None + :param kwargs: key value arguments + :return: {dict} dictionary without keys that have the value None + """ + return {k: v for k, v in kwargs.items() if v is not None} + + +def get_max_hours_backwards_as_date(hours_backwards: int) -> str: + """ + Get Max Hours Backwards as string date + :param hours_backwards: {int} Hours to calculate backwards + :return: {str} + """ + hours_backwards_time = datetime.now() - timedelta(hours=hours_backwards) + return hours_backwards_time.strftime("%Y-%m-%d %H:%M:%S") + + +def is_valid_sh1(file_hash: str) -> bool: + """ + Check if the given string is valid SH1 file hash + :param file_hash: {str} String to check + :return: True if it is a valid SH1 file hash, else False + """ + return ( + True + if file_hash + and len(file_hash) == SHA1_LENGTH + and re.findall(r"([0-9a-fA-F\d]{40})", file_hash) + else False + ) + + +def is_valid_md5(file_hash: str) -> bool: + """ + Check if the given string is valid MD5 file hash + :param file_hash: {str} String to check + :return: True if it is a valid MD5 file hash, else False + """ + return ( + True + if file_hash + and len(file_hash) == MD5_LENGTH + and re.findall(r"([0-9a-fA-F\d]{32})", file_hash) + else False + ) + + +def get_file_hash(file_hash: str) -> Tuple: + """ + Return tuple with the right file hash format and None for the second + :param file_hash: {str} String to check + :return: {Tuple} (SH1 or None, MD5 or None) + raise LastlineInvalidParamException if it is not MD5 or SHA1 file hash formats + """ + sha1 = file_hash if is_valid_sh1(file_hash) else None + md5 = file_hash if is_valid_md5(file_hash) else None + + if sha1 or md5: + return sha1, md5 + raise LastlineInvalidParamException( + "Invalid 'submission name' parameter was provided!" + ) diff --git a/content/response_integrations/google/lastline/definition.yaml b/content/response_integrations/google/lastline/definition.yaml new file mode 100644 index 000000000..8c4740358 --- /dev/null +++ b/content/response_integrations/google/lastline/definition.yaml @@ -0,0 +1,47 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +identifier: Lastline +name: Lastline +parameters: +- name: Api Root + default_value: https://user.lastline.com + type: string + description: '' + is_mandatory: true + integration_identifier: Lastline +- name: Username + default_value: '' + type: string + description: '' + is_mandatory: true + integration_identifier: Lastline +- name: Password + default_value: '' + type: password + description: '' + is_mandatory: true + integration_identifier: Lastline +- name: Verify SSL + default_value: true + type: boolean + description: '' + is_mandatory: false + integration_identifier: Lastline +documentation_link: https://cloud.google.com/chronicle/docs/soar/marketplace-integrations/lastline +categories: +- Malware Analysis +- Security +svg_logo_path: resources/logo.svg +image_path: resources/image.png diff --git a/content/response_integrations/google/lastline/pyproject.toml b/content/response_integrations/google/lastline/pyproject.toml new file mode 100644 index 000000000..25117961b --- /dev/null +++ b/content/response_integrations/google/lastline/pyproject.toml @@ -0,0 +1,34 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[project] +name = "Lastline" +version = "10.0" +description = "Lastline's Network Detection and Response platform, powered by AI, protects on-premises networks, email, and public cloud workloads from cyber threats." +requires-python = ">=3.11" +dependencies = [ "requests==2.32.4", "tipcommon", "validators==0.33.0",] + +[dependency-groups] +dev = [ "pytest>=9.0.3", "pytest-json-report>=1.5.0", "soar-sdk",] + +[tool.uv] +[[tool.uv.index]] +url = "https://pypi.org/simple" +default = true + +[tool.uv.sources.tipcommon] +path = "../../../../packages/tipcommon/whls/TIPCommon-1.0.10-py3-none-any.whl" + +[tool.uv.sources.soar-sdk] +git = "https://github.com/chronicle/soar-sdk.git" diff --git a/content/response_integrations/google/lastline/release_notes.yaml b/content/response_integrations/google/lastline/release_notes.yaml new file mode 100644 index 000000000..089e5b3c0 --- /dev/null +++ b/content/response_integrations/google/lastline/release_notes.yaml @@ -0,0 +1,130 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- description: New Lastline integration added. + integration_version: 1.0 + item_name: Lastline + item_type: Integration + publish_time: '2021-04-07' + ticket_number: TIPG-5996 + new: true + regressive: false + deprecated: false + removed: false +- description: Updated the "Verify SSL" logic of the integration. + integration_version: 2.0 + item_name: Lastline + item_type: Integration + publish_time: '2021-09-01' + ticket_number: TIPG-9685 + new: false + regressive: false + deprecated: false + removed: false +- description: Updated Integration's dependencies. + integration_version: 3.0 + item_name: Lastline + item_type: Integration + publish_time: '2022-08-03' + ticket_number: TIPG-11787 + new: false + regressive: false + deprecated: false + removed: false +- description: Security Enhancements. + integration_version: 4.0 + item_name: Lastline + item_type: Integration + publish_time: '2023-04-27' + ticket_number: '' + new: false + regressive: false + deprecated: false + removed: false +- description: 'Lastline integration - Important - Updated the integration code + to work with Python version 3.11. To ensure compatibility and avoid disruptions, + follow the upgrade best practices described in the following document: https://cloud.google.com/chronicle/docs/soar/respond/integrations-setup/upgrade-python-versions' + integration_version: 5.0 + item_name: Lastline + item_type: Integration + publish_time: '2024-08-21' + ticket_number: '331751541' + new: false + regressive: false + deprecated: false + removed: false +- description: Get Analysis Results - Added Predefined Widget. + integration_version: 6.0 + item_name: Get Analysis Results + item_type: Widget + publish_time: '2024-11-05' + ticket_number: '346745176' + new: true + regressive: false + deprecated: false + removed: false +- description: Search Analysis History, Submit File, Submit URL - Added Predefined + Widgets. + integration_version: 6.0 + item_name: Search Analysis History, Submit File, Submit URL + item_type: Widget + publish_time: '2024-11-05' + ticket_number: '353158439' + new: true + regressive: false + deprecated: false + removed: false +- description: Updated integration metadata + integration_version: 7.0 + item_name: Lastline + item_type: Integration + publish_time: '2025-10-29' + ticket_number: '' + new: false + regressive: false + deprecated: false + removed: false +- description: Search Analysis History, Submit URL, Submit File - Updated Predefined + Widgets. + integration_version: 8.0 + item_name: Search Analysis History, Submit URL, Submit File + item_type: Widget + publish_time: '2026-03-20' + ticket_number: '492135438' + new: false + regressive: false + deprecated: false + removed: false +- description: Get Analysis Results - Updated Predefined Widget. + integration_version: 9.0 + item_name: Get Analysis Results + item_type: Widget + publish_time: '2026-03-27' + ticket_number: '496451947' + new: false + regressive: false + deprecated: false + removed: false + +- description: 'Integration - Source code for the integration is now available publicly + on Github. Link to repo: https://github.com/chronicle/content-hub' + integration_version: 10.0 + item_name: Lastline + item_type: Integration + publish_time: '2026-04-20' + new: false + regressive: false + deprecated: false + removed: false + ticket_number: '495762513' diff --git a/content/response_integrations/google/lastline/resources/ai/actions_ai_description.yaml b/content/response_integrations/google/lastline/resources/ai/actions_ai_description.yaml new file mode 100644 index 000000000..e9d676f63 --- /dev/null +++ b/content/response_integrations/google/lastline/resources/ai/actions_ai_description.yaml @@ -0,0 +1,410 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Get Analysis Results: + ai_description: >- + ### General Description + + Enriches `FileHash` and `URL` entities by retrieving results from previously completed + analysis tasks in Lastline. This action is useful for gaining immediate context + on known files or URLs without triggering a new sandbox submission. It retrieves + scores, malicious activity summaries, and technical metadata, updating the entity's + profile within Google SecOps. + + + ### Parameters Description + + + | Parameter | Type | Mandatory | Description | + + | :--- | :--- | :--- | :--- | + + | Threshold | Integer | Yes | The score threshold (default is 70). If the retrieved + Lastline analysis score is higher than this value, the entity is marked as suspicious + in Google SecOps. | + + | Search in last x scans | Integer | Yes | The number of recent analysis records + in Lastline to search through (default is 25) to find a match for the entity. + | + + | Create Insight? | Boolean | No | If set to `True`, the action creates a detailed + insight on the entity in the Google SecOps case containing the score and observed + malicious activities. | + + + ### Additional Notes + + - The action supports `FileHash` entities in **MD5** and **SHA-1** formats only. + + - It always fetches the **latest** available analysis for the provided entity + within the specified search limit. + + - If no previous analysis is found within the search limit, the entity will not + be enriched. + + + ### Flow Description + + 1. **Authentication**: Connects to the Lastline service using the provided API + credentials. + + 2. **Entity Filtering**: Iterates through the target entities, filtering for supported + types (`URL` and `FileHash`). + + 3. **History Search**: For each valid entity, it searches the Lastline analysis + history (up to the defined 'Search in last x scans' limit) to find the most recent + completed task. + + 4. **Result Retrieval**: If a task is found, it fetches the detailed analysis + report using the task's unique identifier (UUID). + + 5. **Internal Mutation (Enrichment)**: + - Updates the entity's `additional_properties` with technical details (timestamps, + hashes, scores). + - Sets the entity as `is_enriched`. + - Compares the analysis score against the `Threshold`; if exceeded, marks + the entity as `is_suspicious`. + 6. **Reporting**: Adds a data table to the case wall and, if configured, creates + a case insight for the entity. + + 7. **Completion**: Updates the entities in the SOAR and returns the raw JSON results. + capabilities: + can_create_case_comments: false + can_create_insight: true + can_modify_alert_data: false + can_mutate_external_data: false + can_mutate_internal_data: true + can_update_entities: true + external_data_mutation_explanation: null + fetches_data: true + internal_data_mutation_explanation: >- + Updates entity attributes with enrichment data, marks entities as suspicious + based on score thresholds, and creates case insights and data tables. + categories: + enrichment: true + entity_usage: + entity_types: + - FILEHASH + - DestinationURL + filters_by_additional_properties: false + filters_by_alert_identifier: false + filters_by_case_identifier: false + filters_by_creation_time: false + filters_by_entity_type: true + filters_by_identifier: false + filters_by_is_artifact: false + filters_by_is_enriched: false + filters_by_is_internal: false + filters_by_is_pivot: false + filters_by_is_suspicious: false + filters_by_is_vulnerable: false + filters_by_modification_time: false +Ping: + ai_description: "### General Description\nThe **Ping** action is a diagnostic tool\ + \ used to verify the connectivity between Google SecOps and the Lastline service.\ + \ It ensures that the provided integration parameters\u2014such as the API Root,\ + \ Username, and Password\u2014are correct and that the Lastline API is reachable\ + \ from the environment.\n\n### Parameters Description\nThis action uses the integration's\ + \ global configuration parameters rather than action-specific inputs:\n\n| Parameter\ + \ | Type | Mandatory | Description |\n| :--- | :--- | :--- | :--- |\n| **Api Root**\ + \ | String | Yes | The base URL of the Lastline API instance. |\n| **Username**\ + \ | String | Yes | The username used for authenticating with the Lastline service.\ + \ |\n| **Password** | String | Yes | The password associated with the provided\ + \ username. |\n| **Verify SSL** | Boolean | No | If enabled (default), the action\ + \ will verify the SSL certificate of the Lastline server. |\n\n### Additional\ + \ Notes\n- This action is typically used during the initial setup of the integration\ + \ or for troubleshooting connection issues.\n- It does not process any entities\ + \ or modify any data within Google SecOps or Lastline.\n\n### Flow Description\n\ + 1. **Initialization**: The action initializes the Siemplify context and retrieves\ + \ the global configuration parameters (API Root, Username, Password, and SSL verification\ + \ setting).\n2. **Manager Setup**: It instantiates the `LastlineManager`, which\ + \ immediately attempts to establish a session by requesting a session cookie from\ + \ the Lastline `/papi/login` endpoint.\n3. **Connectivity Test**: The action calls\ + \ the `test_connectivity` method, which performs a GET request to the login endpoint\ + \ to validate that the credentials are valid and the service is responsive.\n\ + 4. **Result Handling**: \n - If the request is successful, the action returns\ + \ a success message and sets the execution state to completed.\n - If an authentication\ + \ error occurs, it catches the `LastlineAuthenticationException` and reports a\ + \ failure.\n - If any other network or API error occurs, it catches the general\ + \ exception and provides the error details." + capabilities: + can_create_case_comments: false + can_create_insight: false + can_modify_alert_data: false + can_mutate_external_data: false + can_mutate_internal_data: false + can_update_entities: false + external_data_mutation_explanation: null + fetches_data: true + internal_data_mutation_explanation: null + categories: + enrichment: false + entity_usage: + entity_types: [] + filters_by_additional_properties: false + filters_by_alert_identifier: false + filters_by_case_identifier: false + filters_by_creation_time: false + filters_by_entity_type: false + filters_by_identifier: false + filters_by_is_artifact: false + filters_by_is_enriched: false + filters_by_is_internal: false + filters_by_is_pivot: false + filters_by_is_suspicious: false + filters_by_is_vulnerable: false + filters_by_modification_time: false +Search Analysis History: + ai_description: >- + ### General Description + + This action searches the Lastline analysis history for completed tasks based on + user-provided parameters. It allows security analysts to look up previous scan + results for specific URLs or file hashes (MD5/SHA1) within a defined timeframe + and scan limit. Unlike typical enrichment actions, this action does not process + entities from the SOAR case; it operates exclusively on the input parameters provided + at runtime. + + + ### Parameters Description + + | Parameter | Type | Mandatory | Description | + + | :--- | :--- | :--- | :--- | + + | Submission Name | String | No | The specific URL or Filehash (MD5 or SHA1) to + search for in the history. | + + | Submission Type | String | No | Defines the type of the 'Submission Name'. Options + include 'URL', 'FileHash', or 'Not specified'. If 'Not specified', the action + will attempt to auto-detect the type based on the format of the 'Submission Name'. + | + + | Max Hours Backwards | Integer | No | The time window (in hours) to look back + for completed analysis tasks. Defaults to 24 hours. | + + | Search in last x scans | Integer | Yes | The maximum number of recent scans + to search through in Lastline. Defaults to 100. | + + | Skip first x scans | Integer | No | The number of initial scans to skip in the + search results (offset). Defaults to 0. | + + + ### Additional Notes + + - **Entity Usage:** This action does not interact with or iterate over entities + present in the Google SecOps case. It relies entirely on the 'Submission Name' + parameter. + + - **Hash Support:** When searching for files, the action supports both MD5 and + SHA1 hash formats. + + + ### Flow Description + + 1. **Initialization:** The action retrieves API credentials (API Root, Username, + Password) and connection settings from the integration configuration. + + 2. **Parameter Parsing:** It extracts the search criteria, including the submission + name, type, and search constraints (timeframe and limits). + + 3. **Type Detection:** If the 'Submission Type' is not specified, the script uses + validators to determine if the 'Submission Name' is a URL or a file hash. + + 4. **API Query:** The `LastlineManager` executes a GET request to the `/papi/analysis/get_history` + endpoint with the filtered parameters. + + 5. **Result Processing:** The response is parsed into data models. If matching + tasks are found, the action generates a JSON result and populates a 'Search Results' + data table in the SOAR platform. + capabilities: + can_create_case_comments: false + can_create_insight: false + can_modify_alert_data: false + can_mutate_external_data: false + can_mutate_internal_data: false + can_update_entities: false + external_data_mutation_explanation: null + fetches_data: true + internal_data_mutation_explanation: null + categories: + enrichment: false + entity_usage: + entity_types: [] + filters_by_additional_properties: false + filters_by_alert_identifier: false + filters_by_case_identifier: false + filters_by_creation_time: false + filters_by_entity_type: false + filters_by_identifier: false + filters_by_is_artifact: false + filters_by_is_enriched: false + filters_by_is_internal: false + filters_by_is_pivot: false + filters_by_is_suspicious: false + filters_by_is_vulnerable: false + filters_by_modification_time: false +Submit File: + ai_description: >- + Submits a file to the Lastline sandbox for automated analysis and retrieves the + resulting report. This action operates asynchronously, meaning it can submit the + file and then poll for the analysis results until they are ready. Note that this + action does not process Google SecOps entities; instead, it requires a direct + file path provided as an input parameter. + + + ### Parameters + + | Parameter Name | Type | Mandatory | Description | + + | :--- | :--- | :--- | :--- | + + | File Path | String | Yes | The full local path to the file that should be uploaded + to Lastline for analysis. | + + | Wait for the report? | Boolean | No | If set to 'True' (default), the action + will wait for the analysis to complete and fetch the full report. If 'False', + the action completes immediately after the file is successfully submitted. | + + + ### Flow Description + + 1. **Submission**: The action reads the file from the specified path and performs + a POST request to the Lastline API to create a new analysis task. + + 2. **Async Handling**: If 'Wait for the report?' is enabled, the action enters + an 'In Progress' state, storing the task UUID. + + 3. **Status Polling**: In subsequent execution cycles, the action queries the + Lastline API to check the progress of the analysis. + + 4. **Data Retrieval**: Once the analysis is marked as completed, the action fetches + the final results, including scores and observed malicious activities. + + 5. **Output**: The action outputs the raw analysis data as a JSON object and, + if the report was waited for, attaches a CSV data table to the case containing + the summary of the analysis results. + capabilities: + can_create_case_comments: false + can_create_insight: false + can_modify_alert_data: false + can_mutate_external_data: true + can_mutate_internal_data: true + can_update_entities: false + external_data_mutation_explanation: >- + Submits a file to the Lastline platform, which creates a new analysis task and + consumes sandbox resources. + fetches_data: true + internal_data_mutation_explanation: >- + Adds a data table to the Google SecOps case containing the file analysis results. + categories: + enrichment: false + entity_usage: + entity_types: [] + filters_by_additional_properties: false + filters_by_alert_identifier: false + filters_by_case_identifier: false + filters_by_creation_time: false + filters_by_entity_type: false + filters_by_identifier: false + filters_by_is_artifact: false + filters_by_is_enriched: false + filters_by_is_internal: false + filters_by_is_pivot: false + filters_by_is_suspicious: false + filters_by_is_vulnerable: false + filters_by_modification_time: false +Submit URL: + ai_description: >- + ### General Description + + This action submits a specific URL to the Lastline platform for sandbox analysis. + It is designed to initiate a security scan to determine if a URL is malicious + or benign. Unlike many other actions, this does not automatically process entities + from the Google SecOps case; instead, the URL must be provided manually as an + input parameter. The action supports asynchronous execution, meaning it can wait + for the analysis to complete and retrieve the final report. + + + ### Parameters Description + + | Parameter | Type | Mandatory | Description | + + | :--- | :--- | :--- | :--- | + + | URL For Analysis | String | Yes | The specific URL that you want Lastline to + analyze. | + + | Wait for the report? | Boolean | No | If set to `True` (default), the action + will enter an asynchronous state, polling Lastline until the analysis is finished + to retrieve the full report. If `False`, the action completes immediately after + the submission task is created. | + + + ### Additional Notes + + - This action **does not** use Google SecOps entities (e.g., it won't iterate + over URLs found in the alert). You must map or type the URL into the `URL For + Analysis` parameter. + + - If `Wait for the report?` is enabled, the action may take several minutes to + complete as it waits for the sandbox environment to finish its execution. + + + ### Flow Description + + 1. **Submission**: The action sends a POST request to Lastline's `/papi/analysis/submit_url` + endpoint with the provided URL. + + 2. **Initial Check**: If `Wait for the report?` is `False`, the action finishes + immediately, providing the Task UUID. + + 3. **Polling (Async)**: If `Wait for the report?` is `True`, the action checks + if the report is already available. If not, it enters an 'In Progress' state. + + 4. **Status Retrieval**: In subsequent runs (polling), the action calls `/papi/analysis/get_progress` + using the Task UUID to check if the analysis is finished. + + 5. **Finalization**: Once the analysis is complete, the action calls `/papi/analysis/get_result` + to fetch the full report, which is then attached to the action results as JSON + and a Data Table. + capabilities: + can_create_case_comments: false + can_create_insight: false + can_modify_alert_data: false + can_mutate_external_data: true + can_mutate_internal_data: false + can_update_entities: false + external_data_mutation_explanation: >- + The action creates a new analysis task/job on the Lastline platform by submitting + a URL via a POST request. + fetches_data: true + internal_data_mutation_explanation: null + categories: + enrichment: false + entity_usage: + entity_types: [] + filters_by_additional_properties: false + filters_by_alert_identifier: false + filters_by_case_identifier: false + filters_by_creation_time: false + filters_by_entity_type: false + filters_by_identifier: false + filters_by_is_artifact: false + filters_by_is_enriched: false + filters_by_is_internal: false + filters_by_is_pivot: false + filters_by_is_suspicious: false + filters_by_is_vulnerable: false + filters_by_modification_time: false diff --git a/content/response_integrations/google/lastline/resources/get_analysis_results_JsonResult_example.json b/content/response_integrations/google/lastline/resources/get_analysis_results_JsonResult_example.json new file mode 100644 index 000000000..f8cd7d98f --- /dev/null +++ b/content/response_integrations/google/lastline/resources/get_analysis_results_JsonResult_example.json @@ -0,0 +1,48 @@ +[ + { + "Entity": "https://xxxxx.com", + "EntityResult": { + "success": 1, + "data": { + "submission": "2021-03-22 07:47:17", + "expires": "2021-03-24 07:47:17", + "task_uuid": "123456", + "reports": [ + { + "relevance": 1.0, + "report_uuid": "123456789", + "report_versions": [ + "ll-web" + ], + "description": "Dynamic analysis in instrumented Chrome browser" + }, + { + "relevance": 1.0, + "report_uuid": "123456-123456", + "report_versions": [ + "ll-pcap" + ], + "description": "Pcap analysis" + } + ], + "child_tasks": [ + { + "task_uuid": "123456", + "score": 0, + "tag": "network traffic analysis", + "parent_report_uuid": "123456" + } + ], + "score": 0, + "malicious_activity": [ + "Classification: Trusted URL", + "Info: A whitelisted Domain / URL was visited" + ], + "analysis_subject": { + "url": "https://google.com" + }, + "last_submission_timestamp": "2021-03-24 07:42:52" + } + } + } +] \ No newline at end of file diff --git a/content/response_integrations/google/lastline/resources/image.png b/content/response_integrations/google/lastline/resources/image.png new file mode 100644 index 000000000..7da17f8f0 Binary files /dev/null and b/content/response_integrations/google/lastline/resources/image.png differ diff --git a/content/response_integrations/google/lastline/resources/logo.svg b/content/response_integrations/google/lastline/resources/logo.svg new file mode 100644 index 000000000..ff288ac6e --- /dev/null +++ b/content/response_integrations/google/lastline/resources/logo.svg @@ -0,0 +1,6 @@ + diff --git a/content/response_integrations/google/lastline/resources/search_analysis_history_JsonResult_example.json b/content/response_integrations/google/lastline/resources/search_analysis_history_JsonResult_example.json new file mode 100644 index 000000000..7fda046d7 --- /dev/null +++ b/content/response_integrations/google/lastline/resources/search_analysis_history_JsonResult_example.json @@ -0,0 +1,18 @@ +{ + "success": 1, + "data": [ + { + "username": "XXXX@siemplify.co", + "status": "finished", + "task_subject_filename": null, + "task_subject_sha1": null, + "task_uuid": "123456789", + "task_subject_md5": null, + "task_subject_url": "https://google.com", + "task_start_time": "2021-03-24 07:42:52", + "analysis_history_id": 123456, + "title": null, + "score": 0 + } + ] +} \ No newline at end of file diff --git a/content/response_integrations/google/lastline/resources/submit_file_JsonResult_example.json b/content/response_integrations/google/lastline/resources/submit_file_JsonResult_example.json new file mode 100644 index 000000000..e9519477e --- /dev/null +++ b/content/response_integrations/google/lastline/resources/submit_file_JsonResult_example.json @@ -0,0 +1,28 @@ +{ + "success": 1, + "data": { + "submission": "2021-04-01 09:43:18", + "expires": "2021-04-03 09:43:18", + "task_uuid": "1234", + "reports": [ + { + "relevance": 1.0, + "report_uuid": "1234", + "report_versions": [ + "ll-web" + ], + "description": "Dynamic analysis in simulated file-viewer" + } + ], + "submission_timestamp": "2021-04-01 11:36:20", + "child_tasks": [], + "score": 0, + "analysis_subject": { + "sha256": "12314", + "sha1": "123123123", + "mime_type": "image/svg", + "md5": "123123123" + }, + "last_submission_timestamp": "2021-04-01 11:36:20" + } +} \ No newline at end of file diff --git a/content/response_integrations/google/lastline/resources/submit_url_JsonResult_example.json b/content/response_integrations/google/lastline/resources/submit_url_JsonResult_example.json new file mode 100644 index 000000000..46a732e51 --- /dev/null +++ b/content/response_integrations/google/lastline/resources/submit_url_JsonResult_example.json @@ -0,0 +1,40 @@ +{ + "success": 1, + "data": { + "submission": "2021-03-22 09:00:16", + "expires": "2021-03-24 09:00:16", + "task_uuid": "123456", + "reports": [ + { + "relevance": 1.0, + "report_uuid": "123456", + "report_versions": [ + "ll-web" + ], + "description": "Dynamic analysis in instrumented Chrome browser" + }, + { + "relevance": 1.0, + "report_uuid": "123456-v", + "report_versions": [ + "ll-pcap" + ], + "description": "Pcap analysis" + } + ], + "submission_timestamp": "2021-03-22 09:01:42", + "child_tasks": [ + { + "task_uuid": "123456", + "score": 0, + "tag": "network traffic analysis", + "parent_report_uuid": "123456" + } + ], + "score": 0, + "analysis_subject": { + "url": "https://www.sport1.com" + }, + "last_submission_timestamp": "2021-03-22 09:01:42" + } +} \ No newline at end of file diff --git a/content/response_integrations/google/lastline/tests/__init__.py b/content/response_integrations/google/lastline/tests/__init__.py new file mode 100644 index 000000000..9f71a2dc3 --- /dev/null +++ b/content/response_integrations/google/lastline/tests/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/content/response_integrations/google/lastline/tests/common.py b/content/response_integrations/google/lastline/tests/common.py new file mode 100644 index 000000000..cb340e5ef --- /dev/null +++ b/content/response_integrations/google/lastline/tests/common.py @@ -0,0 +1,21 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import pathlib +import json +from integration_testing.common import get_def_file_content +INTEGRATION_PATH: pathlib.Path = pathlib.Path(__file__).parent.parent +CONFIG_PATH: pathlib.Path = pathlib.Path(__file__).parent / 'config.json' +CONFIG: dict = get_def_file_content(CONFIG_PATH) if CONFIG_PATH.exists() else {} \ No newline at end of file diff --git a/content/response_integrations/google/lastline/tests/config.json b/content/response_integrations/google/lastline/tests/config.json new file mode 100644 index 000000000..d214219ce --- /dev/null +++ b/content/response_integrations/google/lastline/tests/config.json @@ -0,0 +1,6 @@ +{ + "Api Root": "https://user.lastline.com", + "Username": "", + "Password": "", + "Verify SSL": true +} \ No newline at end of file diff --git a/content/response_integrations/google/lastline/tests/test_defaults/__init__.py b/content/response_integrations/google/lastline/tests/test_defaults/__init__.py new file mode 100644 index 000000000..9f71a2dc3 --- /dev/null +++ b/content/response_integrations/google/lastline/tests/test_defaults/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/content/response_integrations/google/lastline/tests/test_defaults/test_imports.py b/content/response_integrations/google/lastline/tests/test_defaults/test_imports.py new file mode 100644 index 000000000..1243de833 --- /dev/null +++ b/content/response_integrations/google/lastline/tests/test_defaults/test_imports.py @@ -0,0 +1,57 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import importlib +import pathlib + +from .. import common + + +VALID_SUFFIXES = (".py",) + + +def import_all_integration_modules(integration: pathlib.Path) -> None: + if not integration.exists(): + msg: str = f"Cannot find integration {integration.name}" + raise AssertionError(msg) + + imports: list[str] = _get_integration_modules_import_strings(integration) + for import_ in imports: + importlib.import_module(import_) + + +def _get_integration_modules_import_strings(integration: pathlib.Path) -> list[str]: + results: list[str] = [] + for package in integration.iterdir(): + if not package.is_dir(): + continue + + for module in package.iterdir(): + if not module.is_file() or module.suffix not in VALID_SUFFIXES: + continue + + import_: str = _get_import_string(integration.stem, package.stem, module.stem) + results.append(import_) + + return results + + +def _get_import_string(integration: str, package: str, module: str) -> str: + return f"{integration}.{package}.{module}" + + +def test_imports() -> None: + import_all_integration_modules(common.INTEGRATION_PATH) diff --git a/content/response_integrations/google/lastline/uv.lock b/content/response_integrations/google/lastline/uv.lock new file mode 100644 index 000000000..9d8c5083c --- /dev/null +++ b/content/response_integrations/google/lastline/uv.lock @@ -0,0 +1,590 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[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 = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[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" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/52/505c207f334d51e937cbaa27ff95776e16e2d120e13cbe491cd7b3a70b50/chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", size = 870747, upload-time = "2026-04-13T21:32:56.916Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", size = 853210, upload-time = "2026-04-13T21:32:58.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/99/f6a822ad1bde25a4c38dc3e770485e78e0893dfd871cd6e18ed3ea3a795e/chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", size = 873625, upload-time = "2026-04-13T21:32:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", size = 883436, upload-time = "2026-04-13T21:33:01.351Z" }, + { url = "https://files.pythonhosted.org/packages/6c/63/0f43e3acf2c436fdb32a0f904aeb03a2904d2126eed34a042a194d235926/chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", size = 876589, upload-time = "2026-04-13T21:33:02.636Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", size = 941866, upload-time = "2026-04-13T21:33:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[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.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "google-auth" +version = "2.49.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, +] + +[[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 = "lastline" +version = "10.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, + { name = "tipcommon" }, + { name = "validators" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-json-report" }, + { name = "soar-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "requests", specifier = "==2.32.4" }, + { name = "tipcommon", path = "../../../../../packages/tipcommon/whls/TIPCommon-1.0.10-py3-none-any.whl" }, + { name = "validators", specifier = "==0.33.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-json-report", specifier = ">=1.5.0" }, + { name = "soar-sdk", git = "https://github.com/chronicle/soar-sdk.git" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[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 = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[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 = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyopenssl" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[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 = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[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 = "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.git#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 = "1.0.10" +source = { path = "../../../../../packages/tipcommon/whls/TIPCommon-1.0.10-py3-none-any.whl" } +dependencies = [ + { name = "chardet" }, + { name = "requests" }, +] +wheels = [ + { filename = "tipcommon-1.0.10-py3-none-any.whl", hash = "sha256:da0469b727d65b51bff0c43dce28f5c0c0463530d9bb2aab0bc8047c5cca1ea0" }, +] + +[package.metadata] +requires-dist = [ + { name = "chardet" }, + { 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 = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[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 = "validators" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/af/5ad4fed95276e3eb7628d858c88cd205799bcad847e46223760a3129cbb1/validators-0.33.0.tar.gz", hash = "sha256:535867e9617f0100e676a1257ba1e206b9bfd847ddc171e4d44811f07ff0bfbf", size = 70741, upload-time = "2024-07-15T02:42:04.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/22/91b4bd36df27e651daedd93d03d5d3bb6029fdb0b55494e45ee46c36c570/validators-0.33.0-py3-none-any.whl", hash = "sha256:134b586a98894f8139865953899fc2daeb3d0c35569552c5518f089ae43ed075", size = 43298, upload-time = "2024-07-15T02:42:00.5Z" }, +] diff --git a/content/response_integrations/google/lastline/widgets/GetAnalysisResults.html b/content/response_integrations/google/lastline/widgets/GetAnalysisResults.html new file mode 100644 index 000000000..08e5d500c --- /dev/null +++ b/content/response_integrations/google/lastline/widgets/GetAnalysisResults.html @@ -0,0 +1,1153 @@ + + + + +
+ + + + +