From 2002aa284576c4a65594ddc5fc726262d13dd122 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 20:24:09 +0000 Subject: [PATCH 01/46] feat: Implement marketplace integrations functionality --- src/secops/chronicle/client.py | 156 ++++++++++++++ .../chronicle/marketplace_integrations.py | 199 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 src/secops/chronicle/marketplace_integrations.py diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 2795f8bc..d493d001 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -333,6 +333,13 @@ create_watchlist as _create_watchlist, update_watchlist as _update_watchlist, ) +from secops.chronicle.marketplace_integrations import ( + list_marketplace_integrations as _list_marketplace_integrations, + get_marketplace_integration as _get_marketplace_integration, + get_marketplace_integration_diff as _get_marketplace_integration_diff, + install_marketplace_integration as _install_marketplace_integration, + uninstall_marketplace_integration as _uninstall_marketplace_integration, +) from secops.exceptions import SecOpsError @@ -760,6 +767,155 @@ def update_watchlist( update_mask, ) + def list_marketplace_integrations( + self, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """Get a list of all marketplace integrations. + + Args: + page_size: Maximum number of integrations to return per page + page_token: Token for the next page of results, if available + filter_string: Filter expression to filter marketplace integrations + order_by: Field to sort the marketplace integrations by + api_version: API version to use. Defaults to V1BETA + as_list: If True, return a list of integrations instead of a dict + with integrations list and nextPageToken. + + Returns: + If as_list is True: List of marketplace integrations. + If as_list is False: Dict with marketplace integrations list and + nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_marketplace_integrations( + self, + page_size, + page_token, + filter_string, + order_by, + api_version, + as_list + ) + + def get_marketplace_integration( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a specific marketplace integration by integration name. + + Args: + integration_name: name of the marketplace integration to retrieve + api_version: API version to use. Defaults to V1BETA + + Returns: + Marketplace integration details + + Raises: + APIError: If the API request fails + """ + return _get_marketplace_integration( + self, + integration_name, + api_version + ) + + def get_marketplace_integration_diff( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get the differences between the currently installed version of + an integration and the commercial version available in the + marketplace. + + Args: + integration_name: name of the marketplace integration + api_version: API version to use. Defaults to V1BETA + + Returns: + Marketplace integration diff details + + Raises: + APIError: If the API request fails + """ + return _get_marketplace_integration_diff( + self, + integration_name, + api_version + ) + + def install_marketplace_integration( + self, + integration_name: str, + override_mapping: bool | None = None, + staging: bool | None = None, + version: str | None = None, + restore_from_snapshot: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Install a marketplace integration by integration name + + Args: + integration_name: Name of the marketplace integration to install + override_mapping: Optional. Determines if the integration should + override the ontology if already installed, if not provided, set to + false by default. + staging: Optional. Determines if the integration should be installed + as staging or production, if not provided, installed as production. + version: Optional. Determines which version of the integration + should be installed. + restore_from_snapshot: Optional. Determines if the integration + should be installed from existing integration snapshot. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Installed marketplace integration details + + Raises: + APIError: If the API request fails + """ + return _install_marketplace_integration( + self, + integration_name, + override_mapping, + staging, + version, + restore_from_snapshot, + api_version + ) + + def uninstall_marketplace_integration( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Uninstall a marketplace integration by integration name + + Args: + integration_name: Name of the marketplace integration to uninstall + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Uninstalled marketplace integration details + + Raises: + APIError: If the API request fails + """ + return _uninstall_marketplace_integration( + self, + integration_name, + api_version + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/marketplace_integrations.py b/src/secops/chronicle/marketplace_integrations.py new file mode 100644 index 00000000..28a7b90f --- /dev/null +++ b/src/secops/chronicle/marketplace_integrations.py @@ -0,0 +1,199 @@ +# Copyright 2025 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. +# +"""Watchlist functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_marketplace_integrations( + client: "ChronicleClient", + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """Get a list of marketplace integrations. + + Args: + client: ChronicleClient instance + page_size: Number of results to return per page + page_token: Token for the page to retrieve + filter_string: Filter expression to filter marketplace integrations + order_by: Field to sort the marketplace integrations by + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of marketplace integrations instead + of a dict with marketplace integrations list and nextPageToken. + + Returns: + If as_list is True: List of marketplace integrations. + If as_list is False: Dict with marketplace integrations list and + nextPageToken. + + Raises: + APIError: If the API request fails + """ + field_map = { + "filter": filter_string, + "orderBy": order_by, + } + + return chronicle_paginated_request( + client, + api_version=api_version, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=page_size, + page_token=page_token, + extra_params={k: v for k, v in field_map.items() if v is not None}, + as_list=as_list, + ) + + +def get_marketplace_integration( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a marketplace integration by integration name + + Args: + client: ChronicleClient instance + integration_name: Name of the marketplace integration to retrieve + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Marketplace integration details + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"marketplaceIntegrations/{integration_name}", + api_version=api_version, + ) + + +def get_marketplace_integration_diff( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get the differences between the currently installed version of + an integration and the commercial version available in the marketplace. + + Args: + client: ChronicleClient instance + integration_name: Name of the marketplace integration to compare + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Marketplace integration diff details + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"marketplaceIntegrations/{integration_name}" + f":fetchCommercialDiff", + api_version=api_version, + ) + + +def install_marketplace_integration( + client: "ChronicleClient", + integration_name: str, + override_mapping: bool | None = None, + staging: bool | None = None, + version: str | None = None, + restore_from_snapshot: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Install a marketplace integration by integration name + + Args: + client: ChronicleClient instance + integration_name: Name of the marketplace integration to install + override_mapping: Optional. Determines if the integration should + override the ontology if already installed, if not provided, set to + false by default. + staging: Optional. Determines if the integration should be installed + as staging or production, if not provided, installed as production. + version: Optional. Determines which version of the integration + should be installed. + restore_from_snapshot: Optional. Determines if the integration + should be installed from existing integration snapshot. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Installed marketplace integration details + + Raises: + APIError: If the API request fails + """ + field_map = { + "overrideMapping": override_mapping, + "staging": staging, + "version": version, + "restoreFromSnapshot": restore_from_snapshot, + } + + return chronicle_request( + client, + method="POST", + endpoint_path=f"marketplaceIntegrations/{integration_name}:install", + json={k: v for k, v in field_map.items() if v is not None}, + api_version=api_version, + ) + + +def uninstall_marketplace_integration( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Uninstall a marketplace integration by integration name + + Args: + client: ChronicleClient instance + integration_name: Name of the marketplace integration to uninstall + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Uninstalled marketplace integration details + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="POST", + endpoint_path=f"marketplaceIntegrations/{integration_name}:uninstall", + api_version=api_version, + ) From 182c2bfeabe7905b988b07b0dff90b997cb5b28f Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 20:37:11 +0000 Subject: [PATCH 02/46] chore: linting and tidying up imports --- src/secops/chronicle/client.py | 328 ++++++++++++--------------------- 1 file changed, 116 insertions(+), 212 deletions(-) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index d493d001..a70c0d9d 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -22,140 +22,119 @@ from google.auth.transport import requests as google_auth_requests +#pylint: disable=line-too-long from secops import auth as secops_auth from secops.auth import RetryConfig from secops.chronicle.alert import get_alerts as _get_alerts from secops.chronicle.case import get_cases_from_list -from secops.chronicle.dashboard import DashboardAccessType, DashboardView -from secops.chronicle.dashboard import add_chart as _add_chart -from secops.chronicle.dashboard import create_dashboard as _create_dashboard -from secops.chronicle.dashboard import delete_dashboard as _delete_dashboard from secops.chronicle.dashboard import ( + DashboardAccessType, + DashboardView, + add_chart as _add_chart, + create_dashboard as _create_dashboard, + delete_dashboard as _delete_dashboard, duplicate_dashboard as _duplicate_dashboard, + edit_chart as _edit_chart, + export_dashboard as _export_dashboard, + get_chart as _get_chart, + get_dashboard as _get_dashboard, + import_dashboard as _import_dashboard, + list_dashboards as _list_dashboards, + remove_chart as _remove_chart, + update_dashboard as _update_dashboard, ) -from secops.chronicle.dashboard import edit_chart as _edit_chart -from secops.chronicle.dashboard import export_dashboard as _export_dashboard -from secops.chronicle.dashboard import get_chart as _get_chart -from secops.chronicle.dashboard import get_dashboard as _get_dashboard -from secops.chronicle.dashboard import import_dashboard as _import_dashboard -from secops.chronicle.dashboard import list_dashboards as _list_dashboards -from secops.chronicle.dashboard import remove_chart as _remove_chart -from secops.chronicle.dashboard import update_dashboard as _update_dashboard from secops.chronicle.dashboard_query import ( execute_query as _execute_dashboard_query, -) -from secops.chronicle.dashboard_query import ( get_execute_query as _get_execute_query, ) from secops.chronicle.data_export import ( cancel_data_export as _cancel_data_export, -) -from secops.chronicle.data_export import ( create_data_export as _create_data_export, -) -from secops.chronicle.data_export import ( fetch_available_log_types as _fetch_available_log_types, -) -from secops.chronicle.data_export import get_data_export as _get_data_export -from secops.chronicle.data_export import list_data_export as _list_data_export -from secops.chronicle.data_export import ( + get_data_export as _get_data_export, + list_data_export as _list_data_export, update_data_export as _update_data_export, ) -from secops.chronicle.data_table import DataTableColumnType -from secops.chronicle.data_table import create_data_table as _create_data_table from secops.chronicle.data_table import ( + DataTableColumnType, + create_data_table as _create_data_table, create_data_table_rows as _create_data_table_rows, -) -from secops.chronicle.data_table import delete_data_table as _delete_data_table -from secops.chronicle.data_table import ( + delete_data_table as _delete_data_table, delete_data_table_rows as _delete_data_table_rows, -) -from secops.chronicle.data_table import get_data_table as _get_data_table -from secops.chronicle.data_table import ( + get_data_table as _get_data_table, list_data_table_rows as _list_data_table_rows, -) -from secops.chronicle.data_table import list_data_tables as _list_data_tables -from secops.chronicle.data_table import ( + list_data_tables as _list_data_tables, replace_data_table_rows as _replace_data_table_rows, -) -from secops.chronicle.data_table import update_data_table as _update_data_table -from secops.chronicle.data_table import ( + update_data_table as _update_data_table, update_data_table_rows as _update_data_table_rows, ) -from secops.chronicle.entity import _detect_value_type_for_query -from secops.chronicle.entity import summarize_entity as _summarize_entity -from secops.chronicle.feeds import CreateFeedModel, UpdateFeedModel -from secops.chronicle.feeds import create_feed as _create_feed -from secops.chronicle.feeds import delete_feed as _delete_feed -from secops.chronicle.feeds import disable_feed as _disable_feed -from secops.chronicle.feeds import enable_feed as _enable_feed -from secops.chronicle.feeds import generate_secret as _generate_secret -from secops.chronicle.feeds import get_feed as _get_feed -from secops.chronicle.feeds import list_feeds as _list_feeds -from secops.chronicle.feeds import update_feed as _update_feed -from secops.chronicle.gemini import GeminiResponse -from secops.chronicle.gemini import opt_in_to_gemini as _opt_in_to_gemini -from secops.chronicle.gemini import query_gemini as _query_gemini -from secops.chronicle.ioc import list_iocs as _list_iocs -from secops.chronicle.investigations import ( - fetch_associated_investigations as _fetch_associated_investigations, +from secops.chronicle.entity import ( + _detect_value_type_for_query, + summarize_entity as _summarize_entity, ) -from secops.chronicle.investigations import ( - get_investigation as _get_investigation, +from secops.chronicle.featured_content_rules import ( + list_featured_content_rules as _list_featured_content_rules, ) -from secops.chronicle.investigations import ( - list_investigations as _list_investigations, +from secops.chronicle.feeds import ( + CreateFeedModel, + UpdateFeedModel, + create_feed as _create_feed, + delete_feed as _delete_feed, + disable_feed as _disable_feed, + enable_feed as _enable_feed, + generate_secret as _generate_secret, + get_feed as _get_feed, + list_feeds as _list_feeds, + update_feed as _update_feed, +) +from secops.chronicle.gemini import ( + GeminiResponse, + opt_in_to_gemini as _opt_in_to_gemini, + query_gemini as _query_gemini, ) from secops.chronicle.investigations import ( + fetch_associated_investigations as _fetch_associated_investigations, + get_investigation as _get_investigation, + list_investigations as _list_investigations, trigger_investigation as _trigger_investigation, ) -from secops.chronicle.log_ingest import create_forwarder as _create_forwarder -from secops.chronicle.log_ingest import delete_forwarder as _delete_forwarder -from secops.chronicle.log_ingest import get_forwarder as _get_forwarder +from secops.chronicle.ioc import list_iocs as _list_iocs from secops.chronicle.log_ingest import ( + create_forwarder as _create_forwarder, + delete_forwarder as _delete_forwarder, + get_forwarder as _get_forwarder, get_or_create_forwarder as _get_or_create_forwarder, + import_entities as _import_entities, + ingest_log as _ingest_log, + ingest_udm as _ingest_udm, + list_forwarders as _list_forwarders, + update_forwarder as _update_forwarder, ) -from secops.chronicle.log_ingest import import_entities as _import_entities -from secops.chronicle.log_ingest import ingest_log as _ingest_log -from secops.chronicle.log_ingest import ingest_udm as _ingest_udm -from secops.chronicle.log_ingest import list_forwarders as _list_forwarders -from secops.chronicle.log_ingest import update_forwarder as _update_forwarder -from secops.chronicle.log_types import classify_logs as _classify_logs -from secops.chronicle.log_types import get_all_log_types as _get_all_log_types -from secops.chronicle.log_types import ( - get_log_type_description as _get_log_type_description, -) -from secops.chronicle.log_types import is_valid_log_type as _is_valid_log_type -from secops.chronicle.log_types import search_log_types as _search_log_types from secops.chronicle.log_processing_pipelines import ( associate_streams as _associate_streams, -) -from secops.chronicle.log_processing_pipelines import ( create_log_processing_pipeline as _create_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( delete_log_processing_pipeline as _delete_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( dissociate_streams as _dissociate_streams, -) -from secops.chronicle.log_processing_pipelines import ( fetch_associated_pipeline as _fetch_associated_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( fetch_sample_logs_by_streams as _fetch_sample_logs_by_streams, -) -from secops.chronicle.log_processing_pipelines import ( get_log_processing_pipeline as _get_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( list_log_processing_pipelines as _list_log_processing_pipelines, -) -from secops.chronicle.log_processing_pipelines import ( + test_pipeline as _test_pipeline, update_log_processing_pipeline as _update_log_processing_pipeline, ) -from secops.chronicle.log_processing_pipelines import ( - test_pipeline as _test_pipeline, +from secops.chronicle.log_types import ( + classify_logs as _classify_logs, + get_all_log_types as _get_all_log_types, + get_log_type_description as _get_log_type_description, + is_valid_log_type as _is_valid_log_type, + search_log_types as _search_log_types, +) +from secops.chronicle.marketplace_integrations import ( + get_marketplace_integration as _get_marketplace_integration, + get_marketplace_integration_diff as _get_marketplace_integration_diff, + install_marketplace_integration as _install_marketplace_integration, + list_marketplace_integrations as _list_marketplace_integrations, + uninstall_marketplace_integration as _uninstall_marketplace_integration, ) from secops.chronicle.models import ( APIVersion, @@ -166,102 +145,70 @@ InputInterval, TileType, ) -from secops.chronicle.nl_search import nl_search as _nl_search -from secops.chronicle.nl_search import translate_nl_to_udm -from secops.chronicle.parser import activate_parser as _activate_parser +from secops.chronicle.nl_search import ( + nl_search as _nl_search, + translate_nl_to_udm, +) from secops.chronicle.parser import ( + activate_parser as _activate_parser, activate_release_candidate_parser as _activate_release_candidate_parser, + copy_parser as _copy_parser, + create_parser as _create_parser, + deactivate_parser as _deactivate_parser, + delete_parser as _delete_parser, + get_parser as _get_parser, + list_parsers as _list_parsers, + run_parser as _run_parser, ) -from secops.chronicle.parser import copy_parser as _copy_parser -from secops.chronicle.parser import create_parser as _create_parser -from secops.chronicle.parser import deactivate_parser as _deactivate_parser -from secops.chronicle.parser import delete_parser as _delete_parser -from secops.chronicle.parser import get_parser as _get_parser -from secops.chronicle.parser import list_parsers as _list_parsers -from secops.chronicle.parser import run_parser as _run_parser -from secops.chronicle.parser_extension import ParserExtensionConfig from secops.chronicle.parser_extension import ( + ParserExtensionConfig, activate_parser_extension as _activate_parser_extension, -) -from secops.chronicle.parser_extension import ( create_parser_extension as _create_parser_extension, -) -from secops.chronicle.parser_extension import ( delete_parser_extension as _delete_parser_extension, -) -from secops.chronicle.parser_extension import ( get_parser_extension as _get_parser_extension, -) -from secops.chronicle.parser_extension import ( list_parser_extensions as _list_parser_extensions, ) from secops.chronicle.reference_list import ( ReferenceListSyntaxType, ReferenceListView, -) -from secops.chronicle.reference_list import ( create_reference_list as _create_reference_list, -) -from secops.chronicle.reference_list import ( get_reference_list as _get_reference_list, -) -from secops.chronicle.reference_list import ( list_reference_lists as _list_reference_lists, -) -from secops.chronicle.reference_list import ( update_reference_list as _update_reference_list, ) - -# Import rule functions -from secops.chronicle.rule import create_rule as _create_rule -from secops.chronicle.rule import delete_rule as _delete_rule -from secops.chronicle.rule import enable_rule as _enable_rule -from secops.chronicle.rule import get_rule as _get_rule -from secops.chronicle.rule import get_rule_deployment as _get_rule_deployment from secops.chronicle.rule import ( + create_rule as _create_rule, + delete_rule as _delete_rule, + enable_rule as _enable_rule, + get_rule as _get_rule, + get_rule_deployment as _get_rule_deployment, list_rule_deployments as _list_rule_deployments, -) -from secops.chronicle.rule import list_rules as _list_rules -from secops.chronicle.rule import run_rule_test -from secops.chronicle.rule import search_rules as _search_rules -from secops.chronicle.rule import set_rule_alerting as _set_rule_alerting -from secops.chronicle.rule import update_rule as _update_rule -from secops.chronicle.rule import ( + list_rules as _list_rules, + run_rule_test, + search_rules as _search_rules, + set_rule_alerting as _set_rule_alerting, + update_rule as _update_rule, update_rule_deployment as _update_rule_deployment, ) from secops.chronicle.rule_alert import ( bulk_update_alerts as _bulk_update_alerts, -) -from secops.chronicle.rule_alert import get_alert as _get_alert -from secops.chronicle.rule_alert import ( + get_alert as _get_alert, search_rule_alerts as _search_rule_alerts, + update_alert as _update_alert, +) +from secops.chronicle.rule_detection import ( + list_detections as _list_detections, + list_errors as _list_errors, ) -from secops.chronicle.rule_alert import update_alert as _update_alert -from secops.chronicle.rule_detection import list_detections as _list_detections -from secops.chronicle.rule_detection import list_errors as _list_errors from secops.chronicle.rule_exclusion import ( RuleExclusionType, UpdateRuleDeployment, -) -from secops.chronicle.rule_exclusion import ( compute_rule_exclusion_activity as _compute_rule_exclusion_activity, -) -from secops.chronicle.rule_exclusion import ( create_rule_exclusion as _create_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( get_rule_exclusion as _get_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( get_rule_exclusion_deployment as _get_rule_exclusion_deployment, -) -from secops.chronicle.rule_exclusion import ( list_rule_exclusions as _list_rule_exclusions, -) -from secops.chronicle.rule_exclusion import ( patch_rule_exclusion as _patch_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( update_rule_exclusion_deployment as _update_rule_exclusion_deployment, ) from secops.chronicle.rule_retrohunt import ( @@ -270,78 +217,42 @@ list_retrohunts as _list_retrohunts, ) from secops.chronicle.rule_set import ( - batch_update_curated_rule_set_deployments as _batch_update_curated_rule_set_deployments, # pylint: disable=line-too-long -) -from secops.chronicle.rule_set import get_curated_rule as _get_curated_rule -from secops.chronicle.rule_set import ( + batch_update_curated_rule_set_deployments as _batch_update_curated_rule_set_deployments, + get_curated_rule as _get_curated_rule, get_curated_rule_by_name as _get_curated_rule_by_name, -) -from secops.chronicle.rule_set import ( get_curated_rule_set as _get_curated_rule_set, -) -from secops.chronicle.rule_set import ( get_curated_rule_set_category as _get_curated_rule_set_category, -) -from secops.chronicle.rule_set import ( get_curated_rule_set_deployment as _get_curated_rule_set_deployment, -) -from secops.chronicle.rule_set import ( - get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, # pylint: disable=line-too-long -) -from secops.chronicle.rule_set import ( + get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, list_curated_rule_set_categories as _list_curated_rule_set_categories, -) -from secops.chronicle.rule_set import ( list_curated_rule_set_deployments as _list_curated_rule_set_deployments, -) -from secops.chronicle.rule_set import ( list_curated_rule_sets as _list_curated_rule_sets, -) -from secops.chronicle.rule_set import list_curated_rules as _list_curated_rules -from secops.chronicle.rule_set import ( + list_curated_rules as _list_curated_rules, search_curated_detections as _search_curated_detections, -) -from secops.chronicle.rule_set import ( update_curated_rule_set_deployment as _update_curated_rule_set_deployment, ) -from secops.chronicle.featured_content_rules import ( - list_featured_content_rules as _list_featured_content_rules, -) from secops.chronicle.rule_validation import validate_rule as _validate_rule from secops.chronicle.search import search_udm as _search_udm from secops.chronicle.stats import get_stats as _get_stats -from secops.chronicle.udm_mapping import RowLogFormat from secops.chronicle.udm_mapping import ( + RowLogFormat, generate_udm_key_value_mappings as _generate_udm_key_value_mappings, ) - -# Import functions from the new modules from secops.chronicle.udm_search import ( fetch_udm_search_csv as _fetch_udm_search_csv, -) -from secops.chronicle.udm_search import ( fetch_udm_search_view as _fetch_udm_search_view, -) -from secops.chronicle.udm_search import ( find_udm_field_values as _find_udm_field_values, ) from secops.chronicle.validate import validate_query as _validate_query from secops.chronicle.watchlist import ( - list_watchlists as _list_watchlists, - get_watchlist as _get_watchlist, - delete_watchlist as _delete_watchlist, create_watchlist as _create_watchlist, + delete_watchlist as _delete_watchlist, + get_watchlist as _get_watchlist, + list_watchlists as _list_watchlists, update_watchlist as _update_watchlist, ) -from secops.chronicle.marketplace_integrations import ( - list_marketplace_integrations as _list_marketplace_integrations, - get_marketplace_integration as _get_marketplace_integration, - get_marketplace_integration_diff as _get_marketplace_integration_diff, - install_marketplace_integration as _install_marketplace_integration, - uninstall_marketplace_integration as _uninstall_marketplace_integration, -) from secops.exceptions import SecOpsError - +#pylint: enable=line-too-long class ValueType(Enum): """Chronicle API value types.""" @@ -802,7 +713,7 @@ def list_marketplace_integrations( filter_string, order_by, api_version, - as_list + as_list, ) def get_marketplace_integration( @@ -822,11 +733,7 @@ def get_marketplace_integration( Raises: APIError: If the API request fails """ - return _get_marketplace_integration( - self, - integration_name, - api_version - ) + return _get_marketplace_integration(self, integration_name, api_version) def get_marketplace_integration_diff( self, @@ -848,9 +755,7 @@ def get_marketplace_integration_diff( APIError: If the API request fails """ return _get_marketplace_integration_diff( - self, - integration_name, - api_version + self, integration_name, api_version ) def install_marketplace_integration( @@ -867,10 +772,11 @@ def install_marketplace_integration( Args: integration_name: Name of the marketplace integration to install override_mapping: Optional. Determines if the integration should - override the ontology if already installed, if not provided, set to - false by default. + override the ontology if already installed, if not provided, + set to false by default. staging: Optional. Determines if the integration should be installed - as staging or production, if not provided, installed as production. + as staging or production, + if not provided, installed as production. version: Optional. Determines which version of the integration should be installed. restore_from_snapshot: Optional. Determines if the integration @@ -890,7 +796,7 @@ def install_marketplace_integration( staging, version, restore_from_snapshot, - api_version + api_version, ) def uninstall_marketplace_integration( @@ -911,9 +817,7 @@ def uninstall_marketplace_integration( APIError: If the API request fails """ return _uninstall_marketplace_integration( - self, - integration_name, - api_version + self, integration_name, api_version ) def get_stats( From da677e7916f994218c1e270346521aa2fb43cad6 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 20:43:18 +0000 Subject: [PATCH 03/46] chore: update docstring --- src/secops/chronicle/client.py | 2 +- src/secops/chronicle/marketplace_integrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index a70c0d9d..73226266 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -811,7 +811,7 @@ def uninstall_marketplace_integration( api_version: API version to use for the request. Default is V1BETA. Returns: - Uninstalled marketplace integration details + Empty dictionary if uninstallation is successful Raises: APIError: If the API request fails diff --git a/src/secops/chronicle/marketplace_integrations.py b/src/secops/chronicle/marketplace_integrations.py index 28a7b90f..0fb02932 100644 --- a/src/secops/chronicle/marketplace_integrations.py +++ b/src/secops/chronicle/marketplace_integrations.py @@ -186,7 +186,7 @@ def uninstall_marketplace_integration( api_version: API version to use for the request. Default is V1BETA. Returns: - Uninstalled marketplace integration details + Empty dictionary if uninstallation is successful Raises: APIError: If the API request fails From a7744134ee89dc19c3332575f8833cd996bac6ae Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 20:55:14 +0000 Subject: [PATCH 04/46] feat: added tests for marketplace integrations --- .../test_marketplace_integrations.py | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 tests/chronicle/test_marketplace_integrations.py diff --git a/tests/chronicle/test_marketplace_integrations.py b/tests/chronicle/test_marketplace_integrations.py new file mode 100644 index 00000000..b0f0dbc8 --- /dev/null +++ b/tests/chronicle/test_marketplace_integrations.py @@ -0,0 +1,522 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.marketplace_integrations import ( + list_marketplace_integrations, + get_marketplace_integration, + get_marketplace_integration_diff, + install_marketplace_integration, + uninstall_marketplace_integration, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +@pytest.fixture +def mock_response() -> Mock: + """Create a mock API response object.""" + mock = Mock() + mock.status_code = 200 + mock.json.return_value = {} + return mock + + +@pytest.fixture +def mock_error_response() -> Mock: + """Create a mock error API response object.""" + mock = Mock() + mock.status_code = 400 + mock.text = "Error message" + mock.raise_for_status.side_effect = Exception("API Error") + return mock + + +# -- list_marketplace_integrations tests -- + + +def test_list_marketplace_integrations_success(chronicle_client): + """Test list_marketplace_integrations delegates to chronicle_paginated_request.""" + expected = { + "marketplaceIntegrations": [ + {"name": "integration1"}, + {"name": "integration2"}, + ] + } + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations( + chronicle_client, + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_marketplace_integrations_default_args(chronicle_client): + """Test list_marketplace_integrations with default args.""" + expected = {"marketplaceIntegrations": []} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations(chronicle_client) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_marketplace_integrations_with_filter(chronicle_client): + """Test list_marketplace_integrations passes filter_string in extra_params.""" + expected = {"marketplaceIntegrations": [{"name": "integration1"}]} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations( + chronicle_client, + filter_string='displayName = "My Integration"', + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=None, + page_token=None, + extra_params={"filter": 'displayName = "My Integration"'}, + as_list=False, + ) + + +def test_list_marketplace_integrations_with_order_by(chronicle_client): + """Test list_marketplace_integrations passes order_by in extra_params.""" + expected = {"marketplaceIntegrations": []} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations( + chronicle_client, + order_by="displayName", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=None, + page_token=None, + extra_params={"orderBy": "displayName"}, + as_list=False, + ) + + +def test_list_marketplace_integrations_with_filter_and_order_by(chronicle_client): + """Test list_marketplace_integrations with both filter_string and order_by.""" + expected = {"marketplaceIntegrations": []} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations( + chronicle_client, + filter_string='displayName = "My Integration"', + order_by="displayName", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=None, + page_token=None, + extra_params={ + "filter": 'displayName = "My Integration"', + "orderBy": "displayName", + }, + as_list=False, + ) + + +def test_list_marketplace_integrations_as_list(chronicle_client): + """Test list_marketplace_integrations with as_list=True.""" + expected = [{"name": "integration1"}, {"name": "integration2"}] + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_marketplace_integrations(chronicle_client, as_list=True) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="marketplaceIntegrations", + items_key="marketplaceIntegrations", + page_size=None, + page_token=None, + extra_params={}, + as_list=True, + ) + + +def test_list_marketplace_integrations_error(chronicle_client): + """Test list_marketplace_integrations propagates APIError from helper.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + side_effect=APIError("Failed to list marketplace integrations"), + ): + with pytest.raises(APIError) as exc_info: + list_marketplace_integrations(chronicle_client) + + assert "Failed to list marketplace integrations" in str(exc_info.value) + + +# -- get_marketplace_integration tests -- + + +def test_get_marketplace_integration_success(chronicle_client): + """Test get_marketplace_integration returns expected result.""" + expected = { + "name": "test-integration", + "displayName": "Test Integration", + "version": "1.0.0", + } + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_marketplace_integration(chronicle_client, "test-integration") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="marketplaceIntegrations/test-integration", + api_version=APIVersion.V1BETA, + ) + + +def test_get_marketplace_integration_error(chronicle_client): + """Test get_marketplace_integration raises APIError on failure.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + side_effect=APIError("Failed to get marketplace integration test-integration"), + ): + with pytest.raises(APIError) as exc_info: + get_marketplace_integration(chronicle_client, "test-integration") + + assert "Failed to get marketplace integration" in str(exc_info.value) + + +# -- get_marketplace_integration_diff tests -- + + +def test_get_marketplace_integration_diff_success(chronicle_client): + """Test get_marketplace_integration_diff returns expected result.""" + expected = { + "name": "test-integration", + "diff": {"added": [], "removed": [], "modified": []}, + } + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_marketplace_integration_diff(chronicle_client, "test-integration") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path=( + "marketplaceIntegrations/test-integration" + ":fetchCommercialDiff" + ), + api_version=APIVersion.V1BETA, + ) + + +def test_get_marketplace_integration_diff_error(chronicle_client): + """Test get_marketplace_integration_diff raises APIError on failure.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + side_effect=APIError("Failed to get marketplace integration diff"), + ): + with pytest.raises(APIError) as exc_info: + get_marketplace_integration_diff(chronicle_client, "test-integration") + + assert "Failed to get marketplace integration diff" in str(exc_info.value) + + +# -- install_marketplace_integration tests -- + + +def test_install_marketplace_integration_no_optional_fields(chronicle_client): + """Test install_marketplace_integration with no optional fields sends empty body.""" + expected = { + "name": "test-integration", + "displayName": "Test Integration", + "installedVersion": "1.0.0", + } + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:install", + json={}, + api_version=APIVersion.V1BETA, + ) + + +def test_install_marketplace_integration_all_fields(chronicle_client): + """Test install_marketplace_integration with all optional fields.""" + expected = { + "name": "test-integration", + "displayName": "Test Integration", + "installedVersion": "2.0.0", + } + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + override_mapping=True, + staging=False, + version="2.0.0", + restore_from_snapshot=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:install", + json={ + "overrideMapping": True, + "staging": False, + "version": "2.0.0", + "restoreFromSnapshot": True, + }, + api_version=APIVersion.V1BETA, + ) + + +def test_install_marketplace_integration_override_mapping_only(chronicle_client): + """Test install_marketplace_integration with only override_mapping set.""" + expected = {"name": "test-integration"} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + override_mapping=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:install", + json={"overrideMapping": True}, + api_version=APIVersion.V1BETA, + ) + + +def test_install_marketplace_integration_version_only(chronicle_client): + """Test install_marketplace_integration with only version set.""" + expected = {"name": "test-integration"} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + version="1.2.3", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:install", + json={"version": "1.2.3"}, + api_version=APIVersion.V1BETA, + ) + + +def test_install_marketplace_integration_none_fields_excluded(chronicle_client): + """Test that None optional fields are not included in the request body.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value={"name": "test-integration"}, + ) as mock_request: + install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + override_mapping=None, + staging=None, + version=None, + restore_from_snapshot=None, + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:install", + json={}, + api_version=APIVersion.V1BETA, + ) + + +def test_install_marketplace_integration_error(chronicle_client): + """Test install_marketplace_integration raises APIError on failure.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + side_effect=APIError("Failed to install marketplace integration"), + ): + with pytest.raises(APIError) as exc_info: + install_marketplace_integration( + chronicle_client, + integration_name="test-integration", + ) + + assert "Failed to install marketplace integration" in str(exc_info.value) + + +# -- uninstall_marketplace_integration tests -- + + +def test_uninstall_marketplace_integration_success(chronicle_client): + """Test uninstall_marketplace_integration returns expected result.""" + expected = {} + + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = uninstall_marketplace_integration( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="marketplaceIntegrations/test-integration:uninstall", + api_version=APIVersion.V1BETA, + ) + + +def test_uninstall_marketplace_integration_error(chronicle_client): + """Test uninstall_marketplace_integration raises APIError on failure.""" + with patch( + "secops.chronicle.marketplace_integrations.chronicle_request", + side_effect=APIError("Failed to uninstall marketplace integration"), + ): + with pytest.raises(APIError) as exc_info: + uninstall_marketplace_integration( + chronicle_client, + integration_name="test-integration", + ) + + assert "Failed to uninstall marketplace integration" in str(exc_info.value) \ No newline at end of file From 6c8c1b19bf8e597b7dc1ff59cb0ac1237a541216 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 21:50:16 +0000 Subject: [PATCH 05/46] fix: rename incorrect field --- src/secops/chronicle/marketplace_integrations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/secops/chronicle/marketplace_integrations.py b/src/secops/chronicle/marketplace_integrations.py index 0fb02932..44380ace 100644 --- a/src/secops/chronicle/marketplace_integrations.py +++ b/src/secops/chronicle/marketplace_integrations.py @@ -129,10 +129,10 @@ def get_marketplace_integration_diff( def install_marketplace_integration( client: "ChronicleClient", integration_name: str, - override_mapping: bool | None = None, - staging: bool | None = None, + override_mapping: bool | None = False, + staging: bool | None = False, version: str | None = None, - restore_from_snapshot: bool | None = None, + restore_from_snapshot: bool | None = False, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: """Install a marketplace integration by integration name @@ -161,7 +161,7 @@ def install_marketplace_integration( "overrideMapping": override_mapping, "staging": staging, "version": version, - "restoreFromSnapshot": restore_from_snapshot, + "restoreIntegrationSnapshot": restore_from_snapshot, } return chronicle_request( From b63941b2cbd57ac69a782fb4a867fde5c6f395f0 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 21:53:15 +0000 Subject: [PATCH 06/46] feat: implement CLI commands for marketplace integrations --- src/secops/cli/cli_client.py | 4 + .../cli/commands/marketplace_integrations.py | 204 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/secops/cli/commands/marketplace_integrations.py diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index 4c483656..4f3124bc 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -39,6 +39,9 @@ from secops.cli.commands.udm_search import setup_udm_search_view_command from secops.cli.commands.watchlist import setup_watchlist_command from secops.cli.commands.rule_retrohunt import setup_rule_retrohunt_command +from secops.cli.commands.marketplace_integrations import ( + setup_marketplace_integrations_command, +) from secops.cli.utils.common_args import add_chronicle_args, add_common_args from secops.cli.utils.config_utils import load_config from secops.exceptions import AuthenticationError, SecOpsError @@ -189,6 +192,7 @@ def build_parser() -> argparse.ArgumentParser: setup_dashboard_query_command(subparsers) setup_watchlist_command(subparsers) setup_rule_retrohunt_command(subparsers) + setup_marketplace_integrations_command(subparsers) return parser diff --git a/src/secops/cli/commands/marketplace_integrations.py b/src/secops/cli/commands/marketplace_integrations.py new file mode 100644 index 00000000..3d251766 --- /dev/null +++ b/src/secops/cli/commands/marketplace_integrations.py @@ -0,0 +1,204 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI marketplace integrations commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_marketplace_integrations_command(subparsers): + """Setup marketplace integrations command""" + mp_parser = subparsers.add_parser( + "marketplace-integrations", + help="Manage Chronicle marketplace integrations", + ) + lvl1 = mp_parser.add_subparsers( + dest="mp_command", help="Marketplace integrations command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List marketplace integrations") + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing marketplace integrations", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing marketplace integrations", + dest="order_by", + ) + list_parser.set_defaults(func=handle_mp_integration_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get marketplace integration details" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the marketplace integration to get", + dest="integration_name", + required=True, + ) + get_parser.set_defaults(func=handle_mp_integration_get_command) + + # diff command + diff_parser = lvl1.add_parser( + "diff", + help="Get marketplace integration diff between " + "installed and latest version", + ) + diff_parser.add_argument( + "--integration-name", + type=str, + help="Name of the marketplace integration to diff", + dest="integration_name", + required=True, + ) + diff_parser.set_defaults(func=handle_mp_integration_diff_command) + + # install command + install_parser = lvl1.add_parser( + "install", help="Install or update a marketplace integration" + ) + install_parser.add_argument( + "--integration-name", + type=str, + help="Name of the marketplace integration to install or update", + dest="integration_name", + required=True, + ) + install_parser.add_argument( + "--override-mapping", + action="store_true", + help="Override existing mapping", + dest="override_mapping", + ) + install_parser.add_argument( + "--staging", + action="store_true", + help="Whether to install the integration in " + "staging environment (true/false)", + dest="staging", + ) + install_parser.add_argument( + "--version", + type=str, + help="Version of the marketplace integration to install", + dest="version", + ) + install_parser.add_argument( + "--restore-from-snapshot", + action="store_true", + help="Whether to restore the integration from existing snapshot " + "(true/false)", + dest="restore_from_snapshot", + ) + install_parser.set_defaults(func=handle_mp_integration_install_command) + + # uninstall command + uninstall_parser = lvl1.add_parser( + "uninstall", help="Uninstall a marketplace integration" + ) + uninstall_parser.add_argument( + "--integration-name", + type=str, + help="Name of the marketplace integration to uninstall", + dest="integration_name", + required=True, + ) + uninstall_parser.set_defaults(func=handle_mp_integration_uninstall_command) + + +def handle_mp_integration_list_command(args, chronicle): + """Handle marketplace integrations list command""" + try: + out = chronicle.list_marketplace_integrations( + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing marketplace integrations: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_mp_integration_get_command(args, chronicle): + """Handle marketplace integrations get command""" + try: + out = chronicle.get_marketplace_integration( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting marketplace integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_mp_integration_diff_command(args, chronicle): + """Handle marketplace integrations diff command""" + try: + out = chronicle.get_marketplace_integration_diff( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting marketplace integration diff: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_mp_integration_install_command(args, chronicle): + """Handle marketplace integrations install command""" + try: + out = chronicle.install_marketplace_integration( + integration_name=args.integration_name, + override_mapping=args.override_mapping, + staging=args.staging, + version=args.version, + restore_from_snapshot=args.restore_from_snapshot, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error installing marketplace integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_mp_integration_uninstall_command(args, chronicle): + """Handle marketplace integrations uninstall command""" + try: + out = chronicle.uninstall_marketplace_integration( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error uninstalling marketplace integration: {e}", file=sys.stderr + ) + sys.exit(1) From 1fa89161d0c3283b816573e8081a944df43aa306 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 15:25:13 +0000 Subject: [PATCH 07/46] chore: add imports --- src/secops/chronicle/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index f38fcf2d..181a158c 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -197,6 +197,13 @@ create_watchlist, update_watchlist, ) +from secops.chronicle.marketplace_integrations import ( + list_marketplace_integrations, + get_marketplace_integration, + get_marketplace_integration_diff, + install_marketplace_integration, + uninstall_marketplace_integration +) __all__ = [ # Client @@ -365,4 +372,10 @@ "delete_watchlist", "create_watchlist", "update_watchlist", + # Marketplace Integrations + "list_marketplace_integrations", + "get_marketplace_integration", + "get_marketplace_integration_diff", + "install_marketplace_integration", + "uninstall_marketplace_integration", ] From 46b72889f4bc2541b96c71378bc427cdc11054f7 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 15:25:53 +0000 Subject: [PATCH 08/46] feat: refactor for integrations and subcommands in CLI --- src/secops/cli/cli_client.py | 6 +++--- src/secops/cli/commands/__init__.py | 0 src/secops/cli/commands/integrations/__init__.py | 0 .../marketplace_integration.py} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 src/secops/cli/commands/__init__.py create mode 100644 src/secops/cli/commands/integrations/__init__.py rename src/secops/cli/commands/{marketplace_integrations.py => integrations/marketplace_integration.py} (99%) diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index 4f3124bc..ed06cfb6 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -39,8 +39,8 @@ from secops.cli.commands.udm_search import setup_udm_search_view_command from secops.cli.commands.watchlist import setup_watchlist_command from secops.cli.commands.rule_retrohunt import setup_rule_retrohunt_command -from secops.cli.commands.marketplace_integrations import ( - setup_marketplace_integrations_command, +from secops.cli.commands.integrations.integrations_client import ( + setup_integrations_command, ) from secops.cli.utils.common_args import add_chronicle_args, add_common_args from secops.cli.utils.config_utils import load_config @@ -192,7 +192,7 @@ def build_parser() -> argparse.ArgumentParser: setup_dashboard_query_command(subparsers) setup_watchlist_command(subparsers) setup_rule_retrohunt_command(subparsers) - setup_marketplace_integrations_command(subparsers) + setup_integrations_command(subparsers) return parser diff --git a/src/secops/cli/commands/__init__.py b/src/secops/cli/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/cli/commands/integrations/__init__.py b/src/secops/cli/commands/integrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/cli/commands/marketplace_integrations.py b/src/secops/cli/commands/integrations/marketplace_integration.py similarity index 99% rename from src/secops/cli/commands/marketplace_integrations.py rename to src/secops/cli/commands/integrations/marketplace_integration.py index 3d251766..f7ccb62c 100644 --- a/src/secops/cli/commands/marketplace_integrations.py +++ b/src/secops/cli/commands/integrations/marketplace_integration.py @@ -26,7 +26,7 @@ def setup_marketplace_integrations_command(subparsers): """Setup marketplace integrations command""" mp_parser = subparsers.add_parser( - "marketplace-integrations", + "marketplace", help="Manage Chronicle marketplace integrations", ) lvl1 = mp_parser.add_subparsers( From eb954ddfad74c27907bee66b4d7b634d98c7df8b Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 15:26:13 +0000 Subject: [PATCH 09/46] feat: refactor for integrations and subcommands in CLI --- .../integrations/integrations_client.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/secops/cli/commands/integrations/integrations_client.py diff --git a/src/secops/cli/commands/integrations/integrations_client.py b/src/secops/cli/commands/integrations/integrations_client.py new file mode 100644 index 00000000..3213e3c6 --- /dev/null +++ b/src/secops/cli/commands/integrations/integrations_client.py @@ -0,0 +1,29 @@ +# Copyright 2025 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. +# +"""Top level arguments for integrations commands""" + +from secops.cli.commands.integrations import marketplace_integration + +def setup_integrations_command(subparsers): + """Setup integrations command""" + integrations_parser = subparsers.add_parser( + "integrations", help="Manage SecOps integrations" + ) + lvl1 = integrations_parser.add_subparsers( + dest="integrations_command", help="Integrations command" + ) + + # Setup all subcommands under `integrations` + marketplace_integration.setup_marketplace_integrations_command(lvl1) \ No newline at end of file From 5cd6ee07ff72e7d35c6fca43404497bec4aa7df8 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 19:54:28 +0000 Subject: [PATCH 10/46] feat: refactor for integrations and subcommands in CLI --- src/secops/chronicle/client.py | 16 ++++++++-------- src/secops/cli/cli_client.py | 2 +- .../{integrations => integration}/__init__.py | 0 .../integration_client.py} | 10 +++++----- .../marketplace_integration.py | 18 +++++++++--------- 5 files changed, 23 insertions(+), 23 deletions(-) rename src/secops/cli/commands/{integrations => integration}/__init__.py (100%) rename src/secops/cli/commands/{integrations/integrations_client.py => integration/integration_client.py} (76%) rename src/secops/cli/commands/{integrations => integration}/marketplace_integration.py (92%) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 73226266..b8dfc68d 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -687,20 +687,20 @@ def list_marketplace_integrations( api_version: APIVersion | None = APIVersion.V1BETA, as_list: bool = False, ) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of all marketplace integrations. + """Get a list of all marketplace integration. Args: - page_size: Maximum number of integrations to return per page + page_size: Maximum number of integration to return per page page_token: Token for the next page of results, if available - filter_string: Filter expression to filter marketplace integrations - order_by: Field to sort the marketplace integrations by + filter_string: Filter expression to filter marketplace integration + order_by: Field to sort the marketplace integration by api_version: API version to use. Defaults to V1BETA - as_list: If True, return a list of integrations instead of a dict - with integrations list and nextPageToken. + as_list: If True, return a list of integration instead of a dict + with integration list and nextPageToken. Returns: - If as_list is True: List of marketplace integrations. - If as_list is False: Dict with marketplace integrations list and + If as_list is True: List of marketplace integration. + If as_list is False: Dict with marketplace integration list and nextPageToken. Raises: diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index ed06cfb6..65b787f2 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -39,7 +39,7 @@ from secops.cli.commands.udm_search import setup_udm_search_view_command from secops.cli.commands.watchlist import setup_watchlist_command from secops.cli.commands.rule_retrohunt import setup_rule_retrohunt_command -from secops.cli.commands.integrations.integrations_client import ( +from secops.cli.commands.integration.integration_client import ( setup_integrations_command, ) from secops.cli.utils.common_args import add_chronicle_args, add_common_args diff --git a/src/secops/cli/commands/integrations/__init__.py b/src/secops/cli/commands/integration/__init__.py similarity index 100% rename from src/secops/cli/commands/integrations/__init__.py rename to src/secops/cli/commands/integration/__init__.py diff --git a/src/secops/cli/commands/integrations/integrations_client.py b/src/secops/cli/commands/integration/integration_client.py similarity index 76% rename from src/secops/cli/commands/integrations/integrations_client.py rename to src/secops/cli/commands/integration/integration_client.py index 3213e3c6..5928b437 100644 --- a/src/secops/cli/commands/integrations/integrations_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -12,18 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Top level arguments for integrations commands""" +"""Top level arguments for integration commands""" -from secops.cli.commands.integrations import marketplace_integration +from secops.cli.commands.integration import marketplace_integration def setup_integrations_command(subparsers): - """Setup integrations command""" + """Setup integration command""" integrations_parser = subparsers.add_parser( - "integrations", help="Manage SecOps integrations" + "integration", help="Manage SecOps integrations" ) lvl1 = integrations_parser.add_subparsers( dest="integrations_command", help="Integrations command" ) - # Setup all subcommands under `integrations` + # Setup all subcommands under `integration` marketplace_integration.setup_marketplace_integrations_command(lvl1) \ No newline at end of file diff --git a/src/secops/cli/commands/integrations/marketplace_integration.py b/src/secops/cli/commands/integration/marketplace_integration.py similarity index 92% rename from src/secops/cli/commands/integrations/marketplace_integration.py rename to src/secops/cli/commands/integration/marketplace_integration.py index f7ccb62c..b5e0014d 100644 --- a/src/secops/cli/commands/integrations/marketplace_integration.py +++ b/src/secops/cli/commands/integration/marketplace_integration.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Google SecOps CLI marketplace integrations commands""" +"""Google SecOps CLI marketplace integration commands""" import sys @@ -24,13 +24,13 @@ def setup_marketplace_integrations_command(subparsers): - """Setup marketplace integrations command""" + """Setup marketplace integration command""" mp_parser = subparsers.add_parser( "marketplace", - help="Manage Chronicle marketplace integrations", + help="Manage Chronicle marketplace integration", ) lvl1 = mp_parser.add_subparsers( - dest="mp_command", help="Marketplace integrations command" + dest="mp_command", help="Marketplace integration command" ) # list command @@ -133,7 +133,7 @@ def setup_marketplace_integrations_command(subparsers): def handle_mp_integration_list_command(args, chronicle): - """Handle marketplace integrations list command""" + """Handle marketplace integration list command""" try: out = chronicle.list_marketplace_integrations( page_size=args.page_size, @@ -149,7 +149,7 @@ def handle_mp_integration_list_command(args, chronicle): def handle_mp_integration_get_command(args, chronicle): - """Handle marketplace integrations get command""" + """Handle marketplace integration get command""" try: out = chronicle.get_marketplace_integration( integration_name=args.integration_name, @@ -161,7 +161,7 @@ def handle_mp_integration_get_command(args, chronicle): def handle_mp_integration_diff_command(args, chronicle): - """Handle marketplace integrations diff command""" + """Handle marketplace integration diff command""" try: out = chronicle.get_marketplace_integration_diff( integration_name=args.integration_name, @@ -175,7 +175,7 @@ def handle_mp_integration_diff_command(args, chronicle): def handle_mp_integration_install_command(args, chronicle): - """Handle marketplace integrations install command""" + """Handle marketplace integration install command""" try: out = chronicle.install_marketplace_integration( integration_name=args.integration_name, @@ -191,7 +191,7 @@ def handle_mp_integration_install_command(args, chronicle): def handle_mp_integration_uninstall_command(args, chronicle): - """Handle marketplace integrations uninstall command""" + """Handle marketplace integration uninstall command""" try: out = chronicle.uninstall_marketplace_integration( integration_name=args.integration_name, From ee73653bf790641ace5fab9996566e85a573d3a8 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 19:57:26 +0000 Subject: [PATCH 11/46] chore: marketplace integration features documentation --- CLI.md | 49 ++++ README.md | 72 +++++ api_module_mapping.md | 611 +++++++++++++++++++++--------------------- 3 files changed, 432 insertions(+), 300 deletions(-) diff --git a/CLI.md b/CLI.md index eb9ec6f2..233ba6b8 100644 --- a/CLI.md +++ b/CLI.md @@ -720,6 +720,55 @@ Delete a watchlist: secops watchlist delete --watchlist-id "abc-123-def" ``` +### Integration Management + +#### Marketplace Integrations + +List marketplace integrations: + +```bash +# List all marketplace integration (returns dict with pagination metadata) +secops integration marketplace list + +# List marketplace integration as a direct list (fetches all pages automatically) +secops integration marketplace list --as-list +``` + +Get marketplace integration details: + +```bash +secops integration marketplace get --integration-name "AWSSecurityHub" +``` + +Get marketplace integration diff between installed version and latest version: + +```bash +secops integration marketplace diff --integration-name "AWSSecurityHub" +``` + +Install or update a marketplace integration: + +```bash +# Install with default settings +secops integration marketplace install --integration-name "AWSSecurityHub" + +# Install to staging environment and override any existing ontology mappings +secops integration marketplace install --integration-name "AWSSecurityHub" --staging --override-mapping + +# Installing a currently installed integration with no specified version +# number will update it to the latest version +secops integration marketplace install --integration-name "AWSSecurityHub" + +# Or you can specify a specific version to install +secops integration marketplace install --integration-name "AWSSecurityHub" --version "5.0" +``` + +Uninstall a marketplace integration: + +```bash +secops integration marketplace uninstall --integration-name "AWSSecurityHub" +``` + ### Rule Management List detection rules: diff --git a/README.md b/README.md index 26951a1c..b7d4e56b 100644 --- a/README.md +++ b/README.md @@ -1865,6 +1865,78 @@ for watchlist in watchlists: print(f"Watchlist: {watchlist.get('displayName')}") ``` +## Integration Management + +### Marketplace Integrations + +List available marketplace integrations: + +```python +# Get all available marketplace integration +integrations = chronicle.list_marketplace_integrations() +for integration in integrations.get("marketplaceIntegrations", []): + integration_title = integration.get("title") + integration_id = integration.get("name", "").split("/")[-1] + integration_version = integration.get("version", "") + documentation_url = integration.get("documentationUri", "") + +# Get all integration as a list +integrations = chronicle.list_marketplace_integrations(as_list=True) + +# Get all currently installed integration +integrations = chronicle.list_marketplace_integrations(filter_string="installed = true") + +# Get all installed integration with updates available +integrations = chronicle.list_marketplace_integrations(filter_string="installed = true AND updateAvailable = true") + +# Specify use of V1 Alpha API version +integrations = chronicle.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) +``` + +Get a specific marketplace integration: + +```python +integration = chronicle.get_marketplace_integration("AWSSecurityHub") +``` + +Get the diff between the currently installed version and the latest +available version of an integration: + +```python +diff = chronicle.get_marketplace_integration_diff("AWSSecurityHub") +``` + +Install or update a marketplace integration: + +```python +# Install an integration with the default settings +integration_name = "AWSSecurityHub" +integration = chronicle.install_marketplace_integration(integration_name) + +# Install to staging environment and override any existing ontology mappings +integration = chronicle.install_marketplace_integration( + integration_name, + staging=True, + override_ontology_mappings=True +) + +# Installing a currently installed integration with no specified version +# number will update it to the latest version +integration = chronicle.install_marketplace_integration(integration_name) + +# Or you can specify a specific version to install +integration = chronicle.install_marketplace_integration( + integration_name, + version="5.0" +) +``` + +Uninstall a marketplace integration: + +```python +chronicle.uninstall_marketplace_integration("AWSSecurityHub") +``` + ## Rule Management The SDK provides comprehensive support for managing Chronicle detection rules: diff --git a/api_module_mapping.md b/api_module_mapping.md index 13683166..a8674677 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,6 +7,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented +- **v1beta:** 5 endpoints implemented - **v1alpha:** 113 endpoints implemented ## Endpoint Mapping @@ -48,7 +49,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | watchlists.delete | v1 | chronicle.watchlist.delete_watchlist | secops watchlist delete | | watchlists.get | v1 | chronicle.watchlist.get_watchlist | secops watchlist get | | watchlists.list | v1 | chronicle.watchlist.list_watchlists | secops watchlist list | -| watchlists.patch | v1 | chronicle.watchlist.update_watchlist | secops watchlist update | +| watchlists.patch | v1 | chronicle.watchlist.update_watchlist | secops watchlist update | | dataAccessLabels.create | v1beta | | | | dataAccessLabels.delete | v1beta | | | | dataAccessLabels.get | v1beta | | | @@ -60,6 +61,11 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | dataAccessScopes.list | v1beta | | | | dataAccessScopes.patch | v1beta | | | | get | v1beta | | | +| marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | +| marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | +| marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | +| marketplaceIntegrations.list | v1beta | chronicle.marketplace_integrations.list_marketplace_integrations | secops integration marketplace list | +| marketplaceIntegrations.uninstall | v1beta | chronicle.marketplace_integrations.uninstall_marketplace_integration | secops integration marketplace uninstall | | operations.cancel | v1beta | | | | operations.delete | v1beta | | | | operations.get | v1beta | | | @@ -102,302 +108,307 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | curatedRuleSetCategories.list | v1alpha | chronicle.rule_set.list_curated_rule_set_categories | secops curated-rule rule-set-category list | | curatedRules.get | v1alpha | chronicle.rule_set.get_curated_rule
chronicle.rule_set.get_curated_rule_by_name | secops curated-rule rule get | | curatedRules.list | v1alpha | chronicle.rule_set.list_curated_rules | secops curated-rule rule list | -| dashboardCharts.batchGet |v1alpha| | | -|dashboardCharts.get |v1alpha|chronicle.dashboard.get_chart |secops dashboard get-chart | -|dashboardQueries.execute |v1alpha|chronicle.dashboard_query.execute_query |secops dashboard-query execute | -|dashboardQueries.get |v1alpha|chronicle.dashboard_query.get_execute_query |secops dashboard-query get | -|dashboards.copy |v1alpha| | | -|dashboards.create |v1alpha| | | -|dashboards.delete |v1alpha| | | -|dashboards.get |v1alpha| | | -|dashboards.list |v1alpha| | | -|dataAccessLabels.create |v1alpha| | | -|dataAccessLabels.delete |v1alpha| | | -|dataAccessLabels.get |v1alpha| | | -|dataAccessLabels.list |v1alpha| | | -|dataAccessLabels.patch |v1alpha| | | -|dataAccessScopes.create |v1alpha| | | -|dataAccessScopes.delete |v1alpha| | | -|dataAccessScopes.get |v1alpha| | | -|dataAccessScopes.list |v1alpha| | | -|dataAccessScopes.patch |v1alpha| | | -|dataExports.cancel |v1alpha|chronicle.data_export.cancel_data_export |secops export cancel | -|dataExports.create |v1alpha|chronicle.data_export.create_data_export |secops export create | -|dataExports.fetchavailablelogtypes |v1alpha|chronicle.data_export.fetch_available_log_types |secops export log-types | -|dataExports.get |v1alpha|chronicle.data_export.get_data_export |secops export status | -|dataExports.list |v1alpha|chronicle.data_export.list_data_export |secops export list | -|dataExports.patch |v1alpha|chronicle.data_export.update_data_export |secops export update | -|dataTableOperationErrors.get |v1alpha| | | -|dataTables.create |v1alpha|chronicle.data_table.create_data_table |secops data-table create | -|dataTables.dataTableRows.bulkCreate |v1alpha|chronicle.data_table.create_data_table_rows |secops data-table add-rows | -|dataTables.dataTableRows.bulkCreateAsync |v1alpha| | | -|dataTables.dataTableRows.bulkGet |v1alpha| | | -|dataTables.dataTableRows.bulkReplace |v1alpha|chronicle.data_table.replace_data_table_rows |secops data-table replace-rows | -|dataTables.dataTableRows.bulkReplaceAsync |v1alpha| | | -|dataTables.dataTableRows.bulkUpdate |v1alpha|chronicle.data_table.update_data_table_rows |secops data-table update-rows | -|dataTables.dataTableRows.bulkUpdateAsync |v1alpha| | | -|dataTables.dataTableRows.create |v1alpha| | | -|dataTables.dataTableRows.delete |v1alpha|chronicle.data_table.delete_data_table_rows |secops data-table delete-rows | -|dataTables.dataTableRows.get |v1alpha| | | -|dataTables.dataTableRows.list |v1alpha|chronicle.data_table.list_data_table_rows |secops data-table list-rows | -|dataTables.dataTableRows.patch |v1alpha| | | -|dataTables.delete |v1alpha|chronicle.data_table.delete_data_table |secops data-table delete | -|dataTables.get |v1alpha|chronicle.data_table.get_data_table |secops data-table get | -|dataTables.list |v1alpha|chronicle.data_table.list_data_tables |secops data-table list | -|dataTables.patch |v1alpha| | | -|dataTables.upload |v1alpha| | | -|dataTaps.create |v1alpha| | | -|dataTaps.delete |v1alpha| | | -|dataTaps.get |v1alpha| | | -|dataTaps.list |v1alpha| | | -|dataTaps.patch |v1alpha| | | -|delete |v1alpha| | | -|enrichmentControls.create |v1alpha| | | -|enrichmentControls.delete |v1alpha| | | -|enrichmentControls.get |v1alpha| | | -|enrichmentControls.list |v1alpha| | | -|entities.get |v1alpha| | | -|entities.import |v1alpha|chronicle.log_ingest.import_entities |secops entity import | -|entities.modifyEntityRiskScore |v1alpha| | | -|entities.queryEntityRiskScoreModifications |v1alpha| | | -|entityRiskScores.query |v1alpha| | | -|errorNotificationConfigs.create |v1alpha| | | -|errorNotificationConfigs.delete |v1alpha| | | -|errorNotificationConfigs.get |v1alpha| | | -|errorNotificationConfigs.list |v1alpha| | | -|errorNotificationConfigs.patch |v1alpha| | | -|events.batchGet |v1alpha| | | -|events.get |v1alpha| | | -|events.import |v1alpha|chronicle.log_ingest.ingest_udm |secops log ingest-udm | -|extractSyslog |v1alpha| | | -|federationGroups.create |v1alpha| | | -|federationGroups.delete |v1alpha| | | -|federationGroups.get |v1alpha| | | -|federationGroups.list |v1alpha| | | -|federationGroups.patch |v1alpha| | | -|feedPacks.get |v1alpha| | | -|feedPacks.list |v1alpha| | | -|feedServiceAccounts.fetchServiceAccountForCustomer |v1alpha| | | -|feedSourceTypeSchemas.list |v1alpha| | | -|feedSourceTypeSchemas.logTypeSchemas.list |v1alpha| | | -|feeds.create |v1alpha|chronicle.feeds.create_feed |secops feed create | -|feeds.delete |v1alpha|chronicle.feeds.delete_feed |secops feed delete | -|feeds.disable |v1alpha|chronicle.feeds.disable_feed |secops feed disable | -|feeds.enable |v1alpha|chronicle.feeds.enable_feed |secops feed enable | -|feeds.generateSecret |v1alpha|chronicle.feeds.generate_secret |secops feed secret | -|feeds.get |v1alpha|chronicle.feeds.get_feed |secops feed get | -|feeds.importPushLogs |v1alpha| | | -|feeds.list |v1alpha|chronicle.feeds.list_feeds |secops feed list | -|feeds.patch |v1alpha|chronicle.feeds.update_feed |secops feed update | -|feeds.scheduleTransfer |v1alpha| | | -|fetchFederationAccess |v1alpha| | | -|findEntity |v1alpha| | | -|findEntityAlerts |v1alpha| | | -|findRelatedEntities |v1alpha| | | -|findUdmFieldValues |v1alpha| | | -|findingsGraph.exploreNode |v1alpha| | | -|findingsGraph.initializeGraph |v1alpha| | | -|findingsRefinements.computeFindingsRefinementActivity |v1alpha|chronicle.rule_exclusion.compute_rule_exclusion_activity |secops rule-exclusion compute-activity | -|findingsRefinements.create |v1alpha|chronicle.rule_exclusion.create_rule_exclusion |secops rule-exclusion create | -|findingsRefinements.get |v1alpha|chronicle.rule_exclusion.get_rule_exclusion |secops rule-exclusion get | -|findingsRefinements.getDeployment |v1alpha|chronicle.rule_exclusion.get_rule_exclusion_deployment |secops rule-exclusion get-deployment | -|findingsRefinements.list |v1alpha|chronicle.rule_exclusion.list_rule_exclusions |secops rule-exclusion list | -|findingsRefinements.patch |v1alpha|chronicle.rule_exclusion.patch_rule_exclusion |secops rule-exclusion update | -|findingsRefinements.updateDeployment |v1alpha|chronicle.rule_exclusion.update_rule_exclusion_deployment |secops rule-exclusion update-deployment| -|forwarders.collectors.create |v1alpha| | | -|forwarders.collectors.delete |v1alpha| | | -|forwarders.collectors.get |v1alpha| | | -|forwarders.collectors.list |v1alpha| | | -|forwarders.collectors.patch |v1alpha| | | -|forwarders.create |v1alpha|chronicle.log_ingest.create_forwarder |secops forwarder create | -|forwarders.delete |v1alpha|chronicle.log_ingest.delete_forwarder |secops forwarder delete | -|forwarders.generateForwarderFiles |v1alpha| | | -|forwarders.get |v1alpha|chronicle.log_ingest.get_forwarder |secops forwarder get | -|forwarders.importStatsEvents |v1alpha| | | -|forwarders.list |v1alpha|chronicle.log_ingest.list_forwarder |secops forwarder list | -|forwarders.patch |v1alpha|chronicle.log_ingest.update_forwarder |secops forwarder update | -|generateCollectionAgentAuth |v1alpha| | | -|generateSoarAuthJwt |v1alpha| | | -|generateUdmKeyValueMappings |v1alpha| | | -|generateWorkspaceConnectionToken |v1alpha| | | -|get |v1alpha| | | -|getBigQueryExport |v1alpha| | | -|getMultitenantDirectory |v1alpha| | | -|getRiskConfig |v1alpha| | | -|ingestionLogLabels.get |v1alpha| | | -|ingestionLogLabels.list |v1alpha| | | -|ingestionLogNamespaces.get |v1alpha| | | -|ingestionLogNamespaces.list |v1alpha| | | -|investigations.fetchAssociated |v1alpha|chronicle.investigations.fetch_associated_investigations |secops investigation fetch-associated | -|investigations.get |v1alpha|chronicle.investigations.get_investigation |secops investigation get | -|investigations.list |v1alpha|chronicle.investigations.list_investigations |secops investigation list | -|investigations.trigger |v1alpha|chronicle.investigations.trigger_investigation |secops investigation trigger | -|iocs.batchGet |v1alpha| | | -|iocs.findFirstAndLastSeen |v1alpha| | | -|iocs.get |v1alpha| | | -|iocs.getIocState |v1alpha| | | -|iocs.searchCuratedDetectionsForIoc |v1alpha| | | -|iocs.updateIocState |v1alpha| | | -|legacy.legacyBatchGetCases |v1alpha|chronicle.case.get_cases_from_list |secops case | -|legacy.legacyBatchGetCollections |v1alpha| | | -|legacy.legacyCreateOrUpdateCase |v1alpha| | | -|legacy.legacyCreateSoarAlert |v1alpha| | | -|legacy.legacyFetchAlertsView |v1alpha|chronicle.alert.get_alerts |secops alert | -|legacy.legacyFetchUdmSearchCsv |v1alpha|chronicle.udm_search.fetch_udm_search_csv |secops search --csv | -|legacy.legacyFetchUdmSearchView |v1alpha|chronicle.udm_search.fetch_udm_search_view |secops udm-search-view | -|legacy.legacyFindAssetEvents |v1alpha| | | -|legacy.legacyFindRawLogs |v1alpha| | | -|legacy.legacyFindUdmEvents |v1alpha| | | -|legacy.legacyGetAlert |v1alpha|chronicle.rule_alert.get_alert | | -|legacy.legacyGetCuratedRulesTrends |v1alpha| | | -|legacy.legacyGetDetection |v1alpha| | | -|legacy.legacyGetEventForDetection |v1alpha| | | -|legacy.legacyGetRuleCounts |v1alpha| | | -|legacy.legacyGetRulesTrends |v1alpha| | | -|legacy.legacyListCases |v1alpha|chronicle.case.get_cases |secops case --ids | -|legacy.legacyRunTestRule |v1alpha|chronicle.rule.run_rule_test |secops rule validate | -|legacy.legacySearchArtifactEvents |v1alpha| | | -|legacy.legacySearchArtifactIoCDetails |v1alpha| | | -|legacy.legacySearchAssetEvents |v1alpha| | | -|legacy.legacySearchCuratedDetections |v1alpha| | | -|legacy.legacySearchCustomerStats |v1alpha| | | -|legacy.legacySearchDetections |v1alpha|chronicle.rule_detection.list_detections | | -|legacy.legacySearchDomainsRecentlyRegistered |v1alpha| | | -|legacy.legacySearchDomainsTimingStats |v1alpha| | | -|legacy.legacySearchEnterpriseWideAlerts |v1alpha| | | -|legacy.legacySearchEnterpriseWideIoCs |v1alpha|chronicle.ioc.list_iocs |secops iocs | -|legacy.legacySearchFindings |v1alpha| | | -|legacy.legacySearchIngestionStats |v1alpha| | | -|legacy.legacySearchIoCInsights |v1alpha| | | -|legacy.legacySearchRawLogs |v1alpha| | | -|legacy.legacySearchRuleDetectionCountBuckets |v1alpha| | | -|legacy.legacySearchRuleDetectionEvents |v1alpha| | | -|legacy.legacySearchRuleResults |v1alpha| | | -|legacy.legacySearchRulesAlerts |v1alpha|chronicle.rule_alert.search_rule_alerts | | -|legacy.legacySearchUserEvents |v1alpha| | | -|legacy.legacyStreamDetectionAlerts |v1alpha| | | -|legacy.legacyTestRuleStreaming |v1alpha| | | -|legacy.legacyUpdateAlert |v1alpha|chronicle.rule_alert.update_alert | | -|listAllFindingsRefinementDeployments |v1alpha| | | -|logTypes.create |v1alpha| | | -|logTypes.generateEventTypesSuggestions |v1alpha| | | -|logTypes.get |v1alpha| | | -|logTypes.getLogTypeSetting |v1alpha| | | -|logTypes.legacySubmitParserExtension |v1alpha| | | -|logTypes.list |v1alpha| | | -|logTypes.logs.export |v1alpha| | | -|logTypes.logs.get |v1alpha| | | -|logTypes.logs.import |v1alpha|chronicle.log_ingest.ingest_log |secops log ingest | -|logTypes.logs.list |v1alpha| | | -|logTypes.parserExtensions.activate |v1alpha|chronicle.parser_extension.activate_parser_extension |secops parser-extension activate | -|logTypes.parserExtensions.create |v1alpha|chronicle.parser_extension.create_parser_extension |secops parser-extension create | -|logTypes.parserExtensions.delete |v1alpha|chronicle.parser_extension.delete_parser_extension |secops parser-extension delete | -|logTypes.parserExtensions.extensionValidationReports.get |v1alpha| | | -|logTypes.parserExtensions.extensionValidationReports.list |v1alpha| | | -|logTypes.parserExtensions.extensionValidationReports.validationErrors.list |v1alpha| | | -|logTypes.parserExtensions.get |v1alpha|chronicle.parser_extension.get_parser_extension |secops parser-extension get | -|logTypes.parserExtensions.list |v1alpha|chronicle.parser_extension.list_parser_extensions |secops parser-extension list | -|logTypes.parserExtensions.validationReports.get |v1alpha| | | -|logTypes.parserExtensions.validationReports.parsingErrors.list |v1alpha| | | -|logTypes.parsers.activate |v1alpha|chronicle.parser.activate_parser |secops parser activate | -|logTypes.parsers.activateReleaseCandidateParser |v1alpha|chronicle.parser.activate_release_candidate |secops parser activate-rc | -|logTypes.parsers.copy |v1alpha|chronicle.parser.copy_parser |secops parser copy | -|logTypes.parsers.create |v1alpha|chronicle.parser.create_parser |secops parser create | -|logTypes.parsers.deactivate |v1alpha|chronicle.parser.deactivate_parser |secops parser deactivate | -|logTypes.parsers.delete |v1alpha|chronicle.parser.delete_parser |secops parser delete | -|logTypes.parsers.get |v1alpha|chronicle.parser.get_parser |secops parser get | -|logTypes.parsers.list |v1alpha|chronicle.parser.list_parsers |secops parser list | -|logTypes.parsers.validationReports.get |v1alpha| | | -|logTypes.parsers.validationReports.parsingErrors.list |v1alpha| | | -|logTypes.patch |v1alpha| | | -|logTypes.runParser |v1alpha|chronicle.parser.run_parser |secops parser run | -|logTypes.updateLogTypeSetting |v1alpha| | | -|logProcessingPipelines.associateStreams |v1alpha|chronicle.log_processing_pipelines.associate_streams |secops log-processing associate-streams| -|logProcessingPipelines.create |v1alpha|chronicle.log_processing_pipelines.create_log_processing_pipeline|secops log-processing create | -|logProcessingPipelines.delete |v1alpha|chronicle.log_processing_pipelines.delete_log_processing_pipeline|secops log-processing delete | -|logProcessingPipelines.dissociateStreams |v1alpha|chronicle.log_processing_pipelines.dissociate_streams |secops log-processing dissociate-streams| -|logProcessingPipelines.fetchAssociatedPipeline |v1alpha|chronicle.log_processing_pipelines.fetch_associated_pipeline|secops log-processing fetch-associated | -|logProcessingPipelines.fetchSampleLogsByStreams |v1alpha|chronicle.log_processing_pipelines.fetch_sample_logs_by_streams|secops log-processing fetch-sample-logs| -|logProcessingPipelines.get |v1alpha|chronicle.log_processing_pipelines.get_log_processing_pipeline|secops log-processing get | -|logProcessingPipelines.list |v1alpha|chronicle.log_processing_pipelines.list_log_processing_pipelines|secops log-processing list | -|logProcessingPipelines.patch |v1alpha|chronicle.log_processing_pipelines.update_log_processing_pipeline|secops log-processing update | -|logProcessingPipelines.testPipeline |v1alpha|chronicle.log_processing_pipelines.test_pipeline |secops log-processing test | -|logs.classify |v1alpha|chronicle.log_types.classify_logs |secops log classify | -| nativeDashboards.addChart | v1alpha |chronicle.dashboard.add_chart |secops dashboard add-chart | -| nativeDashboards.create | v1alpha |chronicle.dashboard.create_dashboard |secops dashboard create | -| nativeDashboards.delete | v1alpha |chronicle.dashboard.delete_dashboard |secops dashboard delete | -| nativeDashboards.duplicate | v1alpha |chronicle.dashboard.duplicate_dashboard |secops dashboard duplicate | -| nativeDashboards.duplicateChart | v1alpha | | | -| nativeDashboards.editChart | v1alpha |chronicle.dashboard.edit_chart |secops dashboard edit-chart | -| nativeDashboards.export | v1alpha |chronicle.dashboard.export_dashboard |secops dashboard export | -| nativeDashboards.get | v1alpha |chronicle.dashboard.get_dashboard |secops dashboard get | -| nativeDashboards.import | v1alpha |chronicle.dashboard.import_dashboard |secops dashboard import | -| nativeDashboards.list | v1alpha |chronicle.dashboard.list_dashboards |secops dashboard list | -| nativeDashboards.patch | v1alpha |chronicle.dashboard.update_dashboard |secops dashboard update | -| nativeDashboards.removeChart | v1alpha |chronicle.dashboard.remove_chart |secops dashboard remove-chart | -|operations.cancel |v1alpha| | | -|operations.delete |v1alpha| | | -|operations.get |v1alpha| | | -|operations.list |v1alpha| | | -|operations.streamSearch |v1alpha| | | -|queryProductSourceStats |v1alpha| | | -|referenceLists.create |v1alpha| | | -|referenceLists.get |v1alpha| | | -|referenceLists.list |v1alpha| | | -|referenceLists.patch |v1alpha| | | -|report |v1alpha| | | -|ruleExecutionErrors.list |v1alpha|chronicle.rule_detection.list_errors | | -|rules.create |v1alpha| | | -|rules.delete |v1alpha| | | -|rules.deployments.list |v1alpha| | | -|rules.get |v1alpha| | | -|rules.getDeployment |v1alpha| | | -|rules.list |v1alpha| | | -|rules.listRevisions |v1alpha| | | -|rules.patch |v1alpha| | | -|rules.retrohunts.create |v1alpha| | | -|rules.retrohunts.get |v1alpha| | | -|rules.retrohunts.list |v1alpha| | | -|rules.updateDeployment |v1alpha| | | -|searchEntities |v1alpha| | | -|searchRawLogs |v1alpha| | | -|summarizeEntitiesFromQuery |v1alpha|chronicle.entity.summarize_entity |secops entity | -|summarizeEntity |v1alpha|chronicle.entity.summarize_entity | | -|testFindingsRefinement |v1alpha| | | -|translateUdmQuery |v1alpha|chronicle.nl_search.translate_nl_to_udm | | -|translateYlRule |v1alpha| | | -|udmSearch |v1alpha|chronicle.search.search_udm |secops search | -|undelete |v1alpha| | | -|updateBigQueryExport |v1alpha| | | -|updateRiskConfig |v1alpha| | | -|users.clearConversationHistory |v1alpha| | | -|users.conversations.create |v1alpha|chronicle.gemini.create_conversation | | -|users.conversations.delete |v1alpha| | | -|users.conversations.get |v1alpha| | | -|users.conversations.list |v1alpha| | | -|users.conversations.messages.create |v1alpha|chronicle.gemini.query_gemini |secops gemini | -|users.conversations.messages.delete |v1alpha| | | -|users.conversations.messages.get |v1alpha| | | -|users.conversations.messages.list |v1alpha| | | -|users.conversations.messages.patch |v1alpha| | | -|users.conversations.patch |v1alpha| | | -|users.getPreferenceSet |v1alpha|chronicle.gemini.opt_in_to_gemini |secops gemini --opt-in | -|users.searchQueries.create |v1alpha| | | -|users.searchQueries.delete |v1alpha| | | -|users.searchQueries.get |v1alpha| | | -|users.searchQueries.list |v1alpha| | | -|users.searchQueries.patch |v1alpha| | | -|users.updatePreferenceSet |v1alpha| | | -|validateQuery |v1alpha|chronicle.validate.validate_query | | -|verifyReferenceList |v1alpha| | | -|verifyRuleText |v1alpha|chronicle.rule_validation.validate_rule |secops rule validate | -|watchlists.create |v1alpha| | | -|watchlists.delete |v1alpha| | | -|watchlists.entities.add |v1alpha| | | -|watchlists.entities.batchAdd |v1alpha| | | -|watchlists.entities.batchRemove |v1alpha| | | -|watchlists.entities.remove |v1alpha| | | -|watchlists.get |v1alpha| | | -|watchlists.list |v1alpha| | | -|watchlists.listEntities |v1alpha| | | -|watchlists.patch |v1alpha| | | +| dashboardCharts.batchGet | v1alpha | | | +| dashboardCharts.get | v1alpha | chronicle.dashboard.get_chart | secops dashboard get-chart | +| dashboardQueries.execute | v1alpha | chronicle.dashboard_query.execute_query | secops dashboard-query execute | +| dashboardQueries.get | v1alpha | chronicle.dashboard_query.get_execute_query | secops dashboard-query get | +| dashboards.copy | v1alpha | | | +| dashboards.create | v1alpha | | | +| dashboards.delete | v1alpha | | | +| dashboards.get | v1alpha | | | +| dashboards.list | v1alpha | | | +| dataAccessLabels.create | v1alpha | | | +| dataAccessLabels.delete | v1alpha | | | +| dataAccessLabels.get | v1alpha | | | +| dataAccessLabels.list | v1alpha | | | +| dataAccessLabels.patch | v1alpha | | | +| dataAccessScopes.create | v1alpha | | | +| dataAccessScopes.delete | v1alpha | | | +| dataAccessScopes.get | v1alpha | | | +| dataAccessScopes.list | v1alpha | | | +| dataAccessScopes.patch | v1alpha | | | +| dataExports.cancel | v1alpha | chronicle.data_export.cancel_data_export | secops export cancel | +| dataExports.create | v1alpha | chronicle.data_export.create_data_export | secops export create | +| dataExports.fetchavailablelogtypes | v1alpha | chronicle.data_export.fetch_available_log_types | secops export log-types | +| dataExports.get | v1alpha | chronicle.data_export.get_data_export | secops export status | +| dataExports.list | v1alpha | chronicle.data_export.list_data_export | secops export list | +| dataExports.patch | v1alpha | chronicle.data_export.update_data_export | secops export update | +| dataTableOperationErrors.get | v1alpha | | | +| dataTables.create | v1alpha | chronicle.data_table.create_data_table | secops data-table create | +| dataTables.dataTableRows.bulkCreate | v1alpha | chronicle.data_table.create_data_table_rows | secops data-table add-rows | +| dataTables.dataTableRows.bulkCreateAsync | v1alpha | | | +| dataTables.dataTableRows.bulkGet | v1alpha | | | +| dataTables.dataTableRows.bulkReplace | v1alpha | chronicle.data_table.replace_data_table_rows | secops data-table replace-rows | +| dataTables.dataTableRows.bulkReplaceAsync | v1alpha | | | +| dataTables.dataTableRows.bulkUpdate | v1alpha | chronicle.data_table.update_data_table_rows | secops data-table update-rows | +| dataTables.dataTableRows.bulkUpdateAsync | v1alpha | | | +| dataTables.dataTableRows.create | v1alpha | | | +| dataTables.dataTableRows.delete | v1alpha | chronicle.data_table.delete_data_table_rows | secops data-table delete-rows | +| dataTables.dataTableRows.get | v1alpha | | | +| dataTables.dataTableRows.list | v1alpha | chronicle.data_table.list_data_table_rows | secops data-table list-rows | +| dataTables.dataTableRows.patch | v1alpha | | | +| dataTables.delete | v1alpha | chronicle.data_table.delete_data_table | secops data-table delete | +| dataTables.get | v1alpha | chronicle.data_table.get_data_table | secops data-table get | +| dataTables.list | v1alpha | chronicle.data_table.list_data_tables | secops data-table list | +| dataTables.patch | v1alpha | | | +| dataTables.upload | v1alpha | | | +| dataTaps.create | v1alpha | | | +| dataTaps.delete | v1alpha | | | +| dataTaps.get | v1alpha | | | +| dataTaps.list | v1alpha | | | +| dataTaps.patch | v1alpha | | | +| delete | v1alpha | | | +| enrichmentControls.create | v1alpha | | | +| enrichmentControls.delete | v1alpha | | | +| enrichmentControls.get | v1alpha | | | +| enrichmentControls.list | v1alpha | | | +| entities.get | v1alpha | | | +| entities.import | v1alpha | chronicle.log_ingest.import_entities | secops entity import | +| entities.modifyEntityRiskScore | v1alpha | | | +| entities.queryEntityRiskScoreModifications | v1alpha | | | +| entityRiskScores.query | v1alpha | | | +| errorNotificationConfigs.create | v1alpha | | | +| errorNotificationConfigs.delete | v1alpha | | | +| errorNotificationConfigs.get | v1alpha | | | +| errorNotificationConfigs.list | v1alpha | | | +| errorNotificationConfigs.patch | v1alpha | | | +| events.batchGet | v1alpha | | | +| events.get | v1alpha | | | +| events.import | v1alpha | chronicle.log_ingest.ingest_udm | secops log ingest-udm | +| extractSyslog | v1alpha | | | +| federationGroups.create | v1alpha | | | +| federationGroups.delete | v1alpha | | | +| federationGroups.get | v1alpha | | | +| federationGroups.list | v1alpha | | | +| federationGroups.patch | v1alpha | | | +| feedPacks.get | v1alpha | | | +| feedPacks.list | v1alpha | | | +| feedServiceAccounts.fetchServiceAccountForCustomer | v1alpha | | | +| feedSourceTypeSchemas.list | v1alpha | | | +| feedSourceTypeSchemas.logTypeSchemas.list | v1alpha | | | +| feeds.create | v1alpha | chronicle.feeds.create_feed | secops feed create | +| feeds.delete | v1alpha | chronicle.feeds.delete_feed | secops feed delete | +| feeds.disable | v1alpha | chronicle.feeds.disable_feed | secops feed disable | +| feeds.enable | v1alpha | chronicle.feeds.enable_feed | secops feed enable | +| feeds.generateSecret | v1alpha | chronicle.feeds.generate_secret | secops feed secret | +| feeds.get | v1alpha | chronicle.feeds.get_feed | secops feed get | +| feeds.importPushLogs | v1alpha | | | +| feeds.list | v1alpha | chronicle.feeds.list_feeds | secops feed list | +| feeds.patch | v1alpha | chronicle.feeds.update_feed | secops feed update | +| feeds.scheduleTransfer | v1alpha | | | +| fetchFederationAccess | v1alpha | | | +| findEntity | v1alpha | | | +| findEntityAlerts | v1alpha | | | +| findRelatedEntities | v1alpha | | | +| findUdmFieldValues | v1alpha | | | +| findingsGraph.exploreNode | v1alpha | | | +| findingsGraph.initializeGraph | v1alpha | | | +| findingsRefinements.computeFindingsRefinementActivity | v1alpha | chronicle.rule_exclusion.compute_rule_exclusion_activity | secops rule-exclusion compute-activity | +| findingsRefinements.create | v1alpha | chronicle.rule_exclusion.create_rule_exclusion | secops rule-exclusion create | +| findingsRefinements.get | v1alpha | chronicle.rule_exclusion.get_rule_exclusion | secops rule-exclusion get | +| findingsRefinements.getDeployment | v1alpha | chronicle.rule_exclusion.get_rule_exclusion_deployment | secops rule-exclusion get-deployment | +| findingsRefinements.list | v1alpha | chronicle.rule_exclusion.list_rule_exclusions | secops rule-exclusion list | +| findingsRefinements.patch | v1alpha | chronicle.rule_exclusion.patch_rule_exclusion | secops rule-exclusion update | +| findingsRefinements.updateDeployment | v1alpha | chronicle.rule_exclusion.update_rule_exclusion_deployment | secops rule-exclusion update-deployment | +| forwarders.collectors.create | v1alpha | | | +| forwarders.collectors.delete | v1alpha | | | +| forwarders.collectors.get | v1alpha | | | +| forwarders.collectors.list | v1alpha | | | +| forwarders.collectors.patch | v1alpha | | | +| forwarders.create | v1alpha | chronicle.log_ingest.create_forwarder | secops forwarder create | +| forwarders.delete | v1alpha | chronicle.log_ingest.delete_forwarder | secops forwarder delete | +| forwarders.generateForwarderFiles | v1alpha | | | +| forwarders.get | v1alpha | chronicle.log_ingest.get_forwarder | secops forwarder get | +| forwarders.importStatsEvents | v1alpha | | | +| forwarders.list | v1alpha | chronicle.log_ingest.list_forwarder | secops forwarder list | +| forwarders.patch | v1alpha | chronicle.log_ingest.update_forwarder | secops forwarder update | +| generateCollectionAgentAuth | v1alpha | | | +| generateSoarAuthJwt | v1alpha | | | +| generateUdmKeyValueMappings | v1alpha | | | +| generateWorkspaceConnectionToken | v1alpha | | | +| get | v1alpha | | | +| getBigQueryExport | v1alpha | | | +| getMultitenantDirectory | v1alpha | | | +| getRiskConfig | v1alpha | | | +| ingestionLogLabels.get | v1alpha | | | +| ingestionLogLabels.list | v1alpha | | | +| ingestionLogNamespaces.get | v1alpha | | | +| ingestionLogNamespaces.list | v1alpha | | | +| investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | +| investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | +| investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | +| investigations.trigger | v1alpha | chronicle.investigations.trigger_investigation | secops investigation trigger | +| iocs.batchGet | v1alpha | | | +| iocs.findFirstAndLastSeen | v1alpha | | | +| iocs.get | v1alpha | | | +| iocs.getIocState | v1alpha | | | +| iocs.searchCuratedDetectionsForIoc | v1alpha | | | +| iocs.updateIocState | v1alpha | | | +| legacy.legacyBatchGetCases | v1alpha | chronicle.case.get_cases_from_list | secops case | +| legacy.legacyBatchGetCollections | v1alpha | | | +| legacy.legacyCreateOrUpdateCase | v1alpha | | | +| legacy.legacyCreateSoarAlert | v1alpha | | | +| legacy.legacyFetchAlertsView | v1alpha | chronicle.alert.get_alerts | secops alert | +| legacy.legacyFetchUdmSearchCsv | v1alpha | chronicle.udm_search.fetch_udm_search_csv | secops search --csv | +| legacy.legacyFetchUdmSearchView | v1alpha | chronicle.udm_search.fetch_udm_search_view | secops udm-search-view | +| legacy.legacyFindAssetEvents | v1alpha | | | +| legacy.legacyFindRawLogs | v1alpha | | | +| legacy.legacyFindUdmEvents | v1alpha | | | +| legacy.legacyGetAlert | v1alpha | chronicle.rule_alert.get_alert | | +| legacy.legacyGetCuratedRulesTrends | v1alpha | | | +| legacy.legacyGetDetection | v1alpha | | | +| legacy.legacyGetEventForDetection | v1alpha | | | +| legacy.legacyGetRuleCounts | v1alpha | | | +| legacy.legacyGetRulesTrends | v1alpha | | | +| legacy.legacyListCases | v1alpha | chronicle.case.get_cases | secops case --ids | +| legacy.legacyRunTestRule | v1alpha | chronicle.rule.run_rule_test | secops rule validate | +| legacy.legacySearchArtifactEvents | v1alpha | | | +| legacy.legacySearchArtifactIoCDetails | v1alpha | | | +| legacy.legacySearchAssetEvents | v1alpha | | | +| legacy.legacySearchCuratedDetections | v1alpha | | | +| legacy.legacySearchCustomerStats | v1alpha | | | +| legacy.legacySearchDetections | v1alpha | chronicle.rule_detection.list_detections | | +| legacy.legacySearchDomainsRecentlyRegistered | v1alpha | | | +| legacy.legacySearchDomainsTimingStats | v1alpha | | | +| legacy.legacySearchEnterpriseWideAlerts | v1alpha | | | +| legacy.legacySearchEnterpriseWideIoCs | v1alpha | chronicle.ioc.list_iocs | secops iocs | +| legacy.legacySearchFindings | v1alpha | | | +| legacy.legacySearchIngestionStats | v1alpha | | | +| legacy.legacySearchIoCInsights | v1alpha | | | +| legacy.legacySearchRawLogs | v1alpha | | | +| legacy.legacySearchRuleDetectionCountBuckets | v1alpha | | | +| legacy.legacySearchRuleDetectionEvents | v1alpha | | | +| legacy.legacySearchRuleResults | v1alpha | | | +| legacy.legacySearchRulesAlerts | v1alpha | chronicle.rule_alert.search_rule_alerts | | +| legacy.legacySearchUserEvents | v1alpha | | | +| legacy.legacyStreamDetectionAlerts | v1alpha | | | +| legacy.legacyTestRuleStreaming | v1alpha | | | +| legacy.legacyUpdateAlert | v1alpha | chronicle.rule_alert.update_alert | | +| listAllFindingsRefinementDeployments | v1alpha | | | +| logProcessingPipelines.associateStreams | v1alpha | chronicle.log_processing_pipelines.associate_streams | secops log-processing associate-streams | +| logProcessingPipelines.create | v1alpha | chronicle.log_processing_pipelines.create_log_processing_pipeline | secops log-processing create | +| logProcessingPipelines.delete | v1alpha | chronicle.log_processing_pipelines.delete_log_processing_pipeline | secops log-processing delete | +| logProcessingPipelines.dissociateStreams | v1alpha | chronicle.log_processing_pipelines.dissociate_streams | secops log-processing dissociate-streams | +| logProcessingPipelines.fetchAssociatedPipeline | v1alpha | chronicle.log_processing_pipelines.fetch_associated_pipeline | secops log-processing fetch-associated | +| logProcessingPipelines.fetchSampleLogsByStreams | v1alpha | chronicle.log_processing_pipelines.fetch_sample_logs_by_streams | secops log-processing fetch-sample-logs | +| logProcessingPipelines.get | v1alpha | chronicle.log_processing_pipelines.get_log_processing_pipeline | secops log-processing get | +| logProcessingPipelines.list | v1alpha | chronicle.log_processing_pipelines.list_log_processing_pipelines | secops log-processing list | +| logProcessingPipelines.patch | v1alpha | chronicle.log_processing_pipelines.update_log_processing_pipeline | secops log-processing update | +| logProcessingPipelines.testPipeline | v1alpha | chronicle.log_processing_pipelines.test_pipeline | secops log-processing test | +| logTypes.create | v1alpha | | | +| logTypes.generateEventTypesSuggestions | v1alpha | | | +| logTypes.get | v1alpha | | | +| logTypes.getLogTypeSetting | v1alpha | | | +| logTypes.legacySubmitParserExtension | v1alpha | | | +| logTypes.list | v1alpha | | | +| logTypes.logs.export | v1alpha | | | +| logTypes.logs.get | v1alpha | | | +| logTypes.logs.import | v1alpha | chronicle.log_ingest.ingest_log | secops log ingest | +| logTypes.logs.list | v1alpha | | | +| logTypes.parserExtensions.activate | v1alpha | chronicle.parser_extension.activate_parser_extension | secops parser-extension activate | +| logTypes.parserExtensions.create | v1alpha | chronicle.parser_extension.create_parser_extension | secops parser-extension create | +| logTypes.parserExtensions.delete | v1alpha | chronicle.parser_extension.delete_parser_extension | secops parser-extension delete | +| logTypes.parserExtensions.extensionValidationReports.get | v1alpha | | | +| logTypes.parserExtensions.extensionValidationReports.list | v1alpha | | | +| logTypes.parserExtensions.extensionValidationReports.validationErrors.list | v1alpha | | | +| logTypes.parserExtensions.get | v1alpha | chronicle.parser_extension.get_parser_extension | secops parser-extension get | +| logTypes.parserExtensions.list | v1alpha | chronicle.parser_extension.list_parser_extensions | secops parser-extension list | +| logTypes.parserExtensions.validationReports.get | v1alpha | | | +| logTypes.parserExtensions.validationReports.parsingErrors.list | v1alpha | | | +| logTypes.parsers.activate | v1alpha | chronicle.parser.activate_parser | secops parser activate | +| logTypes.parsers.activateReleaseCandidateParser | v1alpha | chronicle.parser.activate_release_candidate | secops parser activate-rc | +| logTypes.parsers.copy | v1alpha | chronicle.parser.copy_parser | secops parser copy | +| logTypes.parsers.create | v1alpha | chronicle.parser.create_parser | secops parser create | +| logTypes.parsers.deactivate | v1alpha | chronicle.parser.deactivate_parser | secops parser deactivate | +| logTypes.parsers.delete | v1alpha | chronicle.parser.delete_parser | secops parser delete | +| logTypes.parsers.get | v1alpha | chronicle.parser.get_parser | secops parser get | +| logTypes.parsers.list | v1alpha | chronicle.parser.list_parsers | secops parser list | +| logTypes.parsers.validationReports.get | v1alpha | | | +| logTypes.parsers.validationReports.parsingErrors.list | v1alpha | | | +| logTypes.patch | v1alpha | | | +| logTypes.runParser | v1alpha | chronicle.parser.run_parser | secops parser run | +| logTypes.updateLogTypeSetting | v1alpha | | | +| logs.classify | v1alpha | chronicle.log_types.classify_logs | secops log classify | +| marketplaceIntegrations.get | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration(api_version=APIVersion.V1ALPHA) | | +| marketplaceIntegrations.getDiff | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration_diff(api_version=APIVersion.V1ALPHA) | | +| marketplaceIntegrations.install | v1alpha | chronicle.marketplace_integrations.install_marketplace_integration(api_version=APIVersion.V1ALPHA) | | +| marketplaceIntegrations.list | v1alpha | chronicle.marketplace_integrations.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) | | +| marketplaceIntegrations.uninstall | v1alpha | chronicle.marketplace_integrations.uninstall_marketplace_integration(api_version=APIVersion.V1ALPHA) | | +| nativeDashboards.addChart | v1alpha | chronicle.dashboard.add_chart | secops dashboard add-chart | +| nativeDashboards.create | v1alpha | chronicle.dashboard.create_dashboard | secops dashboard create | +| nativeDashboards.delete | v1alpha | chronicle.dashboard.delete_dashboard | secops dashboard delete | +| nativeDashboards.duplicate | v1alpha | chronicle.dashboard.duplicate_dashboard | secops dashboard duplicate | +| nativeDashboards.duplicateChart | v1alpha | | | +| nativeDashboards.editChart | v1alpha | chronicle.dashboard.edit_chart | secops dashboard edit-chart | +| nativeDashboards.export | v1alpha | chronicle.dashboard.export_dashboard | secops dashboard export | +| nativeDashboards.get | v1alpha | chronicle.dashboard.get_dashboard | secops dashboard get | +| nativeDashboards.import | v1alpha | chronicle.dashboard.import_dashboard | secops dashboard import | +| nativeDashboards.list | v1alpha | chronicle.dashboard.list_dashboards | secops dashboard list | +| nativeDashboards.patch | v1alpha | chronicle.dashboard.update_dashboard | secops dashboard update | +| nativeDashboards.removeChart | v1alpha | chronicle.dashboard.remove_chart | secops dashboard remove-chart | +| operations.cancel | v1alpha | | | +| operations.delete | v1alpha | | | +| operations.get | v1alpha | | | +| operations.list | v1alpha | | | +| operations.streamSearch | v1alpha | | | +| queryProductSourceStats | v1alpha | | | +| referenceLists.create | v1alpha | | | +| referenceLists.get | v1alpha | | | +| referenceLists.list | v1alpha | | | +| referenceLists.patch | v1alpha | | | +| report | v1alpha | | | +| ruleExecutionErrors.list | v1alpha | chronicle.rule_detection.list_errors | | +| rules.create | v1alpha | | | +| rules.delete | v1alpha | | | +| rules.deployments.list | v1alpha | | | +| rules.get | v1alpha | | | +| rules.getDeployment | v1alpha | | | +| rules.list | v1alpha | | | +| rules.listRevisions | v1alpha | | | +| rules.patch | v1alpha | | | +| rules.retrohunts.create | v1alpha | | | +| rules.retrohunts.get | v1alpha | | | +| rules.retrohunts.list | v1alpha | | | +| rules.updateDeployment | v1alpha | | | +| searchEntities | v1alpha | | | +| searchRawLogs | v1alpha | | | +| summarizeEntitiesFromQuery | v1alpha | chronicle.entity.summarize_entity | secops entity | +| summarizeEntity | v1alpha | chronicle.entity.summarize_entity | | +| testFindingsRefinement | v1alpha | | | +| translateUdmQuery | v1alpha | chronicle.nl_search.translate_nl_to_udm | | +| translateYlRule | v1alpha | | | +| udmSearch | v1alpha | chronicle.search.search_udm | secops search | +| undelete | v1alpha | | | +| updateBigQueryExport | v1alpha | | | +| updateRiskConfig | v1alpha | | | +| users.clearConversationHistory | v1alpha | | | +| users.conversations.create | v1alpha | chronicle.gemini.create_conversation | | +| users.conversations.delete | v1alpha | | | +| users.conversations.get | v1alpha | | | +| users.conversations.list | v1alpha | | | +| users.conversations.messages.create | v1alpha | chronicle.gemini.query_gemini | secops gemini | +| users.conversations.messages.delete | v1alpha | | | +| users.conversations.messages.get | v1alpha | | | +| users.conversations.messages.list | v1alpha | | | +| users.conversations.messages.patch | v1alpha | | | +| users.conversations.patch | v1alpha | | | +| users.getPreferenceSet | v1alpha | chronicle.gemini.opt_in_to_gemini | secops gemini --opt-in | +| users.searchQueries.create | v1alpha | | | +| users.searchQueries.delete | v1alpha | | | +| users.searchQueries.get | v1alpha | | | +| users.searchQueries.list | v1alpha | | | +| users.searchQueries.patch | v1alpha | | | +| users.updatePreferenceSet | v1alpha | | | +| validateQuery | v1alpha | chronicle.validate.validate_query | | +| verifyReferenceList | v1alpha | | | +| verifyRuleText | v1alpha | chronicle.rule_validation.validate_rule | secops rule validate | +| watchlists.create | v1alpha | | | +| watchlists.delete | v1alpha | | | +| watchlists.entities.add | v1alpha | | | +| watchlists.entities.batchAdd | v1alpha | | | +| watchlists.entities.batchRemove | v1alpha | | | +| watchlists.entities.remove | v1alpha | | | +| watchlists.get | v1alpha | | | +| watchlists.list | v1alpha | | | +| watchlists.listEntities | v1alpha | | | +| watchlists.patch | v1alpha | | | \ No newline at end of file From 1b3a828fdc36a5b52c99fbb3dfc13a618e69934a Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 28 Feb 2026 19:59:54 +0000 Subject: [PATCH 12/46] chore: linting and formatting --- .../cli/commands/integration/integration_client.py | 3 ++- .../commands/integration/marketplace_integration.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index 5928b437..334398b8 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -16,6 +16,7 @@ from secops.cli.commands.integration import marketplace_integration + def setup_integrations_command(subparsers): """Setup integration command""" integrations_parser = subparsers.add_parser( @@ -26,4 +27,4 @@ def setup_integrations_command(subparsers): ) # Setup all subcommands under `integration` - marketplace_integration.setup_marketplace_integrations_command(lvl1) \ No newline at end of file + marketplace_integration.setup_marketplace_integrations_command(lvl1) diff --git a/src/secops/cli/commands/integration/marketplace_integration.py b/src/secops/cli/commands/integration/marketplace_integration.py index b5e0014d..f8b87aa2 100644 --- a/src/secops/cli/commands/integration/marketplace_integration.py +++ b/src/secops/cli/commands/integration/marketplace_integration.py @@ -143,7 +143,7 @@ def handle_mp_integration_list_command(args, chronicle): as_list=args.as_list, ) output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught print(f"Error listing marketplace integrations: {e}", file=sys.stderr) sys.exit(1) @@ -155,7 +155,7 @@ def handle_mp_integration_get_command(args, chronicle): integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught print(f"Error getting marketplace integration: {e}", file=sys.stderr) sys.exit(1) @@ -167,7 +167,7 @@ def handle_mp_integration_diff_command(args, chronicle): integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught print( f"Error getting marketplace integration diff: {e}", file=sys.stderr ) @@ -185,7 +185,7 @@ def handle_mp_integration_install_command(args, chronicle): restore_from_snapshot=args.restore_from_snapshot, ) output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught print(f"Error installing marketplace integration: {e}", file=sys.stderr) sys.exit(1) @@ -197,7 +197,7 @@ def handle_mp_integration_uninstall_command(args, chronicle): integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught print( f"Error uninstalling marketplace integration: {e}", file=sys.stderr ) From c30d8c6454361564f819a2d504fbb0a128f13d50 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sun, 1 Mar 2026 13:43:34 +0000 Subject: [PATCH 13/46] chore: refactor integrations under directory for future expansion --- src/secops/chronicle/__init__.py | 2 +- src/secops/chronicle/client.py | 2 +- .../chronicle/{ => integration}/marketplace_integrations.py | 2 +- tests/chronicle/test_marketplace_integrations.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/secops/chronicle/{ => integration}/marketplace_integrations.py (99%) diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 181a158c..ef86627f 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -197,7 +197,7 @@ create_watchlist, update_watchlist, ) -from secops.chronicle.marketplace_integrations import ( +from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, get_marketplace_integration_diff, diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index b8dfc68d..46468c9f 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -129,7 +129,7 @@ is_valid_log_type as _is_valid_log_type, search_log_types as _search_log_types, ) -from secops.chronicle.marketplace_integrations import ( +from secops.chronicle.integration.marketplace_integrations import ( get_marketplace_integration as _get_marketplace_integration, get_marketplace_integration_diff as _get_marketplace_integration_diff, install_marketplace_integration as _install_marketplace_integration, diff --git a/src/secops/chronicle/marketplace_integrations.py b/src/secops/chronicle/integration/marketplace_integrations.py similarity index 99% rename from src/secops/chronicle/marketplace_integrations.py rename to src/secops/chronicle/integration/marketplace_integrations.py index 44380ace..48596ef5 100644 --- a/src/secops/chronicle/marketplace_integrations.py +++ b/src/secops/chronicle/integration/marketplace_integrations.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Watchlist functionality for Chronicle.""" +"""Marketplace integrations functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/tests/chronicle/test_marketplace_integrations.py b/tests/chronicle/test_marketplace_integrations.py index b0f0dbc8..e56329a9 100644 --- a/tests/chronicle/test_marketplace_integrations.py +++ b/tests/chronicle/test_marketplace_integrations.py @@ -20,7 +20,7 @@ from secops.chronicle.client import ChronicleClient from secops.chronicle.models import APIVersion -from secops.chronicle.marketplace_integrations import ( +from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, get_marketplace_integration_diff, From c5ed1b9f4f43ee000af1a3e33c8a5877759964e1 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 2 Mar 2026 20:25:22 +0000 Subject: [PATCH 14/46] feat: implement integrations functions --- src/secops/chronicle/client.py | 99 ++- src/secops/chronicle/integration/__init__.py | 0 .../chronicle/integration/integrations.py | 656 ++++++++++++++++++ src/secops/chronicle/models.py | 32 + src/secops/chronicle/utils/format_utils.py | 40 ++ 5 files changed, 825 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/__init__.py create mode 100644 src/secops/chronicle/integration/integrations.py create mode 100644 src/secops/chronicle/utils/format_utils.py diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 46468c9f..fa963588 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -22,7 +22,7 @@ from google.auth.transport import requests as google_auth_requests -#pylint: disable=line-too-long +# pylint: disable=line-too-long from secops import auth as secops_auth from secops.auth import RetryConfig from secops.chronicle.alert import get_alerts as _get_alerts @@ -136,6 +136,12 @@ list_marketplace_integrations as _list_marketplace_integrations, uninstall_marketplace_integration as _uninstall_marketplace_integration, ) +from secops.chronicle.integration.integrations import ( + DiffType, + list_integrations as _list_integrations, + get_integration as _get_integration, + get_integration_diff as _get_integration_diff, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -252,7 +258,9 @@ update_watchlist as _update_watchlist, ) from secops.exceptions import SecOpsError -#pylint: enable=line-too-long + +# pylint: enable=line-too-long + class ValueType(Enum): """Chronicle API value types.""" @@ -820,6 +828,93 @@ def uninstall_marketplace_integration( self, integration_name, api_version ) + def list_integrations( + self, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any]: + """Get a list of all integrations. + + Args: + page_size: Maximum number of integrations to return per page + page_token: Token for the next page of results, if available + filter_string: Filter expression to filter integrations + order_by: Field to sort the integrations by + api_version: API version to use. Defaults to V1BETA + as_list: If True, return a list of integrations instead of a dict + with integration list and nextPageToken. + + Returns: + If as_list is True: List of integration. + If as_list is False: Dict with integration list and + nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_integrations( + self, + page_size, + page_token, + filter_string, + order_by, + api_version, + as_list, + ) + + def get_integration( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a specific integration by integration name. + + Args: + integration_name: name of the integration to retrieve + api_version: API version to use. Defaults to V1BETA + + Returns: + Integration details + + Raises: + APIError: If the API request fails + """ + return _get_integration(self, integration_name, api_version) + + def get_integration_diff( + self, + integration_name: str, + diff_type: DiffType | None = DiffType.COMMERCIAL, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get the configuration diff of a specific integration. + + Args: + integration_name: ID of the integration to retrieve the diff for + diff_type: Type of diff to retrieve (Commercial, Production, or Staging). + Default is Commercial. + COMMERCIAL: Diff between the commercial version of the + integration and the current version in the environment. + PRODUCTION: Returns the difference between the staging + integration and its matching production version. + STAGING: Returns the difference between the production + integration and its corresponding staging version. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the configuration diff of the specified integration + + Raises: + APIError: If the API request fails + """ + return _get_integration_diff( + self, integration_name, diff_type, api_version + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/__init__.py b/src/secops/chronicle/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py new file mode 100644 index 00000000..814904f5 --- /dev/null +++ b/src/secops/chronicle/integration/integrations.py @@ -0,0 +1,656 @@ +# Copyright 2025 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. +# +"""Integrations functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + DiffType, + TargetMode, + PythonVersion, + IntegrationType, +) + +from secops.chronicle.utils.format_utils import build_patch_body +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integrations( + client: "ChronicleClient", + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """Get a list of integrations. + + Args: + client: ChronicleClient instance + page_size: Number of results to return per page + page_token: Token for the page to retrieve + filter_string: Filter expression to filter integrations + order_by: Field to sort the integrations by + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of integrations instead + of a dict with integrations list and nextPageToken. + + Returns: + If as_list is True: List of integrations. + If as_list is False: Dict with integrations list and + nextPageToken. + + Raises: + APIError: If the API request fails + """ + param_fields = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + param_fields = {k: v for k, v in param_fields.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path="integrations", + items_key="integrations", + page_size=page_size, + page_token=page_token, + extra_params=param_fields, + as_list=as_list, + ) + + +def get_integration( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get details of a specific integration. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to retrieve + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}", + api_version=api_version, + ) + + +def delete_integration( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Deletes a specific custom Integration. Commercial integrations cannot + be deleted via this method. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to delete + api_version: API version to use for the request. Default is V1BETA. + + Raises: + APIError: If the API request fails + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=f"integrations/{integration_name}", + api_version=api_version, + ) + + +def create_integration( + client: "ChronicleClient", + display_name: str, + staging: bool, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Creates a new custom SOAR Integration. + + Args: + client: ChronicleClient instance + display_name: Required. The display name of the integration (max 150 characters) + staging: Required. True if the integration is in staging mode + description: Optional. The integration's description (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50). Each parameter is a dict + with keys: id, defaultValue, displayName, propertyName, type, description, + mandatory + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type (response/extension) + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the details of the newly created integration + + Raises: + APIError: If the API request fails + """ + body_fields = { + "displayName": display_name, + "staging": staging, + "description": description, + "imageBase64": image_base64, + "svgIcon": svg_icon, + "pythonVersion": python_version, + "parameters": parameters, + "categories": categories, + "type": integration_type, + } + + # Remove keys with None values + body_fields = {k: v for k, v in body_fields.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path="integrations", + json=body_fields, + api_version=api_version, + ) + + +def download_integration( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Exports the entire integration package as a ZIP file. Includes all + scripts, definitions, and the manifest file. Use this method for backup + or sharing. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to download + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the configuration of the specified integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}:export", + api_version=api_version, + ) + + +def download_integration_dependency( + client: "ChronicleClient", + integration_name: str, + dependency_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Initiates the download of a Python dependency (e.g., a library from + PyPI) for a custom integration. + + Args: + client: ChronicleClient instance + integration_name: name of the integration whose dependency to download + dependency_name: The dependency name to download. It can contain the + version or the repository. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the details of the downloaded dependency + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{integration_name}:downloadDependency", + json={"dependency": dependency_name}, + api_version=api_version, + ) + + +def export_integration_items( + client: "ChronicleClient", + integration_name: str, + actions: list[str] | None = None, + jobs: list[str] | None = None, + connectors: list[str] | None = None, + managers: list[str] | None = None, + transformers: list[str] | None = None, + logical_operators: list[str] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Exports specific items from an integration into a ZIP folder. Use + this method to extract only a subset of capabilities (e.g., just the + connectors) for reuse. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to export items from + actions: Optional. A list the ids of the actions to export. Format: + [1,2,3] + jobs: Optional. A list the ids of the jobs to export. Format: + [1,2,3] + connectors: Optional. A list the ids of the connectors to export. + Format: [1,2,3] + managers: Optional. A list the ids of the managers to export. Format: + [1,2,3] + transformers: Optional. A list the ids of the transformers to export. + Format: [1,2,3] + logical_operators: Optional. A list the ids of the logical + operators to export. Format: [1,2,3] + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the exported items of the specified integration + + Raises: + APIError: If the API request fails + """ + export_items = { + "actions": ",".join(actions) if actions else None, + "jobs": jobs, + "connectors": connectors, + "managers": managers, + "transformers": transformers, + "logicalOperators": logical_operators, + } + + # Remove keys with None values + export_items = {k: v for k, v in export_items.items() if v is not None} + + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}:exportItems", + params=export_items, + api_version=api_version, + ) + + +def get_integration_affected_items( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Identifies all system items (e.g., connector instances, job instances, + playbooks) that would be affected by a change to or deletion of this + integration. Use this method to conduct impact analysis before making + breaking changes. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to check for affected items + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the list of items affected by changes to the specified + integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}:fetchAffectedItems", + api_version=api_version, + ) + + +def get_agent_integrations( + client: "ChronicleClient", + agent_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Returns the set of integrations currently installed and configured on + a specific agent. + + Args: + client: ChronicleClient instance + agent_id: The agent identifier + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the list of agent-based integrations + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path="integrations:fetchAgentIntegrations", + params={"agentId": agent_id}, + api_version=api_version, + ) + + +def get_integration_dependencies( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Returns the complete list of Python dependencies currently associated + with a custom integration. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to check for dependencies + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the list of dependencies for the specified integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}:fetchDependencies", + api_version=api_version, + ) + + +def get_integration_restricted_agents( + client: "ChronicleClient", + integration_name: str, + required_python_version: PythonVersion, + push_request: bool = False, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Identifies remote agents that would be restricted from running an + updated version of the integration, typically due to environment + incompatibilities like unsupported Python versions. + + Args: + client: ChronicleClient instance + integration_name: name of the integration to check for restricted agents + required_python_version: Python version required for the updated + integration. + push_request: Optional. Indicates whether the integration is + pushed to a different mode (production/staging). False by default. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the list of agents that would be restricted from running + + Raises: + APIError: If the API request fails + """ + params_fields = { + "requiredPythonVersion": required_python_version.value, + "pushRequest": push_request, + } + + # Remove keys with None values + params_fields = {k: v for k, v in params_fields.items() if v is not None} + + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}:fetchRestrictedAgents", + params=params_fields, + api_version=api_version, + ) + + +def get_integration_diff( + client: "ChronicleClient", + integration_name: str, + diff_type: DiffType = DiffType.COMMERCIAL, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get the configuration diff of a specific integration. + + Args: + client: ChronicleClient instance + integration_name: ID of the integration to retrieve the diff for + diff_type: Type of diff to retrieve (Commercial, Production, or Staging). + Default is Commercial. + COMMERCIAL: Diff between the commercial version of the + integration and the current version in the environment. + PRODUCTION: Returns the difference between the staging + integration and its matching production version. + STAGING: Returns the difference between the production + integration and its corresponding staging version. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the configuration diff of the specified integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{integration_name}" + f":fetch{diff_type.value}Diff", + api_version=api_version, + ) + + +def transition_integration( + client: "ChronicleClient", + integration_name: str, + target_mode: TargetMode, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Transition an integration to a different environment (e.g. staging to production). + + Args: + client: ChronicleClient instance + integration_name: ID of the integration to transition + target_mode: Target mode to transition the integration to: + PRODUCTION: Transition the integration to production environment. + STAGING: Transition the integration to staging environment. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the details of the transitioned integration + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{integration_name}" + f":pushTo{target_mode.value}", + api_version=api_version, + ) + + +def update_integration( + client: "ChronicleClient", + integration_name: str, + display_name: str | None = None, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + staging: bool | None = None, + dependencies_to_remove: list[str] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing integration. + + Args: + client: ChronicleClient instance + integration_name: ID of the integration to update + display_name: Optional. The display name of the integration (max 150 characters) + description: Optional. The integration's description (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50) + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type (response/extension) + staging: Optional. True if the integration is in staging mode + dependencies_to_remove: Optional. List of dependencies to remove from the + integration. + update_mask: Optional. Comma-separated list of fields to update. + If not provided, all non-None fields will be updated. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the details of the updated integration + + Raises: + APIError: If the API request fails + """ + body, params = build_patch_body( + field_map=[ + ("displayName", "display_name", display_name), + ("description", "description", description), + ("imageBase64", "image_base64", image_base64), + ("svgIcon", "svg_icon", svg_icon), + ("pythonVersion", "python_version", python_version), + ("parameters", "parameters", parameters), + ("categories", "categories", categories), + ("integrationType", "integration_type", integration_type), + ("staging", "staging", staging), + ], + update_mask=update_mask, + ) + + if dependencies_to_remove is not None: + params = params or {} + params["dependenciesToRemove"] = ",".join(dependencies_to_remove) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=f"integrations/{integration_name}", + json=body, + params=params, + api_version=api_version, + ) + +def update_custom_integration( + client: "ChronicleClient", + integration_name: str, + display_name: str | None = None, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + staging: bool | None = None, + dependencies_to_remove: list[str] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Updates a custom integration definition, including its parameters and + dependencies. Use this method to refine the operational behavior of a + locally developed integration. + + Args: + client: ChronicleClient instance + integration_name: Name of the integration to update + display_name: Optional. The display name of the integration (max 150 characters) + description: Optional. The integration's description (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50) + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type (response/extension) + staging: Optional. True if the integration is in staging mode + dependencies_to_remove: Optional. List of dependencies to remove from + the integration + update_mask: Optional. Comma-separated list of fields to update. + If not provided, all non-None fields will be updated. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing: + - successful: Whether the integration was updated successfully + - integration: The updated integration (populated if successful) + - dependencies: Dependency installation statuses (populated if failed) + + Raises: + APIError: If the API request fails + """ + integration_fields = { + "name": integration_name, + "displayName": display_name, + "description": description, + "imageBase64": image_base64, + "svgIcon": svg_icon, + "pythonVersion": python_version, + "parameters": parameters, + "categories": categories, + "type": integration_type, + "staging": staging, + } + + # Remove keys with None values + integration_fields = {k: v for k, v in integration_fields.items() if v is not None} + + body = {"integration": integration_fields} + + if dependencies_to_remove is not None: + body["dependenciesToRemove"] = dependencies_to_remove + + params = {"updateMask": update_mask} if update_mask else None + + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{integration_name}:updateCustomIntegration", + json=body, + params=params, + api_version=api_version, + ) \ No newline at end of file diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index 0074bc53..61baa503 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -73,6 +73,38 @@ class DetectionType(StrEnum): CASE = "DETECTION_TYPE_CASE" +class PythonVersion(str, Enum): + """Python version for compatibility checks.""" + + UNSPECIFIED = "PYTHON_VERSION_UNSPECIFIED" + PYTHON_2_7 = "V2_7" + PYTHON_3_7 = "V3_7" + PYTHON_3_11 = "V3_11" + + +class DiffType(str, Enum): + """Type of diff to retrieve.""" + + COMMERCIAL = "Commercial" + PRODUCTION = "Production" + STAGING = "Staging" + + +class TargetMode(str, Enum): + """Target mode for integration transition.""" + + PRODUCTION = "Production" + STAGING = "Staging" + + +class IntegrationType(str, Enum): + """Type of integration.""" + + UNSPECIFIED = "INTEGRATION_TYPE_UNSPECIFIED" + RESPONSE = "RESPONSE" + EXTENSION = "EXTENSION" + + @dataclass class TimeInterval: """Time interval with start and end times.""" diff --git a/src/secops/chronicle/utils/format_utils.py b/src/secops/chronicle/utils/format_utils.py new file mode 100644 index 00000000..71b7b124 --- /dev/null +++ b/src/secops/chronicle/utils/format_utils.py @@ -0,0 +1,40 @@ +# Copyright 2025 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. +# +"""Formatting helper functions for Chronicle.""" + +from typing import Any + +def build_patch_body( + field_map: list[tuple[str, str, Any]], + update_mask: str | None = None, +) -> tuple[dict[str, Any], dict[str, Any] | None]: + """Build a request body and params dict for a PATCH request. + + Args: + field_map: List of (api_key, mask_key, value) tuples for + each optional field. + update_mask: Explicit update mask. If provided, + overrides the auto-generated mask. + + Returns: + Tuple of (body, params) where params contains the updateMask or is None. + """ + body = {api_key: value for api_key, _, value in field_map if value is not None} + mask_fields = [mask_key for _, mask_key, value in field_map if value is not None] + + resolved_mask = update_mask or (",".join(mask_fields) if mask_fields else None) + params = {"updateMask": resolved_mask} if resolved_mask else None + + return body, params \ No newline at end of file From 083276aa6c5b3c4305bbae7ab8329bbad6fb626e Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 2 Mar 2026 21:19:08 +0000 Subject: [PATCH 15/46] feat: implement bytes request helper for download functions --- src/secops/chronicle/utils/request_utils.py | 64 ++++++- tests/chronicle/utils/test_request_utils.py | 179 ++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) diff --git a/src/secops/chronicle/utils/request_utils.py b/src/secops/chronicle/utils/request_utils.py index 43f2d885..c766a6ae 100644 --- a/src/secops/chronicle/utils/request_utils.py +++ b/src/secops/chronicle/utils/request_utils.py @@ -14,7 +14,7 @@ # """Helper functions for Chronicle.""" -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import requests from google.auth.exceptions import GoogleAuthError @@ -297,3 +297,65 @@ def chronicle_request( ) return data + + +def chronicle_request_bytes( + client: "ChronicleClient", + method: str, + endpoint_path: str, + *, + api_version: str = APIVersion.V1, + params: Optional[dict[str, Any]] = None, + headers: Optional[dict[str, Any]] = None, + expected_status: int | set[int] | tuple[int, ...] | list[int] = 200, + error_message: str | None = None, + timeout: int | None = None, +) -> bytes: + base = f"{client.base_url(api_version)}/{client.instance_id}" + + if endpoint_path.startswith(":"): + url = f"{base}{endpoint_path}" + else: + url = f'{base}/{endpoint_path.lstrip("/")}' + + try: + response = client.session.request( + method=method, + url=url, + params=params, + headers=headers, + timeout=timeout, + stream=True, + ) + except GoogleAuthError as exc: + base_msg = error_message or "Google authentication failed" + raise APIError(f"{base_msg}: authentication_error={exc}") from exc + except requests.RequestException as exc: + base_msg = error_message or "API request failed" + raise APIError( + f"{base_msg}: method={method}, url={url}, " + f"request_error={exc.__class__.__name__}, detail={exc}" + ) from exc + + if isinstance(expected_status, (set, tuple, list)): + status_ok = response.status_code in expected_status + else: + status_ok = response.status_code == expected_status + + if not status_ok: + # try json for detail, else preview text + try: + data = response.json() + raise APIError( + f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"status={response.status_code}, response={data}" + ) from None + except ValueError: + preview = _safe_body_preview(getattr(response, "text", ""), + limit=MAX_BODY_CHARS) + raise APIError( + f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"status={response.status_code}, response_text={preview}" + ) from None + + return response.content \ No newline at end of file diff --git a/tests/chronicle/utils/test_request_utils.py b/tests/chronicle/utils/test_request_utils.py index 6f8687a2..c4e8b5b9 100644 --- a/tests/chronicle/utils/test_request_utils.py +++ b/tests/chronicle/utils/test_request_utils.py @@ -26,6 +26,7 @@ from secops.chronicle.utils.request_utils import ( DEFAULT_PAGE_SIZE, chronicle_request, + chronicle_request_bytes, chronicle_paginated_request, ) from secops.exceptions import APIError @@ -655,3 +656,181 @@ def test_chronicle_request_non_json_error_body_is_truncated(client: Mock) -> Non assert "status=500" in msg # Should not include the full 5000 chars, should include truncation marker assert "truncated" in msg + + +# --------------------------------------------------------------------------- +# chronicle_request_bytes() tests +# --------------------------------------------------------------------------- + +def test_chronicle_request_bytes_success_returns_content_and_stream_true(client: Mock) -> None: + resp = _mock_response(status_code=200, json_value={"ignored": True}) + resp.content = b"PK\x03\x04...zip-bytes..." # ZIP magic prefix in real life + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="integrations/foo:export", + api_version=APIVersion.V1BETA, + params={"alt": "media"}, + headers={"Accept": "application/zip"}, + ) + + assert out == b"PK\x03\x04...zip-bytes..." + + client.base_url.assert_called_once_with(APIVersion.V1BETA) + client.session.request.assert_called_once_with( + method="GET", + url="https://example.test/chronicle/instances/instance-1/integrations/foo:export", + params={"alt": "media"}, + headers={"Accept": "application/zip"}, + timeout=None, + stream=True, + ) + + +def test_chronicle_request_bytes_builds_url_for_rpc_colon_prefix(client: Mock) -> None: + resp = _mock_response(status_code=200, json_value={"ok": True}) + resp.content = b"binary" + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="POST", + endpoint_path=":exportSomething", + api_version=APIVersion.V1ALPHA, + ) + + assert out == b"binary" + + _, kwargs = client.session.request.call_args + assert kwargs["url"] == "https://example.test/chronicle/instances/instance-1:exportSomething" + assert kwargs["stream"] is True + + +def test_chronicle_request_bytes_accepts_multiple_expected_statuses_set(client: Mock) -> None: + resp = _mock_response(status_code=204, json_value=None) + resp.content = b"" + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="DELETE", + endpoint_path="something", + api_version=APIVersion.V1ALPHA, + expected_status={200, 204}, + ) + + assert out == b"" + + +def test_chronicle_request_bytes_status_mismatch_with_json_includes_json(client: Mock) -> None: + resp = _mock_response(status_code=400, json_value={"error": "bad"}) + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"API request failed: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/curatedRules" + r", status=400, response={'error': 'bad'}", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + +def test_chronicle_request_bytes_status_mismatch_non_json_includes_text(client: Mock) -> None: + resp = _mock_response(status_code=500, json_raises=True, text="boom") + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"API request failed: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/curatedRules, " + r"status=500, response_text=boom", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + +def test_chronicle_request_bytes_custom_error_message_used(client: Mock) -> None: + resp = _mock_response(status_code=404, json_value={"message": "not found"}) + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"Failed to download export: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/integrations/foo:export, " + r"status=404, response={'message': 'not found'}", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="integrations/foo:export", + api_version=APIVersion.V1BETA, + error_message="Failed to download export", + ) + + +def test_chronicle_request_bytes_wraps_requests_exception(client: Mock) -> None: + client.session.request.side_effect = requests.RequestException("no route to host") + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "API request failed" in msg + assert "method=GET" in msg + assert "url=https://example.test/chronicle/instances/instance-1/curatedRules" in msg + assert "request_error=RequestException" in msg + + +def test_chronicle_request_bytes_wraps_google_auth_error(client: Mock) -> None: + client.session.request.side_effect = GoogleAuthError("invalid_grant") + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "Google authentication failed" in msg + assert "authentication_error=" in msg + + +def test_chronicle_request_bytes_non_json_error_body_is_truncated(client: Mock) -> None: + long_text = "x" * 5000 + resp = _mock_response(status_code=500, json_raises=True, text=long_text) + resp.content = b"" + resp.headers = {"Content-Type": "text/plain"} + client.session.request.return_value = resp + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "status=500" in msg + assert "truncated" in msg \ No newline at end of file From ae13e722cfd6e11dae2bfb02a185d96f0630df77 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 3 Mar 2026 12:56:29 +0000 Subject: [PATCH 16/46] feat: implement bytes request helper for download functions --- src/secops/chronicle/client.py | 487 +++++++++++++++++- .../chronicle/integration/integrations.py | 17 +- 2 files changed, 487 insertions(+), 17 deletions(-) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index fa963588..5235ecdf 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -137,18 +137,33 @@ uninstall_marketplace_integration as _uninstall_marketplace_integration, ) from secops.chronicle.integration.integrations import ( - DiffType, - list_integrations as _list_integrations, + create_integration as _create_integration, + delete_integration as _delete_integration, + download_integration as _download_integration, + download_integration_dependency as _download_integration_dependency, + export_integration_items as _export_integration_items, + get_agent_integrations as _get_agent_integrations, get_integration as _get_integration, + get_integration_affected_items as _get_integration_affected_items, + get_integration_dependencies as _get_integration_dependencies, get_integration_diff as _get_integration_diff, + get_integration_restricted_agents as _get_integration_restricted_agents, + list_integrations as _list_integrations, + transition_integration as _transition_integration, + update_custom_integration as _update_custom_integration, + update_integration as _update_integration, ) from secops.chronicle.models import ( APIVersion, CaseList, DashboardChart, DashboardQuery, + DiffType, EntitySummary, InputInterval, + IntegrationType, + PythonVersion, + TargetMode, TileType, ) from secops.chronicle.nl_search import ( @@ -686,6 +701,10 @@ def update_watchlist( update_mask, ) + # ------------------------------------------------------------------------- + # Marketplace Integration methods + # ------------------------------------------------------------------------- + def list_marketplace_integrations( self, page_size: int | None = None, @@ -828,6 +847,10 @@ def uninstall_marketplace_integration( self, integration_name, api_version ) + # ------------------------------------------------------------------------- + # Integration methods + # ------------------------------------------------------------------------- + def list_integrations( self, page_size: int | None = None, @@ -842,16 +865,16 @@ def list_integrations( Args: page_size: Maximum number of integrations to return per page page_token: Token for the next page of results, if available - filter_string: Filter expression to filter integrations + filter_string: Filter expression to filter integrations. + Only supports "displayName:" prefix. order_by: Field to sort the integrations by api_version: API version to use. Defaults to V1BETA as_list: If True, return a list of integrations instead of a dict with integration list and nextPageToken. Returns: - If as_list is True: List of integration. - If as_list is False: Dict with integration list and - nextPageToken. + If as_list is True: List of integrations. + If as_list is False: Dict with integration list and nextPageToken. Raises: APIError: If the API request fails @@ -885,6 +908,250 @@ def get_integration( """ return _get_integration(self, integration_name, api_version) + def delete_integration( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Deletes a specific custom integration. Commercial integrations + cannot be deleted via this method. + + Args: + integration_name: Name of the integration to delete + api_version: API version to use for the request. + Default is V1BETA. + + Raises: + APIError: If the API request fails + """ + _delete_integration(self, integration_name, api_version) + + def create_integration( + self, + display_name: str, + staging: bool, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Creates a new custom SOAR integration. + + Args: + display_name: Required. The display name of the integration + (max 150 characters) + staging: Required. True if the integration is in staging mode + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a + base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50) + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type + (response/extension) + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the details of the newly created integration + + Raises: + APIError: If the API request fails + """ + return _create_integration( + self, + display_name=display_name, + staging=staging, + description=description, + image_base64=image_base64, + svg_icon=svg_icon, + python_version=python_version, + parameters=parameters, + categories=categories, + integration_type=integration_type, + api_version=api_version, + ) + + def download_integration( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> bytes: + """Exports the entire integration package as a ZIP file. Includes + all scripts, definitions, and the manifest file. Use this method + for backup or sharing. + + Args: + integration_name: Name of the integration to download + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Bytes of the ZIP file containing the integration package + + Raises: + APIError: If the API request fails + """ + return _download_integration(self, integration_name, api_version) + + def download_integration_dependency( + self, + integration_name: str, + dependency_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Initiates the download of a Python dependency (e.g., a library + from PyPI) for a custom integration. + + Args: + integration_name: Name of the integration whose dependency + to download + dependency_name: The dependency name to download. It can + contain the version or the repository. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the dependency installation result with keys: + - successful: True if installation was successful + - error: Error message if installation failed + + Raises: + APIError: If the API request fails + """ + return _download_integration_dependency( + self, integration_name, dependency_name, api_version + ) + + def export_integration_items( + self, + integration_name: str, + actions: list[str] | str | None = None, + jobs: list[str] | str | None = None, + connectors: list[str] | str | None = None, + managers: list[str] | str | None = None, + transformers: list[str] | str | None = None, + logical_operators: list[str] | str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> bytes: + """Exports specific items from an integration into a ZIP folder. + Use this method to extract only a subset of capabilities (e.g., + just the connectors) for reuse. + + Args: + integration_name: Name of the integration to export items from + actions: Optional. IDs of the actions to export as a list or + comma-separated string. Format: [1,2,3] or "1,2,3" + jobs: Optional. IDs of the jobs to export as a list or + comma-separated string. + connectors: Optional. IDs of the connectors to export as a + list or comma-separated string. + managers: Optional. IDs of the managers to export as a list + or comma-separated string. + transformers: Optional. IDs of the transformers to export as + a list or comma-separated string. + logical_operators: Optional. IDs of the logical operators to + export as a list or comma-separated string. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Bytes of the ZIP file containing the exported items + + Raises: + APIError: If the API request fails + """ + return _export_integration_items( + self, + integration_name, + actions=actions, + jobs=jobs, + connectors=connectors, + managers=managers, + transformers=transformers, + logical_operators=logical_operators, + api_version=api_version, + ) + + def get_integration_affected_items( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Identifies all system items (e.g., connector instances, job + instances, playbooks) that would be affected by a change to or + deletion of this integration. Use this method to conduct impact + analysis before making breaking changes. + + Args: + integration_name: Name of the integration to check for + affected items + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the list of items affected by changes to + the specified integration + + Raises: + APIError: If the API request fails + """ + return _get_integration_affected_items( + self, integration_name, api_version + ) + + def get_agent_integrations( + self, + agent_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Returns the set of integrations currently installed and + configured on a specific agent. + + Args: + agent_id: The agent identifier + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the list of agent-based integrations + + Raises: + APIError: If the API request fails + """ + return _get_agent_integrations(self, agent_id, api_version) + + def get_integration_dependencies( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Returns the complete list of Python dependencies currently + associated with a custom integration. + + Args: + integration_name: Name of the integration to check for + dependencies + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the list of dependencies for the specified + integration + + Raises: + APIError: If the API request fails + """ + return _get_integration_dependencies( + self, integration_name, api_version + ) + def get_integration_diff( self, integration_name: str, @@ -895,14 +1162,14 @@ def get_integration_diff( Args: integration_name: ID of the integration to retrieve the diff for - diff_type: Type of diff to retrieve (Commercial, Production, or Staging). - Default is Commercial. + diff_type: Type of diff to retrieve (Commercial, Production, or + Staging). Default is Commercial. COMMERCIAL: Diff between the commercial version of the - integration and the current version in the environment. + integration and the current version in the environment. PRODUCTION: Returns the difference between the staging integration and its matching production version. STAGING: Returns the difference between the production - integration and its corresponding staging version. + integration and its corresponding staging version. api_version: API version to use for the request. Default is V1BETA. Returns: @@ -915,6 +1182,204 @@ def get_integration_diff( self, integration_name, diff_type, api_version ) + def get_integration_restricted_agents( + self, + integration_name: str, + required_python_version: PythonVersion, + push_request: bool = False, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Identifies remote agents that would be restricted from running + an updated version of the integration, typically due to environment + incompatibilities like unsupported Python versions. + + Args: + integration_name: Name of the integration to check for + restricted agents + required_python_version: Python version required for the + updated integration + push_request: Optional. Indicates whether the integration is + being pushed to a different mode (production/staging). + False by default. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the list of agents that would be restricted + from running the updated integration + + Raises: + APIError: If the API request fails + """ + return _get_integration_restricted_agents( + self, + integration_name, + required_python_version=required_python_version, + push_request=push_request, + api_version=api_version, + ) + + def transition_integration( + self, + integration_name: str, + target_mode: TargetMode, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Transitions an integration to a different environment + (e.g. staging to production). + + Args: + integration_name: Name of the integration to transition + target_mode: Target mode to transition the integration to. + PRODUCTION: Transition the integration to production. + STAGING: Transition the integration to staging. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the details of the transitioned integration + + Raises: + APIError: If the API request fails + """ + return _transition_integration( + self, integration_name, target_mode, api_version + ) + + def update_integration( + self, + integration_name: str, + display_name: str | None = None, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + staging: bool | None = None, + dependencies_to_remove: list[str] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Updates an existing integration's metadata. Use this method to + change the description or display image of a custom integration. + + Args: + integration_name: Name of the integration to update + display_name: Optional. The display name of the integration + (max 150 characters) + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a + base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50) + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type + (response/extension) + staging: Optional. True if the integration is in staging mode + dependencies_to_remove: Optional. List of dependencies to + remove from the integration + update_mask: Optional. Comma-separated list of fields to + update. If not provided, all non-None fields are updated. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing the details of the updated integration + + Raises: + APIError: If the API request fails + """ + return _update_integration( + self, + integration_name, + display_name=display_name, + description=description, + image_base64=image_base64, + svg_icon=svg_icon, + python_version=python_version, + parameters=parameters, + categories=categories, + integration_type=integration_type, + staging=staging, + dependencies_to_remove=dependencies_to_remove, + update_mask=update_mask, + api_version=api_version, + ) + + def update_custom_integration( + self, + integration_name: str, + display_name: str | None = None, + description: str | None = None, + image_base64: str | None = None, + svg_icon: str | None = None, + python_version: PythonVersion | None = None, + parameters: list[dict[str, Any]] | None = None, + categories: list[str] | None = None, + integration_type: IntegrationType | None = None, + staging: bool | None = None, + dependencies_to_remove: list[str] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Updates a custom integration definition, including its + parameters and dependencies. Use this method to refine the + operational behavior of a locally developed integration. + + Args: + integration_name: Name of the integration to update + display_name: Optional. The display name of the integration + (max 150 characters) + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a + base64 string (max 5 MB) + svg_icon: Optional. The integration's SVG icon (max 1 MB) + python_version: Optional. The integration's Python version + parameters: Optional. Integration parameters (max 50) + categories: Optional. Integration categories (max 50) + integration_type: Optional. The integration's type + (response/extension) + staging: Optional. True if the integration is in staging mode + dependencies_to_remove: Optional. List of dependencies to + remove from the integration + update_mask: Optional. Comma-separated list of fields to + update. If not provided, all non-None fields are updated. + api_version: API version to use for the request. + Default is V1BETA. + + Returns: + Dict containing: + - successful: Whether the integration was updated + successfully + - integration: The updated integration (if successful) + - dependencies: Dependency installation statuses + (if failed) + + Raises: + APIError: If the API request fails + """ + return _update_custom_integration( + self, + integration_name, + display_name=display_name, + description=description, + image_base64=image_base64, + svg_icon=svg_icon, + python_version=python_version, + parameters=parameters, + categories=categories, + integration_type=integration_type, + staging=staging, + dependencies_to_remove=dependencies_to_remove, + update_mask=update_mask, + api_version=api_version, + ) + def get_stats( self, query: str, @@ -4674,4 +5139,4 @@ def update_rule_deployment( alerting=alerting, archived=archived, run_frequency=run_frequency, - ) + ) \ No newline at end of file diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py index 814904f5..12672019 100644 --- a/src/secops/chronicle/integration/integrations.py +++ b/src/secops/chronicle/integration/integrations.py @@ -27,6 +27,7 @@ from secops.chronicle.utils.format_utils import build_patch_body from secops.chronicle.utils.request_utils import ( chronicle_paginated_request, + chronicle_request_bytes, chronicle_request, ) @@ -197,7 +198,7 @@ def download_integration( client: "ChronicleClient", integration_name: str, api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: +) -> bytes: """Exports the entire integration package as a ZIP file. Includes all scripts, definitions, and the manifest file. Use this method for backup or sharing. @@ -208,16 +209,18 @@ def download_integration( api_version: API version to use for the request. Default is V1BETA. Returns: - Dict containing the configuration of the specified integration + Bytes of the ZIP file containing the integration package Raises: APIError: If the API request fails """ - return chronicle_request( + return chronicle_request_bytes( client, method="GET", endpoint_path=f"integrations/{integration_name}:export", api_version=api_version, + params={"alt": "media"}, + headers={"Accept": "application/zip"}, ) @@ -262,7 +265,7 @@ def export_integration_items( transformers: list[str] | None = None, logical_operators: list[str] | None = None, api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: +) -> bytes: """Exports specific items from an integration into a ZIP folder. Use this method to extract only a subset of capabilities (e.g., just the connectors) for reuse. @@ -285,7 +288,7 @@ def export_integration_items( api_version: API version to use for the request. Default is V1BETA. Returns: - Dict containing the exported items of the specified integration + Bytes of the ZIP file containing the exported integration items Raises: APIError: If the API request fails @@ -297,17 +300,19 @@ def export_integration_items( "managers": managers, "transformers": transformers, "logicalOperators": logical_operators, + "alt": "media", } # Remove keys with None values export_items = {k: v for k, v in export_items.items() if v is not None} - return chronicle_request( + return chronicle_request_bytes( client, method="GET", endpoint_path=f"integrations/{integration_name}:exportItems", params=export_items, api_version=api_version, + headers={"Accept": "application/zip"}, ) From 0785c0ba64a0cbe7cd28a6f0eb17b277509e3472 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 3 Mar 2026 13:25:22 +0000 Subject: [PATCH 17/46] chore: linting and formatting --- src/secops/chronicle/client.py | 2 +- .../chronicle/integration/integrations.py | 57 ++++++++++++------- src/secops/chronicle/utils/format_utils.py | 4 +- src/secops/chronicle/utils/request_utils.py | 6 +- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 0d3c5ffa..351c81aa 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -5219,4 +5219,4 @@ def update_rule_deployment( alerting=alerting, archived=archived, run_frequency=run_frequency, - ) \ No newline at end of file + ) diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py index 12672019..3982eb86 100644 --- a/src/secops/chronicle/integration/integrations.py +++ b/src/secops/chronicle/integration/integrations.py @@ -151,15 +151,18 @@ def create_integration( Args: client: ChronicleClient instance - display_name: Required. The display name of the integration (max 150 characters) + display_name: Required. The display name of the integration + (max 150 characters) staging: Required. True if the integration is in staging mode - description: Optional. The integration's description (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as + a base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50). Each parameter is a dict - with keys: id, defaultValue, displayName, propertyName, type, description, - mandatory + parameters: Optional. Integration parameters (max 50). Each parameter + is a dict with keys: id, defaultValue, displayName, + propertyName, type, description, mandatory categories: Optional. Integration categories (max 50) integration_type: Optional. The integration's type (response/extension) api_version: API version to use for the request. Default is V1BETA. @@ -455,8 +458,8 @@ def get_integration_diff( Args: client: ChronicleClient instance integration_name: ID of the integration to retrieve the diff for - diff_type: Type of diff to retrieve (Commercial, Production, or Staging). - Default is Commercial. + diff_type: Type of diff to retrieve + (Commercial, Production, or Staging). Default is Commercial. COMMERCIAL: Diff between the commercial version of the integration and the current version in the environment. PRODUCTION: Returns the difference between the staging @@ -486,7 +489,8 @@ def transition_integration( target_mode: TargetMode, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Transition an integration to a different environment (e.g. staging to production). + """Transition an integration to a different environment + (e.g. staging to production). Args: client: ChronicleClient instance @@ -532,17 +536,20 @@ def update_integration( Args: client: ChronicleClient instance integration_name: ID of the integration to update - display_name: Optional. The display name of the integration (max 150 characters) - description: Optional. The integration's description (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + display_name: Optional. The display name of the integration + (max 150 characters) + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a + base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version parameters: Optional. Integration parameters (max 50) categories: Optional. Integration categories (max 50) integration_type: Optional. The integration's type (response/extension) staging: Optional. True if the integration is in staging mode - dependencies_to_remove: Optional. List of dependencies to remove from the - integration. + dependencies_to_remove: Optional. List of dependencies to + remove from the integration. update_mask: Optional. Comma-separated list of fields to update. If not provided, all non-None fields will be updated. api_version: API version to use for the request. Default is V1BETA. @@ -581,6 +588,7 @@ def update_integration( api_version=api_version, ) + def update_custom_integration( client: "ChronicleClient", integration_name: str, @@ -604,9 +612,12 @@ def update_custom_integration( Args: client: ChronicleClient instance integration_name: Name of the integration to update - display_name: Optional. The display name of the integration (max 150 characters) - description: Optional. The integration's description (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a base64 string (max 5 MB) + display_name: Optional. The display name of the integration + (max 150 characters) + description: Optional. The integration's description + (max 1,500 characters) + image_base64: Optional. The integration's image encoded as a + base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version parameters: Optional. Integration parameters (max 50) @@ -623,7 +634,8 @@ def update_custom_integration( Dict containing: - successful: Whether the integration was updated successfully - integration: The updated integration (populated if successful) - - dependencies: Dependency installation statuses (populated if failed) + - dependencies: Dependency installation statuses + (populated if failed) Raises: APIError: If the API request fails @@ -642,7 +654,9 @@ def update_custom_integration( } # Remove keys with None values - integration_fields = {k: v for k, v in integration_fields.items() if v is not None} + integration_fields = { + k: v for k, v in integration_fields.items() if v is not None + } body = {"integration": integration_fields} @@ -654,8 +668,9 @@ def update_custom_integration( return chronicle_request( client, method="POST", - endpoint_path=f"integrations/{integration_name}:updateCustomIntegration", + endpoint_path=f"integrations/" + f"{integration_name}:updateCustomIntegration", json=body, params=params, api_version=api_version, - ) \ No newline at end of file + ) diff --git a/src/secops/chronicle/utils/format_utils.py b/src/secops/chronicle/utils/format_utils.py index 38ddc837..d3812fa5 100644 --- a/src/secops/chronicle/utils/format_utils.py +++ b/src/secops/chronicle/utils/format_utils.py @@ -66,7 +66,7 @@ def parse_json_list( raise APIError(f"Invalid {field_name} JSON") from e return value - +#pylint: disable=line-too-long def build_patch_body( field_map: list[tuple[str, str, Any]], update_mask: str | None = None, @@ -88,4 +88,4 @@ def build_patch_body( resolved_mask = update_mask or (",".join(mask_fields) if mask_fields else None) params = {"updateMask": resolved_mask} if resolved_mask else None - return body, params \ No newline at end of file + return body, params diff --git a/src/secops/chronicle/utils/request_utils.py b/src/secops/chronicle/utils/request_utils.py index c766a6ae..898ca85c 100644 --- a/src/secops/chronicle/utils/request_utils.py +++ b/src/secops/chronicle/utils/request_utils.py @@ -347,15 +347,15 @@ def chronicle_request_bytes( try: data = response.json() raise APIError( - f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"{error_message or "API request failed"}: method={method}, url={url}, " f"status={response.status_code}, response={data}" ) from None except ValueError: preview = _safe_body_preview(getattr(response, "text", ""), limit=MAX_BODY_CHARS) raise APIError( - f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"{error_message or "API request failed"}: method={method}, url={url}, " f"status={response.status_code}, response_text={preview}" ) from None - return response.content \ No newline at end of file + return response.content From efe32b87dce9d68025db33610f034d23d4b57988 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 3 Mar 2026 15:43:07 +0000 Subject: [PATCH 18/46] fix: updates based on testing errors --- .../integration/marketplace_integrations.py | 10 ++--- .../test_marketplace_integrations.py | 38 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/secops/chronicle/integration/marketplace_integrations.py b/src/secops/chronicle/integration/marketplace_integrations.py index 48596ef5..0297d470 100644 --- a/src/secops/chronicle/integration/marketplace_integrations.py +++ b/src/secops/chronicle/integration/marketplace_integrations.py @@ -129,10 +129,10 @@ def get_marketplace_integration_diff( def install_marketplace_integration( client: "ChronicleClient", integration_name: str, - override_mapping: bool | None = False, - staging: bool | None = False, + override_mapping: bool | None = None, + staging: bool | None = None, version: str | None = None, - restore_from_snapshot: bool | None = False, + restore_from_snapshot: bool | None = None, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: """Install a marketplace integration by integration name @@ -161,7 +161,7 @@ def install_marketplace_integration( "overrideMapping": override_mapping, "staging": staging, "version": version, - "restoreIntegrationSnapshot": restore_from_snapshot, + "restoreFromSnapshot": restore_from_snapshot, } return chronicle_request( @@ -196,4 +196,4 @@ def uninstall_marketplace_integration( method="POST", endpoint_path=f"marketplaceIntegrations/{integration_name}:uninstall", api_version=api_version, - ) + ) \ No newline at end of file diff --git a/tests/chronicle/test_marketplace_integrations.py b/tests/chronicle/test_marketplace_integrations.py index e56329a9..15216fd9 100644 --- a/tests/chronicle/test_marketplace_integrations.py +++ b/tests/chronicle/test_marketplace_integrations.py @@ -76,7 +76,7 @@ def test_list_marketplace_integrations_success(chronicle_client): } with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations( @@ -104,7 +104,7 @@ def test_list_marketplace_integrations_default_args(chronicle_client): expected = {"marketplaceIntegrations": []} with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations(chronicle_client) @@ -128,7 +128,7 @@ def test_list_marketplace_integrations_with_filter(chronicle_client): expected = {"marketplaceIntegrations": [{"name": "integration1"}]} with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations( @@ -155,7 +155,7 @@ def test_list_marketplace_integrations_with_order_by(chronicle_client): expected = {"marketplaceIntegrations": []} with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations( @@ -182,7 +182,7 @@ def test_list_marketplace_integrations_with_filter_and_order_by(chronicle_client expected = {"marketplaceIntegrations": []} with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations( @@ -213,7 +213,7 @@ def test_list_marketplace_integrations_as_list(chronicle_client): expected = [{"name": "integration1"}, {"name": "integration2"}] with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_marketplace_integrations(chronicle_client, as_list=True) @@ -235,7 +235,7 @@ def test_list_marketplace_integrations_as_list(chronicle_client): def test_list_marketplace_integrations_error(chronicle_client): """Test list_marketplace_integrations propagates APIError from helper.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", side_effect=APIError("Failed to list marketplace integrations"), ): with pytest.raises(APIError) as exc_info: @@ -256,7 +256,7 @@ def test_get_marketplace_integration_success(chronicle_client): } with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_marketplace_integration(chronicle_client, "test-integration") @@ -274,7 +274,7 @@ def test_get_marketplace_integration_success(chronicle_client): def test_get_marketplace_integration_error(chronicle_client): """Test get_marketplace_integration raises APIError on failure.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", side_effect=APIError("Failed to get marketplace integration test-integration"), ): with pytest.raises(APIError) as exc_info: @@ -294,7 +294,7 @@ def test_get_marketplace_integration_diff_success(chronicle_client): } with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_marketplace_integration_diff(chronicle_client, "test-integration") @@ -315,7 +315,7 @@ def test_get_marketplace_integration_diff_success(chronicle_client): def test_get_marketplace_integration_diff_error(chronicle_client): """Test get_marketplace_integration_diff raises APIError on failure.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", side_effect=APIError("Failed to get marketplace integration diff"), ): with pytest.raises(APIError) as exc_info: @@ -336,7 +336,7 @@ def test_install_marketplace_integration_no_optional_fields(chronicle_client): } with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = install_marketplace_integration( @@ -364,7 +364,7 @@ def test_install_marketplace_integration_all_fields(chronicle_client): } with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = install_marketplace_integration( @@ -397,7 +397,7 @@ def test_install_marketplace_integration_override_mapping_only(chronicle_client) expected = {"name": "test-integration"} with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = install_marketplace_integration( @@ -422,7 +422,7 @@ def test_install_marketplace_integration_version_only(chronicle_client): expected = {"name": "test-integration"} with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = install_marketplace_integration( @@ -445,7 +445,7 @@ def test_install_marketplace_integration_version_only(chronicle_client): def test_install_marketplace_integration_none_fields_excluded(chronicle_client): """Test that None optional fields are not included in the request body.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value={"name": "test-integration"}, ) as mock_request: install_marketplace_integration( @@ -469,7 +469,7 @@ def test_install_marketplace_integration_none_fields_excluded(chronicle_client): def test_install_marketplace_integration_error(chronicle_client): """Test install_marketplace_integration raises APIError on failure.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", side_effect=APIError("Failed to install marketplace integration"), ): with pytest.raises(APIError) as exc_info: @@ -489,7 +489,7 @@ def test_uninstall_marketplace_integration_success(chronicle_client): expected = {} with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", return_value=expected, ) as mock_request: result = uninstall_marketplace_integration( @@ -510,7 +510,7 @@ def test_uninstall_marketplace_integration_success(chronicle_client): def test_uninstall_marketplace_integration_error(chronicle_client): """Test uninstall_marketplace_integration raises APIError on failure.""" with patch( - "secops.chronicle.marketplace_integrations.chronicle_request", + "secops.chronicle.integration.marketplace_integrations.chronicle_request", side_effect=APIError("Failed to uninstall marketplace integration"), ): with pytest.raises(APIError) as exc_info: From 04c85245324fbfe8f7341f7834b52e75f8f580dc Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 3 Mar 2026 15:43:22 +0000 Subject: [PATCH 19/46] feat: added tests for integrations --- tests/chronicle/test_integrations.py | 909 +++++++++++++++++++++++++++ 1 file changed, 909 insertions(+) create mode 100644 tests/chronicle/test_integrations.py diff --git a/tests/chronicle/test_integrations.py b/tests/chronicle/test_integrations.py new file mode 100644 index 00000000..811ab052 --- /dev/null +++ b/tests/chronicle/test_integrations.py @@ -0,0 +1,909 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + DiffType, + TargetMode, + PythonVersion, +) +from secops.chronicle.integration.integrations import ( + list_integrations, + get_integration, + delete_integration, + create_integration, + download_integration, + download_integration_dependency, + export_integration_items, + get_integration_affected_items, + get_agent_integrations, + get_integration_dependencies, + get_integration_restricted_agents, + get_integration_diff, + transition_integration, + update_integration, + update_custom_integration, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +@pytest.fixture +def mock_response() -> Mock: + """Create a mock API response object.""" + mock = Mock() + mock.status_code = 200 + mock.json.return_value = {} + return mock + + +@pytest.fixture +def mock_error_response() -> Mock: + """Create a mock error API response object.""" + mock = Mock() + mock.status_code = 400 + mock.text = "Error message" + mock.raise_for_status.side_effect = Exception("API Error") + return mock + + +# -- list_integrations tests -- + + +def test_list_integrations_success(chronicle_client): + """Test list_integrations delegates to chronicle_paginated_request.""" + expected = {"integrations": [{"name": "i1"}, {"name": "i2"}]} + + with patch( + "secops.chronicle.integration.integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integrations( + chronicle_client, + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations", + items_key="integrations", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integrations_with_filter_and_order_by(chronicle_client): + """Test list_integrations passes filter_string and order_by in extra_params.""" + expected = {"integrations": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integrations( + chronicle_client, + filter_string='displayName = "My Integration"', + order_by="displayName", + as_list=True, + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations", + items_key="integrations", + page_size=None, + page_token=None, + extra_params={ + "filter": 'displayName = "My Integration"', + "orderBy": "displayName", + }, + as_list=True, + ) + + +def test_list_integrations_error(chronicle_client): + """Test list_integrations propagates APIError from helper.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_paginated_request", + side_effect=APIError("Failed to list integrations"), + ): + with pytest.raises(APIError) as exc_info: + list_integrations(chronicle_client) + + assert "Failed to list integrations" in str(exc_info.value) + + +# -- get_integration tests -- + + +def test_get_integration_success(chronicle_client): + """Test get_integration returns expected result.""" + expected = {"name": "integrations/test-integration", "displayName": "Test"} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration(chronicle_client, "test-integration") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_error(chronicle_client): + """Test get_integration raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to get integration"), + ): + with pytest.raises(APIError) as exc_info: + get_integration(chronicle_client, "test-integration") + + assert "Failed to get integration" in str(exc_info.value) + + +# -- delete_integration tests -- + + +def test_delete_integration_success(chronicle_client): + """Test delete_integration delegates to chronicle_request.""" + with patch("secops.chronicle.integration.integrations.chronicle_request") as mock_request: + delete_integration(chronicle_client, "test-integration") + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_error(chronicle_client): + """Test delete_integration propagates APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to delete integration"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration(chronicle_client, "test-integration") + + assert "Failed to delete integration" in str(exc_info.value) + + +# -- create_integration tests -- + + +def test_create_integration_required_fields_only(chronicle_client): + """Test create_integration with required fields only.""" + expected = {"name": "integrations/test", "displayName": "Test"} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration( + chronicle_client, + display_name="Test", + staging=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations", + json={"displayName": "Test", "staging": True}, + api_version=APIVersion.V1BETA, + ) + + +def test_create_integration_all_optional_fields(chronicle_client): + """Test create_integration with all optional fields.""" + expected = {"name": "integrations/test"} + + python_version = list(PythonVersion)[0] + integration_type = Mock(name="integration_type") + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration( + chronicle_client, + display_name="Test", + staging=False, + description="desc", + image_base64="b64", + svg_icon="", + python_version=python_version, + parameters=[{"id": "p1"}], + categories=["cat"], + integration_type=integration_type, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations", + json={ + "displayName": "Test", + "staging": False, + "description": "desc", + "imageBase64": "b64", + "svgIcon": "", + "pythonVersion": python_version, + "parameters": [{"id": "p1"}], + "categories": ["cat"], + "type": integration_type, + }, + api_version=APIVersion.V1BETA, + ) + + +def test_create_integration_none_fields_excluded(chronicle_client): + """Test that None optional fields are excluded from create_integration body.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value={"name": "integrations/test"}, + ) as mock_request: + create_integration( + chronicle_client, + display_name="Test", + staging=True, + description=None, + image_base64=None, + svg_icon=None, + python_version=None, + parameters=None, + categories=None, + integration_type=None, + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations", + json={"displayName": "Test", "staging": True}, + api_version=APIVersion.V1BETA, + ) + + +def test_create_integration_error(chronicle_client): + """Test create_integration raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to create integration"), + ): + with pytest.raises(APIError) as exc_info: + create_integration(chronicle_client, display_name="Test", staging=True) + + assert "Failed to create integration" in str(exc_info.value) + + +# -- download_integration tests -- + + +def test_download_integration_success(chronicle_client): + """Test download_integration uses chronicle_request_bytes with alt=media and zip accept.""" + expected = b"ZIPBYTES" + + with patch( + "secops.chronicle.integration.integrations.chronicle_request_bytes", + return_value=expected, + ) as mock_bytes: + result = download_integration(chronicle_client, "test-integration") + + assert result == expected + + mock_bytes.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:export", + api_version=APIVersion.V1BETA, + params={"alt": "media"}, + headers={"Accept": "application/zip"}, + ) + + +def test_download_integration_error(chronicle_client): + """Test download_integration propagates APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request_bytes", + side_effect=APIError("Failed to download integration"), + ): + with pytest.raises(APIError) as exc_info: + download_integration(chronicle_client, "test-integration") + + assert "Failed to download integration" in str(exc_info.value) + + +# -- download_integration_dependency tests -- + + +def test_download_integration_dependency_success(chronicle_client): + """Test download_integration_dependency posts dependency name.""" + expected = {"dependency": "requests"} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = download_integration_dependency( + chronicle_client, + integration_name="test-integration", + dependency_name="requests==2.32.0", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration:downloadDependency", + json={"dependency": "requests==2.32.0"}, + api_version=APIVersion.V1BETA, + ) + + +def test_download_integration_dependency_error(chronicle_client): + """Test download_integration_dependency raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to download dependency"), + ): + with pytest.raises(APIError) as exc_info: + download_integration_dependency( + chronicle_client, + integration_name="test-integration", + dependency_name="requests", + ) + + assert "Failed to download dependency" in str(exc_info.value) + + +# -- export_integration_items tests -- + + +def test_export_integration_items_success_some_fields(chronicle_client): + """Test export_integration_items builds params correctly and uses chronicle_request_bytes.""" + expected = b"ZIPBYTES" + + with patch( + "secops.chronicle.integration.integrations.chronicle_request_bytes", + return_value=expected, + ) as mock_bytes: + result = export_integration_items( + chronicle_client, + integration_name="test-integration", + actions=["1", "2"], + connectors=["10"], + logical_operators=["7"], + ) + + assert result == expected + + mock_bytes.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:exportItems", + params={ + "actions": "1,2", + "connectors": ["10"], + "logicalOperators": ["7"], + "alt": "media", + }, + api_version=APIVersion.V1BETA, + headers={"Accept": "application/zip"}, + ) + + +def test_export_integration_items_no_fields(chronicle_client): + """Test export_integration_items always includes alt=media.""" + expected = b"ZIPBYTES" + + with patch( + "secops.chronicle.integration.integrations.chronicle_request_bytes", + return_value=expected, + ) as mock_bytes: + result = export_integration_items( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_bytes.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:exportItems", + params={"alt": "media"}, + api_version=APIVersion.V1BETA, + headers={"Accept": "application/zip"}, + ) + + +def test_export_integration_items_error(chronicle_client): + """Test export_integration_items propagates APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request_bytes", + side_effect=APIError("Failed to export integration items"), + ): + with pytest.raises(APIError) as exc_info: + export_integration_items(chronicle_client, "test-integration") + + assert "Failed to export integration items" in str(exc_info.value) + + +# -- get_integration_affected_items tests -- + + +def test_get_integration_affected_items_success(chronicle_client): + """Test get_integration_affected_items delegates to chronicle_request.""" + expected = {"affectedItems": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_affected_items(chronicle_client, "test-integration") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:fetchAffectedItems", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_affected_items_error(chronicle_client): + """Test get_integration_affected_items raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to fetch affected items"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_affected_items(chronicle_client, "test-integration") + + assert "Failed to fetch affected items" in str(exc_info.value) + + +# -- get_agent_integrations tests -- + + +def test_get_agent_integrations_success(chronicle_client): + """Test get_agent_integrations passes agentId parameter.""" + expected = {"integrations": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_agent_integrations(chronicle_client, agent_id="agent-123") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations:fetchAgentIntegrations", + params={"agentId": "agent-123"}, + api_version=APIVersion.V1BETA, + ) + + +def test_get_agent_integrations_error(chronicle_client): + """Test get_agent_integrations raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to fetch agent integrations"), + ): + with pytest.raises(APIError) as exc_info: + get_agent_integrations(chronicle_client, agent_id="agent-123") + + assert "Failed to fetch agent integrations" in str(exc_info.value) + + +# -- get_integration_dependencies tests -- + + +def test_get_integration_dependencies_success(chronicle_client): + """Test get_integration_dependencies delegates to chronicle_request.""" + expected = {"dependencies": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_dependencies(chronicle_client, "test-integration") + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:fetchDependencies", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_dependencies_error(chronicle_client): + """Test get_integration_dependencies raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to fetch dependencies"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_dependencies(chronicle_client, "test-integration") + + assert "Failed to fetch dependencies" in str(exc_info.value) + + +# -- get_integration_restricted_agents tests -- + + +def test_get_integration_restricted_agents_success(chronicle_client): + """Test get_integration_restricted_agents passes required python version and pushRequest.""" + expected = {"restrictedAgents": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_restricted_agents( + chronicle_client, + integration_name="test-integration", + required_python_version=PythonVersion.PYTHON_3_11, + push_request=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:fetchRestrictedAgents", + params={ + "requiredPythonVersion": PythonVersion.PYTHON_3_11.value, + "pushRequest": True, + }, + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_restricted_agents_default_push_request(chronicle_client): + """Test get_integration_restricted_agents default push_request=False is sent.""" + expected = {"restrictedAgents": []} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + get_integration_restricted_agents( + chronicle_client, + integration_name="test-integration", + required_python_version=PythonVersion.PYTHON_3_11, + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:fetchRestrictedAgents", + params={ + "requiredPythonVersion": PythonVersion.PYTHON_3_11.value, + "pushRequest": False, + }, + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_restricted_agents_error(chronicle_client): + """Test get_integration_restricted_agents raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to fetch restricted agents"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_restricted_agents( + chronicle_client, + integration_name="test-integration", + required_python_version=PythonVersion.PYTHON_3_11, + ) + + assert "Failed to fetch restricted agents" in str(exc_info.value) + + +# -- get_integration_diff tests -- + + +def test_get_integration_diff_success(chronicle_client): + """Test get_integration_diff builds endpoint with diff type.""" + expected = {"diff": {}} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_diff( + chronicle_client, + integration_name="test-integration", + diff_type=DiffType.PRODUCTION, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration:fetchProductionDiff", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_diff_error(chronicle_client): + """Test get_integration_diff raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to fetch diff"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_diff(chronicle_client, "test-integration") + + assert "Failed to fetch diff" in str(exc_info.value) + + +# -- transition_integration tests -- + + +def test_transition_integration_success(chronicle_client): + """Test transition_integration posts to pushTo{TargetMode}.""" + expected = {"name": "integrations/test"} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = transition_integration( + chronicle_client, + integration_name="test-integration", + target_mode=TargetMode.PRODUCTION, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration:pushToProduction", + api_version=APIVersion.V1BETA, + ) + + +def test_transition_integration_error(chronicle_client): + """Test transition_integration raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to transition integration"), + ): + with pytest.raises(APIError) as exc_info: + transition_integration( + chronicle_client, + integration_name="test-integration", + target_mode=TargetMode.STAGING, + ) + + assert "Failed to transition integration" in str(exc_info.value) + + +# -- update_integration tests -- + + +def test_update_integration_uses_build_patch_body_and_passes_dependencies_to_remove( + chronicle_client, +): + """Test update_integration uses build_patch_body and adds dependenciesToRemove.""" + body = {"displayName": "New"} + params = {"updateMask": "displayName"} + + with patch( + "secops.chronicle.integration.integrations.build_patch_body", + return_value=(body, params), + ) as mock_build_patch, patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value={"name": "integrations/test"}, + ) as mock_request: + result = update_integration( + chronicle_client, + integration_name="test-integration", + display_name="New", + dependencies_to_remove=["dep1", "dep2"], + update_mask="displayName", + ) + + assert result == {"name": "integrations/test"} + + mock_build_patch.assert_called_once() + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration", + json=body, + params={"updateMask": "displayName", "dependenciesToRemove": "dep1,dep2"}, + api_version=APIVersion.V1BETA, + ) + + +def test_update_integration_when_build_patch_body_returns_no_params(chronicle_client): + """Test update_integration handles params=None from build_patch_body.""" + body = {"description": "New"} + + with patch( + "secops.chronicle.integration.integrations.build_patch_body", + return_value=(body, None), + ), patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value={"name": "integrations/test"}, + ) as mock_request: + update_integration( + chronicle_client, + integration_name="test-integration", + description="New", + dependencies_to_remove=["dep1"], + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration", + json=body, + params={"dependenciesToRemove": "dep1"}, + api_version=APIVersion.V1BETA, + ) + + +def test_update_integration_error(chronicle_client): + """Test update_integration raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.build_patch_body", + return_value=({}, None), + ), patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to update integration"), + ): + with pytest.raises(APIError) as exc_info: + update_integration(chronicle_client, "test-integration") + + assert "Failed to update integration" in str(exc_info.value) + + +# -- update_custom_integration tests -- + + +def test_update_custom_integration_builds_body_and_params(chronicle_client): + """Test update_custom_integration builds nested integration body and updateMask param.""" + expected = {"successful": True, "integration": {"name": "integrations/test"}} + + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_custom_integration( + chronicle_client, + integration_name="test-integration", + display_name="New", + staging=False, + dependencies_to_remove=["dep1"], + update_mask="displayName,staging", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration:updateCustomIntegration", + json={ + "integration": { + "name": "test-integration", + "displayName": "New", + "staging": False, + }, + "dependenciesToRemove": ["dep1"], + }, + params={"updateMask": "displayName,staging"}, + api_version=APIVersion.V1BETA, + ) + + +def test_update_custom_integration_excludes_none_fields(chronicle_client): + """Test update_custom_integration excludes None fields from integration object.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + return_value={"successful": True}, + ) as mock_request: + update_custom_integration( + chronicle_client, + integration_name="test-integration", + display_name=None, + description=None, + image_base64=None, + svg_icon=None, + python_version=None, + parameters=None, + categories=None, + integration_type=None, + staging=None, + dependencies_to_remove=None, + update_mask=None, + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration:updateCustomIntegration", + json={"integration": {"name": "test-integration"}}, + params=None, + api_version=APIVersion.V1BETA, + ) + + +def test_update_custom_integration_error(chronicle_client): + """Test update_custom_integration raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integrations.chronicle_request", + side_effect=APIError("Failed to update custom integration"), + ): + with pytest.raises(APIError) as exc_info: + update_custom_integration(chronicle_client, "test-integration") + + assert "Failed to update custom integration" in str(exc_info.value) From 9a964051a1d12ac24a86fdfe20dc18827cff9dbc Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Wed, 4 Mar 2026 20:35:12 +0000 Subject: [PATCH 20/46] feat: update to use model --- src/secops/chronicle/__init__.py | 6 + src/secops/chronicle/client.py | 11 +- .../chronicle/integration/integrations.py | 19 +++- src/secops/chronicle/models.py | 55 +++++++++ .../integration/integration_client.py | 2 + tests/chronicle/utils/test_format_utils.py | 105 ++++++++++++++++++ 6 files changed, 189 insertions(+), 9 deletions(-) diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index ef86627f..f71d445d 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -106,15 +106,21 @@ DataExportStage, DataExportStatus, DetectionType, + DiffType, Entity, EntityMetadata, EntityMetrics, EntitySummary, FileMetadataAndProperties, InputInterval, + IntegrationParam, + IntegrationParamType, + IntegrationType, ListBasis, PrevalenceData, + PythonVersion, SoarPlatformInfo, + TargetMode, TileType, TimeInterval, Timeline, diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 351c81aa..e85cbb18 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -164,7 +164,7 @@ IntegrationType, PythonVersion, TargetMode, - TileType, + TileType, IntegrationParam, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -859,7 +859,7 @@ def list_integrations( order_by: str | None = None, api_version: APIVersion | None = APIVersion.V1BETA, as_list: bool = False, - ) -> dict[str, Any]: + ) -> dict[str, Any] | list[dict[str, Any]]: """Get a list of all integrations. Args: @@ -934,7 +934,7 @@ def create_integration( image_base64: str | None = None, svg_icon: str | None = None, python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, + parameters: list[IntegrationParam | dict[str, Any]] | None = None, categories: list[str] | None = None, integration_type: IntegrationType | None = None, api_version: APIVersion | None = APIVersion.V1BETA, @@ -951,7 +951,10 @@ def create_integration( base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50) + parameters: Optional. Integration parameters (max 50). Each entry may + be an IntegrationParam dataclass instance or a plain dict with + keys: id, defaultValue, displayName, propertyName, type, + description, mandatory. categories: Optional. Integration categories (max 50) integration_type: Optional. The integration's type (response/extension) diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py index 3982eb86..c2e941c4 100644 --- a/src/secops/chronicle/integration/integrations.py +++ b/src/secops/chronicle/integration/integrations.py @@ -19,6 +19,7 @@ from secops.chronicle.models import ( APIVersion, DiffType, + IntegrationParam, TargetMode, PythonVersion, IntegrationType, @@ -142,7 +143,7 @@ def create_integration( image_base64: str | None = None, svg_icon: str | None = None, python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, + parameters: list[IntegrationParam | dict[str, Any]] | None = None, categories: list[str] | None = None, integration_type: IntegrationType | None = None, api_version: APIVersion | None = APIVersion.V1BETA, @@ -160,9 +161,10 @@ def create_integration( a base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50). Each parameter - is a dict with keys: id, defaultValue, displayName, - propertyName, type, description, mandatory + parameters: Optional. Integration parameters (max 50). Each entry may + be an IntegrationParam dataclass instance or a plain dict with + keys: id, defaultValue, displayName, propertyName, type, + description, mandatory. categories: Optional. Integration categories (max 50) integration_type: Optional. The integration's type (response/extension) api_version: API version to use for the request. Default is V1BETA. @@ -173,6 +175,13 @@ def create_integration( Raises: APIError: If the API request fails """ + serialised_params: list[dict[str, Any]] | None = None + if parameters is not None: + serialised_params = [ + p.to_dict() if isinstance(p, IntegrationParam) else p + for p in parameters + ] + body_fields = { "displayName": display_name, "staging": staging, @@ -180,7 +189,7 @@ def create_integration( "imageBase64": image_base64, "svgIcon": svg_icon, "pythonVersion": python_version, - "parameters": parameters, + "parameters": serialised_params, "categories": categories, "type": integration_type, } diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index 61baa503..f27a5ad4 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -105,6 +105,61 @@ class IntegrationType(str, Enum): EXTENSION = "EXTENSION" +class IntegrationParamType(str, Enum): + """Type of integration parameter.""" + + PARAM_TYPE_UNSPECIFIED = "PARAM_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = "VALUES_AS_SEMICOLON_SEPARATED_STRING" + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + + +@dataclass +class IntegrationParam: + """A parameter definition for a Chronicle SOAR integration. + + Attributes: + display_name: Human-readable label shown in the UI. + property_name: The programmatic key used in code/config. + type: The data type of the parameter (see IntegrationParamType). + description: Optional. Explanation of what the parameter is for. + mandatory: Whether the parameter must be supplied. Defaults to False. + default_value: Optional. Pre-filled value shown in the UI. + """ + + display_name: str + property_name: str + type: IntegrationParamType + mandatory: bool + description: str | None = None + default_value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "propertyName": self.property_name, + "type": str(self.type), + "mandatory": self.mandatory, + } + if self.description is not None: + data["description"] = self.description + if self.default_value is not None: + data["defaultValue"] = self.default_value + return data + + @dataclass class TimeInterval: """Time interval with start and end times.""" diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index 334398b8..ea5f5035 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -15,6 +15,7 @@ """Top level arguments for integration commands""" from secops.cli.commands.integration import marketplace_integration +from secops.cli.commands.integration import integration def setup_integrations_command(subparsers): @@ -28,3 +29,4 @@ def setup_integrations_command(subparsers): # Setup all subcommands under `integration` marketplace_integration.setup_marketplace_integrations_command(lvl1) + integration.setup_integrations_command(lvl1) \ No newline at end of file diff --git a/tests/chronicle/utils/test_format_utils.py b/tests/chronicle/utils/test_format_utils.py index c71bda40..5610c2da 100644 --- a/tests/chronicle/utils/test_format_utils.py +++ b/tests/chronicle/utils/test_format_utils.py @@ -18,6 +18,7 @@ import pytest from secops.chronicle.utils.format_utils import ( + build_patch_body, format_resource_id, parse_json_list, ) @@ -98,3 +99,107 @@ def test_parse_json_list_handles_empty_json_array() -> None: def test_parse_json_list_handles_empty_list_input() -> None: result = parse_json_list([], "filters") assert result == [] + + +def test_build_patch_body_all_fields_set_builds_body_and_mask() -> None: + # All three fields provided — body and mask should include all of them + body, params = build_patch_body([ + ("displayName", "display_name", "My Rule"), + ("enabled", "enabled", True), + ("severity", "severity", "HIGH"), + ]) + + assert body == {"displayName": "My Rule", "enabled": True, "severity": "HIGH"} + assert params == {"updateMask": "display_name,enabled,severity"} + + +def test_build_patch_body_partial_fields_omits_none_values() -> None: + # Only non-None values should appear in body and mask + body, params = build_patch_body([ + ("displayName", "display_name", "New Name"), + ("enabled", "enabled", None), + ("severity", "severity", None), + ]) + + assert body == {"displayName": "New Name"} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_no_fields_set_returns_empty_body_and_none_params() -> None: + # All values are None — body should be empty and params should be None + body, params = build_patch_body([ + ("displayName", "display_name", None), + ("enabled", "enabled", None), + ]) + + assert body == {} + assert params is None + + +def test_build_patch_body_empty_field_map_returns_empty_body_and_none_params() -> None: + # Empty field_map — nothing to build + body, params = build_patch_body([]) + + assert body == {} + assert params is None + + +def test_build_patch_body_explicit_update_mask_overrides_auto_generated() -> None: + # An explicit update_mask should always win, even when fields are set + body, params = build_patch_body( + [ + ("displayName", "display_name", "Name"), + ("enabled", "enabled", True), + ], + update_mask="display_name", + ) + + assert body == {"displayName": "Name", "enabled": True} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_explicit_update_mask_with_no_fields_set_still_applies() -> None: + # Explicit mask should appear even when all values are None (caller's intent) + body, params = build_patch_body( + [ + ("displayName", "display_name", None), + ], + update_mask="display_name", + ) + + assert body == {} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_false_and_zero_are_not_treated_as_none() -> None: + # False-like but non-None values (False, 0, "") should be included in the body + body, params = build_patch_body([ + ("enabled", "enabled", False), + ("count", "count", 0), + ("label", "label", ""), + ]) + + assert body == {"enabled": False, "count": 0, "label": ""} + assert params == {"updateMask": "enabled,count,label"} + + +def test_build_patch_body_single_field_produces_single_entry_mask() -> None: + body, params = build_patch_body([ + ("severity", "severity", "LOW"), + ]) + + assert body == {"severity": "LOW"} + assert params == {"updateMask": "severity"} + + +def test_build_patch_body_mask_order_matches_field_map_order() -> None: + # The mask field order should mirror the order of field_map entries + body, params = build_patch_body([ + ("z", "z_key", "z_val"), + ("a", "a_key", "a_val"), + ("m", "m_key", "m_val"), + ]) + + assert params == {"updateMask": "z_key,a_key,m_key"} + assert list(body.keys()) == ["z", "a", "m"] + From 6e3d8fa3d30ee93abaa4bacbe7b23125ed4af9e6 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Wed, 4 Mar 2026 20:37:26 +0000 Subject: [PATCH 21/46] feat: implement integrations CLI --- .../cli/commands/integration/integration.py | 783 ++++++++++++++++++ 1 file changed, 783 insertions(+) create mode 100644 src/secops/cli/commands/integration/integration.py diff --git a/src/secops/cli/commands/integration/integration.py b/src/secops/cli/commands/integration/integration.py new file mode 100644 index 00000000..a9030b86 --- /dev/null +++ b/src/secops/cli/commands/integration/integration.py @@ -0,0 +1,783 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration commands""" + +import sys + +from secops.chronicle.models import ( + DiffType, + IntegrationType, + PythonVersion, + TargetMode, +) +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_integrations_command(subparsers): + """Setup integrations command""" + integrations_parser = subparsers.add_parser( + "integrations", help="Manage SecOps integrations" + ) + lvl1 = integrations_parser.add_subparsers( + dest="integrations_command", help="Integrations command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integrations") + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing integrations", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing integrations", + dest="order_by", + ) + list_parser.set_defaults(func=handle_integration_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get integration details") + get_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to get details for", + dest="integration_id", + required=True, + ) + get_parser.set_defaults(func=handle_integration_get_command) + + # delete command + delete_parser = lvl1.add_parser("delete", help="Delete an integration") + delete_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to delete", + dest="integration_id", + required=True, + ) + delete_parser.set_defaults(func=handle_integration_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new custom integration" + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the integration (max 150 characters)", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--staging", + action="store_true", + help="Create the integration in staging mode", + dest="staging", + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the integration (max 1,500 characters)", + dest="description", + ) + create_parser.add_argument( + "--image-base64", + type=str, + help="Integration image encoded as a base64 string (max 5 MB)", + dest="image_base64", + ) + create_parser.add_argument( + "--svg-icon", + type=str, + help="Integration SVG icon string (max 1 MB)", + dest="svg_icon", + ) + create_parser.add_argument( + "--python-version", + type=str, + choices=[v.value for v in PythonVersion], + help="Python version for the integration", + dest="python_version", + ) + create_parser.add_argument( + "--integration-type", + type=str, + choices=[t.value for t in IntegrationType], + help="Integration type", + dest="integration_type", + ) + create_parser.set_defaults(func=handle_integration_create_command) + + # download command + download_parser = lvl1.add_parser( + "download", + help="Download an integration package as a ZIP file", + ) + download_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to download", + dest="integration_id", + required=True, + ) + download_parser.add_argument( + "--output-file", + type=str, + help="Path to write the downloaded ZIP file to", + dest="output_file", + required=True, + ) + download_parser.set_defaults(func=handle_integration_download_command) + + # download-dependency command + download_dep_parser = lvl1.add_parser( + "download-dependency", + help="Download a Python dependency for a custom integration", + ) + download_dep_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration", + dest="integration_id", + required=True, + ) + download_dep_parser.add_argument( + "--dependency-name", + type=str, + help=( + "Dependency name to download. Can include version or " + "repository, e.g. 'requests==2.31.0'" + ), + dest="dependency_name", + required=True, + ) + download_dep_parser.set_defaults( + func=handle_download_integration_dependency_command + ) + + # export-items command + export_items_parser = lvl1.add_parser( + "export-items", + help="Export specific items from an integration as a ZIP file", + ) + export_items_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to export items from", + dest="integration_id", + required=True, + ) + export_items_parser.add_argument( + "--output-file", + type=str, + help="Path to write the exported ZIP file to", + dest="output_file", + required=True, + ) + export_items_parser.add_argument( + "--actions", + type=str, + nargs="+", + help="IDs of actions to export", + dest="actions", + ) + export_items_parser.add_argument( + "--jobs", + type=str, + nargs="+", + help="IDs of jobs to export", + dest="jobs", + ) + export_items_parser.add_argument( + "--connectors", + type=str, + nargs="+", + help="IDs of connectors to export", + dest="connectors", + ) + export_items_parser.add_argument( + "--managers", + type=str, + nargs="+", + help="IDs of managers to export", + dest="managers", + ) + export_items_parser.add_argument( + "--transformers", + type=str, + nargs="+", + help="IDs of transformers to export", + dest="transformers", + ) + export_items_parser.add_argument( + "--logical-operators", + type=str, + nargs="+", + help="IDs of logical operators to export", + dest="logical_operators", + ) + export_items_parser.set_defaults( + func=handle_export_integration_items_command + ) + + # affected-items command + affected_parser = lvl1.add_parser( + "affected-items", + help="Get items affected by changes to an integration", + ) + affected_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to check", + dest="integration_id", + required=True, + ) + affected_parser.set_defaults( + func=handle_get_integration_affected_items_command + ) + + # agent-integrations command + agent_parser = lvl1.add_parser( + "agent-integrations", + help="Get integrations installed on a specific agent", + ) + agent_parser.add_argument( + "--agent-id", + type=str, + help="Identifier of the agent", + dest="agent_id", + required=True, + ) + agent_parser.set_defaults(func=handle_get_agent_integrations_command) + + # dependencies command + deps_parser = lvl1.add_parser( + "dependencies", + help="Get Python dependencies for a custom integration", + ) + deps_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration", + dest="integration_id", + required=True, + ) + deps_parser.set_defaults( + func=handle_get_integration_dependencies_command + ) + + # restricted-agents command + restricted_parser = lvl1.add_parser( + "restricted-agents", + help="Get agents restricted from running an updated integration", + ) + restricted_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration", + dest="integration_id", + required=True, + ) + restricted_parser.add_argument( + "--required-python-version", + type=str, + choices=[v.value for v in PythonVersion], + help="Python version required for the updated integration", + dest="required_python_version", + required=True, + ) + restricted_parser.add_argument( + "--push-request", + action="store_true", + help="Indicates the integration is being pushed to a different mode", + dest="push_request", + ) + restricted_parser.set_defaults( + func=handle_get_integration_restricted_agents_command + ) + + # diff command + diff_parser = lvl1.add_parser( + "diff", help="Get the configuration diff for an integration" + ) + diff_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration", + dest="integration_id", + required=True, + ) + diff_parser.add_argument( + "--diff-type", + type=str, + choices=[d.value for d in DiffType], + help=( + "Type of diff to retrieve. " + "COMMERCIAL: diff against the marketplace version. " + "PRODUCTION: diff between staging and production. " + "STAGING: diff between production and staging." + ), + dest="diff_type", + default=DiffType.COMMERCIAL.value, + ) + diff_parser.set_defaults(func=handle_get_integration_diff_command) + + # transition command + transition_parser = lvl1.add_parser( + "transition", + help="Transition an integration to production or staging", + ) + transition_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to transition", + dest="integration_id", + required=True, + ) + transition_parser.add_argument( + "--target-mode", + type=str, + choices=[t.value for t in TargetMode], + help="Target mode to transition the integration to", + dest="target_mode", + required=True, + ) + transition_parser.set_defaults(func=handle_transition_integration_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an existing integration's metadata" + ) + update_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to update", + dest="integration_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the integration (max 150 characters)", + dest="display_name", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the integration (max 1,500 characters)", + dest="description", + ) + update_parser.add_argument( + "--image-base64", + type=str, + help="New integration image encoded as a base64 string (max 5 MB)", + dest="image_base64", + ) + update_parser.add_argument( + "--svg-icon", + type=str, + help="New integration SVG icon string (max 1 MB)", + dest="svg_icon", + ) + update_parser.add_argument( + "--python-version", + type=str, + choices=[v.value for v in PythonVersion], + help="Python version for the integration", + dest="python_version", + ) + update_parser.add_argument( + "--integration-type", + type=str, + choices=[t.value for t in IntegrationType], + help="Integration type", + dest="integration_type", + ) + update_parser.add_argument( + "--staging", + action="store_true", + help="Set the integration to staging mode", + dest="staging", + ) + update_parser.add_argument( + "--dependencies-to-remove", + type=str, + nargs="+", + help="List of dependency names to remove from the integration", + dest="dependencies_to_remove", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help=( + "Comma-separated list of fields to update. " + "If not provided, all supplied fields are updated." + ), + dest="update_mask", + ) + update_parser.set_defaults(func=handle_update_integration_command) + + # update-custom command + update_custom_parser = lvl1.add_parser( + "update-custom", + help=( + "Update a custom integration definition including " + "parameters and dependencies" + ), + ) + update_custom_parser.add_argument( + "--integration-id", + type=str, + help="ID of the integration to update", + dest="integration_id", + required=True, + ) + update_custom_parser.add_argument( + "--display-name", + type=str, + help="New display name for the integration (max 150 characters)", + dest="display_name", + ) + update_custom_parser.add_argument( + "--description", + type=str, + help="New description for the integration (max 1,500 characters)", + dest="description", + ) + update_custom_parser.add_argument( + "--image-base64", + type=str, + help="New integration image encoded as a base64 string (max 5 MB)", + dest="image_base64", + ) + update_custom_parser.add_argument( + "--svg-icon", + type=str, + help="New integration SVG icon string (max 1 MB)", + dest="svg_icon", + ) + update_custom_parser.add_argument( + "--python-version", + type=str, + choices=[v.value for v in PythonVersion], + help="Python version for the integration", + dest="python_version", + ) + update_custom_parser.add_argument( + "--integration-type", + type=str, + choices=[t.value for t in IntegrationType], + help="Integration type", + dest="integration_type", + ) + update_custom_parser.add_argument( + "--staging", + action="store_true", + help="Set the integration to staging mode", + dest="staging", + ) + update_custom_parser.add_argument( + "--dependencies-to-remove", + type=str, + nargs="+", + help="List of dependency names to remove from the integration", + dest="dependencies_to_remove", + ) + update_custom_parser.add_argument( + "--update-mask", + type=str, + help=( + "Comma-separated list of fields to update. " + "If not provided, all supplied fields are updated." + ), + dest="update_mask", + ) + update_custom_parser.set_defaults( + func=handle_updated_custom_integration_command + ) + + +# --------------------------------------------------------------------------- +# Handlers +# --------------------------------------------------------------------------- + + +def handle_integration_list_command(args, chronicle): + """Handle list integrations command""" + try: + out = chronicle.list_integrations( + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integrations: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_get_command(args, chronicle): + """Handle get integration command""" + try: + out = chronicle.get_integration( + integration_name=args.integration_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_delete_command(args, chronicle): + """Handle delete integration command""" + try: + chronicle.delete_integration( + integration_name=args.integration_id, + ) + print( + f"Integration {args.integration_id} deleted successfully." + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_create_command(args, chronicle): + """Handle create integration command""" + try: + out = chronicle.create_integration( + display_name=args.display_name, + staging=args.staging, + description=args.description, + image_base64=args.image_base64, + svg_icon=args.svg_icon, + python_version=( + PythonVersion(args.python_version) + if args.python_version + else None + ), + integration_type=( + IntegrationType(args.integration_type) + if args.integration_type + else None + ), + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_download_command(args, chronicle): + """Handle download integration command""" + try: + zip_bytes = chronicle.download_integration( + integration_name=args.integration_id, + ) + with open(args.output_file, "wb") as f: + f.write(zip_bytes) + print( + f"Integration {args.integration_id} downloaded to " + f"{args.output_file}." + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error downloading integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_download_integration_dependency_command(args, chronicle): + """Handle download integration dependencies command""" + try: + out = chronicle.download_integration_dependency( + integration_name=args.integration_id, + dependency_name=args.dependency_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error downloading integration dependencies: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_export_integration_items_command(args, chronicle): + """Handle export integration items command""" + try: + zip_bytes = chronicle.export_integration_items( + integration_name=args.integration_id, + actions=args.actions, + jobs=args.jobs, + connectors=args.connectors, + managers=args.managers, + transformers=args.transformers, + logical_operators=args.logical_operators, + ) + with open(args.output_file, "wb") as f: + f.write(zip_bytes) + print(f"Integration items exported to {args.output_file}.") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error exporting integration items: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_get_integration_affected_items_command(args, chronicle): + """Handle get integration affected items command""" + try: + out = chronicle.get_integration_affected_items( + integration_name=args.integration_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting integration affected items: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_get_agent_integrations_command(args, chronicle): + """Handle get agent integration command""" + try: + out = chronicle.get_agent_integrations( + agent_id=args.agent_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting agent integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_get_integration_dependencies_command(args, chronicle): + """Handle get integration dependencies command""" + try: + out = chronicle.get_integration_dependencies( + integration_name=args.integration_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting integration dependencies: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_get_integration_restricted_agents_command(args, chronicle): + """Handle get integration restricted agent command""" + try: + out = chronicle.get_integration_restricted_agents( + integration_name=args.integration_id, + required_python_version=PythonVersion(args.required_python_version), + push_request=args.push_request, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting integration restricted agent: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_get_integration_diff_command(args, chronicle): + """Handle get integration diff command""" + try: + out = chronicle.get_integration_diff( + integration_name=args.integration_id, + diff_type=DiffType(args.diff_type), + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration diff: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_transition_integration_command(args, chronicle): + """Handle transition integration command""" + try: + out = chronicle.transition_integration( + integration_name=args.integration_id, + target_mode=TargetMode(args.target_mode), + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error transitioning integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_update_integration_command(args, chronicle): + """Handle update integration command""" + try: + out = chronicle.update_integration( + integration_name=args.integration_id, + display_name=args.display_name, + description=args.description, + image_base64=args.image_base64, + svg_icon=args.svg_icon, + python_version=( + PythonVersion(args.python_version) + if args.python_version + else None + ), + integration_type=( + IntegrationType(args.integration_type) + if args.integration_type + else None + ), + staging=args.staging or None, + dependencies_to_remove=args.dependencies_to_remove, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_updated_custom_integration_command(args, chronicle): + """Handle update custom integration command""" + try: + out = chronicle.update_custom_integration( + integration_name=args.integration_id, + display_name=args.display_name, + description=args.description, + image_base64=args.image_base64, + svg_icon=args.svg_icon, + python_version=( + PythonVersion(args.python_version) + if args.python_version + else None + ), + integration_type=( + IntegrationType(args.integration_type) + if args.integration_type + else None + ), + staging=args.staging or None, + dependencies_to_remove=args.dependencies_to_remove, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating custom integration: {e}", file=sys.stderr) + sys.exit(1) From a27aed97463e45861e7da66b8249bacabf6f4e10 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 6 Mar 2026 14:19:14 +0000 Subject: [PATCH 22/46] feat: implement integration actions functions --- README.md | 116 +++ api_module_mapping.md | 18 +- src/secops/chronicle/client.py | 383 +++++++++- src/secops/chronicle/integration/actions.py | 452 ++++++++++++ .../chronicle/integration/integrations.py | 5 +- src/secops/chronicle/models.py | 72 +- tests/chronicle/integration/__init__.py | 0 tests/chronicle/integration/test_actions.py | 666 ++++++++++++++++++ .../{ => integration}/test_integrations.py | 0 9 files changed, 1699 insertions(+), 13 deletions(-) create mode 100644 src/secops/chronicle/integration/actions.py create mode 100644 tests/chronicle/integration/__init__.py create mode 100644 tests/chronicle/integration/test_actions.py rename tests/chronicle/{ => integration}/test_integrations.py (100%) diff --git a/README.md b/README.md index d43855e0..80307724 100644 --- a/README.md +++ b/README.md @@ -1958,6 +1958,122 @@ Uninstall a marketplace integration: chronicle.uninstall_marketplace_integration("AWSSecurityHub") ``` +### Integration Actions + +List all available actions for an integration: + +```python +# Get all actions for an integration +actions = chronicle.list_integration_actions("AWSSecurityHub") +for action in actions.get("actions", []): + print(f"Action: {action.get('displayName')}, ID: {action.get('name')}") + +# Get all actions as a list +actions = chronicle.list_integration_actions("AWSSecurityHub", as_list=True) + +# Get only enabled actions +actions = chronicle.list_integration_actions("AWSSecurityHub", filter_string="enabled = true") +``` + +Get details of a specific action: + +```python + +action = chronicle.get_integration_action( + integration_name="AWSSecurityHub", + action_id="123" +) +``` + +Create an integration action + +```python +from secops.chronicle.models import ActionParameter, ActionParamType + +new_action = chronicle.create_integration_action( + integration_name="MyIntegration", + display_name="New Action", + description="This is a new action", + enabled=True, + timeout_seconds=900, + is_async=False, + script_result_name="script_result", + parameters=[ + ActionParameter( + display_name="Parameter 1", + type=ActionParamType.STRING, + description="This is parameter 1", + mandatory=True, + ) + ], + script="print('Hello, World!')" + ) +``` + +Update an integration action + +```python +from secops.chronicle.models import ActionParameter, ActionParamType + +updated_action = chronicle.update_integration_action( + integration_name="MyIntegration", + action_id="123", + display_name="Updated Action Name", + description="Updated description", + enabled=False, + parameters=[ + ActionParameter( + display_name="New Parameter", + type=ActionParamType.PASSWORD, + description="This is a new parameter", + mandatory=True, + ) + ], + script="print('Updated script')" +) +``` + +Delete an integration action + +```python +chronicle.delete_integration_action( + integration_name="MyIntegration", + action_id="123" +) +``` + +Execute test run of an integration action + +```python +# Get the integration instance ID by using chronicle.list_integration_instances() +integration_instance_id = "abc-123-def-456" + +test_run = chronicle.execute_integration_action_test( + integration_name="MyIntegration", + test_case_id=123456, + action=chronicle.get_integration_action("MyIntegration", "123"), + scope="TEST", + integration_instance_id=integration_instance_id, +) +``` + +Get integration actions by environment + +```python +# Get all actions for an integration in the Default Environment +actions = chronicle.get_integration_actions_by_environment( + integration_name="MyIntegration", + environments=["Default Environment"], + include_widgets=True, +) +``` + +Get a template for creating an action in an integration + +```python +template = chronicle.get_integration_action_template("MyIntegration") +``` + ## Rule Management The SDK provides comprehensive support for managing Chronicle detection rules: diff --git a/api_module_mapping.md b/api_module_mapping.md index 810c4244..e0a5da89 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,7 +7,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 5 endpoints implemented +- **v1beta:** 13 endpoints implemented - **v1alpha:** 113 endpoints implemented ## Endpoint Mapping @@ -83,6 +83,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.pushToStaging | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.STAGING) | | | integrations.updateCustomIntegration | v1beta | | | | integrations.upload | v1beta | | | +| integrations.actions.create | v1beta | chronicle.integration.actions.create_integration_action | | +| integrations.actions.delete | v1beta | chronicle.integration.actions.delete_integration_action | | +| integrations.actions.executeTest | v1beta | chronicle.integration.actions.execute_integration_action_test | | +| integrations.actions.fetchActionsByEnvironment | v1beta | chronicle.integration.actions.get_integration_actions_by_environment | | +| integrations.actions.fetchTemplate | v1beta | chronicle.integration.actions.get_integration_action_template | | +| integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | | +| integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | | +| integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -278,6 +286,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.pushToStaging | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.STAGING) | | | integrations.updateCustomIntegration | v1alpha | | | | integrations.upload | v1alpha | | | +| integrations.actions.create | v1alpha | chronicle.integration.actions.create_integration_action(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.delete | v1alpha | chronicle.integration.actions.delete_integration_action(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.executeTest | v1alpha | chronicle.integration.actions.execute_integration_action_test(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.fetchActionsByEnvironment | v1alpha | chronicle.integration.actions.get_integration_actions_by_environment(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.fetchTemplate | v1alpha | chronicle.integration.actions.get_integration_action_template(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index e85cbb18..4e4ccd89 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -13,6 +13,7 @@ # limitations under the License. # """Chronicle API client.""" + import ipaddress import re from collections.abc import Iterator @@ -153,6 +154,16 @@ update_custom_integration as _update_custom_integration, update_integration as _update_integration, ) +from secops.chronicle.integration.actions import ( + create_integration_action as _create_integration_action, + delete_integration_action as _delete_integration_action, + execute_integration_action_test as _execute_integration_action_test, + get_integration_action as _get_integration_action, + get_integration_action_template as _get_integration_action_template, + get_integration_actions_by_environment as _get_integration_actions_by_environment, + list_integration_actions as _list_integration_actions, + update_integration_action as _update_integration_action, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -164,7 +175,8 @@ IntegrationType, PythonVersion, TargetMode, - TileType, IntegrationParam, + TileType, + IntegrationParam, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -951,10 +963,10 @@ def create_integration( base64 string (max 5 MB) svg_icon: Optional. The integration's SVG icon (max 1 MB) python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50). Each entry may - be an IntegrationParam dataclass instance or a plain dict with - keys: id, defaultValue, displayName, propertyName, type, - description, mandatory. + parameters: Optional. Integration parameters (max 50). + Each entry may be an IntegrationParam dataclass instance + or a plain dict with keys: id, defaultValue, + displayName, propertyName, type, description, mandatory. categories: Optional. Integration categories (max 50) integration_type: Optional. The integration's type (response/extension) @@ -1021,9 +1033,9 @@ def download_integration_dependency( Default is V1BETA. Returns: - Dict containing the dependency installation result with keys: - - successful: True if installation was successful - - error: Error message if installation failed + Empty dict if the download was successful, + or a dict containing error + details if the download failed Raises: APIError: If the API request fails @@ -1383,6 +1395,361 @@ def update_custom_integration( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Action methods + # ------------------------------------------------------------------------- + + def list_integration_actions( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """Get a list of actions for a given integration. + + Args: + integration_name: Name of the integration to get actions for + page_size: Number of results to return per page + page_token: Token for the page to retrieve + filter_string: Filter expression to filter actions + order_by: Field to sort the actions by + expand: Comma-separated list of fields to expand in the response + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of actions instead of a dict with + actions list and nextPageToken. + + Returns: + If as_list is True: List of actions. + If as_list is False: Dict with actions list and nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_integration_actions( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + expand=expand, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_action( + self, + integration_name: str, + action_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get details of a specific action for a given integration. + + Args: + integration_name: Name of the integration the action belongs to + action_id: ID of the action to retrieve + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified action. + + Raises: + APIError: If the API request fails + """ + return _get_integration_action( + self, + integration_name, + action_id, + api_version=api_version, + ) + + def delete_integration_action( + self, + integration_name: str, + action_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific action from a given integration. + + Args: + integration_name: Name of the integration the action belongs to + action_id: ID of the action to delete + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails + """ + return _delete_integration_action( + self, + integration_name, + action_id, + api_version=api_version, + ) + + def create_integration_action( + self, + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + script_result_name: str, + is_async: bool, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any]] | None = None, + ai_generated: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new custom action for a given integration. + + Args: + integration_name: Name of the integration to + create the action for. + display_name: Action's display name. + Maximum 150 characters. Required. + script: Action's Python script. Maximum size 5MB. Required. + timeout_seconds: Action timeout in seconds. Maximum 1200. Required. + enabled: Whether the action is enabled or disabled. Required. + script_result_name: Field name that holds the script result. + Maximum 100 characters. Required. + is_async: Whether the action is asynchronous. Required. + description: Action's description. Maximum 400 characters. Optional. + default_result_value: Action's default result value. + Maximum 1000 characters. Optional. + async_polling_interval_seconds: Polling interval + in seconds for async actions. + Cannot exceed total timeout. Optional. + async_total_timeout_seconds: Total async timeout in seconds. Maximum + 1209600 (14 days). Optional. + dynamic_results: List of dynamic result metadata dicts. + Max 50. Optional. + parameters: List of action parameter dicts. Max 50. Optional. + ai_generated: Whether the action was generated by AI. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationAction resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_action( + self, + integration_name, + display_name, + script, + timeout_seconds, + enabled, + script_result_name, + is_async, + description=description, + default_result_value=default_result_value, + async_polling_interval_seconds=async_polling_interval_seconds, + async_total_timeout_seconds=async_total_timeout_seconds, + dynamic_results=dynamic_results, + parameters=parameters, + ai_generated=ai_generated, + api_version=api_version, + ) + + def update_integration_action( + self, + integration_name: str, + action_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + script_result_name: str | None = None, + is_async: bool | None = None, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any]] | None = None, + ai_generated: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing custom action for a given integration. + + Only custom actions can be updated; predefined commercial actions are + immutable. + + Args: + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to update. + display_name: Action's display name. Maximum 150 characters. + script: Action's Python script. Maximum size 5MB. + timeout_seconds: Action timeout in seconds. Maximum 1200. + enabled: Whether the action is enabled or disabled. + script_result_name: Field name that holds the script result. + Maximum 100 characters. + is_async: Whether the action is asynchronous. + description: Action's description. Maximum 400 characters. + default_result_value: Action's default result value. + Maximum 1000 characters. + async_polling_interval_seconds: Polling interval + in seconds for async actions. Cannot exceed total timeout. + async_total_timeout_seconds: Total async timeout in seconds. Maximum + 1209600 (14 days). + dynamic_results: List of dynamic result metadata dicts. Max 50. + parameters: List of action parameter dicts. Max 50. + ai_generated: Whether the action was generated by AI. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationAction resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_action( + self, + integration_name, + action_id, + display_name=display_name, + script=script, + timeout_seconds=timeout_seconds, + enabled=enabled, + script_result_name=script_result_name, + is_async=is_async, + description=description, + default_result_value=default_result_value, + async_polling_interval_seconds=async_polling_interval_seconds, + async_total_timeout_seconds=async_total_timeout_seconds, + dynamic_results=dynamic_results, + parameters=parameters, + ai_generated=ai_generated, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_action_test( + self, + integration_name: str, + test_case_id: int, + action: dict[str, Any], + scope: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Execute a test run of an integration action's script. + + Use this method to verify custom action logic, connectivity, and data + parsing against a specified integration instance and test case before + making the action available in playbooks. + + Args: + integration_name: Name of the integration the action belongs to. + test_case_id: ID of the action test case. + action: Dict containing the IntegrationAction to test. + scope: The action test scope. + integration_instance_id: The integration instance ID to use. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict with the test execution results with the following fields: + - output: The script output. + - debugOutput: The script debug output. + - resultJson: The result JSON if it exists (optional). + - resultName: The script result name (optional). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_action_test( + self, + integration_name, + test_case_id, + action, + scope, + integration_instance_id, + api_version=api_version, + ) + + def get_integration_actions_by_environment( + self, + integration_name: str, + environments: list[str], + include_widgets: bool, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """List actions executable within specified environments. + + Use this method to discover which automated tasks have active + integration instances configured for a particular + network or organizational context. + + Args: + integration_name: Name of the integration to fetch actions for. + environments: List of environments to filter actions by. + include_widgets: Whether to include widget actions in the response. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing a list of IntegrationAction objects that have + integration instances in one of the given environments. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_actions_by_environment( + self, + integration_name, + environments, + include_widgets, + api_version=api_version, + ) + + def get_integration_action_template( + self, + integration_name: str, + is_async: bool = False, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new + integration action. + + Use this method to jumpstart the development of a custom automated task + by providing boilerplate code for either synchronous or asynchronous + operations. + + Args: + integration_name: Name of the integration to fetch the template for. + is_async: Whether to fetch a template for an async action. Default + is False. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationAction template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_action_template( + self, + integration_name, + is_async=is_async, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/actions.py b/src/secops/chronicle/integration/actions.py new file mode 100644 index 00000000..5867df24 --- /dev/null +++ b/src/secops/chronicle/integration/actions.py @@ -0,0 +1,452 @@ +# Copyright 2025 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. +# +"""Marketplace integration actions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion, ActionParameter +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_actions( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """Get a list of actions for a given integration. + + Args: + client: ChronicleClient instance + integration_name: Name of the integration to get actions for + page_size: Number of results to return per page + page_token: Token for the page to retrieve + filter_string: Filter expression to filter actions + order_by: Field to sort the actions by + expand: Comma-separated list of fields to expand in the response + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of actions instead of a dict with + actions list and nextPageToken. + + Returns: + If as_list is True: List of actions. + If as_list is False: Dict with actions list and nextPageToken. + + Raises: + APIError: If the API request fails + """ + field_map = { + "filter": filter_string, + "orderBy": order_by, + "expand": expand, + } + + # Remove keys with None values + field_map = {k: v for k, v in field_map.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=f"integrations/{format_resource_id(integration_name)}/actions", + items_key="actions", + page_size=page_size, + page_token=page_token, + extra_params=field_map, + as_list=as_list, + ) + + +def get_integration_action( + client: "ChronicleClient", + integration_name: str, + action_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get details of a specific action for a given integration. + + Args: + client: ChronicleClient instance + integration_name: Name of the integration the action belongs to + action_id: ID of the action to retrieve + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified action. + + Raises: + APIError: If the API request fails + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions/{action_id}", + api_version=api_version, + ) + + +def delete_integration_action( + client: "ChronicleClient", + integration_name: str, + action_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific action from a given integration. + + Args: + client: ChronicleClient instance + integration_name: Name of the integration the action belongs to + action_id: ID of the action to delete + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions/{action_id}", + api_version=api_version, + ) + + +def create_integration_action( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + script_result_name: str, + is_async: bool, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any] | ActionParameter] | None = None, + ai_generated: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new custom action for a given integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the action for. + display_name: Action's display name. Maximum 150 characters. Required. + script: Action's Python script. Maximum size 5MB. Required. + timeout_seconds: Action timeout in seconds. Maximum 1200. Required. + enabled: Whether the action is enabled or disabled. Required. + script_result_name: Field name that holds the script result. + Maximum 100 characters. Required. + is_async: Whether the action is asynchronous. Required. + description: Action's description. Maximum 400 characters. Optional. + default_result_value: Action's default result value. + Maximum 1000 characters. Optional. + async_polling_interval_seconds: Polling interval in seconds for async + actions. Cannot exceed total timeout. Optional. + async_total_timeout_seconds: Total async timeout in seconds. + Maximum 1209600 (14 days). Optional. + dynamic_results: List of dynamic result metadata dicts. + Max 50. Optional. + parameters: List of ActionParameter instances or dicts. + Max 50. Optional. + ai_generated: Whether the action was generated by AI. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationAction resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ActionParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "timeoutSeconds": timeout_seconds, + "enabled": enabled, + "scriptResultName": script_result_name, + "async": is_async, + "description": description, + "defaultResultValue": default_result_value, + "asyncPollingIntervalSeconds": async_polling_interval_seconds, + "asyncTotalTimeoutSeconds": async_total_timeout_seconds, + "dynamicResults": dynamic_results, + "parameters": resolved_parameters, + "aiGenerated": ai_generated, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions", + api_version=api_version, + json=body, + ) + + +def update_integration_action( + client: "ChronicleClient", + integration_name: str, + action_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + script_result_name: str | None = None, + is_async: bool | None = None, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any] | ActionParameter] | None = None, + ai_generated: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing custom action for a given integration. + + Only custom actions can be updated; predefined commercial actions are + immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to update. + display_name: Action's display name. Maximum 150 characters. + script: Action's Python script. Maximum size 5MB. + timeout_seconds: Action timeout in seconds. Maximum 1200. + enabled: Whether the action is enabled or disabled. + script_result_name: Field name that holds the script result. + Maximum 100 characters. + is_async: Whether the action is asynchronous. + description: Action's description. Maximum 400 characters. + default_result_value: Action's default result value. + Maximum 1000 characters. + async_polling_interval_seconds: Polling interval in seconds for async + actions. Cannot exceed total timeout. + async_total_timeout_seconds: Total async timeout in seconds. Maximum + 1209600 (14 days). + dynamic_results: List of dynamic result metadata dicts. Max 50. + parameters: List of ActionParameter instances or dicts. + Max 50. Optional. + ai_generated: Whether the action was generated by AI. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationAction resource. + + Raises: + APIError: If the API request fails. + """ + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("timeoutSeconds", "timeoutSeconds", timeout_seconds), + ("enabled", "enabled", enabled), + ("scriptResultName", "scriptResultName", script_result_name), + ("async", "async", is_async), + ("description", "description", description), + ("defaultResultValue", "defaultResultValue", default_result_value), + ( + "asyncPollingIntervalSeconds", + "asyncPollingIntervalSeconds", + async_polling_interval_seconds, + ), + ( + "asyncTotalTimeoutSeconds", + "asyncTotalTimeoutSeconds", + async_total_timeout_seconds, + ), + ("dynamicResults", "dynamicResults", dynamic_results), + ("parameters", "parameters", parameters), + ("aiGenerated", "aiGenerated", ai_generated), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions/{action_id}", + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_action_test( + client: "ChronicleClient", + integration_name: str, + test_case_id: int, + action: dict[str, Any], + scope: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Execute a test run of an integration action's script. + + Use this method to verify custom action logic, connectivity, and data + parsing against a specified integration instance and test case before + making the action available in playbooks. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + test_case_id: ID of the action test case. + action: Dict containing the IntegrationAction to test. + scope: The action test scope. + integration_instance_id: The integration instance ID to use. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the test execution results with the following fields: + - output: The script output. + - debugOutput: The script debug output. + - resultJson: The result JSON if it exists (optional). + - resultName: The script result name (optional). + + Raises: + APIError: If the API request fails. + """ + body = { + "testCaseId": test_case_id, + "action": action, + "scope": scope, + "integrationInstanceId": integration_instance_id, + } + + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions:executeTest", + api_version=api_version, + json=body, + ) + + +def get_integration_actions_by_environment( + client: "ChronicleClient", + integration_name: str, + environments: list[str], + include_widgets: bool, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """List actions executable within specified environments. + + Use this method to discover which automated tasks have active integration + instances configured for a particular network or organizational context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch actions for. + environments: List of environments to filter actions by. + include_widgets: Whether to include widget actions in the response. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing a list of IntegrationAction objects that have + integration instances in one of the given environments. + + Raises: + APIError: If the API request fails. + """ + params = { + "environments": environments, + "includeWidgets": include_widgets, + } + + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions:fetchActionsByEnvironment", + api_version=api_version, + params=params, + ) + + +def get_integration_action_template( + client: "ChronicleClient", + integration_name: str, + is_async: bool = False, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a new integration action. + + Use this method to jumpstart the development of a custom automated task + by providing boilerplate code for either synchronous or asynchronous + operations. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + is_async: Whether to fetch a template for an async action. Default + is False. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationAction template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/actions:fetchTemplate", + api_version=api_version, + params={"async": is_async}, + ) diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py index c2e941c4..4f72f5ea 100644 --- a/src/secops/chronicle/integration/integrations.py +++ b/src/secops/chronicle/integration/integrations.py @@ -253,7 +253,8 @@ def download_integration_dependency( api_version: API version to use for the request. Default is V1BETA. Returns: - Dict containing the details of the downloaded dependency + Empty dict if the download was successful, or a dict containing error + details if the download failed Raises: APIError: If the API request fails @@ -678,7 +679,7 @@ def update_custom_integration( client, method="POST", endpoint_path=f"integrations/" - f"{integration_name}:updateCustomIntegration", + f"{integration_name}:updateCustomIntegration", json=body, params=params, api_version=api_version, diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index f27a5ad4..b5492dff 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -13,6 +13,7 @@ # limitations under the License. # """Data models for Chronicle API responses.""" + import json import sys from dataclasses import asdict, dataclass, field @@ -119,7 +120,9 @@ class IntegrationParamType(str, Enum): DOMAIN = "DOMAIN" EMAIL = "EMAIL" VALUES_LIST = "VALUES_LIST" - VALUES_AS_SEMICOLON_SEPARATED_STRING = "VALUES_AS_SEMICOLON_SEPARATED_STRING" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" SCRIPT = "SCRIPT" FILTER_LIST = "FILTER_LIST" @@ -150,7 +153,7 @@ def to_dict(self) -> dict: data: dict = { "displayName": self.display_name, "propertyName": self.property_name, - "type": str(self.type), + "type": str(self.type.value), "mandatory": self.mandatory, } if self.description is not None: @@ -160,6 +163,71 @@ def to_dict(self) -> dict: return data +class ActionParamType(str, Enum): + """Action parameter types for Chronicle SOAR integration actions.""" + + STRING = "STRING" + BOOLEAN = "BOOLEAN" + WFS_REPOSITORY = "WFS_REPOSITORY" + USER_REPOSITORY = "USER_REPOSITORY" + STAGES_REPOSITORY = "STAGES_REPOSITORY" + CLOSE_CASE_REASON_REPOSITORY = "CLOSE_CASE_REASON_REPOSITORY" + CLOSE_CASE_ROOT_CAUSE_REPOSITORY = "CLOSE_CASE_ROOT_CAUSE_REPOSITORY" + PRIORITIES_REPOSITORY = "PRIORITIES_REPOSITORY" + EMAIL_CONTENT = "EMAIL_CONTENT" + CONTENT = "CONTENT" + PASSWORD = "PASSWORD" + ENTITY_TYPE = "ENTITY_TYPE" + MULTI_VALUES = "MULTI_VALUES" + LIST = "LIST" + CODE = "CODE" + MULTIPLE_CHOICE_PARAMETER = "MULTIPLE_CHOICE_PARAMETER" + + +class ActionType(str, Enum): + """Action types for Chronicle SOAR integration actions.""" + + UNSPECIFIED = "ACTION_TYPE_UNSPECIFIED" + STANDARD = "STANDARD" + AI_AGENT = "AI_AGENT" + + +@dataclass +class ActionParameter: + """A parameter definition for a Chronicle SOAR integration action. + + Attributes: + display_name: The parameter's display name. Maximum 150 characters. + type: The parameter's type. + description: The parameter's description. Maximum 150 characters. + mandatory: Whether the parameter is mandatory. + default_value: The default value of the parameter. + Maximum 150 characters. + optional_values: Parameter's optional values. Maximum 50 items. + """ + + display_name: str + type: ActionParamType + description: str + mandatory: bool + default_value: str | None = None + optional_values: list[str] | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "type": str(self.type.value), + "description": self.description, + "mandatory": self.mandatory, + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.optional_values is not None: + data["optionalValues"] = self.optional_values + return data + + @dataclass class TimeInterval: """Time interval with start and end times.""" diff --git a/tests/chronicle/integration/__init__.py b/tests/chronicle/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/chronicle/integration/test_actions.py b/tests/chronicle/integration/test_actions.py new file mode 100644 index 00000000..6cd0a9ac --- /dev/null +++ b/tests/chronicle/integration/test_actions.py @@ -0,0 +1,666 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration actions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.actions import ( + list_integration_actions, + get_integration_action, + delete_integration_action, + create_integration_action, + update_integration_action, + execute_integration_action_test, + get_integration_actions_by_environment, + get_integration_action_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_actions tests -- + + +def test_list_integration_actions_success(chronicle_client): + """Test list_integration_actions delegates to chronicle_paginated_request.""" + expected = {"actions": [{"name": "a1"}, {"name": "a2"}], "nextPageToken": "t"} + + with patch( + "secops.chronicle.integration.actions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + # Avoid assuming how format_resource_id encodes/cases values + "secops.chronicle.integration.actions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_actions( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/My Integration/actions", + items_key="actions", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_actions_default_args(chronicle_client): + """Test list_integration_actions with default args.""" + expected = {"actions": []} + + with patch( + "secops.chronicle.integration.actions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_actions( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/test-integration/actions", + items_key="actions", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_integration_actions_with_filter_order_expand(chronicle_client): + """Test list_integration_actions passes filter/orderBy/expand in extra_params.""" + expected = {"actions": [{"name": "a1"}]} + + with patch( + "secops.chronicle.integration.actions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_actions( + chronicle_client, + integration_name="test-integration", + filter_string='displayName = "My Action"', + order_by="displayName", + expand="parameters,dynamicResults", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/test-integration/actions", + items_key="actions", + page_size=None, + page_token=None, + extra_params={ + "filter": 'displayName = "My Action"', + "orderBy": "displayName", + "expand": "parameters,dynamicResults", + }, + as_list=False, + ) + + +def test_list_integration_actions_as_list(chronicle_client): + """Test list_integration_actions with as_list=True.""" + expected = [{"name": "a1"}, {"name": "a2"}] + + with patch( + "secops.chronicle.integration.actions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_actions( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/test-integration/actions", + items_key="actions", + page_size=None, + page_token=None, + extra_params={}, + as_list=True, + ) + + +def test_list_integration_actions_error(chronicle_client): + """Test list_integration_actions propagates APIError from helper.""" + with patch( + "secops.chronicle.integration.actions.chronicle_paginated_request", + side_effect=APIError("Failed to list integration actions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_actions( + chronicle_client, + integration_name="test-integration", + ) + + assert "Failed to list integration actions" in str(exc_info.value) + + +# -- get_integration_action tests -- + + +def test_get_integration_action_success(chronicle_client): + """Test get_integration_action returns expected result.""" + expected = {"name": "actions/a1", "displayName": "Action 1"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/actions/a1", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_action_error(chronicle_client): + """Test get_integration_action raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to get integration action"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + assert "Failed to get integration action" in str(exc_info.value) + + +# -- delete_integration_action tests -- + + +def test_delete_integration_action_success(chronicle_client): + """Test delete_integration_action issues DELETE request.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/actions/a1", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_action_error(chronicle_client): + """Test delete_integration_action raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to delete integration action"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + assert "Failed to delete integration action" in str(exc_info.value) + + +# -- create_integration_action tests -- + + +def test_create_integration_action_required_fields_only(chronicle_client): + """Test create_integration_action sends only required fields when optionals omitted.""" + expected = {"name": "actions/new", "displayName": "My Action"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_action( + chronicle_client, + integration_name="test-integration", + display_name="My Action", + script="print('hi')", + timeout_seconds=120, + enabled=True, + script_result_name="result", + is_async=False, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/actions", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Action", + "script": "print('hi')", + "timeoutSeconds": 120, + "enabled": True, + "scriptResultName": "result", + "async": False, + }, + ) + + +def test_create_integration_action_all_fields(chronicle_client): + """Test create_integration_action with all optional fields.""" + expected = {"name": "actions/new"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_action( + chronicle_client, + integration_name="test-integration", + display_name="My Action", + script="print('hi')", + timeout_seconds=120, + enabled=True, + script_result_name="result", + is_async=True, + description="desc", + default_result_value="default", + async_polling_interval_seconds=5, + async_total_timeout_seconds=60, + dynamic_results=[{"name": "dr1"}], + parameters=[{"name": "p1"}], + ai_generated=False, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/actions", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Action", + "script": "print('hi')", + "timeoutSeconds": 120, + "enabled": True, + "scriptResultName": "result", + "async": True, + "description": "desc", + "defaultResultValue": "default", + "asyncPollingIntervalSeconds": 5, + "asyncTotalTimeoutSeconds": 60, + "dynamicResults": [{"name": "dr1"}], + "parameters": [{"name": "p1"}], + "aiGenerated": False, + }, + ) + + +def test_create_integration_action_none_fields_excluded(chronicle_client): + """Test that None optional fields are not included in request body.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value={"name": "actions/new"}, + ) as mock_request: + create_integration_action( + chronicle_client, + integration_name="test-integration", + display_name="My Action", + script="print('hi')", + timeout_seconds=120, + enabled=True, + script_result_name="result", + is_async=False, + description=None, + default_result_value=None, + async_polling_interval_seconds=None, + async_total_timeout_seconds=None, + dynamic_results=None, + parameters=None, + ai_generated=None, + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/actions", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Action", + "script": "print('hi')", + "timeoutSeconds": 120, + "enabled": True, + "scriptResultName": "result", + "async": False, + }, + ) + + +def test_create_integration_action_error(chronicle_client): + """Test create_integration_action raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to create integration action"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_action( + chronicle_client, + integration_name="test-integration", + display_name="My Action", + script="print('hi')", + timeout_seconds=120, + enabled=True, + script_result_name="result", + is_async=False, + ) + assert "Failed to create integration action" in str(exc_info.value) + + +# -- update_integration_action tests -- + + +def test_update_integration_action_with_explicit_update_mask(chronicle_client): + """Test update_integration_action passes through explicit update_mask.""" + expected = {"name": "actions/a1", "displayName": "New Name"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + display_name="New Name", + update_mask="displayName", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration/actions/a1", + api_version=APIVersion.V1BETA, + json={"displayName": "New Name"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_action_auto_update_mask(chronicle_client): + """Test update_integration_action auto-generates updateMask based on fields. + + build_patch_body ordering isn't guaranteed; assert order-insensitively. + """ + expected = {"name": "actions/a1"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + enabled=False, + timeout_seconds=300, + ) + + assert result == expected + + # Assert the call happened once and inspect args to avoid ordering issues. + assert mock_request.call_count == 1 + _, kwargs = mock_request.call_args + + assert kwargs["method"] == "PATCH" + assert kwargs["endpoint_path"] == "integrations/test-integration/actions/a1" + assert kwargs["api_version"] == APIVersion.V1BETA + + assert kwargs["json"] == {"enabled": False, "timeoutSeconds": 300} + + update_mask = kwargs["params"]["updateMask"] + assert set(update_mask.split(",")) == {"enabled", "timeoutSeconds"} + + +def test_update_integration_action_error(chronicle_client): + """Test update_integration_action raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to update integration action"), + ): + with pytest.raises(APIError) as exc_info: + update_integration_action( + chronicle_client, + integration_name="test-integration", + action_id="a1", + display_name="New Name", + ) + assert "Failed to update integration action" in str(exc_info.value) + + +# -- test_integration_action tests -- + + +def test_execute_test_integration_action_success(chronicle_client): + """Test test_integration_action issues executeTest POST with correct body.""" + expected = {"output": "ok", "debugOutput": ""} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + action = {"displayName": "My Action", "script": "print('hi')"} + result = execute_integration_action_test( + chronicle_client, + integration_name="test-integration", + test_case_id=123, + action=action, + scope="INTEGRATION_INSTANCE", + integration_instance_id="inst-1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/actions:executeTest", + api_version=APIVersion.V1BETA, + json={ + "testCaseId": 123, + "action": action, + "scope": "INTEGRATION_INSTANCE", + "integrationInstanceId": "inst-1", + }, + ) + + +def test_execute_test_integration_action_error(chronicle_client): + """Test test_integration_action raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to test integration action"), + ): + with pytest.raises(APIError) as exc_info: + execute_integration_action_test( + chronicle_client, + integration_name="test-integration", + test_case_id=123, + action={"displayName": "My Action"}, + scope="INTEGRATION_INSTANCE", + integration_instance_id="inst-1", + ) + assert "Failed to test integration action" in str(exc_info.value) + + +# -- get_integration_actions_by_environment tests -- + + +def test_get_integration_actions_by_environment_success(chronicle_client): + """Test get_integration_actions_by_environment issues GET with correct params.""" + expected = {"actions": [{"name": "a1"}]} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_actions_by_environment( + chronicle_client, + integration_name="test-integration", + environments=["prod", "dev"], + include_widgets=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/actions:fetchActionsByEnvironment", + api_version=APIVersion.V1BETA, + params={"environments": ["prod", "dev"], "includeWidgets": True}, + ) + + +def test_get_integration_actions_by_environment_error(chronicle_client): + """Test get_integration_actions_by_environment raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to fetch actions by environment"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_actions_by_environment( + chronicle_client, + integration_name="test-integration", + environments=["prod"], + include_widgets=False, + ) + assert "Failed to fetch actions by environment" in str(exc_info.value) + + +# -- get_integration_action_template tests -- + + +def test_get_integration_action_template_default_async_false(chronicle_client): + """Test get_integration_action_template uses async=False by default.""" + expected = {"script": "# template"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_action_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/actions:fetchTemplate", + api_version=APIVersion.V1BETA, + params={"async": False}, + ) + + +def test_get_integration_action_template_async_true(chronicle_client): + """Test get_integration_action_template with is_async=True.""" + expected = {"script": "# async template"} + + with patch( + "secops.chronicle.integration.actions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_action_template( + chronicle_client, + integration_name="test-integration", + is_async=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/actions:fetchTemplate", + api_version=APIVersion.V1BETA, + params={"async": True}, + ) + + +def test_get_integration_action_template_error(chronicle_client): + """Test get_integration_action_template raises APIError on failure.""" + with patch( + "secops.chronicle.integration.actions.chronicle_request", + side_effect=APIError("Failed to fetch action template"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_action_template( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to fetch action template" in str(exc_info.value) \ No newline at end of file diff --git a/tests/chronicle/test_integrations.py b/tests/chronicle/integration/test_integrations.py similarity index 100% rename from tests/chronicle/test_integrations.py rename to tests/chronicle/integration/test_integrations.py From 8a4ff68a6a480bacd58c01c2b089957245020e12 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 6 Mar 2026 14:20:35 +0000 Subject: [PATCH 23/46] chore: black formatting and linting --- src/secops/chronicle/__init__.py | 2 +- src/secops/chronicle/entity.py | 1 + src/secops/chronicle/feeds.py | 1 + src/secops/chronicle/gemini.py | 1 + .../integration/marketplace_integrations.py | 2 +- src/secops/chronicle/stats.py | 1 + src/secops/chronicle/utils/format_utils.py | 17 +++++++++---- src/secops/chronicle/utils/request_utils.py | 25 ++++++++++--------- .../cli/commands/integration/integration.py | 16 +++--------- .../integration/integration_client.py | 2 +- 10 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index f71d445d..cb1c8065 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -208,7 +208,7 @@ get_marketplace_integration, get_marketplace_integration_diff, install_marketplace_integration, - uninstall_marketplace_integration + uninstall_marketplace_integration, ) __all__ = [ diff --git a/src/secops/chronicle/entity.py b/src/secops/chronicle/entity.py index 429d4393..84e5060a 100644 --- a/src/secops/chronicle/entity.py +++ b/src/secops/chronicle/entity.py @@ -15,6 +15,7 @@ """ Provides entity search, analysis and summarization functionality for Chronicle. """ + import ipaddress import re from datetime import datetime diff --git a/src/secops/chronicle/feeds.py b/src/secops/chronicle/feeds.py index b9ed7f22..8030b753 100644 --- a/src/secops/chronicle/feeds.py +++ b/src/secops/chronicle/feeds.py @@ -15,6 +15,7 @@ """ Provides ingestion feed management functionality for Chronicle. """ + import json import os import sys diff --git a/src/secops/chronicle/gemini.py b/src/secops/chronicle/gemini.py index abed52cb..eee42374 100644 --- a/src/secops/chronicle/gemini.py +++ b/src/secops/chronicle/gemini.py @@ -16,6 +16,7 @@ Provides access to Chronicle's Gemini conversational AI interface. """ + import re from typing import Any diff --git a/src/secops/chronicle/integration/marketplace_integrations.py b/src/secops/chronicle/integration/marketplace_integrations.py index 0297d470..2cd3fe75 100644 --- a/src/secops/chronicle/integration/marketplace_integrations.py +++ b/src/secops/chronicle/integration/marketplace_integrations.py @@ -196,4 +196,4 @@ def uninstall_marketplace_integration( method="POST", endpoint_path=f"marketplaceIntegrations/{integration_name}:uninstall", api_version=api_version, - ) \ No newline at end of file + ) diff --git a/src/secops/chronicle/stats.py b/src/secops/chronicle/stats.py index 99b46309..42e31aba 100644 --- a/src/secops/chronicle/stats.py +++ b/src/secops/chronicle/stats.py @@ -13,6 +13,7 @@ # limitations under the License. # """Statistics functionality for Chronicle searches.""" + from datetime import datetime from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/utils/format_utils.py b/src/secops/chronicle/utils/format_utils.py index d3812fa5..126ae503 100644 --- a/src/secops/chronicle/utils/format_utils.py +++ b/src/secops/chronicle/utils/format_utils.py @@ -66,7 +66,8 @@ def parse_json_list( raise APIError(f"Invalid {field_name} JSON") from e return value -#pylint: disable=line-too-long + +# pylint: disable=line-too-long def build_patch_body( field_map: list[tuple[str, str, Any]], update_mask: str | None = None, @@ -82,10 +83,16 @@ def build_patch_body( Returns: Tuple of (body, params) where params contains the updateMask or is None. """ - body = {api_key: value for api_key, _, value in field_map if value is not None} - mask_fields = [mask_key for _, mask_key, value in field_map if value is not None] - - resolved_mask = update_mask or (",".join(mask_fields) if mask_fields else None) + body = { + api_key: value for api_key, _, value in field_map if value is not None + } + mask_fields = [ + mask_key for _, mask_key, value in field_map if value is not None + ] + + resolved_mask = update_mask or ( + ",".join(mask_fields) if mask_fields else None + ) params = {"updateMask": resolved_mask} if resolved_mask else None return body, params diff --git a/src/secops/chronicle/utils/request_utils.py b/src/secops/chronicle/utils/request_utils.py index 898ca85c..70395141 100644 --- a/src/secops/chronicle/utils/request_utils.py +++ b/src/secops/chronicle/utils/request_utils.py @@ -300,16 +300,16 @@ def chronicle_request( def chronicle_request_bytes( - client: "ChronicleClient", - method: str, - endpoint_path: str, - *, - api_version: str = APIVersion.V1, - params: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, - expected_status: int | set[int] | tuple[int, ...] | list[int] = 200, - error_message: str | None = None, - timeout: int | None = None, + client: "ChronicleClient", + method: str, + endpoint_path: str, + *, + api_version: str = APIVersion.V1, + params: Optional[dict[str, Any]] = None, + headers: Optional[dict[str, Any]] = None, + expected_status: int | set[int] | tuple[int, ...] | list[int] = 200, + error_message: str | None = None, + timeout: int | None = None, ) -> bytes: base = f"{client.base_url(api_version)}/{client.instance_id}" @@ -351,8 +351,9 @@ def chronicle_request_bytes( f"status={response.status_code}, response={data}" ) from None except ValueError: - preview = _safe_body_preview(getattr(response, "text", ""), - limit=MAX_BODY_CHARS) + preview = _safe_body_preview( + getattr(response, "text", ""), limit=MAX_BODY_CHARS + ) raise APIError( f"{error_message or "API request failed"}: method={method}, url={url}, " f"status={response.status_code}, response_text={preview}" diff --git a/src/secops/cli/commands/integration/integration.py b/src/secops/cli/commands/integration/integration.py index a9030b86..dd73a600 100644 --- a/src/secops/cli/commands/integration/integration.py +++ b/src/secops/cli/commands/integration/integration.py @@ -283,9 +283,7 @@ def setup_integrations_command(subparsers): dest="integration_id", required=True, ) - deps_parser.set_defaults( - func=handle_get_integration_dependencies_command - ) + deps_parser.set_defaults(func=handle_get_integration_dependencies_command) # restricted-agents command restricted_parser = lvl1.add_parser( @@ -557,9 +555,7 @@ def handle_integration_delete_command(args, chronicle): chronicle.delete_integration( integration_name=args.integration_id, ) - print( - f"Integration {args.integration_id} deleted successfully." - ) + print(f"Integration {args.integration_id} deleted successfully.") except Exception as e: # pylint: disable=broad-exception-caught print(f"Error deleting integration: {e}", file=sys.stderr) sys.exit(1) @@ -651,9 +647,7 @@ def handle_get_integration_affected_items_command(args, chronicle): ) output_formatter(out, getattr(args, "output", "json")) except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting integration affected items: {e}", file=sys.stderr - ) + print(f"Error getting integration affected items: {e}", file=sys.stderr) sys.exit(1) @@ -677,9 +671,7 @@ def handle_get_integration_dependencies_command(args, chronicle): ) output_formatter(out, getattr(args, "output", "json")) except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting integration dependencies: {e}", file=sys.stderr - ) + print(f"Error getting integration dependencies: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index ea5f5035..8fb00a83 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -29,4 +29,4 @@ def setup_integrations_command(subparsers): # Setup all subcommands under `integration` marketplace_integration.setup_marketplace_integrations_command(lvl1) - integration.setup_integrations_command(lvl1) \ No newline at end of file + integration.setup_integrations_command(lvl1) From 2e8e4d966f41196f255c5309bf25e7535c92576f Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 6 Mar 2026 18:57:05 +0000 Subject: [PATCH 24/46] feat: add functions for integration connectors --- README.md | 146 ++++ api_module_mapping.md | 16 +- src/secops/chronicle/client.py | 317 +++++++++ src/secops/chronicle/integration/actions.py | 10 +- .../chronicle/integration/connectors.py | 405 +++++++++++ src/secops/chronicle/models.py | 100 +++ .../chronicle/integration/test_connectors.py | 665 ++++++++++++++++++ 7 files changed, 1653 insertions(+), 6 deletions(-) create mode 100644 src/secops/chronicle/integration/connectors.py create mode 100644 tests/chronicle/integration/test_connectors.py diff --git a/README.md b/README.md index 80307724..35dc877a 100644 --- a/README.md +++ b/README.md @@ -2074,6 +2074,152 @@ Get a template for creating an action in an integration template = chronicle.get_integration_action_template("MyIntegration") ``` +### Integration Connectors + +List all available connectors for an integration: + +```python +# Get all connectors for an integration +connectors = chronicle.list_integration_connectors("AWSSecurityHub") + +# Get all connectors as a list +connectors = chronicle.list_integration_connectors("AWSSecurityHub", as_list=True) + +# Get only enabled connectors +connectors = chronicle.list_integration_connectors( + "AWSSecurityHub", + filter_string="enabled = true" +) + +# Exclude staging connectors +connectors = chronicle.list_integration_connectors( + "AWSSecurityHub", + exclude_staging=True +) +``` + +Get details of a specific connector: + +```python +connector = chronicle.get_integration_connector( + integration_name="AWSSecurityHub", + connector_id="123" +) +``` + +Create an integration connector: + +```python +from secops.chronicle.models import ( + ConnectorParameter, + ConnectorParamType, + ConnectorParamMode, + ConnectorRule, + ConnectorRuleType +) + +new_connector = chronicle.create_integration_connector( + integration_name="MyIntegration", + display_name="New Connector", + description="This is a new connector", + script="print('Fetching data...')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event_type", + parameters=[ + ConnectorParameter( + display_name="API Key", + type=ConnectorParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="API key for authentication" + ) + ], + rules=[ + ConnectorRule( + display_name="Allow List", + type=ConnectorRuleType.ALLOW_LIST + ) + ] +) +``` + +Update an integration connector: + +```python +from secops.chronicle.models import ( + ConnectorParameter, + ConnectorParamType, + ConnectorParamMode +) + +updated_connector = chronicle.update_integration_connector( + integration_name="MyIntegration", + connector_id="123", + display_name="Updated Connector Name", + description="Updated description", + enabled=False, + timeout_seconds=600, + parameters=[ + ConnectorParameter( + display_name="API Token", + type=ConnectorParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="Updated authentication token" + ) + ], + script="print('Updated connector script')" +) +``` + +Delete an integration connector: + +```python +chronicle.delete_integration_connector( + integration_name="MyIntegration", + connector_id="123" +) +``` + +Execute a test run of an integration connector: + +```python +# Test a connector before saving it +connector_config = { + "displayName": "Test Connector", + "script": "print('Testing connector')", + "enabled": True, + "timeoutSeconds": 300, + "productFieldName": "product", + "eventFieldName": "event_type" +} + +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=connector_config +) + +print(f"Output: {test_result.get('outputMessage')}") +print(f"Debug: {test_result.get('debugOutputMessage')}") + +# Test with a specific agent for remote execution +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=connector_config, + agent_identifier="agent-123" +) +``` + +Get a template for creating a connector in an integration: + +```python +template = chronicle.get_integration_connector_template("MyIntegration") +print(f"Template script: {template.get('script')}") +``` + + ## Rule Management The SDK provides comprehensive support for managing Chronicle detection rules: diff --git a/api_module_mapping.md b/api_module_mapping.md index e0a5da89..1bae1718 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,7 +7,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 13 endpoints implemented +- **v1beta:** 20 endpoints implemented - **v1alpha:** 113 endpoints implemented ## Endpoint Mapping @@ -91,6 +91,13 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | | | integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | | | integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | | +| integrations.connectors.create | v1beta | chronicle.integration.connectors.create_integration_connector | | +| integrations.connectors.delete | v1beta | chronicle.integration.connectors.delete_integration_connector | | +| integrations.connectors.executeTest | v1beta | chronicle.integration.connectors.execute_integration_connector_test | | +| integrations.connectors.fetchTemplate | v1beta | chronicle.integration.connectors.get_integration_connector_template | | +| integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | | +| integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | | +| integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -294,6 +301,13 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | | | integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | | | integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.create | v1alpha | chronicle.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.delete | v1alpha | chronicle.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.executeTest | v1alpha | chronicle.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.fetchTemplate | v1alpha | chronicle.integration.connectors.get_integration_connector_template(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 4e4ccd89..2711cb0a 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -164,9 +164,20 @@ list_integration_actions as _list_integration_actions, update_integration_action as _update_integration_action, ) +from secops.chronicle.integration.connectors import ( + create_integration_connector as _create_integration_connector, + delete_integration_connector as _delete_integration_connector, + execute_integration_connector_test as _execute_integration_connector_test, + get_integration_connector as _get_integration_connector, + get_integration_connector_template as _get_integration_connector_template, + list_integration_connectors as _list_integration_connectors, + update_integration_connector as _update_integration_connector, +) from secops.chronicle.models import ( APIVersion, CaseList, + ConnectorParameter, + ConnectorRule, DashboardChart, DashboardQuery, DiffType, @@ -1750,6 +1761,312 @@ def get_integration_action_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Connector methods + # ------------------------------------------------------------------------- + + def list_integration_connectors( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all connectors defined for a specific integration. + + Args: + integration_name: Name of the integration to list connectors + for. + page_size: Maximum number of connectors to return. Defaults + to 50, maximum is 1000. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter connectors. + order_by: Field to sort the connectors by. + exclude_staging: Whether to exclude staging connectors from + the response. By default, staging connectors are included. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of connectors instead of a + dict with connectors list and nextPageToken. + + Returns: + If as_list is True: List of connectors. + If as_list is False: Dict with connectors list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_connectors( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + exclude_staging=exclude_staging, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_connector( + self, + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single connector for a given integration. + + Use this method to retrieve the Python script, configuration parameters, + and field mapping logic for a specific connector. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationConnector. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_connector( + self, + integration_name, + connector_id, + api_version=api_version, + ) + + def delete_integration_connector( + self, + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific custom connector from a given integration. + + Only custom connectors can be deleted; commercial connectors are + immutable. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_connector( + self, + integration_name, + connector_id, + api_version=api_version, + ) + + def create_integration_connector( + self, + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + product_field_name: str, + event_field_name: str, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new custom connector for a given integration. + + Use this method to define how to fetch and parse alerts from a + unique or unofficial data source. Each connector must have a + unique display name and a functional Python script. + + Args: + integration_name: Name of the integration to create the + connector for. + display_name: Connector's display name. Required. + script: Connector's Python script. Required. + timeout_seconds: Timeout in seconds for a single script run. + Required. + enabled: Whether the connector is enabled or disabled. + Required. + product_field_name: Field name used to determine the device + product. Required. + event_field_name: Field name used to determine the event + name (sub-type). Required. + description: Connector's description. Optional. + parameters: List of ConnectorParameter instances or dicts. + Optional. + rules: List of ConnectorRule instances or dicts. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationConnector + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_connector( + self, + integration_name, + display_name, + script, + timeout_seconds, + enabled, + product_field_name, + event_field_name, + description=description, + parameters=parameters, + rules=rules, + api_version=api_version, + ) + + def update_integration_connector( + self, + integration_name: str, + connector_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing custom connector for a given integration. + + Only custom connectors can be updated; commercial connectors are + immutable. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to update. + display_name: Connector's display name. + script: Connector's Python script. + timeout_seconds: Timeout in seconds for a single script run. + enabled: Whether the connector is enabled or disabled. + product_field_name: Field name used to determine the device product. + event_field_name: Field name used to determine the event name + (sub-type). + description: Connector's description. + parameters: List of ConnectorParameter instances or dicts. + rules: List of ConnectorRule instances or dicts. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_connector( + self, + integration_name, + connector_id, + display_name=display_name, + script=script, + timeout_seconds=timeout_seconds, + enabled=enabled, + product_field_name=product_field_name, + event_field_name=event_field_name, + description=description, + parameters=parameters, + rules=rules, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_connector_test( + self, + integration_name: str, + connector: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Execute a test run of a connector's Python script. + + Use this method to verify data fetching logic, authentication, + and parsing logic before enabling the connector for production + ingestion. The full connector object is required as the test + can be run without saving the connector first. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector: Dict containing the IntegrationConnector to test. + agent_identifier: Agent identifier for remote testing. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the test execution results with the + following fields: + - outputMessage: Human-readable output message set by + the script. + - debugOutputMessage: The script debug output. + - resultJson: The result JSON if it exists (optional). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_connector_test( + self, + integration_name, + connector, + agent_identifier=agent_identifier, + api_version=api_version, + ) + + def get_integration_connector_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new + integration connector. + + Use this method to rapidly initialize the development of a new + connector. + + Args: + integration_name: Name of the integration to fetch the + template for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationConnector template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_connector_template( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/actions.py b/src/secops/chronicle/integration/actions.py index 5867df24..6482c0db 100644 --- a/src/secops/chronicle/integration/actions.py +++ b/src/secops/chronicle/integration/actions.py @@ -223,7 +223,7 @@ def create_integration_action( client, method="POST", endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/actions", + f"/actions", api_version=api_version, json=body, ) @@ -318,7 +318,7 @@ def update_integration_action( client, method="PATCH", endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/actions/{action_id}", + f"/actions/{action_id}", api_version=api_version, json=body, params=params, @@ -370,7 +370,7 @@ def execute_integration_action_test( client, method="POST", endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/actions:executeTest", + f"/actions:executeTest", api_version=api_version, json=body, ) @@ -411,7 +411,7 @@ def get_integration_actions_by_environment( client, method="GET", endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/actions:fetchActionsByEnvironment", + f"/actions:fetchActionsByEnvironment", api_version=api_version, params=params, ) @@ -446,7 +446,7 @@ def get_integration_action_template( client, method="GET", endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/actions:fetchTemplate", + f"/actions:fetchTemplate", api_version=api_version, params={"async": is_async}, ) diff --git a/src/secops/chronicle/integration/connectors.py b/src/secops/chronicle/integration/connectors.py new file mode 100644 index 00000000..7cae92d0 --- /dev/null +++ b/src/secops/chronicle/integration/connectors.py @@ -0,0 +1,405 @@ +# Copyright 2025 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. +# +"""Marketplace integration connectors functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + ConnectorParameter, + ConnectorRule, +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_connectors( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all connectors defined for a specific integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list connectors for. + page_size: Maximum number of connectors to return. Defaults to 50, + maximum is 1000. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter connectors. + order_by: Field to sort the connectors by. + exclude_staging: Whether to exclude staging connectors from the + response. By default, staging connectors are included. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of connectors instead of a dict with + connectors list and nextPageToken. + + Returns: + If as_list is True: List of connectors. + If as_list is False: Dict with connectors list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + "excludeStaging": exclude_staging, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=f"integrations/{format_resource_id(integration_name)}/connectors", + items_key="connectors", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single connector for a given integration. + + Use this method to retrieve the Python script, configuration parameters, + and field mapping logic for a specific connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationConnector. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + ) + + +def delete_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific custom connector from a given integration. + + Only custom connectors can be deleted; commercial connectors are + immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + ) + + +def create_integration_connector( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + product_field_name: str, + event_field_name: str, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new custom connector for a given integration. + + Use this method to define how to fetch and parse alerts from a unique or + unofficial data source. Each connector must have a unique display name + and a functional Python script. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the connector for. + display_name: Connector's display name. Required. + script: Connector's Python script. Required. + timeout_seconds: Timeout in seconds for a single script run. Required. + enabled: Whether the connector is enabled or disabled. Required. + product_field_name: Field name used to determine the device product. + Required. + event_field_name: Field name used to determine the event name + (sub-type). Required. + description: Connector's description. Optional. + parameters: List of ConnectorParameter instances or dicts. Optional. + rules: List of ConnectorRule instances or dicts. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + resolved_rules = ( + [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] + if rules is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "timeoutSeconds": timeout_seconds, + "enabled": enabled, + "productFieldName": product_field_name, + "eventFieldName": event_field_name, + "description": description, + "parameters": resolved_parameters, + "rules": resolved_rules, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing custom connector for a given integration. + + Only custom connectors can be updated; commercial connectors are + immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to update. + display_name: Connector's display name. + script: Connector's Python script. + timeout_seconds: Timeout in seconds for a single script run. + enabled: Whether the connector is enabled or disabled. + product_field_name: Field name used to determine the device product. + event_field_name: Field name used to determine the event name + (sub-type). + description: Connector's description. + parameters: List of ConnectorParameter instances or dicts. + rules: List of ConnectorRule instances or dicts. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + resolved_rules = ( + [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] + if rules is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("timeoutSeconds", "timeoutSeconds", timeout_seconds), + ("enabled", "enabled", enabled), + ("productFieldName", "productFieldName", product_field_name), + ("eventFieldName", "eventFieldName", event_field_name), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ("rules", "rules", resolved_rules), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_connector_test( + client: "ChronicleClient", + integration_name: str, + connector: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Execute a test run of a connector's Python script. + + Use this method to verify data fetching logic, authentication, and parsing + logic before enabling the connector for production ingestion. The full + connector object is required as the test can be run without saving the + connector first. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector: Dict containing the IntegrationConnector to test. + agent_identifier: Agent identifier for remote testing. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the test execution results with the following fields: + - outputMessage: Human-readable output message set by the script. + - debugOutputMessage: The script debug output. + - resultJson: The result JSON if it exists (optional). + + Raises: + APIError: If the API request fails. + """ + body: dict[str, Any] = {"connector": connector} + + if agent_identifier is not None: + body["agentIdentifier"] = agent_identifier + + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/connectors:executeTest", + api_version=api_version, + json=body, + ) + + +def get_integration_connector_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a + new integration connector. + + Use this method to rapidly initialize the development of a new connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationConnector template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index b5492dff..ee9c3a00 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -228,6 +228,106 @@ def to_dict(self) -> dict: return data +class ConnectorParamType(str, Enum): + """Parameter types for Chronicle SOAR integration connectors.""" + + UNSPECIFIED = "PARAM_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + + +class ConnectorParamMode(str, Enum): + """Parameter modes for Chronicle SOAR integration connectors.""" + + UNSPECIFIED = "PARAM_MODE_UNSPECIFIED" + REGULAR = "REGULAR" + CONNECTIVITY = "CONNECTIVITY" + SCRIPT = "SCRIPT" + + +class ConnectorRuleType(str, Enum): + """Rule types for Chronicle SOAR integration connectors.""" + + UNSPECIFIED = "RULE_TYPE_UNSPECIFIED" + ALLOW_LIST = "ALLOW_LIST" + BLOCK_LIST = "BLOCK_LIST" + + +@dataclass +class ConnectorParameter: + """A parameter definition for a Chronicle SOAR integration connector. + + Attributes: + display_name: The parameter's display name. + type: The parameter's type. + mode: The parameter's mode. + mandatory: Whether the parameter is mandatory for configuring a + connector instance. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + description: The parameter's description. + advanced: The parameter's advanced flag. + """ + + display_name: str + type: ConnectorParamType + mode: ConnectorParamMode + mandatory: bool + default_value: str | None = None + description: str | None = None + advanced: bool | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "type": str(self.type.value), + "mode": str(self.mode.value), + "mandatory": self.mandatory, + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.description is not None: + data["description"] = self.description + if self.advanced is not None: + data["advanced"] = self.advanced + return data + + +@dataclass +class ConnectorRule: + """A rule definition for a Chronicle SOAR integration connector. + + Attributes: + display_name: Connector's rule data name. + type: Connector's rule data type. + """ + + display_name: str + type: ConnectorRuleType + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "displayName": self.display_name, + "type": str(self.type.value), + } + + @dataclass class TimeInterval: """Time interval with start and end times.""" diff --git a/tests/chronicle/integration/test_connectors.py b/tests/chronicle/integration/test_connectors.py new file mode 100644 index 00000000..0667bc35 --- /dev/null +++ b/tests/chronicle/integration/test_connectors.py @@ -0,0 +1,665 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration connectors functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + ConnectorParameter, + ConnectorParamType, + ConnectorParamMode, + ConnectorRule, + ConnectorRuleType, +) +from secops.chronicle.integration.connectors import ( + list_integration_connectors, + get_integration_connector, + delete_integration_connector, + create_integration_connector, + update_integration_connector, + execute_integration_connector_test, + get_integration_connector_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_connectors tests -- + + +def test_list_integration_connectors_success(chronicle_client): + """Test list_integration_connectors delegates to chronicle_paginated_request.""" + expected = {"connectors": [{"name": "c1"}, {"name": "c2"}], "nextPageToken": "t"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connectors.format_resource_id", + return_value="My Integration", + ): + result = list_integration_connectors( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/My Integration/connectors", + items_key="connectors", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_connectors_default_args(chronicle_client): + """Test list_integration_connectors with default args.""" + expected = {"connectors": []} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + +def test_list_integration_connectors_with_filters(chronicle_client): + """Test list_integration_connectors with filter and order_by.""" + expected = {"connectors": [{"name": "c1"}]} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + filter_string="enabled=true", + order_by="displayName", + exclude_staging=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "enabled=true", + "orderBy": "displayName", + "excludeStaging": True, + } + + +def test_list_integration_connectors_as_list(chronicle_client): + """Test list_integration_connectors returns list when as_list=True.""" + expected = [{"name": "c1"}, {"name": "c2"}] + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_connectors_error(chronicle_client): + """Test list_integration_connectors raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + side_effect=APIError("Failed to list integration connectors"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_connectors( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to list integration connectors" in str(exc_info.value) + + +# -- get_integration_connector tests -- + + +def test_get_integration_connector_success(chronicle_client): + """Test get_integration_connector issues GET request.""" + expected = { + "name": "connectors/c1", + "displayName": "My Connector", + "script": "print('hello')", + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_connector_error(chronicle_client): + """Test get_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to get integration connector"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to get integration connector" in str(exc_info.value) + + +# -- delete_integration_connector tests -- + + +def test_delete_integration_connector_success(chronicle_client): + """Test delete_integration_connector issues DELETE request.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_connector_error(chronicle_client): + """Test delete_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to delete integration connector"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to delete integration connector" in str(exc_info.value) + + +# -- create_integration_connector tests -- + + +def test_create_integration_connector_required_fields_only(chronicle_client): + """Test create_integration_connector sends only required fields when optionals omitted.""" + expected = {"name": "connectors/new", "displayName": "My Connector"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Connector", + "script": "print('hi')", + "timeoutSeconds": 300, + "enabled": True, + "productFieldName": "product", + "eventFieldName": "event", + }, + ) + + +def test_create_integration_connector_with_optional_fields(chronicle_client): + """Test create_integration_connector includes optional fields when provided.""" + expected = {"name": "connectors/new"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + description="Test connector", + parameters=[{"name": "p1", "type": "STRING"}], + rules=[{"name": "r1", "type": "MAPPING"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test connector" + assert kwargs["json"]["parameters"] == [{"name": "p1", "type": "STRING"}] + assert kwargs["json"]["rules"] == [{"name": "r1", "type": "MAPPING"}] + + +def test_create_integration_connector_with_dataclass_parameters(chronicle_client): + """Test create_integration_connector converts ConnectorParameter dataclasses.""" + expected = {"name": "connectors/new"} + + param = ConnectorParameter( + display_name="API Key", + type=ConnectorParamType.STRING, + mode=ConnectorParamMode.REGULAR, + mandatory=True, + description="API key for authentication", + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["displayName"] == "API Key" + assert params_sent[0]["type"] == "STRING" + + +def test_create_integration_connector_with_dataclass_rules(chronicle_client): + """Test create_integration_connector converts ConnectorRule dataclasses.""" + expected = {"name": "connectors/new"} + + rule = ConnectorRule( + display_name="Mapping Rule", + type=ConnectorRuleType.ALLOW_LIST, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + rules=[rule], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + rules_sent = kwargs["json"]["rules"] + assert len(rules_sent) == 1 + assert rules_sent[0]["displayName"] == "Mapping Rule" + assert rules_sent[0]["type"] == "ALLOW_LIST" + + +def test_create_integration_connector_error(chronicle_client): + """Test create_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to create integration connector"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + ) + assert "Failed to create integration connector" in str(exc_info.value) + + +# -- update_integration_connector tests -- + + +def test_update_integration_connector_with_explicit_update_mask(chronicle_client): + """Test update_integration_connector passes through explicit update_mask.""" + expected = {"name": "connectors/c1", "displayName": "New Name"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + display_name="New Name", + update_mask="displayName", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + json={"displayName": "New Name"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_connector_auto_update_mask(chronicle_client): + """Test update_integration_connector auto-generates updateMask based on fields.""" + expected = {"name": "connectors/c1"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + enabled=False, + timeout_seconds=600, + ) + + assert result == expected + + assert mock_request.call_count == 1 + _, kwargs = mock_request.call_args + + assert kwargs["method"] == "PATCH" + assert kwargs["endpoint_path"] == "integrations/test-integration/connectors/c1" + assert kwargs["api_version"] == APIVersion.V1BETA + + assert kwargs["json"] == {"enabled": False, "timeoutSeconds": 600} + + update_mask = kwargs["params"]["updateMask"] + assert set(update_mask.split(",")) == {"enabled", "timeoutSeconds"} + + +def test_update_integration_connector_with_parameters(chronicle_client): + """Test update_integration_connector with parameters field.""" + expected = {"name": "connectors/c1"} + + param = ConnectorParameter( + display_name="Auth Token", + type=ConnectorParamType.STRING, + mode=ConnectorParamMode.REGULAR, + mandatory=True, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["displayName"] == "Auth Token" + + +def test_update_integration_connector_with_rules(chronicle_client): + """Test update_integration_connector with rules field.""" + expected = {"name": "connectors/c1"} + + rule = ConnectorRule( + display_name="Filter Rule", + type=ConnectorRuleType.BLOCK_LIST, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + rules=[rule], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + rules_sent = kwargs["json"]["rules"] + assert len(rules_sent) == 1 + assert rules_sent[0]["displayName"] == "Filter Rule" + + +def test_update_integration_connector_error(chronicle_client): + """Test update_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to update integration connector"), + ): + with pytest.raises(APIError) as exc_info: + update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + display_name="New Name", + ) + assert "Failed to update integration connector" in str(exc_info.value) + + +# -- execute_integration_connector_test tests -- + + +def test_execute_integration_connector_test_success(chronicle_client): + """Test execute_integration_connector_test sends POST request with connector.""" + expected = { + "outputMessage": "Success", + "debugOutputMessage": "Debug info", + "resultJson": {"status": "ok"}, + } + + connector = { + "displayName": "Test Connector", + "script": "print('test')", + "enabled": True, + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector=connector, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors:executeTest", + api_version=APIVersion.V1BETA, + json={"connector": connector}, + ) + + +def test_execute_integration_connector_test_with_agent_identifier(chronicle_client): + """Test execute_integration_connector_test includes agent_identifier when provided.""" + expected = {"outputMessage": "Success"} + + connector = {"displayName": "Test", "script": "print('test')"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector=connector, + agent_identifier="agent-123", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["agentIdentifier"] == "agent-123" + + +def test_execute_integration_connector_test_error(chronicle_client): + """Test execute_integration_connector_test raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to execute connector test"), + ): + with pytest.raises(APIError) as exc_info: + execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector={"displayName": "Test"}, + ) + assert "Failed to execute connector test" in str(exc_info.value) + + +# -- get_integration_connector_template tests -- + + +def test_get_integration_connector_template_success(chronicle_client): + """Test get_integration_connector_template issues GET request.""" + expected = { + "script": "# Template script\nprint('hello')", + "displayName": "Template Connector", + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_connector_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/connectors:fetchTemplate", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_connector_template_error(chronicle_client): + """Test get_integration_connector_template raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to get connector template"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_connector_template( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to get connector template" in str(exc_info.value) + From 1e1d97940f3c3e686c4746dac56a3e31392989be Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 6 Mar 2026 20:53:17 +0000 Subject: [PATCH 25/46] feat: add functions for integration jobs --- README.md | 223 +++++-- api_module_mapping.md | 18 +- src/secops/chronicle/client.py | 308 +++++++++ src/secops/chronicle/integration/jobs.py | 371 +++++++++++ src/secops/chronicle/models.py | 41 +- .../chronicle/integration/test_connectors.py | 6 +- tests/chronicle/integration/test_jobs.py | 594 ++++++++++++++++++ 7 files changed, 1506 insertions(+), 55 deletions(-) create mode 100644 src/secops/chronicle/integration/jobs.py create mode 100644 tests/chronicle/integration/test_jobs.py diff --git a/README.md b/README.md index 35dc877a..061fb79d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +from tests.chronicle.test_rule_integration import chronicle + # Google SecOps SDK for Python [![PyPI version](https://img.shields.io/pypi/v/secops.svg)](https://pypi.org/project/secops/) @@ -2111,37 +2113,37 @@ Create an integration connector: ```python from secops.chronicle.models import ( - ConnectorParameter, - ConnectorParamType, - ConnectorParamMode, - ConnectorRule, - ConnectorRuleType + ConnectorParameter, + ParamType, + ConnectorParamMode, + ConnectorRule, + ConnectorRuleType ) new_connector = chronicle.create_integration_connector( - integration_name="MyIntegration", - display_name="New Connector", - description="This is a new connector", - script="print('Fetching data...')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event_type", - parameters=[ - ConnectorParameter( - display_name="API Key", - type=ConnectorParamType.PASSWORD, - mode=ConnectorParamMode.CONNECTIVITY, - mandatory=True, - description="API key for authentication" - ) - ], - rules=[ - ConnectorRule( - display_name="Allow List", - type=ConnectorRuleType.ALLOW_LIST - ) - ] + integration_name="MyIntegration", + display_name="New Connector", + description="This is a new connector", + script="print('Fetching data...')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event_type", + parameters=[ + ConnectorParameter( + display_name="API Key", + type=ParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="API key for authentication" + ) + ], + rules=[ + ConnectorRule( + display_name="Allow List", + type=ConnectorRuleType.ALLOW_LIST + ) + ] ) ``` @@ -2149,28 +2151,28 @@ Update an integration connector: ```python from secops.chronicle.models import ( - ConnectorParameter, - ConnectorParamType, - ConnectorParamMode + ConnectorParameter, + ParamType, + ConnectorParamMode ) updated_connector = chronicle.update_integration_connector( - integration_name="MyIntegration", - connector_id="123", - display_name="Updated Connector Name", - description="Updated description", - enabled=False, - timeout_seconds=600, - parameters=[ - ConnectorParameter( - display_name="API Token", - type=ConnectorParamType.PASSWORD, - mode=ConnectorParamMode.CONNECTIVITY, - mandatory=True, - description="Updated authentication token" - ) - ], - script="print('Updated connector script')" + integration_name="MyIntegration", + connector_id="123", + display_name="Updated Connector Name", + description="Updated description", + enabled=False, + timeout_seconds=600, + parameters=[ + ConnectorParameter( + display_name="API Token", + type=ParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="Updated authentication token" + ) + ], + script="print('Updated connector script')" ) ``` @@ -2219,6 +2221,133 @@ template = chronicle.get_integration_connector_template("MyIntegration") print(f"Template script: {template.get('script')}") ``` +### Integration Jobs + +List all available jobs for an integration: + +```python +# Get all jobs for an integration +jobs = chronicle.list_integration_jobs("MyIntegration") +for job in jobs.get("jobs", []): + print(f"Job: {job.get('displayName')}, ID: {job.get('name')}") + +# Get all jobs as a list +jobs = chronicle.list_integration_jobs("MyIntegration", as_list=True) + +# Get only custom jobs +jobs = chronicle.list_integration_jobs( + "MyIntegration", + filter_string="custom = true" +) + +# Exclude staging jobs +jobs = chronicle.list_integration_jobs( + "MyIntegration", + exclude_staging=True +) +``` + +Get details of a specific job: + +```python +job = chronicle.get_integration_job( + integration_name="MyIntegration", + job_id="123" +) +``` + +Create an integration job: + +```python +from secops.chronicle.models import JobParameter, ParamType + +new_job = chronicle.create_integration_job( + integration_name="MyIntegration", + display_name="Scheduled Sync Job", + description="Syncs data from external source", + script="print('Running scheduled job...')", + version=1, + enabled=True, + custom=True, + parameters=[ + JobParameter( + id=1, + display_name="Sync Interval", + description="Interval in minutes", + type=ParamType.INT, + mandatory=True, + default_value="60" + ) + ] +) +``` + +Update an integration job: + +```python +from secops.chronicle.models import JobParameter, ParamType + +updated_job = chronicle.update_integration_job( + integration_name="MyIntegration", + job_id="123", + display_name="Updated Job Name", + description="Updated description", + enabled=False, + version=2, + parameters=[ + JobParameter( + id=1, + display_name="New Parameter", + description="Updated parameter", + type=ParamType.STRING, + mandatory=True, + ) + ], + script="print('Updated job script')" +) +``` + +Delete an integration job: + +```python +chronicle.delete_integration_job( + integration_name="MyIntegration", + job_id="123" +) +``` + +Execute a test run of an integration job: + +```python +# Test a job before saving it +job = chronicle.get_integration_job( + integration_name="MyIntegration", + job_id="123" +) + +test_result = chronicle.execute_integration_job_test( + integration_name="MyIntegration", + job=job +) + +print(f"Output: {test_result.get('output')}") +print(f"Debug: {test_result.get('debugOutput')}") + +# Test with a specific agent for remote execution +test_result = chronicle.execute_integration_job_test( + integration_name="MyIntegration", + job=job, + agent_identifier="agent-123" +) +``` + +Get a template for creating a job in an integration: + +```python +template = chronicle.get_integration_job_template("MyIntegration") +print(f"Template script: {template.get('script')}") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 1bae1718..4e2d4e9d 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 20 endpoints implemented -- **v1alpha:** 113 endpoints implemented +- **v1beta:** 27 endpoints implemented +- **v1alpha:** 120 endpoints implemented ## Endpoint Mapping @@ -98,6 +98,13 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | | | integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | | | integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | | +| integrations.jobs.create | v1beta | chronicle.integration.jobs.create_integration_job | | +| integrations.jobs.delete | v1beta | chronicle.integration.jobs.delete_integration_job | | +| integrations.jobs.executeTest | v1beta | chronicle.integration.jobs.execute_integration_job_test | | +| integrations.jobs.fetchTemplate | v1beta | chronicle.integration.jobs.get_integration_job_template | | +| integrations.jobs.get | v1beta | chronicle.integration.jobs.get_integration_job | | +| integrations.jobs.list | v1beta | chronicle.integration.jobs.list_integration_jobs | | +| integrations.jobs.patch | v1beta | chronicle.integration.jobs.update_integration_job | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -308,6 +315,13 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.fetchTemplate | v1alpha | chronicle.integration.jobs.get_integration_job_template(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.get | v1alpha | chronicle.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.list | v1alpha | chronicle.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.patch | v1alpha | chronicle.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 2711cb0a..f5414737 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -173,6 +173,15 @@ list_integration_connectors as _list_integration_connectors, update_integration_connector as _update_integration_connector, ) +from secops.chronicle.integration.jobs import ( + create_integration_job as _create_integration_job, + delete_integration_job as _delete_integration_job, + execute_integration_job_test as _execute_integration_job_test, + get_integration_job as _get_integration_job, + get_integration_job_template as _get_integration_job_template, + list_integration_jobs as _list_integration_jobs, + update_integration_job as _update_integration_job, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -184,6 +193,7 @@ EntitySummary, InputInterval, IntegrationType, + JobParameter, PythonVersion, TargetMode, TileType, @@ -2067,6 +2077,304 @@ def get_integration_connector_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Job methods + # ------------------------------------------------------------------------- + + def list_integration_jobs( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all jobs defined for a specific integration. + + Use this method to browse the available background and scheduled + automation capabilities provided by a third-party connection. + + Args: + integration_name: Name of the integration to list jobs for. + page_size: Maximum number of jobs to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter jobs. Allowed + filters are: id, custom, system, author, version, + integration. + order_by: Field to sort the jobs by. + exclude_staging: Whether to exclude staging jobs from the + response. By default, staging jobs are included. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of jobs instead of a dict + with jobs list and nextPageToken. + + Returns: + If as_list is True: List of jobs. + If as_list is False: Dict with jobs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_jobs( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + exclude_staging=exclude_staging, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_job( + self, + integration_name: str, + job_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single job for a given integration. + + Use this method to retrieve the Python script, execution + parameters, and versioning information for a background + automation task. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified IntegrationJob. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_job( + self, + integration_name, + job_id, + api_version=api_version, + ) + + def delete_integration_job( + self, + integration_name: str, + job_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific custom job from a given integration. + + Only custom jobs can be deleted; commercial and system jobs + are immutable. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_job( + self, + integration_name, + job_id, + api_version=api_version, + ) + + def create_integration_job( + self, + integration_name: str, + display_name: str, + script: str, + version: int, + enabled: bool, + custom: bool, + description: str | None = None, + parameters: list[dict[str, Any] | JobParameter] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new custom job for a given integration. + + Each job must have a unique display name and a functional + Python script for its background execution. + + Args: + integration_name: Name of the integration to create the job + for. + display_name: Job's display name. Maximum 400 characters. + Required. + script: Job's Python script. Required. + version: Job's version. Required. + enabled: Whether the job is enabled. Required. + custom: Whether the job is custom or commercial. Required. + description: Job's description. Optional. + parameters: List of JobParameter instances or dicts. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationJob resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_job( + self, + integration_name, + display_name, + script, + version, + enabled, + custom, + description=description, + parameters=parameters, + api_version=api_version, + ) + + def update_integration_job( + self, + integration_name: str, + job_id: str, + display_name: str | None = None, + script: str | None = None, + version: int | None = None, + enabled: bool | None = None, + custom: bool | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | JobParameter] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing custom job for a given integration. + + Use this method to modify the Python script or adjust the + parameter definitions for a job. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to update. + display_name: Job's display name. Maximum 400 characters. + script: Job's Python script. + version: Job's version. + enabled: Whether the job is enabled. + custom: Whether the job is custom or commercial. + description: Job's description. + parameters: List of JobParameter instances or dicts. + update_mask: Comma-separated list of fields to update. If + omitted, the mask is auto-generated from whichever + fields are provided. Example: "displayName,script". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated IntegrationJob resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_job( + self, + integration_name, + job_id, + display_name=display_name, + script=script, + version=version, + enabled=enabled, + custom=custom, + description=description, + parameters=parameters, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_job_test( + self, + integration_name: str, + job: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Execute a test run of an integration job's Python script. + + Use this method to verify background automation logic and + connectivity before deploying the job to an instance for + recurring execution. + + Args: + integration_name: Name of the integration the job belongs + to. + job: Dict containing the IntegrationJob to test. + agent_identifier: Agent identifier for remote testing. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the test execution results with the + following fields: + - output: The script output. + - debugOutput: The script debug output. + - resultObjectJson: The result JSON if it exists + (optional). + - resultName: The script result name (optional). + - resultValue: The script result value (optional). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_job_test( + self, + integration_name, + job, + agent_identifier=agent_identifier, + api_version=api_version, + ) + + def get_integration_job_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new + integration job. + + Use this method to rapidly initialize the development of a new + job. + + Args: + integration_name: Name of the integration to fetch the + template for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationJob template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_job_template( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/jobs.py b/src/secops/chronicle/integration/jobs.py new file mode 100644 index 00000000..6d122f8d --- /dev/null +++ b/src/secops/chronicle/integration/jobs.py @@ -0,0 +1,371 @@ +# Copyright 2025 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. +# +"""Marketplace integration jobs functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion, JobParameter +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_jobs( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all jobs defined for a specific integration. + + Use this method to browse the available background and scheduled automation + capabilities provided by a third-party connection. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list jobs for. + page_size: Maximum number of jobs to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter jobs. Allowed filters are: + id, custom, system, author, version, integration. + order_by: Field to sort the jobs by. + exclude_staging: Whether to exclude staging jobs from the response. + By default, staging jobs are included. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of jobs instead of a dict with jobs + list and nextPageToken. + + Returns: + If as_list is True: List of jobs. + If as_list is False: Dict with jobs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + "excludeStaging": exclude_staging, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=f"integrations/{format_resource_id(integration_name)}/jobs", + items_key="jobs", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_job( + client: "ChronicleClient", + integration_name: str, + job_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single job for a given integration. + + Use this method to retrieve the Python script, execution parameters, and + versioning information for a background automation task. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationJob. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}" + ), + api_version=api_version, + ) + + +def delete_integration_job( + client: "ChronicleClient", + integration_name: str, + job_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific custom job from a given integration. + + Only custom jobs can be deleted; commercial and system jobs are immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}" + ), + api_version=api_version, + ) + + +def create_integration_job( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + version: int, + enabled: bool, + custom: bool, + description: str | None = None, + parameters: list[dict[str, Any] | JobParameter] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new custom job for a given integration. + + Each job must have a unique display name and a functional Python script + for its background execution. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the job for. + display_name: Job's display name. Maximum 400 characters. Required. + script: Job's Python script. Required. + version: Job's version. Required. + enabled: Whether the job is enabled. Required. + custom: Whether the job is custom or commercial. Required. + description: Job's description. Optional. + parameters: List of JobParameter instances or dicts. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationJob resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, JobParameter) else p for p in parameters] + if parameters is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "version": version, + "enabled": enabled, + "custom": custom, + "description": description, + "parameters": resolved_parameters, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/jobs" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_job( + client: "ChronicleClient", + integration_name: str, + job_id: str, + display_name: str | None = None, + script: str | None = None, + version: int | None = None, + enabled: bool | None = None, + custom: bool | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | JobParameter] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing custom job for a given integration. + + Use this method to modify the Python script or adjust the parameter + definitions for a job. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to update. + display_name: Job's display name. Maximum 400 characters. + script: Job's Python script. + version: Job's version. + enabled: Whether the job is enabled. + custom: Whether the job is custom or commercial. + description: Job's description. + parameters: List of JobParameter instances or dicts. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationJob resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, JobParameter) else p for p in parameters] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("version", "version", version), + ("enabled", "enabled", enabled), + ("custom", "custom", custom), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_job_test( + client: "ChronicleClient", + integration_name: str, + job: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Execute a test run of an integration job's Python script. + + Use this method to verify background automation logic and connectivity + before deploying the job to an instance for recurring execution. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job: Dict containing the IntegrationJob to test. + agent_identifier: Agent identifier for remote testing. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the test execution results with the following fields: + - output: The script output. + - debugOutput: The script debug output. + - resultObjectJson: The result JSON if it exists (optional). + - resultName: The script result name (optional). + - resultValue: The script result value (optional). + + Raises: + APIError: If the API request fails. + """ + body: dict[str, Any] = {"job": job} + + if agent_identifier is not None: + body["agentIdentifier"] = agent_identifier + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs:executeTest" + ), + api_version=api_version, + json=body, + ) + + +def get_integration_job_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a new integration job. + + Use this method to rapidly initialize the development of a new job. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationJob template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index ee9c3a00..ba71a13e 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -228,8 +228,8 @@ def to_dict(self) -> dict: return data -class ConnectorParamType(str, Enum): - """Parameter types for Chronicle SOAR integration connectors.""" +class ParamType(str, Enum): + """Parameter types for Chronicle SOAR integration functions.""" UNSPECIFIED = "PARAM_TYPE_UNSPECIFIED" BOOLEAN = "BOOLEAN" @@ -248,6 +248,7 @@ class ConnectorParamType(str, Enum): MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" SCRIPT = "SCRIPT" FILTER_LIST = "FILTER_LIST" + NUMERICAL_VALUES = "NUMERICAL_VALUES" class ConnectorParamMode(str, Enum): @@ -284,7 +285,7 @@ class ConnectorParameter: """ display_name: str - type: ConnectorParamType + type: ParamType mode: ConnectorParamMode mandatory: bool default_value: str | None = None @@ -308,6 +309,40 @@ def to_dict(self) -> dict: return data +@dataclass +class JobParameter: + """A parameter definition for a Chronicle SOAR integration job. + + Attributes: + id: The parameter's id. + display_name: The parameter's display name. + description: The parameter's description. + mandatory: Whether the parameter is mandatory. + type: The parameter's type. + default_value: The default value of the parameter. + """ + + id: int + display_name: str + description: str + mandatory: bool + type: ParamType + default_value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "id": self.id, + "displayName": self.display_name, + "description": self.description, + "mandatory": self.mandatory, + "type": str(self.type.value), + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + return data + + @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/tests/chronicle/integration/test_connectors.py b/tests/chronicle/integration/test_connectors.py index 0667bc35..3aca859a 100644 --- a/tests/chronicle/integration/test_connectors.py +++ b/tests/chronicle/integration/test_connectors.py @@ -22,7 +22,7 @@ from secops.chronicle.models import ( APIVersion, ConnectorParameter, - ConnectorParamType, + ParamType, ConnectorParamMode, ConnectorRule, ConnectorRuleType, @@ -324,7 +324,7 @@ def test_create_integration_connector_with_dataclass_parameters(chronicle_client param = ConnectorParameter( display_name="API Key", - type=ConnectorParamType.STRING, + type=ParamType.STRING, mode=ConnectorParamMode.REGULAR, mandatory=True, description="API key for authentication", @@ -477,7 +477,7 @@ def test_update_integration_connector_with_parameters(chronicle_client): param = ConnectorParameter( display_name="Auth Token", - type=ConnectorParamType.STRING, + type=ParamType.STRING, mode=ConnectorParamMode.REGULAR, mandatory=True, ) diff --git a/tests/chronicle/integration/test_jobs.py b/tests/chronicle/integration/test_jobs.py new file mode 100644 index 00000000..a318a890 --- /dev/null +++ b/tests/chronicle/integration/test_jobs.py @@ -0,0 +1,594 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration jobs functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion, JobParameter, ParamType +from secops.chronicle.integration.jobs import ( + list_integration_jobs, + get_integration_job, + delete_integration_job, + create_integration_job, + update_integration_job, + execute_integration_job_test, + get_integration_job_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_jobs tests -- + + +def test_list_integration_jobs_success(chronicle_client): + """Test list_integration_jobs delegates to chronicle_paginated_request.""" + expected = {"jobs": [{"name": "j1"}, {"name": "j2"}], "nextPageToken": "t"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.jobs.format_resource_id", + return_value="My Integration", + ): + result = list_integration_jobs( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/My Integration/jobs", + items_key="jobs", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_jobs_default_args(chronicle_client): + """Test list_integration_jobs with default args.""" + expected = {"jobs": []} + + with patch( + "secops.chronicle.integration.jobs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_jobs( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + +def test_list_integration_jobs_with_filters(chronicle_client): + """Test list_integration_jobs with filter and order_by.""" + expected = {"jobs": [{"name": "j1"}]} + + with patch( + "secops.chronicle.integration.jobs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_jobs( + chronicle_client, + integration_name="test-integration", + filter_string="custom=true", + order_by="displayName", + exclude_staging=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "custom=true", + "orderBy": "displayName", + "excludeStaging": True, + } + + +def test_list_integration_jobs_as_list(chronicle_client): + """Test list_integration_jobs returns list when as_list=True.""" + expected = [{"name": "j1"}, {"name": "j2"}] + + with patch( + "secops.chronicle.integration.jobs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_jobs( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_jobs_error(chronicle_client): + """Test list_integration_jobs raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_paginated_request", + side_effect=APIError("Failed to list integration jobs"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_jobs( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to list integration jobs" in str(exc_info.value) + + +# -- get_integration_job tests -- + + +def test_get_integration_job_success(chronicle_client): + """Test get_integration_job issues GET request.""" + expected = { + "name": "jobs/j1", + "displayName": "My Job", + "script": "print('hello')", + } + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/jobs/j1", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_job_error(chronicle_client): + """Test get_integration_job raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to get integration job"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to get integration job" in str(exc_info.value) + + +# -- delete_integration_job tests -- + + +def test_delete_integration_job_success(chronicle_client): + """Test delete_integration_job issues DELETE request.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/jobs/j1", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_job_error(chronicle_client): + """Test delete_integration_job raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to delete integration job"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to delete integration job" in str(exc_info.value) + + +# -- create_integration_job tests -- + + +def test_create_integration_job_required_fields_only(chronicle_client): + """Test create_integration_job sends only required fields when optionals omitted.""" + expected = {"name": "jobs/new", "displayName": "My Job"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job( + chronicle_client, + integration_name="test-integration", + display_name="My Job", + script="print('hi')", + version=1, + enabled=True, + custom=True, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/jobs", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Job", + "script": "print('hi')", + "version": 1, + "enabled": True, + "custom": True, + }, + ) + + +def test_create_integration_job_with_optional_fields(chronicle_client): + """Test create_integration_job includes optional fields when provided.""" + expected = {"name": "jobs/new"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job( + chronicle_client, + integration_name="test-integration", + display_name="My Job", + script="print('hi')", + version=1, + enabled=True, + custom=True, + description="Test job", + parameters=[{"id": 1, "displayName": "p1", "type": "STRING"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test job" + assert kwargs["json"]["parameters"] == [ + {"id": 1, "displayName": "p1", "type": "STRING"} + ] + + +def test_create_integration_job_with_dataclass_parameters(chronicle_client): + """Test create_integration_job converts JobParameter dataclasses.""" + expected = {"name": "jobs/new"} + + param = JobParameter( + id=1, + display_name="API Key", + description="API key for authentication", + type=ParamType.STRING, + mandatory=True, + ) + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job( + chronicle_client, + integration_name="test-integration", + display_name="My Job", + script="print('hi')", + version=1, + enabled=True, + custom=True, + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["id"] == 1 + assert params_sent[0]["displayName"] == "API Key" + assert params_sent[0]["type"] == "STRING" + + +def test_create_integration_job_error(chronicle_client): + """Test create_integration_job raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to create integration job"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_job( + chronicle_client, + integration_name="test-integration", + display_name="My Job", + script="print('hi')", + version=1, + enabled=True, + custom=True, + ) + assert "Failed to create integration job" in str(exc_info.value) + + +# -- update_integration_job tests -- + + +def test_update_integration_job_with_explicit_update_mask(chronicle_client): + """Test update_integration_job passes through explicit update_mask.""" + expected = {"name": "jobs/j1", "displayName": "New Name"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="New Name", + update_mask="displayName", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration/jobs/j1", + api_version=APIVersion.V1BETA, + json={"displayName": "New Name"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_job_auto_update_mask(chronicle_client): + """Test update_integration_job auto-generates updateMask based on fields.""" + expected = {"name": "jobs/j1"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + enabled=False, + version=2, + ) + + assert result == expected + + assert mock_request.call_count == 1 + _, kwargs = mock_request.call_args + + assert kwargs["method"] == "PATCH" + assert kwargs["endpoint_path"] == "integrations/test-integration/jobs/j1" + assert kwargs["api_version"] == APIVersion.V1BETA + + assert kwargs["json"] == {"enabled": False, "version": 2} + + update_mask = kwargs["params"]["updateMask"] + assert set(update_mask.split(",")) == {"enabled", "version"} + + +def test_update_integration_job_with_parameters(chronicle_client): + """Test update_integration_job with parameters field.""" + expected = {"name": "jobs/j1"} + + param = JobParameter( + id=2, + display_name="Auth Token", + description="Authentication token", + type=ParamType.PASSWORD, + mandatory=True, + ) + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["id"] == 2 + assert params_sent[0]["displayName"] == "Auth Token" + + +def test_update_integration_job_error(chronicle_client): + """Test update_integration_job raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to update integration job"), + ): + with pytest.raises(APIError) as exc_info: + update_integration_job( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="New Name", + ) + assert "Failed to update integration job" in str(exc_info.value) + + +# -- execute_integration_job_test tests -- + + +def test_execute_integration_job_test_success(chronicle_client): + """Test execute_integration_job_test sends POST request with job.""" + expected = { + "output": "Success", + "debugOutput": "Debug info", + "resultObjectJson": {"status": "ok"}, + } + + job = { + "displayName": "Test Job", + "script": "print('test')", + "enabled": True, + } + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_job_test( + chronicle_client, + integration_name="test-integration", + job=job, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/jobs:executeTest", + api_version=APIVersion.V1BETA, + json={"job": job}, + ) + + +def test_execute_integration_job_test_with_agent_identifier(chronicle_client): + """Test execute_integration_job_test includes agent_identifier when provided.""" + expected = {"output": "Success"} + + job = {"displayName": "Test", "script": "print('test')"} + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_job_test( + chronicle_client, + integration_name="test-integration", + job=job, + agent_identifier="agent-123", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["agentIdentifier"] == "agent-123" + + +def test_execute_integration_job_test_error(chronicle_client): + """Test execute_integration_job_test raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to execute job test"), + ): + with pytest.raises(APIError) as exc_info: + execute_integration_job_test( + chronicle_client, + integration_name="test-integration", + job={"displayName": "Test"}, + ) + assert "Failed to execute job test" in str(exc_info.value) + + +# -- get_integration_job_template tests -- + + +def test_get_integration_job_template_success(chronicle_client): + """Test get_integration_job_template issues GET request.""" + expected = { + "script": "# Template script\nprint('hello')", + "displayName": "Template Job", + } + + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_job_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/jobs:fetchTemplate", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_job_template_error(chronicle_client): + """Test get_integration_job_template raises APIError on failure.""" + with patch( + "secops.chronicle.integration.jobs.chronicle_request", + side_effect=APIError("Failed to get job template"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_job_template( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to get job template" in str(exc_info.value) + From 16bcff095c46548d397cdc46b9985948bc8bc43b Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 7 Mar 2026 07:54:55 +0000 Subject: [PATCH 26/46] feat: add functions for integration managers --- README.md | 98 ++++ api_module_mapping.md | 16 +- src/secops/chronicle/client.py | 238 ++++++++++ src/secops/chronicle/integration/managers.py | 285 ++++++++++++ tests/chronicle/integration/test_managers.py | 460 +++++++++++++++++++ 5 files changed, 1095 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/managers.py create mode 100644 tests/chronicle/integration/test_managers.py diff --git a/README.md b/README.md index 061fb79d..f9606434 100644 --- a/README.md +++ b/README.md @@ -2348,6 +2348,104 @@ template = chronicle.get_integration_job_template("MyIntegration") print(f"Template script: {template.get('script')}") ``` +### Integration Managers + +List all available managers for an integration: + +```python +# Get all managers for an integration +managers = chronicle.list_integration_managers("MyIntegration") +for manager in managers.get("managers", []): + print(f"Manager: {manager.get('displayName')}, ID: {manager.get('name')}") + +# Get all managers as a list +managers = chronicle.list_integration_managers("MyIntegration", as_list=True) + +# Filter managers by display name +managers = chronicle.list_integration_managers( + "MyIntegration", + filter_string='displayName = "API Helper"' +) + +# Sort managers by display name +managers = chronicle.list_integration_managers( + "MyIntegration", + order_by="displayName" +) +``` + +Get details of a specific manager: + +```python +manager = chronicle.get_integration_manager( + integration_name="MyIntegration", + manager_id="123" +) +``` + +Create an integration manager: + +```python +new_manager = chronicle.create_integration_manager( + integration_name="MyIntegration", + display_name="API Helper", + description="Shared utility functions for API calls", + script=""" +def make_api_request(url, headers=None): + '''Helper function to make API requests''' + import requests + return requests.get(url, headers=headers) + +def parse_response(response): + '''Parse API response''' + return response.json() +""" +) +``` + +Update an integration manager: + +```python +updated_manager = chronicle.update_integration_manager( + integration_name="MyIntegration", + manager_id="123", + display_name="Updated API Helper", + description="Updated shared utility functions", + script=""" +def make_api_request(url, headers=None, method='GET'): + '''Updated helper function with method parameter''' + import requests + if method == 'GET': + return requests.get(url, headers=headers) + elif method == 'POST': + return requests.post(url, headers=headers) +""" +) + +# Update only specific fields +updated_manager = chronicle.update_integration_manager( + integration_name="MyIntegration", + manager_id="123", + description="New description only" +) +``` + +Delete an integration manager: + +```python +chronicle.delete_integration_manager( + integration_name="MyIntegration", + manager_id="123" +) +``` + +Get a template for creating a manager in an integration: + +```python +template = chronicle.get_integration_manager_template("MyIntegration") +print(f"Template script: {template.get('script')}") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 4e2d4e9d..264e8428 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 27 endpoints implemented -- **v1alpha:** 120 endpoints implemented +- **v1beta:** 33 endpoints implemented +- **v1alpha:** 126 endpoints implemented ## Endpoint Mapping @@ -105,6 +105,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.get | v1beta | chronicle.integration.jobs.get_integration_job | | | integrations.jobs.list | v1beta | chronicle.integration.jobs.list_integration_jobs | | | integrations.jobs.patch | v1beta | chronicle.integration.jobs.update_integration_job | | +| integrations.managers.create | v1beta | chronicle.integration.managers.create_integration_manager | | +| integrations.managers.delete | v1beta | chronicle.integration.managers.delete_integration_manager | | +| integrations.managers.fetchTemplate | v1beta | chronicle.integration.managers.get_integration_manager_template | | +| integrations.managers.get | v1beta | chronicle.integration.managers.get_integration_manager | | +| integrations.managers.list | v1beta | chronicle.integration.managers.list_integration_managers | | +| integrations.managers.patch | v1beta | chronicle.integration.managers.update_integration_manager | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -322,6 +328,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.get | v1alpha | chronicle.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.list | v1alpha | chronicle.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.patch | v1alpha | chronicle.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.create | v1alpha | chronicle.integration.managers.create_integration_manager(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.delete | v1alpha | chronicle.integration.managers.delete_integration_manager(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.fetchTemplate | v1alpha | chronicle.integration.managers.get_integration_manager_template(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.get | v1alpha | chronicle.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.list | v1alpha | chronicle.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.patch | v1alpha | chronicle.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index f5414737..2fe21fb8 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -182,6 +182,14 @@ list_integration_jobs as _list_integration_jobs, update_integration_job as _update_integration_job, ) +from secops.chronicle.integration.managers import ( + create_integration_manager as _create_integration_manager, + delete_integration_manager as _delete_integration_manager, + get_integration_manager as _get_integration_manager, + get_integration_manager_template as _get_integration_manager_template, + list_integration_managers as _list_integration_managers, + update_integration_manager as _update_integration_manager, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -2375,6 +2383,236 @@ def get_integration_job_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Manager methods + # ------------------------------------------------------------------------- + + def list_integration_managers( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all managers defined for a specific integration. + + Use this method to discover the library of managers available + within a particular integration's scope. + + Args: + integration_name: Name of the integration to list managers + for. + page_size: Maximum number of managers to return. Defaults to + 100, maximum is 100. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter managers. + order_by: Field to sort the managers by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of managers instead of a + dict with managers list and nextPageToken. + + Returns: + If as_list is True: List of managers. + If as_list is False: Dict with managers list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_managers( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_manager( + self, + integration_name: str, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single manager for a given integration. + + Use this method to retrieve the manager script and its metadata + for review or reference. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified IntegrationManager. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_manager( + self, + integration_name, + manager_id, + api_version=api_version, + ) + + def delete_integration_manager( + self, + integration_name: str, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific custom manager from a given integration. + + Note that deleting a manager may break components (actions, + jobs) that depend on its code. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_manager( + self, + integration_name, + manager_id, + api_version=api_version, + ) + + def create_integration_manager( + self, + integration_name: str, + display_name: str, + script: str, + description: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new custom manager for a given integration. + + Use this method to add a new shared code utility. Each manager + must have a unique display name and a script containing valid + Python logic for reuse across actions, jobs, and connectors. + + Args: + integration_name: Name of the integration to create the + manager for. + display_name: Manager's display name. Maximum 150 + characters. Required. + script: Manager's Python script. Maximum 5MB. Required. + description: Manager's description. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationManager + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_manager( + self, + integration_name, + display_name, + script, + description=description, + api_version=api_version, + ) + + def update_integration_manager( + self, + integration_name: str, + manager_id: str, + display_name: str | None = None, + script: str | None = None, + description: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing custom manager for a given integration. + + Use this method to modify the shared code, adjust its + description, or refine its logic across all components that + import it. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to update. + display_name: Manager's display name. Maximum 150 + characters. + script: Manager's Python script. Maximum 5MB. + description: Manager's description. Maximum 400 characters. + update_mask: Comma-separated list of fields to update. If + omitted, the mask is auto-generated from whichever + fields are provided. Example: "displayName,script". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated IntegrationManager resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_manager( + self, + integration_name, + manager_id, + display_name=display_name, + script=script, + description=description, + update_mask=update_mask, + api_version=api_version, + ) + + def get_integration_manager_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new + integration manager. + + Use this method to quickly start developing new managers. + + Args: + integration_name: Name of the integration to fetch the + template for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationManager template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_manager_template( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/managers.py b/src/secops/chronicle/integration/managers.py new file mode 100644 index 00000000..dcdcce46 --- /dev/null +++ b/src/secops/chronicle/integration/managers.py @@ -0,0 +1,285 @@ +# Copyright 2025 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. +# +"""Marketplace integration manager functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_managers( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all managers defined for a specific integration. + + Use this method to discover the library of managers available within a + particular integration's scope. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list managers for. + page_size: Maximum number of managers to return. Defaults to 100, + maximum is 100. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter managers. + order_by: Field to sort the managers by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of managers instead of a dict with + managers list and nextPageToken. + + Returns: + If as_list is True: List of managers. + If as_list is False: Dict with managers list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=f"integrations/{format_resource_id(integration_name)}/managers", + items_key="managers", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_manager( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single manager for a given integration. + + Use this method to retrieve the manager script and its metadata for + review or reference. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationManager. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}" + ), + api_version=api_version, + ) + + +def delete_integration_manager( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific custom manager from a given integration. + + Note that deleting a manager may break components (actions, jobs) that + depend on its code. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}" + ), + api_version=api_version, + ) + + +def create_integration_manager( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + description: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new custom manager for a given integration. + + Use this method to add a new shared code utility. Each manager must have + a unique display name and a script containing valid Python logic for reuse + across actions, jobs, and connectors. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the manager for. + display_name: Manager's display name. Maximum 150 characters. Required. + script: Manager's Python script. Maximum 5MB. Required. + description: Manager's description. Maximum 400 characters. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationManager resource. + + Raises: + APIError: If the API request fails. + """ + body = { + "displayName": display_name, + "script": script, + } + + if description is not None: + body["description"] = description + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/managers" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_manager( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + display_name: str | None = None, + script: str | None = None, + description: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing custom manager for a given integration. + + Use this method to modify the shared code, adjust its description, or + refine its logic across all components that import it. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to update. + display_name: Manager's display name. Maximum 150 characters. + script: Manager's Python script. Maximum 5MB. + description: Manager's description. Maximum 400 characters. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationManager resource. + + Raises: + APIError: If the API request fails. + """ + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("description", "description", description), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def get_integration_manager_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a new integration manager. + + Use this method to quickly start developing new managers. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationManager template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + "managers:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_managers.py b/tests/chronicle/integration/test_managers.py new file mode 100644 index 00000000..c6bf5c4a --- /dev/null +++ b/tests/chronicle/integration/test_managers.py @@ -0,0 +1,460 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration managers functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.managers import ( + list_integration_managers, + get_integration_manager, + delete_integration_manager, + create_integration_manager, + update_integration_manager, + get_integration_manager_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_managers tests -- + + +def test_list_integration_managers_success(chronicle_client): + """Test list_integration_managers delegates to chronicle_paginated_request.""" + expected = {"managers": [{"name": "m1"}, {"name": "m2"}], "nextPageToken": "t"} + + with patch( + "secops.chronicle.integration.managers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.managers.format_resource_id", + return_value="My Integration", + ): + result = list_integration_managers( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/My Integration/managers", + items_key="managers", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_managers_default_args(chronicle_client): + """Test list_integration_managers with default args.""" + expected = {"managers": []} + + with patch( + "secops.chronicle.integration.managers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_managers( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + +def test_list_integration_managers_with_filters(chronicle_client): + """Test list_integration_managers with filter and order_by.""" + expected = {"managers": [{"name": "m1"}]} + + with patch( + "secops.chronicle.integration.managers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_managers( + chronicle_client, + integration_name="test-integration", + filter_string='displayName = "My Manager"', + order_by="displayName", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'displayName = "My Manager"', + "orderBy": "displayName", + } + + +def test_list_integration_managers_as_list(chronicle_client): + """Test list_integration_managers returns list when as_list=True.""" + expected = [{"name": "m1"}, {"name": "m2"}] + + with patch( + "secops.chronicle.integration.managers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_managers( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_managers_error(chronicle_client): + """Test list_integration_managers raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_paginated_request", + side_effect=APIError("Failed to list integration managers"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_managers( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to list integration managers" in str(exc_info.value) + + +# -- get_integration_manager tests -- + + +def test_get_integration_manager_success(chronicle_client): + """Test get_integration_manager issues GET request.""" + expected = { + "name": "managers/m1", + "displayName": "My Manager", + "script": "def helper(): pass", + } + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/managers/m1", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_manager_error(chronicle_client): + """Test get_integration_manager raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + side_effect=APIError("Failed to get integration manager"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + assert "Failed to get integration manager" in str(exc_info.value) + + +# -- delete_integration_manager tests -- + + +def test_delete_integration_manager_success(chronicle_client): + """Test delete_integration_manager issues DELETE request.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/managers/m1", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_manager_error(chronicle_client): + """Test delete_integration_manager raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + side_effect=APIError("Failed to delete integration manager"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + assert "Failed to delete integration manager" in str(exc_info.value) + + +# -- create_integration_manager tests -- + + +def test_create_integration_manager_required_fields_only(chronicle_client): + """Test create_integration_manager sends only required fields when optionals omitted.""" + expected = {"name": "managers/new", "displayName": "My Manager"} + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_manager( + chronicle_client, + integration_name="test-integration", + display_name="My Manager", + script="def helper(): pass", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/managers", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Manager", + "script": "def helper(): pass", + }, + ) + + +def test_create_integration_manager_with_description(chronicle_client): + """Test create_integration_manager includes description when provided.""" + expected = {"name": "managers/new", "displayName": "My Manager"} + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_manager( + chronicle_client, + integration_name="test-integration", + display_name="My Manager", + script="def helper(): pass", + description="A helpful manager", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "A helpful manager" + + +def test_create_integration_manager_error(chronicle_client): + """Test create_integration_manager raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + side_effect=APIError("Failed to create integration manager"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_manager( + chronicle_client, + integration_name="test-integration", + display_name="My Manager", + script="def helper(): pass", + ) + assert "Failed to create integration manager" in str(exc_info.value) + + +# -- update_integration_manager tests -- + + +def test_update_integration_manager_single_field(chronicle_client): + """Test update_integration_manager updates a single field.""" + expected = {"name": "managers/m1", "displayName": "Updated Manager"} + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.managers.build_patch_body", + return_value=({"displayName": "Updated Manager"}, {"updateMask": "displayName"}), + ) as mock_build_patch: + result = update_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + display_name="Updated Manager", + ) + + assert result == expected + + mock_build_patch.assert_called_once() + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration/managers/m1", + api_version=APIVersion.V1BETA, + json={"displayName": "Updated Manager"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_manager_multiple_fields(chronicle_client): + """Test update_integration_manager updates multiple fields.""" + expected = {"name": "managers/m1", "displayName": "Updated Manager"} + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.managers.build_patch_body", + return_value=( + { + "displayName": "Updated Manager", + "script": "def new_helper(): pass", + "description": "New description", + }, + {"updateMask": "displayName,script,description"}, + ), + ): + result = update_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + display_name="Updated Manager", + script="def new_helper(): pass", + description="New description", + ) + + assert result == expected + + +def test_update_integration_manager_with_update_mask(chronicle_client): + """Test update_integration_manager respects explicit update_mask.""" + expected = {"name": "managers/m1", "displayName": "Updated Manager"} + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.managers.build_patch_body", + return_value=( + {"displayName": "Updated Manager"}, + {"updateMask": "displayName"}, + ), + ): + result = update_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + display_name="Updated Manager", + update_mask="displayName", + ) + + assert result == expected + + +def test_update_integration_manager_error(chronicle_client): + """Test update_integration_manager raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + side_effect=APIError("Failed to update integration manager"), + ), patch( + "secops.chronicle.integration.managers.build_patch_body", + return_value=({"displayName": "Updated"}, {"updateMask": "displayName"}), + ): + with pytest.raises(APIError) as exc_info: + update_integration_manager( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + display_name="Updated", + ) + assert "Failed to update integration manager" in str(exc_info.value) + + +# -- get_integration_manager_template tests -- + + +def test_get_integration_manager_template_success(chronicle_client): + """Test get_integration_manager_template issues GET request.""" + expected = { + "displayName": "Template Manager", + "script": "# Template script\ndef template(): pass", + } + + with patch( + "secops.chronicle.integration.managers.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_manager_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/managers:fetchTemplate", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_manager_template_error(chronicle_client): + """Test get_integration_manager_template raises APIError on failure.""" + with patch( + "secops.chronicle.integration.managers.chronicle_request", + side_effect=APIError("Failed to get integration manager template"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_manager_template( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to get integration manager template" in str(exc_info.value) + From 40d19a8bf50b4f7716957a467d826e526204e85f Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 7 Mar 2026 19:34:59 +0000 Subject: [PATCH 27/46] feat: add functions for integration manager revisions --- README.md | 81 ++++ api_module_mapping.md | 14 +- src/secops/chronicle/__init__.py | 106 +++++ src/secops/chronicle/client.py | 207 +++++++++ .../integration/manager_revisions.py | 243 ++++++++++ .../integration/test_manager_revisions.py | 417 ++++++++++++++++++ 6 files changed, 1066 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/manager_revisions.py create mode 100644 tests/chronicle/integration/test_manager_revisions.py diff --git a/README.md b/README.md index f9606434..e2a48745 100644 --- a/README.md +++ b/README.md @@ -2446,6 +2446,87 @@ template = chronicle.get_integration_manager_template("MyIntegration") print(f"Template script: {template.get('script')}") ``` +### Integration Manager Revisions + +List all revisions for a specific manager: + +```python +# Get all revisions for a manager +revisions = chronicle.list_integration_manager_revisions( + integration_name="MyIntegration", + manager_id="123" +) +for revision in revisions.get("revisions", []): + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_manager_revisions( + integration_name="MyIntegration", + manager_id="123", + as_list=True +) + +# Filter revisions +revisions = chronicle.list_integration_manager_revisions( + integration_name="MyIntegration", + manager_id="123", + filter_string='comment contains "backup"', + order_by="createTime desc" +) +``` + +Get details of a specific revision: + +```python +revision = chronicle.get_integration_manager_revision( + integration_name="MyIntegration", + manager_id="123", + revision_id="r1" +) +print(f"Revision script: {revision.get('manager', {}).get('script')}") +``` + +Create a new revision snapshot: + +```python +# Get the current manager +manager = chronicle.get_integration_manager( + integration_name="MyIntegration", + manager_id="123" +) + +# Create a revision before making changes +revision = chronicle.create_integration_manager_revision( + integration_name="MyIntegration", + manager_id="123", + manager=manager, + comment="Backup before major refactor" +) +print(f"Created revision: {revision.get('name')}") +``` + +Rollback to a previous revision: + +```python +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_manager_revision( + integration_name="MyIntegration", + manager_id="123", + revision_id="acb123de-abcd-1234-ef00-1234567890ab" +) +print(f"Rolled back to: {rollback_result.get('name')}") +``` + +Delete a revision: + +```python +chronicle.delete_integration_manager_revision( + integration_name="MyIntegration", + manager_id="123", + revision_id="r1" +) +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 264e8428..78e63b8b 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 33 endpoints implemented -- **v1alpha:** 126 endpoints implemented +- **v1beta:** 38 endpoints implemented +- **v1alpha:** 131 endpoints implemented ## Endpoint Mapping @@ -111,6 +111,11 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.managers.get | v1beta | chronicle.integration.managers.get_integration_manager | | | integrations.managers.list | v1beta | chronicle.integration.managers.list_integration_managers | | | integrations.managers.patch | v1beta | chronicle.integration.managers.update_integration_manager | | +| integrations.managers.revisions.create | v1beta | chronicle.integration.manager_revisions.create_integration_manager_revision | | +| integrations.managers.revisions.delete | v1beta | chronicle.integration.manager_revisions.delete_integration_manager_revision | | +| integrations.managers.revisions.get | v1beta | chronicle.integration.manager_revisions.get_integration_manager_revision | | +| integrations.managers.revisions.list | v1beta | chronicle.integration.manager_revisions.list_integration_manager_revisions | | +| integrations.managers.revisions.rollback | v1beta | chronicle.integration.manager_revisions.rollback_integration_manager_revision | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -334,6 +339,11 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.managers.get | v1alpha | chronicle.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | | | integrations.managers.list | v1alpha | chronicle.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | | | integrations.managers.patch | v1alpha | chronicle.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.revisions.create | v1alpha | chronicle.integration.manager_revisions.create_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.revisions.delete | v1alpha | chronicle.integration.manager_revisions.delete_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.revisions.get | v1alpha | chronicle.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.revisions.list | v1alpha | chronicle.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | | +| integrations.managers.revisions.rollback | v1alpha | chronicle.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index cb1c8065..9c78666d 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -203,6 +203,62 @@ create_watchlist, update_watchlist, ) +from secops.chronicle.integration.integrations import ( + list_integrations, + get_integration, + delete_integration, + create_integration, + transition_integration, + update_integration, + update_custom_integration, + get_integration_affected_items, + get_integration_dependencies, + get_integration_diff, + get_integration_restricted_agents, +) +from secops.chronicle.integration.actions import ( + list_integration_actions, + get_integration_action, + delete_integration_action, + create_integration_action, + update_integration_action, + execute_integration_action_test, + get_integration_actions_by_environment, + get_integration_action_template, +) +from secops.chronicle.integration.connectors import ( + list_integration_connectors, + get_integration_connector, + delete_integration_connector, + create_integration_connector, + update_integration_connector, + execute_integration_connector_test, + get_integration_connector_template, +) +from secops.chronicle.integration.jobs import ( + list_integration_jobs, + get_integration_job, + delete_integration_job, + create_integration_job, + update_integration_job, + execute_integration_job_test, + get_integration_job_template, +) +from secops.chronicle.integration.managers import ( + list_integration_managers, + get_integration_manager, + delete_integration_manager, + create_integration_manager, + update_integration_manager, + get_integration_manager_template, +) +from secops.chronicle.integration.manager_revisions import ( + list_integration_manager_revisions, + get_integration_manager_revision, + delete_integration_manager_revision, + create_integration_manager_revision, + rollback_integration_manager_revision, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -378,6 +434,56 @@ "delete_watchlist", "create_watchlist", "update_watchlist", + # Integrations + "list_integrations", + "get_integration", + "delete_integration", + "create_integration", + "transition_integration", + "update_integration", + "update_custom_integration", + "get_integration_affected_items", + "get_integration_dependencies", + "get_integration_diff", + "get_integration_restricted_agents", + # Integration Actions + "list_integration_actions", + "get_integration_action", + "delete_integration_action", + "create_integration_action", + "update_integration_action", + "execute_integration_action_test", + "get_integration_actions_by_environment", + "get_integration_action_template", + # Integration Connectors + "list_integration_connectors", + "get_integration_connector", + "delete_integration_connector", + "create_integration_connector", + "update_integration_connector", + "execute_integration_connector_test", + "get_integration_connector_template", + # Integration Jobs + "list_integration_jobs", + "get_integration_job", + "delete_integration_job", + "create_integration_job", + "update_integration_job", + "execute_integration_job_test", + "get_integration_job_template", + # Integration Managers + "list_integration_managers", + "get_integration_manager", + "delete_integration_manager", + "create_integration_manager", + "update_integration_manager", + "get_integration_manager_template", + # Integration Manager Revisions + "list_integration_manager_revisions", + "get_integration_manager_revision", + "delete_integration_manager_revision", + "create_integration_manager_revision", + "rollback_integration_manager_revision", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 2fe21fb8..0712e765 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -190,6 +190,13 @@ list_integration_managers as _list_integration_managers, update_integration_manager as _update_integration_manager, ) +from secops.chronicle.integration.manager_revisions import ( + create_integration_manager_revision as _create_integration_manager_revision, + delete_integration_manager_revision as _delete_integration_manager_revision, + get_integration_manager_revision as _get_integration_manager_revision, + list_integration_manager_revisions as _list_integration_manager_revisions, + rollback_integration_manager_revision as _rollback_integration_manager_revision, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -2613,6 +2620,206 @@ def get_integration_manager_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Manager Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_manager_revisions( + self, + integration_name: str, + manager_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration manager. + + Use this method to browse the version history and identify + previous functional states of a manager. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_manager_revisions( + self, + integration_name, + manager_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_manager_revision( + self, + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single revision for a specific integration manager. + + Use this method to retrieve a specific snapshot of an + IntegrationManagerRevision for comparison or review. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager the revision belongs to. + revision_id: ID of the revision to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified + IntegrationManagerRevision. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_manager_revision( + self, + integration_name, + manager_id, + revision_id, + api_version=api_version, + ) + + def delete_integration_manager_revision( + self, + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific revision for a given integration manager. + + Use this method to clean up obsolete snapshots and manage the + historical record of managers. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_manager_revision( + self, + integration_name, + manager_id, + revision_id, + api_version=api_version, + ) + + def create_integration_manager_revision( + self, + integration_name: str, + manager_id: str, + manager: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new revision snapshot of the current integration + manager. + + Use this method to establish a recovery point before making + significant updates to a manager. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to create a revision for. + manager: Dict containing the IntegrationManager to snapshot. + comment: Comment describing the revision. Maximum 400 + characters. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created + IntegrationManagerRevision resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_manager_revision( + self, + integration_name, + manager_id, + manager, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_manager_revision( + self, + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Revert the current manager definition to a previously saved + revision. + + Use this method to rapidly recover a functional state for + common code if an update causes operational issues in dependent + actions or jobs. + + Args: + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationManagerRevision rolled back + to. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_manager_revision( + self, + integration_name, + manager_id, + revision_id, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/manager_revisions.py b/src/secops/chronicle/integration/manager_revisions.py new file mode 100644 index 00000000..614232b6 --- /dev/null +++ b/src/secops/chronicle/integration/manager_revisions.py @@ -0,0 +1,243 @@ +# Copyright 2025 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. +# +"""Marketplace integration manager revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_manager_revisions( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration manager. + + Use this method to browse the version history and identify previous + functional states of a manager. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_manager_revision( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single revision for a specific integration manager. + + Use this method to retrieve a specific snapshot of an + IntegrationManagerRevision for comparison or review. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager the revision belongs to. + revision_id: ID of the revision to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationManagerRevision. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}/revisions/" + f"{format_resource_id(revision_id)}" + ), + api_version=api_version, + ) + + +def delete_integration_manager_revision( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific revision for a given integration manager. + + Use this method to clean up obsolete snapshots and manage the historical + record of managers. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}/revisions/" + f"{format_resource_id(revision_id)}" + ), + api_version=api_version, + ) + + +def create_integration_manager_revision( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + manager: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration manager. + + Use this method to establish a recovery point before making significant + updates to a manager. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to create a revision for. + manager: Dict containing the IntegrationManager to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationManagerRevision resource. + + Raises: + APIError: If the API request fails. + """ + body = {"manager": manager} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_manager_revision( + client: "ChronicleClient", + integration_name: str, + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Revert the current manager definition to a previously saved revision. + + Use this method to rapidly recover a functional state for common code if + an update causes operational issues in dependent actions or jobs. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the manager belongs to. + manager_id: ID of the manager to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationManagerRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"managers/{manager_id}/revisions/" + f"{format_resource_id(revision_id)}:rollback" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_manager_revisions.py b/tests/chronicle/integration/test_manager_revisions.py new file mode 100644 index 00000000..7076bd54 --- /dev/null +++ b/tests/chronicle/integration/test_manager_revisions.py @@ -0,0 +1,417 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration manager revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.manager_revisions import ( + list_integration_manager_revisions, + get_integration_manager_revision, + delete_integration_manager_revision, + create_integration_manager_revision, + rollback_integration_manager_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_manager_revisions tests -- + + +def test_list_integration_manager_revisions_success(chronicle_client): + """Test list_integration_manager_revisions delegates to chronicle_paginated_request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.manager_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_manager_revisions( + chronicle_client, + integration_name="My Integration", + manager_id="m1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "managers/m1/revisions" in kwargs["path"] + assert kwargs["items_key"] == "revisions" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_manager_revisions_default_args(chronicle_client): + """Test list_integration_manager_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_manager_revisions( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + + assert result == expected + + +def test_list_integration_manager_revisions_with_filters(chronicle_client): + """Test list_integration_manager_revisions with filter and order_by.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_manager_revisions( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + filter_string='version = "1.0"', + order_by="createTime", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'version = "1.0"', + "orderBy": "createTime", + } + + +def test_list_integration_manager_revisions_as_list(chronicle_client): + """Test list_integration_manager_revisions returns list when as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_manager_revisions( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_manager_revisions_error(chronicle_client): + """Test list_integration_manager_revisions raises APIError on failure.""" + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + side_effect=APIError("Failed to list manager revisions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_manager_revisions( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + ) + assert "Failed to list manager revisions" in str(exc_info.value) + + +# -- get_integration_manager_revision tests -- + + +def test_get_integration_manager_revision_success(chronicle_client): + """Test get_integration_manager_revision issues GET request.""" + expected = { + "name": "revisions/r1", + "manager": { + "displayName": "My Manager", + "script": "def helper(): pass", + }, + "comment": "Initial version", + } + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "managers/m1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_integration_manager_revision_error(chronicle_client): + """Test get_integration_manager_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + side_effect=APIError("Failed to get manager revision"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + assert "Failed to get manager revision" in str(exc_info.value) + + +# -- delete_integration_manager_revision tests -- + + +def test_delete_integration_manager_revision_success(chronicle_client): + """Test delete_integration_manager_revision issues DELETE request.""" + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "managers/m1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_manager_revision_error(chronicle_client): + """Test delete_integration_manager_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + side_effect=APIError("Failed to delete manager revision"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + assert "Failed to delete manager revision" in str(exc_info.value) + + +# -- create_integration_manager_revision tests -- + + +def test_create_integration_manager_revision_required_fields_only( + chronicle_client, +): + """Test create_integration_manager_revision with required fields only.""" + expected = {"name": "revisions/new", "manager": {"displayName": "My Manager"}} + manager_dict = { + "displayName": "My Manager", + "script": "def helper(): pass", + } + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + manager=manager_dict, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/managers/m1/revisions" + ), + api_version=APIVersion.V1BETA, + json={"manager": manager_dict}, + ) + + +def test_create_integration_manager_revision_with_comment(chronicle_client): + """Test create_integration_manager_revision includes comment when provided.""" + expected = {"name": "revisions/new"} + manager_dict = {"displayName": "My Manager", "script": "def helper(): pass"} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + manager=manager_dict, + comment="Backup before major refactor", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["comment"] == "Backup before major refactor" + assert kwargs["json"]["manager"] == manager_dict + + +def test_create_integration_manager_revision_error(chronicle_client): + """Test create_integration_manager_revision raises APIError on failure.""" + manager_dict = {"displayName": "My Manager", "script": "def helper(): pass"} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + side_effect=APIError("Failed to create manager revision"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + manager=manager_dict, + ) + assert "Failed to create manager revision" in str(exc_info.value) + + +# -- rollback_integration_manager_revision tests -- + + +def test_rollback_integration_manager_revision_success(chronicle_client): + """Test rollback_integration_manager_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "manager": { + "displayName": "My Manager", + "script": "def helper(): pass", + }, + } + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "managers/m1/revisions/r1:rollback" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_rollback_integration_manager_revision_error(chronicle_client): + """Test rollback_integration_manager_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + side_effect=APIError("Failed to rollback manager revision"), + ): + with pytest.raises(APIError) as exc_info: + rollback_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + ) + assert "Failed to rollback manager revision" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_manager_revisions_custom_api_version(chronicle_client): + """Test list_integration_manager_revisions with custom API version.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_manager_revisions( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_integration_manager_revision_custom_api_version(chronicle_client): + """Test get_integration_manager_revision with custom API version.""" + expected = {"name": "revisions/r1"} + + with patch( + "secops.chronicle.integration.manager_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_manager_revision( + chronicle_client, + integration_name="test-integration", + manager_id="m1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From 4285d2bc7d665cf0c1a33ecc227926f723418852 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sat, 7 Mar 2026 19:52:27 +0000 Subject: [PATCH 28/46] feat: add functions for integration job revisions --- README.md | 70 ++++ api_module_mapping.md | 12 +- src/secops/chronicle/__init__.py | 11 + src/secops/chronicle/client.py | 170 ++++++++ .../chronicle/integration/job_revisions.py | 204 ++++++++++ .../integration/test_job_revisions.py | 378 ++++++++++++++++++ 6 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/job_revisions.py create mode 100644 tests/chronicle/integration/test_job_revisions.py diff --git a/README.md b/README.md index e2a48745..8d48b8ff 100644 --- a/README.md +++ b/README.md @@ -2527,6 +2527,76 @@ chronicle.delete_integration_manager_revision( ) ``` +### Integration Job Revisions + +List all revisions for a specific job: + +```python +# Get all revisions for a job +revisions = chronicle.list_integration_job_revisions( + integration_name="MyIntegration", + job_id="456" +) +for revision in revisions.get("revisions", []): + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_job_revisions( + integration_name="MyIntegration", + job_id="456", + as_list=True +) + +# Filter revisions by version +revisions = chronicle.list_integration_job_revisions( + integration_name="MyIntegration", + job_id="456", + filter_string='version = "2"', + order_by="createTime desc" +) +``` + +Delete a job revision: + +```python +chronicle.delete_integration_job_revision( + integration_name="MyIntegration", + job_id="456", + revision_id="r2" +) +``` + +Create a new job revision snapshot: + +```python +# Get the current job +job = chronicle.get_integration_job( + integration_name="MyIntegration", + job_id="456" +) + +# Create a revision before making changes +revision = chronicle.create_integration_job_revision( + integration_name="MyIntegration", + job_id="456", + job=job, + comment="Backup before scheduled update" +) +print(f"Created revision: {revision.get('name')}") +``` + +Rollback to a previous job revision: + +```python +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_job_revision( + integration_name="MyIntegration", + job_id="456", + revision_id="r2" +) +print(f"Rolled back to: {rollback_result.get('name')}") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 78e63b8b..7d273821 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 38 endpoints implemented -- **v1alpha:** 131 endpoints implemented +- **v1beta:** 42 endpoints implemented +- **v1alpha:** 135 endpoints implemented ## Endpoint Mapping @@ -116,6 +116,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.managers.revisions.get | v1beta | chronicle.integration.manager_revisions.get_integration_manager_revision | | | integrations.managers.revisions.list | v1beta | chronicle.integration.manager_revisions.list_integration_manager_revisions | | | integrations.managers.revisions.rollback | v1beta | chronicle.integration.manager_revisions.rollback_integration_manager_revision | | +| integrations.jobs.revisions.create | v1beta | chronicle.integration.job_revisions.create_integration_job_revision | | +| integrations.jobs.revisions.delete | v1beta | chronicle.integration.job_revisions.delete_integration_job_revision | | +| integrations.jobs.revisions.list | v1beta | chronicle.integration.job_revisions.list_integration_job_revisions | | +| integrations.jobs.revisions.rollback | v1beta | chronicle.integration.job_revisions.rollback_integration_job_revision | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -344,6 +348,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.managers.revisions.get | v1alpha | chronicle.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | | integrations.managers.revisions.list | v1alpha | chronicle.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | | | integrations.managers.revisions.rollback | v1alpha | chronicle.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.revisions.create | v1alpha | chronicle.integration.job_revisions.create_integration_job_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.revisions.delete | v1alpha | chronicle.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.revisions.list | v1alpha | chronicle.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.revisions.rollback | v1alpha | chronicle.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 9c78666d..ea5826ec 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -259,6 +259,12 @@ create_integration_manager_revision, rollback_integration_manager_revision, ) +from secops.chronicle.integration.job_revisions import ( + list_integration_job_revisions, + delete_integration_job_revision, + create_integration_job_revision, + rollback_integration_job_revision, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -484,6 +490,11 @@ "delete_integration_manager_revision", "create_integration_manager_revision", "rollback_integration_manager_revision", + # Integration Job Revisions + "list_integration_job_revisions", + "delete_integration_job_revision", + "create_integration_job_revision", + "rollback_integration_job_revision", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 0712e765..70262512 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -197,6 +197,12 @@ list_integration_manager_revisions as _list_integration_manager_revisions, rollback_integration_manager_revision as _rollback_integration_manager_revision, ) +from secops.chronicle.integration.job_revisions import ( + create_integration_job_revision as _create_integration_job_revision, + delete_integration_job_revision as _delete_integration_job_revision, + list_integration_job_revisions as _list_integration_job_revisions, + rollback_integration_job_revision as _rollback_integration_job_revision, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -2820,6 +2826,170 @@ def rollback_integration_manager_revision( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Job Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_job_revisions( + self, + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration job. + + Use this method to browse the version history of a job and + identify previous functional states. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_job_revisions( + self, + integration_name, + job_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_job_revision( + self, + integration_name: str, + job_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific revision for a given integration job. + + Use this method to clean up obsolete snapshots and manage the + historical record of jobs. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_job_revision( + self, + integration_name, + job_id, + revision_id, + api_version=api_version, + ) + + def create_integration_job_revision( + self, + integration_name: str, + job_id: str, + job: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new revision snapshot of the current integration + job. + + Use this method to establish a recovery point before making + significant updates to a job. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to create a revision for. + job: Dict containing the IntegrationJob to snapshot. + comment: Comment describing the revision. Maximum 400 + characters. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationJobRevision + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_job_revision( + self, + integration_name, + job_id, + job, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_job_revision( + self, + integration_name: str, + job_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Revert the current job definition to a previously saved + revision. + + Use this method to rapidly recover a functional state if an + update causes operational issues in scheduled or background + automation. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationJobRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_job_revision( + self, + integration_name, + job_id, + revision_id, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/job_revisions.py b/src/secops/chronicle/integration/job_revisions.py new file mode 100644 index 00000000..6c62b989 --- /dev/null +++ b/src/secops/chronicle/integration/job_revisions.py @@ -0,0 +1,204 @@ +# Copyright 2025 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. +# +"""Marketplace integration job revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_job_revisions( + client: "ChronicleClient", + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration job. + + Use this method to browse the version history and identify previous + configurations of a recurring job. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_job_revision( + client: "ChronicleClient", + integration_name: str, + job_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific revision for a given integration job. + + Use this method to clean up obsolete snapshots and manage the historical + record of background automation tasks. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_job_revision( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration job. + + Use this method to establish a recovery point before making significant + changes to a background job's script or parameters. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to create a revision for. + job: Dict containing the IntegrationJob to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationJobRevision resource. + + Raises: + APIError: If the API request fails. + """ + body = {"job": job} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_job_revision( + client: "ChronicleClient", + integration_name: str, + job_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Revert the current job definition to a previously saved revision. + + Use this method to rapidly recover a functional automation state if an + update causes operational issues. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationJobRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/revisions/{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_job_revisions.py b/tests/chronicle/integration/test_job_revisions.py new file mode 100644 index 00000000..3a81682c --- /dev/null +++ b/tests/chronicle/integration/test_job_revisions.py @@ -0,0 +1,378 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration job revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.job_revisions import ( + list_integration_job_revisions, + delete_integration_job_revision, + create_integration_job_revision, + rollback_integration_job_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_job_revisions tests -- + + +def test_list_integration_job_revisions_success(chronicle_client): + """Test list_integration_job_revisions delegates to chronicle_paginated_request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.job_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_job_revisions( + chronicle_client, + integration_name="My Integration", + job_id="j1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "jobs/j1/revisions" in kwargs["path"] + assert kwargs["items_key"] == "revisions" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_job_revisions_default_args(chronicle_client): + """Test list_integration_job_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_revisions( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + assert result == expected + + +def test_list_integration_job_revisions_with_filters(chronicle_client): + """Test list_integration_job_revisions with filter and order_by.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_revisions( + chronicle_client, + integration_name="test-integration", + job_id="j1", + filter_string='version = "1.0"', + order_by="createTime", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'version = "1.0"', + "orderBy": "createTime", + } + + +def test_list_integration_job_revisions_as_list(chronicle_client): + """Test list_integration_job_revisions returns list when as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_revisions( + chronicle_client, + integration_name="test-integration", + job_id="j1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_job_revisions_error(chronicle_client): + """Test list_integration_job_revisions raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + side_effect=APIError("Failed to list job revisions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_job_revisions( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to list job revisions" in str(exc_info.value) + + +# -- delete_integration_job_revision tests -- + + +def test_delete_integration_job_revision_success(chronicle_client): + """Test delete_integration_job_revision issues DELETE request.""" + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + revision_id="r1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "jobs/j1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_job_revision_error(chronicle_client): + """Test delete_integration_job_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + side_effect=APIError("Failed to delete job revision"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + revision_id="r1", + ) + assert "Failed to delete job revision" in str(exc_info.value) + + +# -- create_integration_job_revision tests -- + + +def test_create_integration_job_revision_required_fields_only( + chronicle_client, +): + """Test create_integration_job_revision with required fields only.""" + expected = {"name": "revisions/new", "job": {"displayName": "My Job"}} + job_dict = { + "displayName": "My Job", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job=job_dict, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/jobs/j1/revisions" + ), + api_version=APIVersion.V1BETA, + json={"job": job_dict}, + ) + + +def test_create_integration_job_revision_with_comment(chronicle_client): + """Test create_integration_job_revision includes comment when provided.""" + expected = {"name": "revisions/new"} + job_dict = { + "displayName": "My Job", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job=job_dict, + comment="Backup before major update", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["comment"] == "Backup before major update" + assert kwargs["json"]["job"] == job_dict + + +def test_create_integration_job_revision_error(chronicle_client): + """Test create_integration_job_revision raises APIError on failure.""" + job_dict = { + "displayName": "My Job", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + side_effect=APIError("Failed to create job revision"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job=job_dict, + ) + assert "Failed to create job revision" in str(exc_info.value) + + +# -- rollback_integration_job_revision tests -- + + +def test_rollback_integration_job_revision_success(chronicle_client): + """Test rollback_integration_job_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "job": { + "displayName": "My Job", + "script": "print('hello')", + }, + } + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "jobs/j1/revisions/r1:rollback" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_rollback_integration_job_revision_error(chronicle_client): + """Test rollback_integration_job_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + side_effect=APIError("Failed to rollback job revision"), + ): + with pytest.raises(APIError) as exc_info: + rollback_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + revision_id="r1", + ) + assert "Failed to rollback job revision" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_job_revisions_custom_api_version(chronicle_client): + """Test list_integration_job_revisions with custom API version.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.job_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_revisions( + chronicle_client, + integration_name="test-integration", + job_id="j1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_integration_job_revision_custom_api_version(chronicle_client): + """Test delete_integration_job_revision with custom API version.""" + with patch( + "secops.chronicle.integration.job_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_job_revision( + chronicle_client, + integration_name="test-integration", + job_id="j1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From c4bb017dfa672898636149dc941b59f3650c7080 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sun, 8 Mar 2026 19:47:15 +0000 Subject: [PATCH 29/46] feat: add functions for integration job instances --- README.md | 281 +++++++ api_module_mapping.md | 16 +- src/secops/chronicle/__init__.py | 53 +- src/secops/chronicle/client.py | 290 +++++++ .../chronicle/integration/job_instances.py | 385 +++++++++ src/secops/chronicle/models.py | 225 ++++++ .../integration/test_job_instances.py | 733 ++++++++++++++++++ 7 files changed, 1972 insertions(+), 11 deletions(-) create mode 100644 src/secops/chronicle/integration/job_instances.py create mode 100644 tests/chronicle/integration/test_job_instances.py diff --git a/README.md b/README.md index 8d48b8ff..2b116c43 100644 --- a/README.md +++ b/README.md @@ -2597,6 +2597,287 @@ rollback_result = chronicle.rollback_integration_job_revision( print(f"Rolled back to: {rollback_result.get('name')}") ``` +### Integration Job Instances + +List all job instances for a specific job: + +```python +# Get all job instances for a job +job_instances = chronicle.list_integration_job_instances( + integration_name="MyIntegration", + job_id="456" +) +for instance in job_instances.get("jobInstances", []): + print(f"Instance: {instance.get('displayName')}, Enabled: {instance.get('enabled')}") + +# Get all job instances as a list +job_instances = chronicle.list_integration_job_instances( + integration_name="MyIntegration", + job_id="456", + as_list=True +) + +# Filter job instances +job_instances = chronicle.list_integration_job_instances( + integration_name="MyIntegration", + job_id="456", + filter_string="enabled = true", + order_by="displayName" +) +``` + +Get details of a specific job instance: + +```python +job_instance = chronicle.get_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1" +) +print(f"Interval: {job_instance.get('intervalSeconds')} seconds") +``` + +Create a new job instance: + +```python +from secops.chronicle.models import IntegrationJobInstanceParameter + +# Create a job instance with basic scheduling (interval-based) +new_job_instance = chronicle.create_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + display_name="Daily Data Sync", + description="Syncs data from external source daily", + interval_seconds=86400, # 24 hours + enabled=True, + advanced=False, + parameters=[ + IntegrationJobInstanceParameter(value="production"), + IntegrationJobInstanceParameter(value="https://api.example.com") + ] +) +``` + +Create a job instance with advanced scheduling: + +```python +from secops.chronicle.models import ( + AdvancedConfig, + ScheduleType, + DailyScheduleDetails, + Date, + TimeOfDay +) + +# Create with daily schedule +advanced_job_instance = chronicle.create_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + display_name="Daily Backup at 2 AM", + interval_seconds=86400, + enabled=True, + advanced=True, + advanced_config=AdvancedConfig( + time_zone="America/New_York", + schedule_type=ScheduleType.DAILY, + daily_schedule=DailyScheduleDetails( + start_date=Date(year=2025, month=1, day=1), + time=TimeOfDay(hours=2, minutes=0), + interval=1 # Every 1 day + ) + ), + agent="agent-123" # For remote execution +) +``` + +Create a job instance with weekly schedule: + +```python +from secops.chronicle.models import ( + AdvancedConfig, + ScheduleType, + WeeklyScheduleDetails, + DayOfWeek, + Date, + TimeOfDay +) + +# Run every Monday and Friday at 9 AM +weekly_job_instance = chronicle.create_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + display_name="Weekly Report", + interval_seconds=604800, # 1 week + enabled=True, + advanced=True, + advanced_config=AdvancedConfig( + time_zone="UTC", + schedule_type=ScheduleType.WEEKLY, + weekly_schedule=WeeklyScheduleDetails( + start_date=Date(year=2025, month=1, day=1), + days=[DayOfWeek.MONDAY, DayOfWeek.FRIDAY], + time=TimeOfDay(hours=9, minutes=0), + interval=1 # Every 1 week + ) + ) +) +``` + +Create a job instance with monthly schedule: + +```python +from secops.chronicle.models import ( + AdvancedConfig, + ScheduleType, + MonthlyScheduleDetails, + Date, + TimeOfDay +) + +# Run on the 1st of every month at midnight +monthly_job_instance = chronicle.create_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + display_name="Monthly Cleanup", + interval_seconds=2592000, # ~30 days + enabled=True, + advanced=True, + advanced_config=AdvancedConfig( + time_zone="America/Los_Angeles", + schedule_type=ScheduleType.MONTHLY, + monthly_schedule=MonthlyScheduleDetails( + start_date=Date(year=2025, month=1, day=1), + day=1, # Day of month (1-31) + time=TimeOfDay(hours=0, minutes=0), + interval=1 # Every 1 month + ) + ) +) +``` + +Create a one-time job instance: + +```python +from secops.chronicle.models import ( + AdvancedConfig, + ScheduleType, + OneTimeScheduleDetails, + Date, + TimeOfDay +) + +# Run once at a specific date and time +onetime_job_instance = chronicle.create_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + display_name="One-Time Migration", + interval_seconds=0, # Not used for one-time + enabled=True, + advanced=True, + advanced_config=AdvancedConfig( + time_zone="Europe/London", + schedule_type=ScheduleType.ONCE, + one_time_schedule=OneTimeScheduleDetails( + start_date=Date(year=2025, month=12, day=25), + time=TimeOfDay(hours=10, minutes=30) + ) + ) +) +``` + +Update a job instance: + +```python +from secops.chronicle.models import IntegrationJobInstanceParameter + +# Update scheduling and enable/disable +updated_instance = chronicle.update_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + display_name="Updated Sync Job", + interval_seconds=43200, # 12 hours + enabled=False +) + +# Update parameters +updated_instance = chronicle.update_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + parameters=[ + IntegrationJobInstanceParameter(value="staging"), + IntegrationJobInstanceParameter(value="https://staging-api.example.com") + ] +) + +# Update to use advanced scheduling +from secops.chronicle.models import ( + AdvancedConfig, + ScheduleType, + DailyScheduleDetails, + Date, + TimeOfDay +) + +updated_instance = chronicle.update_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + advanced=True, + advanced_config=AdvancedConfig( + time_zone="UTC", + schedule_type=ScheduleType.DAILY, + daily_schedule=DailyScheduleDetails( + start_date=Date(year=2025, month=1, day=1), + time=TimeOfDay(hours=12, minutes=0), + interval=1 + ) + ) +) + +# Update only specific fields +updated_instance = chronicle.update_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + enabled=True, + update_mask="enabled" +) +``` + +Delete a job instance: + +```python +chronicle.delete_integration_job_instance( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1" +) +``` + +Run a job instance on demand: + +```python +# Run immediately without waiting for schedule +result = chronicle.run_integration_job_instance_on_demand( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1" +) +print(f"Job execution started: {result}") + +# Run with parameter overrides +result = chronicle.run_integration_job_instance_on_demand( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + parameters=[ + IntegrationJobInstanceParameter(id=1, value="test-mode") + ] +) +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 7d273821..5066ec02 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 42 endpoints implemented -- **v1alpha:** 135 endpoints implemented +- **v1beta:** 48 endpoints implemented +- **v1alpha:** 141 endpoints implemented ## Endpoint Mapping @@ -120,6 +120,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.revisions.delete | v1beta | chronicle.integration.job_revisions.delete_integration_job_revision | | | integrations.jobs.revisions.list | v1beta | chronicle.integration.job_revisions.list_integration_job_revisions | | | integrations.jobs.revisions.rollback | v1beta | chronicle.integration.job_revisions.rollback_integration_job_revision | | +| integrations.jobs.jobInstances.create | v1beta | chronicle.integration.job_instances.create_integration_job_instance | | +| integrations.jobs.jobInstances.delete | v1beta | chronicle.integration.job_instances.delete_integration_job_instance | | +| integrations.jobs.jobInstances.get | v1beta | chronicle.integration.job_instances.get_integration_job_instance | | +| integrations.jobs.jobInstances.list | v1beta | chronicle.integration.job_instances.list_integration_job_instances | | +| integrations.jobs.jobInstances.patch | v1beta | chronicle.integration.job_instances.update_integration_job_instance | | +| integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.integration.job_instances.run_integration_job_instance_on_demand | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -352,6 +358,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.revisions.delete | v1alpha | chronicle.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.revisions.list | v1alpha | chronicle.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.revisions.rollback | v1alpha | chronicle.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.create | v1alpha | chronicle.integration.job_instances.create_integration_job_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.delete | v1alpha | chronicle.integration.job_instances.delete_integration_job_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.get | v1alpha | chronicle.integration.job_instances.get_integration_job_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.list | v1alpha | chronicle.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.patch | v1alpha | chronicle.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.integration.job_instances.run_integration_job_instance_on_demand(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index ea5826ec..f6c9f834 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -98,13 +98,17 @@ search_log_types, ) from secops.chronicle.models import ( + AdvancedConfig, AlertCount, AlertState, Case, CaseList, + DailyScheduleDetails, DataExport, DataExportStage, DataExportStatus, + Date, + DayOfWeek, DetectionType, DiffType, Entity, @@ -113,18 +117,24 @@ EntitySummary, FileMetadataAndProperties, InputInterval, + IntegrationJobInstanceParameter, IntegrationParam, IntegrationParamType, IntegrationType, ListBasis, + MonthlyScheduleDetails, + OneTimeScheduleDetails, PrevalenceData, PythonVersion, + ScheduleType, SoarPlatformInfo, TargetMode, TileType, TimeInterval, Timeline, TimelineBucket, + TimeOfDay, + WeeklyScheduleDetails, WidgetMetadata, ) from secops.chronicle.nl_search import translate_nl_to_udm @@ -265,6 +275,14 @@ create_integration_job_revision, rollback_integration_job_revision, ) +from secops.chronicle.integration.job_instances import ( + list_integration_job_instances, + get_integration_job_instance, + delete_integration_job_instance, + create_integration_job_instance, + update_integration_job_instance, + run_integration_job_instance_on_demand, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -388,21 +406,31 @@ "execute_query", "get_execute_query", # Models + "AdvancedConfig", + "AlertCount", + "AlertState", + "Case", + "CaseList", + "DailyScheduleDetails", + "Date", + "DayOfWeek", "Entity", "EntityMetadata", "EntityMetrics", + "EntitySummary", + "FileMetadataAndProperties", + "IntegrationJobInstanceParameter", + "MonthlyScheduleDetails", + "OneTimeScheduleDetails", + "PrevalenceData", + "ScheduleType", + "SoarPlatformInfo", "TimeInterval", - "TimelineBucket", "Timeline", + "TimelineBucket", + "TimeOfDay", + "WeeklyScheduleDetails", "WidgetMetadata", - "EntitySummary", - "AlertCount", - "AlertState", - "Case", - "SoarPlatformInfo", - "CaseList", - "PrevalenceData", - "FileMetadataAndProperties", "ValidationResult", "GeminiResponse", "Block", @@ -495,6 +523,13 @@ "delete_integration_job_revision", "create_integration_job_revision", "rollback_integration_job_revision", + # Integration Job Instances + "list_integration_job_instances", + "get_integration_job_instance", + "delete_integration_job_instance", + "create_integration_job_instance", + "update_integration_job_instance", + "run_integration_job_instance_on_demand", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 70262512..f0c286e0 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -203,6 +203,14 @@ list_integration_job_revisions as _list_integration_job_revisions, rollback_integration_job_revision as _rollback_integration_job_revision, ) +from secops.chronicle.integration.job_instances import ( + create_integration_job_instance as _create_integration_job_instance, + delete_integration_job_instance as _delete_integration_job_instance, + get_integration_job_instance as _get_integration_job_instance, + list_integration_job_instances as _list_integration_job_instances, + run_integration_job_instance_on_demand as _run_integration_job_instance_on_demand, + update_integration_job_instance as _update_integration_job_instance, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -2990,6 +2998,288 @@ def rollback_integration_job_revision( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Job Instances methods + # ------------------------------------------------------------------------- + + def list_integration_job_instances( + self, + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all job instances for a specific integration job. + + Use this method to browse the active job instances and their + last execution status. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to list instances for. + page_size: Maximum number of job instances to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter job instances. + order_by: Field to sort the job instances by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of job instances instead of + a dict with job instances list and nextPageToken. + + Returns: + If as_list is True: List of job instances. + If as_list is False: Dict with job instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_job_instances( + self, + integration_name, + job_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_job_instance( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single job instance for a specific integration job. + + Use this method to retrieve configuration details and the + current schedule settings for a job instance. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified + IntegrationJobInstance. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_job_instance( + self, + integration_name, + job_id, + job_instance_id, + api_version=api_version, + ) + + def delete_integration_job_instance( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific job instance for a given integration job. + + Use this method to remove scheduled or configured job instances + that are no longer needed. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_job_instance( + self, + integration_name, + job_id, + job_instance_id, + api_version=api_version, + ) + + def create_integration_job_instance( + self, + integration_name: str, + job_id: str, + display_name: str, + interval_seconds: int, + enabled: bool, + advanced: bool, + description: str | None = None, + parameters: list[dict[str, Any]] | None = None, + agent: str | None = None, + advanced_config: dict[str, Any] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new job instance for a given integration job. + + Use this method to schedule a job to run at regular intervals + or with advanced cron-style scheduling. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to create an instance for. + display_name: Display name for the job instance. + interval_seconds: Interval in seconds between job runs. + enabled: Whether the job instance is enabled. + advanced: Whether advanced scheduling is used. + description: Description of the job instance. Optional. + parameters: List of parameter values for the job instance. + Optional. + agent: Agent identifier for remote execution. Optional. + advanced_config: Advanced scheduling configuration. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationJobInstance + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_job_instance( + self, + integration_name, + job_id, + display_name, + interval_seconds, + enabled, + advanced, + description=description, + parameters=parameters, + agent=agent, + advanced_config=advanced_config, + api_version=api_version, + ) + + def update_integration_job_instance( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + display_name: str | None = None, + description: str | None = None, + interval_seconds: int | None = None, + enabled: bool | None = None, + advanced: bool | None = None, + parameters: list[dict[str, Any]] | None = None, + advanced_config: dict[str, Any] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing job instance for a given integration job. + + Use this method to modify scheduling, parameters, or enable/ + disable a job instance. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to update. + display_name: Display name for the job instance. Optional. + description: Description of the job instance. Optional. + interval_seconds: Interval in seconds between job runs. + Optional. + enabled: Whether the job instance is enabled. Optional. + advanced: Whether advanced scheduling is used. Optional. + parameters: List of parameter values for the job instance. + Optional. + advanced_config: Advanced scheduling configuration. + Optional. + update_mask: Comma-separated field paths to update. If not + provided, will be auto-generated. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated IntegrationJobInstance. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_job_instance( + self, + integration_name, + job_id, + job_instance_id, + display_name=display_name, + description=description, + interval_seconds=interval_seconds, + enabled=enabled, + advanced=advanced, + parameters=parameters, + advanced_config=advanced_config, + update_mask=update_mask, + api_version=api_version, + ) + + def run_integration_job_instance_on_demand( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + parameters: list[dict[str, Any]] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Run a job instance immediately without waiting for the next + scheduled execution. + + Use this method to manually trigger a job instance for testing + or immediate data collection. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to run. + parameters: Optional parameter overrides for this run. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the result of the on-demand run. + + Raises: + APIError: If the API request fails. + """ + return _run_integration_job_instance_on_demand( + self, + integration_name, + job_id, + job_instance_id, + parameters=parameters, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/job_instances.py b/src/secops/chronicle/integration/job_instances.py new file mode 100644 index 00000000..4544a271 --- /dev/null +++ b/src/secops/chronicle/integration/job_instances.py @@ -0,0 +1,385 @@ +# Copyright 2025 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. +# +"""Marketplace integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + AdvancedConfig, + IntegrationJobInstanceParameter +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_job_instances( + client: "ChronicleClient", + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all job instances for a specific integration job. + + Use this method to browse the active job instances and their last + execution status. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to list instances for. + page_size: Maximum number of job instances to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter job instances. + order_by: Field to sort the job instances by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of job instances instead of a dict + with job instances list and nextPageToken. + + Returns: + If as_list is True: List of job instances. + If as_list is False: Dict with job instances list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/jobs/" + f"{job_id}/jobInstances" + ), + items_key="jobInstances", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_job_instance( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single job instance for a specific integration job. + + Use this method to retrieve the execution status, last run time, and + active schedule for a specific background task. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationJobInstance. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/jobs/" + f"{job_id}/jobInstances/{job_instance_id}" + ), + api_version=api_version, + ) + + +def delete_integration_job_instance( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific job instance for a given integration job. + + Use this method to permanently stop and remove a scheduled background task. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/jobs/" + f"{job_id}/jobInstances/{job_instance_id}" + ), + api_version=api_version, + ) + + +#pylint: disable=line-too-long +def create_integration_job_instance( + client: "ChronicleClient", + integration_name: str, + job_id: str, + display_name: str, + interval_seconds: int, + enabled: bool, + advanced: bool, + description: str | None = None, + parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + advanced_config: dict[str, Any] | AdvancedConfig | None = None, + agent: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + #pylint: enable=line-too-long + """Create a new job instance for a specific integration job. + + Use this method to schedule a new recurring background job. You must + provide a valid execution interval and any required script parameters. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to create an instance for. + display_name: Job instance display name. Required. + interval_seconds: Job execution interval in seconds. Minimum 60. + Required. + enabled: Whether the job instance is enabled. Required. + advanced: Whether the job instance uses advanced scheduling. Required. + description: Job instance description. Optional. + parameters: List of IntegrationJobInstanceParameter instances or + dicts. Optional. + advanced_config: Advanced scheduling configuration. Accepts an + AdvancedConfig instance or a raw dict. Optional. + agent: Agent identifier for remote job execution. Cannot be patched + after creation. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationJobInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters] + if parameters is not None + else None + ) + resolved_advanced_config = ( + advanced_config.to_dict() + if isinstance(advanced_config, AdvancedConfig) + else advanced_config + ) + + body = { + "displayName": display_name, + "intervalSeconds": interval_seconds, + "enabled": enabled, + "advanced": advanced, + "description": description, + "parameters": resolved_parameters, + "advancedConfig": resolved_advanced_config, + "agent": agent, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}" + f"/jobs/{job_id}/jobInstances" + ), + api_version=api_version, + json=body, + ) + +#pylint: disable=line-too-long +def update_integration_job_instance( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + display_name: str | None = None, + interval_seconds: int | None = None, + enabled: bool | None = None, + advanced: bool | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + advanced_config: dict[str, Any] | AdvancedConfig | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + # pylint: enable=line-too-long + """Update an existing job instance for a given integration job. + + Use this method to modify the execution interval, enable/disable the job + instance, or adjust the parameters passed to the background script. + + Note: The agent field cannot be updated after creation. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to update. + display_name: Job instance display name. + interval_seconds: Job execution interval in seconds. Minimum 60. + enabled: Whether the job instance is enabled. + advanced: Whether the job instance uses advanced scheduling. + description: Job instance description. + parameters: List of IntegrationJobInstanceParameter instances or + dicts. + advanced_config: Advanced scheduling configuration. Accepts an + AdvancedConfig instance or a raw dict. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,intervalSeconds". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationJobInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters] + if parameters is not None + else None + ) + resolved_advanced_config = ( + advanced_config.to_dict() + if isinstance(advanced_config, AdvancedConfig) + else advanced_config + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("intervalSeconds", "intervalSeconds", interval_seconds), + ("enabled", "enabled", enabled), + ("advanced", "advanced", advanced), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ("advancedConfig", "advancedConfig", resolved_advanced_config), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/jobInstances/{job_instance_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + +#pylint: disable=line-too-long +def run_integration_job_instance_on_demand( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + # pylint: enable=line-too-long + """Execute a job instance immediately, bypassing its normal schedule. + + Use this method to trigger an on-demand run of a job for synchronization + or troubleshooting purposes. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to run on demand. + parameters: List of IntegrationJobInstanceParameter instances or + dicts. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing a success boolean indicating whether the job run + completed successfully. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters] + if parameters is not None + else None + ) + + body = {} + if resolved_parameters is not None: + body["parameters"] = resolved_parameters + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}" + f"/jobs/{job_id}/jobInstances/{job_instance_id}:runOnDemand" + ), + api_version=api_version, + json=body, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index ba71a13e..f7f342d9 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -309,6 +309,231 @@ def to_dict(self) -> dict: return data +@dataclass +class IntegrationJobInstanceParameter: + """A parameter instance for a Chronicle SOAR integration job instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring a job instance. + + Attributes: + value: The value of the parameter. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + +class ScheduleType(str, Enum): + """Schedule types for Chronicle SOAR integration job + instance advanced config.""" + + UNSPECIFIED = "SCHEDULE_TYPE_UNSPECIFIED" + ONCE = "ONCE" + DAILY = "DAILY" + WEEKLY = "WEEKLY" + MONTHLY = "MONTHLY" + + +class DayOfWeek(str, Enum): + """Days of the week for Chronicle SOAR weekly schedule details.""" + + UNSPECIFIED = "DAY_OF_WEEK_UNSPECIFIED" + MONDAY = "MONDAY" + TUESDAY = "TUESDAY" + WEDNESDAY = "WEDNESDAY" + THURSDAY = "THURSDAY" + FRIDAY = "FRIDAY" + SATURDAY = "SATURDAY" + SUNDAY = "SUNDAY" + + +@dataclass +class Date: + """A calendar date for Chronicle SOAR schedule details. + + Attributes: + year: The year. + month: The month of the year (1-12). + day: The day of the month (1-31). + """ + + year: int + month: int + day: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return {"year": self.year, "month": self.month, "day": self.day} + + +@dataclass +class TimeOfDay: + """A time of day for Chronicle SOAR schedule details. + + Attributes: + hours: The hour of the day (0-23). + minutes: The minute of the hour (0-59). + seconds: The second of the minute (0-59). + nanos: The nanoseconds of the second (0-999999999). + """ + + hours: int + minutes: int + seconds: int = 0 + nanos: int = 0 + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "hours": self.hours, + "minutes": self.minutes, + "seconds": self.seconds, + "nanos": self.nanos, + } + + +@dataclass +class OneTimeScheduleDetails: + """One-time schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The date to run the job. + time: The time to run the job. + """ + + start_date: Date + time: TimeOfDay + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "time": self.time.to_dict(), + } + + +@dataclass +class DailyScheduleDetails: + """Daily schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + time: The time to run the job. + interval: The day interval. + """ + + start_date: Date + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class WeeklyScheduleDetails: + """Weekly schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + days: The days of the week to run the job. + time: The time to run the job. + interval: The week interval. + """ + + start_date: Date + days: list[DayOfWeek] + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "days": [d.value for d in self.days], + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class MonthlyScheduleDetails: + """Monthly schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + day: The day of the month to run the job. + time: The time to run the job. + interval: The month interval. + """ + + start_date: Date + day: int + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "day": self.day, + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class AdvancedConfig: + """Advanced scheduling configuration for a Chronicle SOAR job instance. + + Exactly one of the schedule detail fields should be provided, corresponding + to the schedule_type. + + Attributes: + time_zone: The zone id. + schedule_type: The schedule type. + one_time_schedule: One-time schedule details. Use with ONCE. + daily_schedule: Daily schedule details. Use with DAILY. + weekly_schedule: Weekly schedule details. Use with WEEKLY. + monthly_schedule: Monthly schedule details. Use with MONTHLY. + """ + + time_zone: str + schedule_type: ScheduleType + one_time_schedule: OneTimeScheduleDetails | None = None + daily_schedule: DailyScheduleDetails | None = None + weekly_schedule: WeeklyScheduleDetails | None = None + monthly_schedule: MonthlyScheduleDetails | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "timeZone": self.time_zone, + "scheduleType": str(self.schedule_type.value), + } + if self.one_time_schedule is not None: + data["oneTimeSchedule"] = self.one_time_schedule.to_dict() + if self.daily_schedule is not None: + data["dailySchedule"] = self.daily_schedule.to_dict() + if self.weekly_schedule is not None: + data["weeklySchedule"] = self.weekly_schedule.to_dict() + if self.monthly_schedule is not None: + data["monthlySchedule"] = self.monthly_schedule.to_dict() + return data + + @dataclass class JobParameter: """A parameter definition for a Chronicle SOAR integration job. diff --git a/tests/chronicle/integration/test_job_instances.py b/tests/chronicle/integration/test_job_instances.py new file mode 100644 index 00000000..b64adff4 --- /dev/null +++ b/tests/chronicle/integration/test_job_instances.py @@ -0,0 +1,733 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration job instances functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + IntegrationJobInstanceParameter, + AdvancedConfig, + ScheduleType, + DailyScheduleDetails, + Date, + TimeOfDay, +) +from secops.chronicle.integration.job_instances import ( + list_integration_job_instances, + get_integration_job_instance, + delete_integration_job_instance, + create_integration_job_instance, + update_integration_job_instance, + run_integration_job_instance_on_demand, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_job_instances tests -- + + +def test_list_integration_job_instances_success(chronicle_client): + """Test list_integration_job_instances delegates to chronicle_paginated_request.""" + expected = { + "jobInstances": [{"name": "ji1"}, {"name": "ji2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.job_instances.format_resource_id", + return_value="My Integration", + ): + result = list_integration_job_instances( + chronicle_client, + integration_name="My Integration", + job_id="j1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "jobs/j1/jobInstances" in kwargs["path"] + assert kwargs["items_key"] == "jobInstances" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_job_instances_default_args(chronicle_client): + """Test list_integration_job_instances with default args.""" + expected = {"jobInstances": []} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_instances( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + assert result == expected + + +def test_list_integration_job_instances_with_filters(chronicle_client): + """Test list_integration_job_instances with filter and order_by.""" + expected = {"jobInstances": [{"name": "ji1"}]} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_instances( + chronicle_client, + integration_name="test-integration", + job_id="j1", + filter_string="enabled = true", + order_by="displayName", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "enabled = true", + "orderBy": "displayName", + } + + +def test_list_integration_job_instances_as_list(chronicle_client): + """Test list_integration_job_instances returns list when as_list=True.""" + expected = [{"name": "ji1"}, {"name": "ji2"}] + + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_instances( + chronicle_client, + integration_name="test-integration", + job_id="j1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_job_instances_error(chronicle_client): + """Test list_integration_job_instances raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + side_effect=APIError("Failed to list job instances"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_job_instances( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to list job instances" in str(exc_info.value) + + +# -- get_integration_job_instance tests -- + + +def test_get_integration_job_instance_success(chronicle_client): + """Test get_integration_job_instance issues GET request.""" + expected = { + "name": "jobInstances/ji1", + "displayName": "My Job Instance", + "intervalSeconds": 300, + "enabled": True, + } + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "jobs/j1/jobInstances/ji1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_integration_job_instance_error(chronicle_client): + """Test get_integration_job_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + side_effect=APIError("Failed to get job instance"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + assert "Failed to get job instance" in str(exc_info.value) + + +# -- delete_integration_job_instance tests -- + + +def test_delete_integration_job_instance_success(chronicle_client): + """Test delete_integration_job_instance issues DELETE request.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "jobs/j1/jobInstances/ji1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_job_instance_error(chronicle_client): + """Test delete_integration_job_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + side_effect=APIError("Failed to delete job instance"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + assert "Failed to delete job instance" in str(exc_info.value) + + +# -- create_integration_job_instance tests -- + + +def test_create_integration_job_instance_required_fields_only(chronicle_client): + """Test create_integration_job_instance sends only required fields.""" + expected = {"name": "jobInstances/new", "displayName": "My Job Instance"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="My Job Instance", + interval_seconds=300, + enabled=True, + advanced=False, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/jobs/j1/jobInstances", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Job Instance", + "intervalSeconds": 300, + "enabled": True, + "advanced": False, + }, + ) + + +def test_create_integration_job_instance_with_optional_fields(chronicle_client): + """Test create_integration_job_instance includes optional fields when provided.""" + expected = {"name": "jobInstances/new"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="My Job Instance", + interval_seconds=300, + enabled=True, + advanced=False, + description="Test job instance", + parameters=[{"id": 1, "value": "test"}], + agent="agent-123", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test job instance" + assert kwargs["json"]["parameters"] == [{"value": "test"}] + assert kwargs["json"]["agent"] == "agent-123" + + +def test_create_integration_job_instance_with_dataclass_params(chronicle_client): + """Test create_integration_job_instance converts dataclass parameters.""" + expected = {"name": "jobInstances/new"} + + param = IntegrationJobInstanceParameter(value="test-value") + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="My Job Instance", + interval_seconds=300, + enabled=True, + advanced=False, + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["value"] == "test-value" + + +def test_create_integration_job_instance_with_advanced_config(chronicle_client): + """Test create_integration_job_instance with AdvancedConfig dataclass.""" + expected = {"name": "jobInstances/new"} + + advanced_config = AdvancedConfig( + time_zone="America/New_York", + schedule_type=ScheduleType.DAILY, + daily_schedule=DailyScheduleDetails( + start_date=Date(year=2026, month=3, day=8), + time=TimeOfDay(hours=2, minutes=0), + interval=1 + ) + ) + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="My Job Instance", + interval_seconds=300, + enabled=True, + advanced=True, + advanced_config=advanced_config, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + config_sent = kwargs["json"]["advancedConfig"] + assert config_sent["timeZone"] == "America/New_York" + assert config_sent["scheduleType"] == "DAILY" + assert "dailySchedule" in config_sent + + +def test_create_integration_job_instance_error(chronicle_client): + """Test create_integration_job_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + side_effect=APIError("Failed to create job instance"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + display_name="My Job Instance", + interval_seconds=300, + enabled=True, + advanced=False, + ) + assert "Failed to create job instance" in str(exc_info.value) + + +# -- update_integration_job_instance tests -- + + +def test_update_integration_job_instance_single_field(chronicle_client): + """Test update_integration_job_instance updates a single field.""" + expected = {"name": "jobInstances/ji1", "displayName": "Updated Instance"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=( + {"displayName": "Updated Instance"}, + {"updateMask": "displayName"}, + ), + ) as mock_build_patch: + result = update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + display_name="Updated Instance", + ) + + assert result == expected + + mock_build_patch.assert_called_once() + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path=( + "integrations/test-integration/jobs/j1/jobInstances/ji1" + ), + api_version=APIVersion.V1BETA, + json={"displayName": "Updated Instance"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_job_instance_multiple_fields(chronicle_client): + """Test update_integration_job_instance updates multiple fields.""" + expected = {"name": "jobInstances/ji1"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=( + { + "displayName": "Updated", + "intervalSeconds": 600, + "enabled": False, + }, + {"updateMask": "displayName,intervalSeconds,enabled"}, + ), + ): + result = update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + display_name="Updated", + interval_seconds=600, + enabled=False, + ) + + assert result == expected + + +def test_update_integration_job_instance_with_dataclass_params(chronicle_client): + """Test update_integration_job_instance converts dataclass parameters.""" + expected = {"name": "jobInstances/ji1"} + + param = IntegrationJobInstanceParameter(value="updated-value") + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=( + {"parameters": [{"value": "updated-value"}]}, + {"updateMask": "parameters"}, + ), + ): + result = update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + parameters=[param], + ) + + assert result == expected + + +def test_update_integration_job_instance_with_advanced_config(chronicle_client): + """Test update_integration_job_instance with AdvancedConfig dataclass.""" + expected = {"name": "jobInstances/ji1"} + + advanced_config = AdvancedConfig( + time_zone="UTC", + schedule_type=ScheduleType.DAILY, + daily_schedule=DailyScheduleDetails( + start_date=Date(year=2026, month=3, day=8), + time=TimeOfDay(hours=0, minutes=0), + interval=1 + ) + ) + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=( + { + "advancedConfig": { + "timeZone": "UTC", + "scheduleType": "DAILY", + "dailySchedule": { + "startDate": {"year": 2026, "month": 3, "day": 8}, + "time": {"hours": 0, "minutes": 0}, + "interval": 1 + } + } + }, + {"updateMask": "advancedConfig"}, + ), + ): + result = update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + advanced_config=advanced_config, + ) + + assert result == expected + + +def test_update_integration_job_instance_with_update_mask(chronicle_client): + """Test update_integration_job_instance respects explicit update_mask.""" + expected = {"name": "jobInstances/ji1"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=( + {"displayName": "Updated"}, + {"updateMask": "displayName"}, + ), + ): + result = update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + display_name="Updated", + update_mask="displayName", + ) + + assert result == expected + + +def test_update_integration_job_instance_error(chronicle_client): + """Test update_integration_job_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + side_effect=APIError("Failed to update job instance"), + ), patch( + "secops.chronicle.integration.job_instances.build_patch_body", + return_value=({"enabled": False}, {"updateMask": "enabled"}), + ): + with pytest.raises(APIError) as exc_info: + update_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + enabled=False, + ) + assert "Failed to update job instance" in str(exc_info.value) + + +# -- run_integration_job_instance_on_demand tests -- + + +def test_run_integration_job_instance_on_demand_success(chronicle_client): + """Test run_integration_job_instance_on_demand issues POST request.""" + expected = {"success": True} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_integration_job_instance_on_demand( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/jobs/j1/jobInstances/ji1:runOnDemand" + ), + api_version=APIVersion.V1BETA, + json={}, + ) + + +def test_run_integration_job_instance_on_demand_with_params(chronicle_client): + """Test run_integration_job_instance_on_demand with parameters.""" + expected = {"success": True} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_integration_job_instance_on_demand( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + parameters=[{"id": 1, "value": "override"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["parameters"] == [{"value": "override"}] + + +def test_run_integration_job_instance_on_demand_with_dataclass(chronicle_client): + """Test run_integration_job_instance_on_demand converts dataclass parameters.""" + expected = {"success": True} + + param = IntegrationJobInstanceParameter(value="test") + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_integration_job_instance_on_demand( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["value"] == "test" + + +def test_run_integration_job_instance_on_demand_error(chronicle_client): + """Test run_integration_job_instance_on_demand raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + side_effect=APIError("Failed to run job instance on demand"), + ): + with pytest.raises(APIError) as exc_info: + run_integration_job_instance_on_demand( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + assert "Failed to run job instance on demand" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_job_instances_custom_api_version(chronicle_client): + """Test list_integration_job_instances with custom API version.""" + expected = {"jobInstances": []} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_job_instances( + chronicle_client, + integration_name="test-integration", + job_id="j1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_integration_job_instance_custom_api_version(chronicle_client): + """Test get_integration_job_instance with custom API version.""" + expected = {"name": "jobInstances/ji1"} + + with patch( + "secops.chronicle.integration.job_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_job_instance( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From aa69ab8fb08bc430e95271748b5879b74c8ee4ea Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sun, 8 Mar 2026 20:30:35 +0000 Subject: [PATCH 30/46] feat: add functions for integration job context properties --- README.md | 100 ++++ api_module_mapping.md | 16 +- src/secops/chronicle/__init__.py | 15 + src/secops/chronicle/client.py | 252 +++++++++ src/secops/chronicle/integration/actions.py | 2 +- .../chronicle/integration/connectors.py | 2 +- .../integration/job_context_properties.py | 298 +++++++++++ .../chronicle/integration/job_instances.py | 2 +- .../chronicle/integration/job_revisions.py | 2 +- src/secops/chronicle/integration/jobs.py | 2 +- .../integration/manager_revisions.py | 2 +- src/secops/chronicle/integration/managers.py | 2 +- .../integration/marketplace_integrations.py | 2 +- .../test_job_context_properties.py | 506 ++++++++++++++++++ .../integration/test_job_instances.py | 4 +- 15 files changed, 1195 insertions(+), 12 deletions(-) create mode 100644 src/secops/chronicle/integration/job_context_properties.py create mode 100644 tests/chronicle/integration/test_job_context_properties.py diff --git a/README.md b/README.md index 2b116c43..077e2994 100644 --- a/README.md +++ b/README.md @@ -2878,6 +2878,106 @@ result = chronicle.run_integration_job_instance_on_demand( ) ``` +### Job Context Properties + +List all context properties for a job: + +```python +# Get all context properties for a job +context_properties = chronicle.list_job_context_properties( + integration_name="MyIntegration", + job_id="456" +) +for prop in context_properties.get("contextProperties", []): + print(f"Key: {prop.get('key')}, Value: {prop.get('value')}") + +# Get all context properties as a list +context_properties = chronicle.list_job_context_properties( + integration_name="MyIntegration", + job_id="456", + as_list=True +) + +# Filter context properties +context_properties = chronicle.list_job_context_properties( + integration_name="MyIntegration", + job_id="456", + filter_string='key = "api-token"', + order_by="key" +) +``` + +Get a specific context property: + +```python +property_value = chronicle.get_job_context_property( + integration_name="MyIntegration", + job_id="456", + context_property_id="api-endpoint" +) +print(f"Value: {property_value.get('value')}") +``` + +Create a new context property: + +```python +# Create with auto-generated key +new_property = chronicle.create_job_context_property( + integration_name="MyIntegration", + job_id="456", + value="https://api.example.com/v2" +) +print(f"Created property: {new_property.get('key')}") + +# Create with custom key (must be 4-63 chars, match /[a-z][0-9]-/) +new_property = chronicle.create_job_context_property( + integration_name="MyIntegration", + job_id="456", + value="my-secret-token", + key="apitoken" +) +``` + +Update a context property: + +```python +# Update the value of an existing property +updated_property = chronicle.update_job_context_property( + integration_name="MyIntegration", + job_id="456", + context_property_id="api-endpoint", + value="https://api.example.com/v3" +) +print(f"Updated to: {updated_property.get('value')}") +``` + +Delete a context property: + +```python +chronicle.delete_job_context_property( + integration_name="MyIntegration", + job_id="456", + context_property_id="api-endpoint" +) +``` + +Delete all context properties: + +```python +# Clear all context properties for a job +chronicle.delete_all_job_context_properties( + integration_name="MyIntegration", + job_id="456" +) + +# Clear all properties for a specific context ID +chronicle.delete_all_job_context_properties( + integration_name="MyIntegration", + job_id="456", + context_id="mycontext" +) +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 5066ec02..71275878 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 48 endpoints implemented -- **v1alpha:** 141 endpoints implemented +- **v1beta:** 54 endpoints implemented +- **v1alpha:** 147 endpoints implemented ## Endpoint Mapping @@ -126,6 +126,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.jobInstances.list | v1beta | chronicle.integration.job_instances.list_integration_job_instances | | | integrations.jobs.jobInstances.patch | v1beta | chronicle.integration.job_instances.update_integration_job_instance | | | integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.integration.job_instances.run_integration_job_instance_on_demand | | +| integrations.jobs.contextProperties.clearAll | v1beta | chronicle.integration.job_context_properties.delete_all_job_context_properties | | +| integrations.jobs.contextProperties.create | v1beta | chronicle.integration.job_context_properties.create_job_context_property | | +| integrations.jobs.contextProperties.delete | v1beta | chronicle.integration.job_context_properties.delete_job_context_property | | +| integrations.jobs.contextProperties.get | v1beta | chronicle.integration.job_context_properties.get_job_context_property | | +| integrations.jobs.contextProperties.list | v1beta | chronicle.integration.job_context_properties.list_job_context_properties | | +| integrations.jobs.contextProperties.patch | v1beta | chronicle.integration.job_context_properties.update_job_context_property | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -364,6 +370,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.jobInstances.list | v1alpha | chronicle.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.jobInstances.patch | v1alpha | chronicle.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.integration.job_instances.run_integration_job_instance_on_demand(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.clearAll | v1alpha | chronicle.integration.job_context_properties.delete_all_job_context_properties(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.create | v1alpha | chronicle.integration.job_context_properties.create_job_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.delete | v1alpha | chronicle.integration.job_context_properties.delete_job_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.get | v1alpha | chronicle.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.list | v1alpha | chronicle.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.contextProperties.patch | v1alpha | chronicle.integration.job_context_properties.update_job_context_property(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index f6c9f834..d6b9fa1f 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -283,6 +283,14 @@ update_integration_job_instance, run_integration_job_instance_on_demand, ) +from secops.chronicle.integration.job_context_properties import ( + list_job_context_properties, + get_job_context_property, + delete_job_context_property, + create_job_context_property, + update_job_context_property, + delete_all_job_context_properties, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -530,6 +538,13 @@ "create_integration_job_instance", "update_integration_job_instance", "run_integration_job_instance_on_demand", + # Job Context Properties + "list_job_context_properties", + "get_job_context_property", + "delete_job_context_property", + "create_job_context_property", + "update_job_context_property", + "delete_all_job_context_properties", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index f0c286e0..fa10cf5b 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -211,6 +211,14 @@ run_integration_job_instance_on_demand as _run_integration_job_instance_on_demand, update_integration_job_instance as _update_integration_job_instance, ) +from secops.chronicle.integration.job_context_properties import ( + create_job_context_property as _create_job_context_property, + delete_all_job_context_properties as _delete_all_job_context_properties, + delete_job_context_property as _delete_job_context_property, + get_job_context_property as _get_job_context_property, + list_job_context_properties as _list_job_context_properties, + update_job_context_property as _update_job_context_property, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -3280,6 +3288,250 @@ def run_integration_job_instance_on_demand( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Job Context Properties methods + # ------------------------------------------------------------------------- + + def list_job_context_properties( + self, + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration job. + + Use this method to discover all custom data points associated + with a job. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to list context properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter context + properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of context properties + instead of a dict with context properties list and + nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_job_context_properties( + self, + integration_name, + job_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_job_context_property( + self, + integration_name: str, + job_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single context property for a specific integration + job. + + Use this method to retrieve the value of a specific key within + a job's context. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return _get_job_context_property( + self, + integration_name, + job_id, + context_property_id, + api_version=api_version, + ) + + def delete_job_context_property( + self, + integration_name: str, + job_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific context property for a given integration + job. + + Use this method to remove a custom data point that is no longer + relevant to the job's context. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_job_context_property( + self, + integration_name, + job_id, + context_property_id, + api_version=api_version, + ) + + def create_job_context_property( + self, + integration_name: str, + job_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new context property for a specific integration + job. + + Use this method to attach custom data to a job's context. + Property keys must be unique within their context. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to create the context property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 + characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _create_job_context_property( + self, + integration_name, + job_id, + value, + key=key, + api_version=api_version, + ) + + def update_job_context_property( + self, + integration_name: str, + job_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing context property for a given integration + job. + + Use this method to modify the value of a previously saved key. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only + "value" is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _update_job_context_property( + self, + integration_name, + job_id, + context_property_id, + value, + update_mask=update_mask, + api_version=api_version, + ) + + def delete_all_job_context_properties( + self, + integration_name: str, + job_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete all context properties for a specific integration + job. + + Use this method to quickly clear all supplemental data from a + job's context. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job to clear context properties from. + context_id: The context ID to remove context properties + from. Must be 4-63 characters and match /[a-z][0-9]-/. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_all_job_context_properties( + self, + integration_name, + job_id, + context_id=context_id, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/actions.py b/src/secops/chronicle/integration/actions.py index 6482c0db..d52ba28b 100644 --- a/src/secops/chronicle/integration/actions.py +++ b/src/secops/chronicle/integration/actions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration actions functionality for Chronicle.""" +"""Integration actions functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/connectors.py b/src/secops/chronicle/integration/connectors.py index 7cae92d0..3978ce0d 100644 --- a/src/secops/chronicle/integration/connectors.py +++ b/src/secops/chronicle/integration/connectors.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration connectors functionality for Chronicle.""" +"""Integration connectors functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/job_context_properties.py b/src/secops/chronicle/integration/job_context_properties.py new file mode 100644 index 00000000..de40a6f8 --- /dev/null +++ b/src/secops/chronicle/integration/job_context_properties.py @@ -0,0 +1,298 @@ +# Copyright 2025 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. +# +"""Integration job context property functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_job_context_properties( + client: "ChronicleClient", + integration_name: str, + job_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration job. + + Use this method to discover all custom data points associated with a job. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to list context properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter context properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of context properties instead of a + dict with context properties list and nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties" + ), + items_key="contextProperties", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_job_context_property( + client: "ChronicleClient", + integration_name: str, + job_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single context property for a specific integration job. + + Use this method to retrieve the value of a specific key within a job's + context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def delete_job_context_property( + client: "ChronicleClient", + integration_name: str, + job_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific context property for a given integration job. + + Use this method to remove a custom data point that is no longer relevant + to the job's context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def create_job_context_property( + client: "ChronicleClient", + integration_name: str, + job_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new context property for a specific integration job. + + Use this method to attach custom data to a job's context. Property keys + must be unique within their context. Key values must be 4-63 characters + and match /[a-z][0-9]-/. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to create the context property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 characters and + match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body = {"value": value} + + if key is not None: + body["key"] = key + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties" + ), + api_version=api_version, + json=body, + ) + + +def update_job_context_property( + client: "ChronicleClient", + integration_name: str, + job_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing context property for a given integration job. + + Use this method to modify the value of a previously saved key. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the context property belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only "value" + is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body, params = build_patch_body( + field_map=[ + ("value", "value", value), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def delete_all_job_context_properties( + client: "ChronicleClient", + integration_name: str, + job_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete all context properties for a specific integration job. + + Use this method to quickly clear all supplemental data from a job's + context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job to clear context properties from. + context_id: The context ID to remove context properties from. Must be + 4-63 characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + body = {} + + if context_id is not None: + body["contextId"] = context_id + + chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/contextProperties:clearAll" + ), + api_version=api_version, + json=body, + ) diff --git a/src/secops/chronicle/integration/job_instances.py b/src/secops/chronicle/integration/job_instances.py index 4544a271..6d9f8fc0 100644 --- a/src/secops/chronicle/integration/job_instances.py +++ b/src/secops/chronicle/integration/job_instances.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration job instances functionality for Chronicle.""" +"""Integration job instances functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/job_revisions.py b/src/secops/chronicle/integration/job_revisions.py index 6c62b989..391daacb 100644 --- a/src/secops/chronicle/integration/job_revisions.py +++ b/src/secops/chronicle/integration/job_revisions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration job revisions functionality for Chronicle.""" +"""Integration job revisions functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/jobs.py b/src/secops/chronicle/integration/jobs.py index 6d122f8d..cbcbc410 100644 --- a/src/secops/chronicle/integration/jobs.py +++ b/src/secops/chronicle/integration/jobs.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration jobs functionality for Chronicle.""" +"""Integration jobs functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/manager_revisions.py b/src/secops/chronicle/integration/manager_revisions.py index 614232b6..644a8490 100644 --- a/src/secops/chronicle/integration/manager_revisions.py +++ b/src/secops/chronicle/integration/manager_revisions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration manager revisions functionality for Chronicle.""" +"""Integration manager revisions functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/managers.py b/src/secops/chronicle/integration/managers.py index dcdcce46..ced5b199 100644 --- a/src/secops/chronicle/integration/managers.py +++ b/src/secops/chronicle/integration/managers.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integration manager functionality for Chronicle.""" +"""Integration manager functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/marketplace_integrations.py b/src/secops/chronicle/integration/marketplace_integrations.py index 2cd3fe75..fb9006cc 100644 --- a/src/secops/chronicle/integration/marketplace_integrations.py +++ b/src/secops/chronicle/integration/marketplace_integrations.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Marketplace integrations functionality for Chronicle.""" +"""Integrations functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/tests/chronicle/integration/test_job_context_properties.py b/tests/chronicle/integration/test_job_context_properties.py new file mode 100644 index 00000000..5fdce61c --- /dev/null +++ b/tests/chronicle/integration/test_job_context_properties.py @@ -0,0 +1,506 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration job context properties functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.job_context_properties import ( + list_job_context_properties, + get_job_context_property, + delete_job_context_property, + create_job_context_property, + update_job_context_property, + delete_all_job_context_properties, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_job_context_properties tests -- + + +def test_list_job_context_properties_success(chronicle_client): + """Test list_job_context_properties delegates to paginated request.""" + expected = { + "contextProperties": [{"key": "prop1"}, {"key": "prop2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.job_context_properties.format_resource_id", + return_value="My Integration", + ): + result = list_job_context_properties( + chronicle_client, + integration_name="My Integration", + job_id="j1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "jobs/j1/contextProperties" in kwargs["path"] + assert kwargs["items_key"] == "contextProperties" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_job_context_properties_default_args(chronicle_client): + """Test list_job_context_properties with default args.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + return_value=expected, + ): + result = list_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + assert result == expected + + +def test_list_job_context_properties_with_filters(chronicle_client): + """Test list_job_context_properties with filter and order_by.""" + expected = {"contextProperties": [{"key": "prop1"}]} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + filter_string='key = "prop1"', + order_by="key", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'key = "prop1"', + "orderBy": "key", + } + + +def test_list_job_context_properties_as_list(chronicle_client): + """Test list_job_context_properties returns list when as_list=True.""" + expected = [{"key": "prop1"}, {"key": "prop2"}] + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_job_context_properties_error(chronicle_client): + """Test list_job_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + side_effect=APIError("Failed to list context properties"), + ): + with pytest.raises(APIError) as exc_info: + list_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to list context properties" in str(exc_info.value) + + +# -- get_job_context_property tests -- + + +def test_get_job_context_property_success(chronicle_client): + """Test get_job_context_property issues GET request.""" + expected = { + "name": "contextProperties/prop1", + "key": "prop1", + "value": "test-value", + } + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "jobs/j1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_job_context_property_error(chronicle_client): + """Test get_job_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + side_effect=APIError("Failed to get context property"), + ): + with pytest.raises(APIError) as exc_info: + get_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + ) + assert "Failed to get context property" in str(exc_info.value) + + +# -- delete_job_context_property tests -- + + +def test_delete_job_context_property_success(chronicle_client): + """Test delete_job_context_property issues DELETE request.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "jobs/j1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_job_context_property_error(chronicle_client): + """Test delete_job_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + side_effect=APIError("Failed to delete context property"), + ): + with pytest.raises(APIError) as exc_info: + delete_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + ) + assert "Failed to delete context property" in str(exc_info.value) + + +# -- create_job_context_property tests -- + + +def test_create_job_context_property_value_only(chronicle_client): + """Test create_job_context_property with value only.""" + expected = {"name": "contextProperties/new", "value": "test-value"} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + value="test-value", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/jobs/j1/contextProperties" + ), + api_version=APIVersion.V1BETA, + json={"value": "test-value"}, + ) + + +def test_create_job_context_property_with_key(chronicle_client): + """Test create_job_context_property with key specified.""" + expected = {"name": "contextProperties/mykey", "value": "test-value"} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + value="test-value", + key="mykey", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["value"] == "test-value" + assert kwargs["json"]["key"] == "mykey" + + +def test_create_job_context_property_error(chronicle_client): + """Test create_job_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + side_effect=APIError("Failed to create context property"), + ): + with pytest.raises(APIError) as exc_info: + create_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + value="test-value", + ) + assert "Failed to create context property" in str(exc_info.value) + + +# -- update_job_context_property tests -- + + +def test_update_job_context_property_success(chronicle_client): + """Test update_job_context_property issues PATCH request.""" + expected = {"name": "contextProperties/prop1", "value": "updated-value"} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_context_properties.build_patch_body", + return_value=( + {"value": "updated-value"}, + {"updateMask": "value"}, + ), + ): + result = update_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + value="updated-value", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path=( + "integrations/test-integration/jobs/j1/contextProperties/prop1" + ), + api_version=APIVersion.V1BETA, + json={"value": "updated-value"}, + params={"updateMask": "value"}, + ) + + +def test_update_job_context_property_with_update_mask(chronicle_client): + """Test update_job_context_property with explicit update_mask.""" + expected = {"name": "contextProperties/prop1", "value": "updated-value"} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.job_context_properties.build_patch_body", + return_value=( + {"value": "updated-value"}, + {"updateMask": "value"}, + ), + ): + result = update_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + value="updated-value", + update_mask="value", + ) + + assert result == expected + + +def test_update_job_context_property_error(chronicle_client): + """Test update_job_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + side_effect=APIError("Failed to update context property"), + ), patch( + "secops.chronicle.integration.job_context_properties.build_patch_body", + return_value=({"value": "updated"}, {"updateMask": "value"}), + ): + with pytest.raises(APIError) as exc_info: + update_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + value="updated", + ) + assert "Failed to update context property" in str(exc_info.value) + + +# -- delete_all_job_context_properties tests -- + + +def test_delete_all_job_context_properties_success(chronicle_client): + """Test delete_all_job_context_properties issues POST request.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/" + "jobs/j1/contextProperties:clearAll" + ), + api_version=APIVersion.V1BETA, + json={}, + ) + + +def test_delete_all_job_context_properties_with_context_id(chronicle_client): + """Test delete_all_job_context_properties with context_id specified.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_id="mycontext", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "contextProperties:clearAll" in kwargs["endpoint_path"] + assert kwargs["json"]["contextId"] == "mycontext" + + +def test_delete_all_job_context_properties_error(chronicle_client): + """Test delete_all_job_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + side_effect=APIError("Failed to delete all context properties"), + ): + with pytest.raises(APIError) as exc_info: + delete_all_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + ) + assert "Failed to delete all context properties" in str( + exc_info.value + ) + + +# -- API version tests -- + + +def test_list_job_context_properties_custom_api_version(chronicle_client): + """Test list_job_context_properties with custom API version.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_context_properties( + chronicle_client, + integration_name="test-integration", + job_id="j1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_job_context_property_custom_api_version(chronicle_client): + """Test get_job_context_property with custom API version.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.job_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_job_context_property( + chronicle_client, + integration_name="test-integration", + job_id="j1", + context_property_id="prop1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + diff --git a/tests/chronicle/integration/test_job_instances.py b/tests/chronicle/integration/test_job_instances.py index b64adff4..3e8ca386 100644 --- a/tests/chronicle/integration/test_job_instances.py +++ b/tests/chronicle/integration/test_job_instances.py @@ -312,7 +312,7 @@ def test_create_integration_job_instance_with_optional_fields(chronicle_client): _, kwargs = mock_request.call_args assert kwargs["json"]["description"] == "Test job instance" - assert kwargs["json"]["parameters"] == [{"value": "test"}] + assert kwargs["json"]["parameters"] == [{"id": 1, "value": "test"}] assert kwargs["json"]["agent"] == "agent-123" @@ -641,7 +641,7 @@ def test_run_integration_job_instance_on_demand_with_params(chronicle_client): assert result == expected _, kwargs = mock_request.call_args - assert kwargs["json"]["parameters"] == [{"value": "override"}] + assert kwargs["json"]["parameters"] == [{"id": 1, "value": "override"}] def test_run_integration_job_instance_on_demand_with_dataclass(chronicle_client): From 584d2b9532ff6f6d6f906fb6a8bdccc821bcd097 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Sun, 8 Mar 2026 20:51:16 +0000 Subject: [PATCH 31/46] feat: add functions for integration job instance logs --- README.md | 96 +++++++ api_module_mapping.md | 8 +- src/secops/chronicle/__init__.py | 7 + src/secops/chronicle/client.py | 98 +++++++ .../integration/job_instance_logs.py | 127 +++++++++ .../integration/test_job_instance_logs.py | 256 ++++++++++++++++++ 6 files changed, 590 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/job_instance_logs.py create mode 100644 tests/chronicle/integration/test_job_instance_logs.py diff --git a/README.md b/README.md index 077e2994..c99c9f39 100644 --- a/README.md +++ b/README.md @@ -2978,6 +2978,102 @@ chronicle.delete_all_job_context_properties( ) ``` +### Job Instance Logs + +List all execution logs for a job instance: + +```python +# Get all logs for a job instance +logs = chronicle.list_job_instance_logs( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1" +) +for log in logs.get("logs", []): + print(f"Log ID: {log.get('name')}, Status: {log.get('status')}") + print(f"Start: {log.get('startTime')}, End: {log.get('endTime')}") + +# Get all logs as a list +logs = chronicle.list_job_instance_logs( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + as_list=True +) + +# Filter logs by status +logs = chronicle.list_job_instance_logs( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + filter_string="status = SUCCESS", + order_by="startTime desc" +) +``` + +Get a specific log entry: + +```python +log_entry = chronicle.get_job_instance_log( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + log_id="log123" +) +print(f"Status: {log_entry.get('status')}") +print(f"Start Time: {log_entry.get('startTime')}") +print(f"End Time: {log_entry.get('endTime')}") +print(f"Output: {log_entry.get('output')}") +``` + +Browse historical execution logs to monitor job performance: + +```python +# Get recent logs for monitoring +recent_logs = chronicle.list_job_instance_logs( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + order_by="startTime desc", + page_size=10, + as_list=True +) + +# Check for failures +for log in recent_logs: + if log.get("status") == "FAILED": + print(f"Failed execution at {log.get('startTime')}") + log_details = chronicle.get_job_instance_log( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + log_id=log.get("name").split("/")[-1] + ) + print(f"Error output: {log_details.get('output')}") +``` + +Monitor job reliability and performance: + +```python +# Get all logs to calculate success rate +all_logs = chronicle.list_job_instance_logs( + integration_name="MyIntegration", + job_id="456", + job_instance_id="ji1", + as_list=True +) + +successful = sum(1 for log in all_logs if log.get("status") == "SUCCESS") +failed = sum(1 for log in all_logs if log.get("status") == "FAILED") +total = len(all_logs) + +if total > 0: + success_rate = (successful / total) * 100 + print(f"Success Rate: {success_rate:.2f}%") + print(f"Total Executions: {total}") + print(f"Successful: {successful}, Failed: {failed}") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 71275878..882a4136 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 54 endpoints implemented -- **v1alpha:** 147 endpoints implemented +- **v1beta:** 56 endpoints implemented +- **v1alpha:** 149 endpoints implemented ## Endpoint Mapping @@ -132,6 +132,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.contextProperties.get | v1beta | chronicle.integration.job_context_properties.get_job_context_property | | | integrations.jobs.contextProperties.list | v1beta | chronicle.integration.job_context_properties.list_job_context_properties | | | integrations.jobs.contextProperties.patch | v1beta | chronicle.integration.job_context_properties.update_job_context_property | | +| integrations.jobs.jobInstances.logs.get | v1beta | chronicle.integration.job_instance_logs.get_job_instance_log | | +| integrations.jobs.jobInstances.logs.list | v1beta | chronicle.integration.job_instance_logs.list_job_instance_logs | | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -376,6 +378,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.jobs.contextProperties.get | v1alpha | chronicle.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.contextProperties.list | v1alpha | chronicle.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.contextProperties.patch | v1alpha | chronicle.integration.job_context_properties.update_job_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.logs.get | v1alpha | chronicle.integration.job_instance_logs.get_job_instance_log(api_version=APIVersion.V1ALPHA) | | +| integrations.jobs.jobInstances.logs.list | v1alpha | chronicle.integration.job_instance_logs.list_job_instance_logs(api_version=APIVersion.V1ALPHA) | | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index d6b9fa1f..f075d8f1 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -291,6 +291,10 @@ update_job_context_property, delete_all_job_context_properties, ) +from secops.chronicle.integration.job_instance_logs import ( + list_job_instance_logs, + get_job_instance_log, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -545,6 +549,9 @@ "create_job_context_property", "update_job_context_property", "delete_all_job_context_properties", + # Job Instance Logs + "list_job_instance_logs", + "get_job_instance_log", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index fa10cf5b..90e4f26f 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -219,6 +219,10 @@ list_job_context_properties as _list_job_context_properties, update_job_context_property as _update_job_context_property, ) +from secops.chronicle.integration.job_instance_logs import ( + get_job_instance_log as _get_job_instance_log, + list_job_instance_logs as _list_job_instance_logs, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -3532,6 +3536,100 @@ def delete_all_job_context_properties( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Job Instance Logs methods + # ------------------------------------------------------------------------- + + def list_job_instance_logs( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all execution logs for a specific job instance. + + Use this method to browse the historical performance and + reliability of a background automation schedule. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to list logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of logs instead of a dict + with logs list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_job_instance_logs( + self, + integration_name, + job_id, + job_instance_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_job_instance_log( + self, + integration_name: str, + job_id: str, + job_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single log entry for a specific job instance. + + Use this method to retrieve the detailed output message, + start/end times, and final status of a specific background task + execution. + + Args: + integration_name: Name of the integration the job belongs + to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance the log belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified JobInstanceLog. + + Raises: + APIError: If the API request fails. + """ + return _get_job_instance_log( + self, + integration_name, + job_id, + job_instance_id, + log_id, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/job_instance_logs.py b/src/secops/chronicle/integration/job_instance_logs.py new file mode 100644 index 00000000..7e1d0ccd --- /dev/null +++ b/src/secops/chronicle/integration/job_instance_logs.py @@ -0,0 +1,127 @@ +# Copyright 2025 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. +# +"""Integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_job_instance_logs( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all execution logs for a specific job instance. + + Use this method to browse the historical performance and reliability of a + background automation schedule. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance to list logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of logs instead of a dict with logs + list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/jobInstances/{job_instance_id}/logs" + ), + items_key="logs", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_job_instance_log( + client: "ChronicleClient", + integration_name: str, + job_id: str, + job_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single log entry for a specific job instance. + + Use this method to retrieve the detailed output message, start/end times, + and final status of a specific background task execution. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the job belongs to. + job_id: ID of the job the instance belongs to. + job_instance_id: ID of the job instance the log belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified JobInstanceLog. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"jobs/{job_id}/jobInstances/{job_instance_id}/logs/{log_id}" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_job_instance_logs.py b/tests/chronicle/integration/test_job_instance_logs.py new file mode 100644 index 00000000..ad456e79 --- /dev/null +++ b/tests/chronicle/integration/test_job_instance_logs.py @@ -0,0 +1,256 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration job instance logs functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.job_instance_logs import ( + list_job_instance_logs, + get_job_instance_log, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_job_instance_logs tests -- + + +def test_list_job_instance_logs_success(chronicle_client): + """Test list_job_instance_logs delegates to paginated request.""" + expected = { + "logs": [{"name": "log1"}, {"name": "log2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.job_instance_logs.format_resource_id", + return_value="My Integration", + ): + result = list_job_instance_logs( + chronicle_client, + integration_name="My Integration", + job_id="j1", + job_instance_id="ji1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "jobs/j1/jobInstances/ji1/logs" in kwargs["path"] + assert kwargs["items_key"] == "logs" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_job_instance_logs_default_args(chronicle_client): + """Test list_job_instance_logs with default args.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + return_value=expected, + ): + result = list_job_instance_logs( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + + assert result == expected + + +def test_list_job_instance_logs_with_filters(chronicle_client): + """Test list_job_instance_logs with filter and order_by.""" + expected = {"logs": [{"name": "log1"}]} + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_instance_logs( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + filter_string="status = SUCCESS", + order_by="startTime desc", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "status = SUCCESS", + "orderBy": "startTime desc", + } + + +def test_list_job_instance_logs_as_list(chronicle_client): + """Test list_job_instance_logs returns list when as_list=True.""" + expected = [{"name": "log1"}, {"name": "log2"}] + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_instance_logs( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_job_instance_logs_error(chronicle_client): + """Test list_job_instance_logs raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + side_effect=APIError("Failed to list job instance logs"), + ): + with pytest.raises(APIError) as exc_info: + list_job_instance_logs( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + ) + assert "Failed to list job instance logs" in str(exc_info.value) + + +# -- get_job_instance_log tests -- + + +def test_get_job_instance_log_success(chronicle_client): + """Test get_job_instance_log issues GET request.""" + expected = { + "name": "logs/log1", + "status": "SUCCESS", + "startTime": "2026-03-08T10:00:00Z", + "endTime": "2026-03-08T10:05:00Z", + } + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_job_instance_log( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + log_id="log1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "jobs/j1/jobInstances/ji1/logs/log1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_job_instance_log_error(chronicle_client): + """Test get_job_instance_log raises APIError on failure.""" + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_request", + side_effect=APIError("Failed to get job instance log"), + ): + with pytest.raises(APIError) as exc_info: + get_job_instance_log( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + log_id="log1", + ) + assert "Failed to get job instance log" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_job_instance_logs_custom_api_version(chronicle_client): + """Test list_job_instance_logs with custom API version.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_job_instance_logs( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_job_instance_log_custom_api_version(chronicle_client): + """Test get_job_instance_log with custom API version.""" + expected = {"name": "logs/log1"} + + with patch( + "secops.chronicle.integration.job_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_job_instance_log( + chronicle_client, + integration_name="test-integration", + job_id="j1", + job_instance_id="ji1", + log_id="log1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From 6e9840931aff1fd46de5923816fc65410a831dd2 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 08:59:09 +0000 Subject: [PATCH 32/46] feat: add functions for integration instances --- README.md | 142 ++++ api_module_mapping.md | 20 +- src/secops/chronicle/__init__.py | 19 + src/secops/chronicle/client.py | 344 ++++++++++ .../integration/integration_instances.py | 403 +++++++++++ .../integration/job_instance_logs.py | 4 +- .../chronicle/integration/job_instances.py | 42 +- src/secops/chronicle/models.py | 43 ++ .../integration/test_integration_instances.py | 623 ++++++++++++++++++ 9 files changed, 1621 insertions(+), 19 deletions(-) create mode 100644 src/secops/chronicle/integration/integration_instances.py create mode 100644 tests/chronicle/integration/test_integration_instances.py diff --git a/README.md b/README.md index c99c9f39..7de3d163 100644 --- a/README.md +++ b/README.md @@ -3074,6 +3074,148 @@ if total > 0: print(f"Successful: {successful}, Failed: {failed}") ``` +### Integration Instances + +List all instances for a specific integration: + +```python +# Get all instances for an integration +instances = chronicle.list_integration_instances("MyIntegration") +for instance in instances.get("integrationInstances", []): + print(f"Instance: {instance.get('displayName')}, ID: {instance.get('name')}") + print(f"Environment: {instance.get('environment')}") + +# Get all instances as a list +instances = chronicle.list_integration_instances("MyIntegration", as_list=True) + +# Get instances for a specific environment +instances = chronicle.list_integration_instances( + "MyIntegration", + filter_string="environment = 'production'" +) +``` + +Get details of a specific integration instance: + +```python +instance = chronicle.get_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1" +) +print(f"Display Name: {instance.get('displayName')}") +print(f"Environment: {instance.get('environment')}") +print(f"Agent: {instance.get('agent')}") +``` + +Create a new integration instance: + +```python +from secops.chronicle.models import IntegrationInstanceParameter + +# Create instance with required fields only +new_instance = chronicle.create_integration_instance( + integration_name="MyIntegration", + environment="production" +) + +# Create instance with all fields +new_instance = chronicle.create_integration_instance( + integration_name="MyIntegration", + environment="production", + display_name="Production Instance", + description="Main production integration instance", + parameters=[ + IntegrationInstanceParameter( + value="api_key_value" + ), + IntegrationInstanceParameter( + value="https://api.example.com" + ) + ], + agent="agent-123" +) +``` + +Update an existing integration instance: + +```python +from secops.chronicle.models import IntegrationInstanceParameter + +# Update instance display name +updated_instance = chronicle.update_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", + display_name="Updated Production Instance" +) + +# Update multiple fields including parameters +updated_instance = chronicle.update_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", + display_name="Updated Instance", + description="Updated description", + environment="staging", + parameters=[ + IntegrationInstanceParameter( + value="new_api_key" + ) + ] +) + +# Use custom update mask +updated_instance = chronicle.update_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", + display_name="New Name", + update_mask="displayName" +) +``` + +Delete an integration instance: + +```python +chronicle.delete_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1" +) +``` + +Execute a connectivity test for an integration instance: + +```python +# Test if the instance can connect to the third-party service +test_result = chronicle.execute_integration_instance_test( + integration_name="MyIntegration", + integration_instance_id="ii1" +) +print(f"Test Successful: {test_result.get('successful')}") +print(f"Message: {test_result.get('message')}") +``` + +Get affected items (playbooks) that depend on an integration instance: + +```python +# Perform impact analysis before deleting or modifying an instance +affected_items = chronicle.get_integration_instance_affected_items( + integration_name="MyIntegration", + integration_instance_id="ii1" +) +for playbook in affected_items.get("affectedPlaybooks", []): + print(f"Playbook: {playbook.get('displayName')}") + print(f" ID: {playbook.get('name')}") +``` + +Get the default integration instance: + +```python +# Get the system default configuration for a commercial product +default_instance = chronicle.get_default_integration_instance( + integration_name="AWSSecurityHub" +) +print(f"Default Instance: {default_instance.get('displayName')}") +print(f"Environment: {default_instance.get('environment')}") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 882a4136..6275cb0b 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 56 endpoints implemented -- **v1alpha:** 149 endpoints implemented +- **v1beta:** 64 endpoints implemented +- **v1alpha:** 157 endpoints implemented ## Endpoint Mapping @@ -98,6 +98,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | | | integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | | | integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | | +| integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | +| integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | +| integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | +| integrations.integrationInstances.fetchAffectedItems | v1beta | chronicle.integration.integration_instances.get_integration_instance_affected_items | | +| integrations.integrationInstances.fetchDefaultInstance | v1beta | chronicle.integration.integration_instances.get_default_integration_instance | | +| integrations.integrationInstances.get | v1beta | chronicle.integration.integration_instances.get_integration_instance | | +| integrations.integrationInstances.list | v1beta | chronicle.integration.integration_instances.list_integration_instances | | +| integrations.integrationInstances.patch | v1beta | chronicle.integration.integration_instances.update_integration_instance | | | integrations.jobs.create | v1beta | chronicle.integration.jobs.create_integration_job | | | integrations.jobs.delete | v1beta | chronicle.integration.jobs.delete_integration_job | | | integrations.jobs.executeTest | v1beta | chronicle.integration.jobs.execute_integration_job_test | | @@ -344,6 +352,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.fetchAffectedItems | v1alpha | chronicle.integration.integration_instances.get_integration_instance_affected_items(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.fetchDefaultInstance | v1alpha | chronicle.integration.integration_instances.get_default_integration_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.get | v1alpha | chronicle.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.list | v1alpha | chronicle.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | | +| integrations.integrationInstances.patch | v1alpha | chronicle.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index f075d8f1..6d0dea52 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -295,6 +295,16 @@ list_job_instance_logs, get_job_instance_log, ) +from secops.chronicle.integration.integration_instances import ( + list_integration_instances, + get_integration_instance, + delete_integration_instance, + create_integration_instance, + update_integration_instance, + execute_integration_instance_test, + get_integration_instance_affected_items, + get_default_integration_instance, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -552,6 +562,15 @@ # Job Instance Logs "list_job_instance_logs", "get_job_instance_log", + # Integration Instances + "list_integration_instances", + "get_integration_instance", + "delete_integration_instance", + "create_integration_instance", + "update_integration_instance", + "execute_integration_instance_test", + "get_integration_instance_affected_items", + "get_default_integration_instance", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 90e4f26f..0ba7af80 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -223,6 +223,16 @@ get_job_instance_log as _get_job_instance_log, list_job_instance_logs as _list_job_instance_logs, ) +from secops.chronicle.integration.integration_instances import ( + create_integration_instance as _create_integration_instance, + delete_integration_instance as _delete_integration_instance, + execute_integration_instance_test as _execute_integration_instance_test, + get_default_integration_instance as _get_default_integration_instance, + get_integration_instance as _get_integration_instance, + get_integration_instance_affected_items as _get_integration_instance_affected_items, + list_integration_instances as _list_integration_instances, + update_integration_instance as _update_integration_instance, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -233,6 +243,7 @@ DiffType, EntitySummary, InputInterval, + IntegrationInstanceParameter, IntegrationType, JobParameter, PythonVersion, @@ -3630,6 +3641,339 @@ def get_job_instance_log( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Instances methods + # ------------------------------------------------------------------------- + + def list_integration_instances( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration. + + Use this method to browse the configured integration instances + available for a custom or third-party product across different + environments. + + Args: + integration_name: Name of the integration to list instances + for. + page_size: Maximum number of integration instances to + return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter integration + instances. + order_by: Field to sort the integration instances by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of integration instances + instead of a dict with integration instances list and + nextPageToken. + + Returns: + If as_list is True: List of integration instances. + If as_list is False: Dict with integration instances list + and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_instances( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_instance( + self, + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single instance for a specific integration. + + Use this method to retrieve the specific configuration, + connection status, and environment mapping for an active + integration. + + Args: + integration_name: Name of the integration the instance + belongs to. + integration_instance_id: ID of the integration instance to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified + IntegrationInstance. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_instance( + self, + integration_name, + integration_instance_id, + api_version=api_version, + ) + + def delete_integration_instance( + self, + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific integration instance. + + Use this method to permanently remove an integration instance + and stop all associated automated tasks (connectors or jobs) + using this instance. + + Args: + integration_name: Name of the integration the instance + belongs to. + integration_instance_id: ID of the integration instance to + delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_instance( + self, + integration_name, + integration_instance_id, + api_version=api_version, + ) + + def create_integration_instance( + self, + integration_name: str, + environment: str, + display_name: str | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationInstanceParameter] | None + ) = None, + agent: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new integration instance for a specific + integration. + + Use this method to establish a new integration instance to a + custom or third-party security product for a specific + environment. All mandatory parameters required by the + integration definition must be provided. + + Args: + integration_name: Name of the integration to create the + instance for. + environment: The integration instance environment. Required. + display_name: The display name of the integration instance. + Automatically generated if not provided. Maximum 110 + characters. + description: The integration instance description. Maximum + 1500 characters. + parameters: List of IntegrationInstanceParameter instances + or dicts. + agent: Agent identifier for a remote integration instance. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationInstance + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_instance( + self, + integration_name, + environment, + display_name=display_name, + description=description, + parameters=parameters, + agent=agent, + api_version=api_version, + ) + + def update_integration_instance( + self, + integration_name: str, + integration_instance_id: str, + environment: str | None = None, + display_name: str | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationInstanceParameter] | None + ) = None, + agent: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing integration instance. + + Use this method to modify connection parameters (e.g., rotate + an API key), change the display name, or update the description + of a configured integration instance. + + Args: + integration_name: Name of the integration the instance + belongs to. + integration_instance_id: ID of the integration instance to + update. + environment: The integration instance environment. + display_name: The display name of the integration instance. + Maximum 110 characters. + description: The integration instance description. Maximum + 1500 characters. + parameters: List of IntegrationInstanceParameter instances + or dicts. + agent: Agent identifier for a remote integration instance. + update_mask: Comma-separated list of fields to update. If + omitted, the mask is auto-generated from whichever + fields are provided. Example: + "displayName,description". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated IntegrationInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_instance( + self, + integration_name, + integration_instance_id, + environment=environment, + display_name=display_name, + description=description, + parameters=parameters, + agent=agent, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_instance_test( + self, + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Execute a connectivity test for a specific integration + instance. + + Use this method to verify that SecOps can successfully + communicate with the third-party security product using the + provided credentials. + + Args: + integration_name: Name of the integration the instance + belongs to. + integration_instance_id: ID of the integration instance to + test. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the test results with the following fields: + - successful: Indicates if the test was successful. + - message: Test result message (optional). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_instance_test( + self, + integration_name, + integration_instance_id, + api_version=api_version, + ) + + def get_integration_instance_affected_items( + self, + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """List all playbooks that depend on a specific integration + instance. + + Use this method to perform impact analysis before deleting or + significantly changing a connection configuration. + + Args: + integration_name: Name of the integration the instance + belongs to. + integration_instance_id: ID of the integration instance to + fetch affected items for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing a list of AffectedPlaybookResponse objects + that depend on the specified integration instance. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_instance_affected_items( + self, + integration_name, + integration_instance_id, + api_version=api_version, + ) + + def get_default_integration_instance( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get the system default configuration for a specific + integration. + + Use this method to retrieve the baseline integration instance + details provided for a commercial product. + + Args: + integration_name: Name of the integration to fetch the + default instance for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the default IntegrationInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _get_default_integration_instance( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/integration_instances.py b/src/secops/chronicle/integration/integration_instances.py new file mode 100644 index 00000000..c7e88dd7 --- /dev/null +++ b/src/secops/chronicle/integration/integration_instances.py @@ -0,0 +1,403 @@ +# Copyright 2025 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. +# +"""Integration instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion, IntegrationInstanceParameter +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_instances( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration. + + Use this method to browse the configured integration instances available + for a custom or third-party product across different environments. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list instances for. + page_size: Maximum number of integration instances to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter integration instances. + order_by: Field to sort the integration instances by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of integration instances instead of a + dict with integration instances list and nextPageToken. + + Returns: + If as_list is True: List of integration instances. + If as_list is False: Dict with integration instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances" + ), + items_key="integrationInstances", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_instance( + client: "ChronicleClient", + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single instance for a specific integration. + + Use this method to retrieve the specific configuration, connection status, + and environment mapping for an active integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the instance belongs to. + integration_instance_id: ID of the integration instance to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationInstance. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances/{integration_instance_id}" + ), + api_version=api_version, + ) + + +def delete_integration_instance( + client: "ChronicleClient", + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific integration instance. + + Use this method to permanently remove an integration instance and stop all + associated automated tasks (connectors or jobs) using this instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the instance belongs to. + integration_instance_id: ID of the integration instance to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances/{integration_instance_id}" + ), + api_version=api_version, + ) + + +def create_integration_instance( + client: "ChronicleClient", + integration_name: str, + environment: str, + display_name: str | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationInstanceParameter] | None + ) = None, + agent: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new integration instance for a specific integration. + + Use this method to establish a new integration instance to a custom or + third-party security product for a specific environment. All mandatory + parameters required by the integration definition must be provided. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the instance for. + environment: The integration instance environment. Required. + display_name: The display name of the integration instance. + Automatically generated if not provided. Maximum 110 characters. + description: The integration instance description. Maximum 1500 + characters. + parameters: List of IntegrationInstanceParameter instances or dicts. + agent: Agent identifier for a remote integration instance. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, IntegrationInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body = { + "environment": environment, + "displayName": display_name, + "description": description, + "parameters": resolved_parameters, + "agent": agent, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_instance( + client: "ChronicleClient", + integration_name: str, + integration_instance_id: str, + environment: str | None = None, + display_name: str | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationInstanceParameter] | None + ) = None, + agent: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing integration instance. + + Use this method to modify connection parameters (e.g., rotate an API + key), change the display name, or update the description of a configured + integration instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the instance belongs to. + integration_instance_id: ID of the integration instance to update. + environment: The integration instance environment. + display_name: The display name of the integration instance. Maximum + 110 characters. + description: The integration instance description. Maximum 1500 + characters. + parameters: List of IntegrationInstanceParameter instances or dicts. + agent: Agent identifier for a remote integration instance. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,description". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, IntegrationInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("environment", "environment", environment), + ("displayName", "displayName", display_name), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ("agent", "agent", agent), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances/{integration_instance_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_instance_test( + client: "ChronicleClient", + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Execute a connectivity test for a specific integration instance. + + Use this method to verify that SecOps can successfully communicate with + the third-party security product using the provided credentials. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the instance belongs to. + integration_instance_id: ID of the integration instance to test. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the test results with the following fields: + - successful: Indicates if the test was successful. + - message: Test result message (optional). + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances/{integration_instance_id}:executeTest" + ), + api_version=api_version, + ) + + +def get_integration_instance_affected_items( + client: "ChronicleClient", + integration_name: str, + integration_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """List all playbooks that depend on a specific integration instance. + + Use this method to perform impact analysis before deleting or + significantly changing a connection configuration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the instance belongs to. + integration_instance_id: ID of the integration instance to fetch + affected items for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing a list of AffectedPlaybookResponse objects that + depend on the specified integration instance. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances/{integration_instance_id}:fetchAffectedItems" + ), + api_version=api_version, + ) + + +def get_default_integration_instance( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get the system default configuration for a specific integration. + + Use this method to retrieve the baseline integration instance details + provided for a commercial product. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the default + instance for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the default IntegrationInstance resource. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"integrationInstances:fetchDefaultInstance" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/job_instance_logs.py b/src/secops/chronicle/integration/job_instance_logs.py index 7e1d0ccd..f58d568f 100644 --- a/src/secops/chronicle/integration/job_instance_logs.py +++ b/src/secops/chronicle/integration/job_instance_logs.py @@ -17,9 +17,7 @@ from typing import Any, TYPE_CHECKING from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import ( - format_resource_id -) +from secops.chronicle.utils.format_utils import format_resource_id from secops.chronicle.utils.request_utils import ( chronicle_paginated_request, chronicle_request, diff --git a/src/secops/chronicle/integration/job_instances.py b/src/secops/chronicle/integration/job_instances.py index 6d9f8fc0..c64705ee 100644 --- a/src/secops/chronicle/integration/job_instances.py +++ b/src/secops/chronicle/integration/job_instances.py @@ -19,7 +19,7 @@ from secops.chronicle.models import ( APIVersion, AdvancedConfig, - IntegrationJobInstanceParameter + IntegrationJobInstanceParameter, ) from secops.chronicle.utils.format_utils import ( format_resource_id, @@ -163,7 +163,7 @@ def delete_integration_job_instance( ) -#pylint: disable=line-too-long +# pylint: disable=line-too-long def create_integration_job_instance( client: "ChronicleClient", integration_name: str, @@ -173,12 +173,14 @@ def create_integration_job_instance( enabled: bool, advanced: bool, description: str | None = None, - parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + parameters: ( + list[dict[str, Any] | IntegrationJobInstanceParameter] | None + ) = None, advanced_config: dict[str, Any] | AdvancedConfig | None = None, agent: str | None = None, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - #pylint: enable=line-too-long + # pylint: enable=line-too-long """Create a new job instance for a specific integration job. Use this method to schedule a new recurring background job. You must @@ -209,8 +211,10 @@ def create_integration_job_instance( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters] + [ + p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters + ] if parameters is not None else None ) @@ -245,7 +249,8 @@ def create_integration_job_instance( json=body, ) -#pylint: disable=line-too-long + +# pylint: disable=line-too-long def update_integration_job_instance( client: "ChronicleClient", integration_name: str, @@ -256,7 +261,9 @@ def update_integration_job_instance( enabled: bool | None = None, advanced: bool | None = None, description: str | None = None, - parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + parameters: ( + list[dict[str, Any] | IntegrationJobInstanceParameter] | None + ) = None, advanced_config: dict[str, Any] | AdvancedConfig | None = None, update_mask: str | None = None, api_version: APIVersion | None = APIVersion.V1BETA, @@ -295,8 +302,10 @@ def update_integration_job_instance( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters] + [ + p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters + ] if parameters is not None else None ) @@ -331,13 +340,16 @@ def update_integration_job_instance( params=params, ) -#pylint: disable=line-too-long + +# pylint: disable=line-too-long def run_integration_job_instance_on_demand( client: "ChronicleClient", integration_name: str, job_id: str, job_instance_id: str, - parameters: list[dict[str, Any] | IntegrationJobInstanceParameter] | None = None, + parameters: ( + list[dict[str, Any] | IntegrationJobInstanceParameter] | None + ) = None, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: # pylint: enable=line-too-long @@ -363,8 +375,10 @@ def run_integration_job_instance_on_demand( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters] + [ + p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p + for p in parameters + ] if parameters is not None else None ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index f7f342d9..b52f729a 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -568,6 +568,49 @@ def to_dict(self) -> dict: return data +class IntegrationParameterType(str, Enum): + """Parameter types for Chronicle SOAR integration instances.""" + + UNSPECIFIED = "INTEGRATION_PARAMETER_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + + +@dataclass +class IntegrationInstanceParameter: + """A parameter instance for a Chronicle SOAR integration instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring an integration instance. + + Attributes: + value: The parameter's value. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/tests/chronicle/integration/test_integration_instances.py b/tests/chronicle/integration/test_integration_instances.py new file mode 100644 index 00000000..153390ad --- /dev/null +++ b/tests/chronicle/integration/test_integration_instances.py @@ -0,0 +1,623 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration instances functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + IntegrationInstanceParameter, +) +from secops.chronicle.integration.integration_instances import ( + list_integration_instances, + get_integration_instance, + delete_integration_instance, + create_integration_instance, + update_integration_instance, + execute_integration_instance_test, + get_integration_instance_affected_items, + get_default_integration_instance, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_instances tests -- + + +def test_list_integration_instances_success(chronicle_client): + """Test list_integration_instances delegates to chronicle_paginated_request.""" + expected = { + "integrationInstances": [{"name": "ii1"}, {"name": "ii2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.integration_instances.format_resource_id", + return_value="My Integration", + ): + result = list_integration_instances( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "integrationInstances" in kwargs["path"] + assert kwargs["items_key"] == "integrationInstances" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_instances_default_args(chronicle_client): + """Test list_integration_instances with default args.""" + expected = {"integrationInstances": []} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + return_value=expected, + ): + result = list_integration_instances( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + +def test_list_integration_instances_with_filters(chronicle_client): + """Test list_integration_instances with filter and order_by.""" + expected = {"integrationInstances": [{"name": "ii1"}]} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_instances( + chronicle_client, + integration_name="test-integration", + filter_string="environment = 'prod'", + order_by="displayName", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "environment = 'prod'", + "orderBy": "displayName", + } + + +def test_list_integration_instances_as_list(chronicle_client): + """Test list_integration_instances returns list when as_list=True.""" + expected = [{"name": "ii1"}, {"name": "ii2"}] + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_instances( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_instances_error(chronicle_client): + """Test list_integration_instances raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + side_effect=APIError("Failed to list integration instances"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_instances( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to list integration instances" in str(exc_info.value) + + +# -- get_integration_instance tests -- + + +def test_get_integration_instance_success(chronicle_client): + """Test get_integration_instance issues GET request.""" + expected = { + "name": "integrationInstances/ii1", + "displayName": "My Instance", + "environment": "production", + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "integrationInstances/ii1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_integration_instance_error(chronicle_client): + """Test get_integration_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to get integration instance"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + assert "Failed to get integration instance" in str(exc_info.value) + + +# -- delete_integration_instance tests -- + + +def test_delete_integration_instance_success(chronicle_client): + """Test delete_integration_instance issues DELETE request.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "integrationInstances/ii1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_instance_error(chronicle_client): + """Test delete_integration_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to delete integration instance"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + assert "Failed to delete integration instance" in str(exc_info.value) + + +# -- create_integration_instance tests -- + + +def test_create_integration_instance_required_fields_only(chronicle_client): + """Test create_integration_instance sends only required fields.""" + expected = {"name": "integrationInstances/new", "displayName": "My Instance"} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_instance( + chronicle_client, + integration_name="test-integration", + environment="production", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/integrationInstances", + api_version=APIVersion.V1BETA, + json={ + "environment": "production", + }, + ) + + +def test_create_integration_instance_with_optional_fields(chronicle_client): + """Test create_integration_instance includes optional fields when provided.""" + expected = {"name": "integrationInstances/new"} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_instance( + chronicle_client, + integration_name="test-integration", + environment="production", + display_name="My Instance", + description="Test instance", + parameters=[{"id": 1, "value": "test"}], + agent="agent-123", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["environment"] == "production" + assert kwargs["json"]["displayName"] == "My Instance" + assert kwargs["json"]["description"] == "Test instance" + assert kwargs["json"]["parameters"] == [{"id": 1, "value": "test"}] + assert kwargs["json"]["agent"] == "agent-123" + + +def test_create_integration_instance_with_dataclass_params(chronicle_client): + """Test create_integration_instance converts dataclass parameters.""" + expected = {"name": "integrationInstances/new"} + + param = IntegrationInstanceParameter(value="test-value") + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_instance( + chronicle_client, + integration_name="test-integration", + environment="production", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["value"] == "test-value" + + +def test_create_integration_instance_error(chronicle_client): + """Test create_integration_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to create integration instance"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_instance( + chronicle_client, + integration_name="test-integration", + environment="production", + ) + assert "Failed to create integration instance" in str(exc_info.value) + + +# -- update_integration_instance tests -- + + +def test_update_integration_instance_with_single_field(chronicle_client): + """Test update_integration_instance with single field updates updateMask.""" + expected = {"name": "integrationInstances/ii1", "displayName": "Updated"} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + display_name="Updated", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "PATCH" + assert "integrationInstances/ii1" in kwargs["endpoint_path"] + assert kwargs["json"]["displayName"] == "Updated" + assert kwargs["params"]["updateMask"] == "displayName" + + +def test_update_integration_instance_with_multiple_fields(chronicle_client): + """Test update_integration_instance with multiple fields.""" + expected = {"name": "integrationInstances/ii1"} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + display_name="Updated", + description="New description", + environment="staging", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["displayName"] == "Updated" + assert kwargs["json"]["description"] == "New description" + assert kwargs["json"]["environment"] == "staging" + assert "displayName" in kwargs["params"]["updateMask"] + assert "description" in kwargs["params"]["updateMask"] + assert "environment" in kwargs["params"]["updateMask"] + + +def test_update_integration_instance_with_custom_update_mask(chronicle_client): + """Test update_integration_instance with explicitly provided update_mask.""" + expected = {"name": "integrationInstances/ii1"} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + display_name="Updated", + update_mask="displayName,environment", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["params"]["updateMask"] == "displayName,environment" + + +def test_update_integration_instance_with_dataclass_params(chronicle_client): + """Test update_integration_instance converts dataclass parameters.""" + expected = {"name": "integrationInstances/ii1"} + + param = IntegrationInstanceParameter(value="test-value") + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["value"] == "test-value" + + +def test_update_integration_instance_error(chronicle_client): + """Test update_integration_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to update integration instance"), + ): + with pytest.raises(APIError) as exc_info: + update_integration_instance( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + display_name="Updated", + ) + assert "Failed to update integration instance" in str(exc_info.value) + + +# -- execute_integration_instance_test tests -- + + +def test_execute_integration_instance_test_success(chronicle_client): + """Test execute_integration_instance_test issues POST request.""" + expected = { + "successful": True, + "message": "Test successful", + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_instance_test( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "integrationInstances/ii1:executeTest" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_execute_integration_instance_test_failure(chronicle_client): + """Test execute_integration_instance_test when test fails.""" + expected = { + "successful": False, + "message": "Connection failed", + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ): + result = execute_integration_instance_test( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + assert result == expected + assert result["successful"] is False + + +def test_execute_integration_instance_test_error(chronicle_client): + """Test execute_integration_instance_test raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to execute test"), + ): + with pytest.raises(APIError) as exc_info: + execute_integration_instance_test( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + assert "Failed to execute test" in str(exc_info.value) + + +# -- get_integration_instance_affected_items tests -- + + +def test_get_integration_instance_affected_items_success(chronicle_client): + """Test get_integration_instance_affected_items issues GET request.""" + expected = { + "affectedPlaybooks": [ + {"name": "playbook1", "displayName": "Playbook 1"}, + {"name": "playbook2", "displayName": "Playbook 2"}, + ] + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_instance_affected_items( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "integrationInstances/ii1:fetchAffectedItems" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_integration_instance_affected_items_empty(chronicle_client): + """Test get_integration_instance_affected_items with no affected items.""" + expected = {"affectedPlaybooks": []} + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ): + result = get_integration_instance_affected_items( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + + assert result == expected + assert len(result["affectedPlaybooks"]) == 0 + + +def test_get_integration_instance_affected_items_error(chronicle_client): + """Test get_integration_instance_affected_items raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to fetch affected items"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_instance_affected_items( + chronicle_client, + integration_name="test-integration", + integration_instance_id="ii1", + ) + assert "Failed to fetch affected items" in str(exc_info.value) + + +# -- get_default_integration_instance tests -- + + +def test_get_default_integration_instance_success(chronicle_client): + """Test get_default_integration_instance issues GET request.""" + expected = { + "name": "integrationInstances/default", + "displayName": "Default Instance", + "environment": "default", + } + + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_default_integration_instance( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "integrationInstances:fetchDefaultInstance" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_default_integration_instance_error(chronicle_client): + """Test get_default_integration_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.integration_instances.chronicle_request", + side_effect=APIError("Failed to get default instance"), + ): + with pytest.raises(APIError) as exc_info: + get_default_integration_instance( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to get default instance" in str(exc_info.value) + From 4967db366cd1c15c995ae330e35f0d4f2d2939c1 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 09:15:01 +0000 Subject: [PATCH 33/46] feat: add functions for integration connector revisions --- README.md | 128 ++++++ api_module_mapping.md | 12 +- src/secops/chronicle/__init__.py | 11 + src/secops/chronicle/client.py | 171 ++++++++ .../integration/connector_revisions.py | 202 +++++++++ .../integration/test_connector_revisions.py | 385 ++++++++++++++++++ 6 files changed, 907 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/connector_revisions.py create mode 100644 tests/chronicle/integration/test_connector_revisions.py diff --git a/README.md b/README.md index 7de3d163..a007d576 100644 --- a/README.md +++ b/README.md @@ -2221,6 +2221,134 @@ template = chronicle.get_integration_connector_template("MyIntegration") print(f"Template script: {template.get('script')}") ``` +### Integration Connector Revisions + +List all revisions for a specific integration connector: + +```python +# Get all revisions for a connector +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1" +) +for revision in revisions.get("revisions", []): + print(f"Revision ID: {revision.get('name')}") + print(f"Comment: {revision.get('comment')}") + print(f"Created: {revision.get('createTime')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter revisions with order +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1", + order_by="createTime desc", + page_size=10 +) +``` + +Delete a specific connector revision: + +```python +# Clean up old revision from version history +chronicle.delete_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id="r1" +) +``` + +Create a new connector revision snapshot: + +```python +# Get the current connector configuration +connector = chronicle.get_integration_connector( + integration_name="MyIntegration", + connector_id="c1" +) + +# Create a revision without comment +new_revision = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector +) + +# Create a revision with descriptive comment +new_revision = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector, + comment="Stable version before adding new field mapping" +) + +print(f"Created revision: {new_revision.get('name')}") +``` + +Rollback a connector to a previous revision: + +```python +# Revert to a known good configuration +rolled_back = chronicle.rollback_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id="r1" +) + +print(f"Rolled back to revision: {rolled_back.get('name')}") +print(f"Connector script restored") +``` + +Example workflow: Safe connector updates with revisions: + +```python +# 1. Get current connector +connector = chronicle.get_integration_connector( + integration_name="MyIntegration", + connector_id="c1" +) + +# 2. Create backup revision before changes +backup = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector, + comment="Backup before timeout increase" +) +print(f"Backup created: {backup.get('name')}") + +# 3. Update the connector +updated_connector = chronicle.update_integration_connector( + integration_name="MyIntegration", + connector_id="c1", + timeout_seconds=600, + description="Increased timeout for large data pulls" +) + +# 4. Test the updated connector +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=updated_connector +) + +# 5. If test fails, rollback to the backup +if not test_result.get("outputMessage"): + print("Test failed, rolling back...") + chronicle.rollback_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id=backup.get("name").split("/")[-1] + ) + print("Rollback complete") +else: + print("Test passed, changes applied successfully") +``` + ### Integration Jobs List all available jobs for an integration: diff --git a/api_module_mapping.md b/api_module_mapping.md index 6275cb0b..8f37d213 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 64 endpoints implemented -- **v1alpha:** 157 endpoints implemented +- **v1beta:** 68 endpoints implemented +- **v1alpha:** 161 endpoints implemented ## Endpoint Mapping @@ -98,6 +98,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | | | integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | | | integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | | +| integrations.connectors.revisions.create | v1beta | chronicle.integration.connector_revisions.create_integration_connector_revision | | +| integrations.connectors.revisions.delete | v1beta | chronicle.integration.connector_revisions.delete_integration_connector_revision | | +| integrations.connectors.revisions.list | v1beta | chronicle.integration.connector_revisions.list_integration_connector_revisions | | +| integrations.connectors.revisions.rollback | v1beta | chronicle.integration.connector_revisions.rollback_integration_connector_revision | | | integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | | integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | | integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | @@ -352,6 +356,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.revisions.create | v1alpha | chronicle.integration.connector_revisions.create_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.revisions.delete | v1alpha | chronicle.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.revisions.list | v1alpha | chronicle.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.revisions.rollback | v1alpha | chronicle.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 6d0dea52..80eab595 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -245,6 +245,12 @@ execute_integration_connector_test, get_integration_connector_template, ) +from secops.chronicle.integration.connector_revisions import ( + list_integration_connector_revisions, + delete_integration_connector_revision, + create_integration_connector_revision, + rollback_integration_connector_revision, +) from secops.chronicle.integration.jobs import ( list_integration_jobs, get_integration_job, @@ -519,6 +525,11 @@ "update_integration_connector", "execute_integration_connector_test", "get_integration_connector_template", + # Integration Connector Revisions + "list_integration_connector_revisions", + "delete_integration_connector_revision", + "create_integration_connector_revision", + "rollback_integration_connector_revision", # Integration Jobs "list_integration_jobs", "get_integration_job", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 0ba7af80..43626a99 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -173,6 +173,12 @@ list_integration_connectors as _list_integration_connectors, update_integration_connector as _update_integration_connector, ) +from secops.chronicle.integration.connector_revisions import ( + create_integration_connector_revision as _create_integration_connector_revision, + delete_integration_connector_revision as _delete_integration_connector_revision, + list_integration_connector_revisions as _list_integration_connector_revisions, + rollback_integration_connector_revision as _rollback_integration_connector_revision, +) from secops.chronicle.integration.jobs import ( create_integration_job as _create_integration_job, delete_integration_job as _delete_integration_job, @@ -2129,6 +2135,171 @@ def get_integration_connector_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Connector Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_connector_revisions( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration connector. + + Use this method to browse the version history and identify + potential rollback targets. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_connector_revisions( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific revision for a given integration + connector. + + Use this method to clean up old or incorrect snapshots from the + version history. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_connector_revision( + self, + integration_name, + connector_id, + revision_id, + api_version=api_version, + ) + + def create_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + connector: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new revision snapshot of the current integration + connector. + + Use this method to save a stable configuration before making + experimental changes. Only custom connectors can be versioned. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create a revision for. + connector: Dict containing the IntegrationConnector to + snapshot. + comment: Comment describing the revision. Maximum 400 + characters. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ConnectorRevision + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_connector_revision( + self, + integration_name, + connector_id, + connector, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Revert the current connector definition to a previously + saved revision. + + Use this method to quickly revert to a known good configuration + if an investigation or update is unsuccessful. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the ConnectorRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_connector_revision( + self, + integration_name, + connector_id, + revision_id, + api_version=api_version, + ) + # ------------------------------------------------------------------------- # Integration Job methods # ------------------------------------------------------------------------- diff --git a/src/secops/chronicle/integration/connector_revisions.py b/src/secops/chronicle/integration/connector_revisions.py new file mode 100644 index 00000000..128e9d05 --- /dev/null +++ b/src/secops/chronicle/integration/connector_revisions.py @@ -0,0 +1,202 @@ +# Copyright 2025 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. +# +"""Integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_connector_revisions( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration connector. + + Use this method to browse the version history and identify potential + rollback targets. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific revision for a given integration connector. + + Use this method to clean up old or incorrect snapshots from the version + history. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration connector. + + Use this method to save a stable configuration before making experimental + changes. Only custom connectors can be versioned. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create a revision for. + connector: Dict containing the IntegrationConnector to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ConnectorRevision resource. + + Raises: + APIError: If the API request fails. + """ + body: dict[str, Any] = {"connector": connector} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Revert the current connector definition to a previously saved revision. + + Use this method to quickly revert to a known good configuration if an + investigation or update is unsuccessful. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the ConnectorRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions/{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_connector_revisions.py b/tests/chronicle/integration/test_connector_revisions.py new file mode 100644 index 00000000..7b214bcb --- /dev/null +++ b/tests/chronicle/integration/test_connector_revisions.py @@ -0,0 +1,385 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration connector revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_revisions import ( + list_integration_connector_revisions, + delete_integration_connector_revision, + create_integration_connector_revision, + rollback_integration_connector_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_connector_revisions tests -- + + +def test_list_integration_connector_revisions_success(chronicle_client): + """Test list_integration_connector_revisions delegates to chronicle_paginated_request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_connector_revisions( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/revisions" in kwargs["path"] + assert kwargs["items_key"] == "revisions" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_connector_revisions_default_args(chronicle_client): + """Test list_integration_connector_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_integration_connector_revisions_with_filters(chronicle_client): + """Test list_integration_connector_revisions with filter and order_by.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='version = "1.0"', + order_by="createTime", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'version = "1.0"', + "orderBy": "createTime", + } + + +def test_list_integration_connector_revisions_as_list(chronicle_client): + """Test list_integration_connector_revisions returns list when as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_connector_revisions_error(chronicle_client): + """Test list_integration_connector_revisions raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + side_effect=APIError("Failed to list connector revisions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list connector revisions" in str(exc_info.value) + + +# -- delete_integration_connector_revision tests -- + + +def test_delete_integration_connector_revision_success(chronicle_client): + """Test delete_integration_connector_revision issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_connector_revision_error(chronicle_client): + """Test delete_integration_connector_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to delete connector revision"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + assert "Failed to delete connector revision" in str(exc_info.value) + + +# -- create_integration_connector_revision tests -- + + +def test_create_integration_connector_revision_required_fields_only( + chronicle_client, +): + """Test create_integration_connector_revision with required fields only.""" + expected = { + "name": "revisions/new", + "connector": {"displayName": "My Connector"}, + } + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/connectors/c1/revisions" + ), + api_version=APIVersion.V1BETA, + json={"connector": connector_dict}, + ) + + +def test_create_integration_connector_revision_with_comment(chronicle_client): + """Test create_integration_connector_revision includes comment when provided.""" + expected = {"name": "revisions/new"} + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + comment="Backup before major update", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["comment"] == "Backup before major update" + assert kwargs["json"]["connector"] == connector_dict + + +def test_create_integration_connector_revision_error(chronicle_client): + """Test create_integration_connector_revision raises APIError on failure.""" + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to create connector revision"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + ) + assert "Failed to create connector revision" in str(exc_info.value) + + +# -- rollback_integration_connector_revision tests -- + + +def test_rollback_integration_connector_revision_success(chronicle_client): + """Test rollback_integration_connector_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "connector": { + "displayName": "My Connector", + "script": "print('hello')", + }, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/revisions/r1:rollback" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_rollback_integration_connector_revision_error(chronicle_client): + """Test rollback_integration_connector_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to rollback connector revision"), + ): + with pytest.raises(APIError) as exc_info: + rollback_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + assert "Failed to rollback connector revision" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_connector_revisions_custom_api_version( + chronicle_client, +): + """Test list_integration_connector_revisions with custom API version.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_integration_connector_revision_custom_api_version( + chronicle_client, +): + """Test delete_integration_connector_revision with custom API version.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From 0e6b0a441eb9b91b10adb088d17f77b25578d339 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 09:53:34 +0000 Subject: [PATCH 34/46] feat: add functions for integration connector context properties --- README.md | 152 +++++ api_module_mapping.md | 16 +- src/secops/chronicle/__init__.py | 15 + src/secops/chronicle/client.py | 260 ++++++++ .../connector_context_properties.py | 299 ++++++++++ .../test_connector_context_properties.py | 561 ++++++++++++++++++ 6 files changed, 1301 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/connector_context_properties.py create mode 100644 tests/chronicle/integration/test_connector_context_properties.py diff --git a/README.md b/README.md index a007d576..5c986002 100644 --- a/README.md +++ b/README.md @@ -2349,6 +2349,158 @@ else: print("Test passed, changes applied successfully") ``` +### Connector Context Properties + +List all context properties for a specific connector: + +```python +# Get all context properties for a connector +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1" +) +for prop in context_properties.get("contextProperties", []): + print(f"Key: {prop.get('key')}, Value: {prop.get('value')}") + +# Get all context properties as a list +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter context properties +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + filter_string='key = "last_run_time"', + order_by="key" +) +``` + +Get a specific context property: + +```python +property_value = chronicle.get_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last_run_time" +) +print(f"Value: {property_value.get('value')}") +``` + +Create a new context property: + +```python +# Create context property with auto-generated key +new_property = chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-03-09T10:00:00Z" +) + +# Create context property with custom key +new_property = chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-03-09T10:00:00Z", + key="last-sync-time" +) +print(f"Created property: {new_property.get('name')}") +``` + +Update an existing context property: + +```python +# Update property value +updated_property = chronicle.update_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-sync-time", + value="2026-03-09T11:00:00Z" +) +print(f"Updated value: {updated_property.get('value')}") +``` + +Delete a context property: + +```python +chronicle.delete_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-sync-time" +) +``` + +Delete all context properties: + +```python +# Clear all properties for a connector +chronicle.delete_all_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1" +) + +# Clear all properties for a specific context ID +chronicle.delete_all_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + context_id="my-context" +) +``` + +Example workflow: Track connector state with context properties: + +```python +# 1. Check if we have a last run time stored +try: + last_run = chronicle.get_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-run-time" + ) + print(f"Last run: {last_run.get('value')}") +except APIError: + print("No previous run time found") + # Create initial property + chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-01-01T00:00:00Z", + key="last-run-time" + ) + +# 2. Run the connector and process data +# ... connector execution logic ... + +# 3. Update the last run time after successful execution +from datetime import datetime +current_time = datetime.utcnow().isoformat() + "Z" +chronicle.update_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-run-time", + value=current_time +) + +# 4. Store additional context like record count +chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="1500", + key="records-processed" +) + +# 5. List all context to see connector state +all_context = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) +for prop in all_context: + print(f"{prop.get('key')}: {prop.get('value')}") +``` + ### Integration Jobs List all available jobs for an integration: diff --git a/api_module_mapping.md b/api_module_mapping.md index 8f37d213..7c4ca6d0 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 68 endpoints implemented -- **v1alpha:** 161 endpoints implemented +- **v1beta:** 74 endpoints implemented +- **v1alpha:** 167 endpoints implemented ## Endpoint Mapping @@ -102,6 +102,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.revisions.delete | v1beta | chronicle.integration.connector_revisions.delete_integration_connector_revision | | | integrations.connectors.revisions.list | v1beta | chronicle.integration.connector_revisions.list_integration_connector_revisions | | | integrations.connectors.revisions.rollback | v1beta | chronicle.integration.connector_revisions.rollback_integration_connector_revision | | +| integrations.connectors.contextProperties.clearAll | v1beta | chronicle.integration.connector_context_properties.delete_all_connector_context_properties | | +| integrations.connectors.contextProperties.create | v1beta | chronicle.integration.connector_context_properties.create_connector_context_property | | +| integrations.connectors.contextProperties.delete | v1beta | chronicle.integration.connector_context_properties.delete_connector_context_property | | +| integrations.connectors.contextProperties.get | v1beta | chronicle.integration.connector_context_properties.get_connector_context_property | | +| integrations.connectors.contextProperties.list | v1beta | chronicle.integration.connector_context_properties.list_connector_context_properties | | +| integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | | | integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | | integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | | integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | @@ -360,6 +366,12 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.revisions.delete | v1alpha | chronicle.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.revisions.list | v1alpha | chronicle.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.revisions.rollback | v1alpha | chronicle.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.clearAll | v1alpha | chronicle.integration.connector_context_properties.delete_all_connector_context_properties(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.create | v1alpha | chronicle.integration.connector_context_properties.create_connector_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.delete | v1alpha | chronicle.integration.connector_context_properties.delete_connector_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.get | v1alpha | chronicle.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.list | v1alpha | chronicle.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 80eab595..ee66eb94 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -251,6 +251,14 @@ create_integration_connector_revision, rollback_integration_connector_revision, ) +from secops.chronicle.integration.connector_context_properties import ( + list_connector_context_properties, + get_connector_context_property, + delete_connector_context_property, + create_connector_context_property, + update_connector_context_property, + delete_all_connector_context_properties, +) from secops.chronicle.integration.jobs import ( list_integration_jobs, get_integration_job, @@ -530,6 +538,13 @@ "delete_integration_connector_revision", "create_integration_connector_revision", "rollback_integration_connector_revision", + # Connector Context Properties + "list_connector_context_properties", + "get_connector_context_property", + "delete_connector_context_property", + "create_connector_context_property", + "update_connector_context_property", + "delete_all_connector_context_properties", # Integration Jobs "list_integration_jobs", "get_integration_job", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 43626a99..2f885833 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -179,6 +179,14 @@ list_integration_connector_revisions as _list_integration_connector_revisions, rollback_integration_connector_revision as _rollback_integration_connector_revision, ) +from secops.chronicle.integration.connector_context_properties import ( + create_connector_context_property as _create_connector_context_property, + delete_all_connector_context_properties as _delete_all_connector_context_properties, + delete_connector_context_property as _delete_connector_context_property, + get_connector_context_property as _get_connector_context_property, + list_connector_context_properties as _list_connector_context_properties, + update_connector_context_property as _update_connector_context_property, +) from secops.chronicle.integration.jobs import ( create_integration_job as _create_integration_job, delete_integration_job as _delete_integration_job, @@ -2300,6 +2308,258 @@ def rollback_integration_connector_revision( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Connector Context Properties methods + # ------------------------------------------------------------------------- + + def list_connector_context_properties( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration + connector. + + Use this method to discover all custom data points associated + with a connector. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list context + properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter context + properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of context properties + instead of a dict with context properties list and + nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_context_properties( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single context property for a specific integration + connector. + + Use this method to retrieve the value of a specific key within + a connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + api_version=api_version, + ) + + def delete_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific context property for a given integration + connector. + + Use this method to remove a custom data point that is no longer + relevant to the connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + api_version=api_version, + ) + + def create_connector_context_property( + self, + integration_name: str, + connector_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new context property for a specific integration + connector. + + Use this method to attach custom data to a connector's context. + Property keys must be unique within their context. Key values + must be 4-63 characters and match /[a-z][0-9]-/. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create the context + property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 + characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _create_connector_context_property( + self, + integration_name, + connector_id, + value, + key=key, + api_version=api_version, + ) + + def update_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing context property for a given integration + connector. + + Use this method to modify the value of a previously saved key. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only + "value" is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _update_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + value, + update_mask=update_mask, + api_version=api_version, + ) + + def delete_all_connector_context_properties( + self, + integration_name: str, + connector_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete all context properties for a specific integration + connector. + + Use this method to quickly clear all supplemental data from a + connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to clear context + properties from. + context_id: The context ID to remove context properties + from. Must be 4-63 characters and match /[a-z][0-9]-/. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_all_connector_context_properties( + self, + integration_name, + connector_id, + context_id=context_id, + api_version=api_version, + ) + # ------------------------------------------------------------------------- # Integration Job methods # ------------------------------------------------------------------------- diff --git a/src/secops/chronicle/integration/connector_context_properties.py b/src/secops/chronicle/integration/connector_context_properties.py new file mode 100644 index 00000000..416e52dd --- /dev/null +++ b/src/secops/chronicle/integration/connector_context_properties.py @@ -0,0 +1,299 @@ +# Copyright 2025 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. +# +"""Integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_context_properties( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration connector. + + Use this method to discover all custom data points associated with a + connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list context properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter context properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of context properties instead of a + dict with context properties list and nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties" + ), + items_key="contextProperties", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single context property for a specific integration connector. + + Use this method to retrieve the value of a specific key within a + connector's context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def delete_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific context property for a given integration connector. + + Use this method to remove a custom data point that is no longer relevant + to the connector's context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def create_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new context property for a specific integration connector. + + Use this method to attach custom data to a connector's context. Property + keys must be unique within their context. Key values must be 4-63 + characters and match /[a-z][0-9]-/. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create the context property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 characters and + match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body: dict[str, Any] = {"value": value} + + if key is not None: + body["key"] = key + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties" + ), + api_version=api_version, + json=body, + ) + + +def update_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing context property for a given integration connector. + + Use this method to modify the value of a previously saved key. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only "value" + is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body, params = build_patch_body( + field_map=[ + ("value", "value", value), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def delete_all_connector_context_properties( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete all context properties for a specific integration connector. + + Use this method to quickly clear all supplemental data from a connector's + context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to clear context properties from. + context_id: The context ID to remove context properties from. Must be + 4-63 characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + body: dict[str, Any] = {} + + if context_id is not None: + body["contextId"] = context_id + + chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties:clearAll" + ), + api_version=api_version, + json=body, + ) diff --git a/tests/chronicle/integration/test_connector_context_properties.py b/tests/chronicle/integration/test_connector_context_properties.py new file mode 100644 index 00000000..33941087 --- /dev/null +++ b/tests/chronicle/integration/test_connector_context_properties.py @@ -0,0 +1,561 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration connector context properties functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_context_properties import ( + list_connector_context_properties, + get_connector_context_property, + delete_connector_context_property, + create_connector_context_property, + update_connector_context_property, + delete_all_connector_context_properties, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_context_properties tests -- + + +def test_list_connector_context_properties_success(chronicle_client): + """Test list_connector_context_properties delegates to paginated request.""" + expected = { + "contextProperties": [{"key": "prop1"}, {"key": "prop2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_context_properties.format_resource_id", + return_value="My Integration", + ): + result = list_connector_context_properties( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/contextProperties" in kwargs["path"] + assert kwargs["items_key"] == "contextProperties" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_context_properties_default_args(chronicle_client): + """Test list_connector_context_properties with default args.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_connector_context_properties_with_filters(chronicle_client): + """Test list_connector_context_properties with filter and order_by.""" + expected = {"contextProperties": [{"key": "prop1"}]} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='key = "prop1"', + order_by="key", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'key = "prop1"', + "orderBy": "key", + } + + +def test_list_connector_context_properties_as_list(chronicle_client): + """Test list_connector_context_properties returns list when as_list=True.""" + expected = [{"key": "prop1"}, {"key": "prop2"}] + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_context_properties_error(chronicle_client): + """Test list_connector_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + side_effect=APIError("Failed to list context properties"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list context properties" in str(exc_info.value) + + +# -- get_connector_context_property tests -- + + +def test_get_connector_context_property_success(chronicle_client): + """Test get_connector_context_property issues GET request.""" + expected = { + "name": "contextProperties/prop1", + "key": "prop1", + "value": "test-value", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_context_property_error(chronicle_client): + """Test get_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to get context property"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + assert "Failed to get context property" in str(exc_info.value) + + +# -- delete_connector_context_property tests -- + + +def test_delete_connector_context_property_success(chronicle_client): + """Test delete_connector_context_property issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_connector_context_property_error(chronicle_client): + """Test delete_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to delete context property"), + ): + with pytest.raises(APIError) as exc_info: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + assert "Failed to delete context property" in str(exc_info.value) + + +# -- create_connector_context_property tests -- + + +def test_create_connector_context_property_required_fields_only(chronicle_client): + """Test create_connector_context_property with required fields only.""" + expected = {"name": "contextProperties/new", "value": "test-value"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors/c1/contextProperties", + api_version=APIVersion.V1BETA, + json={"value": "test-value"}, + ) + + +def test_create_connector_context_property_with_key(chronicle_client): + """Test create_connector_context_property includes key when provided.""" + expected = {"name": "contextProperties/custom-key", "value": "test-value"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + key="custom-key", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["value"] == "test-value" + assert kwargs["json"]["key"] == "custom-key" + + +def test_create_connector_context_property_error(chronicle_client): + """Test create_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to create context property"), + ): + with pytest.raises(APIError) as exc_info: + create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + ) + assert "Failed to create context property" in str(exc_info.value) + + +# -- update_connector_context_property tests -- + + +def test_update_connector_context_property_success(chronicle_client): + """Test update_connector_context_property updates value.""" + expected = { + "name": "contextProperties/prop1", + "value": "updated-value", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "PATCH" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["json"]["value"] == "updated-value" + assert kwargs["params"]["updateMask"] == "value" + + +def test_update_connector_context_property_with_custom_mask(chronicle_client): + """Test update_connector_context_property with custom update_mask.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + update_mask="value", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["params"]["updateMask"] == "value" + + +def test_update_connector_context_property_error(chronicle_client): + """Test update_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to update context property"), + ): + with pytest.raises(APIError) as exc_info: + update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + ) + assert "Failed to update context property" in str(exc_info.value) + + +# -- delete_all_connector_context_properties tests -- + + +def test_delete_all_connector_context_properties_success(chronicle_client): + """Test delete_all_connector_context_properties issues POST request.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/contextProperties:clearAll" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + assert kwargs["json"] == {} + + +def test_delete_all_connector_context_properties_with_context_id(chronicle_client): + """Test delete_all_connector_context_properties with context_id.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_id="my-context", + ) + + _, kwargs = mock_request.call_args + assert kwargs["json"]["contextId"] == "my-context" + + +def test_delete_all_connector_context_properties_error(chronicle_client): + """Test delete_all_connector_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to clear context properties"), + ): + with pytest.raises(APIError) as exc_info: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to clear context properties" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_context_properties_custom_api_version(chronicle_client): + """Test list_connector_context_properties with custom API version.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_context_property_custom_api_version(chronicle_client): + """Test get_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_connector_context_property_custom_api_version(chronicle_client): + """Test delete_connector_context_property with custom API version.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_create_connector_context_property_custom_api_version(chronicle_client): + """Test create_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/new"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_update_connector_context_property_custom_api_version(chronicle_client): + """Test update_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_all_connector_context_properties_custom_api_version(chronicle_client): + """Test delete_all_connector_context_properties with custom API version.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From 9bf1aa2e2362a971b4e3006f1bec84ab62b0c8b9 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 10:54:57 +0000 Subject: [PATCH 35/46] feat: add functions for integration connector instances --- README.md | 96 +++++++ api_module_mapping.md | 8 +- src/secops/chronicle/__init__.py | 7 + src/secops/chronicle/client.py | 102 +++++++ .../integration/connector_instance_logs.py | 130 +++++++++ .../test_connector_instance_logs.py | 256 ++++++++++++++++++ 6 files changed, 597 insertions(+), 2 deletions(-) create mode 100644 src/secops/chronicle/integration/connector_instance_logs.py create mode 100644 tests/chronicle/integration/test_connector_instance_logs.py diff --git a/README.md b/README.md index 5c986002..c1a91b21 100644 --- a/README.md +++ b/README.md @@ -2501,6 +2501,102 @@ for prop in all_context: print(f"{prop.get('key')}: {prop.get('value')}") ``` +### Connector Instance Logs + +List all execution logs for a connector instance: + +```python +# Get all logs for a connector instance +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +for log in logs.get("logs", []): + print(f"Log ID: {log.get('name')}, Severity: {log.get('severity')}") + print(f"Timestamp: {log.get('timestamp')}") + print(f"Message: {log.get('message')}") + +# Get all logs as a list +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True +) + +# Filter logs by severity +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + filter_string='severity = "ERROR"', + order_by="timestamp desc" +) +``` + +Get a specific log entry: + +```python +log_entry = chronicle.get_connector_instance_log( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log123" +) +print(f"Severity: {log_entry.get('severity')}") +print(f"Timestamp: {log_entry.get('timestamp')}") +print(f"Message: {log_entry.get('message')}") +``` + +Monitor connector execution and troubleshooting: + +```python +# Get recent logs for monitoring +recent_logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + order_by="timestamp desc", + page_size=10, + as_list=True +) + +# Check for errors +for log in recent_logs: + if log.get("severity") in ["ERROR", "CRITICAL"]: + print(f"Error at {log.get('timestamp')}") + log_details = chronicle.get_connector_instance_log( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + log_id=log.get("name").split("/")[-1] + ) + print(f"Error message: {log_details.get('message')}") +``` + +Analyze connector performance and reliability: + +```python +# Get all logs to calculate error rate +all_logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True +) + +errors = sum(1 for log in all_logs if log.get("severity") in ["ERROR", "CRITICAL"]) +warnings = sum(1 for log in all_logs if log.get("severity") == "WARNING") +total = len(all_logs) + +if total > 0: + error_rate = (errors / total) * 100 + print(f"Error Rate: {error_rate:.2f}%") + print(f"Total Logs: {total}") + print(f"Errors: {errors}, Warnings: {warnings}") +``` + ### Integration Jobs List all available jobs for an integration: diff --git a/api_module_mapping.md b/api_module_mapping.md index 7c4ca6d0..2bb599b5 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 74 endpoints implemented -- **v1alpha:** 167 endpoints implemented +- **v1beta:** 76 endpoints implemented +- **v1alpha:** 169 endpoints implemented ## Endpoint Mapping @@ -108,6 +108,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.contextProperties.get | v1beta | chronicle.integration.connector_context_properties.get_connector_context_property | | | integrations.connectors.contextProperties.list | v1beta | chronicle.integration.connector_context_properties.list_connector_context_properties | | | integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | | +| integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.integration.connector_instance_logs.get_connector_instance_log | | +| integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.integration.connector_instance_logs.list_connector_instance_logs | | | integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | | integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | | integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | @@ -372,6 +374,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.contextProperties.get | v1alpha | chronicle.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.contextProperties.list | v1alpha | chronicle.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.logs.get | v1alpha | chronicle.integration.connector_instance_logs.get_connector_instance_log(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.logs.list | v1alpha | chronicle.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index ee66eb94..923d51b6 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -259,6 +259,10 @@ update_connector_context_property, delete_all_connector_context_properties, ) +from secops.chronicle.integration.connector_instance_logs import ( + list_connector_instance_logs, + get_connector_instance_log, +) from secops.chronicle.integration.jobs import ( list_integration_jobs, get_integration_job, @@ -545,6 +549,9 @@ "create_connector_context_property", "update_connector_context_property", "delete_all_connector_context_properties", + # Connector Instance Logs + "list_connector_instance_logs", + "get_connector_instance_log", # Integration Jobs "list_integration_jobs", "get_integration_job", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 2f885833..953d4dee 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -187,6 +187,10 @@ list_connector_context_properties as _list_connector_context_properties, update_connector_context_property as _update_connector_context_property, ) +from secops.chronicle.integration.connector_instance_logs import ( + get_connector_instance_log as _get_connector_instance_log, + list_connector_instance_logs as _list_connector_instance_logs, +) from secops.chronicle.integration.jobs import ( create_integration_job as _create_integration_job, delete_integration_job as _delete_integration_job, @@ -2560,6 +2564,104 @@ def delete_all_connector_context_properties( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Connector Instance Logs methods + # ------------------------------------------------------------------------- + + def list_connector_instance_logs( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all logs for a specific connector instance. + + Use this method to browse the execution history and diagnostic + output of a connector. Supports filtering and pagination to + efficiently navigate large volumes of log data. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to list + logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of logs instead of a dict + with logs list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_instance_logs( + self, + integration_name, + connector_id, + connector_instance_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_instance_log( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single log entry for a specific connector instance. + + Use this method to retrieve a specific log entry from a + connector instance's execution, including its message, + timestamp, and severity level. Useful for auditing and detailed + troubleshooting of a specific connector run. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance the log + belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ConnectorLog. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance_log( + self, + integration_name, + connector_id, + connector_instance_id, + log_id, + api_version=api_version, + ) + # ------------------------------------------------------------------------- # Integration Job methods # ------------------------------------------------------------------------- diff --git a/src/secops/chronicle/integration/connector_instance_logs.py b/src/secops/chronicle/integration/connector_instance_logs.py new file mode 100644 index 00000000..76fe2c19 --- /dev/null +++ b/src/secops/chronicle/integration/connector_instance_logs.py @@ -0,0 +1,130 @@ +# Copyright 2025 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. +# +"""Integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_instance_logs( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all logs for a specific connector instance. + + Use this method to browse the execution history and diagnostic output of + a connector. Supports filtering and pagination to efficiently navigate + large volumes of log data. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to list logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of logs instead of a dict with logs + list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}/logs" + ), + items_key="logs", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_instance_log( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single log entry for a specific connector instance. + + Use this method to retrieve a specific log entry from a connector + instance's execution, including its message, timestamp, and severity + level. Useful for auditing and detailed troubleshooting of a specific + connector run. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance the log belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ConnectorLog. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}/logs/{log_id}" + ), + api_version=api_version, + ) diff --git a/tests/chronicle/integration/test_connector_instance_logs.py b/tests/chronicle/integration/test_connector_instance_logs.py new file mode 100644 index 00000000..873264fc --- /dev/null +++ b/tests/chronicle/integration/test_connector_instance_logs.py @@ -0,0 +1,256 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration connector instance logs functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_instance_logs import ( + list_connector_instance_logs, + get_connector_instance_log, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_instance_logs tests -- + + +def test_list_connector_instance_logs_success(chronicle_client): + """Test list_connector_instance_logs delegates to paginated request.""" + expected = { + "logs": [{"name": "log1"}, {"name": "log2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_instance_logs.format_resource_id", + return_value="My Integration", + ): + result = list_connector_instance_logs( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + connector_instance_id="ci1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/connectorInstances/ci1/logs" in kwargs["path"] + assert kwargs["items_key"] == "logs" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_instance_logs_default_args(chronicle_client): + """Test list_connector_instance_logs with default args.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + +def test_list_connector_instance_logs_with_filters(chronicle_client): + """Test list_connector_instance_logs with filter and order_by.""" + expected = {"logs": [{"name": "log1"}]} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + filter_string='severity = "ERROR"', + order_by="timestamp desc", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'severity = "ERROR"', + "orderBy": "timestamp desc", + } + + +def test_list_connector_instance_logs_as_list(chronicle_client): + """Test list_connector_instance_logs returns list when as_list=True.""" + expected = [{"name": "log1"}, {"name": "log2"}] + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_instance_logs_error(chronicle_client): + """Test list_connector_instance_logs raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + side_effect=APIError("Failed to list connector instance logs"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to list connector instance logs" in str(exc_info.value) + + +# -- get_connector_instance_log tests -- + + +def test_get_connector_instance_log_success(chronicle_client): + """Test get_connector_instance_log issues GET request.""" + expected = { + "name": "logs/log1", + "message": "Test log message", + "severity": "INFO", + "timestamp": "2026-03-09T10:00:00Z", + } + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/connectorInstances/ci1/logs/log1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_log_error(chronicle_client): + """Test get_connector_instance_log raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + side_effect=APIError("Failed to get connector instance log"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + ) + assert "Failed to get connector instance log" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_instance_logs_custom_api_version(chronicle_client): + """Test list_connector_instance_logs with custom API version.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_log_custom_api_version(chronicle_client): + """Test get_connector_instance_log with custom API version.""" + expected = {"name": "logs/log1"} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From 933e7d3b79450f0bee85ab501eb58b100d14773c Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 14:13:05 +0000 Subject: [PATCH 36/46] feat: add functions for integration connector instances --- README.md | 245 +++++ api_module_mapping.md | 20 +- src/secops/chronicle/__init__.py | 19 + src/secops/chronicle/client.py | 407 +++++++++ .../connector_context_properties.py | 2 +- .../integration/connector_instance_logs.py | 2 +- .../integration/connector_instances.py | 489 ++++++++++ .../integration/connector_revisions.py | 2 +- src/secops/chronicle/models.py | 28 + .../integration/test_connector_instances.py | 845 ++++++++++++++++++ 10 files changed, 2054 insertions(+), 5 deletions(-) create mode 100644 src/secops/chronicle/integration/connector_instances.py create mode 100644 tests/chronicle/integration/test_connector_instances.py diff --git a/README.md b/README.md index c1a91b21..25e353d4 100644 --- a/README.md +++ b/README.md @@ -2597,6 +2597,251 @@ if total > 0: print(f"Errors: {errors}, Warnings: {warnings}") ``` +### Connector Instances + +List all connector instances for a specific connector: + +```python +# Get all instances for a connector +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1" +) +for instance in instances.get("connectorInstances", []): + print(f"Instance: {instance.get('displayName')}, Enabled: {instance.get('enabled')}") + +# Get all instances as a list +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter instances +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + filter_string='enabled = true', + order_by="displayName" +) +``` + +Get a specific connector instance: + +```python +instance = chronicle.get_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +print(f"Display Name: {instance.get('displayName')}") +print(f"Environment: {instance.get('environment')}") +print(f"Interval: {instance.get('intervalSeconds')} seconds") +``` + +Create a new connector instance: + +```python +from secops.chronicle.models import ConnectorInstanceParameter + +# Create basic connector instance +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="production", + display_name="Production Instance", + interval_seconds=3600, # Run every hour + timeout_seconds=300, # 5 minute timeout + enabled=True +) + +# Create instance with parameters +param = ConnectorInstanceParameter() +param.value = "my-api-key" + +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="production", + display_name="Production Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Main production connector instance", + parameters=[param], + enabled=True +) +print(f"Created instance: {new_instance.get('name')}") +``` + +Update an existing connector instance: + +```python +# Update display name +updated_instance = chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated Production Instance" +) + +# Update multiple fields including parameters +param = ConnectorInstanceParameter() +param.value = "new-api-key" + +updated_instance = chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated Instance", + interval_seconds=7200, # Change to every 2 hours + parameters=[param], + enabled=True +) +``` + +Delete a connector instance: + +```python +chronicle.delete_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +``` + +Refresh instance with latest connector definition: + +```python +# Fetch latest definition from marketplace +refreshed_instance = chronicle.get_connector_instance_latest_definition( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +print(f"Updated to latest definition") +``` + +Enable/disable logs collection for debugging: + +```python +# Enable logs collection +result = chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True +) +print(f"Logs enabled until: {result.get('loggingEnabledUntilUnixMs')}") + +# Disable logs collection +chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + enabled=False +) +``` + +Run a connector instance on demand for testing: + +```python +# Get the current instance configuration +instance = chronicle.get_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) + +# Run on demand to test configuration +test_result = chronicle.run_connector_instance_on_demand( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=instance +) + +if test_result.get("success"): + print("Test execution successful!") + print(f"Debug output: {test_result.get('debugOutput')}") +else: + print("Test execution failed") + print(f"Error: {test_result.get('debugOutput')}") +``` + +Example workflow: Deploy and test a new connector instance: + +```python +from secops.chronicle.models import ConnectorInstanceParameter + +# 1. Create a new connector instance +param = ConnectorInstanceParameter() +param.value = "test-api-key" + +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="development", + display_name="Dev Test Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Development testing instance", + parameters=[param], + enabled=False # Start disabled for testing +) + +instance_id = new_instance.get("name").split("/")[-1] +print(f"Created instance: {instance_id}") + +# 2. Enable logs collection for debugging +chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + enabled=True +) + +# 3. Run on demand to test +test_result = chronicle.run_connector_instance_on_demand( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + connector_instance=new_instance +) + +# 4. Check test results +if test_result.get("success"): + print("✓ Test passed - enabling instance") + # Enable the instance for scheduled runs + chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + enabled=True + ) +else: + print("✗ Test failed - reviewing logs") + # Get logs to debug the issue + logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + filter_string='severity = "ERROR"', + as_list=True + ) + for log in logs: + print(f"Error: {log.get('message')}") + +# 5. Monitor execution after enabling +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + filter_string=f'name = "{new_instance.get("name")}"', + as_list=True +) +if instances: + print(f"Instance status: Enabled={instances[0].get('enabled')}") +``` + ### Integration Jobs List all available jobs for an integration: diff --git a/api_module_mapping.md b/api_module_mapping.md index 2bb599b5..8ce062cd 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 76 endpoints implemented -- **v1alpha:** 169 endpoints implemented +- **v1beta:** 84 endpoints implemented +- **v1alpha:** 177 endpoints implemented ## Endpoint Mapping @@ -110,6 +110,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | | | integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.integration.connector_instance_logs.get_connector_instance_log | | | integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.integration.connector_instance_logs.list_connector_instance_logs | | +| integrations.connectors.connectorInstances.create | v1beta | chronicle.integration.connector_instances.create_connector_instance | | +| integrations.connectors.connectorInstances.delete | v1beta | chronicle.integration.connector_instances.delete_connector_instance | | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1beta | chronicle.integration.connector_instances.get_connector_instance_latest_definition | | +| integrations.connectors.connectorInstances.get | v1beta | chronicle.integration.connector_instances.get_connector_instance | | +| integrations.connectors.connectorInstances.list | v1beta | chronicle.integration.connector_instances.list_connector_instances | | +| integrations.connectors.connectorInstances.patch | v1beta | chronicle.integration.connector_instances.update_connector_instance | | +| integrations.connectors.connectorInstances.runOnDemand | v1beta | chronicle.integration.connector_instances.run_connector_instance_on_demand | | +| integrations.connectors.connectorInstances.setLogsCollection | v1beta | chronicle.integration.connector_instances.set_connector_instance_logs_collection | | | integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | | integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | | integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | @@ -376,6 +384,14 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.connectorInstances.logs.get | v1alpha | chronicle.integration.connector_instance_logs.get_connector_instance_log(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.connectorInstances.logs.list | v1alpha | chronicle.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.create | v1alpha | chronicle.integration.connector_instances.create_connector_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.delete | v1alpha | chronicle.integration.connector_instances.delete_connector_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1alpha | chronicle.integration.connector_instances.get_connector_instance_latest_definition(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.get | v1alpha | chronicle.integration.connector_instances.get_connector_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.list | v1alpha | chronicle.integration.connector_instances.list_connector_instances(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.patch | v1alpha | chronicle.integration.connector_instances.update_connector_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.runOnDemand | v1alpha | chronicle.integration.connector_instances.run_connector_instance_on_demand(api_version=APIVersion.V1ALPHA) | | +| integrations.connectors.connectorInstances.setLogsCollection | v1alpha | chronicle.integration.connector_instances.set_connector_instance_logs_collection(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 923d51b6..bfd7069a 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -263,6 +263,16 @@ list_connector_instance_logs, get_connector_instance_log, ) +from secops.chronicle.integration.connector_instances import ( + list_connector_instances, + get_connector_instance, + delete_connector_instance, + create_connector_instance, + update_connector_instance, + get_connector_instance_latest_definition, + set_connector_instance_logs_collection, + run_connector_instance_on_demand, +) from secops.chronicle.integration.jobs import ( list_integration_jobs, get_integration_job, @@ -552,6 +562,15 @@ # Connector Instance Logs "list_connector_instance_logs", "get_connector_instance_log", + # Connector Instances + "list_connector_instances", + "get_connector_instance", + "delete_connector_instance", + "create_connector_instance", + "update_connector_instance", + "get_connector_instance_latest_definition", + "set_connector_instance_logs_collection", + "run_connector_instance_on_demand", # Integration Jobs "list_integration_jobs", "get_integration_job", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 953d4dee..31a60775 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -191,6 +191,16 @@ get_connector_instance_log as _get_connector_instance_log, list_connector_instance_logs as _list_connector_instance_logs, ) +from secops.chronicle.integration.connector_instances import ( + create_connector_instance as _create_connector_instance, + delete_connector_instance as _delete_connector_instance, + get_connector_instance as _get_connector_instance, + get_connector_instance_latest_definition as _get_connector_instance_latest_definition, + list_connector_instances as _list_connector_instances, + run_connector_instance_on_demand as _run_connector_instance_on_demand, + set_connector_instance_logs_collection as _set_connector_instance_logs_collection, + update_connector_instance as _update_connector_instance, +) from secops.chronicle.integration.jobs import ( create_integration_job as _create_integration_job, delete_integration_job as _delete_integration_job, @@ -268,6 +278,7 @@ TargetMode, TileType, IntegrationParam, + ConnectorInstanceParameter ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -2662,6 +2673,402 @@ def get_connector_instance_log( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Connector Instance methods + # ------------------------------------------------------------------------- + + def list_connector_instances( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration connector. + + Use this method to discover all configured instances of a + connector. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list instances for. + page_size: Maximum number of instances to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter instances. + order_by: Field to sort the instances by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of instances instead of a + dict with instances list and nextPageToken. + + Returns: + If as_list is True: List of connector instances. + If as_list is False: Dict with connector instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_instances( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single connector instance by ID. + + Use this method to retrieve the configuration of a specific + connector instance, including its parameters, schedule, and + runtime settings. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ConnectorInstance. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def delete_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific connector instance. + + Use this method to permanently remove a connector instance and + its configuration. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def create_connector_instance( + self, + integration_name: str, + connector_id: str, + environment: str, + display_name: str, + interval_seconds: int, + timeout_seconds: int, + description: str | None = None, + parameters: list[ConnectorInstanceParameter | dict] | None = None, + agent: str | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + connector_instance_id: str | None = None, + enabled: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new connector instance. + + Use this method to configure a new instance of a connector with + specific parameters and schedule settings. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create an instance for. + environment: Environment for the instance (e.g., + "production"). + display_name: Display name for the instance. Required. + interval_seconds: Interval in seconds for recurring + execution. Required. + timeout_seconds: Timeout in seconds for execution. Required. + description: Description of the instance. Optional. + parameters: List of parameters for the instance. Optional. + agent: Agent identifier for remote execution. Optional. + allow_list: List of allowed IP addresses. Optional. + product_field_name: Product field name. Optional. + event_field_name: Event field name. Optional. + integration_version: Integration version. Optional. + version: Version. Optional. + logging_enabled_until_unix_ms: Logging enabled until + timestamp. Optional. + connector_instance_id: Custom ID for the instance. Optional. + enabled: Whether the instance is enabled. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _create_connector_instance( + self, + integration_name, + connector_id, + environment, + display_name, + interval_seconds, + timeout_seconds, + description=description, + parameters=parameters, + agent=agent, + allow_list=allow_list, + product_field_name=product_field_name, + event_field_name=event_field_name, + integration_version=integration_version, + version=version, + logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, + connector_instance_id=connector_instance_id, + enabled=enabled, + api_version=api_version, + ) + + def update_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + display_name: str | None = None, + description: str | None = None, + interval_seconds: int | None = None, + timeout_seconds: int | None = None, + parameters: list[ConnectorInstanceParameter | dict] | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + enabled: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing connector instance. + + Use this method to modify the configuration, parameters, or + schedule of a connector instance. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + update. + display_name: Display name for the instance. Optional. + description: Description of the instance. Optional. + interval_seconds: Interval in seconds for recurring + execution. Optional. + timeout_seconds: Timeout in seconds for execution. Optional. + parameters: List of parameters for the instance. Optional. + agent: Agent identifier for remote execution. Optional. + allow_list: List of allowed IP addresses. Optional. + product_field_name: Product field name. Optional. + event_field_name: Event field name. Optional. + integration_version: Integration version. Optional. + version: Version. Optional. + logging_enabled_until_unix_ms: Logging enabled until + timestamp. Optional. + enabled: Whether the instance is enabled. Optional. + update_mask: Comma-separated list of fields to update. If + omitted, all provided fields will be updated. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _update_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + display_name=display_name, + description=description, + interval_seconds=interval_seconds, + timeout_seconds=timeout_seconds, + parameters=parameters, + allow_list=allow_list, + product_field_name=product_field_name, + event_field_name=event_field_name, + integration_version=integration_version, + version=version, + logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, + enabled=enabled, + update_mask=update_mask, + api_version=api_version, + ) + + def get_connector_instance_latest_definition( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Fetch the latest definition for a connector instance. + + Use this method to refresh a connector instance with the latest + connector definition from the marketplace. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + refresh. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the refreshed ConnectorInstance with latest + definition. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance_latest_definition( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def set_connector_instance_logs_collection( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + enabled: bool, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Enable or disable logs collection for a connector instance. + + Use this method to control whether execution logs are collected + for a specific connector instance. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + configure. + enabled: Whether to enable or disable logs collection. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated logs collection status. + + Raises: + APIError: If the API request fails. + """ + return _set_connector_instance_logs_collection( + self, + integration_name, + connector_id, + connector_instance_id, + enabled, + api_version=api_version, + ) + + def run_connector_instance_on_demand( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + connector_instance: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Run a connector instance on demand for testing. + + Use this method to execute a connector instance immediately + without waiting for its scheduled run. Useful for testing + configuration changes. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to run. + connector_instance: The connector instance configuration to + test. Should include parameters and other settings. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the execution result, including success + status and debug output. + + Raises: + APIError: If the API request fails. + """ + return _run_connector_instance_on_demand( + self, + integration_name, + connector_id, + connector_instance_id, + connector_instance, + api_version=api_version, + ) + # ------------------------------------------------------------------------- # Integration Job methods # ------------------------------------------------------------------------- diff --git a/src/secops/chronicle/integration/connector_context_properties.py b/src/secops/chronicle/integration/connector_context_properties.py index 416e52dd..cf8b75e2 100644 --- a/src/secops/chronicle/integration/connector_context_properties.py +++ b/src/secops/chronicle/integration/connector_context_properties.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Integration job instances functionality for Chronicle.""" +"""Integration connector context properties functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/connector_instance_logs.py b/src/secops/chronicle/integration/connector_instance_logs.py index 76fe2c19..0be7bd25 100644 --- a/src/secops/chronicle/integration/connector_instance_logs.py +++ b/src/secops/chronicle/integration/connector_instance_logs.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Integration job instances functionality for Chronicle.""" +"""Integration connector instance logs functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/connector_instances.py b/src/secops/chronicle/integration/connector_instances.py new file mode 100644 index 00000000..33385499 --- /dev/null +++ b/src/secops/chronicle/integration/connector_instances.py @@ -0,0 +1,489 @@ +# Copyright 2025 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. +# +"""Integration job instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + ConnectorInstanceParameter, +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_instances( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration connector. + + Use this method to discover all configured instances of a connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list instances for. + page_size: Maximum number of connector instances to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter connector instances. + order_by: Field to sort the connector instances by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of connector instances instead of a + dict with connector instances list and nextPageToken. + + Returns: + If as_list is True: List of connector instances. + If as_list is False: Dict with connector instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances" + ), + items_key="connectorInstances", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single instance for a specific integration connector. + + Use this method to retrieve the configuration and status of a specific + connector instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ConnectorInstance. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + ) + + +def delete_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific connector instance. + + Use this method to permanently remove a data ingestion stream. For remote + connectors, the associated agent must be live and have no pending packages. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + ) + + +def create_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + environment: str, + display_name: str, + interval_seconds: int, + timeout_seconds: int, + description: str | None = None, + agent: str | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, + connector_instance_id: str | None = None, + enabled: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new connector instance for a specific integration connector. + + Use this method to establish a new data ingestion stream from a security + product. Note that agent and remote cannot be patched after creation. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create an instance for. + environment: Connector instance environment. Cannot be patched for + remote connectors. Required. + display_name: Connector instance display name. Required. + interval_seconds: Connector instance execution interval in seconds. + Required. + timeout_seconds: Timeout of a single Python script run. Required. + description: Connector instance description. Optional. + agent: Agent identifier for a remote connector instance. Cannot be + patched after creation. Optional. + allow_list: Connector instance allow list. Optional. + product_field_name: Connector's device product field. Optional. + event_field_name: Connector's event name field. Optional. + integration_version: The integration version. Optional. + version: The connector instance version. Optional. + logging_enabled_until_unix_ms: Timeout when log collecting will be + disabled. Optional. + parameters: List of ConnectorInstanceParameter instances or dicts. + Optional. + connector_instance_id: The connector instance id. Optional. + enabled: Whether the connector instance is enabled. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body = { + "environment": environment, + "displayName": display_name, + "intervalSeconds": interval_seconds, + "timeoutSeconds": timeout_seconds, + "description": description, + "agent": agent, + "allowList": allow_list, + "productFieldName": product_field_name, + "eventFieldName": event_field_name, + "integrationVersion": integration_version, + "version": version, + "loggingEnabledUntilUnixMs": logging_enabled_until_unix_ms, + "parameters": resolved_parameters, + "id": connector_instance_id, + "enabled": enabled, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances" + ), + api_version=api_version, + json=body, + ) + + +def update_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + display_name: str | None = None, + description: str | None = None, + interval_seconds: int | None = None, + timeout_seconds: int | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, + enabled: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing connector instance. + + Use this method to enable or disable a connector, change its display + name, or adjust its ingestion parameters. Note that agent, remote, and + environment cannot be patched after creation. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to update. + display_name: Connector instance display name. + description: Connector instance description. + interval_seconds: Connector instance execution interval in seconds. + timeout_seconds: Timeout of a single Python script run. + allow_list: Connector instance allow list. + product_field_name: Connector's device product field. + event_field_name: Connector's event name field. + integration_version: The integration version. Required on patch if + provided. + version: The connector instance version. + logging_enabled_until_unix_ms: Timeout when log collecting will be + disabled. + parameters: List of ConnectorInstanceParameter instances or dicts. + enabled: Whether the connector instance is enabled. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,intervalSeconds". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("description", "description", description), + ("intervalSeconds", "intervalSeconds", interval_seconds), + ("timeoutSeconds", "timeoutSeconds", timeout_seconds), + ("allowList", "allowList", allow_list), + ("productFieldName", "productFieldName", product_field_name), + ("eventFieldName", "eventFieldName", event_field_name), + ("integrationVersion", "integrationVersion", integration_version), + ("version", "version", version), + ( + "loggingEnabledUntilUnixMs", + "loggingEnabledUntilUnixMs", + logging_enabled_until_unix_ms, + ), + ("parameters", "parameters", resolved_parameters), + ("id", "id", connector_instance_id), + ("enabled", "enabled", enabled), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def get_connector_instance_latest_definition( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Refresh a connector instance with the latest definition. + + Use this method to discover new parameters or updated scripts for an + existing connector instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to refresh. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the refreshed ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:fetchLatestDefinition" + ), + api_version=api_version, + ) + + +def set_connector_instance_logs_collection( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + enabled: bool, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Enable or disable debug log collection for a connector instance. + + When enabled is set to True, existing logs are cleared and a new + collection period is started. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to configure. + enabled: Whether logs collection is enabled for the connector + instance. Required. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the log enable expiration time in unix ms. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:setLogsCollection" + ), + api_version=api_version, + json={"enabled": enabled}, + ) + + +def run_connector_instance_on_demand( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + connector_instance: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Trigger an immediate, single execution of a connector instance. + + Use this method for testing configuration changes or manually + force-starting a data ingestion cycle. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to run. + connector_instance: Dict containing the ConnectorInstance with + values to use for the run. Required. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the run results with the following fields: + - debugOutput: The execution debug output message. + - success: True if the execution was successful. + - sampleCases: List of alerts produced by the connector run. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:runOnDemand" + ), + api_version=api_version, + json={"connectorInstance": connector_instance}, + ) diff --git a/src/secops/chronicle/integration/connector_revisions.py b/src/secops/chronicle/integration/connector_revisions.py index 128e9d05..146dc873 100644 --- a/src/secops/chronicle/integration/connector_revisions.py +++ b/src/secops/chronicle/integration/connector_revisions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Integration job instances functionality for Chronicle.""" +"""Integration connector revisions functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index b52f729a..f199c50a 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -611,6 +611,34 @@ def to_dict(self) -> dict: return data +class ConnectorConnectivityStatus(str, Enum): + """Connectivity status for Chronicle SOAR connector instances.""" + + LIVE = "LIVE" + NOT_LIVE = "NOT_LIVE" + + +@dataclass +class ConnectorInstanceParameter: + """A parameter instance for a Chronicle SOAR connector instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring a connector instance. + + Attributes: + value: The value of the parameter. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/tests/chronicle/integration/test_connector_instances.py b/tests/chronicle/integration/test_connector_instances.py new file mode 100644 index 00000000..25bf3abe --- /dev/null +++ b/tests/chronicle/integration/test_connector_instances.py @@ -0,0 +1,845 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration connector instances functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + ConnectorInstanceParameter, +) +from secops.chronicle.integration.connector_instances import ( + list_connector_instances, + get_connector_instance, + delete_connector_instance, + create_connector_instance, + update_connector_instance, + get_connector_instance_latest_definition, + set_connector_instance_logs_collection, + run_connector_instance_on_demand, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_instances tests -- + + +def test_list_connector_instances_success(chronicle_client): + """Test list_connector_instances delegates to chronicle_paginated_request.""" + expected = { + "connectorInstances": [{"name": "ci1"}, {"name": "ci2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_instances.format_resource_id", + return_value="My Integration", + ): + result = list_connector_instances( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/connectorInstances" in kwargs["path"] + assert kwargs["items_key"] == "connectorInstances" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_instances_default_args(chronicle_client): + """Test list_connector_instances with default args.""" + expected = {"connectorInstances": []} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_connector_instances_with_filters(chronicle_client): + """Test list_connector_instances with filter and order_by.""" + expected = {"connectorInstances": [{"name": "ci1"}]} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='enabled = true', + order_by="displayName", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'enabled = true', + "orderBy": "displayName", + } + + +def test_list_connector_instances_as_list(chronicle_client): + """Test list_connector_instances returns list when as_list=True.""" + expected = [{"name": "ci1"}, {"name": "ci2"}] + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_instances_error(chronicle_client): + """Test list_connector_instances raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + side_effect=APIError("Failed to list connector instances"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list connector instances" in str(exc_info.value) + + +# -- get_connector_instance tests -- + + +def test_get_connector_instance_success(chronicle_client): + """Test get_connector_instance issues GET request.""" + expected = { + "name": "connectorInstances/ci1", + "displayName": "Test Instance", + "enabled": True, + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_error(chronicle_client): + """Test get_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to get connector instance"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to get connector instance" in str(exc_info.value) + + +# -- delete_connector_instance tests -- + + +def test_delete_connector_instance_success(chronicle_client): + """Test delete_connector_instance issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_connector_instance_error(chronicle_client): + """Test delete_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to delete connector instance"), + ): + with pytest.raises(APIError) as exc_info: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to delete connector instance" in str(exc_info.value) + + +# -- create_connector_instance tests -- + + +def test_create_connector_instance_required_fields_only(chronicle_client): + """Test create_connector_instance with required fields only.""" + expected = {"name": "connectorInstances/new", "displayName": "New Instance"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + ) + + assert result == expected + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/connectorInstances" in kwargs["endpoint_path"] + assert kwargs["json"]["environment"] == "production" + assert kwargs["json"]["displayName"] == "New Instance" + assert kwargs["json"]["intervalSeconds"] == 3600 + assert kwargs["json"]["timeoutSeconds"] == 300 + + +def test_create_connector_instance_with_optional_fields(chronicle_client): + """Test create_connector_instance includes optional fields when provided.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Test description", + agent="agent-123", + allow_list=["192.168.1.0/24"], + product_field_name="product", + event_field_name="event", + integration_version="1.0.0", + version="2.0.0", + logging_enabled_until_unix_ms="1234567890000", + connector_instance_id="custom-id", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test description" + assert kwargs["json"]["agent"] == "agent-123" + assert kwargs["json"]["allowList"] == ["192.168.1.0/24"] + assert kwargs["json"]["productFieldName"] == "product" + assert kwargs["json"]["eventFieldName"] == "event" + assert kwargs["json"]["integrationVersion"] == "1.0.0" + assert kwargs["json"]["version"] == "2.0.0" + assert kwargs["json"]["loggingEnabledUntilUnixMs"] == "1234567890000" + assert kwargs["json"]["id"] == "custom-id" + assert kwargs["json"]["enabled"] is True + + +def test_create_connector_instance_with_parameters(chronicle_client): + """Test create_connector_instance with ConnectorInstanceParameter objects.""" + expected = {"name": "connectorInstances/new"} + + param = ConnectorInstanceParameter() + param.value = "secret-key" + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert len(kwargs["json"]["parameters"]) == 1 + assert kwargs["json"]["parameters"][0]["value"] == "secret-key" + + +def test_create_connector_instance_with_dict_parameters(chronicle_client): + """Test create_connector_instance with dict parameters.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + parameters=[{"displayName": "API Key", "value": "secret-key"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["parameters"][0]["displayName"] == "API Key" + + +def test_create_connector_instance_error(chronicle_client): + """Test create_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to create connector instance"), + ): + with pytest.raises(APIError) as exc_info: + create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + ) + assert "Failed to create connector instance" in str(exc_info.value) + + +# -- update_connector_instance tests -- + + +def test_update_connector_instance_success(chronicle_client): + """Test update_connector_instance updates fields.""" + expected = {"name": "connectorInstances/ci1", "displayName": "Updated"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "PATCH" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["json"]["displayName"] == "Updated" + assert kwargs["json"]["enabled"] is True + # Check that update mask contains the expected fields + assert "displayName" in kwargs["params"]["updateMask"] + assert "enabled" in kwargs["params"]["updateMask"] + + +def test_update_connector_instance_with_custom_mask(chronicle_client): + """Test update_connector_instance with custom update_mask.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + update_mask="displayName", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["params"]["updateMask"] == "displayName" + + +def test_update_connector_instance_with_parameters(chronicle_client): + """Test update_connector_instance with parameters.""" + expected = {"name": "connectorInstances/ci1"} + + param = ConnectorInstanceParameter() + param.value = "new-key" + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert len(kwargs["json"]["parameters"]) == 1 + assert kwargs["json"]["parameters"][0]["value"] == "new-key" + + +def test_update_connector_instance_error(chronicle_client): + """Test update_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to update connector instance"), + ): + with pytest.raises(APIError) as exc_info: + update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + ) + assert "Failed to update connector instance" in str(exc_info.value) + + +# -- get_connector_instance_latest_definition tests -- + + +def test_get_connector_instance_latest_definition_success(chronicle_client): + """Test get_connector_instance_latest_definition issues GET request.""" + expected = { + "name": "connectorInstances/ci1", + "displayName": "Refreshed Instance", + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectorInstances/ci1:fetchLatestDefinition" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_latest_definition_error(chronicle_client): + """Test get_connector_instance_latest_definition raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to fetch latest definition"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to fetch latest definition" in str(exc_info.value) + + +# -- set_connector_instance_logs_collection tests -- + + +def test_set_connector_instance_logs_collection_enable(chronicle_client): + """Test set_connector_instance_logs_collection enables logs.""" + expected = {"loggingEnabledUntilUnixMs": "1234567890000"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectorInstances/ci1:setLogsCollection" in kwargs["endpoint_path"] + assert kwargs["json"]["enabled"] is True + + +def test_set_connector_instance_logs_collection_disable(chronicle_client): + """Test set_connector_instance_logs_collection disables logs.""" + expected = {} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=False, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["enabled"] is False + + +def test_set_connector_instance_logs_collection_error(chronicle_client): + """Test set_connector_instance_logs_collection raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to set logs collection"), + ): + with pytest.raises(APIError) as exc_info: + set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + ) + assert "Failed to set logs collection" in str(exc_info.value) + + +# -- run_connector_instance_on_demand tests -- + + +def test_run_connector_instance_on_demand_success(chronicle_client): + """Test run_connector_instance_on_demand triggers execution.""" + expected = { + "debugOutput": "Execution completed", + "success": True, + "sampleCases": [], + } + + connector_instance = { + "name": "connectorInstances/ci1", + "displayName": "Test Instance", + "parameters": [{"displayName": "param1", "value": "value1"}], + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectorInstances/ci1:runOnDemand" in kwargs["endpoint_path"] + assert kwargs["json"]["connectorInstance"] == connector_instance + + +def test_run_connector_instance_on_demand_error(chronicle_client): + """Test run_connector_instance_on_demand raises APIError on failure.""" + connector_instance = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to run connector instance"), + ): + with pytest.raises(APIError) as exc_info: + run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + ) + assert "Failed to run connector instance" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_instances_custom_api_version(chronicle_client): + """Test list_connector_instances with custom API version.""" + expected = {"connectorInstances": []} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_custom_api_version(chronicle_client): + """Test get_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_connector_instance_custom_api_version(chronicle_client): + """Test delete_connector_instance with custom API version.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_create_connector_instance_custom_api_version(chronicle_client): + """Test create_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_update_connector_instance_custom_api_version(chronicle_client): + """Test update_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_latest_definition_custom_api_version(chronicle_client): + """Test get_connector_instance_latest_definition with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_set_connector_instance_logs_collection_custom_api_version(chronicle_client): + """Test set_connector_instance_logs_collection with custom API version.""" + expected = {} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_run_connector_instance_on_demand_custom_api_version(chronicle_client): + """Test run_connector_instance_on_demand with custom API version.""" + expected = {"success": True} + connector_instance = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + + + From 40a2e859b9eec10f14b054571e95191b13dcf1c5 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 15:11:46 +0000 Subject: [PATCH 37/46] feat: add functions for integration action revisions functions --- README.md | 126 ++++++ api_module_mapping.md | 12 +- src/secops/chronicle/__init__.py | 11 + src/secops/chronicle/client.py | 167 +++++++ .../chronicle/integration/action_revisions.py | 201 +++++++++ .../connector_context_properties.py | 4 +- .../integration/connector_instances.py | 2 +- .../integration/connector_revisions.py | 2 +- .../chronicle/integration/connectors.py | 2 +- src/secops/chronicle/integration/jobs.py | 2 +- .../integration/test_action_revisions.py | 409 ++++++++++++++++++ 11 files changed, 930 insertions(+), 8 deletions(-) create mode 100644 src/secops/chronicle/integration/action_revisions.py create mode 100644 tests/chronicle/integration/test_action_revisions.py diff --git a/README.md b/README.md index 25e353d4..8964849f 100644 --- a/README.md +++ b/README.md @@ -2076,6 +2076,132 @@ Get a template for creating an action in an integration template = chronicle.get_integration_action_template("MyIntegration") ``` +### Integration Action Revisions + +List all revisions for an action: + +```python +# Get all revisions for an action +revisions = chronicle.list_integration_action_revisions( + integration_name="MyIntegration", + action_id="123" +) +for revision in revisions.get("revisions", []): + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_action_revisions( + integration_name="MyIntegration", + action_id="123", + as_list=True +) + +# Filter revisions +revisions = chronicle.list_integration_action_revisions( + integration_name="MyIntegration", + action_id="123", + filter_string='version = "1.0"', + order_by="createTime desc" +) +``` + +Delete a specific action revision: + +```python +chronicle.delete_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + revision_id="rev-456" +) +``` + +Create a new revision before making changes: + +```python +# Get the current action +action = chronicle.get_integration_action( + integration_name="MyIntegration", + action_id="123" +) + +# Create a backup revision +new_revision = chronicle.create_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + action=action, + comment="Backup before major refactor" +) +print(f"Created revision: {new_revision.get('name')}") + +# Create revision with custom comment +new_revision = chronicle.create_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + action=action, + comment="Version 2.0 - Added error handling" +) +``` + +Rollback to a previous revision: + +```python +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + revision_id="rev-456" +) +print(f"Rolled back to: {rollback_result.get('name')}") +``` + +Example workflow: Safe action updates with revision control: + +```python +# 1. Get the current action +action = chronicle.get_integration_action( + integration_name="MyIntegration", + action_id="123" +) + +# 2. Create a backup revision +backup = chronicle.create_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + action=action, + comment="Backup before updating logic" +) + +# 3. Make changes to the action +updated_action = chronicle.update_integration_action( + integration_name="MyIntegration", + action_id="123", + display_name="Updated Action", + script=""" +def main(context): + # New logic here + return {"status": "success"} +""" +) + +# 4. Test the updated action +test_result = chronicle.execute_integration_action_test( + integration_name="MyIntegration", + action_id="123", + action=updated_action +) + +# 5. If test fails, rollback to backup +if not test_result.get("successful"): + print("Test failed - rolling back") + chronicle.rollback_integration_action_revision( + integration_name="MyIntegration", + action_id="123", + revision_id=backup.get("name").split("/")[-1] + ) +else: + print("Test passed - changes saved") +``` + ### Integration Connectors List all available connectors for an integration: diff --git a/api_module_mapping.md b/api_module_mapping.md index 8ce062cd..c3fb14f5 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,8 +7,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1beta:** 84 endpoints implemented -- **v1alpha:** 177 endpoints implemented +- **v1beta:** 88 endpoints implemented +- **v1alpha:** 181 endpoints implemented ## Endpoint Mapping @@ -91,6 +91,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | | | integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | | | integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | | +| integrations.actions.revisions.create | v1beta | chronicle.integration.action_revisions.create_integration_action_revision | | +| integrations.actions.revisions.delete | v1beta | chronicle.integration.action_revisions.delete_integration_action_revision | | +| integrations.actions.revisions.list | v1beta | chronicle.integration.action_revisions.list_integration_action_revisions | | +| integrations.actions.revisions.rollback | v1beta | chronicle.integration.action_revisions.rollback_integration_action_revision | | | integrations.connectors.create | v1beta | chronicle.integration.connectors.create_integration_connector | | | integrations.connectors.delete | v1beta | chronicle.integration.connectors.delete_integration_connector | | | integrations.connectors.executeTest | v1beta | chronicle.integration.connectors.execute_integration_connector_test | | @@ -365,6 +369,10 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | | | integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | | | integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.revisions.create | v1alpha | chronicle.integration.action_revisions.create_integration_action_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.revisions.delete | v1alpha | chronicle.integration.action_revisions.delete_integration_action_revision(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.revisions.list | v1alpha | chronicle.integration.action_revisions.list_integration_action_revisions(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.revisions.rollback | v1alpha | chronicle.integration.action_revisions.rollback_integration_action_revision(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.create | v1alpha | chronicle.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.delete | v1alpha | chronicle.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | | | integrations.connectors.executeTest | v1alpha | chronicle.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index bfd7069a..8a0c640e 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -236,6 +236,12 @@ get_integration_actions_by_environment, get_integration_action_template, ) +from secops.chronicle.integration.action_revisions import ( + list_integration_action_revisions, + delete_integration_action_revision, + create_integration_action_revision, + rollback_integration_action_revision, +) from secops.chronicle.integration.connectors import ( list_integration_connectors, get_integration_connector, @@ -539,6 +545,11 @@ "execute_integration_action_test", "get_integration_actions_by_environment", "get_integration_action_template", + # Integration Action Revisions + "list_integration_action_revisions", + "delete_integration_action_revision", + "create_integration_action_revision", + "rollback_integration_action_revision", # Integration Connectors "list_integration_connectors", "get_integration_connector", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 31a60775..d3b0718d 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -164,6 +164,12 @@ list_integration_actions as _list_integration_actions, update_integration_action as _update_integration_action, ) +from secops.chronicle.integration.action_revisions import ( + create_integration_action_revision as _create_integration_action_revision, + delete_integration_action_revision as _delete_integration_action_revision, + list_integration_action_revisions as _list_integration_action_revisions, + rollback_integration_action_revision as _rollback_integration_action_revision, +) from secops.chronicle.integration.connectors import ( create_integration_connector as _create_integration_connector, delete_integration_connector as _delete_integration_connector, @@ -1852,6 +1858,167 @@ def get_integration_action_template( api_version=api_version, ) + # ------------------------------------------------------------------------- + # Integration Action Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_action_revisions( + self, + integration_name: str, + action_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration action. + + Use this method to view the history of changes to an action, + enabling version control and the ability to rollback to + previous configurations. + + Args: + integration_name: Name of the integration the action + belongs to. + action_id: ID of the action to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. + + Returns: + If as_list is True: List of action revisions. + If as_list is False: Dict with action revisions list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_action_revisions( + self, + integration_name, + action_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_action_revision( + self, + integration_name: str, + action_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific action revision. + + Use this method to permanently remove a revision from the + action's history. + + Args: + integration_name: Name of the integration the action + belongs to. + action_id: ID of the action the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_action_revision( + self, + integration_name, + action_id, + revision_id, + api_version=api_version, + ) + + def create_integration_action_revision( + self, + integration_name: str, + action_id: str, + action: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new revision for an integration action. + + Use this method to save a snapshot of the current action + configuration before making changes, enabling easy rollback if + needed. + + Args: + integration_name: Name of the integration the action + belongs to. + action_id: ID of the action to create a revision for. + action: The action object to save as a revision. + comment: Optional comment describing the revision. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ActionRevision resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_action_revision( + self, + integration_name, + action_id, + action, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_action_revision( + self, + integration_name: str, + action_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Rollback an integration action to a previous revision. + + Use this method to restore an action to a previously saved + state, reverting any changes made since that revision. + + Args: + integration_name: Name of the integration the action + belongs to. + action_id: ID of the action to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the rolled back IntegrationAction resource. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_action_revision( + self, + integration_name, + action_id, + revision_id, + api_version=api_version, + ) + # ------------------------------------------------------------------------- # Integration Connector methods # ------------------------------------------------------------------------- diff --git a/src/secops/chronicle/integration/action_revisions.py b/src/secops/chronicle/integration/action_revisions.py new file mode 100644 index 00000000..b5229b3c --- /dev/null +++ b/src/secops/chronicle/integration/action_revisions.py @@ -0,0 +1,201 @@ +# Copyright 2025 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. +# +"""Integration action revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_action_revisions( + client: "ChronicleClient", + integration_name: str, + action_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration action. + + Use this method to browse the version history and identify previous + configurations of an automated task. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"actions/{action_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_action_revision( + client: "ChronicleClient", + integration_name: str, + action_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific revision for a given integration action. + + Use this method to clean up obsolete action revisions. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + action_id: ID of the action the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"actions/{action_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_action_revision( + client: "ChronicleClient", + integration_name: str, + action_id: str, + action: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration action. + + Use this method to establish a recovery point before making significant + changes to a security operation's script or parameters. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to create a revision for. + action: Dict containing the IntegrationAction to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationActionRevision resource. + + Raises: + APIError: If the API request fails. + """ + body = {"action": action} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"actions/{action_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_action_revision( + client: "ChronicleClient", + integration_name: str, + action_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Revert the current action definition to a previously saved revision. + + Use this method to rapidly recover a functional automation state if an + update causes operational issues. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationActionRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"actions/{action_id}/revisions/{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/connector_context_properties.py b/src/secops/chronicle/integration/connector_context_properties.py index cf8b75e2..24e59f66 100644 --- a/src/secops/chronicle/integration/connector_context_properties.py +++ b/src/secops/chronicle/integration/connector_context_properties.py @@ -190,7 +190,7 @@ def create_connector_context_property( Raises: APIError: If the API request fails. """ - body: dict[str, Any] = {"value": value} + body = {"value": value} if key is not None: body["key"] = key @@ -282,7 +282,7 @@ def delete_all_connector_context_properties( Raises: APIError: If the API request fails. """ - body: dict[str, Any] = {} + body = {} if context_id is not None: body["contextId"] = context_id diff --git a/src/secops/chronicle/integration/connector_instances.py b/src/secops/chronicle/integration/connector_instances.py index 33385499..c6b563cc 100644 --- a/src/secops/chronicle/integration/connector_instances.py +++ b/src/secops/chronicle/integration/connector_instances.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -"""Integration job instances functionality for Chronicle.""" +"""Integration connector instances functionality for Chronicle.""" from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/integration/connector_revisions.py b/src/secops/chronicle/integration/connector_revisions.py index 146dc873..a5908864 100644 --- a/src/secops/chronicle/integration/connector_revisions.py +++ b/src/secops/chronicle/integration/connector_revisions.py @@ -149,7 +149,7 @@ def create_integration_connector_revision( Raises: APIError: If the API request fails. """ - body: dict[str, Any] = {"connector": connector} + body = {"connector": connector} if comment is not None: body["comment"] = comment diff --git a/src/secops/chronicle/integration/connectors.py b/src/secops/chronicle/integration/connectors.py index 3978ce0d..b2c0ccd1 100644 --- a/src/secops/chronicle/integration/connectors.py +++ b/src/secops/chronicle/integration/connectors.py @@ -358,7 +358,7 @@ def execute_integration_connector_test( Raises: APIError: If the API request fails. """ - body: dict[str, Any] = {"connector": connector} + body = {"connector": connector} if agent_identifier is not None: body["agentIdentifier"] = agent_identifier diff --git a/src/secops/chronicle/integration/jobs.py b/src/secops/chronicle/integration/jobs.py index cbcbc410..b7600a76 100644 --- a/src/secops/chronicle/integration/jobs.py +++ b/src/secops/chronicle/integration/jobs.py @@ -323,7 +323,7 @@ def execute_integration_job_test( Raises: APIError: If the API request fails. """ - body: dict[str, Any] = {"job": job} + body = {"job": job} if agent_identifier is not None: body["agentIdentifier"] = agent_identifier diff --git a/tests/chronicle/integration/test_action_revisions.py b/tests/chronicle/integration/test_action_revisions.py new file mode 100644 index 00000000..f9abd9bc --- /dev/null +++ b/tests/chronicle/integration/test_action_revisions.py @@ -0,0 +1,409 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle marketplace integration action revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.action_revisions import ( + list_integration_action_revisions, + delete_integration_action_revision, + create_integration_action_revision, + rollback_integration_action_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_action_revisions tests -- + + +def test_list_integration_action_revisions_success(chronicle_client): + """Test list_integration_action_revisions delegates to chronicle_paginated_request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.action_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_action_revisions( + chronicle_client, + integration_name="My Integration", + action_id="a1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "actions/a1/revisions" in kwargs["path"] + assert kwargs["items_key"] == "revisions" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_action_revisions_default_args(chronicle_client): + """Test list_integration_action_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_action_revisions( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + + assert result == expected + + +def test_list_integration_action_revisions_with_filters(chronicle_client): + """Test list_integration_action_revisions with filter and order_by.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_action_revisions( + chronicle_client, + integration_name="test-integration", + action_id="a1", + filter_string='version = "1.0"', + order_by="createTime", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'version = "1.0"', + "orderBy": "createTime", + } + + +def test_list_integration_action_revisions_as_list(chronicle_client): + """Test list_integration_action_revisions returns list when as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_action_revisions( + chronicle_client, + integration_name="test-integration", + action_id="a1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_action_revisions_error(chronicle_client): + """Test list_integration_action_revisions raises APIError on failure.""" + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + side_effect=APIError("Failed to list action revisions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_action_revisions( + chronicle_client, + integration_name="test-integration", + action_id="a1", + ) + assert "Failed to list action revisions" in str(exc_info.value) + + +# -- delete_integration_action_revision tests -- + + +def test_delete_integration_action_revision_success(chronicle_client): + """Test delete_integration_action_revision issues DELETE request.""" + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "actions/a1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_action_revision_error(chronicle_client): + """Test delete_integration_action_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + side_effect=APIError("Failed to delete action revision"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + ) + assert "Failed to delete action revision" in str(exc_info.value) + + +# -- create_integration_action_revision tests -- + + +def test_create_integration_action_revision_success(chronicle_client): + """Test create_integration_action_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "comment": "Test revision", + } + + action = { + "name": "actions/a1", + "displayName": "Test Action", + "code": "print('hello')", + } + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + action=action, + comment="Test revision", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "actions/a1/revisions" in kwargs["endpoint_path"] + assert kwargs["json"]["action"] == action + assert kwargs["json"]["comment"] == "Test revision" + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_create_integration_action_revision_without_comment(chronicle_client): + """Test create_integration_action_revision without comment.""" + expected = {"name": "revisions/r1"} + + action = { + "name": "actions/a1", + "displayName": "Test Action", + } + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + action=action, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["action"] == action + assert "comment" not in kwargs["json"] + + +def test_create_integration_action_revision_error(chronicle_client): + """Test create_integration_action_revision raises APIError on failure.""" + action = {"name": "actions/a1"} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + side_effect=APIError("Failed to create action revision"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + action=action, + ) + assert "Failed to create action revision" in str(exc_info.value) + + +# -- rollback_integration_action_revision tests -- + + +def test_rollback_integration_action_revision_success(chronicle_client): + """Test rollback_integration_action_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "comment": "Rolled back", + } + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "actions/a1/revisions/r1:rollback" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_rollback_integration_action_revision_error(chronicle_client): + """Test rollback_integration_action_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + side_effect=APIError("Failed to rollback action revision"), + ): + with pytest.raises(APIError) as exc_info: + rollback_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + ) + assert "Failed to rollback action revision" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_action_revisions_custom_api_version(chronicle_client): + """Test list_integration_action_revisions with custom API version.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_action_revisions( + chronicle_client, + integration_name="test-integration", + action_id="a1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_integration_action_revision_custom_api_version(chronicle_client): + """Test delete_integration_action_revision with custom API version.""" + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_create_integration_action_revision_custom_api_version(chronicle_client): + """Test create_integration_action_revision with custom API version.""" + expected = {"name": "revisions/r1"} + action = {"name": "actions/a1"} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + action=action, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_rollback_integration_action_revision_custom_api_version(chronicle_client): + """Test rollback_integration_action_revision with custom API version.""" + expected = {"name": "revisions/r1"} + + with patch( + "secops.chronicle.integration.action_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_action_revision( + chronicle_client, + integration_name="test-integration", + action_id="a1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + From bb04df60549fa684c5d287b43f3959d0ec3b4955 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Mon, 9 Mar 2026 19:53:44 +0000 Subject: [PATCH 38/46] feat: implement integration CLI functions --- CLI.md | 1326 ++++++++++++++++- src/secops/chronicle/client.py | 2 +- .../commands/integration/action_revisions.py | 215 +++ .../cli/commands/integration/actions.py | 382 +++++ .../connector_context_properties.py | 375 +++++ .../integration/connector_instance_logs.py | 142 ++ .../integration/connector_instances.py | 473 ++++++ .../integration/connector_revisions.py | 217 +++ .../cli/commands/integration/connectors.py | 325 ++++ .../integration/integration_client.py | 40 +- .../integration/integration_instances.py | 392 +++++ .../integration/job_context_properties.py | 354 +++++ .../commands/integration/job_instance_logs.py | 140 ++ .../cli/commands/integration/job_instances.py | 407 +++++ .../cli/commands/integration/job_revisions.py | 213 +++ src/secops/cli/commands/integration/jobs.py | 356 +++++ .../commands/integration/manager_revisions.py | 254 ++++ .../cli/commands/integration/managers.py | 283 ++++ 18 files changed, 5856 insertions(+), 40 deletions(-) create mode 100644 src/secops/cli/commands/integration/action_revisions.py create mode 100644 src/secops/cli/commands/integration/actions.py create mode 100644 src/secops/cli/commands/integration/connector_context_properties.py create mode 100644 src/secops/cli/commands/integration/connector_instance_logs.py create mode 100644 src/secops/cli/commands/integration/connector_instances.py create mode 100644 src/secops/cli/commands/integration/connector_revisions.py create mode 100644 src/secops/cli/commands/integration/connectors.py create mode 100644 src/secops/cli/commands/integration/integration_instances.py create mode 100644 src/secops/cli/commands/integration/job_context_properties.py create mode 100644 src/secops/cli/commands/integration/job_instance_logs.py create mode 100644 src/secops/cli/commands/integration/job_instances.py create mode 100644 src/secops/cli/commands/integration/job_revisions.py create mode 100644 src/secops/cli/commands/integration/jobs.py create mode 100644 src/secops/cli/commands/integration/manager_revisions.py create mode 100644 src/secops/cli/commands/integration/managers.py diff --git a/CLI.md b/CLI.md index d8270e22..1330ff7b 100644 --- a/CLI.md +++ b/CLI.md @@ -779,6 +779,1292 @@ Uninstall a marketplace integration: secops integration marketplace uninstall --integration-name "AWSSecurityHub" ``` +#### Integration Actions + +List integration actions: + +```bash +# List all actions for an integration +secops integration actions list --integration-name "MyIntegration" + +# List actions as a direct list (fetches all pages automatically) +secops integration actions list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration actions list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration actions list --integration-name "MyIntegration" --filter-string "enabled = true" +``` + +Get action details: + +```bash +secops integration actions get --integration-name "MyIntegration" --action-id "123" +``` + +Create a new action: + +```bash +# Create a basic action with Python code +secops integration actions create \ + --integration-name "MyIntegration" \ + --display-name "Send Alert" \ + --code "def main(context): return {'status': 'success'}" + +# Create an async action +secops integration actions create \ + --integration-name "MyIntegration" \ + --display-name "Async Task" \ + --code "async def main(context): return await process()" \ + --is-async + +# Create with description +secops integration actions create \ + --integration-name "MyIntegration" \ + --display-name "My Action" \ + --code "def main(context): return {}" \ + --description "Action description" +``` + +> **Note:** When creating an action, the following default values are automatically applied: +> - `timeout_seconds`: 300 (5 minutes) +> - `enabled`: true +> - `script_result_name`: "result" +> +> The `--code` parameter contains the Python script that will be executed by the action. + +Update an existing action: + +```bash +# Update display name +secops integration actions update \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --display-name "Updated Action Name" + +# Update code +secops integration actions update \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --code "def main(context): return {'status': 'updated'}" + +# Update multiple fields with update mask +secops integration actions update \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete an action: + +```bash +secops integration actions delete --integration-name "MyIntegration" --action-id "123" +``` + +Test an action: + +```bash +# Test an action to verify it executes correctly +secops integration actions test --integration-name "MyIntegration" --action-id "123" +``` + +Get action template: + +```bash +# Get synchronous action template +secops integration actions template --integration-name "MyIntegration" + +# Get asynchronous action template +secops integration actions template --integration-name "MyIntegration" --is-async +``` + +#### Action Revisions + +List action revisions: + +```bash +# List all revisions for an action +secops integration action-revisions list \ + --integration-name "MyIntegration" \ + --action-id "123" + +# List revisions as a direct list +secops integration action-revisions list \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --as-list + +# List with pagination +secops integration action-revisions list \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --page-size 10 + +# List with filtering and ordering +secops integration action-revisions list \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --filter-string 'version = "1.0"' \ + --order-by "createTime desc" +``` + +Create a revision backup: + +```bash +# Create revision with comment +secops integration action-revisions create \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --comment "Backup before major refactor" + +# Create revision without comment +secops integration action-revisions create \ + --integration-name "MyIntegration" \ + --action-id "123" +``` + +Rollback to a previous revision: + +```bash +secops integration action-revisions rollback \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --revision-id "r456" +``` + +Delete an old revision: + +```bash +secops integration action-revisions delete \ + --integration-name "MyIntegration" \ + --action-id "123" \ + --revision-id "r789" +``` + +#### Integration Connectors + +List integration connectors: + +```bash +# List all connectors for an integration +secops integration connectors list --integration-name "MyIntegration" + +# List connectors as a direct list (fetches all pages automatically) +secops integration connectors list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration connectors list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration connectors list --integration-name "MyIntegration" --filter-string "enabled = true" +``` + +Get connector details: + +```bash +secops integration connectors get --integration-name "MyIntegration" --connector-id "c1" +``` + +Create a new connector: + +```bash +secops integration connectors create \ + --integration-name "MyIntegration" \ + --display-name "Data Ingestion" \ + --code "def fetch_data(context): return []" + +# Create with description and custom ID +secops integration connectors create \ + --integration-name "MyIntegration" \ + --display-name "My Connector" \ + --code "def fetch_data(context): return []" \ + --description "Connector description" \ + --connector-id "custom-connector-id" +``` + +Update an existing connector: + +```bash +# Update display name +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --display-name "Updated Connector Name" + +# Update code +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --code "def fetch_data(context): return updated_data()" + +# Update multiple fields with update mask +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete a connector: + +```bash +secops integration connectors delete --integration-name "MyIntegration" --connector-id "c1" +``` + +Test a connector: + +```bash +secops integration connectors test --integration-name "MyIntegration" --connector-id "c1" +``` + +Get connector template: + +```bash +secops integration connectors template --integration-name "MyIntegration" +``` + +#### Connector Revisions + +List connector revisions: + +```bash +# List all revisions for a connector +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" + +# List revisions as a direct list +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --as-list + +# List with pagination +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --page-size 10 + +# List with filtering and ordering +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --filter-string 'version = "1.0"' \ + --order-by "createTime desc" +``` + +Create a revision backup: + +```bash +# Create revision with comment +secops integration connector-revisions create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --comment "Backup before field mapping changes" + +# Create revision without comment +secops integration connector-revisions create \ + --integration-name "MyIntegration" \ + --connector-id "c1" +``` + +Rollback to a previous revision: + +```bash +secops integration connector-revisions rollback \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --revision-id "r456" +``` + +Delete an old revision: + +```bash +secops integration connector-revisions delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --revision-id "r789" +``` + +#### Connector Context Properties + +List connector context properties: + +```bash +# List all properties for a connector context +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" + +# List properties as a direct list +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --as-list + +# List with pagination +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --page-size 50 + +# List with filtering +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --filter-string 'key = "last_run_time"' +``` + +Get a specific context property: + +```bash +secops integration connector-context-properties get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Create a new context property: + +```bash +# Store last run time +secops integration connector-context-properties create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --key "last_run_time" \ + --value "2026-03-09T10:00:00Z" + +# Store checkpoint for incremental sync +secops integration connector-context-properties create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --key "checkpoint" \ + --value "page_token_xyz123" +``` + +Update a context property: + +```bash +# Update last run time +secops integration connector-context-properties update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" \ + --value "2026-03-09T11:00:00Z" +``` + +Delete a context property: + +```bash +secops integration connector-context-properties delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Clear all context properties: + +```bash +# Clear all properties for a specific context +secops integration connector-context-properties clear-all \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" +``` + +#### Connector Instance Logs + +List connector instance logs: + +```bash +# List all logs for a connector instance +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" + +# List logs as a direct list +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --as-list + +# List with pagination +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --page-size 50 + +# List with filtering (filter by severity or timestamp) +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --filter-string 'severity = "ERROR"' \ + --order-by "createTime desc" +``` + +Get a specific log entry: + +```bash +secops integration connector-instance-logs get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --log-id "log456" +``` + +#### Connector Instances + +List connector instances: + +```bash +# List all instances for a connector +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" + +# List instances as a direct list +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --as-list + +# List with pagination +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --page-size 50 + +# List with filtering +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --filter-string 'enabled = true' +``` + +Get connector instance details: + +```bash +secops integration connector-instances get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Create a new connector instance: + +```bash +# Create basic connector instance +secops integration connector-instances create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --environment "production" \ + --display-name "Production Data Collector" + +# Create with schedule and timeout +secops integration connector-instances create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --environment "production" \ + --display-name "Hourly Sync" \ + --interval-seconds 3600 \ + --timeout-seconds 300 \ + --enabled +``` + +Update a connector instance: + +```bash +# Update display name +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --display-name "Updated Display Name" + +# Update interval and timeout +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --interval-seconds 7200 \ + --timeout-seconds 600 + +# Enable or disable instance +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled true + +# Update multiple fields with update mask +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --display-name "New Name" \ + --interval-seconds 3600 \ + --update-mask "displayName,intervalSeconds" +``` + +Delete a connector instance: + +```bash +secops integration connector-instances delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Fetch latest definition: + +```bash +# Get the latest definition of a connector instance +secops integration connector-instances fetch-latest \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Enable or disable log collection: + +```bash +# Enable log collection for debugging +secops integration connector-instances set-logs \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled true + +# Disable log collection +secops integration connector-instances set-logs \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled false +``` + +Run connector instance on demand: + +```bash +# Trigger an immediate execution for testing +secops integration connector-instances run-ondemand \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +#### Integration Jobs + +List integration jobs: + +```bash +# List all jobs for an integration +secops integration jobs list --integration-name "MyIntegration" + +# List jobs as a direct list (fetches all pages automatically) +secops integration jobs list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration jobs list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration jobs list --integration-name "MyIntegration" --filter-string "enabled = true" + +# Exclude staging jobs +secops integration jobs list --integration-name "MyIntegration" --exclude-staging +``` + +Get job details: + +```bash +secops integration jobs get --integration-name "MyIntegration" --job-id "job1" +``` + +Create a new job: + +```bash +secops integration jobs create \ + --integration-name "MyIntegration" \ + --display-name "Data Processing Job" \ + --code "def process_data(context): return {'status': 'processed'}" + +# Create with description and custom ID +secops integration jobs create \ + --integration-name "MyIntegration" \ + --display-name "Scheduled Report" \ + --code "def generate_report(context): return report_data()" \ + --description "Daily report generation job" \ + --job-id "daily-report-job" + +# Create with parameters +secops integration jobs create \ + --integration-name "MyIntegration" \ + --display-name "Configurable Job" \ + --code "def run(context, params): return process(params)" \ + --parameters '[{"name":"interval","type":"STRING","required":true}]' +``` + +Update an existing job: + +```bash +# Update display name +secops integration jobs update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --display-name "Updated Job Name" + +# Update code +secops integration jobs update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --code "def run(context): return {'status': 'updated'}" + +# Update multiple fields with update mask +secops integration jobs update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" + +# Update parameters +secops integration jobs update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --parameters '[{"name":"timeout","type":"INTEGER","required":false}]' +``` + +Delete a job: + +```bash +secops integration jobs delete --integration-name "MyIntegration" --job-id "job1" +``` + +Test a job: + +```bash +secops integration jobs test --integration-name "MyIntegration" --job-id "job1" +``` + +Get job template: + +```bash +secops integration jobs template --integration-name "MyIntegration" +``` + +#### Job Revisions + +List job revisions: + +```bash +# List all revisions for a job +secops integration job-revisions list \ + --integration-name "MyIntegration" \ + --job-id "job1" + +# List revisions as a direct list +secops integration job-revisions list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --as-list + +# List with pagination +secops integration job-revisions list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --page-size 10 + +# List with filtering and ordering +secops integration job-revisions list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --filter-string 'version = "1.0"' \ + --order-by "createTime desc" +``` + +Create a revision backup: + +```bash +# Create revision with comment +secops integration job-revisions create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --comment "Backup before refactoring job logic" + +# Create revision without comment +secops integration job-revisions create \ + --integration-name "MyIntegration" \ + --job-id "job1" +``` + +Rollback to a previous revision: + +```bash +secops integration job-revisions rollback \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --revision-id "r456" +``` + +Delete an old revision: + +```bash +secops integration job-revisions delete \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --revision-id "r789" +``` + +#### Job Context Properties + +List job context properties: + +```bash +# List all properties for a job context +secops integration job-context-properties list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" + +# List properties as a direct list +secops integration job-context-properties list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --as-list + +# List with pagination +secops integration job-context-properties list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --page-size 50 + +# List with filtering +secops integration job-context-properties list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --filter-string 'key = "last_run_time"' +``` + +Get a specific context property: + +```bash +secops integration job-context-properties get \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Create a new context property: + +```bash +# Store last execution time +secops integration job-context-properties create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --key "last_execution_time" \ + --value "2026-03-09T10:00:00Z" + +# Store job state for resumable operations +secops integration job-context-properties create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --key "processing_offset" \ + --value "1000" +``` + +Update a context property: + +```bash +# Update execution time +secops integration job-context-properties update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --property-id "prop123" \ + --value "2026-03-09T11:00:00Z" +``` + +Delete a context property: + +```bash +secops integration job-context-properties delete \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Clear all context properties: + +```bash +# Clear all properties for a specific context +secops integration job-context-properties clear-all \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --context-id "mycontext" +``` + +#### Job Instance Logs + +List job instance logs: + +```bash +# List all logs for a job instance +secops integration job-instance-logs list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" + +# List logs as a direct list +secops integration job-instance-logs list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --as-list + +# List with pagination +secops integration job-instance-logs list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --page-size 50 + +# List with filtering (filter by severity or timestamp) +secops integration job-instance-logs list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --filter-string 'severity = "ERROR"' \ + --order-by "createTime desc" +``` + +Get a specific log entry: + +```bash +secops integration job-instance-logs get \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --log-id "log456" +``` + +#### Job Instances + +List job instances: + +```bash +# List all instances for a job +secops integration job-instances list \ + --integration-name "MyIntegration" \ + --job-id "job1" + +# List instances as a direct list +secops integration job-instances list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --as-list + +# List with pagination +secops integration job-instances list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --page-size 50 + +# List with filtering +secops integration job-instances list \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --filter-string 'enabled = true' +``` + +Get job instance details: + +```bash +secops integration job-instances get \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" +``` + +Create a new job instance: + +```bash +# Create basic job instance +secops integration job-instances create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --environment "production" \ + --display-name "Daily Report Generator" + +# Create with schedule and timeout +secops integration job-instances create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --environment "production" \ + --display-name "Hourly Data Sync" \ + --schedule "0 * * * *" \ + --timeout-seconds 300 \ + --enabled + +# Create with parameters +secops integration job-instances create \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --environment "production" \ + --display-name "Custom Job Instance" \ + --schedule "0 0 * * *" \ + --parameters '[{"name":"batch_size","value":"1000"}]' +``` + +Update a job instance: + +```bash +# Update display name +secops integration job-instances update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --display-name "Updated Display Name" + +# Update schedule and timeout +secops integration job-instances update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --schedule "0 */2 * * *" \ + --timeout-seconds 600 + +# Enable or disable instance +secops integration job-instances update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --enabled true + +# Update multiple fields with update mask +secops integration job-instances update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --display-name "New Name" \ + --schedule "0 6 * * *" \ + --update-mask "displayName,schedule" + +# Update parameters +secops integration job-instances update \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --parameters '[{"name":"batch_size","value":"2000"}]' +``` + +Delete a job instance: + +```bash +secops integration job-instances delete \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" +``` + +Run job instance on demand: + +```bash +# Trigger an immediate execution for testing +secops integration job-instances run-ondemand \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" + +# Run with custom parameters +secops integration job-instances run-ondemand \ + --integration-name "MyIntegration" \ + --job-id "job1" \ + --job-instance-id "inst123" \ + --parameters '[{"name":"batch_size","value":"500"}]' +``` + +#### Integration Managers + +List integration managers: + +```bash +# List all managers for an integration +secops integration managers list --integration-name "MyIntegration" + +# List managers as a direct list (fetches all pages automatically) +secops integration managers list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration managers list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration managers list --integration-name "MyIntegration" --filter-string "enabled = true" +``` + +Get manager details: + +```bash +secops integration managers get --integration-name "MyIntegration" --manager-id "mgr1" +``` + +Create a new manager: + +```bash +secops integration managers create \ + --integration-name "MyIntegration" \ + --display-name "Configuration Manager" \ + --code "def manage_config(context): return {'status': 'configured'}" + +# Create with description and custom ID +secops integration managers create \ + --integration-name "MyIntegration" \ + --display-name "My Manager" \ + --code "def manage(context): return {}" \ + --description "Manager description" \ + --manager-id "custom-manager-id" +``` + +Update an existing manager: + +```bash +# Update display name +secops integration managers update \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --display-name "Updated Manager Name" + +# Update code +secops integration managers update \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --code "def manage(context): return {'status': 'updated'}" + +# Update multiple fields with update mask +secops integration managers update \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete a manager: + +```bash +secops integration managers delete --integration-name "MyIntegration" --manager-id "mgr1" +``` + +Get manager template: + +```bash +secops integration managers template --integration-name "MyIntegration" +``` + +#### Manager Revisions + +List manager revisions: + +```bash +# List all revisions for a manager +secops integration manager-revisions list \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" + +# List revisions as a direct list +secops integration manager-revisions list \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --as-list + +# List with pagination +secops integration manager-revisions list \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --page-size 10 + +# List with filtering and ordering +secops integration manager-revisions list \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --filter-string 'version = "1.0"' \ + --order-by "createTime desc" +``` + +Get a specific revision: + +```bash +secops integration manager-revisions get \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --revision-id "r456" +``` + +Create a revision backup: + +```bash +# Create revision with comment +secops integration manager-revisions create \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --comment "Backup before major refactor" + +# Create revision without comment +secops integration manager-revisions create \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" +``` + +Rollback to a previous revision: + +```bash +secops integration manager-revisions rollback \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --revision-id "r456" +``` + +Delete an old revision: + +```bash +secops integration manager-revisions delete \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --revision-id "r789" +``` + +#### Integration Instances + +List integration instances: + +```bash +# List all instances for an integration +secops integration instances list --integration-name "MyIntegration" + +# List instances as a direct list (fetches all pages automatically) +secops integration instances list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration instances list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration instances list --integration-name "MyIntegration" --filter-string "enabled = true" +``` + +Get integration instance details: + +```bash +secops integration instances get \ + --integration-name "MyIntegration" \ + --instance-id "inst123" +``` + +Create a new integration instance: + +```bash +# Create basic integration instance +secops integration instances create \ + --integration-name "MyIntegration" \ + --display-name "Production Instance" \ + --environment "production" + +# Create with description and custom ID +secops integration instances create \ + --integration-name "MyIntegration" \ + --display-name "Test Instance" \ + --environment "test" \ + --description "Testing environment instance" \ + --instance-id "test-inst-001" + +# Create with configuration +secops integration instances create \ + --integration-name "MyIntegration" \ + --display-name "Configured Instance" \ + --environment "production" \ + --config '{"api_key":"secret123","region":"us-east1"}' +``` + +Update an integration instance: + +```bash +# Update display name +secops integration instances update \ + --integration-name "MyIntegration" \ + --instance-id "inst123" \ + --display-name "Updated Instance Name" + +# Update configuration +secops integration instances update \ + --integration-name "MyIntegration" \ + --instance-id "inst123" \ + --config '{"api_key":"newsecret456","region":"us-west1"}' + +# Update multiple fields with update mask +secops integration instances update \ + --integration-name "MyIntegration" \ + --instance-id "inst123" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete an integration instance: + +```bash +secops integration instances delete \ + --integration-name "MyIntegration" \ + --instance-id "inst123" +``` + +Test an integration instance: + +```bash +# Test the instance configuration +secops integration instances test \ + --integration-name "MyIntegration" \ + --instance-id "inst123" +``` + +Get affected items: + +```bash +# Get items affected by this instance +secops integration instances get-affected-items \ + --integration-name "MyIntegration" \ + --instance-id "inst123" +``` + +Get default instance: + +```bash +# Get the default integration instance +secops integration instances get-default \ + --integration-name "MyIntegration" +``` + ### Rule Management List detection rules: @@ -930,7 +2216,6 @@ secops curated-rule search-detections \ --end-time "2024-01-31T23:59:59Z" \ --list-basis "DETECTION_TIME" \ --page-size 50 - ``` List all curated rule sets: @@ -1577,39 +2862,7 @@ secops reference-list create \ secops parser list # Get details of a specific parser -secops parser get --log-type "WINDOWS" --id "pa_12345" - -# Create a custom parser for a new log format -secops parser create \ - --log-type "CUSTOM_APPLICATION" \ - --parser-code-file "/path/to/custom_parser.conf" \ - --validated-on-empty-logs - -# Copy an existing parser as a starting point -secops parser copy --log-type "OKTA" --id "pa_okta_base" - -# Activate your custom parser -secops parser activate --log-type "CUSTOM_APPLICATION" --id "pa_new_custom" - -# If needed, deactivate and delete old parser -secops parser deactivate --log-type "CUSTOM_APPLICATION" --id "pa_old_custom" -secops parser delete --log-type "CUSTOM_APPLICATION" --id "pa_old_custom" -``` - -### Complete Parser Workflow Example: Retrieve, Run, and Ingest - -This example demonstrates the complete workflow of retrieving an OKTA parser, running it against a sample log, and ingesting the parsed UDM event: - -```bash -# Step 1: List OKTA parsers to find an active one -secops parser list --log-type "OKTA" > okta_parsers.json - -# Extract the first parser ID (you can use jq or grep) -PARSER_ID=$(cat okta_parsers.json | jq -r '.[0].name' | awk -F'/' '{print $NF}') -echo "Using parser: $PARSER_ID" - -# Step 2: Get the parser details and save to a file -secops parser get --log-type "OKTA" --id "$PARSER_ID" > parser_details.json +secops parser get --log-type "WINDOWS" --id "$PARSER_ID" > parser_details.json # Extract and decode the parser code (base64 encoded in 'cbn' field) cat parser_details.json | jq -r '.cbn' | base64 -d > okta_parser.conf @@ -1747,7 +3000,7 @@ secops feed update --id "feed-123" --display-name "Updated Feed Name" secops feed update --id "feed-123" --details '{"httpSettings":{"uri":"https://example.com/updated-feed","sourceType":"FILES"}}' # Update both display name and details -secops feed update --id "feed-123" --display-name "Updated Name" --details '{"httpSettings":{"uri":"https://example.com/updated-feed"}}' +secops feed update --id "feed-123" --display-name "New Name" --details '{"httpSettings":{"uri":"https://example.com/updated-feed"}}' ``` Enable and disable feeds: @@ -1888,4 +3141,5 @@ secops dashboard-query get --id query-id ## Conclusion -The SecOps CLI provides a powerful way to interact with Google Security Operations products directly from your terminal. For more detailed information about the SDK capabilities, refer to the [main README](README.md). \ No newline at end of file +The SecOps CLI provides a powerful way to interact with Google Security Operations products directly from your terminal. For more detailed information about the SDK capabilities, refer to the [main README](README.md). + diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index d3b0718d..9eb937aa 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -284,7 +284,7 @@ TargetMode, TileType, IntegrationParam, - ConnectorInstanceParameter + ConnectorInstanceParameter, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, diff --git a/src/secops/cli/commands/integration/action_revisions.py b/src/secops/cli/commands/integration/action_revisions.py new file mode 100644 index 00000000..d6999bac --- /dev/null +++ b/src/secops/cli/commands/integration/action_revisions.py @@ -0,0 +1,215 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration action revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_action_revisions_command(subparsers): + """Setup integration action revisions command""" + revisions_parser = subparsers.add_parser( + "action-revisions", + help="Manage integration action revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="action_revisions_command", + help="Integration action revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List integration action revisions" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--action-id", + type=str, + help="ID of the action", + dest="action_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_action_revisions_list_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration action revision" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--action-id", + type=str, + help="ID of the action", + dest="action_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults(func=handle_action_revisions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration action revision" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--action-id", + type=str, + help="ID of the action", + dest="action_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults(func=handle_action_revisions_create_command) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", help="Rollback action to a previous revision" + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--action-id", + type=str, + help="ID of the action", + dest="action_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults(func=handle_action_revisions_rollback_command) + + +def handle_action_revisions_list_command(args, chronicle): + """Handle integration action revisions list command""" + try: + out = chronicle.list_integration_action_revisions( + integration_name=args.integration_name, + action_id=args.action_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing action revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_action_revisions_delete_command(args, chronicle): + """Handle integration action revision delete command""" + try: + chronicle.delete_integration_action_revision( + integration_name=args.integration_name, + action_id=args.action_id, + revision_id=args.revision_id, + ) + print(f"Action revision {args.revision_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting action revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_action_revisions_create_command(args, chronicle): + """Handle integration action revision create command""" + try: + # Get the current action to create a revision + action = chronicle.get_integration_action( + integration_name=args.integration_name, + action_id=args.action_id, + ) + out = chronicle.create_integration_action_revision( + integration_name=args.integration_name, + action_id=args.action_id, + action=action, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating action revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_action_revisions_rollback_command(args, chronicle): + """Handle integration action revision rollback command""" + try: + out = chronicle.rollback_integration_action_revision( + integration_name=args.integration_name, + action_id=args.action_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error rolling back action revision: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/actions.py b/src/secops/cli/commands/integration/actions.py new file mode 100644 index 00000000..a389c8aa --- /dev/null +++ b/src/secops/cli/commands/integration/actions.py @@ -0,0 +1,382 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration actions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_actions_command(subparsers): + """Setup integration actions command""" + actions_parser = subparsers.add_parser( + "actions", + help="Manage integration actions", + ) + lvl1 = actions_parser.add_subparsers( + dest="actions_command", help="Integration actions command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration actions") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing actions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing actions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_actions_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get integration action details") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--action-id", + type=str, + help="ID of the action to get", + dest="action_id", + required=True, + ) + get_parser.set_defaults(func=handle_actions_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration action", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--action-id", + type=str, + help="ID of the action to delete", + dest="action_id", + required=True, + ) + delete_parser.set_defaults(func=handle_actions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration action" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the action", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--code", + type=str, + help="Python code for the action", + dest="code", + required=True, + ) + create_parser.add_argument( + "--is-async", + action="store_true", + help="Whether the action is asynchronous", + dest="is_async", + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the action", + dest="description", + ) + create_parser.add_argument( + "--action-id", + type=str, + help="Custom ID for the action", + dest="action_id", + ) + create_parser.set_defaults(func=handle_actions_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an integration action" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--action-id", + type=str, + help="ID of the action to update", + dest="action_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the action", + dest="display_name", + ) + update_parser.add_argument( + "--script", + type=str, + help="New Python script for the action", + dest="script", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the action", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_actions_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", help="Execute an integration action test" + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--action-id", + type=str, + help="ID of the action to test", + dest="action_id", + required=True, + ) + test_parser.set_defaults(func=handle_actions_test_command) + + # by-environment command + by_env_parser = lvl1.add_parser( + "by-environment", + help="Get integration actions by environment", + ) + by_env_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + by_env_parser.add_argument( + "--environments", + type=str, + nargs="+", + help="List of environments to filter by", + dest="environments", + required=True, + ) + by_env_parser.add_argument( + "--include-widgets", + action="store_true", + help="Whether to include widgets in the response", + dest="include_widgets", + ) + by_env_parser.set_defaults(func=handle_actions_by_environment_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get a template for creating an action", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.add_argument( + "--is-async", + action="store_true", + help="Whether to fetch template for async action", + dest="is_async", + ) + template_parser.set_defaults(func=handle_actions_template_command) + + +def handle_actions_list_command(args, chronicle): + """Handle integration actions list command""" + try: + out = chronicle.list_integration_actions( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration actions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_get_command(args, chronicle): + """Handle integration action get command""" + try: + out = chronicle.get_integration_action( + integration_name=args.integration_name, + action_id=args.action_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration action: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_delete_command(args, chronicle): + """Handle integration action delete command""" + try: + chronicle.delete_integration_action( + integration_name=args.integration_name, + action_id=args.action_id, + ) + print(f"Action {args.action_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration action: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_create_command(args, chronicle): + """Handle integration action create command""" + try: + out = chronicle.create_integration_action( + integration_name=args.integration_name, + display_name=args.display_name, + script=args.code, # CLI uses --code flag but API expects script + timeout_seconds=300, # Default 5 minutes + enabled=True, # Default to enabled + script_result_name="result", # Default result field name + is_async=args.is_async, + description=args.description, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration action: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_update_command(args, chronicle): + """Handle integration action update command""" + try: + out = chronicle.update_integration_action( + integration_name=args.integration_name, + action_id=args.action_id, + display_name=args.display_name, + script=( + args.script if args.script else None + ), # CLI uses --code flag but API expects script + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration action: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_test_command(args, chronicle): + """Handle integration action test command""" + try: + # First get the action to test + action = chronicle.get_integration_action( + integration_name=args.integration_name, + action_id=args.action_id, + ) + out = chronicle.execute_integration_action_test( + integration_name=args.integration_name, + action_id=args.action_id, + action=action, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error testing integration action: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_by_environment_command(args, chronicle): + """Handle get actions by environment command""" + try: + out = chronicle.get_integration_actions_by_environment( + integration_name=args.integration_name, + environments=args.environments, + include_widgets=args.include_widgets, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting actions by environment: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_actions_template_command(args, chronicle): + """Handle get action template command""" + try: + out = chronicle.get_integration_action_template( + integration_name=args.integration_name, + is_async=args.is_async, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting action template: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_context_properties.py b/src/secops/cli/commands/integration/connector_context_properties.py new file mode 100644 index 00000000..46b2d936 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_context_properties.py @@ -0,0 +1,375 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI connector context properties commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_context_properties_command(subparsers): + """Setup connector context properties command""" + properties_parser = subparsers.add_parser( + "connector-context-properties", + help="Manage connector context properties", + ) + lvl1 = properties_parser.add_subparsers( + dest="connector_context_properties_command", + help="Connector context properties command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List connector context properties" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + list_parser.add_argument( + "--context-id", + type=str, + help="Context ID to filter properties", + dest="context_id", + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing properties", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing properties", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_connector_context_properties_list_command, + ) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector context property" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + get_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to get", + dest="property_id", + required=True, + ) + get_parser.set_defaults( + func=handle_connector_context_properties_get_command + ) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete a connector context property" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + delete_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to delete", + dest="property_id", + required=True, + ) + delete_parser.set_defaults( + func=handle_connector_context_properties_delete_command + ) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new connector context property" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--context-id", + type=str, + help="Context ID for the property", + dest="context_id", + required=True, + ) + create_parser.add_argument( + "--key", + type=str, + help="Key for the property", + dest="key", + required=True, + ) + create_parser.add_argument( + "--value", + type=str, + help="Value for the property", + dest="value", + required=True, + ) + create_parser.set_defaults( + func=handle_connector_context_properties_create_command + ) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update a connector context property" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + update_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to update", + dest="property_id", + required=True, + ) + update_parser.add_argument( + "--value", + type=str, + help="New value for the property", + dest="value", + required=True, + ) + update_parser.set_defaults( + func=handle_connector_context_properties_update_command + ) + + # clear-all command + clear_parser = lvl1.add_parser( + "clear-all", help="Delete all connector context properties" + ) + clear_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + clear_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + clear_parser.add_argument( + "--context-id", + type=str, + help="Context ID to clear all properties for", + dest="context_id", + required=True, + ) + clear_parser.set_defaults( + func=handle_connector_context_properties_clear_command + ) + + +def handle_connector_context_properties_list_command(args, chronicle): + """Handle connector context properties list command""" + try: + out = chronicle.list_connector_context_properties( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error listing connector context properties: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_get_command(args, chronicle): + """Handle connector context property get command""" + try: + out = chronicle.get_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_context_properties_delete_command(args, chronicle): + """Handle connector context property delete command""" + try: + chronicle.delete_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + print( + f"Connector context property " + f"{args.property_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error deleting connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_create_command(args, chronicle): + """Handle connector context property create command""" + try: + out = chronicle.create_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + key=args.key, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_update_command(args, chronicle): + """Handle connector context property update command""" + try: + out = chronicle.update_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error updating connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_clear_command(args, chronicle): + """Handle clear all connector context properties command""" + try: + chronicle.delete_all_connector_context_properties( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + ) + print( + f"All connector context properties for context " + f"{args.context_id} cleared successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error clearing connector context properties: {e}", file=sys.stderr + ) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instance_logs.py b/src/secops/cli/commands/integration/connector_instance_logs.py new file mode 100644 index 00000000..b67e35f2 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_instance_logs.py @@ -0,0 +1,142 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI connector instance logs commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_instance_logs_command(subparsers): + """Setup connector instance logs command""" + logs_parser = subparsers.add_parser( + "connector-instance-logs", + help="View connector instance logs", + ) + lvl1 = logs_parser.add_subparsers( + dest="connector_instance_logs_command", + help="Connector instance logs command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List connector instance logs") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + list_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing logs", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing logs", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_instance_logs_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector instance log" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + get_parser.add_argument( + "--log-id", + type=str, + help="ID of the log to get", + dest="log_id", + required=True, + ) + get_parser.set_defaults(func=handle_connector_instance_logs_get_command) + + +def handle_connector_instance_logs_list_command(args, chronicle): + """Handle connector instance logs list command""" + try: + out = chronicle.list_connector_instance_logs( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector instance logs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instance_logs_get_command(args, chronicle): + """Handle connector instance log get command""" + try: + out = chronicle.get_connector_instance_log( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + connector_instance_log_id=args.log_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector instance log: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instances.py b/src/secops/cli/commands/integration/connector_instances.py new file mode 100644 index 00000000..df68bfde --- /dev/null +++ b/src/secops/cli/commands/integration/connector_instances.py @@ -0,0 +1,473 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI connector instances commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_instances_command(subparsers): + """Setup connector instances command""" + instances_parser = subparsers.add_parser( + "connector-instances", + help="Manage connector instances", + ) + lvl1 = instances_parser.add_subparsers( + dest="connector_instances_command", + help="Connector instances command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List connector instances") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing instances", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing instances", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_instances_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector instance" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to get", + dest="connector_instance_id", + required=True, + ) + get_parser.set_defaults(func=handle_connector_instances_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete a connector instance" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to delete", + dest="connector_instance_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connector_instances_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new connector instance" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--environment", + type=str, + help="Environment for the connector instance", + dest="environment", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the connector instance", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--interval-seconds", + type=int, + help="Interval in seconds for connector execution", + dest="interval_seconds", + ) + create_parser.add_argument( + "--timeout-seconds", + type=int, + help="Timeout in seconds for connector execution", + dest="timeout_seconds", + ) + create_parser.add_argument( + "--enabled", + action="store_true", + help="Enable the connector instance", + dest="enabled", + ) + create_parser.set_defaults(func=handle_connector_instances_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update a connector instance" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to update", + dest="connector_instance_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the connector instance", + dest="display_name", + ) + update_parser.add_argument( + "--interval-seconds", + type=int, + help="New interval in seconds for connector execution", + dest="interval_seconds", + ) + update_parser.add_argument( + "--timeout-seconds", + type=int, + help="New timeout in seconds for connector execution", + dest="timeout_seconds", + ) + update_parser.add_argument( + "--enabled", + type=str, + choices=["true", "false"], + help="Enable or disable the connector instance", + dest="enabled", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_connector_instances_update_command) + + # fetch-latest command + fetch_parser = lvl1.add_parser( + "fetch-latest", + help="Get the latest definition of a connector instance", + ) + fetch_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + fetch_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + fetch_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + fetch_parser.set_defaults( + func=handle_connector_instances_fetch_latest_command + ) + + # set-logs command + logs_parser = lvl1.add_parser( + "set-logs", + help="Enable or disable log collection for a connector instance", + ) + logs_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + logs_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + logs_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + logs_parser.add_argument( + "--enabled", + type=str, + choices=["true", "false"], + help="Enable or disable log collection", + dest="enabled", + required=True, + ) + logs_parser.set_defaults(func=handle_connector_instances_set_logs_command) + + # run-ondemand command + run_parser = lvl1.add_parser( + "run-ondemand", + help="Run a connector instance on demand", + ) + run_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + run_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + run_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to run", + dest="connector_instance_id", + required=True, + ) + run_parser.set_defaults( + func=handle_connector_instances_run_ondemand_command + ) + + +def handle_connector_instances_list_command(args, chronicle): + """Handle connector instances list command""" + try: + out = chronicle.list_connector_instances( + integration_name=args.integration_name, + connector_id=args.connector_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector instances: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_get_command(args, chronicle): + """Handle connector instance get command""" + try: + out = chronicle.get_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_delete_command(args, chronicle): + """Handle connector instance delete command""" + try: + chronicle.delete_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + print( + f"Connector instance {args.connector_instance_id}" + f" deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_create_command(args, chronicle): + """Handle connector instance create command""" + try: + out = chronicle.create_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + environment=args.environment, + display_name=args.display_name, + interval_seconds=args.interval_seconds, + timeout_seconds=args.timeout_seconds, + enabled=args.enabled, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_update_command(args, chronicle): + """Handle connector instance update command""" + try: + # Convert enabled string to boolean if provided + enabled = None + if args.enabled: + enabled = args.enabled.lower() == "true" + + out = chronicle.update_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + display_name=args.display_name, + interval_seconds=args.interval_seconds, + timeout_seconds=args.timeout_seconds, + enabled=enabled, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_fetch_latest_command(args, chronicle): + """Handle fetch latest connector instance definition command""" + try: + out = chronicle.get_connector_instance_latest_definition( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error fetching latest connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_set_logs_command(args, chronicle): + """Handle set connector instance logs collection command""" + try: + enabled = args.enabled.lower() == "true" + out = chronicle.set_connector_instance_logs_collection( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + enabled=enabled, + ) + status = "enabled" if enabled else "disabled" + print(f"Log collection {status} for connector instance successfully") + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error setting connector instance logs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_run_ondemand_command(args, chronicle): + """Handle run connector instance on demand command""" + try: + # Get the connector instance first + connector_instance = chronicle.get_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + out = chronicle.run_connector_instance_on_demand( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + connector_instance=connector_instance, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error running connector instance on demand: {e}", file=sys.stderr + ) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_revisions.py b/src/secops/cli/commands/integration/connector_revisions.py new file mode 100644 index 00000000..779888c9 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_revisions.py @@ -0,0 +1,217 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration connector revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_revisions_command(subparsers): + """Setup integration connector revisions command""" + revisions_parser = subparsers.add_parser( + "connector-revisions", + help="Manage integration connector revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="connector_revisions_command", + help="Integration connector revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List integration connector revisions" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_revisions_list_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration connector revision" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connector_revisions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration connector revision" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults(func=handle_connector_revisions_create_command) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", help="Rollback connector to a previous revision" + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults( + func=handle_connector_revisions_rollback_command, + ) + + +def handle_connector_revisions_list_command(args, chronicle): + """Handle integration connector revisions list command""" + try: + out = chronicle.list_integration_connector_revisions( + integration_name=args.integration_name, + connector_id=args.connector_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_delete_command(args, chronicle): + """Handle integration connector revision delete command""" + try: + chronicle.delete_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + revision_id=args.revision_id, + ) + print(f"Connector revision {args.revision_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting connector revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_create_command(args, chronicle): + """Handle integration connector revision create command""" + try: + # Get the current connector to create a revision + connector = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + out = chronicle.create_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector=connector, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating connector revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_rollback_command(args, chronicle): + """Handle integration connector revision rollback command""" + try: + out = chronicle.rollback_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error rolling back connector revision: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connectors.py b/src/secops/cli/commands/integration/connectors.py new file mode 100644 index 00000000..fe8e03ef --- /dev/null +++ b/src/secops/cli/commands/integration/connectors.py @@ -0,0 +1,325 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration connectors commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connectors_command(subparsers): + """Setup integration connectors command""" + connectors_parser = subparsers.add_parser( + "connectors", + help="Manage integration connectors", + ) + lvl1 = connectors_parser.add_subparsers( + dest="connectors_command", help="Integration connectors command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration connectors") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing connectors", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing connectors", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_connectors_list_command, + ) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get integration connector details" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to get", + dest="connector_id", + required=True, + ) + get_parser.set_defaults(func=handle_connectors_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration connector" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to delete", + dest="connector_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connectors_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration connector" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the connector", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--code", + type=str, + help="Python code for the connector", + dest="code", + required=True, + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the connector", + dest="description", + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="Custom ID for the connector", + dest="connector_id", + ) + create_parser.set_defaults(func=handle_connectors_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an integration connector" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to update", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the connector", + dest="display_name", + ) + update_parser.add_argument( + "--code", + type=str, + help="New Python code for the connector", + dest="code", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the connector", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_connectors_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", help="Execute an integration connector test" + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to test", + dest="connector_id", + required=True, + ) + test_parser.set_defaults(func=handle_connectors_test_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get a template for creating a connector", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_connectors_template_command) + + +def handle_connectors_list_command(args, chronicle): + """Handle integration connectors list command""" + try: + out = chronicle.list_integration_connectors( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration connectors: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_get_command(args, chronicle): + """Handle integration connector get command""" + try: + out = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_delete_command(args, chronicle): + """Handle integration connector delete command""" + try: + chronicle.delete_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + print(f"Connector {args.connector_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_create_command(args, chronicle): + """Handle integration connector create command""" + try: + out = chronicle.create_integration_connector( + integration_name=args.integration_name, + display_name=args.display_name, + code=args.code, + description=args.description, + connector_id=args.connector_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_update_command(args, chronicle): + """Handle integration connector update command""" + try: + out = chronicle.update_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + display_name=args.display_name, + code=args.code, + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_test_command(args, chronicle): + """Handle integration connector test command""" + try: + # First get the connector to test + connector = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + out = chronicle.execute_integration_connector_test( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector=connector, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error testing integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_template_command(args, chronicle): + """Handle get connector template command""" + try: + out = chronicle.get_integration_connector_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector template: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index 8fb00a83..b0636f07 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -14,8 +14,25 @@ # """Top level arguments for integration commands""" -from secops.cli.commands.integration import marketplace_integration -from secops.cli.commands.integration import integration +from secops.cli.commands.integration import ( + marketplace_integration, + integration, + actions, + action_revisions, + connectors, + connector_revisions, + connector_context_properties, + connector_instance_logs, + connector_instances, + jobs, + job_revisions, + job_context_properties, + job_instance_logs, + job_instances, + managers, + manager_revisions, + integration_instances, +) def setup_integrations_command(subparsers): @@ -28,5 +45,22 @@ def setup_integrations_command(subparsers): ) # Setup all subcommands under `integration` - marketplace_integration.setup_marketplace_integrations_command(lvl1) integration.setup_integrations_command(lvl1) + integration_instances.setup_integration_instances_command(lvl1) + actions.setup_actions_command(lvl1) + action_revisions.setup_action_revisions_command(lvl1) + connectors.setup_connectors_command(lvl1) + connector_revisions.setup_connector_revisions_command(lvl1) + connector_context_properties.setup_connector_context_properties_command( + lvl1 + ) + connector_instance_logs.setup_connector_instance_logs_command(lvl1) + connector_instances.setup_connector_instances_command(lvl1) + jobs.setup_jobs_command(lvl1) + job_revisions.setup_job_revisions_command(lvl1) + job_context_properties.setup_job_context_properties_command(lvl1) + job_instance_logs.setup_job_instance_logs_command(lvl1) + job_instances.setup_job_instances_command(lvl1) + managers.setup_managers_command(lvl1) + manager_revisions.setup_manager_revisions_command(lvl1) + marketplace_integration.setup_marketplace_integrations_command(lvl1) diff --git a/src/secops/cli/commands/integration/integration_instances.py b/src/secops/cli/commands/integration/integration_instances.py new file mode 100644 index 00000000..2d375346 --- /dev/null +++ b/src/secops/cli/commands/integration/integration_instances.py @@ -0,0 +1,392 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration instances commands""" + +import json +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_integration_instances_command(subparsers): + """Setup integration instances command""" + instances_parser = subparsers.add_parser( + "instances", + help="Manage integration instances", + ) + lvl1 = instances_parser.add_subparsers( + dest="integration_instances_command", + help="Integration instances command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration instances") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing instances", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing instances", + dest="order_by", + ) + list_parser.set_defaults(func=handle_integration_instances_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get integration instance details") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--instance-id", + type=str, + help="ID of the instance to get", + dest="instance_id", + required=True, + ) + get_parser.set_defaults(func=handle_integration_instances_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration instance" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--instance-id", + type=str, + help="ID of the instance to delete", + dest="instance_id", + required=True, + ) + delete_parser.set_defaults(func=handle_integration_instances_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration instance" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the instance", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--environment", + type=str, + help="Environment name for the instance", + dest="environment", + required=True, + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the instance", + dest="description", + ) + create_parser.add_argument( + "--instance-id", + type=str, + help="Custom ID for the instance", + dest="instance_id", + ) + create_parser.add_argument( + "--config", + type=str, + help="JSON string of instance configuration", + dest="config", + ) + create_parser.set_defaults(func=handle_integration_instances_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an integration instance" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--instance-id", + type=str, + help="ID of the instance to update", + dest="instance_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the instance", + dest="display_name", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the instance", + dest="description", + ) + update_parser.add_argument( + "--config", + type=str, + help="JSON string of new instance configuration", + dest="config", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_integration_instances_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", help="Execute an integration instance test" + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--instance-id", + type=str, + help="ID of the instance to test", + dest="instance_id", + required=True, + ) + test_parser.set_defaults(func=handle_integration_instances_test_command) + + # get-affected-items command + affected_parser = lvl1.add_parser( + "get-affected-items", + help="Get items affected by an integration instance", + ) + affected_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + affected_parser.add_argument( + "--instance-id", + type=str, + help="ID of the instance", + dest="instance_id", + required=True, + ) + affected_parser.set_defaults( + func=handle_integration_instances_get_affected_items_command + ) + + # get-default command + default_parser = lvl1.add_parser( + "get-default", + help="Get the default integration instance", + ) + default_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + default_parser.set_defaults( + func=handle_integration_instances_get_default_command + ) + + +def handle_integration_instances_list_command(args, chronicle): + """Handle integration instances list command""" + try: + out = chronicle.list_integration_instances( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration instances: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_get_command(args, chronicle): + """Handle integration instance get command""" + try: + out = chronicle.get_integration_instance( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_delete_command(args, chronicle): + """Handle integration instance delete command""" + try: + chronicle.delete_integration_instance( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + ) + print(f"Integration instance {args.instance_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_create_command(args, chronicle): + """Handle integration instance create command""" + try: + # Parse config if provided + + config = None + if args.config: + config = json.loads(args.config) + + out = chronicle.create_integration_instance( + integration_name=args.integration_name, + display_name=args.display_name, + environment=args.environment, + description=args.description, + integration_instance_id=args.instance_id, + config=config, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing config JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_update_command(args, chronicle): + """Handle integration instance update command""" + try: + # Parse config if provided + + config = None + if args.config: + config = json.loads(args.config) + + out = chronicle.update_integration_instance( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + display_name=args.display_name, + description=args.description, + config=config, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing config JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_test_command(args, chronicle): + """Handle integration instance test command""" + try: + # Get the instance first + instance = chronicle.get_integration_instance( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + ) + + out = chronicle.execute_integration_instance_test( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + integration_instance=instance, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error testing integration instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_integration_instances_get_affected_items_command(args, chronicle): + """Handle get integration instance affected items command""" + try: + out = chronicle.get_integration_instance_affected_items( + integration_name=args.integration_name, + integration_instance_id=args.instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting integration instance affected items: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_integration_instances_get_default_command(args, chronicle): + """Handle get default integration instance command""" + try: + out = chronicle.get_default_integration_instance( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting default integration instance: {e}", file=sys.stderr + ) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_context_properties.py b/src/secops/cli/commands/integration/job_context_properties.py new file mode 100644 index 00000000..5da5cdb3 --- /dev/null +++ b/src/secops/cli/commands/integration/job_context_properties.py @@ -0,0 +1,354 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI job context properties commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_job_context_properties_command(subparsers): + """Setup job context properties command""" + properties_parser = subparsers.add_parser( + "job-context-properties", + help="Manage job context properties", + ) + lvl1 = properties_parser.add_subparsers( + dest="job_context_properties_command", + help="Job context properties command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List job context properties") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + list_parser.add_argument( + "--context-id", + type=str, + help="Context ID to filter properties", + dest="context_id", + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing properties", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing properties", + dest="order_by", + ) + list_parser.set_defaults(func=handle_job_context_properties_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific job context property" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + get_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + get_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to get", + dest="property_id", + required=True, + ) + get_parser.set_defaults(func=handle_job_context_properties_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete a job context property" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + delete_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + delete_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to delete", + dest="property_id", + required=True, + ) + delete_parser.set_defaults( + func=handle_job_context_properties_delete_command + ) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new job context property" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + create_parser.add_argument( + "--context-id", + type=str, + help="Context ID for the property", + dest="context_id", + required=True, + ) + create_parser.add_argument( + "--key", + type=str, + help="Key for the property", + dest="key", + required=True, + ) + create_parser.add_argument( + "--value", + type=str, + help="Value for the property", + dest="value", + required=True, + ) + create_parser.set_defaults( + func=handle_job_context_properties_create_command + ) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update a job context property" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + update_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + update_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to update", + dest="property_id", + required=True, + ) + update_parser.add_argument( + "--value", + type=str, + help="New value for the property", + dest="value", + required=True, + ) + update_parser.set_defaults( + func=handle_job_context_properties_update_command + ) + + # clear-all command + clear_parser = lvl1.add_parser( + "clear-all", help="Delete all job context properties" + ) + clear_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + clear_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + clear_parser.add_argument( + "--context-id", + type=str, + help="Context ID to clear all properties for", + dest="context_id", + required=True, + ) + clear_parser.set_defaults(func=handle_job_context_properties_clear_command) + + +def handle_job_context_properties_list_command(args, chronicle): + """Handle job context properties list command""" + try: + out = chronicle.list_job_context_properties( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing job context properties: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_context_properties_get_command(args, chronicle): + """Handle job context property get command""" + try: + out = chronicle.get_job_context_property( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting job context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_context_properties_delete_command(args, chronicle): + """Handle job context property delete command""" + try: + chronicle.delete_job_context_property( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + print(f"Job context property {args.property_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting job context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_context_properties_create_command(args, chronicle): + """Handle job context property create command""" + try: + out = chronicle.create_job_context_property( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + key=args.key, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating job context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_context_properties_update_command(args, chronicle): + """Handle job context property update command""" + try: + out = chronicle.update_job_context_property( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + context_property_id=args.property_id, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating job context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_context_properties_clear_command(args, chronicle): + """Handle clear all job context properties command""" + try: + chronicle.delete_all_job_context_properties( + integration_name=args.integration_name, + job_id=args.job_id, + context_id=args.context_id, + ) + print( + f"All job context properties for context " + f"{args.context_id} cleared successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error clearing job context properties: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_instance_logs.py b/src/secops/cli/commands/integration/job_instance_logs.py new file mode 100644 index 00000000..d18e2ad4 --- /dev/null +++ b/src/secops/cli/commands/integration/job_instance_logs.py @@ -0,0 +1,140 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI job instance logs commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_job_instance_logs_command(subparsers): + """Setup job instance logs command""" + logs_parser = subparsers.add_parser( + "job-instance-logs", + help="View job instance logs", + ) + lvl1 = logs_parser.add_subparsers( + dest="job_instance_logs_command", + help="Job instance logs command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List job instance logs") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + list_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance", + dest="job_instance_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing logs", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing logs", + dest="order_by", + ) + list_parser.set_defaults(func=handle_job_instance_logs_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get a specific job instance log") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + get_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance", + dest="job_instance_id", + required=True, + ) + get_parser.add_argument( + "--log-id", + type=str, + help="ID of the log to get", + dest="log_id", + required=True, + ) + get_parser.set_defaults(func=handle_job_instance_logs_get_command) + + +def handle_job_instance_logs_list_command(args, chronicle): + """Handle job instance logs list command""" + try: + out = chronicle.list_job_instance_logs( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing job instance logs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instance_logs_get_command(args, chronicle): + """Handle job instance log get command""" + try: + out = chronicle.get_job_instance_log( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + job_instance_log_id=args.log_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting job instance log: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_instances.py b/src/secops/cli/commands/integration/job_instances.py new file mode 100644 index 00000000..53c9a202 --- /dev/null +++ b/src/secops/cli/commands/integration/job_instances.py @@ -0,0 +1,407 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration job instances commands""" + +import json +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_job_instances_command(subparsers): + """Setup integration job instances command""" + instances_parser = subparsers.add_parser( + "job-instances", + help="Manage job instances", + ) + lvl1 = instances_parser.add_subparsers( + dest="job_instances_command", + help="Job instances command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List job instances") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing instances", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing instances", + dest="order_by", + ) + list_parser.set_defaults(func=handle_job_instances_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get a specific job instance") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + get_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance to get", + dest="job_instance_id", + required=True, + ) + get_parser.set_defaults(func=handle_job_instances_get_command) + + # delete command + delete_parser = lvl1.add_parser("delete", help="Delete a job instance") + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + delete_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance to delete", + dest="job_instance_id", + required=True, + ) + delete_parser.set_defaults(func=handle_job_instances_delete_command) + + # create command + create_parser = lvl1.add_parser("create", help="Create a new job instance") + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + create_parser.add_argument( + "--environment", + type=str, + help="Environment for the job instance", + dest="environment", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the job instance", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--schedule", + type=str, + help="Cron schedule for the job instance", + dest="schedule", + ) + create_parser.add_argument( + "--timeout-seconds", + type=int, + help="Timeout in seconds for job execution", + dest="timeout_seconds", + ) + create_parser.add_argument( + "--enabled", + action="store_true", + help="Enable the job instance", + dest="enabled", + ) + create_parser.add_argument( + "--parameters", + type=str, + help="JSON string of job parameters", + dest="parameters", + ) + create_parser.set_defaults(func=handle_job_instances_create_command) + + # update command + update_parser = lvl1.add_parser("update", help="Update a job instance") + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + update_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance to update", + dest="job_instance_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the job instance", + dest="display_name", + ) + update_parser.add_argument( + "--schedule", + type=str, + help="New cron schedule for the job instance", + dest="schedule", + ) + update_parser.add_argument( + "--timeout-seconds", + type=int, + help="New timeout in seconds for job execution", + dest="timeout_seconds", + ) + update_parser.add_argument( + "--enabled", + type=str, + choices=["true", "false"], + help="Enable or disable the job instance", + dest="enabled", + ) + update_parser.add_argument( + "--parameters", + type=str, + help="JSON string of new job parameters", + dest="parameters", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_job_instances_update_command) + + # run-ondemand command + run_parser = lvl1.add_parser( + "run-ondemand", + help="Run a job instance on demand", + ) + run_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + run_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + run_parser.add_argument( + "--job-instance-id", + type=str, + help="ID of the job instance to run", + dest="job_instance_id", + required=True, + ) + run_parser.add_argument( + "--parameters", + type=str, + help="JSON string of parameters for this run", + dest="parameters", + ) + run_parser.set_defaults(func=handle_job_instances_run_ondemand_command) + + +def handle_job_instances_list_command(args, chronicle): + """Handle job instances list command""" + try: + out = chronicle.list_integration_job_instances( + integration_name=args.integration_name, + job_id=args.job_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing job instances: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instances_get_command(args, chronicle): + """Handle job instance get command""" + try: + out = chronicle.get_integration_job_instance( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting job instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instances_delete_command(args, chronicle): + """Handle job instance delete command""" + try: + chronicle.delete_integration_job_instance( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + ) + print(f"Job instance {args.job_instance_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting job instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instances_create_command(args, chronicle): + """Handle job instance create command""" + try: + # Parse parameters if provided + parameters = None + if args.parameters: + parameters = json.loads(args.parameters) + + out = chronicle.create_integration_job_instance( + integration_name=args.integration_name, + job_id=args.job_id, + environment=args.environment, + display_name=args.display_name, + schedule=args.schedule, + timeout_seconds=args.timeout_seconds, + enabled=args.enabled, + parameters=parameters, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing parameters JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating job instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instances_update_command(args, chronicle): + """Handle job instance update command""" + try: + # Parse parameters if provided + parameters = None + if args.parameters: + parameters = json.loads(args.parameters) + + # Convert enabled string to boolean if provided + enabled = None + if args.enabled: + enabled = args.enabled.lower() == "true" + + out = chronicle.update_integration_job_instance( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + display_name=args.display_name, + schedule=args.schedule, + timeout_seconds=args.timeout_seconds, + enabled=enabled, + parameters=parameters, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing parameters JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating job instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_instances_run_ondemand_command(args, chronicle): + """Handle run job instance on demand command""" + try: + # Parse parameters if provided + parameters = None + if args.parameters: + parameters = json.loads(args.parameters) + + # Get the job instance first + job_instance = chronicle.get_integration_job_instance( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + ) + + out = chronicle.run_integration_job_instance_on_demand( + integration_name=args.integration_name, + job_id=args.job_id, + job_instance_id=args.job_instance_id, + job_instance=job_instance, + parameters=parameters, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing parameters JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error running job instance on demand: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_revisions.py b/src/secops/cli/commands/integration/job_revisions.py new file mode 100644 index 00000000..36b24850 --- /dev/null +++ b/src/secops/cli/commands/integration/job_revisions.py @@ -0,0 +1,213 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration job revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_job_revisions_command(subparsers): + """Setup integration job revisions command""" + revisions_parser = subparsers.add_parser( + "job-revisions", + help="Manage integration job revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="job_revisions_command", + help="Integration job revisions command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration job revisions") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_job_revisions_list_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration job revision" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults(func=handle_job_revisions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration job revision" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults(func=handle_job_revisions_create_command) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", help="Rollback job to a previous revision" + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--job-id", + type=str, + help="ID of the job", + dest="job_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults(func=handle_job_revisions_rollback_command) + + +def handle_job_revisions_list_command(args, chronicle): + """Handle integration job revisions list command""" + try: + out = chronicle.list_integration_job_revisions( + integration_name=args.integration_name, + job_id=args.job_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing job revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_revisions_delete_command(args, chronicle): + """Handle integration job revision delete command""" + try: + chronicle.delete_integration_job_revision( + integration_name=args.integration_name, + job_id=args.job_id, + revision_id=args.revision_id, + ) + print(f"Job revision {args.revision_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting job revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_revisions_create_command(args, chronicle): + """Handle integration job revision create command""" + try: + # Get the current job to create a revision + job = chronicle.get_integration_job( + integration_name=args.integration_name, + job_id=args.job_id, + ) + out = chronicle.create_integration_job_revision( + integration_name=args.integration_name, + job_id=args.job_id, + job=job, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating job revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_job_revisions_rollback_command(args, chronicle): + """Handle integration job revision rollback command""" + try: + out = chronicle.rollback_integration_job_revision( + integration_name=args.integration_name, + job_id=args.job_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error rolling back job revision: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/jobs.py b/src/secops/cli/commands/integration/jobs.py new file mode 100644 index 00000000..4cd04e8c --- /dev/null +++ b/src/secops/cli/commands/integration/jobs.py @@ -0,0 +1,356 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration jobs commands""" + +import json +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_jobs_command(subparsers): + """Setup integration jobs command""" + jobs_parser = subparsers.add_parser( + "jobs", + help="Manage integration jobs", + ) + lvl1 = jobs_parser.add_subparsers( + dest="jobs_command", help="Integration jobs command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration jobs") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing jobs", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing jobs", + dest="order_by", + ) + list_parser.add_argument( + "--exclude-staging", + action="store_true", + help="Exclude staging jobs from the list", + dest="exclude_staging", + ) + list_parser.set_defaults(func=handle_jobs_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get integration job details") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--job-id", + type=str, + help="ID of the job to get", + dest="job_id", + required=True, + ) + get_parser.set_defaults(func=handle_jobs_get_command) + + # delete command + delete_parser = lvl1.add_parser("delete", help="Delete an integration job") + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--job-id", + type=str, + help="ID of the job to delete", + dest="job_id", + required=True, + ) + delete_parser.set_defaults(func=handle_jobs_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", + help="Create a new integration job", + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the job", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--code", + type=str, + help="Python code for the job", + dest="code", + required=True, + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the job", + dest="description", + ) + create_parser.add_argument( + "--job-id", + type=str, + help="Custom ID for the job", + dest="job_id", + ) + create_parser.add_argument( + "--parameters", + type=str, + help="JSON string of job parameters", + dest="parameters", + ) + create_parser.set_defaults(func=handle_jobs_create_command) + + # update command + update_parser = lvl1.add_parser("update", help="Update an integration job") + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--job-id", + type=str, + help="ID of the job to update", + dest="job_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the job", + dest="display_name", + ) + update_parser.add_argument( + "--code", + type=str, + help="New Python code for the job", + dest="code", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the job", + dest="description", + ) + update_parser.add_argument( + "--parameters", + type=str, + help="JSON string of new job parameters", + dest="parameters", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_jobs_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", help="Execute an integration job test" + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--job-id", + type=str, + help="ID of the job to test", + dest="job_id", + required=True, + ) + test_parser.set_defaults(func=handle_jobs_test_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get a template for creating a job", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_jobs_template_command) + + +def handle_jobs_list_command(args, chronicle): + """Handle integration jobs list command""" + try: + out = chronicle.list_integration_jobs( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + exclude_staging=args.exclude_staging, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration jobs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_get_command(args, chronicle): + """Handle integration job get command""" + try: + out = chronicle.get_integration_job( + integration_name=args.integration_name, + job_id=args.job_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration job: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_delete_command(args, chronicle): + """Handle integration job delete command""" + try: + chronicle.delete_integration_job( + integration_name=args.integration_name, + job_id=args.job_id, + ) + print(f"Job {args.job_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration job: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_create_command(args, chronicle): + """Handle integration job create command""" + try: + # Parse parameters if provided + parameters = None + if args.parameters: + parameters = json.loads(args.parameters) + + out = chronicle.create_integration_job( + integration_name=args.integration_name, + display_name=args.display_name, + code=args.code, + description=args.description, + job_id=args.job_id, + parameters=parameters, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing parameters JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration job: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_update_command(args, chronicle): + """Handle integration job update command""" + try: + # Parse parameters if provided + parameters = None + if args.parameters: + parameters = json.loads(args.parameters) + + out = chronicle.update_integration_job( + integration_name=args.integration_name, + job_id=args.job_id, + display_name=args.display_name, + code=args.code, + description=args.description, + parameters=parameters, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except json.JSONDecodeError as e: + print(f"Error parsing parameters JSON: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration job: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_test_command(args, chronicle): + """Handle integration job test command""" + try: + # First get the job to test + job = chronicle.get_integration_job( + integration_name=args.integration_name, + job_id=args.job_id, + ) + out = chronicle.execute_integration_job_test( + integration_name=args.integration_name, + job_id=args.job_id, + job=job, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error testing integration job: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_jobs_template_command(args, chronicle): + """Handle get job template command""" + try: + out = chronicle.get_integration_job_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting job template: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/manager_revisions.py b/src/secops/cli/commands/integration/manager_revisions.py new file mode 100644 index 00000000..82116abe --- /dev/null +++ b/src/secops/cli/commands/integration/manager_revisions.py @@ -0,0 +1,254 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration manager revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_manager_revisions_command(subparsers): + """Setup integration manager revisions command""" + revisions_parser = subparsers.add_parser( + "manager-revisions", + help="Manage integration manager revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="manager_revisions_command", + help="Integration manager revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List integration manager revisions" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager", + dest="manager_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_manager_revisions_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get a specific manager revision") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager", + dest="manager_id", + required=True, + ) + get_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to get", + dest="revision_id", + required=True, + ) + get_parser.set_defaults(func=handle_manager_revisions_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration manager revision" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager", + dest="manager_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults(func=handle_manager_revisions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration manager revision" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager", + dest="manager_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults(func=handle_manager_revisions_create_command) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", help="Rollback manager to a previous revision" + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager", + dest="manager_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults(func=handle_manager_revisions_rollback_command) + + +def handle_manager_revisions_list_command(args, chronicle): + """Handle integration manager revisions list command""" + try: + out = chronicle.list_integration_manager_revisions( + integration_name=args.integration_name, + manager_id=args.manager_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing manager revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_manager_revisions_get_command(args, chronicle): + """Handle integration manager revision get command""" + try: + out = chronicle.get_integration_manager_revision( + integration_name=args.integration_name, + manager_id=args.manager_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting manager revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_manager_revisions_delete_command(args, chronicle): + """Handle integration manager revision delete command""" + try: + chronicle.delete_integration_manager_revision( + integration_name=args.integration_name, + manager_id=args.manager_id, + revision_id=args.revision_id, + ) + print(f"Manager revision {args.revision_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting manager revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_manager_revisions_create_command(args, chronicle): + """Handle integration manager revision create command""" + try: + # Get the current manager to create a revision + manager = chronicle.get_integration_manager( + integration_name=args.integration_name, + manager_id=args.manager_id, + ) + out = chronicle.create_integration_manager_revision( + integration_name=args.integration_name, + manager_id=args.manager_id, + manager=manager, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating manager revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_manager_revisions_rollback_command(args, chronicle): + """Handle integration manager revision rollback command""" + try: + out = chronicle.rollback_integration_manager_revision( + integration_name=args.integration_name, + manager_id=args.manager_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error rolling back manager revision: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/managers.py b/src/secops/cli/commands/integration/managers.py new file mode 100644 index 00000000..e5f202a0 --- /dev/null +++ b/src/secops/cli/commands/integration/managers.py @@ -0,0 +1,283 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration managers commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_managers_command(subparsers): + """Setup integration managers command""" + managers_parser = subparsers.add_parser( + "managers", + help="Manage integration managers", + ) + lvl1 = managers_parser.add_subparsers( + dest="managers_command", help="Integration managers command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration managers") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing managers", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing managers", + dest="order_by", + ) + list_parser.set_defaults(func=handle_managers_list_command) + + # get command + get_parser = lvl1.add_parser("get", help="Get integration manager details") + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager to get", + dest="manager_id", + required=True, + ) + get_parser.set_defaults(func=handle_managers_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration manager", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager to delete", + dest="manager_id", + required=True, + ) + delete_parser.set_defaults(func=handle_managers_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration manager" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the manager", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--code", + type=str, + help="Python code for the manager", + dest="code", + required=True, + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the manager", + dest="description", + ) + create_parser.add_argument( + "--manager-id", + type=str, + help="Custom ID for the manager", + dest="manager_id", + ) + create_parser.set_defaults(func=handle_managers_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an integration manager" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--manager-id", + type=str, + help="ID of the manager to update", + dest="manager_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the manager", + dest="display_name", + ) + update_parser.add_argument( + "--code", + type=str, + help="New Python code for the manager", + dest="code", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the manager", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_managers_update_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get a template for creating a manager", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_managers_template_command) + + +def handle_managers_list_command(args, chronicle): + """Handle integration managers list command""" + try: + out = chronicle.list_integration_managers( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration managers: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_managers_get_command(args, chronicle): + """Handle integration manager get command""" + try: + out = chronicle.get_integration_manager( + integration_name=args.integration_name, + manager_id=args.manager_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration manager: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_managers_delete_command(args, chronicle): + """Handle integration manager delete command""" + try: + chronicle.delete_integration_manager( + integration_name=args.integration_name, + manager_id=args.manager_id, + ) + print(f"Manager {args.manager_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration manager: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_managers_create_command(args, chronicle): + """Handle integration manager create command""" + try: + out = chronicle.create_integration_manager( + integration_name=args.integration_name, + display_name=args.display_name, + code=args.code, + description=args.description, + manager_id=args.manager_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration manager: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_managers_update_command(args, chronicle): + """Handle integration manager update command""" + try: + out = chronicle.update_integration_manager( + integration_name=args.integration_name, + manager_id=args.manager_id, + display_name=args.display_name, + code=args.code, + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration manager: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_managers_template_command(args, chronicle): + """Handle get manager template command""" + try: + out = chronicle.get_integration_manager_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting manager template: {e}", file=sys.stderr) + sys.exit(1) From 852851074e44d930017374362240f9a61e8a19f3 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 08:31:31 +0000 Subject: [PATCH 39/46] chore: update documentation for integrations --- README.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/README.md b/README.md index 8964849f..155c37b1 100644 --- a/README.md +++ b/README.md @@ -1960,6 +1960,210 @@ Uninstall a marketplace integration: chronicle.uninstall_marketplace_integration("AWSSecurityHub") ``` +### Integrations +List all installed integrations: + +```python +# Get all integrations +integrations = chronicle.list_integrations() +for i in integrations.get("integrations", []): + integration_id = i["identifier"] + integration_display_name = i["displayName"] + integration_type = i["type"] + +# Get all integrations as a list +integrations = chronicle.list_integrations(as_list=True) + +for i in integrations: + integration = chronicle.get_integration(i["identifier"]) + if integration.get("parameters"): + print(json.dumps(integration, indent=2)) + + +# Get integrations ordered by display name +integrations = chronicle.list_integrations(order_by="displayName", as_list=True) + ``` + +Get details of a specific integration: + +```python +integration = chronicle.get_integration("AWSSecurityHub") +``` + +Create an integration: + +```python +from secops.chronicle.models ( + IntegrationParam, + IntegrationParamType, + IntegrationType, + PythonVersion +) + +integration = chronicle.create_integration( + display_name="MyNewIntegration", + staging=True, + description="This is my integration", + python_version=PythonVersion.PYTHON_3_11, + parameters=[ + IntegrationParam( + display_name="AWS Access Key", + property_name="aws_access_key", + type=IntegrationParamType.STRING, + description="AWS access key for authentication", + mandatory=True, + ), + IntegrationParam( + display_name="AWS Secret Key", + property_name="aws_secret_key", + type=IntegrationParamType.PASSWORD, + description="AWS secret key for authentication", + mandatory=False, + ), + ], + categories=[ + "Cloud Security", + "Cloud", + "Security" + ], + integration_type=IntegrationType.RESPONSE, +) +``` + +Update an integration: + +```python +from secops.chronicle.models import IntegrationParam, IntegrationParamType + +updated_integration = chronicle.update_integration( + integration_name="MyNewIntegration", + display_name="Updated Integration Name", + description="Updated description", + parameters=[ + IntegrationParam( + display_name="AWS Region", + property_name="aws_region", + type=IntegrationParamType.STRING, + description="AWS region to use", + mandatory=True, + ), + ], + categories=[ + "Cloud Security", + "Cloud", + "Security" + ], +) +``` + +Delete an integration: + +```python +chronicle.delete_integration("MyNewIntegration") +``` + +Download an entire integration as a bytes object and save it as a .zip file +This includes all the integration details, parameters, and actions in a format that can be re-uploaded to Chronicle or used for backup purposes. + +```python +integration_bytes = chronicle.download_integration("MyIntegration") +with open("MyIntegration.zip", "wb") as f: + f.write(integration_bytes) +``` + +Export selected items from an integration (e.g. only actions) as a .zip file: + +```python +# Export only actions with IDs 1 and 2 from the integration + +export_bytes = chronicle.export_integration_items( + integration_name="AWSSecurityHub", + actions=["1", "2"] # IDs of the actions to export +) +with open("AWSSecurityHub_FullExport.zip", "wb") as f: + f.write(export_bytes) +``` + +Get dependencies for an integration: + +```python +dependencies = chronicle.get_integration_dependencies("AWSSecurityHub") +for dep in dependencies.get("dependencies", []): + parts = dep.split("-") + dependency_name = parts[0] + dependency_version = parts[1] if len(parts) > 1 else "latest" + print(f"Dependency: {dependency_name}, Version: {dependency_version}") +``` + +Force dependency update for an integration: + +```python +# Defining a version: +chronicle.download_integration_dependency( + "MyIntegration", + "boto3==1.42.44" +) + +# Install the latest version of a dependency: +chronicle.download_integration_dependency( + "MyIntegration", + "boto3" +) +``` + +Get remote agents that would be restricted from running an updated version of the integration + +```python +from secops.chronicle.models import PythonVersion + +agents = chronicle.get_integration_restricted_agents( + integration_name="AWSSecurityHub", + required_python_version=PythonVersion.PYTHON_3_11, +) +``` + +Get integration diff between two versions of an integration: + +```python +from secops.chronicle.models import DiffType + +# Get the diff between the commercial version of the integration and the current version in the environment. +diff = chronicle.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.COMMERCIAL +) + +# Get the difference between the staging integration and its matching production version. +diff = chronicle.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.PRODUCTION +) + +# Get the difference between the production integration and its corresponding staging version. +diff = chronicle.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.STAGING +) +``` + +Transition an integration to staging or production environment: + +```python +from secops.chronicle.models import TargetMode + +# Transition to staging environment +chronicle.transition_integration_environment( + integration_name="AWSSecurityHub", + target_mode=TargetMode.STAGING +) + +# Transition to production environment +chronicle.transition_integration_environment( + integration_name="AWSSecurityHub", + target_mode=TargetMode.PRODUCTION +) +``` + ### Integration Actions List all available actions for an integration: From f43a5cc35555167e135f1e00e2dfeb6f06132688 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 09:24:30 +0000 Subject: [PATCH 40/46] feat: implement integration transformers --- CLI.md | 122 ++++ README.md | 313 ++++++++++ api_module_mapping.md | 9 +- src/secops/chronicle/__init__.py | 17 + src/secops/chronicle/client.py | 320 ++++++++++ .../chronicle/integration/transformers.py | 405 +++++++++++++ src/secops/chronicle/models.py | 45 ++ .../integration/integration_client.py | 2 + .../cli/commands/integration/transformers.py | 390 ++++++++++++ .../integration/test_transformers.py | 555 ++++++++++++++++++ 10 files changed, 2177 insertions(+), 1 deletion(-) create mode 100644 src/secops/chronicle/integration/transformers.py create mode 100644 src/secops/cli/commands/integration/transformers.py create mode 100644 tests/chronicle/integration/test_transformers.py diff --git a/CLI.md b/CLI.md index 1330ff7b..184a6e36 100644 --- a/CLI.md +++ b/CLI.md @@ -2065,6 +2065,128 @@ secops integration instances get-default \ --integration-name "MyIntegration" ``` +#### Integration Transformers + +List integration transformers: + +```bash +# List all transformers for an integration +secops integration transformers list --integration-name "MyIntegration" + +# List transformers as a direct list (fetches all pages automatically) +secops integration transformers list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration transformers list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration transformers list --integration-name "MyIntegration" --filter-string "enabled = true" + +# Exclude staging transformers +secops integration transformers list --integration-name "MyIntegration" --exclude-staging + +# List with expanded details +secops integration transformers list --integration-name "MyIntegration" --expand "parameters" +``` + +Get transformer details: + +```bash +# Get basic transformer details +secops integration transformers get \ + --integration-name "MyIntegration" \ + --transformer-id "t1" + +# Get transformer with expanded parameters +secops integration transformers get \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --expand "parameters" +``` + +Create a new transformer: + +```bash +# Create a basic transformer +secops integration transformers create \ + --integration-name "MyIntegration" \ + --display-name "JSON Parser" \ + --script "def transform(data): import json; return json.loads(data)" \ + --script-timeout "60s" \ + --enabled + +# Create transformer with description +secops integration transformers create \ + --integration-name "MyIntegration" \ + --display-name "Data Enricher" \ + --script "def transform(data): return {'enriched': data, 'timestamp': '2024-01-01'}" \ + --script-timeout "120s" \ + --description "Enriches data with additional fields" \ + --enabled +``` + +> **Note:** When creating a transformer: +> - `--script-timeout` should be specified with a unit (e.g., "60s", "2m") +> - Use `--enabled` flag to enable the transformer on creation (default is disabled) +> - The script must be valid Python code with a `transform()` function + +Update an existing transformer: + +```bash +# Update display name +secops integration transformers update \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --display-name "Updated Transformer Name" + +# Update script +secops integration transformers update \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --script "def transform(data): return data.upper()" + +# Update multiple fields +secops integration transformers update \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --display-name "Enhanced Transformer" \ + --description "Updated with better error handling" \ + --script-timeout "90s" \ + --enabled true + +# Update with custom update mask +secops integration transformers update \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete a transformer: + +```bash +secops integration transformers delete \ + --integration-name "MyIntegration" \ + --transformer-id "t1" +``` + +Test a transformer: + +```bash +# Test an existing transformer to verify it works correctly +secops integration transformers test \ + --integration-name "MyIntegration" \ + --transformer-id "t1" +``` + +Get transformer template: + +```bash +# Get a boilerplate template for creating a new transformer +secops integration transformers template --integration-name "MyIntegration" +``` + ### Rule Management List detection rules: diff --git a/README.md b/README.md index 155c37b1..eff9594a 100644 --- a/README.md +++ b/README.md @@ -4167,6 +4167,319 @@ print(f"Default Instance: {default_instance.get('displayName')}") print(f"Environment: {default_instance.get('environment')}") ``` +### Integration Transformers + +List all transformers for a specific integration: + +```python +# Get all transformers for an integration +transformers = chronicle.list_integration_transformers("MyIntegration") +for transformer in transformers.get("transformers", []): + print(f"Transformer: {transformer.get('displayName')}, ID: {transformer.get('name')}") + +# Get all transformers as a list +transformers = chronicle.list_integration_transformers("MyIntegration", as_list=True) + +# Get only enabled transformers +transformers = chronicle.list_integration_transformers( + "MyIntegration", + filter_string="enabled = true" +) + +# Exclude staging transformers +transformers = chronicle.list_integration_transformers( + "MyIntegration", + exclude_staging=True +) + +# Get transformers with expanded details +transformers = chronicle.list_integration_transformers( + "MyIntegration", + expand="parameters" +) +``` + +Get details of a specific transformer: + +```python +transformer = chronicle.get_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1" +) +print(f"Display Name: {transformer.get('displayName')}") +print(f"Script: {transformer.get('script')}") +print(f"Enabled: {transformer.get('enabled')}") + +# Get transformer with expanded parameters +transformer = chronicle.get_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + expand="parameters" +) +``` + +Create a new transformer: + +```python +# Create a basic transformer +new_transformer = chronicle.create_integration_transformer( + integration_name="MyIntegration", + display_name="JSON Parser", + script=""" +def transform(data): + import json + try: + return json.loads(data) + except Exception as e: + return {"error": str(e)} +""", + script_timeout="60s", + enabled=True +) + +# Create transformer with all fields +new_transformer = chronicle.create_integration_transformer( + integration_name="MyIntegration", + display_name="Advanced Data Transformer", + description="Transforms and enriches incoming data", + script=""" +def transform(data, api_key, endpoint_url): + import json + import requests + + # Parse input data + parsed = json.loads(data) + + # Enrich with external API call + response = requests.get( + endpoint_url, + headers={"Authorization": f"Bearer {api_key}"} + ) + parsed["enrichment"] = response.json() + + return parsed +""", + script_timeout="120s", + enabled=True, + parameters=[ + { + "name": "api_key", + "type": "STRING", + "displayName": "API Key", + "mandatory": True + }, + { + "name": "endpoint_url", + "type": "STRING", + "displayName": "Endpoint URL", + "mandatory": True + } + ], + usage_example="Used to enrich security events with external threat intelligence", + expected_input='{"event": "data", "timestamp": "2024-01-01T00:00:00Z"}', + expected_output='{"event": "data", "timestamp": "2024-01-01T00:00:00Z", "enrichment": {...}}' +) +``` + +Update an existing transformer: + +```python +# Update transformer display name +updated_transformer = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + display_name="Updated Transformer Name" +) + +# Update transformer script +updated_transformer = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + script=""" +def transform(data): + # Updated transformation logic + return data.upper() +""" +) + +# Update multiple fields including parameters +updated_transformer = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + display_name="Enhanced Transformer", + description="Updated with better error handling", + script=""" +def transform(data, timeout=30): + import json + try: + result = json.loads(data) + result["processed"] = True + return result + except Exception as e: + return {"error": str(e), "original": data} +""", + script_timeout="90s", + enabled=True, + parameters=[ + { + "name": "timeout", + "type": "INTEGER", + "displayName": "Processing Timeout", + "mandatory": False, + "defaultValue": "30" + } + ] +) + +# Use custom update mask +updated_transformer = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + display_name="New Name", + description="New Description", + update_mask="displayName,description" +) +``` + +Delete a transformer: + +```python +chronicle.delete_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1" +) +``` + +Execute a test run of a transformer: + +```python +# Get the transformer +transformer = chronicle.get_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1" +) + +# Test the transformer with sample data +test_result = chronicle.execute_integration_transformer_test( + integration_name="MyIntegration", + transformer=transformer +) +print(f"Output Message: {test_result.get('outputMessage')}") +print(f"Debug Output: {test_result.get('debugOutputMessage')}") +print(f"Result Value: {test_result.get('resultValue')}") + +# You can also test a transformer before creating it +test_transformer = { + "displayName": "Test Transformer", + "script": """ +def transform(data): + return {"transformed": data, "status": "success"} +""", + "scriptTimeout": "60s", + "enabled": True +} + +test_result = chronicle.execute_integration_transformer_test( + integration_name="MyIntegration", + transformer=test_transformer +) +``` + +Get a template for creating a transformer: + +```python +# Get a boilerplate template for a new transformer +template = chronicle.get_integration_transformer_template("MyIntegration") +print(f"Template Script: {template.get('script')}") +print(f"Template Display Name: {template.get('displayName')}") + +# Use the template as a starting point +new_transformer = chronicle.create_integration_transformer( + integration_name="MyIntegration", + display_name="My Custom Transformer", + script=template.get('script'), # Customize this + script_timeout="60s", + enabled=True +) +``` + +Example workflow: Safe transformer development with testing: + +```python +# 1. Get a template to start with +template = chronicle.get_integration_transformer_template("MyIntegration") + +# 2. Customize the script +custom_transformer = { + "displayName": "CSV to JSON Transformer", + "description": "Converts CSV data to JSON format", + "script": """ +def transform(data): + import csv + import json + from io import StringIO + + # Parse CSV + reader = csv.DictReader(StringIO(data)) + rows = list(reader) + + return json.dumps(rows) +""", + "scriptTimeout": "60s", + "enabled": False, # Start disabled for testing + "usageExample": "Input CSV with headers, output JSON array of objects" +} + +# 3. Test the transformer before creating it +test_result = chronicle.execute_integration_transformer_test( + integration_name="MyIntegration", + transformer=custom_transformer +) + +# 4. If test is successful, create the transformer +if test_result.get('resultValue'): + created_transformer = chronicle.create_integration_transformer( + integration_name="MyIntegration", + display_name=custom_transformer["displayName"], + description=custom_transformer["description"], + script=custom_transformer["script"], + script_timeout=custom_transformer["scriptTimeout"], + enabled=True, # Enable after successful testing + usage_example=custom_transformer["usageExample"] + ) + print(f"Transformer created: {created_transformer.get('name')}") +else: + print(f"Test failed: {test_result.get('debugOutputMessage')}") + +# 5. Continue testing and refining +transformer_id = created_transformer.get('name').split('/')[-1] +updated = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id=transformer_id, + script=""" +def transform(data, delimiter=','): + import csv + import json + from io import StringIO + + # Parse CSV with custom delimiter + reader = csv.DictReader(StringIO(data), delimiter=delimiter) + rows = list(reader) + + return json.dumps(rows, indent=2) +""", + parameters=[ + { + "name": "delimiter", + "type": "STRING", + "displayName": "CSV Delimiter", + "mandatory": False, + "defaultValue": "," + } + ] +) +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index c3fb14f5..d5031246 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -8,7 +8,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ - **v1:** 17 endpoints implemented - **v1beta:** 88 endpoints implemented -- **v1alpha:** 181 endpoints implemented +- **v1alpha:** 188 endpoints implemented ## Endpoint Mapping @@ -408,6 +408,13 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.integrationInstances.get | v1alpha | chronicle.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.list | v1alpha | chronicle.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | | | integrations.integrationInstances.patch | v1alpha | chronicle.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | | +| integrations.transformers.create | v1alpha | chronicle.integration.transformers.create_integration_transformer | | +| integrations.transformers.delete | v1alpha | chronicle.integration.transformers.delete_integration_transformer | | +| integrations.transformers.executeTest | v1alpha | chronicle.integration.transformers.execute_integration_transformer_test | | +| integrations.transformers.fetchTemplate | v1alpha | chronicle.integration.transformers.get_integration_transformer_template | | +| integrations.transformers.get | v1alpha | chronicle.integration.transformers.get_integration_transformer | | +| integrations.transformers.list | v1alpha | chronicle.integration.transformers.list_integration_transformers | | +| integrations.transformers.patch | v1alpha | chronicle.integration.transformers.update_integration_transformer | | | integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 8a0c640e..cc6563bf 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -339,6 +339,15 @@ get_integration_instance_affected_items, get_default_integration_instance, ) +from secops.chronicle.integration.transformers import ( + list_integration_transformers, + get_integration_transformer, + delete_integration_transformer, + create_integration_transformer, + update_integration_transformer, + execute_integration_transformer_test, + get_integration_transformer_template, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -634,6 +643,14 @@ "execute_integration_instance_test", "get_integration_instance_affected_items", "get_default_integration_instance", + # Integration Transformers + "list_integration_transformers", + "get_integration_transformer", + "delete_integration_transformer", + "create_integration_transformer", + "update_integration_transformer", + "execute_integration_transformer_test", + "get_integration_transformer_template", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 9eb937aa..b38d67a8 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -267,6 +267,15 @@ list_integration_instances as _list_integration_instances, update_integration_instance as _update_integration_instance, ) +from secops.chronicle.integration.transformers import ( + create_integration_transformer as _create_integration_transformer, + delete_integration_transformer as _delete_integration_transformer, + execute_integration_transformer_test as _execute_integration_transformer_test, + get_integration_transformer as _get_integration_transformer, + get_integration_transformer_template as _get_integration_transformer_template, + list_integration_transformers as _list_integration_transformers, + update_integration_transformer as _update_integration_transformer, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -5081,6 +5090,317 @@ def get_default_integration_instance( api_version=api_version, ) + # -- Integration Transformers methods -- + + def list_integration_transformers( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all transformer definitions for a specific integration. + + Use this method to browse the available transformers. + + Args: + integration_name: Name of the integration to list transformers + for. + page_size: Maximum number of transformers to return. Defaults + to 100, maximum is 200. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter transformers. + order_by: Field to sort the transformers by. + exclude_staging: Whether to exclude staging transformers from + the response. By default, staging transformers are included. + expand: Expand the response with the full transformer details. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + If as_list is True: List of transformers. + If as_list is False: Dict with transformers list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_transformers( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + exclude_staging=exclude_staging, + expand=expand, + api_version=api_version, + ) + + def get_integration_transformer( + self, + integration_name: str, + transformer_id: str, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Get a single transformer definition for a specific integration. + + Use this method to retrieve the Python script, input parameters, + and expected input, output and usage example schema for a specific + data transformation logic within an integration. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to retrieve. + expand: Expand the response with the full transformer details. + Optional. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing details of the specified TransformerDefinition. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_transformer( + self, + integration_name, + transformer_id, + expand=expand, + api_version=api_version, + ) + + def delete_integration_transformer( + self, + integration_name: str, + transformer_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> None: + """Delete a custom transformer definition from a given integration. + + Use this method to permanently remove an obsolete transformer from + an integration. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to delete. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_transformer( + self, + integration_name, + transformer_id, + api_version=api_version, + ) + + def create_integration_transformer( + self, + integration_name: str, + display_name: str, + script: str, + script_timeout: str, + enabled: bool, + description: str | None = None, + parameters: list[dict[str, Any]] | None = None, + usage_example: str | None = None, + expected_output: str | None = None, + expected_input: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Create a new transformer definition for a given integration. + + Use this method to define a transformer, specifying its functional + Python script and necessary input parameters. + + Args: + integration_name: Name of the integration to create the + transformer for. + display_name: Transformer's display name. Maximum 150 characters. + Required. + script: Transformer's Python script. Required. + script_timeout: Timeout in seconds for a single script run. + Default is 60. Required. + enabled: Whether the transformer is enabled or disabled. + Required. + description: Transformer's description. Maximum 2050 characters. + Optional. + parameters: List of transformer parameter dicts. Optional. + usage_example: Transformer's usage example. Optional. + expected_output: Transformer's expected output. Optional. + expected_input: Transformer's expected input. Optional. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the newly created TransformerDefinition + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_transformer( + self, + integration_name, + display_name, + script, + script_timeout, + enabled, + description=description, + parameters=parameters, + usage_example=usage_example, + expected_output=expected_output, + expected_input=expected_input, + api_version=api_version, + ) + + def update_integration_transformer( + self, + integration_name: str, + transformer_id: str, + display_name: str | None = None, + script: str | None = None, + script_timeout: str | None = None, + enabled: bool | None = None, + description: str | None = None, + parameters: list[dict[str, Any]] | None = None, + usage_example: str | None = None, + expected_output: str | None = None, + expected_input: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Update an existing transformer definition for a given + integration. + + Use this method to modify a transformation's Python script, adjust + its description, or refine its parameter definitions. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to update. + display_name: Transformer's display name. Maximum 150 + characters. + script: Transformer's Python script. + script_timeout: Timeout in seconds for a single script run. + enabled: Whether the transformer is enabled or disabled. + description: Transformer's description. Maximum 2050 characters. + parameters: List of transformer parameter dicts. When updating + existing parameters, id must be provided in each parameter. + usage_example: Transformer's usage example. + expected_output: Transformer's expected output. + expected_input: Transformer's expected input. + update_mask: Comma-separated list of fields to update. If + omitted, the mask is auto-generated from whichever fields + are provided. Example: "displayName,script". + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the updated TransformerDefinition resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_transformer( + self, + integration_name, + transformer_id, + display_name=display_name, + script=script, + script_timeout=script_timeout, + enabled=enabled, + description=description, + parameters=parameters, + usage_example=usage_example, + expected_output=expected_output, + expected_input=expected_input, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_transformer_test( + self, + integration_name: str, + transformer: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Execute a test run of a transformer's Python script. + + Use this method to verify transformation logic and ensure data is + being parsed and formatted correctly before saving or deploying + the transformer. The full transformer object is required as the + test can be run without saving the transformer first. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer: Dict containing the TransformerDefinition to test. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the test execution results with the following + fields: + - outputMessage: Human-readable output message set by the + script. + - debugOutputMessage: The script debug output. + - resultValue: The script result value. + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_transformer_test( + self, + integration_name, + transformer, + api_version=api_version, + ) + + def get_integration_transformer_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new transformer. + + Use this method to jumpstart the development of a custom data + transformation logic by providing boilerplate code. + + Args: + integration_name: Name of the integration to fetch the template + for. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the TransformerDefinition template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_transformer_template( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/transformers.py b/src/secops/chronicle/integration/transformers.py new file mode 100644 index 00000000..be34f279 --- /dev/null +++ b/src/secops/chronicle/integration/transformers.py @@ -0,0 +1,405 @@ +# Copyright 2025 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. +# +"""Integration transformers functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + TransformerDefinitionParameter +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_transformers( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all transformer definitions for a specific integration. + + Use this method to browse the available transformers. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list transformers for. + page_size: Maximum number of transformers to return. Defaults to 100, + maximum is 200. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter transformers. + order_by: Field to sort the transformers by. + exclude_staging: Whether to exclude staging transformers from the + response. By default, staging transformers are included. + expand: Expand the response with the full transformer details. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + If as_list is True: List of transformers. + If as_list is False: Dict with transformers list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + "excludeStaging": exclude_staging, + "expand": expand, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers" + ), + items_key="transformers", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=False, + ) + + +def get_integration_transformer( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Get a single transformer definition for a specific integration. + + Use this method to retrieve the Python script, input parameters, and + expected input, output and usage example schema for a specific data + transformation logic within an integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to retrieve. + expand: Expand the response with the full transformer details. + Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing details of the specified TransformerDefinition. + + Raises: + APIError: If the API request fails. + """ + params = {} + if expand is not None: + params["expand"] = expand + + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}" + ), + api_version=api_version, + params=params if params else None, + ) + + +def delete_integration_transformer( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> None: + """Delete a custom transformer definition from a given integration. + + Use this method to permanently remove an obsolete transformer from an + integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to delete. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}" + ), + api_version=api_version, + ) + + +def create_integration_transformer( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + script_timeout: str, + enabled: bool, + description: str | None = None, + parameters: ( + list[dict[str, Any] | TransformerDefinitionParameter] | None + ) = None, + usage_example: str | None = None, + expected_output: str | None = None, + expected_input: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Create a new transformer definition for a given integration. + + Use this method to define a transformer, specifying its functional Python + script and necessary input parameters. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the transformer + for. + display_name: Transformer's display name. Maximum 150 characters. + Required. + script: Transformer's Python script. Required. + script_timeout: Timeout in seconds for a single script run. Default + is 60. Required. + enabled: Whether the transformer is enabled or disabled. Required. + description: Transformer's description. Maximum 2050 characters. + Optional. + parameters: List of TransformerDefinitionParameter instances or + dicts. Optional. + usage_example: Transformer's usage example. Optional. + expected_output: Transformer's expected output. Optional. + expected_input: Transformer's expected input. Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the newly created TransformerDefinition resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p + for p in parameters] + if parameters is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "scriptTimeout": script_timeout, + "enabled": enabled, + "description": description, + "parameters": resolved_parameters, + "usageExample": usage_example, + "expectedOutput": expected_output, + "expectedInput": expected_input, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/transformers" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_transformer( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + display_name: str | None = None, + script: str | None = None, + script_timeout: str | None = None, + enabled: bool | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | TransformerDefinitionParameter] | None + ) = None, + usage_example: str | None = None, + expected_output: str | None = None, + expected_input: str | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Update an existing transformer definition for a given integration. + + Use this method to modify a transformation's Python script, adjust its + description, or refine its parameter definitions. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to update. + display_name: Transformer's display name. Maximum 150 characters. + script: Transformer's Python script. + script_timeout: Timeout in seconds for a single script run. + enabled: Whether the transformer is enabled or disabled. + description: Transformer's description. Maximum 2050 characters. + parameters: List of TransformerDefinitionParameter instances or + dicts. When updating existing parameters, id must be provided + in each TransformerDefinitionParameter. + usage_example: Transformer's usage example. + expected_output: Transformer's expected output. + expected_input: Transformer's expected input. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the updated TransformerDefinition resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p + for p in parameters] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("scriptTimeout", "scriptTimeout", script_timeout), + ("enabled", "enabled", enabled), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ("usageExample", "usageExample", usage_example), + ("expectedOutput", "expectedOutput", expected_output), + ("expectedInput", "expectedInput", expected_input), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_transformer_test( + client: "ChronicleClient", + integration_name: str, + transformer: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Execute a test run of a transformer's Python script. + + Use this method to verify transformation logic and ensure data is being + parsed and formatted correctly before saving or deploying the transformer. + The full transformer object is required as the test can be run without + saving the transformer first. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer: Dict containing the TransformerDefinition to test. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the test execution results with the following fields: + - outputMessage: Human-readable output message set by the script. + - debugOutputMessage: The script debug output. + - resultValue: The script result value. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers:executeTest" + ), + api_version=api_version, + json={"transformer": transformer}, + ) + + +def get_integration_transformer_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a new transformer. + + Use this method to jumpstart the development of a custom data + transformation logic by providing boilerplate code. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the TransformerDefinition template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index f199c50a..f26e9668 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -639,6 +639,51 @@ def to_dict(self) -> dict: return data +class TransformerType(str, Enum): + """Transformer types for Chronicle SOAR integration transformers.""" + + UNSPECIFIED = "TRANSFORMER_TYPE_UNSPECIFIED" + BUILT_IN = "BUILT_IN" + CUSTOM = "CUSTOM" + + +@dataclass +class TransformerDefinitionParameter: + """A parameter definition for a Chronicle SOAR transformer definition. + + Attributes: + display_name: The parameter's display name. May contain letters, + numbers, and underscores. Maximum 150 characters. + mandatory: Whether the parameter is mandatory for configuring a + transformer instance. + id: The parameter's id. Server-generated on creation; must be + provided when updating an existing parameter. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + description: The parameter's description. Maximum 2050 characters. + """ + + display_name: str + mandatory: bool + id: str | None = None + default_value: str | None = None + description: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "mandatory": self.mandatory, + } + if self.id is not None: + data["id"] = self.id + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.description is not None: + data["description"] = self.description + return data + + @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index b0636f07..6541df10 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -32,6 +32,7 @@ managers, manager_revisions, integration_instances, + transformers, ) @@ -47,6 +48,7 @@ def setup_integrations_command(subparsers): # Setup all subcommands under `integration` integration.setup_integrations_command(lvl1) integration_instances.setup_integration_instances_command(lvl1) + transformers.setup_transformers_command(lvl1) actions.setup_actions_command(lvl1) action_revisions.setup_action_revisions_command(lvl1) connectors.setup_connectors_command(lvl1) diff --git a/src/secops/cli/commands/integration/transformers.py b/src/secops/cli/commands/integration/transformers.py new file mode 100644 index 00000000..de2851cc --- /dev/null +++ b/src/secops/cli/commands/integration/transformers.py @@ -0,0 +1,390 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration transformers commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_transformers_command(subparsers): + """Setup integration transformers command""" + transformers_parser = subparsers.add_parser( + "transformers", + help="Manage integration transformers", + ) + lvl1 = transformers_parser.add_subparsers( + dest="transformers_command", + help="Integration transformers command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", + help="List integration transformers", + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing transformers", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing transformers", + dest="order_by", + ) + list_parser.add_argument( + "--exclude-staging", + action="store_true", + help="Exclude staging transformers from the response", + dest="exclude_staging", + ) + list_parser.add_argument( + "--expand", + type=str, + help="Expand the response with full transformer details", + dest="expand", + ) + list_parser.set_defaults(func=handle_transformers_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", + help="Get integration transformer details", + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer to get", + dest="transformer_id", + required=True, + ) + get_parser.add_argument( + "--expand", + type=str, + help="Expand the response with full transformer details", + dest="expand", + ) + get_parser.set_defaults(func=handle_transformers_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration transformer", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer to delete", + dest="transformer_id", + required=True, + ) + delete_parser.set_defaults(func=handle_transformers_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", + help="Create a new integration transformer", + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the transformer", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--script", + type=str, + help="Python script for the transformer", + dest="script", + required=True, + ) + create_parser.add_argument( + "--script-timeout", + type=str, + help="Timeout for script execution (e.g., '60s')", + dest="script_timeout", + required=True, + ) + create_parser.add_argument( + "--enabled", + action="store_true", + help="Enable the transformer (default: disabled)", + dest="enabled", + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the transformer", + dest="description", + ) + create_parser.set_defaults(func=handle_transformers_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", + help="Update an integration transformer", + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer to update", + dest="transformer_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the transformer", + dest="display_name", + ) + update_parser.add_argument( + "--script", + type=str, + help="New Python script for the transformer", + dest="script", + ) + update_parser.add_argument( + "--script-timeout", + type=str, + help="New timeout for script execution", + dest="script_timeout", + ) + update_parser.add_argument( + "--enabled", + type=lambda x: x.lower() == "true", + help="Enable or disable the transformer (true/false)", + dest="enabled", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the transformer", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_transformers_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", + help="Execute an integration transformer test", + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer to test", + dest="transformer_id", + required=True, + ) + test_parser.set_defaults(func=handle_transformers_test_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get transformer template", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_transformers_template_command) + + +def handle_transformers_list_command(args, chronicle): + """Handle integration transformers list command""" + try: + out = chronicle.list_integration_transformers( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + exclude_staging=args.exclude_staging, + expand=args.expand, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration transformers: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_transformers_get_command(args, chronicle): + """Handle integration transformer get command""" + try: + out = chronicle.get_integration_transformer( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + expand=args.expand, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration transformer: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_transformers_delete_command(args, chronicle): + """Handle integration transformer delete command""" + try: + chronicle.delete_integration_transformer( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + ) + print( + f"Transformer {args.transformer_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration transformer: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_transformers_create_command(args, chronicle): + """Handle integration transformer create command""" + try: + out = chronicle.create_integration_transformer( + integration_name=args.integration_name, + display_name=args.display_name, + script=args.script, + script_timeout=args.script_timeout, + enabled=args.enabled, + description=args.description, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating integration transformer: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_transformers_update_command(args, chronicle): + """Handle integration transformer update command""" + try: + out = chronicle.update_integration_transformer( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + display_name=args.display_name, + script=args.script, + script_timeout=args.script_timeout, + enabled=args.enabled, + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error updating integration transformer: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_transformers_test_command(args, chronicle): + """Handle integration transformer test command""" + try: + # Get the transformer first + transformer = chronicle.get_integration_transformer( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + ) + + out = chronicle.execute_integration_transformer_test( + integration_name=args.integration_name, + transformer=transformer, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error testing integration transformer: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_transformers_template_command(args, chronicle): + """Handle integration transformer template command""" + try: + out = chronicle.get_integration_transformer_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting transformer template: {e}", + file=sys.stderr, + ) + sys.exit(1) + diff --git a/tests/chronicle/integration/test_transformers.py b/tests/chronicle/integration/test_transformers.py new file mode 100644 index 00000000..43d8687e --- /dev/null +++ b/tests/chronicle/integration/test_transformers.py @@ -0,0 +1,555 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration transformers functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.transformers import ( + list_integration_transformers, + get_integration_transformer, + delete_integration_transformer, + create_integration_transformer, + update_integration_transformer, + execute_integration_transformer_test, + get_integration_transformer_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1ALPHA, + ) + + +# -- list_integration_transformers tests -- + + +def test_list_integration_transformers_success(chronicle_client): + """Test list_integration_transformers delegates to chronicle_paginated_request.""" + expected = { + "transformers": [{"name": "t1"}, {"name": "t2"}], + "nextPageToken": "token", + } + + with patch( + "secops.chronicle.integration.transformers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="My Integration", + ): + result = list_integration_transformers( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/My Integration/transformers", + items_key="transformers", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_transformers_default_args(chronicle_client): + """Test list_integration_transformers with default args.""" + expected = {"transformers": []} + + with patch( + "secops.chronicle.integration.transformers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_transformers( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/transformers", + items_key="transformers", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_integration_transformers_with_filter_order_expand(chronicle_client): + """Test list passes filter/orderBy/excludeStaging/expand in extra_params.""" + expected = {"transformers": [{"name": "t1"}]} + + with patch( + "secops.chronicle.integration.transformers.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_transformers( + chronicle_client, + integration_name="test-integration", + filter_string='displayName = "My Transformer"', + order_by="displayName", + exclude_staging=True, + expand="parameters", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/transformers", + items_key="transformers", + page_size=None, + page_token=None, + extra_params={ + "filter": 'displayName = "My Transformer"', + "orderBy": "displayName", + "excludeStaging": True, + "expand": "parameters", + }, + as_list=False, + ) + + +# -- get_integration_transformer tests -- + + +def test_get_integration_transformer_success(chronicle_client): + """Test get_integration_transformer delegates to chronicle_request.""" + expected = {"name": "transformer1", "displayName": "My Transformer"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="test-integration", + ): + result = get_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/transformers/transformer1", + api_version=APIVersion.V1ALPHA, + params=None, + ) + + +def test_get_integration_transformer_with_expand(chronicle_client): + """Test get_integration_transformer with expand parameter.""" + expected = {"name": "transformer1"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + expand="parameters", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/transformers/transformer1", + api_version=APIVersion.V1ALPHA, + params={"expand": "parameters"}, + ) + + +# -- delete_integration_transformer tests -- + + +def test_delete_integration_transformer_success(chronicle_client): + """Test delete_integration_transformer delegates to chronicle_request.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=None, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="test-integration", + ): + delete_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/transformers/transformer1", + api_version=APIVersion.V1ALPHA, + ) + + +# -- create_integration_transformer tests -- + + +def test_create_integration_transformer_minimal(chronicle_client): + """Test create_integration_transformer with minimal required fields.""" + expected = {"name": "transformer1", "displayName": "New Transformer"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="test-integration", + ): + result = create_integration_transformer( + chronicle_client, + integration_name="test-integration", + display_name="New Transformer", + script="def transform(data): return data", + script_timeout="60s", + enabled=True, + ) + + assert result == expected + + mock_request.assert_called_once() + call_kwargs = mock_request.call_args[1] + assert call_kwargs["method"] == "POST" + assert ( + call_kwargs["endpoint_path"] + == "integrations/test-integration/transformers" + ) + assert call_kwargs["api_version"] == APIVersion.V1ALPHA + assert call_kwargs["json"]["displayName"] == "New Transformer" + assert call_kwargs["json"]["script"] == "def transform(data): return data" + assert call_kwargs["json"]["scriptTimeout"] == "60s" + assert call_kwargs["json"]["enabled"] is True + + +def test_create_integration_transformer_with_all_fields(chronicle_client): + """Test create_integration_transformer with all optional fields.""" + expected = {"name": "transformer1"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_transformer( + chronicle_client, + integration_name="test-integration", + display_name="Full Transformer", + script="def transform(data): return data", + script_timeout="120s", + enabled=False, + description="Test transformer description", + parameters=[{"name": "param1", "type": "STRING"}], + usage_example="Example usage", + expected_output="Output format", + expected_input="Input format", + ) + + assert result == expected + + call_kwargs = mock_request.call_args[1] + body = call_kwargs["json"] + assert body["displayName"] == "Full Transformer" + assert body["description"] == "Test transformer description" + assert body["parameters"] == [{"name": "param1", "type": "STRING"}] + assert body["usageExample"] == "Example usage" + assert body["expectedOutput"] == "Output format" + assert body["expectedInput"] == "Input format" + + +# -- update_integration_transformer tests -- + + +def test_update_integration_transformer_display_name(chronicle_client): + """Test update_integration_transformer updates display name.""" + expected = {"name": "transformer1", "displayName": "Updated Name"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.build_patch_body", + return_value=({"displayName": "Updated Name"}, {"updateMask": "displayName"}), + ) as mock_build: + result = update_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + display_name="Updated Name", + ) + + assert result == expected + + mock_build.assert_called_once() + mock_request.assert_called_once() + call_kwargs = mock_request.call_args[1] + assert call_kwargs["method"] == "PATCH" + assert ( + call_kwargs["endpoint_path"] + == "integrations/test-integration/transformers/transformer1" + ) + + +def test_update_integration_transformer_with_update_mask(chronicle_client): + """Test update_integration_transformer with explicit update mask.""" + expected = {"name": "transformer1"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.build_patch_body", + return_value=( + {"displayName": "New Name", "enabled": True}, + {"updateMask": "displayName,enabled"}, + ), + ): + result = update_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + display_name="New Name", + enabled=True, + update_mask="displayName,enabled", + ) + + assert result == expected + + +def test_update_integration_transformer_all_fields(chronicle_client): + """Test update_integration_transformer with all fields.""" + expected = {"name": "transformer1"} + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.build_patch_body", + return_value=( + { + "displayName": "Updated", + "script": "new script", + "scriptTimeout": "90s", + "enabled": False, + "description": "Updated description", + "parameters": [{"name": "p1"}], + "usageExample": "New example", + "expectedOutput": "New output", + "expectedInput": "New input", + }, + {"updateMask": "displayName,script,scriptTimeout,enabled,description"}, + ), + ): + result = update_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + display_name="Updated", + script="new script", + script_timeout="90s", + enabled=False, + description="Updated description", + parameters=[{"name": "p1"}], + usage_example="New example", + expected_output="New output", + expected_input="New input", + ) + + assert result == expected + + +# -- execute_integration_transformer_test tests -- + + +def test_execute_integration_transformer_test_success(chronicle_client): + """Test execute_integration_transformer_test delegates to chronicle_request.""" + transformer = { + "displayName": "Test Transformer", + "script": "def transform(data): return data", + } + expected = { + "outputMessage": "Success", + "debugOutputMessage": "Debug info", + "resultValue": {"status": "ok"}, + } + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="test-integration", + ): + result = execute_integration_transformer_test( + chronicle_client, + integration_name="test-integration", + transformer=transformer, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/transformers:executeTest", + api_version=APIVersion.V1ALPHA, + json={"transformer": transformer}, + ) + + +# -- get_integration_transformer_template tests -- + + +def test_get_integration_transformer_template_success(chronicle_client): + """Test get_integration_transformer_template delegates to chronicle_request.""" + expected = { + "script": "def transform(data):\n # Template code\n return data", + "displayName": "Template Transformer", + } + + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformers.format_resource_id", + return_value="test-integration", + ): + result = get_integration_transformer_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/transformers:fetchTemplate", + api_version=APIVersion.V1ALPHA, + ) + + +# -- Error handling tests -- + + +def test_list_integration_transformers_api_error(chronicle_client): + """Test list_integration_transformers handles API errors.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_paginated_request", + side_effect=APIError("API Error"), + ): + with pytest.raises(APIError, match="API Error"): + list_integration_transformers( + chronicle_client, + integration_name="test-integration", + ) + + +def test_get_integration_transformer_api_error(chronicle_client): + """Test get_integration_transformer handles API errors.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + side_effect=APIError("Not found"), + ): + with pytest.raises(APIError, match="Not found"): + get_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="nonexistent", + ) + + +def test_create_integration_transformer_api_error(chronicle_client): + """Test create_integration_transformer handles API errors.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + side_effect=APIError("Creation failed"), + ): + with pytest.raises(APIError, match="Creation failed"): + create_integration_transformer( + chronicle_client, + integration_name="test-integration", + display_name="New Transformer", + script="def transform(data): return data", + script_timeout="60s", + enabled=True, + ) + + +def test_update_integration_transformer_api_error(chronicle_client): + """Test update_integration_transformer handles API errors.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + side_effect=APIError("Update failed"), + ), patch( + "secops.chronicle.integration.transformers.build_patch_body", + return_value=({"displayName": "Updated"}, {"updateMask": "displayName"}), + ): + with pytest.raises(APIError, match="Update failed"): + update_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + display_name="Updated", + ) + + +def test_delete_integration_transformer_api_error(chronicle_client): + """Test delete_integration_transformer handles API errors.""" + with patch( + "secops.chronicle.integration.transformers.chronicle_request", + side_effect=APIError("Delete failed"), + ): + with pytest.raises(APIError, match="Delete failed"): + delete_integration_transformer( + chronicle_client, + integration_name="test-integration", + transformer_id="transformer1", + ) + From 22df29de69707c6cfc56e0184744cd8f1e865bd3 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 10:01:06 +0000 Subject: [PATCH 41/46] feat: implement logical operators --- CLI.md | 266 +++++++++ README.md | 556 ++++++++++++++++++ api_module_mapping.md | 13 +- src/secops/chronicle/__init__.py | 28 + src/secops/chronicle/client.py | 478 +++++++++++++++ .../integration/logical_operators.py | 401 +++++++++++++ .../integration/transformer_revisions.py | 202 +++++++ src/secops/chronicle/models.py | 51 ++ .../integration/integration_client.py | 4 + .../commands/integration/logical_operators.py | 399 +++++++++++++ .../integration/transformer_revisions.py | 239 ++++++++ .../integration/test_logical_operators.py | 547 +++++++++++++++++ .../integration/test_transformer_revisions.py | 366 ++++++++++++ 13 files changed, 3549 insertions(+), 1 deletion(-) create mode 100644 src/secops/chronicle/integration/logical_operators.py create mode 100644 src/secops/chronicle/integration/transformer_revisions.py create mode 100644 src/secops/cli/commands/integration/logical_operators.py create mode 100644 src/secops/cli/commands/integration/transformer_revisions.py create mode 100644 tests/chronicle/integration/test_logical_operators.py create mode 100644 tests/chronicle/integration/test_transformer_revisions.py diff --git a/CLI.md b/CLI.md index 184a6e36..2ac44f54 100644 --- a/CLI.md +++ b/CLI.md @@ -2187,6 +2187,272 @@ Get transformer template: secops integration transformers template --integration-name "MyIntegration" ``` +#### Transformer Revisions + +List transformer revisions: + +```bash +# List all revisions for a transformer +secops integration transformer-revisions list \ + --integration-name "MyIntegration" \ + --transformer-id "t1" + +# List revisions as a direct list +secops integration transformer-revisions list \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --as-list + +# List with pagination +secops integration transformer-revisions list \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --page-size 10 + +# List with filtering and ordering +secops integration transformer-revisions list \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --filter-string "version = '1.0'" \ + --order-by "createTime desc" +``` + +Delete a transformer revision: + +```bash +# Delete a specific revision +secops integration transformer-revisions delete \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --revision-id "rev-456" +``` + +Create a new revision: + +```bash +# Create a backup revision before making changes +secops integration transformer-revisions create \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --comment "Backup before major refactor" + +# Create a revision with descriptive comment +secops integration transformer-revisions create \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --comment "Version 2.0 - Enhanced error handling" +``` + +Rollback to a previous revision: + +```bash +# Rollback transformer to a specific revision +secops integration transformer-revisions rollback \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --revision-id "rev-456" +``` + +Example workflow: Safe transformer updates with revision control: + +```bash +# 1. Create a backup revision +secops integration transformer-revisions create \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --comment "Backup before updating transformation logic" + +# 2. Update the transformer +secops integration transformers update \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --script "def transform(data): return data.upper()" \ + --description "Updated with new transformation" + +# 3. Test the updated transformer +secops integration transformers test \ + --integration-name "MyIntegration" \ + --transformer-id "t1" + +# 4. If test fails, rollback to the backup revision +# First, list revisions to get the backup revision ID +secops integration transformer-revisions list \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --order-by "createTime desc" \ + --page-size 1 + +# Then rollback using the revision ID +secops integration transformer-revisions rollback \ + --integration-name "MyIntegration" \ + --transformer-id "t1" \ + --revision-id "rev-backup-id" +``` + +#### Logical Operators + +List logical operators: + +```bash +# List all logical operators for an integration +secops integration logical-operators list --integration-name "MyIntegration" + +# List logical operators as a direct list +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --as-list + +# List with pagination +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --page-size 50 + +# List with filtering +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --filter-string "enabled = true" + +# Exclude staging logical operators +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --exclude-staging + +# List with expanded details +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --expand "parameters" +``` + +Get logical operator details: + +```bash +# Get basic logical operator details +secops integration logical-operators get \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" + +# Get logical operator with expanded parameters +secops integration logical-operators get \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --expand "parameters" +``` + +Create a new logical operator: + +```bash +# Create a basic equality operator +secops integration logical-operators create \ + --integration-name "MyIntegration" \ + --display-name "Equals Operator" \ + --script "def evaluate(a, b): return a == b" \ + --script-timeout "60s" \ + --enabled + +# Create logical operator with description +secops integration logical-operators create \ + --integration-name "MyIntegration" \ + --display-name "Threshold Checker" \ + --script "def evaluate(value, threshold): return value > threshold" \ + --script-timeout "30s" \ + --description "Checks if value exceeds threshold" \ + --enabled +``` + +> **Note:** When creating a logical operator: +> - `--script-timeout` should be specified with a unit (e.g., "60s", "2m") +> - Use `--enabled` flag to enable the operator on creation (default is disabled) +> - The script must be valid Python code with an `evaluate()` function + +Update an existing logical operator: + +```bash +# Update display name +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --display-name "Updated Operator Name" + +# Update script +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --script "def evaluate(a, b): return a != b" + +# Update multiple fields +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --display-name "Enhanced Operator" \ + --description "Updated with better logic" \ + --script-timeout "45s" \ + --enabled true + +# Update with custom update mask +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete a logical operator: + +```bash +secops integration logical-operators delete \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" +``` + +Test a logical operator: + +```bash +# Test an existing logical operator to verify it works correctly +secops integration logical-operators test \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" +``` + +Get logical operator template: + +```bash +# Get a boilerplate template for creating a new logical operator +secops integration logical-operators template --integration-name "MyIntegration" +``` + +Example workflow: Building conditional logic: + +```bash +# 1. Get a template to start with +secops integration logical-operators template \ + --integration-name "MyIntegration" + +# 2. Create a severity checker operator +secops integration logical-operators create \ + --integration-name "MyIntegration" \ + --display-name "Severity Level Check" \ + --script "def evaluate(severity, min_level): return severity >= min_level" \ + --script-timeout "30s" \ + --description "Checks if severity meets minimum threshold" + +# 3. Test the operator +secops integration logical-operators test \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" + +# 4. Enable the operator if test passes +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --enabled true + +# 5. List all operators to see what's available +secops integration logical-operators list \ + --integration-name "MyIntegration" \ + --as-list +``` + ### Rule Management List detection rules: diff --git a/README.md b/README.md index eff9594a..2a2308a7 100644 --- a/README.md +++ b/README.md @@ -4480,6 +4480,562 @@ def transform(data, delimiter=','): ) ``` +### Integration Transformer Revisions + +List all revisions for a transformer: + +```python +# Get all revisions for a transformer +revisions = chronicle.list_integration_transformer_revisions( + integration_name="MyIntegration", + transformer_id="t1" +) +for revision in revisions.get("revisions", []): + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_transformer_revisions( + integration_name="MyIntegration", + transformer_id="t1", + as_list=True +) + +# Filter revisions +revisions = chronicle.list_integration_transformer_revisions( + integration_name="MyIntegration", + transformer_id="t1", + filter_string='version = "1.0"', + order_by="createTime desc" +) +``` + +Delete a specific transformer revision: + +```python +chronicle.delete_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + revision_id="rev-456" +) +``` + +Create a new revision before making changes: + +```python +# Get the current transformer +transformer = chronicle.get_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1" +) + +# Create a backup revision +new_revision = chronicle.create_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + transformer=transformer, + comment="Backup before major refactor" +) +print(f"Created revision: {new_revision.get('name')}") + +# Create revision with custom comment +new_revision = chronicle.create_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + transformer=transformer, + comment="Version 2.0 - Enhanced error handling" +) +``` + +Rollback to a previous revision: + +```python +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + revision_id="rev-456" +) +print(f"Rolled back to: {rollback_result.get('name')}") +``` + +Example workflow: Safe transformer updates with revision control: + +```python +# 1. Get the current transformer +transformer = chronicle.get_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1" +) + +# 2. Create a backup revision +backup = chronicle.create_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + transformer=transformer, + comment="Backup before updating transformation logic" +) + +# 3. Make changes to the transformer +updated_transformer = chronicle.update_integration_transformer( + integration_name="MyIntegration", + transformer_id="t1", + display_name="Enhanced Transformer", + script=""" +def transform(data, enrichment_enabled=True): + import json + + try: + # Parse input data + parsed = json.loads(data) + + # Apply transformations + parsed["processed"] = True + parsed["timestamp"] = "2024-01-01T00:00:00Z" + + # Optional enrichment + if enrichment_enabled: + parsed["enriched"] = True + + return json.dumps(parsed) + except Exception as e: + return json.dumps({"error": str(e), "original": data}) +""" +) + +# 4. Test the updated transformer +test_result = chronicle.execute_integration_transformer_test( + integration_name="MyIntegration", + transformer=updated_transformer +) + +# 5. If test fails, rollback to backup +if not test_result.get("resultValue"): + print("Test failed - rolling back") + chronicle.rollback_integration_transformer_revision( + integration_name="MyIntegration", + transformer_id="t1", + revision_id=backup.get("name").split("/")[-1] + ) +else: + print("Test passed - transformer updated successfully") + +# 6. List all revisions to see history +all_revisions = chronicle.list_integration_transformer_revisions( + integration_name="MyIntegration", + transformer_id="t1", + as_list=True +) +print(f"Total revisions: {len(all_revisions)}") +for rev in all_revisions: + print(f" - {rev.get('comment', 'No comment')} (ID: {rev.get('name').split('/')[-1]})") +``` + +### Integration Logical Operators + +List all logical operators for a specific integration: + +```python +# Get all logical operators for an integration +logical_operators = chronicle.list_integration_logical_operators("MyIntegration") +for operator in logical_operators.get("logicalOperators", []): + print(f"Operator: {operator.get('displayName')}, ID: {operator.get('name')}") + +# Get all logical operators as a list +logical_operators = chronicle.list_integration_logical_operators( + "MyIntegration", + as_list=True +) + +# Get only enabled logical operators +logical_operators = chronicle.list_integration_logical_operators( + "MyIntegration", + filter_string="enabled = true" +) + +# Exclude staging logical operators +logical_operators = chronicle.list_integration_logical_operators( + "MyIntegration", + exclude_staging=True +) + +# Get logical operators with expanded details +logical_operators = chronicle.list_integration_logical_operators( + "MyIntegration", + expand="parameters" +) +``` + +Get details of a specific logical operator: + +```python +operator = chronicle.get_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1" +) +print(f"Display Name: {operator.get('displayName')}") +print(f"Script: {operator.get('script')}") +print(f"Enabled: {operator.get('enabled')}") + +# Get logical operator with expanded parameters +operator = chronicle.get_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + expand="parameters" +) +``` + +Create a new logical operator: + +```python +# Create a basic equality operator +new_operator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="Equals Operator", + script=""" +def evaluate(a, b): + return a == b +""", + script_timeout="60s", + enabled=True +) + +# Create a more complex conditional operator with parameters +new_operator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="Threshold Checker", + description="Checks if a value exceeds a threshold", + script=""" +def evaluate(value, threshold, inclusive=False): + if inclusive: + return value >= threshold + else: + return value > threshold +""", + script_timeout="30s", + enabled=True, + parameters=[ + { + "name": "value", + "type": "INTEGER", + "displayName": "Value to Check", + "mandatory": True + }, + { + "name": "threshold", + "type": "INTEGER", + "displayName": "Threshold Value", + "mandatory": True + }, + { + "name": "inclusive", + "type": "BOOLEAN", + "displayName": "Inclusive Comparison", + "mandatory": False, + "defaultValue": "false" + } + ] +) + +# Create a string matching operator +pattern_operator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="Pattern Matcher", + description="Matches strings against patterns", + script=""" +def evaluate(text, pattern, case_sensitive=True): + import re + flags = 0 if case_sensitive else re.IGNORECASE + return bool(re.search(pattern, text, flags)) +""", + script_timeout="60s", + enabled=True, + parameters=[ + { + "name": "text", + "type": "STRING", + "displayName": "Text to Match", + "mandatory": True + }, + { + "name": "pattern", + "type": "STRING", + "displayName": "Regex Pattern", + "mandatory": True + }, + { + "name": "case_sensitive", + "type": "BOOLEAN", + "displayName": "Case Sensitive", + "mandatory": False, + "defaultValue": "true" + } + ] +) +``` + +Update an existing logical operator: + +```python +# Update logical operator display name +updated_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + display_name="Updated Operator Name" +) + +# Update logical operator script +updated_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + script=""" +def evaluate(a, b): + # Updated logic with type checking + if type(a) != type(b): + return False + return a == b +""" +) + +# Update multiple fields including parameters +updated_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + display_name="Enhanced Operator", + description="Updated with better validation", + script=""" +def evaluate(value, min_value, max_value): + try: + return min_value <= value <= max_value + except Exception: + return False +""", + script_timeout="45s", + enabled=True, + parameters=[ + { + "name": "value", + "type": "INTEGER", + "displayName": "Value", + "mandatory": True + }, + { + "name": "min_value", + "type": "INTEGER", + "displayName": "Minimum Value", + "mandatory": True + }, + { + "name": "max_value", + "type": "INTEGER", + "displayName": "Maximum Value", + "mandatory": True + } + ] +) + +# Use custom update mask +updated_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + display_name="New Name", + description="New Description", + update_mask="displayName,description" +) +``` + +Delete a logical operator: + +```python +chronicle.delete_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1" +) +``` + +Execute a test run of a logical operator: + +```python +# Get the logical operator +operator = chronicle.get_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1" +) + +# Test the logical operator with sample data +test_result = chronicle.execute_integration_logical_operator_test( + integration_name="MyIntegration", + logical_operator=operator +) +print(f"Output Message: {test_result.get('outputMessage')}") +print(f"Debug Output: {test_result.get('debugOutputMessage')}") +print(f"Result Value: {test_result.get('resultValue')}") # True or False + +# You can also test a logical operator before creating it +test_operator = { + "displayName": "Test Equality Operator", + "script": """ +def evaluate(a, b): + return a == b +""", + "scriptTimeout": "30s", + "enabled": True +} + +test_result = chronicle.execute_integration_logical_operator_test( + integration_name="MyIntegration", + logical_operator=test_operator +) +``` + +Get a template for creating a logical operator: + +```python +# Get a boilerplate template for a new logical operator +template = chronicle.get_integration_logical_operator_template("MyIntegration") +print(f"Template Script: {template.get('script')}") +print(f"Template Display Name: {template.get('displayName')}") + +# Use the template as a starting point +new_operator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="My Custom Operator", + script=template.get('script'), # Customize this + script_timeout="60s", + enabled=True +) +``` + +Example workflow: Building conditional logic for integration workflows: + +```python +# 1. Get a template to start with +template = chronicle.get_integration_logical_operator_template("MyIntegration") + +# 2. Create a custom logical operator for severity checking +severity_operator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="Severity Level Check", + description="Checks if severity meets minimum threshold", + script=""" +def evaluate(severity, min_severity='MEDIUM'): + severity_levels = { + 'LOW': 1, + 'MEDIUM': 2, + 'HIGH': 3, + 'CRITICAL': 4 + } + + current_level = severity_levels.get(severity.upper(), 0) + min_level = severity_levels.get(min_severity.upper(), 0) + + return current_level >= min_level +""", + script_timeout="30s", + enabled=False, # Start disabled for testing + parameters=[ + { + "name": "severity", + "type": "STRING", + "displayName": "Event Severity", + "mandatory": True + }, + { + "name": "min_severity", + "type": "STRING", + "displayName": "Minimum Severity", + "mandatory": False, + "defaultValue": "MEDIUM" + } + ] +) + +# 3. Test the operator before enabling +test_result = chronicle.execute_integration_logical_operator_test( + integration_name="MyIntegration", + logical_operator=severity_operator +) + +# 4. If test is successful, enable the operator +if test_result.get('resultValue') is not None: + operator_id = severity_operator.get('name').split('/')[-1] + enabled_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id=operator_id, + enabled=True + ) + print(f"Operator enabled: {enabled_operator.get('name')}") +else: + print(f"Test failed: {test_result.get('debugOutputMessage')}") + +# 5. Create additional operators for workflow automation +# IP address validation operator +ip_validator = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="IP Address Validator", + description="Validates if a string is a valid IP address", + script=""" +def evaluate(ip_string): + import ipaddress + try: + ipaddress.ip_address(ip_string) + return True + except ValueError: + return False +""", + script_timeout="30s", + enabled=True +) + +# Time range checker +time_checker = chronicle.create_integration_logical_operator( + integration_name="MyIntegration", + display_name="Business Hours Checker", + description="Checks if timestamp falls within business hours", + script=""" +def evaluate(timestamp, start_hour=9, end_hour=17): + from datetime import datetime + + dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + hour = dt.hour + + return start_hour <= hour < end_hour +""", + script_timeout="30s", + enabled=True, + parameters=[ + { + "name": "timestamp", + "type": "STRING", + "displayName": "Timestamp", + "mandatory": True + }, + { + "name": "start_hour", + "type": "INTEGER", + "displayName": "Business Day Start Hour", + "mandatory": False, + "defaultValue": "9" + }, + { + "name": "end_hour", + "type": "INTEGER", + "displayName": "Business Day End Hour", + "mandatory": False, + "defaultValue": "17" + } + ] +) + +# 6. List all logical operators for the integration +all_operators = chronicle.list_integration_logical_operators( + integration_name="MyIntegration", + as_list=True +) +print(f"Total logical operators: {len(all_operators)}") +for op in all_operators: + print(f" - {op.get('displayName')} (Enabled: {op.get('enabled')})") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index d5031246..1e2b1824 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -8,7 +8,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ - **v1:** 17 endpoints implemented - **v1beta:** 88 endpoints implemented -- **v1alpha:** 188 endpoints implemented +- **v1alpha:** 199 endpoints implemented ## Endpoint Mapping @@ -415,6 +415,17 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | integrations.transformers.get | v1alpha | chronicle.integration.transformers.get_integration_transformer | | | integrations.transformers.list | v1alpha | chronicle.integration.transformers.list_integration_transformers | | | integrations.transformers.patch | v1alpha | chronicle.integration.transformers.update_integration_transformer | | +| integrations.transformers.revisions.create | v1alpha | chronicle.integration.transformer_revisions.create_integration_transformer_revision | | +| integrations.transformers.revisions.delete | v1alpha | chronicle.integration.transformer_revisions.delete_integration_transformer_revision | | +| integrations.transformers.revisions.list | v1alpha | chronicle.integration.transformer_revisions.list_integration_transformer_revisions | | +| integrations.transformers.revisions.rollback | v1alpha | chronicle.integration.transformer_revisions.rollback_integration_transformer_revision | | +| integrations.logicalOperators.create | v1alpha | chronicle.integration.logical_operators.create_integration_logical_operator | | +| integrations.logicalOperators.delete | v1alpha | chronicle.integration.logical_operators.delete_integration_logical_operator | | +| integrations.logicalOperators.executeTest | v1alpha | chronicle.integration.logical_operators.execute_integration_logical_operator_test | | +| integrations.logicalOperators.fetchTemplate | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator_template | | +| integrations.logicalOperators.get | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator | | +| integrations.logicalOperators.list | v1alpha | chronicle.integration.logical_operators.list_integration_logical_operators | | +| integrations.logicalOperators.patch | v1alpha | chronicle.integration.logical_operators.update_integration_logical_operator | | | integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | | | integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index cc6563bf..0bf5b5de 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -348,6 +348,21 @@ execute_integration_transformer_test, get_integration_transformer_template, ) +from secops.chronicle.integration.transformer_revisions import ( + list_integration_transformer_revisions, + delete_integration_transformer_revision, + create_integration_transformer_revision, + rollback_integration_transformer_revision, +) +from secops.chronicle.integration.logical_operators import ( + list_integration_logical_operators, + get_integration_logical_operator, + delete_integration_logical_operator, + create_integration_logical_operator, + update_integration_logical_operator, + execute_integration_logical_operator_test, + get_integration_logical_operator_template, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -651,6 +666,19 @@ "update_integration_transformer", "execute_integration_transformer_test", "get_integration_transformer_template", + # Integration Transformer Revisions + "list_integration_transformer_revisions", + "delete_integration_transformer_revision", + "create_integration_transformer_revision", + "rollback_integration_transformer_revision", + # Integration Logical Operators + "list_integration_logical_operators", + "get_integration_logical_operator", + "delete_integration_logical_operator", + "create_integration_logical_operator", + "update_integration_logical_operator", + "execute_integration_logical_operator_test", + "get_integration_logical_operator_template", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index b38d67a8..3921ad2a 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -276,6 +276,21 @@ list_integration_transformers as _list_integration_transformers, update_integration_transformer as _update_integration_transformer, ) +from secops.chronicle.integration.transformer_revisions import ( + create_integration_transformer_revision as _create_integration_transformer_revision, + delete_integration_transformer_revision as _delete_integration_transformer_revision, + list_integration_transformer_revisions as _list_integration_transformer_revisions, + rollback_integration_transformer_revision as _rollback_integration_transformer_revision, +) +from secops.chronicle.integration.logical_operators import ( + create_integration_logical_operator as _create_integration_logical_operator, + delete_integration_logical_operator as _delete_integration_logical_operator, + execute_integration_logical_operator_test as _execute_integration_logical_operator_test, + get_integration_logical_operator as _get_integration_logical_operator, + get_integration_logical_operator_template as _get_integration_logical_operator_template, + list_integration_logical_operators as _list_integration_logical_operators, + update_integration_logical_operator as _update_integration_logical_operator, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -5401,6 +5416,469 @@ def get_integration_transformer_template( api_version=api_version, ) + # -- Integration Transformer Revisions methods -- + + def list_integration_transformer_revisions( + self, + integration_name: str, + transformer_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific transformer. + + Use this method to view the revision history of a transformer, + enabling you to track changes and potentially rollback to previous + versions. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to list revisions for. + page_size: Maximum number of revisions to return. Defaults to + 100, maximum is 200. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1ALPHA. + as_list: If True, automatically fetches all pages and returns + a list of revisions. If False, returns dict with revisions + and nextPageToken. + + Returns: + If as_list is True: List of transformer revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_transformer_revisions( + self, + integration_name, + transformer_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_transformer_revision( + self, + integration_name: str, + transformer_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> None: + """Delete a specific transformer revision. + + Use this method to remove obsolete or incorrect revisions from + a transformer's history. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_transformer_revision( + self, + integration_name, + transformer_id, + revision_id, + api_version=api_version, + ) + + def create_integration_transformer_revision( + self, + integration_name: str, + transformer_id: str, + transformer: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Create a new revision for a transformer. + + Use this method to create a snapshot of the transformer's current + state before making changes, enabling you to rollback if needed. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to create a revision for. + transformer: Dict containing the TransformerDefinition to save + as a revision. + comment: Optional comment describing the revision or changes. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the newly created TransformerRevision resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_transformer_revision( + self, + integration_name, + transformer_id, + transformer, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_transformer_revision( + self, + integration_name: str, + transformer_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Rollback a transformer to a previous revision. + + Use this method to restore a transformer to a previous working + state by rolling back to a specific revision. + + Args: + integration_name: Name of the integration the transformer + belongs to. + transformer_id: ID of the transformer to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the updated TransformerDefinition resource. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_transformer_revision( + self, + integration_name, + transformer_id, + revision_id, + api_version=api_version, + ) + + # -- Integration Logical Operators methods -- + + def list_integration_logical_operators( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all logical operator definitions for a specific integration. + + Use this method to browse the available logical operators that can + be used for conditional logic in your integration workflows. + + Args: + integration_name: Name of the integration to list logical + operators for. + page_size: Maximum number of logical operators to return. + Defaults to 100, maximum is 200. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter logical operators. + order_by: Field to sort the logical operators by. + exclude_staging: Whether to exclude staging logical operators + from the response. By default, staging operators are included. + expand: Expand the response with the full logical operator + details. + api_version: API version to use for the request. Default is + V1ALPHA. + as_list: If True, automatically fetches all pages and returns + a list. If False, returns dict with list and nextPageToken. + + Returns: + If as_list is True: List of logical operators. + If as_list is False: Dict with logicalOperators list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_logical_operators( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + exclude_staging=exclude_staging, + expand=expand, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_logical_operator( + self, + integration_name: str, + logical_operator_id: str, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Get a single logical operator definition for a specific integration. + + Use this method to retrieve the Python script, input parameters, + and evaluation logic for a specific logical operator within an + integration. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to retrieve. + expand: Expand the response with the full logical operator + details. Optional. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing details of the specified LogicalOperator + definition. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_logical_operator( + self, + integration_name, + logical_operator_id, + expand=expand, + api_version=api_version, + ) + + def delete_integration_logical_operator( + self, + integration_name: str, + logical_operator_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> None: + """Delete a custom logical operator definition from a given integration. + + Use this method to permanently remove an obsolete logical operator + from an integration. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to delete. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_logical_operator( + self, + integration_name, + logical_operator_id, + api_version=api_version, + ) + + def create_integration_logical_operator( + self, + integration_name: str, + display_name: str, + script: str, + script_timeout: str, + enabled: bool, + description: str | None = None, + parameters: list[dict[str, Any]] | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Create a new logical operator definition for a given integration. + + Use this method to define a logical operator, specifying its + functional Python script and necessary input parameters for + conditional evaluations. + + Args: + integration_name: Name of the integration to create the + logical operator for. + display_name: Logical operator's display name. Maximum 150 + characters. Required. + script: Logical operator's Python script. Required. + script_timeout: Timeout in seconds for a single script run. + Default is 60. Required. + enabled: Whether the logical operator is enabled or disabled. + Required. + description: Logical operator's description. Maximum 2050 + characters. Optional. + parameters: List of logical operator parameter dicts. Optional. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the newly created LogicalOperator resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_logical_operator( + self, + integration_name, + display_name, + script, + script_timeout, + enabled, + description=description, + parameters=parameters, + api_version=api_version, + ) + + def update_integration_logical_operator( + self, + integration_name: str, + logical_operator_id: str, + display_name: str | None = None, + script: str | None = None, + script_timeout: str | None = None, + enabled: bool | None = None, + description: str | None = None, + parameters: list[dict[str, Any]] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Update an existing logical operator definition for a given + integration. + + Use this method to modify a logical operator's Python script, + adjust its description, or refine its parameter definitions. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to update. + display_name: Logical operator's display name. Maximum 150 + characters. + script: Logical operator's Python script. + script_timeout: Timeout in seconds for a single script run. + enabled: Whether the logical operator is enabled or disabled. + description: Logical operator's description. Maximum 2050 + characters. + parameters: List of logical operator parameter dicts. When + updating existing parameters, id must be provided in each + parameter. + update_mask: Comma-separated list of fields to update. If + omitted, the mask is auto-generated from whichever fields + are provided. Example: "displayName,script". + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the updated LogicalOperator resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_logical_operator( + self, + integration_name, + logical_operator_id, + display_name=display_name, + script=script, + script_timeout=script_timeout, + enabled=enabled, + description=description, + parameters=parameters, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_logical_operator_test( + self, + integration_name: str, + logical_operator: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Execute a test run of a logical operator's Python script. + + Use this method to verify logical operator evaluation logic and + ensure conditions are being assessed correctly before saving or + deploying the operator. The full logical operator object is + required as the test can be run without saving the operator first. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator: Dict containing the LogicalOperator + definition to test. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the test execution results with the following + fields: + - outputMessage: Human-readable output message set by the + script. + - debugOutputMessage: The script debug output. + - resultValue: The script result value (True/False). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_logical_operator_test( + self, + integration_name, + logical_operator, + api_version=api_version, + ) + + def get_integration_logical_operator_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new logical operator. + + Use this method to jumpstart the development of a custom + conditional logic by providing boilerplate code. + + Args: + integration_name: Name of the integration to fetch the template + for. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the LogicalOperator template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_logical_operator_template( + self, + integration_name, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/logical_operators.py b/src/secops/chronicle/integration/logical_operators.py new file mode 100644 index 00000000..c8c27204 --- /dev/null +++ b/src/secops/chronicle/integration/logical_operators.py @@ -0,0 +1,401 @@ +# Copyright 2025 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. +# +"""Integration logical operators functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + IntegrationLogicalOperatorParameter +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_logical_operators( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all logical operator definitions for a specific integration. + + Use this method to discover the custom logic operators available for use + within playbook decision steps. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list logical operators + for. + page_size: Maximum number of logical operators to return. Defaults + to 100, maximum is 200. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter logical operators. + order_by: Field to sort the logical operators by. + exclude_staging: Whether to exclude staging logical operators from + the response. By default, staging logical operators are included. + expand: Expand the response with the full logical operator details. + api_version: API version to use for the request. Default is V1ALPHA. + as_list: If True, return a list of logical operators instead of a + dict with logical operators list and nextPageToken. + + Returns: + If as_list is True: List of logical operators. + If as_list is False: Dict with logical operators list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + "excludeStaging": exclude_staging, + "expand": expand, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators" + ), + items_key="logicalOperators", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_logical_operator( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + expand: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Get a single logical operator definition for a specific integration. + + Use this method to retrieve the Python script, evaluation parameters, + and description for a custom logical operator. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to retrieve. + expand: Expand the response with the full logical operator details. + Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing details of the specified IntegrationLogicalOperator. + + Raises: + APIError: If the API request fails. + """ + params = {} + if expand is not None: + params["expand"] = expand + + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}" + ), + api_version=api_version, + params=params if params else None, + ) + + +def delete_integration_logical_operator( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> None: + """Delete a specific custom logical operator from a given integration. + + Only custom logical operators can be deleted; predefined built-in + operators are immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to delete. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}" + ), + api_version=api_version, + ) + + +def create_integration_logical_operator( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + script_timeout: str, + enabled: bool, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationLogicalOperatorParameter] | None + ) = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Create a new custom logical operator for a given integration. + + Each operator must have a unique display name and a functional Python + script that returns a boolean result. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the logical + operator for. + display_name: Logical operator's display name. Maximum 150 + characters. Required. + script: Logical operator's Python script. Required. + script_timeout: Timeout in seconds for a single script run. Default + is 60. Required. + enabled: Whether the logical operator is enabled or disabled. + Required. + description: Logical operator's description. Maximum 2050 characters. + Optional. + parameters: List of IntegrationLogicalOperatorParameter instances or + dicts. Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the newly created IntegrationLogicalOperator resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() + if isinstance(p, IntegrationLogicalOperatorParameter) else p + for p in parameters] + if parameters is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "scriptTimeout": script_timeout, + "enabled": enabled, + "description": description, + "parameters": resolved_parameters, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_logical_operator( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + display_name: str | None = None, + script: str | None = None, + script_timeout: str | None = None, + enabled: bool | None = None, + description: str | None = None, + parameters: ( + list[dict[str, Any] | IntegrationLogicalOperatorParameter] | None + ) = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Update an existing custom logical operator for a given integration. + + Use this method to modify the logical operator script, refine parameter + descriptions, or adjust the timeout for a logical operator. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to update. + display_name: Logical operator's display name. Maximum 150 characters. + script: Logical operator's Python script. + script_timeout: Timeout in seconds for a single script run. + enabled: Whether the logical operator is enabled or disabled. + description: Logical operator's description. Maximum 2050 characters. + parameters: List of IntegrationLogicalOperatorParameter instances or + dicts. When updating existing parameters, id must be provided + in each IntegrationLogicalOperatorParameter. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the updated IntegrationLogicalOperator resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [p.to_dict() + if isinstance(p, IntegrationLogicalOperatorParameter) else p + for p in parameters] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("scriptTimeout", "scriptTimeout", script_timeout), + ("enabled", "enabled", enabled), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_logical_operator_test( + client: "ChronicleClient", + integration_name: str, + logical_operator: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Execute a test run of a logical operator's evaluation script. + + Use this method to verify decision logic and ensure it correctly handles + various input data before deployment in a playbook. The full logical + operator object is required as the test can be run without saving the + logical operator first. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator: Dict containing the IntegrationLogicalOperator to + test. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the test execution results with the following fields: + - outputMessage: Human-readable output message set by the script. + - debugOutputMessage: The script debug output. + - resultValue: The script result value. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators:executeTest" + ), + api_version=api_version, + json={"logicalOperator": logical_operator}, + ) + + +def get_integration_logical_operator_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a new logical operator. + + Use this method to rapidly initialize the development of a new logical + operator. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the IntegrationLogicalOperator template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/transformer_revisions.py b/src/secops/chronicle/integration/transformer_revisions.py new file mode 100644 index 00000000..397d3fbf --- /dev/null +++ b/src/secops/chronicle/integration/transformer_revisions.py @@ -0,0 +1,202 @@ +# Copyright 2025 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. +# +"""Integration transformer revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_transformer_revisions( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration transformer. + + Use this method to browse through the version history of a custom + transformer definition. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1ALPHA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_transformer_revision( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> None: + """Delete a specific revision for a given integration transformer. + + Permanently removes the versioned snapshot from the transformer's history. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_transformer_revision( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + transformer: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration transformer. + + Use this method to save the current state of a transformer definition. + Revisions can only be created for custom transformers. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to create a revision for. + transformer: Dict containing the TransformerDefinition to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the newly created TransformerRevision resource. + + Raises: + APIError: If the API request fails. + """ + body = {"transformer": transformer} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_transformer_revision( + client: "ChronicleClient", + integration_name: str, + transformer_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Roll back the current transformer definition to + a previously saved revision. + + This updates the active transformer definition with the configuration + stored in the specified revision. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the transformer belongs to. + transformer_id: ID of the transformer to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the TransformerRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"transformers/{transformer_id}/revisions/{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index f26e9668..85cec48c 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -684,6 +684,57 @@ def to_dict(self) -> dict: return data +class LogicalOperatorType(str, Enum): + """Logical operator types for Chronicle SOAR + integration logical operators.""" + + UNSPECIFIED = "LOGICAL_OPERATOR_TYPE_UNSPECIFIED" + BUILT_IN = "BUILT_IN" + CUSTOM = "CUSTOM" + + +@dataclass +class IntegrationLogicalOperatorParameter: + """A parameter definition for a Chronicle SOAR logical operator. + + Attributes: + display_name: The parameter's display name. May contain letters, + numbers, and underscores. Maximum 150 characters. + mandatory: Whether the parameter is mandatory for configuring a + logical operator instance. + id: The parameter's id. Server-generated on creation; must be + provided when updating an existing parameter. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + order: The parameter's order in the parameters list. + description: The parameter's description. Maximum 2050 characters. + """ + + display_name: str + mandatory: bool + id: str | None = None + default_value: str | None = None + order: int | None = None + description: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "mandatory": self.mandatory, + } + if self.id is not None: + data["id"] = self.id + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.order is not None: + data["order"] = self.order + if self.description is not None: + data["description"] = self.description + return data + + + @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index 6541df10..5d283122 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -33,6 +33,8 @@ manager_revisions, integration_instances, transformers, + transformer_revisions, + logical_operators, ) @@ -49,6 +51,8 @@ def setup_integrations_command(subparsers): integration.setup_integrations_command(lvl1) integration_instances.setup_integration_instances_command(lvl1) transformers.setup_transformers_command(lvl1) + transformer_revisions.setup_transformer_revisions_command(lvl1) + logical_operators.setup_logical_operators_command(lvl1) actions.setup_actions_command(lvl1) action_revisions.setup_action_revisions_command(lvl1) connectors.setup_connectors_command(lvl1) diff --git a/src/secops/cli/commands/integration/logical_operators.py b/src/secops/cli/commands/integration/logical_operators.py new file mode 100644 index 00000000..502eb35d --- /dev/null +++ b/src/secops/cli/commands/integration/logical_operators.py @@ -0,0 +1,399 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration logical operators commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_logical_operators_command(subparsers): + """Setup integration logical operators command""" + operators_parser = subparsers.add_parser( + "logical-operators", + help="Manage integration logical operators", + ) + lvl1 = operators_parser.add_subparsers( + dest="logical_operators_command", + help="Integration logical operators command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", + help="List integration logical operators", + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing logical operators", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing logical operators", + dest="order_by", + ) + list_parser.add_argument( + "--exclude-staging", + action="store_true", + help="Exclude staging logical operators from the response", + dest="exclude_staging", + ) + list_parser.add_argument( + "--expand", + type=str, + help="Expand the response with full logical operator details", + dest="expand", + ) + list_parser.set_defaults(func=handle_logical_operators_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", + help="Get integration logical operator details", + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator to get", + dest="logical_operator_id", + required=True, + ) + get_parser.add_argument( + "--expand", + type=str, + help="Expand the response with full logical operator details", + dest="expand", + ) + get_parser.set_defaults(func=handle_logical_operators_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration logical operator", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator to delete", + dest="logical_operator_id", + required=True, + ) + delete_parser.set_defaults(func=handle_logical_operators_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", + help="Create a new integration logical operator", + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the logical operator", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--script", + type=str, + help="Python script for the logical operator", + dest="script", + required=True, + ) + create_parser.add_argument( + "--script-timeout", + type=str, + help="Timeout for script execution (e.g., '60s')", + dest="script_timeout", + required=True, + ) + create_parser.add_argument( + "--enabled", + action="store_true", + help="Enable the logical operator (default: disabled)", + dest="enabled", + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the logical operator", + dest="description", + ) + create_parser.set_defaults(func=handle_logical_operators_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", + help="Update an integration logical operator", + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator to update", + dest="logical_operator_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the logical operator", + dest="display_name", + ) + update_parser.add_argument( + "--script", + type=str, + help="New Python script for the logical operator", + dest="script", + ) + update_parser.add_argument( + "--script-timeout", + type=str, + help="New timeout for script execution", + dest="script_timeout", + ) + update_parser.add_argument( + "--enabled", + type=lambda x: x.lower() == "true", + help="Enable or disable the logical operator (true/false)", + dest="enabled", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the logical operator", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_logical_operators_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", + help="Execute an integration logical operator test", + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator to test", + dest="logical_operator_id", + required=True, + ) + test_parser.set_defaults(func=handle_logical_operators_test_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get logical operator template", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_logical_operators_template_command) + + +def handle_logical_operators_list_command(args, chronicle): + """Handle integration logical operators list command""" + try: + out = chronicle.list_integration_logical_operators( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + exclude_staging=args.exclude_staging, + expand=args.expand, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error listing integration logical operators: {e}", + file=sys.stderr + ) + sys.exit(1) + + +def handle_logical_operators_get_command(args, chronicle): + """Handle integration logical operator get command""" + try: + out = chronicle.get_integration_logical_operator( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + expand=args.expand, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting integration logical operator: {e}", + file=sys.stderr + ) + sys.exit(1) + + +def handle_logical_operators_delete_command(args, chronicle): + """Handle integration logical operator delete command""" + try: + chronicle.delete_integration_logical_operator( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + ) + print( + f"Logical operator {args.logical_operator_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error deleting integration logical operator: {e}", + file=sys.stderr + ) + sys.exit(1) + + +def handle_logical_operators_create_command(args, chronicle): + """Handle integration logical operator create command""" + try: + out = chronicle.create_integration_logical_operator( + integration_name=args.integration_name, + display_name=args.display_name, + script=args.script, + script_timeout=args.script_timeout, + enabled=args.enabled, + description=args.description, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating integration logical operator: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_logical_operators_update_command(args, chronicle): + """Handle integration logical operator update command""" + try: + out = chronicle.update_integration_logical_operator( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + display_name=args.display_name, + script=args.script, + script_timeout=args.script_timeout, + enabled=args.enabled, + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error updating integration logical operator: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_logical_operators_test_command(args, chronicle): + """Handle integration logical operator test command""" + try: + # Get the logical operator first + logical_operator = chronicle.get_integration_logical_operator( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + ) + + out = chronicle.execute_integration_logical_operator_test( + integration_name=args.integration_name, + logical_operator=logical_operator, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error testing integration logical operator: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_logical_operators_template_command(args, chronicle): + """Handle integration logical operator template command""" + try: + out = chronicle.get_integration_logical_operator_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error getting logical operator template: {e}", + file=sys.stderr, + ) + sys.exit(1) + diff --git a/src/secops/cli/commands/integration/transformer_revisions.py b/src/secops/cli/commands/integration/transformer_revisions.py new file mode 100644 index 00000000..3406f60a --- /dev/null +++ b/src/secops/cli/commands/integration/transformer_revisions.py @@ -0,0 +1,239 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration transformer revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_transformer_revisions_command(subparsers): + """Setup integration transformer revisions command""" + revisions_parser = subparsers.add_parser( + "transformer-revisions", + help="Manage integration transformer revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="transformer_revisions_command", + help="Integration transformer revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", + help="List integration transformer revisions", + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer", + dest="transformer_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_transformer_revisions_list_command, + ) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration transformer revision", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer", + dest="transformer_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults( + func=handle_transformer_revisions_delete_command, + ) + + # create command + create_parser = lvl1.add_parser( + "create", + help="Create a new integration transformer revision", + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer", + dest="transformer_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults( + func=handle_transformer_revisions_create_command, + ) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", + help="Rollback transformer to a previous revision", + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--transformer-id", + type=str, + help="ID of the transformer", + dest="transformer_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults( + func=handle_transformer_revisions_rollback_command, + ) + + +def handle_transformer_revisions_list_command(args, chronicle): + """Handle integration transformer revisions list command""" + try: + out = chronicle.list_integration_transformer_revisions( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing transformer revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_transformer_revisions_delete_command(args, chronicle): + """Handle integration transformer revision delete command""" + try: + chronicle.delete_integration_transformer_revision( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + revision_id=args.revision_id, + ) + print( + f"Transformer revision {args.revision_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error deleting transformer revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_transformer_revisions_create_command(args, chronicle): + """Handle integration transformer revision create command""" + try: + # Get the current transformer to create a revision + transformer = chronicle.get_integration_transformer( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + ) + out = chronicle.create_integration_transformer_revision( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + transformer=transformer, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating transformer revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_transformer_revisions_rollback_command(args, chronicle): + """Handle integration transformer revision rollback command""" + try: + out = chronicle.rollback_integration_transformer_revision( + integration_name=args.integration_name, + transformer_id=args.transformer_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error rolling back transformer revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + diff --git a/tests/chronicle/integration/test_logical_operators.py b/tests/chronicle/integration/test_logical_operators.py new file mode 100644 index 00000000..df495750 --- /dev/null +++ b/tests/chronicle/integration/test_logical_operators.py @@ -0,0 +1,547 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration logical operators functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.logical_operators import ( + list_integration_logical_operators, + get_integration_logical_operator, + delete_integration_logical_operator, + create_integration_logical_operator, + update_integration_logical_operator, + execute_integration_logical_operator_test, + get_integration_logical_operator_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1ALPHA, + ) + + +# -- list_integration_logical_operators tests -- + + +def test_list_integration_logical_operators_success(chronicle_client): + """Test list_integration_logical_operators delegates to paginated request.""" + expected = { + "logicalOperators": [{"name": "lo1"}, {"name": "lo2"}], + "nextPageToken": "token", + } + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="My Integration", + ): + result = list_integration_logical_operators( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/My Integration/logicalOperators", + items_key="logicalOperators", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_logical_operators_default_args(chronicle_client): + """Test list_integration_logical_operators with default args.""" + expected = {"logicalOperators": []} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_logical_operators( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/logicalOperators", + items_key="logicalOperators", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_integration_logical_operators_with_filter_order_expand( + chronicle_client, +): + """Test list passes filter/orderBy/excludeStaging/expand in extra_params.""" + expected = {"logicalOperators": [{"name": "lo1"}]} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_logical_operators( + chronicle_client, + integration_name="test-integration", + filter_string='displayName = "My Operator"', + order_by="displayName", + exclude_staging=True, + expand="parameters", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/logicalOperators", + items_key="logicalOperators", + page_size=None, + page_token=None, + extra_params={ + "filter": 'displayName = "My Operator"', + "orderBy": "displayName", + "excludeStaging": True, + "expand": "parameters", + }, + as_list=False, + ) + + +# -- get_integration_logical_operator tests -- + + +def test_get_integration_logical_operator_success(chronicle_client): + """Test get_integration_logical_operator delegates to chronicle_request.""" + expected = {"name": "lo1", "displayName": "My Operator"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="test-integration", + ): + result = get_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/logicalOperators/lo1", + api_version=APIVersion.V1ALPHA, + params=None, + ) + + +def test_get_integration_logical_operator_with_expand(chronicle_client): + """Test get_integration_logical_operator with expand parameter.""" + expected = {"name": "lo1"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + expand="parameters", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/logicalOperators/lo1", + api_version=APIVersion.V1ALPHA, + params={"expand": "parameters"}, + ) + + +# -- delete_integration_logical_operator tests -- + + +def test_delete_integration_logical_operator_success(chronicle_client): + """Test delete_integration_logical_operator delegates to chronicle_request.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=None, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="test-integration", + ): + delete_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/logicalOperators/lo1", + api_version=APIVersion.V1ALPHA, + ) + + +# -- create_integration_logical_operator tests -- + + +def test_create_integration_logical_operator_minimal(chronicle_client): + """Test create_integration_logical_operator with minimal required fields.""" + expected = {"name": "lo1", "displayName": "New Operator"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="test-integration", + ): + result = create_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + display_name="New Operator", + script="def evaluate(a, b): return a == b", + script_timeout="60s", + enabled=True, + ) + + assert result == expected + + mock_request.assert_called_once() + call_kwargs = mock_request.call_args[1] + assert call_kwargs["method"] == "POST" + assert ( + call_kwargs["endpoint_path"] + == "integrations/test-integration/logicalOperators" + ) + assert call_kwargs["api_version"] == APIVersion.V1ALPHA + assert call_kwargs["json"]["displayName"] == "New Operator" + assert call_kwargs["json"]["script"] == "def evaluate(a, b): return a == b" + assert call_kwargs["json"]["scriptTimeout"] == "60s" + assert call_kwargs["json"]["enabled"] is True + + +def test_create_integration_logical_operator_with_all_fields(chronicle_client): + """Test create_integration_logical_operator with all optional fields.""" + expected = {"name": "lo1"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + display_name="Full Operator", + script="def evaluate(a, b): return a > b", + script_timeout="120s", + enabled=False, + description="Test logical operator description", + parameters=[{"name": "param1", "type": "STRING"}], + ) + + assert result == expected + + call_kwargs = mock_request.call_args[1] + body = call_kwargs["json"] + assert body["displayName"] == "Full Operator" + assert body["description"] == "Test logical operator description" + assert body["parameters"] == [{"name": "param1", "type": "STRING"}] + + +# -- update_integration_logical_operator tests -- + + +def test_update_integration_logical_operator_display_name(chronicle_client): + """Test update_integration_logical_operator updates display name.""" + expected = {"name": "lo1", "displayName": "Updated Name"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.build_patch_body", + return_value=({"displayName": "Updated Name"}, {"updateMask": "displayName"}), + ) as mock_build: + result = update_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + display_name="Updated Name", + ) + + assert result == expected + + mock_build.assert_called_once() + mock_request.assert_called_once() + call_kwargs = mock_request.call_args[1] + assert call_kwargs["method"] == "PATCH" + assert ( + call_kwargs["endpoint_path"] + == "integrations/test-integration/logicalOperators/lo1" + ) + + +def test_update_integration_logical_operator_with_update_mask(chronicle_client): + """Test update_integration_logical_operator with explicit update mask.""" + expected = {"name": "lo1"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.build_patch_body", + return_value=( + {"displayName": "New Name", "enabled": True}, + {"updateMask": "displayName,enabled"}, + ), + ): + result = update_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + display_name="New Name", + enabled=True, + update_mask="displayName,enabled", + ) + + assert result == expected + + +def test_update_integration_logical_operator_all_fields(chronicle_client): + """Test update_integration_logical_operator with all fields.""" + expected = {"name": "lo1"} + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.build_patch_body", + return_value=( + { + "displayName": "Updated", + "script": "new script", + "scriptTimeout": "90s", + "enabled": False, + "description": "Updated description", + "parameters": [{"name": "p1"}], + }, + {"updateMask": "displayName,script,scriptTimeout,enabled,description"}, + ), + ): + result = update_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + display_name="Updated", + script="new script", + script_timeout="90s", + enabled=False, + description="Updated description", + parameters=[{"name": "p1"}], + ) + + assert result == expected + + +# -- execute_integration_logical_operator_test tests -- + + +def test_execute_integration_logical_operator_test_success(chronicle_client): + """Test execute_integration_logical_operator_test delegates to chronicle_request.""" + logical_operator = { + "displayName": "Test Operator", + "script": "def evaluate(a, b): return a == b", + } + expected = { + "outputMessage": "Success", + "debugOutputMessage": "Debug info", + "resultValue": True, + } + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="test-integration", + ): + result = execute_integration_logical_operator_test( + chronicle_client, + integration_name="test-integration", + logical_operator=logical_operator, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/logicalOperators:executeTest", + api_version=APIVersion.V1ALPHA, + json={"logicalOperator": logical_operator}, + ) + + +# -- get_integration_logical_operator_template tests -- + + +def test_get_integration_logical_operator_template_success(chronicle_client): + """Test get_integration_logical_operator_template delegates to chronicle_request.""" + expected = { + "script": "def evaluate(a, b):\n # Template code\n return True", + "displayName": "Template Operator", + } + + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operators.format_resource_id", + return_value="test-integration", + ): + result = get_integration_logical_operator_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path=( + "integrations/test-integration/logicalOperators:fetchTemplate" + ), + api_version=APIVersion.V1ALPHA, + ) + + +# -- Error handling tests -- + + +def test_list_integration_logical_operators_api_error(chronicle_client): + """Test list_integration_logical_operators handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_paginated_request", + side_effect=APIError("API Error"), + ): + with pytest.raises(APIError, match="API Error"): + list_integration_logical_operators( + chronicle_client, + integration_name="test-integration", + ) + + +def test_get_integration_logical_operator_api_error(chronicle_client): + """Test get_integration_logical_operator handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + side_effect=APIError("Not found"), + ): + with pytest.raises(APIError, match="Not found"): + get_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="nonexistent", + ) + + +def test_create_integration_logical_operator_api_error(chronicle_client): + """Test create_integration_logical_operator handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + side_effect=APIError("Creation failed"), + ): + with pytest.raises(APIError, match="Creation failed"): + create_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + display_name="New Operator", + script="def evaluate(a, b): return a == b", + script_timeout="60s", + enabled=True, + ) + + +def test_update_integration_logical_operator_api_error(chronicle_client): + """Test update_integration_logical_operator handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + side_effect=APIError("Update failed"), + ), patch( + "secops.chronicle.integration.logical_operators.build_patch_body", + return_value=({"displayName": "Updated"}, {"updateMask": "displayName"}), + ): + with pytest.raises(APIError, match="Update failed"): + update_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + display_name="Updated", + ) + + +def test_delete_integration_logical_operator_api_error(chronicle_client): + """Test delete_integration_logical_operator handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operators.chronicle_request", + side_effect=APIError("Delete failed"), + ): + with pytest.raises(APIError, match="Delete failed"): + delete_integration_logical_operator( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + ) + diff --git a/tests/chronicle/integration/test_transformer_revisions.py b/tests/chronicle/integration/test_transformer_revisions.py new file mode 100644 index 00000000..8107891e --- /dev/null +++ b/tests/chronicle/integration/test_transformer_revisions.py @@ -0,0 +1,366 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration transformer revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.transformer_revisions import ( + list_integration_transformer_revisions, + delete_integration_transformer_revision, + create_integration_transformer_revision, + rollback_integration_transformer_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1ALPHA, + ) + + +# -- list_integration_transformer_revisions tests -- + + +def test_list_integration_transformer_revisions_success(chronicle_client): + """Test list_integration_transformer_revisions delegates to paginated request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "token", + } + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.transformer_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_transformer_revisions( + chronicle_client, + integration_name="My Integration", + transformer_id="t1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/My Integration/transformers/t1/revisions", + items_key="revisions", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_transformer_revisions_default_args(chronicle_client): + """Test list_integration_transformer_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_transformer_revisions( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/transformers/t1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_integration_transformer_revisions_with_filter_order( + chronicle_client, +): + """Test list passes filter/orderBy in extra_params.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_transformer_revisions( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + filter_string='version = "1.0"', + order_by="createTime desc", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/transformers/t1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={ + "filter": 'version = "1.0"', + "orderBy": "createTime desc", + }, + as_list=False, + ) + + +def test_list_integration_transformer_revisions_as_list(chronicle_client): + """Test list_integration_transformer_revisions with as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_transformer_revisions( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + as_list=True, + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/transformers/t1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={}, + as_list=True, + ) + + +# -- delete_integration_transformer_revision tests -- + + +def test_delete_integration_transformer_revision_success(chronicle_client): + """Test delete_integration_transformer_revision delegates to chronicle_request.""" + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + return_value=None, + ) as mock_request, patch( + "secops.chronicle.integration.transformer_revisions.format_resource_id", + return_value="test-integration", + ): + delete_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + revision_id="rev1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path=( + "integrations/test-integration/transformers/t1/revisions/rev1" + ), + api_version=APIVersion.V1ALPHA, + ) + + +# -- create_integration_transformer_revision tests -- + + +def test_create_integration_transformer_revision_minimal(chronicle_client): + """Test create_integration_transformer_revision with minimal fields.""" + transformer = { + "displayName": "Test Transformer", + "script": "def transform(data): return data", + } + expected = {"name": "rev1", "comment": ""} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformer_revisions.format_resource_id", + return_value="test-integration", + ): + result = create_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + transformer=transformer, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/transformers/t1/revisions" + ), + api_version=APIVersion.V1ALPHA, + json={"transformer": transformer}, + ) + + +def test_create_integration_transformer_revision_with_comment(chronicle_client): + """Test create_integration_transformer_revision with comment.""" + transformer = { + "displayName": "Test Transformer", + "script": "def transform(data): return data", + } + expected = {"name": "rev1", "comment": "Version 2.0"} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + transformer=transformer, + comment="Version 2.0", + ) + + assert result == expected + + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["transformer"] == transformer + assert call_kwargs["json"]["comment"] == "Version 2.0" + + +# -- rollback_integration_transformer_revision tests -- + + +def test_rollback_integration_transformer_revision_success(chronicle_client): + """Test rollback_integration_transformer_revision delegates to chronicle_request.""" + expected = {"name": "rev1", "comment": "Rolled back"} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.transformer_revisions.format_resource_id", + return_value="test-integration", + ): + result = rollback_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + revision_id="rev1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/transformers/t1/revisions/rev1:rollback" + ), + api_version=APIVersion.V1ALPHA, + ) + + +# -- Error handling tests -- + + +def test_list_integration_transformer_revisions_api_error(chronicle_client): + """Test list_integration_transformer_revisions handles API errors.""" + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", + side_effect=APIError("API Error"), + ): + with pytest.raises(APIError, match="API Error"): + list_integration_transformer_revisions( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + ) + + +def test_delete_integration_transformer_revision_api_error(chronicle_client): + """Test delete_integration_transformer_revision handles API errors.""" + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + side_effect=APIError("Delete failed"), + ): + with pytest.raises(APIError, match="Delete failed"): + delete_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + revision_id="rev1", + ) + + +def test_create_integration_transformer_revision_api_error(chronicle_client): + """Test create_integration_transformer_revision handles API errors.""" + transformer = {"displayName": "Test"} + + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + side_effect=APIError("Creation failed"), + ): + with pytest.raises(APIError, match="Creation failed"): + create_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + transformer=transformer, + ) + + +def test_rollback_integration_transformer_revision_api_error(chronicle_client): + """Test rollback_integration_transformer_revision handles API errors.""" + with patch( + "secops.chronicle.integration.transformer_revisions.chronicle_request", + side_effect=APIError("Rollback failed"), + ): + with pytest.raises(APIError, match="Rollback failed"): + rollback_integration_transformer_revision( + chronicle_client, + integration_name="test-integration", + transformer_id="t1", + revision_id="rev1", + ) + From 398d0d805f00cbea939f33730c9eb57feda54b50 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 10:02:49 +0000 Subject: [PATCH 42/46] chore: black formatting --- .../integration/logical_operators.py | 24 +++++++++++++------ .../chronicle/integration/transformers.py | 17 ++++++------- src/secops/chronicle/models.py | 1 - .../commands/integration/logical_operators.py | 10 +++----- .../integration/transformer_revisions.py | 5 +--- .../cli/commands/integration/transformers.py | 5 +--- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/secops/chronicle/integration/logical_operators.py b/src/secops/chronicle/integration/logical_operators.py index c8c27204..fe5da103 100644 --- a/src/secops/chronicle/integration/logical_operators.py +++ b/src/secops/chronicle/integration/logical_operators.py @@ -18,7 +18,7 @@ from secops.chronicle.models import ( APIVersion, - IntegrationLogicalOperatorParameter + IntegrationLogicalOperatorParameter, ) from secops.chronicle.utils.format_utils import ( format_resource_id, @@ -219,9 +219,14 @@ def create_integration_logical_operator( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() - if isinstance(p, IntegrationLogicalOperatorParameter) else p - for p in parameters] + [ + ( + p.to_dict() + if isinstance(p, IntegrationLogicalOperatorParameter) + else p + ) + for p in parameters + ] if parameters is not None else None ) @@ -295,9 +300,14 @@ def update_integration_logical_operator( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() - if isinstance(p, IntegrationLogicalOperatorParameter) else p - for p in parameters] + [ + ( + p.to_dict() + if isinstance(p, IntegrationLogicalOperatorParameter) + else p + ) + for p in parameters + ] if parameters is not None else None ) diff --git a/src/secops/chronicle/integration/transformers.py b/src/secops/chronicle/integration/transformers.py index be34f279..a2a0b817 100644 --- a/src/secops/chronicle/integration/transformers.py +++ b/src/secops/chronicle/integration/transformers.py @@ -16,10 +16,7 @@ from typing import Any, TYPE_CHECKING -from secops.chronicle.models import ( - APIVersion, - TransformerDefinitionParameter -) +from secops.chronicle.models import APIVersion, TransformerDefinitionParameter from secops.chronicle.utils.format_utils import ( format_resource_id, build_patch_body, @@ -217,8 +214,10 @@ def create_integration_transformer( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p - for p in parameters] + [ + p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p + for p in parameters + ] if parameters is not None else None ) @@ -299,8 +298,10 @@ def update_integration_transformer( APIError: If the API request fails. """ resolved_parameters = ( - [p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p - for p in parameters] + [ + p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p + for p in parameters + ] if parameters is not None else None ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index 85cec48c..06d81da9 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -734,7 +734,6 @@ def to_dict(self) -> dict: return data - @dataclass class ConnectorRule: """A rule definition for a Chronicle SOAR integration connector. diff --git a/src/secops/cli/commands/integration/logical_operators.py b/src/secops/cli/commands/integration/logical_operators.py index 502eb35d..0bf65725 100644 --- a/src/secops/cli/commands/integration/logical_operators.py +++ b/src/secops/cli/commands/integration/logical_operators.py @@ -278,8 +278,7 @@ def handle_logical_operators_list_command(args, chronicle): output_formatter(out, getattr(args, "output", "json")) except Exception as e: # pylint: disable=broad-exception-caught print( - f"Error listing integration logical operators: {e}", - file=sys.stderr + f"Error listing integration logical operators: {e}", file=sys.stderr ) sys.exit(1) @@ -295,8 +294,7 @@ def handle_logical_operators_get_command(args, chronicle): output_formatter(out, getattr(args, "output", "json")) except Exception as e: # pylint: disable=broad-exception-caught print( - f"Error getting integration logical operator: {e}", - file=sys.stderr + f"Error getting integration logical operator: {e}", file=sys.stderr ) sys.exit(1) @@ -313,8 +311,7 @@ def handle_logical_operators_delete_command(args, chronicle): ) except Exception as e: # pylint: disable=broad-exception-caught print( - f"Error deleting integration logical operator: {e}", - file=sys.stderr + f"Error deleting integration logical operator: {e}", file=sys.stderr ) sys.exit(1) @@ -396,4 +393,3 @@ def handle_logical_operators_template_command(args, chronicle): file=sys.stderr, ) sys.exit(1) - diff --git a/src/secops/cli/commands/integration/transformer_revisions.py b/src/secops/cli/commands/integration/transformer_revisions.py index 3406f60a..1075a696 100644 --- a/src/secops/cli/commands/integration/transformer_revisions.py +++ b/src/secops/cli/commands/integration/transformer_revisions.py @@ -187,9 +187,7 @@ def handle_transformer_revisions_delete_command(args, chronicle): transformer_id=args.transformer_id, revision_id=args.revision_id, ) - print( - f"Transformer revision {args.revision_id} deleted successfully" - ) + print(f"Transformer revision {args.revision_id} deleted successfully") except Exception as e: # pylint: disable=broad-exception-caught print( f"Error deleting transformer revision: {e}", @@ -236,4 +234,3 @@ def handle_transformer_revisions_rollback_command(args, chronicle): file=sys.stderr, ) sys.exit(1) - diff --git a/src/secops/cli/commands/integration/transformers.py b/src/secops/cli/commands/integration/transformers.py index de2851cc..65dcd32d 100644 --- a/src/secops/cli/commands/integration/transformers.py +++ b/src/secops/cli/commands/integration/transformers.py @@ -302,9 +302,7 @@ def handle_transformers_delete_command(args, chronicle): integration_name=args.integration_name, transformer_id=args.transformer_id, ) - print( - f"Transformer {args.transformer_id} deleted successfully" - ) + print(f"Transformer {args.transformer_id} deleted successfully") except Exception as e: # pylint: disable=broad-exception-caught print(f"Error deleting integration transformer: {e}", file=sys.stderr) sys.exit(1) @@ -387,4 +385,3 @@ def handle_transformers_template_command(args, chronicle): file=sys.stderr, ) sys.exit(1) - From 513a8248fb23f5b3a1fbe3e84034d642bd66d5fb Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 10:47:41 +0000 Subject: [PATCH 43/46] feat: implement logical operator revision functions --- CLI.md | 102 ++++ README.md | 147 ++++++ api_module_mapping.md | 440 +++++++++--------- src/secops/chronicle/__init__.py | 11 + src/secops/chronicle/client.py | 170 +++++++ .../integration/logical_operator_revisions.py | 212 +++++++++ .../integration/integration_client.py | 2 + .../integration/logical_operator_revisions.py | 239 ++++++++++ .../test_logical_operator_revisions.py | 367 +++++++++++++++ 9 files changed, 1472 insertions(+), 218 deletions(-) create mode 100644 src/secops/chronicle/integration/logical_operator_revisions.py create mode 100644 src/secops/cli/commands/integration/logical_operator_revisions.py create mode 100644 tests/chronicle/integration/test_logical_operator_revisions.py diff --git a/CLI.md b/CLI.md index 2ac44f54..9873085e 100644 --- a/CLI.md +++ b/CLI.md @@ -2453,6 +2453,108 @@ secops integration logical-operators list \ --as-list ``` +#### Logical Operator Revisions + +List logical operator revisions: + +```bash +# List all revisions for a logical operator +secops integration logical-operator-revisions list \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" + +# List revisions as a direct list +secops integration logical-operator-revisions list \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --as-list + +# List with pagination +secops integration logical-operator-revisions list \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --page-size 10 + +# List with filtering and ordering +secops integration logical-operator-revisions list \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --filter-string "version = '1.0'" \ + --order-by "createTime desc" +``` + +Delete a logical operator revision: + +```bash +# Delete a specific revision +secops integration logical-operator-revisions delete \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --revision-id "rev-456" +``` + +Create a new revision: + +```bash +# Create a backup revision before making changes +secops integration logical-operator-revisions create \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --comment "Backup before refactoring evaluation logic" + +# Create a revision with descriptive comment +secops integration logical-operator-revisions create \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --comment "Version 2.0 - Enhanced comparison logic" +``` + +Rollback to a previous revision: + +```bash +# Rollback logical operator to a specific revision +secops integration logical-operator-revisions rollback \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --revision-id "rev-456" +``` + +Example workflow: Safe logical operator updates with revision control: + +```bash +# 1. Create a backup revision +secops integration logical-operator-revisions create \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --comment "Backup before updating conditional logic" + +# 2. Update the logical operator +secops integration logical-operators update \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --script "def evaluate(a, b): return a >= b" \ + --description "Updated with greater-than-or-equal logic" + +# 3. Test the updated logical operator +secops integration logical-operators test \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" + +# 4. If test fails, rollback to the backup revision +# First, list revisions to get the backup revision ID +secops integration logical-operator-revisions list \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --order-by "createTime desc" \ + --page-size 1 + +# Then rollback using the revision ID +secops integration logical-operator-revisions rollback \ + --integration-name "MyIntegration" \ + --logical-operator-id "lo1" \ + --revision-id "rev-backup-id" +``` + ### Rule Management List detection rules: diff --git a/README.md b/README.md index 2a2308a7..9fe18c94 100644 --- a/README.md +++ b/README.md @@ -5036,6 +5036,153 @@ for op in all_operators: print(f" - {op.get('displayName')} (Enabled: {op.get('enabled')})") ``` +### Integration Logical Operator Revisions + +List all revisions for a logical operator: + +```python +# Get all revisions for a logical operator +revisions = chronicle.list_integration_logical_operator_revisions( + integration_name="MyIntegration", + logical_operator_id="lo1" +) +for revision in revisions.get("revisions", []): + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_logical_operator_revisions( + integration_name="MyIntegration", + logical_operator_id="lo1", + as_list=True +) + +# Filter revisions +revisions = chronicle.list_integration_logical_operator_revisions( + integration_name="MyIntegration", + logical_operator_id="lo1", + filter_string='version = "1.0"', + order_by="createTime desc" +) +``` + +Delete a specific logical operator revision: + +```python +chronicle.delete_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + revision_id="rev-456" +) +``` + +Create a new revision before making changes: + +```python +# Get the current logical operator +logical_operator = chronicle.get_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1" +) + +# Create a backup revision +new_revision = chronicle.create_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + logical_operator=logical_operator, + comment="Backup before refactoring conditional logic" +) +print(f"Created revision: {new_revision.get('name')}") + +# Create revision with custom comment +new_revision = chronicle.create_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + logical_operator=logical_operator, + comment="Version 2.0 - Enhanced comparison logic" +) +``` + +Rollback to a previous revision: + +```python +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + revision_id="rev-456" +) +print(f"Rolled back to: {rollback_result.get('name')}") +``` + +Example workflow: Safe logical operator updates with revision control: + +```python +# 1. Get the current logical operator +logical_operator = chronicle.get_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1" +) + +# 2. Create a backup revision +backup = chronicle.create_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + logical_operator=logical_operator, + comment="Backup before updating evaluation logic" +) + +# 3. Make changes to the logical operator +updated_operator = chronicle.update_integration_logical_operator( + integration_name="MyIntegration", + logical_operator_id="lo1", + display_name="Enhanced Conditional Operator", + script=""" +def evaluate(severity, threshold, include_medium=False): + severity_levels = { + 'LOW': 1, + 'MEDIUM': 2, + 'HIGH': 3, + 'CRITICAL': 4 + } + + current = severity_levels.get(severity.upper(), 0) + min_level = severity_levels.get(threshold.upper(), 0) + + if include_medium and current >= severity_levels['MEDIUM']: + return True + + return current >= min_level +""" +) + +# 4. Test the updated logical operator +test_result = chronicle.execute_integration_logical_operator_test( + integration_name="MyIntegration", + logical_operator=updated_operator +) + +# 5. If test fails, rollback to backup +if test_result.get("resultValue") is None or "error" in test_result.get("debugOutputMessage", "").lower(): + print("Test failed - rolling back") + chronicle.rollback_integration_logical_operator_revision( + integration_name="MyIntegration", + logical_operator_id="lo1", + revision_id=backup.get("name").split("/")[-1] + ) +else: + print("Test passed - logical operator updated successfully") + +# 6. List all revisions to see history +all_revisions = chronicle.list_integration_logical_operator_revisions( + integration_name="MyIntegration", + logical_operator_id="lo1", + as_list=True +) +print(f"Total revisions: {len(all_revisions)}") +for rev in all_revisions: + print(f" - {rev.get('comment', 'No comment')} (ID: {rev.get('name').split('/')[-1]})") +``` + ## Rule Management diff --git a/api_module_mapping.md b/api_module_mapping.md index 1e2b1824..266bd86d 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -8,7 +8,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ - **v1:** 17 endpoints implemented - **v1beta:** 88 endpoints implemented -- **v1alpha:** 199 endpoints implemented +- **v1alpha:** 203 endpoints implemented ## Endpoint Mapping @@ -62,110 +62,110 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | dataAccessScopes.patch | v1beta | | | | get | v1beta | | | | integrations.create | v1beta | | | -| integrations.delete | v1beta | chronicle.integration.integrations.delete_integration | | -| integrations.download | v1beta | chronicle.integration.integrations.download_integration | | -| integrations.downloadDependency | v1beta | chronicle.integration.integrations.download_integration_dependency | | -| integrations.exportIntegrationItems | v1beta | chronicle.integration.integrations.export_integration_items | | -| integrations.fetchAffectedItems | v1beta | chronicle.integration.integrations.get_integration_affected_items | | -| integrations.fetchAgentIntegrations | v1beta | chronicle.integration.integrations.get_agent_integrations | | -| integrations.fetchCommercialDiff | v1beta | chronicle.integration.integrations.get_integration_diff | | -| integrations.fetchDependencies | v1beta | chronicle.integration.integrations.get_integration_dependencies | | -| integrations.fetchRestrictedAgents | v1beta | chronicle.integration.integrations.get_integration_restricted_agents | | -| integrations.get | v1beta | chronicle.integration.integrations.get_integration | | -| integrations.getFetchProductionDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.PRODUCTION) | | -| integrations.getFetchStagingDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.STAGING) | | +| integrations.delete | v1beta | chronicle.integration.integrations.delete_integration | secops integration integrations delete | +| integrations.download | v1beta | chronicle.integration.integrations.download_integration | secops integration integrations download | +| integrations.downloadDependency | v1beta | chronicle.integration.integrations.download_integration_dependency | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1beta | chronicle.integration.integrations.export_integration_items | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1beta | chronicle.integration.integrations.get_integration_affected_items | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1beta | chronicle.integration.integrations.get_agent_integrations | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1beta | chronicle.integration.integrations.get_integration_diff | secops integration integrations get-diff | +| integrations.fetchDependencies | v1beta | chronicle.integration.integrations.get_integration_dependencies | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1beta | chronicle.integration.integrations.get_integration_restricted_agents | secops integration integrations get-restricted-agents | +| integrations.get | v1beta | chronicle.integration.integrations.get_integration | secops integration integrations get | +| integrations.getFetchProductionDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.STAGING) | secops integration integrations get-diff | | integrations.import | v1beta | | | | integrations.importIntegrationDependency | v1beta | | | | integrations.importIntegrationItems | v1beta | | | -| integrations.list | v1beta | chronicle.integration.integrations.list_integrations | | +| integrations.list | v1beta | chronicle.integration.integrations.list_integrations | secops integration integrations list | | integrations.patch | v1beta | | | -| integrations.pushToProduction | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.PRODUCTION) | | -| integrations.pushToStaging | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.STAGING) | | +| integrations.pushToProduction | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.STAGING) | secops integration integrations transition | | integrations.updateCustomIntegration | v1beta | | | | integrations.upload | v1beta | | | -| integrations.actions.create | v1beta | chronicle.integration.actions.create_integration_action | | -| integrations.actions.delete | v1beta | chronicle.integration.actions.delete_integration_action | | -| integrations.actions.executeTest | v1beta | chronicle.integration.actions.execute_integration_action_test | | +| integrations.actions.create | v1beta | chronicle.integration.actions.create_integration_action | secops integration actions create | +| integrations.actions.delete | v1beta | chronicle.integration.actions.delete_integration_action | secops integration actions delete | +| integrations.actions.executeTest | v1beta | chronicle.integration.actions.execute_integration_action_test | secops integration actions test | | integrations.actions.fetchActionsByEnvironment | v1beta | chronicle.integration.actions.get_integration_actions_by_environment | | -| integrations.actions.fetchTemplate | v1beta | chronicle.integration.actions.get_integration_action_template | | -| integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | | -| integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | | -| integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | | -| integrations.actions.revisions.create | v1beta | chronicle.integration.action_revisions.create_integration_action_revision | | -| integrations.actions.revisions.delete | v1beta | chronicle.integration.action_revisions.delete_integration_action_revision | | -| integrations.actions.revisions.list | v1beta | chronicle.integration.action_revisions.list_integration_action_revisions | | -| integrations.actions.revisions.rollback | v1beta | chronicle.integration.action_revisions.rollback_integration_action_revision | | -| integrations.connectors.create | v1beta | chronicle.integration.connectors.create_integration_connector | | -| integrations.connectors.delete | v1beta | chronicle.integration.connectors.delete_integration_connector | | -| integrations.connectors.executeTest | v1beta | chronicle.integration.connectors.execute_integration_connector_test | | -| integrations.connectors.fetchTemplate | v1beta | chronicle.integration.connectors.get_integration_connector_template | | -| integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | | -| integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | | -| integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | | -| integrations.connectors.revisions.create | v1beta | chronicle.integration.connector_revisions.create_integration_connector_revision | | -| integrations.connectors.revisions.delete | v1beta | chronicle.integration.connector_revisions.delete_integration_connector_revision | | -| integrations.connectors.revisions.list | v1beta | chronicle.integration.connector_revisions.list_integration_connector_revisions | | -| integrations.connectors.revisions.rollback | v1beta | chronicle.integration.connector_revisions.rollback_integration_connector_revision | | -| integrations.connectors.contextProperties.clearAll | v1beta | chronicle.integration.connector_context_properties.delete_all_connector_context_properties | | -| integrations.connectors.contextProperties.create | v1beta | chronicle.integration.connector_context_properties.create_connector_context_property | | -| integrations.connectors.contextProperties.delete | v1beta | chronicle.integration.connector_context_properties.delete_connector_context_property | | -| integrations.connectors.contextProperties.get | v1beta | chronicle.integration.connector_context_properties.get_connector_context_property | | -| integrations.connectors.contextProperties.list | v1beta | chronicle.integration.connector_context_properties.list_connector_context_properties | | -| integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | | -| integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.integration.connector_instance_logs.get_connector_instance_log | | -| integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.integration.connector_instance_logs.list_connector_instance_logs | | -| integrations.connectors.connectorInstances.create | v1beta | chronicle.integration.connector_instances.create_connector_instance | | -| integrations.connectors.connectorInstances.delete | v1beta | chronicle.integration.connector_instances.delete_connector_instance | | -| integrations.connectors.connectorInstances.fetchLatestDefinition | v1beta | chronicle.integration.connector_instances.get_connector_instance_latest_definition | | -| integrations.connectors.connectorInstances.get | v1beta | chronicle.integration.connector_instances.get_connector_instance | | -| integrations.connectors.connectorInstances.list | v1beta | chronicle.integration.connector_instances.list_connector_instances | | -| integrations.connectors.connectorInstances.patch | v1beta | chronicle.integration.connector_instances.update_connector_instance | | -| integrations.connectors.connectorInstances.runOnDemand | v1beta | chronicle.integration.connector_instances.run_connector_instance_on_demand | | -| integrations.connectors.connectorInstances.setLogsCollection | v1beta | chronicle.integration.connector_instances.set_connector_instance_logs_collection | | -| integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | | -| integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | | -| integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | | -| integrations.integrationInstances.fetchAffectedItems | v1beta | chronicle.integration.integration_instances.get_integration_instance_affected_items | | -| integrations.integrationInstances.fetchDefaultInstance | v1beta | chronicle.integration.integration_instances.get_default_integration_instance | | -| integrations.integrationInstances.get | v1beta | chronicle.integration.integration_instances.get_integration_instance | | -| integrations.integrationInstances.list | v1beta | chronicle.integration.integration_instances.list_integration_instances | | -| integrations.integrationInstances.patch | v1beta | chronicle.integration.integration_instances.update_integration_instance | | -| integrations.jobs.create | v1beta | chronicle.integration.jobs.create_integration_job | | -| integrations.jobs.delete | v1beta | chronicle.integration.jobs.delete_integration_job | | -| integrations.jobs.executeTest | v1beta | chronicle.integration.jobs.execute_integration_job_test | | -| integrations.jobs.fetchTemplate | v1beta | chronicle.integration.jobs.get_integration_job_template | | -| integrations.jobs.get | v1beta | chronicle.integration.jobs.get_integration_job | | -| integrations.jobs.list | v1beta | chronicle.integration.jobs.list_integration_jobs | | -| integrations.jobs.patch | v1beta | chronicle.integration.jobs.update_integration_job | | -| integrations.managers.create | v1beta | chronicle.integration.managers.create_integration_manager | | -| integrations.managers.delete | v1beta | chronicle.integration.managers.delete_integration_manager | | -| integrations.managers.fetchTemplate | v1beta | chronicle.integration.managers.get_integration_manager_template | | -| integrations.managers.get | v1beta | chronicle.integration.managers.get_integration_manager | | -| integrations.managers.list | v1beta | chronicle.integration.managers.list_integration_managers | | -| integrations.managers.patch | v1beta | chronicle.integration.managers.update_integration_manager | | -| integrations.managers.revisions.create | v1beta | chronicle.integration.manager_revisions.create_integration_manager_revision | | -| integrations.managers.revisions.delete | v1beta | chronicle.integration.manager_revisions.delete_integration_manager_revision | | -| integrations.managers.revisions.get | v1beta | chronicle.integration.manager_revisions.get_integration_manager_revision | | -| integrations.managers.revisions.list | v1beta | chronicle.integration.manager_revisions.list_integration_manager_revisions | | -| integrations.managers.revisions.rollback | v1beta | chronicle.integration.manager_revisions.rollback_integration_manager_revision | | -| integrations.jobs.revisions.create | v1beta | chronicle.integration.job_revisions.create_integration_job_revision | | -| integrations.jobs.revisions.delete | v1beta | chronicle.integration.job_revisions.delete_integration_job_revision | | -| integrations.jobs.revisions.list | v1beta | chronicle.integration.job_revisions.list_integration_job_revisions | | -| integrations.jobs.revisions.rollback | v1beta | chronicle.integration.job_revisions.rollback_integration_job_revision | | -| integrations.jobs.jobInstances.create | v1beta | chronicle.integration.job_instances.create_integration_job_instance | | -| integrations.jobs.jobInstances.delete | v1beta | chronicle.integration.job_instances.delete_integration_job_instance | | -| integrations.jobs.jobInstances.get | v1beta | chronicle.integration.job_instances.get_integration_job_instance | | -| integrations.jobs.jobInstances.list | v1beta | chronicle.integration.job_instances.list_integration_job_instances | | -| integrations.jobs.jobInstances.patch | v1beta | chronicle.integration.job_instances.update_integration_job_instance | | -| integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.integration.job_instances.run_integration_job_instance_on_demand | | -| integrations.jobs.contextProperties.clearAll | v1beta | chronicle.integration.job_context_properties.delete_all_job_context_properties | | -| integrations.jobs.contextProperties.create | v1beta | chronicle.integration.job_context_properties.create_job_context_property | | -| integrations.jobs.contextProperties.delete | v1beta | chronicle.integration.job_context_properties.delete_job_context_property | | -| integrations.jobs.contextProperties.get | v1beta | chronicle.integration.job_context_properties.get_job_context_property | | -| integrations.jobs.contextProperties.list | v1beta | chronicle.integration.job_context_properties.list_job_context_properties | | -| integrations.jobs.contextProperties.patch | v1beta | chronicle.integration.job_context_properties.update_job_context_property | | -| integrations.jobs.jobInstances.logs.get | v1beta | chronicle.integration.job_instance_logs.get_job_instance_log | | -| integrations.jobs.jobInstances.logs.list | v1beta | chronicle.integration.job_instance_logs.list_job_instance_logs | | +| integrations.actions.fetchTemplate | v1beta | chronicle.integration.actions.get_integration_action_template | secops integration actions template | +| integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | secops integration actions get | +| integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | secops integration actions list | +| integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | secops integration actions update | +| integrations.actions.revisions.create | v1beta | chronicle.integration.action_revisions.create_integration_action_revision | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1beta | chronicle.integration.action_revisions.delete_integration_action_revision | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1beta | chronicle.integration.action_revisions.list_integration_action_revisions | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1beta | chronicle.integration.action_revisions.rollback_integration_action_revision | secops integration action-revisions rollback | +| integrations.connectors.create | v1beta | chronicle.integration.connectors.create_integration_connector | secops integration connectors create | +| integrations.connectors.delete | v1beta | chronicle.integration.connectors.delete_integration_connector | secops integration connectors delete | +| integrations.connectors.executeTest | v1beta | chronicle.integration.connectors.execute_integration_connector_test | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1beta | chronicle.integration.connectors.get_integration_connector_template | secops integration connectors template | +| integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | secops integration connectors get | +| integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | secops integration connectors list | +| integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | secops integration connectors update | +| integrations.connectors.revisions.create | v1beta | chronicle.integration.connector_revisions.create_integration_connector_revision | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1beta | chronicle.integration.connector_revisions.delete_integration_connector_revision | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1beta | chronicle.integration.connector_revisions.list_integration_connector_revisions | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1beta | chronicle.integration.connector_revisions.rollback_integration_connector_revision | secops integration connector-revisions rollback| +| integrations.connectors.contextProperties.clearAll | v1beta | chronicle.integration.connector_context_properties.delete_all_connector_context_properties | secops integration connector-context-properties delete-all | +| integrations.connectors.contextProperties.create | v1beta | chronicle.integration.connector_context_properties.create_connector_context_property | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1beta | chronicle.integration.connector_context_properties.delete_connector_context_property | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1beta | chronicle.integration.connector_context_properties.get_connector_context_property | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1beta | chronicle.integration.connector_context_properties.list_connector_context_properties | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | secops integration connector-context-properties update | +| integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.integration.connector_instance_logs.get_connector_instance_log | secops integration connector-instance-logs get | +| integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.integration.connector_instance_logs.list_connector_instance_logs | secops integration connector-instance-logs list| +| integrations.connectors.connectorInstances.create | v1beta | chronicle.integration.connector_instances.create_connector_instance | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1beta | chronicle.integration.connector_instances.delete_connector_instance | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1beta | chronicle.integration.connector_instances.get_connector_instance_latest_definition | secops integration connector-instances get-latest-definition | +| integrations.connectors.connectorInstances.get | v1beta | chronicle.integration.connector_instances.get_connector_instance | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1beta | chronicle.integration.connector_instances.list_connector_instances | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1beta | chronicle.integration.connector_instances.update_connector_instance | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1beta | chronicle.integration.connector_instances.run_connector_instance_on_demand | secops integration connector-instances run-on-demand | +| integrations.connectors.connectorInstances.setLogsCollection | v1beta | chronicle.integration.connector_instances.set_connector_instance_logs_collection | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | secops integration instances create | +| integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1beta | chronicle.integration.integration_instances.get_integration_instance_affected_items | secops integration instances get-affected-items| +| integrations.integrationInstances.fetchDefaultInstance | v1beta | chronicle.integration.integration_instances.get_default_integration_instance | secops integration instances get-default | +| integrations.integrationInstances.get | v1beta | chronicle.integration.integration_instances.get_integration_instance | secops integration instances get | +| integrations.integrationInstances.list | v1beta | chronicle.integration.integration_instances.list_integration_instances | secops integration instances list | +| integrations.integrationInstances.patch | v1beta | chronicle.integration.integration_instances.update_integration_instance | secops integration instances update | +| integrations.jobs.create | v1beta | chronicle.integration.jobs.create_integration_job | secops integration jobs create | +| integrations.jobs.delete | v1beta | chronicle.integration.jobs.delete_integration_job | secops integration jobs delete | +| integrations.jobs.executeTest | v1beta | chronicle.integration.jobs.execute_integration_job_test | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1beta | chronicle.integration.jobs.get_integration_job_template | secops integration jobs template | +| integrations.jobs.get | v1beta | chronicle.integration.jobs.get_integration_job | secops integration jobs get | +| integrations.jobs.list | v1beta | chronicle.integration.jobs.list_integration_jobs | secops integration jobs list | +| integrations.jobs.patch | v1beta | chronicle.integration.jobs.update_integration_job | secops integration jobs update | +| integrations.managers.create | v1beta | chronicle.integration.managers.create_integration_manager | secops integration managers create | +| integrations.managers.delete | v1beta | chronicle.integration.managers.delete_integration_manager | secops integration managers delete | +| integrations.managers.fetchTemplate | v1beta | chronicle.integration.managers.get_integration_manager_template | secops integration managers template | +| integrations.managers.get | v1beta | chronicle.integration.managers.get_integration_manager | secops integration managers get | +| integrations.managers.list | v1beta | chronicle.integration.managers.list_integration_managers | secops integration managers list | +| integrations.managers.patch | v1beta | chronicle.integration.managers.update_integration_manager | secops integration managers update | +| integrations.managers.revisions.create | v1beta | chronicle.integration.manager_revisions.create_integration_manager_revision | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1beta | chronicle.integration.manager_revisions.delete_integration_manager_revision | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1beta | chronicle.integration.manager_revisions.get_integration_manager_revision | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1beta | chronicle.integration.manager_revisions.list_integration_manager_revisions | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1beta | chronicle.integration.manager_revisions.rollback_integration_manager_revision | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1beta | chronicle.integration.job_revisions.create_integration_job_revision | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1beta | chronicle.integration.job_revisions.delete_integration_job_revision | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1beta | chronicle.integration.job_revisions.list_integration_job_revisions | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1beta | chronicle.integration.job_revisions.rollback_integration_job_revision | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1beta | chronicle.integration.job_instances.create_integration_job_instance | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1beta | chronicle.integration.job_instances.delete_integration_job_instance | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1beta | chronicle.integration.job_instances.get_integration_job_instance | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1beta | chronicle.integration.job_instances.list_integration_job_instances | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1beta | chronicle.integration.job_instances.update_integration_job_instance | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.integration.job_instances.run_integration_job_instance_on_demand | secops integration job-instances run-on-demand | +| integrations.jobs.contextProperties.clearAll | v1beta | chronicle.integration.job_context_properties.delete_all_job_context_properties | secops integration job-context-properties delete-all | +| integrations.jobs.contextProperties.create | v1beta | chronicle.integration.job_context_properties.create_job_context_property | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1beta | chronicle.integration.job_context_properties.delete_job_context_property | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1beta | chronicle.integration.job_context_properties.get_job_context_property | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1beta | chronicle.integration.job_context_properties.list_job_context_properties | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1beta | chronicle.integration.job_context_properties.update_job_context_property | secops integration job-context-properties update | +| integrations.jobs.jobInstances.logs.get | v1beta | chronicle.integration.job_instance_logs.get_job_instance_log | secops integration job-instance-logs get | +| integrations.jobs.jobInstances.logs.list | v1beta | chronicle.integration.job_instance_logs.list_job_instance_logs | secops integration job-instance-logs list | | marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | | marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | | marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | @@ -340,128 +340,132 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | ingestionLogNamespaces.get | v1alpha | | | | ingestionLogNamespaces.list | v1alpha | | | | integrations.create | v1alpha | | | -| integrations.delete | v1alpha | chronicle.integration.integrations.delete_integration(api_version=APIVersion.V1ALPHA) | | -| integrations.download | v1alpha | chronicle.integration.integrations.download_integration(api_version=APIVersion.V1ALPHA) | | -| integrations.downloadDependency | v1alpha | chronicle.integration.integrations.download_integration_dependency(api_version=APIVersion.V1ALPHA) | | -| integrations.exportIntegrationItems | v1alpha | chronicle.integration.integrations.export_integration_items(api_version=APIVersion.V1ALPHA) | | -| integrations.fetchAffectedItems | v1alpha | chronicle.integration.integrations.get_integration_affected_items(api_version=APIVersion.V1ALPHA) | | -| integrations.fetchAgentIntegrations | v1alpha | chronicle.integration.integrations.get_agent_integrations(api_version=APIVersion.V1ALPHA) | | -| integrations.fetchCommercialDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA) | | -| integrations.fetchDependencies | v1alpha | chronicle.integration.integrations.get_integration_dependencies(api_version=APIVersion.V1ALPHA) | | -| integrations.fetchRestrictedAgents | v1alpha | chronicle.integration.integrations.get_integration_restricted_agents(api_version=APIVersion.V1ALPHA) | | -| integrations.get | v1alpha | chronicle.integration.integrations.get_integration(api_version=APIVersion.V1ALPHA) | | -| integrations.getFetchProductionDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA, diff_type=DiffType.PRODUCTION) | | -| integrations.getFetchStagingDiff | v1alpha | chronicle.integration.integrations.get_integration_diffapi_version=APIVersion.V1ALPHA, (diff_type=DiffType.STAGING) | | +| integrations.delete | v1alpha | chronicle.integration.integrations.delete_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations delete | +| integrations.download | v1alpha | chronicle.integration.integrations.download_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations download | +| integrations.downloadDependency | v1alpha | chronicle.integration.integrations.download_integration_dependency(api_version=APIVersion.V1ALPHA) | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1alpha | chronicle.integration.integrations.export_integration_items(api_version=APIVersion.V1ALPHA) | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1alpha | chronicle.integration.integrations.get_integration_affected_items(api_version=APIVersion.V1ALPHA) | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1alpha | chronicle.integration.integrations.get_agent_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA) | secops integration integrations get-diff | +| integrations.fetchDependencies | v1alpha | chronicle.integration.integrations.get_integration_dependencies(api_version=APIVersion.V1ALPHA) | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1alpha | chronicle.integration.integrations.get_integration_restricted_agents(api_version=APIVersion.V1ALPHA) | secops integration integrations get-restricted-agents | +| integrations.get | v1alpha | chronicle.integration.integrations.get_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations get | +| integrations.getFetchProductionDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA, diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1alpha | chronicle.integration.integrations.get_integration_diffapi_version=APIVersion.V1ALPHA, (diff_type=DiffType.STAGING) | secops integration integrations get-diff | | integrations.import | v1alpha | | | | integrations.importIntegrationDependency | v1alpha | | | | integrations.importIntegrationItems | v1alpha | | | -| integrations.list | v1alpha | chronicle.integration.integrations.list_integrations(api_version=APIVersion.V1ALPHA) | | +| integrations.list | v1alpha | chronicle.integration.integrations.list_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations list | | integrations.patch | v1alpha | | | -| integrations.pushToProduction | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.PRODUCTION) | | -| integrations.pushToStaging | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.STAGING) | | +| integrations.pushToProduction | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.STAGING) | secops integration integrations transition | | integrations.updateCustomIntegration | v1alpha | | | | integrations.upload | v1alpha | | | -| integrations.actions.create | v1alpha | chronicle.integration.actions.create_integration_action(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.delete | v1alpha | chronicle.integration.actions.delete_integration_action(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.executeTest | v1alpha | chronicle.integration.actions.execute_integration_action_test(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.create | v1alpha | chronicle.integration.actions.create_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions create | +| integrations.actions.delete | v1alpha | chronicle.integration.actions.delete_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions delete | +| integrations.actions.executeTest | v1alpha | chronicle.integration.actions.execute_integration_action_test(api_version=APIVersion.V1ALPHA) | secops integration actions test | | integrations.actions.fetchActionsByEnvironment | v1alpha | chronicle.integration.actions.get_integration_actions_by_environment(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.fetchTemplate | v1alpha | chronicle.integration.actions.get_integration_action_template(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.revisions.create | v1alpha | chronicle.integration.action_revisions.create_integration_action_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.revisions.delete | v1alpha | chronicle.integration.action_revisions.delete_integration_action_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.revisions.list | v1alpha | chronicle.integration.action_revisions.list_integration_action_revisions(api_version=APIVersion.V1ALPHA) | | -| integrations.actions.revisions.rollback | v1alpha | chronicle.integration.action_revisions.rollback_integration_action_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.create | v1alpha | chronicle.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.delete | v1alpha | chronicle.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.executeTest | v1alpha | chronicle.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.fetchTemplate | v1alpha | chronicle.integration.connectors.get_integration_connector_template(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.revisions.create | v1alpha | chronicle.integration.connector_revisions.create_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.revisions.delete | v1alpha | chronicle.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.revisions.list | v1alpha | chronicle.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.revisions.rollback | v1alpha | chronicle.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.clearAll | v1alpha | chronicle.integration.connector_context_properties.delete_all_connector_context_properties(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.create | v1alpha | chronicle.integration.connector_context_properties.create_connector_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.delete | v1alpha | chronicle.integration.connector_context_properties.delete_connector_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.get | v1alpha | chronicle.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.list | v1alpha | chronicle.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.logs.get | v1alpha | chronicle.integration.connector_instance_logs.get_connector_instance_log(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.logs.list | v1alpha | chronicle.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.create | v1alpha | chronicle.integration.connector_instances.create_connector_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.delete | v1alpha | chronicle.integration.connector_instances.delete_connector_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.fetchLatestDefinition | v1alpha | chronicle.integration.connector_instances.get_connector_instance_latest_definition(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.get | v1alpha | chronicle.integration.connector_instances.get_connector_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.list | v1alpha | chronicle.integration.connector_instances.list_connector_instances(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.patch | v1alpha | chronicle.integration.connector_instances.update_connector_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.runOnDemand | v1alpha | chronicle.integration.connector_instances.run_connector_instance_on_demand(api_version=APIVersion.V1ALPHA) | | -| integrations.connectors.connectorInstances.setLogsCollection | v1alpha | chronicle.integration.connector_instances.set_connector_instance_logs_collection(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.fetchAffectedItems | v1alpha | chronicle.integration.integration_instances.get_integration_instance_affected_items(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.fetchDefaultInstance | v1alpha | chronicle.integration.integration_instances.get_default_integration_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.get | v1alpha | chronicle.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.list | v1alpha | chronicle.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | | -| integrations.integrationInstances.patch | v1alpha | chronicle.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.transformers.create | v1alpha | chronicle.integration.transformers.create_integration_transformer | | -| integrations.transformers.delete | v1alpha | chronicle.integration.transformers.delete_integration_transformer | | -| integrations.transformers.executeTest | v1alpha | chronicle.integration.transformers.execute_integration_transformer_test | | -| integrations.transformers.fetchTemplate | v1alpha | chronicle.integration.transformers.get_integration_transformer_template | | -| integrations.transformers.get | v1alpha | chronicle.integration.transformers.get_integration_transformer | | -| integrations.transformers.list | v1alpha | chronicle.integration.transformers.list_integration_transformers | | -| integrations.transformers.patch | v1alpha | chronicle.integration.transformers.update_integration_transformer | | -| integrations.transformers.revisions.create | v1alpha | chronicle.integration.transformer_revisions.create_integration_transformer_revision | | -| integrations.transformers.revisions.delete | v1alpha | chronicle.integration.transformer_revisions.delete_integration_transformer_revision | | -| integrations.transformers.revisions.list | v1alpha | chronicle.integration.transformer_revisions.list_integration_transformer_revisions | | -| integrations.transformers.revisions.rollback | v1alpha | chronicle.integration.transformer_revisions.rollback_integration_transformer_revision | | -| integrations.logicalOperators.create | v1alpha | chronicle.integration.logical_operators.create_integration_logical_operator | | -| integrations.logicalOperators.delete | v1alpha | chronicle.integration.logical_operators.delete_integration_logical_operator | | -| integrations.logicalOperators.executeTest | v1alpha | chronicle.integration.logical_operators.execute_integration_logical_operator_test | | -| integrations.logicalOperators.fetchTemplate | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator_template | | -| integrations.logicalOperators.get | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator | | -| integrations.logicalOperators.list | v1alpha | chronicle.integration.logical_operators.list_integration_logical_operators | | -| integrations.logicalOperators.patch | v1alpha | chronicle.integration.logical_operators.update_integration_logical_operator | | -| integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.fetchTemplate | v1alpha | chronicle.integration.jobs.get_integration_job_template(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.get | v1alpha | chronicle.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.list | v1alpha | chronicle.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.patch | v1alpha | chronicle.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.create | v1alpha | chronicle.integration.managers.create_integration_manager(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.delete | v1alpha | chronicle.integration.managers.delete_integration_manager(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.fetchTemplate | v1alpha | chronicle.integration.managers.get_integration_manager_template(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.get | v1alpha | chronicle.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.list | v1alpha | chronicle.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.patch | v1alpha | chronicle.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.revisions.create | v1alpha | chronicle.integration.manager_revisions.create_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.revisions.delete | v1alpha | chronicle.integration.manager_revisions.delete_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.revisions.get | v1alpha | chronicle.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.revisions.list | v1alpha | chronicle.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | | -| integrations.managers.revisions.rollback | v1alpha | chronicle.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.revisions.create | v1alpha | chronicle.integration.job_revisions.create_integration_job_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.revisions.delete | v1alpha | chronicle.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.revisions.list | v1alpha | chronicle.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.revisions.rollback | v1alpha | chronicle.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.create | v1alpha | chronicle.integration.job_instances.create_integration_job_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.delete | v1alpha | chronicle.integration.job_instances.delete_integration_job_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.get | v1alpha | chronicle.integration.job_instances.get_integration_job_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.list | v1alpha | chronicle.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.patch | v1alpha | chronicle.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.integration.job_instances.run_integration_job_instance_on_demand(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.clearAll | v1alpha | chronicle.integration.job_context_properties.delete_all_job_context_properties(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.create | v1alpha | chronicle.integration.job_context_properties.create_job_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.delete | v1alpha | chronicle.integration.job_context_properties.delete_job_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.get | v1alpha | chronicle.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.list | v1alpha | chronicle.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.contextProperties.patch | v1alpha | chronicle.integration.job_context_properties.update_job_context_property(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.logs.get | v1alpha | chronicle.integration.job_instance_logs.get_job_instance_log(api_version=APIVersion.V1ALPHA) | | -| integrations.jobs.jobInstances.logs.list | v1alpha | chronicle.integration.job_instance_logs.list_job_instance_logs(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.fetchTemplate | v1alpha | chronicle.integration.actions.get_integration_action_template(api_version=APIVersion.V1ALPHA) | secops integration actions template | +| integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions get | +| integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | secops integration actions list | +| integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions update | +| integrations.actions.revisions.create | v1alpha | chronicle.integration.action_revisions.create_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1alpha | chronicle.integration.action_revisions.delete_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1alpha | chronicle.integration.action_revisions.list_integration_action_revisions(api_version=APIVersion.V1ALPHA) | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1alpha | chronicle.integration.action_revisions.rollback_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions rollback | +| integrations.connectors.create | v1alpha | chronicle.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors create | +| integrations.connectors.delete | v1alpha | chronicle.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors delete | +| integrations.connectors.executeTest | v1alpha | chronicle.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1alpha | chronicle.integration.connectors.get_integration_connector_template(api_version=APIVersion.V1ALPHA) | secops integration connectors template | +| integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors get | +| integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | secops integration connectors list | +| integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors update | +| integrations.connectors.revisions.create | v1alpha | chronicle.integration.connector_revisions.create_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1alpha | chronicle.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1alpha | chronicle.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1alpha | chronicle.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions rollback| +| integrations.connectors.contextProperties.clearAll | v1alpha | chronicle.integration.connector_context_properties.delete_all_connector_context_properties(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties delete-all | +| integrations.connectors.contextProperties.create | v1alpha | chronicle.integration.connector_context_properties.create_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1alpha | chronicle.integration.connector_context_properties.delete_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1alpha | chronicle.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1alpha | chronicle.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties update | +| integrations.connectors.connectorInstances.logs.get | v1alpha | chronicle.integration.connector_instance_logs.get_connector_instance_log(api_version=APIVersion.V1ALPHA) | secops integration connector-instance-logs get | +| integrations.connectors.connectorInstances.logs.list | v1alpha | chronicle.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | secops integration connector-instance-logs list| +| integrations.connectors.connectorInstances.create | v1alpha | chronicle.integration.connector_instances.create_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1alpha | chronicle.integration.connector_instances.delete_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1alpha | chronicle.integration.connector_instances.get_connector_instance_latest_definition(api_version=APIVersion.V1ALPHA) | secops integration connector-instances get-latest-definition | +| integrations.connectors.connectorInstances.get | v1alpha | chronicle.integration.connector_instances.get_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1alpha | chronicle.integration.connector_instances.list_connector_instances(api_version=APIVersion.V1ALPHA) | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1alpha | chronicle.integration.connector_instances.update_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1alpha | chronicle.integration.connector_instances.run_connector_instance_on_demand(api_version=APIVersion.V1ALPHA) | secops integration connector-instances run-on-demand | +| integrations.connectors.connectorInstances.setLogsCollection | v1alpha | chronicle.integration.connector_instances.set_connector_instance_logs_collection(api_version=APIVersion.V1ALPHA) | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances create | +| integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1alpha | chronicle.integration.integration_instances.get_integration_instance_affected_items(api_version=APIVersion.V1ALPHA) | secops integration instances get-affected-items| +| integrations.integrationInstances.fetchDefaultInstance | v1alpha | chronicle.integration.integration_instances.get_default_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get-default | +| integrations.integrationInstances.get | v1alpha | chronicle.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get | +| integrations.integrationInstances.list | v1alpha | chronicle.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | secops integration instances list | +| integrations.integrationInstances.patch | v1alpha | chronicle.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances update | +| integrations.transformers.create | v1alpha | chronicle.integration.transformers.create_integration_transformer | secops integration transformers create | +| integrations.transformers.delete | v1alpha | chronicle.integration.transformers.delete_integration_transformer | secops integration transformers delete | +| integrations.transformers.executeTest | v1alpha | chronicle.integration.transformers.execute_integration_transformer_test | secops integration transformers test | +| integrations.transformers.fetchTemplate | v1alpha | chronicle.integration.transformers.get_integration_transformer_template | secops integration transformers template | +| integrations.transformers.get | v1alpha | chronicle.integration.transformers.get_integration_transformer | secops integration transformers get | +| integrations.transformers.list | v1alpha | chronicle.integration.transformers.list_integration_transformers | secops integration transformers list | +| integrations.transformers.patch | v1alpha | chronicle.integration.transformers.update_integration_transformer | secops integration transformers update | +| integrations.transformers.revisions.create | v1alpha | chronicle.integration.transformer_revisions.create_integration_transformer_revision | secops integration transformer-revisions create| +| integrations.transformers.revisions.delete | v1alpha | chronicle.integration.transformer_revisions.delete_integration_transformer_revision | secops integration transformer-revisions delete| +| integrations.transformers.revisions.list | v1alpha | chronicle.integration.transformer_revisions.list_integration_transformer_revisions | secops integration transformer-revisions list | +| integrations.transformers.revisions.rollback | v1alpha | chronicle.integration.transformer_revisions.rollback_integration_transformer_revision | secops integration transformer-revisions rollback| +| integrations.logicalOperators.create | v1alpha | chronicle.integration.logical_operators.create_integration_logical_operator | secops integration logical-operators create | +| integrations.logicalOperators.delete | v1alpha | chronicle.integration.logical_operators.delete_integration_logical_operator | secops integration logical-operators delete | +| integrations.logicalOperators.executeTest | v1alpha | chronicle.integration.logical_operators.execute_integration_logical_operator_test | secops integration logical-operators test | +| integrations.logicalOperators.fetchTemplate | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator_template | secops integration logical-operators template | +| integrations.logicalOperators.get | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator | secops integration logical-operators get | +| integrations.logicalOperators.list | v1alpha | chronicle.integration.logical_operators.list_integration_logical_operators | secops integration logical-operators list | +| integrations.logicalOperators.patch | v1alpha | chronicle.integration.logical_operators.update_integration_logical_operator | secops integration logical-operators update | +| integrations.logicalOperators.revisions.create | v1alpha | chronicle.integration.logical_operator_revisions.create_integration_logical_operator_revision | secops integration logical-operator-revisions create | +| integrations.logicalOperators.revisions.delete | v1alpha | chronicle.integration.logical_operator_revisions.delete_integration_logical_operator_revision | secops integration logical-operator-revisions delete | +| integrations.logicalOperators.revisions.list | v1alpha | chronicle.integration.logical_operator_revisions.list_integration_logical_operator_revisions | secops integration logical-operator-revisions list | +| integrations.logicalOperators.revisions.rollback | v1alpha | chronicle.integration.logical_operator_revisions.rollback_integration_logical_operator_revision | secops integration logical-operator-revisions rollback | +| integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs create | +| integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs delete | +| integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1alpha | chronicle.integration.jobs.get_integration_job_template(api_version=APIVersion.V1ALPHA) | secops integration jobs template | +| integrations.jobs.get | v1alpha | chronicle.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs get | +| integrations.jobs.list | v1alpha | chronicle.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | secops integration jobs list | +| integrations.jobs.patch | v1alpha | chronicle.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs update | +| integrations.managers.create | v1alpha | chronicle.integration.managers.create_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers create | +| integrations.managers.delete | v1alpha | chronicle.integration.managers.delete_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers delete | +| integrations.managers.fetchTemplate | v1alpha | chronicle.integration.managers.get_integration_manager_template(api_version=APIVersion.V1ALPHA) | secops integration managers template | +| integrations.managers.get | v1alpha | chronicle.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers get | +| integrations.managers.list | v1alpha | chronicle.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | secops integration managers list | +| integrations.managers.patch | v1alpha | chronicle.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers update | +| integrations.managers.revisions.create | v1alpha | chronicle.integration.manager_revisions.create_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1alpha | chronicle.integration.manager_revisions.delete_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1alpha | chronicle.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1alpha | chronicle.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1alpha | chronicle.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1alpha | chronicle.integration.job_revisions.create_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1alpha | chronicle.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1alpha | chronicle.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1alpha | chronicle.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1alpha | chronicle.integration.job_instances.create_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1alpha | chronicle.integration.job_instances.delete_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1alpha | chronicle.integration.job_instances.get_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1alpha | chronicle.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1alpha | chronicle.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.integration.job_instances.run_integration_job_instance_on_demand(api_version=APIVersion.V1ALPHA) | secops integration job-instances run-on-demand | +| integrations.jobs.contextProperties.clearAll | v1alpha | chronicle.integration.job_context_properties.delete_all_job_context_properties(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties delete-all | +| integrations.jobs.contextProperties.create | v1alpha | chronicle.integration.job_context_properties.create_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1alpha | chronicle.integration.job_context_properties.delete_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1alpha | chronicle.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1alpha | chronicle.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1alpha | chronicle.integration.job_context_properties.update_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties update | +| integrations.jobs.jobInstances.logs.get | v1alpha | chronicle.integration.job_instance_logs.get_job_instance_log(api_version=APIVersion.V1ALPHA) | secops integration job-instance-logs get | +| integrations.jobs.jobInstances.logs.list | v1alpha | chronicle.integration.job_instance_logs.list_job_instance_logs(api_version=APIVersion.V1ALPHA) | secops integration job-instance-logs list | | investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | | investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | | investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | @@ -557,11 +561,11 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | logTypes.runParser | v1alpha | chronicle.parser.run_parser | secops parser run | | logTypes.updateLogTypeSetting | v1alpha | | | | logs.classify | v1alpha | chronicle.log_types.classify_logs | secops log classify | -| marketplaceIntegrations.get | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration(api_version=APIVersion.V1ALPHA) | | -| marketplaceIntegrations.getDiff | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration_diff(api_version=APIVersion.V1ALPHA) | | -| marketplaceIntegrations.install | v1alpha | chronicle.marketplace_integrations.install_marketplace_integration(api_version=APIVersion.V1ALPHA) | | -| marketplaceIntegrations.list | v1alpha | chronicle.marketplace_integrations.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) | | -| marketplaceIntegrations.uninstall | v1alpha | chronicle.marketplace_integrations.uninstall_marketplace_integration(api_version=APIVersion.V1ALPHA) | | +| marketplaceIntegrations.get | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace get | +| marketplaceIntegrations.getDiff | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration_diff(api_version=APIVersion.V1ALPHA) | secops integration marketplace diff | +| marketplaceIntegrations.install | v1alpha | chronicle.marketplace_integrations.install_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace install | +| marketplaceIntegrations.list | v1alpha | chronicle.marketplace_integrations.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) | secops integration marketplace list | +| marketplaceIntegrations.uninstall | v1alpha | chronicle.marketplace_integrations.uninstall_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace uninstall | | nativeDashboards.addChart | v1alpha | chronicle.dashboard.add_chart | secops dashboard add-chart | | nativeDashboards.create | v1alpha | chronicle.dashboard.create_dashboard | secops dashboard create | | nativeDashboards.delete | v1alpha | chronicle.dashboard.delete_dashboard | secops dashboard delete | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 0bf5b5de..e7365ac9 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -363,6 +363,12 @@ execute_integration_logical_operator_test, get_integration_logical_operator_template, ) +from secops.chronicle.integration.logical_operator_revisions import ( + list_integration_logical_operator_revisions, + delete_integration_logical_operator_revision, + create_integration_logical_operator_revision, + rollback_integration_logical_operator_revision, +) from secops.chronicle.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, @@ -679,6 +685,11 @@ "update_integration_logical_operator", "execute_integration_logical_operator_test", "get_integration_logical_operator_template", + # Integration Logical Operator Revisions + "list_integration_logical_operator_revisions", + "delete_integration_logical_operator_revision", + "create_integration_logical_operator_revision", + "rollback_integration_logical_operator_revision", # Marketplace Integrations "list_marketplace_integrations", "get_marketplace_integration", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 3921ad2a..eceeda5b 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -291,6 +291,12 @@ list_integration_logical_operators as _list_integration_logical_operators, update_integration_logical_operator as _update_integration_logical_operator, ) +from secops.chronicle.integration.logical_operator_revisions import ( + create_integration_logical_operator_revision as _create_integration_logical_operator_revision, + delete_integration_logical_operator_revision as _delete_integration_logical_operator_revision, + list_integration_logical_operator_revisions as _list_integration_logical_operator_revisions, + rollback_integration_logical_operator_revision as _rollback_integration_logical_operator_revision, +) from secops.chronicle.models import ( APIVersion, CaseList, @@ -5879,6 +5885,170 @@ def get_integration_logical_operator_template( api_version=api_version, ) + # -- Integration Logical Operator Revisions methods -- + + def list_integration_logical_operator_revisions( + self, + integration_name: str, + logical_operator_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific logical operator. + + Use this method to view the revision history of a logical operator, + enabling you to track changes and potentially rollback to previous + versions. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to list + revisions for. + page_size: Maximum number of revisions to return. Defaults to + 100, maximum is 200. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1ALPHA. + as_list: If True, automatically fetches all pages and returns + a list of revisions. If False, returns dict with revisions + and nextPageToken. + + Returns: + If as_list is True: List of logical operator revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_logical_operator_revisions( + self, + integration_name, + logical_operator_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_logical_operator_revision( + self, + integration_name: str, + logical_operator_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> None: + """Delete a specific logical operator revision. + + Use this method to remove obsolete or incorrect revisions from + a logical operator's history. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_logical_operator_revision( + self, + integration_name, + logical_operator_id, + revision_id, + api_version=api_version, + ) + + def create_integration_logical_operator_revision( + self, + integration_name: str, + logical_operator_id: str, + logical_operator: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Create a new revision for a logical operator. + + Use this method to create a snapshot of the logical operator's + current state before making changes, enabling you to rollback if + needed. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to create a + revision for. + logical_operator: Dict containing the LogicalOperator + definition to save as a revision. + comment: Optional comment describing the revision or changes. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the newly created LogicalOperatorRevision + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_logical_operator_revision( + self, + integration_name, + logical_operator_id, + logical_operator, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_logical_operator_revision( + self, + integration_name: str, + logical_operator_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, + ) -> dict[str, Any]: + """Rollback a logical operator to a previous revision. + + Use this method to restore a logical operator to a previous + working state by rolling back to a specific revision. + + Args: + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1ALPHA. + + Returns: + Dict containing the updated LogicalOperator resource. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_logical_operator_revision( + self, + integration_name, + logical_operator_id, + revision_id, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/integration/logical_operator_revisions.py b/src/secops/chronicle/integration/logical_operator_revisions.py new file mode 100644 index 00000000..f7f00cee --- /dev/null +++ b/src/secops/chronicle/integration/logical_operator_revisions.py @@ -0,0 +1,212 @@ +# Copyright 2025 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. +# +"""Integration logical operator revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_logical_operator_revisions( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration logical operator. + + Use this method to browse through the version history of a custom logical + operator definition. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1ALPHA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_logical_operator_revision( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> None: + """Delete a specific revision for a given integration logical operator. + + Permanently removes the versioned snapshot from the logical operator's + history. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator the revision belongs + to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_logical_operator_revision( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + logical_operator: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration + logical operator. + + Use this method to save the current state of a logical operator + definition. Revisions can only be created for custom logical operators. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to create a revision + for. + logical_operator: Dict containing the IntegrationLogicalOperator to + snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the newly created IntegrationLogicalOperatorRevision + resource. + + Raises: + APIError: If the API request fails. + """ + body = {"logicalOperator": logical_operator} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_logical_operator_revision( + client: "ChronicleClient", + integration_name: str, + logical_operator_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1ALPHA, +) -> dict[str, Any]: + """Roll back the current logical operator to a previously saved revision. + + This updates the active logical operator definition with the configuration + stored in the specified revision. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the logical operator + belongs to. + logical_operator_id: ID of the logical operator to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1ALPHA. + + Returns: + Dict containing the IntegrationLogicalOperatorRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"logicalOperators/{logical_operator_id}/revisions/" + f"{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index 5d283122..d84bd383 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -35,6 +35,7 @@ transformers, transformer_revisions, logical_operators, + logical_operator_revisions, ) @@ -53,6 +54,7 @@ def setup_integrations_command(subparsers): transformers.setup_transformers_command(lvl1) transformer_revisions.setup_transformer_revisions_command(lvl1) logical_operators.setup_logical_operators_command(lvl1) + logical_operator_revisions.setup_logical_operator_revisions_command(lvl1) actions.setup_actions_command(lvl1) action_revisions.setup_action_revisions_command(lvl1) connectors.setup_connectors_command(lvl1) diff --git a/src/secops/cli/commands/integration/logical_operator_revisions.py b/src/secops/cli/commands/integration/logical_operator_revisions.py new file mode 100644 index 00000000..d09b9d70 --- /dev/null +++ b/src/secops/cli/commands/integration/logical_operator_revisions.py @@ -0,0 +1,239 @@ +# Copyright 2025 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. +# +"""Google SecOps CLI integration logical operator revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_logical_operator_revisions_command(subparsers): + """Setup integration logical operator revisions command""" + revisions_parser = subparsers.add_parser( + "logical-operator-revisions", + help="Manage integration logical operator revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="logical_operator_revisions_command", + help="Integration logical operator revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", + help="List integration logical operator revisions", + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator", + dest="logical_operator_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_logical_operator_revisions_list_command, + ) + + # delete command + delete_parser = lvl1.add_parser( + "delete", + help="Delete an integration logical operator revision", + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator", + dest="logical_operator_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults( + func=handle_logical_operator_revisions_delete_command, + ) + + # create command + create_parser = lvl1.add_parser( + "create", + help="Create a new integration logical operator revision", + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator", + dest="logical_operator_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults( + func=handle_logical_operator_revisions_create_command, + ) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", + help="Rollback logical operator to a previous revision", + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--logical-operator-id", + type=str, + help="ID of the logical operator", + dest="logical_operator_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults( + func=handle_logical_operator_revisions_rollback_command, + ) + + +def handle_logical_operator_revisions_list_command(args, chronicle): + """Handle integration logical operator revisions list command""" + try: + out = chronicle.list_integration_logical_operator_revisions( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing logical operator revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_logical_operator_revisions_delete_command(args, chronicle): + """Handle integration logical operator revision delete command""" + try: + chronicle.delete_integration_logical_operator_revision( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + revision_id=args.revision_id, + ) + print( + f"Logical operator revision {args.revision_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error deleting logical operator revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_logical_operator_revisions_create_command(args, chronicle): + """Handle integration logical operator revision create command""" + try: + # Get the current logical operator to create a revision + logical_operator = chronicle.get_integration_logical_operator( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + ) + out = chronicle.create_integration_logical_operator_revision( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + logical_operator=logical_operator, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating logical operator revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + + +def handle_logical_operator_revisions_rollback_command(args, chronicle): + """Handle integration logical operator revision rollback command""" + try: + out = chronicle.rollback_integration_logical_operator_revision( + integration_name=args.integration_name, + logical_operator_id=args.logical_operator_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error rolling back logical operator revision: {e}", + file=sys.stderr, + ) + sys.exit(1) + diff --git a/tests/chronicle/integration/test_logical_operator_revisions.py b/tests/chronicle/integration/test_logical_operator_revisions.py new file mode 100644 index 00000000..29e912e6 --- /dev/null +++ b/tests/chronicle/integration/test_logical_operator_revisions.py @@ -0,0 +1,367 @@ +# Copyright 2025 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. +# +"""Tests for Chronicle integration logical operator revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.logical_operator_revisions import ( + list_integration_logical_operator_revisions, + delete_integration_logical_operator_revision, + create_integration_logical_operator_revision, + rollback_integration_logical_operator_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1ALPHA, + ) + + +# -- list_integration_logical_operator_revisions tests -- + + +def test_list_integration_logical_operator_revisions_success(chronicle_client): + """Test list_integration_logical_operator_revisions delegates to paginated request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "token", + } + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.logical_operator_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_logical_operator_revisions( + chronicle_client, + integration_name="My Integration", + logical_operator_id="lo1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/My Integration/logicalOperators/lo1/revisions", + items_key="revisions", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_logical_operator_revisions_default_args(chronicle_client): + """Test list_integration_logical_operator_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_logical_operator_revisions( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/logicalOperators/lo1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={}, + as_list=False, + ) + + +def test_list_integration_logical_operator_revisions_with_filter_order( + chronicle_client, +): + """Test list passes filter/orderBy in extra_params.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_logical_operator_revisions( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + filter_string='version = "1.0"', + order_by="createTime desc", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/logicalOperators/lo1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={ + "filter": 'version = "1.0"', + "orderBy": "createTime desc", + }, + as_list=False, + ) + + +def test_list_integration_logical_operator_revisions_as_list(chronicle_client): + """Test list_integration_logical_operator_revisions with as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_logical_operator_revisions( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + as_list=True, + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1ALPHA, + path="integrations/test-integration/logicalOperators/lo1/revisions", + items_key="revisions", + page_size=None, + page_token=None, + extra_params={}, + as_list=True, + ) + + +# -- delete_integration_logical_operator_revision tests -- + + +def test_delete_integration_logical_operator_revision_success(chronicle_client): + """Test delete_integration_logical_operator_revision delegates to chronicle_request.""" + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + return_value=None, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operator_revisions.format_resource_id", + return_value="test-integration", + ): + delete_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + revision_id="rev1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path=( + "integrations/test-integration/logicalOperators/lo1/revisions/rev1" + ), + api_version=APIVersion.V1ALPHA, + ) + + +# -- create_integration_logical_operator_revision tests -- + + +def test_create_integration_logical_operator_revision_minimal(chronicle_client): + """Test create_integration_logical_operator_revision with minimal fields.""" + logical_operator = { + "displayName": "Test Operator", + "script": "def evaluate(a, b): return a == b", + } + expected = {"name": "rev1", "comment": ""} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operator_revisions.format_resource_id", + return_value="test-integration", + ): + result = create_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + logical_operator=logical_operator, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/logicalOperators/lo1/revisions" + ), + api_version=APIVersion.V1ALPHA, + json={"logicalOperator": logical_operator}, + ) + + +def test_create_integration_logical_operator_revision_with_comment(chronicle_client): + """Test create_integration_logical_operator_revision with comment.""" + logical_operator = { + "displayName": "Test Operator", + "script": "def evaluate(a, b): return a == b", + } + expected = {"name": "rev1", "comment": "Version 2.0"} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + logical_operator=logical_operator, + comment="Version 2.0", + ) + + assert result == expected + + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["logicalOperator"] == logical_operator + assert call_kwargs["json"]["comment"] == "Version 2.0" + + +# -- rollback_integration_logical_operator_revision tests -- + + +def test_rollback_integration_logical_operator_revision_success(chronicle_client): + """Test rollback_integration_logical_operator_revision delegates to chronicle_request.""" + expected = {"name": "rev1", "comment": "Rolled back"} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + return_value=expected, + ) as mock_request, patch( + "secops.chronicle.integration.logical_operator_revisions.format_resource_id", + return_value="test-integration", + ): + result = rollback_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + revision_id="rev1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/logicalOperators/lo1/" + "revisions/rev1:rollback" + ), + api_version=APIVersion.V1ALPHA, + ) + + +# -- Error handling tests -- + + +def test_list_integration_logical_operator_revisions_api_error(chronicle_client): + """Test list_integration_logical_operator_revisions handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", + side_effect=APIError("API Error"), + ): + with pytest.raises(APIError, match="API Error"): + list_integration_logical_operator_revisions( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + ) + + +def test_delete_integration_logical_operator_revision_api_error(chronicle_client): + """Test delete_integration_logical_operator_revision handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + side_effect=APIError("Delete failed"), + ): + with pytest.raises(APIError, match="Delete failed"): + delete_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + revision_id="rev1", + ) + + +def test_create_integration_logical_operator_revision_api_error(chronicle_client): + """Test create_integration_logical_operator_revision handles API errors.""" + logical_operator = {"displayName": "Test"} + + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + side_effect=APIError("Creation failed"), + ): + with pytest.raises(APIError, match="Creation failed"): + create_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + logical_operator=logical_operator, + ) + + +def test_rollback_integration_logical_operator_revision_api_error(chronicle_client): + """Test rollback_integration_logical_operator_revision handles API errors.""" + with patch( + "secops.chronicle.integration.logical_operator_revisions.chronicle_request", + side_effect=APIError("Rollback failed"), + ): + with pytest.raises(APIError, match="Rollback failed"): + rollback_integration_logical_operator_revision( + chronicle_client, + integration_name="test-integration", + logical_operator_id="lo1", + revision_id="rev1", + ) + From 2ab83f60f9aa4d970e465e92a754ff952120569d Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 12:35:42 +0000 Subject: [PATCH 44/46] chore: move test case directory --- .../chronicle/{ => integration}/test_marketplace_integrations.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/chronicle/{ => integration}/test_marketplace_integrations.py (100%) diff --git a/tests/chronicle/test_marketplace_integrations.py b/tests/chronicle/integration/test_marketplace_integrations.py similarity index 100% rename from tests/chronicle/test_marketplace_integrations.py rename to tests/chronicle/integration/test_marketplace_integrations.py From 068762b62fb5e7f05ed04015623cd6304589b37e Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 15:07:00 +0000 Subject: [PATCH 45/46] fix: PyLint error on f-strings --- src/secops/chronicle/utils/request_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/secops/chronicle/utils/request_utils.py b/src/secops/chronicle/utils/request_utils.py index 70395141..c3b2cd8a 100644 --- a/src/secops/chronicle/utils/request_utils.py +++ b/src/secops/chronicle/utils/request_utils.py @@ -347,7 +347,7 @@ def chronicle_request_bytes( try: data = response.json() raise APIError( - f"{error_message or "API request failed"}: method={method}, url={url}, " + f"{error_message or 'API request failed'}: method={method}, url={url}, " f"status={response.status_code}, response={data}" ) from None except ValueError: @@ -355,7 +355,7 @@ def chronicle_request_bytes( getattr(response, "text", ""), limit=MAX_BODY_CHARS ) raise APIError( - f"{error_message or "API request failed"}: method={method}, url={url}, " + f"{error_message or 'API request failed'}: method={method}, url={url}, " f"status={response.status_code}, response_text={preview}" ) from None From 3936b15300a5e6ff2c7c4ce4232e0923b3e26486 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 15:23:02 +0000 Subject: [PATCH 46/46] chore: refactor for split PR --- CLI.md | 1584 +---- README.md | 3063 +--------- src/secops/chronicle/__init__.py | 247 - src/secops/chronicle/client.py | 5352 ++--------------- .../connector_context_properties.py | 299 - .../integration/connector_instance_logs.py | 130 - .../integration/connector_instances.py | 489 -- .../integration/connector_revisions.py | 202 - .../chronicle/integration/connectors.py | 405 -- .../integration/integration_instances.py | 403 -- .../chronicle/integration/integrations.py | 686 --- .../integration/job_context_properties.py | 298 - .../integration/job_instance_logs.py | 125 - .../chronicle/integration/job_instances.py | 399 -- .../chronicle/integration/job_revisions.py | 204 - src/secops/chronicle/integration/jobs.py | 371 -- .../integration/logical_operator_revisions.py | 212 - .../integration/logical_operators.py | 411 -- .../integration/marketplace_integrations.py | 199 - .../integration/transformer_revisions.py | 202 - .../chronicle/integration/transformers.py | 406 -- .../connector_context_properties.py | 375 -- .../integration/connector_instance_logs.py | 142 - .../integration/connector_instances.py | 473 -- .../integration/connector_revisions.py | 217 - .../cli/commands/integration/connectors.py | 325 - .../cli/commands/integration/integration.py | 775 --- .../integration/integration_client.py | 36 - .../integration/integration_instances.py | 392 -- .../integration/job_context_properties.py | 354 -- .../commands/integration/job_instance_logs.py | 140 - .../cli/commands/integration/job_instances.py | 407 -- .../cli/commands/integration/job_revisions.py | 213 - src/secops/cli/commands/integration/jobs.py | 356 -- .../integration/logical_operator_revisions.py | 239 - .../commands/integration/logical_operators.py | 395 -- .../integration/marketplace_integration.py | 204 - .../integration/transformer_revisions.py | 236 - .../cli/commands/integration/transformers.py | 387 -- .../test_connector_context_properties.py | 561 -- .../test_connector_instance_logs.py | 256 - .../integration/test_connector_instances.py | 845 --- .../integration/test_connector_revisions.py | 385 -- .../chronicle/integration/test_connectors.py | 665 -- .../integration/test_integration_instances.py | 623 -- .../integration/test_integrations.py | 909 --- .../test_job_context_properties.py | 506 -- .../integration/test_job_instance_logs.py | 256 - .../integration/test_job_instances.py | 733 --- .../integration/test_job_revisions.py | 378 -- tests/chronicle/integration/test_jobs.py | 594 -- .../test_logical_operator_revisions.py | 367 -- .../integration/test_logical_operators.py | 547 -- .../test_marketplace_integrations.py | 522 -- .../integration/test_transformer_revisions.py | 366 -- .../integration/test_transformers.py | 555 -- 56 files changed, 633 insertions(+), 29788 deletions(-) delete mode 100644 src/secops/chronicle/integration/connector_context_properties.py delete mode 100644 src/secops/chronicle/integration/connector_instance_logs.py delete mode 100644 src/secops/chronicle/integration/connector_instances.py delete mode 100644 src/secops/chronicle/integration/connector_revisions.py delete mode 100644 src/secops/chronicle/integration/connectors.py delete mode 100644 src/secops/chronicle/integration/integration_instances.py delete mode 100644 src/secops/chronicle/integration/integrations.py delete mode 100644 src/secops/chronicle/integration/job_context_properties.py delete mode 100644 src/secops/chronicle/integration/job_instance_logs.py delete mode 100644 src/secops/chronicle/integration/job_instances.py delete mode 100644 src/secops/chronicle/integration/job_revisions.py delete mode 100644 src/secops/chronicle/integration/jobs.py delete mode 100644 src/secops/chronicle/integration/logical_operator_revisions.py delete mode 100644 src/secops/chronicle/integration/logical_operators.py delete mode 100644 src/secops/chronicle/integration/marketplace_integrations.py delete mode 100644 src/secops/chronicle/integration/transformer_revisions.py delete mode 100644 src/secops/chronicle/integration/transformers.py delete mode 100644 src/secops/cli/commands/integration/connector_context_properties.py delete mode 100644 src/secops/cli/commands/integration/connector_instance_logs.py delete mode 100644 src/secops/cli/commands/integration/connector_instances.py delete mode 100644 src/secops/cli/commands/integration/connector_revisions.py delete mode 100644 src/secops/cli/commands/integration/connectors.py delete mode 100644 src/secops/cli/commands/integration/integration.py delete mode 100644 src/secops/cli/commands/integration/integration_instances.py delete mode 100644 src/secops/cli/commands/integration/job_context_properties.py delete mode 100644 src/secops/cli/commands/integration/job_instance_logs.py delete mode 100644 src/secops/cli/commands/integration/job_instances.py delete mode 100644 src/secops/cli/commands/integration/job_revisions.py delete mode 100644 src/secops/cli/commands/integration/jobs.py delete mode 100644 src/secops/cli/commands/integration/logical_operator_revisions.py delete mode 100644 src/secops/cli/commands/integration/logical_operators.py delete mode 100644 src/secops/cli/commands/integration/marketplace_integration.py delete mode 100644 src/secops/cli/commands/integration/transformer_revisions.py delete mode 100644 src/secops/cli/commands/integration/transformers.py delete mode 100644 tests/chronicle/integration/test_connector_context_properties.py delete mode 100644 tests/chronicle/integration/test_connector_instance_logs.py delete mode 100644 tests/chronicle/integration/test_connector_instances.py delete mode 100644 tests/chronicle/integration/test_connector_revisions.py delete mode 100644 tests/chronicle/integration/test_connectors.py delete mode 100644 tests/chronicle/integration/test_integration_instances.py delete mode 100644 tests/chronicle/integration/test_integrations.py delete mode 100644 tests/chronicle/integration/test_job_context_properties.py delete mode 100644 tests/chronicle/integration/test_job_instance_logs.py delete mode 100644 tests/chronicle/integration/test_job_instances.py delete mode 100644 tests/chronicle/integration/test_job_revisions.py delete mode 100644 tests/chronicle/integration/test_jobs.py delete mode 100644 tests/chronicle/integration/test_logical_operator_revisions.py delete mode 100644 tests/chronicle/integration/test_logical_operators.py delete mode 100644 tests/chronicle/integration/test_marketplace_integrations.py delete mode 100644 tests/chronicle/integration/test_transformer_revisions.py delete mode 100644 tests/chronicle/integration/test_transformers.py diff --git a/CLI.md b/CLI.md index bbf327b5..7fa8d2a0 100644 --- a/CLI.md +++ b/CLI.md @@ -959,1617 +959,155 @@ secops integration action-revisions delete \ --revision-id "r789" ``` -#### Integration Connectors +#### Integration Managers -List integration connectors: +List integration managers: ```bash -# List all connectors for an integration -secops integration connectors list --integration-name "MyIntegration" +# List all managers for an integration +secops integration managers list --integration-name "MyIntegration" -# List connectors as a direct list (fetches all pages automatically) -secops integration connectors list --integration-name "MyIntegration" --as-list +# List managers as a direct list (fetches all pages automatically) +secops integration managers list --integration-name "MyIntegration" --as-list # List with pagination -secops integration connectors list --integration-name "MyIntegration" --page-size 50 +secops integration managers list --integration-name "MyIntegration" --page-size 50 # List with filtering -secops integration connectors list --integration-name "MyIntegration" --filter-string "enabled = true" +secops integration managers list --integration-name "MyIntegration" --filter-string "enabled = true" ``` -Get connector details: +Get manager details: ```bash -secops integration connectors get --integration-name "MyIntegration" --connector-id "c1" +secops integration managers get --integration-name "MyIntegration" --manager-id "mgr1" ``` -Create a new connector: +Create a new manager: ```bash -secops integration connectors create \ +secops integration managers create \ --integration-name "MyIntegration" \ - --display-name "Data Ingestion" \ - --code "def fetch_data(context): return []" + --display-name "Configuration Manager" \ + --code "def manage_config(context): return {'status': 'configured'}" # Create with description and custom ID -secops integration connectors create \ +secops integration managers create \ --integration-name "MyIntegration" \ - --display-name "My Connector" \ - --code "def fetch_data(context): return []" \ - --description "Connector description" \ - --connector-id "custom-connector-id" + --display-name "My Manager" \ + --code "def manage(context): return {}" \ + --description "Manager description" \ + --manager-id "custom-manager-id" ``` -Update an existing connector: +Update an existing manager: ```bash # Update display name -secops integration connectors update \ +secops integration managers update \ --integration-name "MyIntegration" \ - --connector-id "c1" \ - --display-name "Updated Connector Name" + --manager-id "mgr1" \ + --display-name "Updated Manager Name" # Update code -secops integration connectors update \ +secops integration managers update \ --integration-name "MyIntegration" \ - --connector-id "c1" \ - --code "def fetch_data(context): return updated_data()" + --manager-id "mgr1" \ + --code "def manage(context): return {'status': 'updated'}" # Update multiple fields with update mask -secops integration connectors update \ +secops integration managers update \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --display-name "New Name" \ --description "New description" \ --update-mask "displayName,description" ``` -Delete a connector: - -```bash -secops integration connectors delete --integration-name "MyIntegration" --connector-id "c1" -``` - -Test a connector: +Delete a manager: ```bash -secops integration connectors test --integration-name "MyIntegration" --connector-id "c1" +secops integration managers delete --integration-name "MyIntegration" --manager-id "mgr1" ``` -Get connector template: +Get manager template: ```bash -secops integration connectors template --integration-name "MyIntegration" +secops integration managers template --integration-name "MyIntegration" ``` -#### Connector Revisions +#### Manager Revisions -List connector revisions: +List manager revisions: ```bash -# List all revisions for a connector -secops integration connector-revisions list \ +# List all revisions for a manager +secops integration manager-revisions list \ --integration-name "MyIntegration" \ - --connector-id "c1" + --manager-id "mgr1" # List revisions as a direct list -secops integration connector-revisions list \ +secops integration manager-revisions list \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --as-list # List with pagination -secops integration connector-revisions list \ +secops integration manager-revisions list \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --page-size 10 # List with filtering and ordering -secops integration connector-revisions list \ +secops integration manager-revisions list \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --filter-string 'version = "1.0"' \ --order-by "createTime desc" ``` +Get a specific revision: + +```bash +secops integration manager-revisions get \ + --integration-name "MyIntegration" \ + --manager-id "mgr1" \ + --revision-id "r456" +``` + Create a revision backup: ```bash # Create revision with comment -secops integration connector-revisions create \ +secops integration manager-revisions create \ --integration-name "MyIntegration" \ - --connector-id "c1" \ - --comment "Backup before field mapping changes" + --manager-id "mgr1" \ + --comment "Backup before major refactor" # Create revision without comment -secops integration connector-revisions create \ +secops integration manager-revisions create \ --integration-name "MyIntegration" \ - --connector-id "c1" + --manager-id "mgr1" ``` Rollback to a previous revision: ```bash -secops integration connector-revisions rollback \ +secops integration manager-revisions rollback \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --revision-id "r456" ``` Delete an old revision: ```bash -secops integration connector-revisions delete \ +secops integration manager-revisions delete \ --integration-name "MyIntegration" \ - --connector-id "c1" \ + --manager-id "mgr1" \ --revision-id "r789" ``` -#### Connector Context Properties - -List connector context properties: - -```bash -# List all properties for a connector context -secops integration connector-context-properties list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" - -# List properties as a direct list -secops integration connector-context-properties list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --as-list - -# List with pagination -secops integration connector-context-properties list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --page-size 50 - -# List with filtering -secops integration connector-context-properties list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --filter-string 'key = "last_run_time"' -``` - -Get a specific context property: - -```bash -secops integration connector-context-properties get \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --property-id "prop123" -``` - -Create a new context property: - -```bash -# Store last run time -secops integration connector-context-properties create \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --key "last_run_time" \ - --value "2026-03-09T10:00:00Z" - -# Store checkpoint for incremental sync -secops integration connector-context-properties create \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --key "checkpoint" \ - --value "page_token_xyz123" -``` - -Update a context property: - -```bash -# Update last run time -secops integration connector-context-properties update \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --property-id "prop123" \ - --value "2026-03-09T11:00:00Z" -``` - -Delete a context property: - -```bash -secops integration connector-context-properties delete \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" \ - --property-id "prop123" -``` - -Clear all context properties: - -```bash -# Clear all properties for a specific context -secops integration connector-context-properties clear-all \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --context-id "mycontext" -``` - -#### Connector Instance Logs - -List connector instance logs: - -```bash -# List all logs for a connector instance -secops integration connector-instance-logs list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" - -# List logs as a direct list -secops integration connector-instance-logs list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --as-list - -# List with pagination -secops integration connector-instance-logs list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --page-size 50 - -# List with filtering (filter by severity or timestamp) -secops integration connector-instance-logs list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --filter-string 'severity = "ERROR"' \ - --order-by "createTime desc" -``` - -Get a specific log entry: - -```bash -secops integration connector-instance-logs get \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --log-id "log456" -``` - -#### Connector Instances - -List connector instances: - -```bash -# List all instances for a connector -secops integration connector-instances list \ - --integration-name "MyIntegration" \ - --connector-id "c1" - -# List instances as a direct list -secops integration connector-instances list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --as-list - -# List with pagination -secops integration connector-instances list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --page-size 50 - -# List with filtering -secops integration connector-instances list \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --filter-string 'enabled = true' -``` - -Get connector instance details: - -```bash -secops integration connector-instances get \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" -``` - -Create a new connector instance: - -```bash -# Create basic connector instance -secops integration connector-instances create \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --environment "production" \ - --display-name "Production Data Collector" - -# Create with schedule and timeout -secops integration connector-instances create \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --environment "production" \ - --display-name "Hourly Sync" \ - --interval-seconds 3600 \ - --timeout-seconds 300 \ - --enabled -``` - -Update a connector instance: - -```bash -# Update display name -secops integration connector-instances update \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --display-name "Updated Display Name" - -# Update interval and timeout -secops integration connector-instances update \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --interval-seconds 7200 \ - --timeout-seconds 600 - -# Enable or disable instance -secops integration connector-instances update \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --enabled true - -# Update multiple fields with update mask -secops integration connector-instances update \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --display-name "New Name" \ - --interval-seconds 3600 \ - --update-mask "displayName,intervalSeconds" -``` - -Delete a connector instance: - -```bash -secops integration connector-instances delete \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" -``` - -Fetch latest definition: - -```bash -# Get the latest definition of a connector instance -secops integration connector-instances fetch-latest \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" -``` - -Enable or disable log collection: - -```bash -# Enable log collection for debugging -secops integration connector-instances set-logs \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --enabled true - -# Disable log collection -secops integration connector-instances set-logs \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" \ - --enabled false -``` - -Run connector instance on demand: - -```bash -# Trigger an immediate execution for testing -secops integration connector-instances run-ondemand \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --connector-instance-id "inst123" -``` - -#### Integration Jobs - -List integration jobs: - -```bash -# List all jobs for an integration -secops integration jobs list --integration-name "MyIntegration" - -# List jobs as a direct list (fetches all pages automatically) -secops integration jobs list --integration-name "MyIntegration" --as-list - -# List with pagination -secops integration jobs list --integration-name "MyIntegration" --page-size 50 - -# List with filtering -secops integration jobs list --integration-name "MyIntegration" --filter-string "enabled = true" - -# Exclude staging jobs -secops integration jobs list --integration-name "MyIntegration" --exclude-staging -``` - -Get job details: - -```bash -secops integration jobs get --integration-name "MyIntegration" --job-id "job1" -``` - -Create a new job: - -```bash -secops integration jobs create \ - --integration-name "MyIntegration" \ - --display-name "Data Processing Job" \ - --code "def process_data(context): return {'status': 'processed'}" - -# Create with description and custom ID -secops integration jobs create \ - --integration-name "MyIntegration" \ - --display-name "Scheduled Report" \ - --code "def generate_report(context): return report_data()" \ - --description "Daily report generation job" \ - --job-id "daily-report-job" - -# Create with parameters -secops integration jobs create \ - --integration-name "MyIntegration" \ - --display-name "Configurable Job" \ - --code "def run(context, params): return process(params)" \ - --parameters '[{"name":"interval","type":"STRING","required":true}]' -``` - -Update an existing job: - -```bash -# Update display name -secops integration jobs update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --display-name "Updated Job Name" - -# Update code -secops integration jobs update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --code "def run(context): return {'status': 'updated'}" - -# Update multiple fields with update mask -secops integration jobs update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --display-name "New Name" \ - --description "New description" \ - --update-mask "displayName,description" - -# Update parameters -secops integration jobs update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --parameters '[{"name":"timeout","type":"INTEGER","required":false}]' -``` - -Delete a job: - -```bash -secops integration jobs delete --integration-name "MyIntegration" --job-id "job1" -``` - -Test a job: - -```bash -secops integration jobs test --integration-name "MyIntegration" --job-id "job1" -``` - -Get job template: - -```bash -secops integration jobs template --integration-name "MyIntegration" -``` - -#### Job Revisions - -List job revisions: - -```bash -# List all revisions for a job -secops integration job-revisions list \ - --integration-name "MyIntegration" \ - --job-id "job1" - -# List revisions as a direct list -secops integration job-revisions list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --as-list - -# List with pagination -secops integration job-revisions list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --page-size 10 - -# List with filtering and ordering -secops integration job-revisions list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --filter-string 'version = "1.0"' \ - --order-by "createTime desc" -``` - -Create a revision backup: - -```bash -# Create revision with comment -secops integration job-revisions create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --comment "Backup before refactoring job logic" - -# Create revision without comment -secops integration job-revisions create \ - --integration-name "MyIntegration" \ - --job-id "job1" -``` - -Rollback to a previous revision: - -```bash -secops integration job-revisions rollback \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --revision-id "r456" -``` - -Delete an old revision: - -```bash -secops integration job-revisions delete \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --revision-id "r789" -``` - -#### Job Context Properties - -List job context properties: - -```bash -# List all properties for a job context -secops integration job-context-properties list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" - -# List properties as a direct list -secops integration job-context-properties list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --as-list - -# List with pagination -secops integration job-context-properties list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --page-size 50 - -# List with filtering -secops integration job-context-properties list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --filter-string 'key = "last_run_time"' -``` - -Get a specific context property: - -```bash -secops integration job-context-properties get \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --property-id "prop123" -``` - -Create a new context property: - -```bash -# Store last execution time -secops integration job-context-properties create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --key "last_execution_time" \ - --value "2026-03-09T10:00:00Z" - -# Store job state for resumable operations -secops integration job-context-properties create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --key "processing_offset" \ - --value "1000" -``` - -Update a context property: - -```bash -# Update execution time -secops integration job-context-properties update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --property-id "prop123" \ - --value "2026-03-09T11:00:00Z" -``` - -Delete a context property: - -```bash -secops integration job-context-properties delete \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" \ - --property-id "prop123" -``` - -Clear all context properties: - -```bash -# Clear all properties for a specific context -secops integration job-context-properties clear-all \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --context-id "mycontext" -``` - -#### Job Instance Logs - -List job instance logs: - -```bash -# List all logs for a job instance -secops integration job-instance-logs list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" - -# List logs as a direct list -secops integration job-instance-logs list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --as-list - -# List with pagination -secops integration job-instance-logs list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --page-size 50 - -# List with filtering (filter by severity or timestamp) -secops integration job-instance-logs list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --filter-string 'severity = "ERROR"' \ - --order-by "createTime desc" -``` - -Get a specific log entry: - -```bash -secops integration job-instance-logs get \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --log-id "log456" -``` - -#### Job Instances - -List job instances: - -```bash -# List all instances for a job -secops integration job-instances list \ - --integration-name "MyIntegration" \ - --job-id "job1" - -# List instances as a direct list -secops integration job-instances list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --as-list - -# List with pagination -secops integration job-instances list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --page-size 50 - -# List with filtering -secops integration job-instances list \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --filter-string 'enabled = true' -``` - -Get job instance details: - -```bash -secops integration job-instances get \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" -``` - -Create a new job instance: - -```bash -# Create basic job instance -secops integration job-instances create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --environment "production" \ - --display-name "Daily Report Generator" - -# Create with schedule and timeout -secops integration job-instances create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --environment "production" \ - --display-name "Hourly Data Sync" \ - --schedule "0 * * * *" \ - --timeout-seconds 300 \ - --enabled - -# Create with parameters -secops integration job-instances create \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --environment "production" \ - --display-name "Custom Job Instance" \ - --schedule "0 0 * * *" \ - --parameters '[{"name":"batch_size","value":"1000"}]' -``` - -Update a job instance: - -```bash -# Update display name -secops integration job-instances update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --display-name "Updated Display Name" - -# Update schedule and timeout -secops integration job-instances update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --schedule "0 */2 * * *" \ - --timeout-seconds 600 - -# Enable or disable instance -secops integration job-instances update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --enabled true - -# Update multiple fields with update mask -secops integration job-instances update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --display-name "New Name" \ - --schedule "0 6 * * *" \ - --update-mask "displayName,schedule" - -# Update parameters -secops integration job-instances update \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --parameters '[{"name":"batch_size","value":"2000"}]' -``` - -Delete a job instance: - -```bash -secops integration job-instances delete \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" -``` - -Run job instance on demand: - -```bash -# Trigger an immediate execution for testing -secops integration job-instances run-ondemand \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" - -# Run with custom parameters -secops integration job-instances run-ondemand \ - --integration-name "MyIntegration" \ - --job-id "job1" \ - --job-instance-id "inst123" \ - --parameters '[{"name":"batch_size","value":"500"}]' -``` - -#### Integration Managers - -List integration managers: - -```bash -# List all managers for an integration -secops integration managers list --integration-name "MyIntegration" - -# List managers as a direct list (fetches all pages automatically) -secops integration managers list --integration-name "MyIntegration" --as-list - -# List with pagination -secops integration managers list --integration-name "MyIntegration" --page-size 50 - -# List with filtering -secops integration managers list --integration-name "MyIntegration" --filter-string "enabled = true" -``` - -Get manager details: - -```bash -secops integration managers get --integration-name "MyIntegration" --manager-id "mgr1" -``` - -Create a new manager: - -```bash -secops integration managers create \ - --integration-name "MyIntegration" \ - --display-name "Configuration Manager" \ - --code "def manage_config(context): return {'status': 'configured'}" - -# Create with description and custom ID -secops integration managers create \ - --integration-name "MyIntegration" \ - --display-name "My Manager" \ - --code "def manage(context): return {}" \ - --description "Manager description" \ - --manager-id "custom-manager-id" -``` - -Update an existing manager: - -```bash -# Update display name -secops integration managers update \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --display-name "Updated Manager Name" - -# Update code -secops integration managers update \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --code "def manage(context): return {'status': 'updated'}" - -# Update multiple fields with update mask -secops integration managers update \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --display-name "New Name" \ - --description "New description" \ - --update-mask "displayName,description" -``` - -Delete a manager: - -```bash -secops integration managers delete --integration-name "MyIntegration" --manager-id "mgr1" -``` - -Get manager template: - -```bash -secops integration managers template --integration-name "MyIntegration" -``` - -#### Manager Revisions - -List manager revisions: - -```bash -# List all revisions for a manager -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" - -# List revisions as a direct list -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --as-list - -# List with pagination -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --page-size 10 - -# List with filtering and ordering -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --filter-string 'version = "1.0"' \ - --order-by "createTime desc" -``` - -Get a specific revision: - -```bash -secops integration manager-revisions get \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r456" -``` - -Create a revision backup: - -```bash -# Create revision with comment -secops integration manager-revisions create \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --comment "Backup before major refactor" - -# Create revision without comment -secops integration manager-revisions create \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" -``` - -Rollback to a previous revision: - -```bash -secops integration manager-revisions rollback \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r456" -``` - -Delete an old revision: - -```bash -secops integration manager-revisions delete \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r789" -``` - -#### Integration Instances - -List integration instances: - -```bash -# List all instances for an integration -secops integration instances list --integration-name "MyIntegration" - -# List instances as a direct list (fetches all pages automatically) -secops integration instances list --integration-name "MyIntegration" --as-list - -# List with pagination -secops integration instances list --integration-name "MyIntegration" --page-size 50 - -# List with filtering -secops integration instances list --integration-name "MyIntegration" --filter-string "enabled = true" -``` - -Get integration instance details: - -```bash -secops integration instances get \ - --integration-name "MyIntegration" \ - --instance-id "inst123" -``` - -Create a new integration instance: - -```bash -# Create basic integration instance -secops integration instances create \ - --integration-name "MyIntegration" \ - --display-name "Production Instance" \ - --environment "production" - -# Create with description and custom ID -secops integration instances create \ - --integration-name "MyIntegration" \ - --display-name "Test Instance" \ - --environment "test" \ - --description "Testing environment instance" \ - --instance-id "test-inst-001" - -# Create with configuration -secops integration instances create \ - --integration-name "MyIntegration" \ - --display-name "Configured Instance" \ - --environment "production" \ - --config '{"api_key":"secret123","region":"us-east1"}' -``` - -Update an integration instance: - -```bash -# Update display name -secops integration instances update \ - --integration-name "MyIntegration" \ - --instance-id "inst123" \ - --display-name "Updated Instance Name" - -# Update configuration -secops integration instances update \ - --integration-name "MyIntegration" \ - --instance-id "inst123" \ - --config '{"api_key":"newsecret456","region":"us-west1"}' - -# Update multiple fields with update mask -secops integration instances update \ - --integration-name "MyIntegration" \ - --instance-id "inst123" \ - --display-name "New Name" \ - --description "New description" \ - --update-mask "displayName,description" -``` - -Delete an integration instance: - -```bash -secops integration instances delete \ - --integration-name "MyIntegration" \ - --instance-id "inst123" -``` - -Test an integration instance: - -```bash -# Test the instance configuration -secops integration instances test \ - --integration-name "MyIntegration" \ - --instance-id "inst123" -``` - -Get affected items: - -```bash -# Get items affected by this instance -secops integration instances get-affected-items \ - --integration-name "MyIntegration" \ - --instance-id "inst123" -``` - -Get default instance: - -```bash -# Get the default integration instance -secops integration instances get-default \ - --integration-name "MyIntegration" -``` - -#### Integration Transformers - -List integration transformers: - -```bash -# List all transformers for an integration -secops integration transformers list --integration-name "MyIntegration" - -# List transformers as a direct list (fetches all pages automatically) -secops integration transformers list --integration-name "MyIntegration" --as-list - -# List with pagination -secops integration transformers list --integration-name "MyIntegration" --page-size 50 - -# List with filtering -secops integration transformers list --integration-name "MyIntegration" --filter-string "enabled = true" - -# Exclude staging transformers -secops integration transformers list --integration-name "MyIntegration" --exclude-staging - -# List with expanded details -secops integration transformers list --integration-name "MyIntegration" --expand "parameters" -``` - -Get transformer details: - -```bash -# Get basic transformer details -secops integration transformers get \ - --integration-name "MyIntegration" \ - --transformer-id "t1" - -# Get transformer with expanded parameters -secops integration transformers get \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --expand "parameters" -``` - -Create a new transformer: - -```bash -# Create a basic transformer -secops integration transformers create \ - --integration-name "MyIntegration" \ - --display-name "JSON Parser" \ - --script "def transform(data): import json; return json.loads(data)" \ - --script-timeout "60s" \ - --enabled - -# Create transformer with description -secops integration transformers create \ - --integration-name "MyIntegration" \ - --display-name "Data Enricher" \ - --script "def transform(data): return {'enriched': data, 'timestamp': '2024-01-01'}" \ - --script-timeout "120s" \ - --description "Enriches data with additional fields" \ - --enabled -``` - -> **Note:** When creating a transformer: -> - `--script-timeout` should be specified with a unit (e.g., "60s", "2m") -> - Use `--enabled` flag to enable the transformer on creation (default is disabled) -> - The script must be valid Python code with a `transform()` function - -Update an existing transformer: - -```bash -# Update display name -secops integration transformers update \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --display-name "Updated Transformer Name" - -# Update script -secops integration transformers update \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --script "def transform(data): return data.upper()" - -# Update multiple fields -secops integration transformers update \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --display-name "Enhanced Transformer" \ - --description "Updated with better error handling" \ - --script-timeout "90s" \ - --enabled true - -# Update with custom update mask -secops integration transformers update \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --display-name "New Name" \ - --description "New description" \ - --update-mask "displayName,description" -``` - -Delete a transformer: - -```bash -secops integration transformers delete \ - --integration-name "MyIntegration" \ - --transformer-id "t1" -``` - -Test a transformer: - -```bash -# Test an existing transformer to verify it works correctly -secops integration transformers test \ - --integration-name "MyIntegration" \ - --transformer-id "t1" -``` - -Get transformer template: - -```bash -# Get a boilerplate template for creating a new transformer -secops integration transformers template --integration-name "MyIntegration" -``` - -#### Transformer Revisions - -List transformer revisions: - -```bash -# List all revisions for a transformer -secops integration transformer-revisions list \ - --integration-name "MyIntegration" \ - --transformer-id "t1" - -# List revisions as a direct list -secops integration transformer-revisions list \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --as-list - -# List with pagination -secops integration transformer-revisions list \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --page-size 10 - -# List with filtering and ordering -secops integration transformer-revisions list \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --filter-string "version = '1.0'" \ - --order-by "createTime desc" -``` - -Delete a transformer revision: - -```bash -# Delete a specific revision -secops integration transformer-revisions delete \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --revision-id "rev-456" -``` - -Create a new revision: - -```bash -# Create a backup revision before making changes -secops integration transformer-revisions create \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --comment "Backup before major refactor" - -# Create a revision with descriptive comment -secops integration transformer-revisions create \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --comment "Version 2.0 - Enhanced error handling" -``` - -Rollback to a previous revision: - -```bash -# Rollback transformer to a specific revision -secops integration transformer-revisions rollback \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --revision-id "rev-456" -``` - -Example workflow: Safe transformer updates with revision control: - -```bash -# 1. Create a backup revision -secops integration transformer-revisions create \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --comment "Backup before updating transformation logic" - -# 2. Update the transformer -secops integration transformers update \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --script "def transform(data): return data.upper()" \ - --description "Updated with new transformation" - -# 3. Test the updated transformer -secops integration transformers test \ - --integration-name "MyIntegration" \ - --transformer-id "t1" - -# 4. If test fails, rollback to the backup revision -# First, list revisions to get the backup revision ID -secops integration transformer-revisions list \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --order-by "createTime desc" \ - --page-size 1 - -# Then rollback using the revision ID -secops integration transformer-revisions rollback \ - --integration-name "MyIntegration" \ - --transformer-id "t1" \ - --revision-id "rev-backup-id" -``` - -#### Logical Operators - -List logical operators: - -```bash -# List all logical operators for an integration -secops integration logical-operators list --integration-name "MyIntegration" - -# List logical operators as a direct list -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --as-list - -# List with pagination -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --page-size 50 - -# List with filtering -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --filter-string "enabled = true" - -# Exclude staging logical operators -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --exclude-staging - -# List with expanded details -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --expand "parameters" -``` - -Get logical operator details: - -```bash -# Get basic logical operator details -secops integration logical-operators get \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" - -# Get logical operator with expanded parameters -secops integration logical-operators get \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --expand "parameters" -``` - -Create a new logical operator: - -```bash -# Create a basic equality operator -secops integration logical-operators create \ - --integration-name "MyIntegration" \ - --display-name "Equals Operator" \ - --script "def evaluate(a, b): return a == b" \ - --script-timeout "60s" \ - --enabled - -# Create logical operator with description -secops integration logical-operators create \ - --integration-name "MyIntegration" \ - --display-name "Threshold Checker" \ - --script "def evaluate(value, threshold): return value > threshold" \ - --script-timeout "30s" \ - --description "Checks if value exceeds threshold" \ - --enabled -``` - -> **Note:** When creating a logical operator: -> - `--script-timeout` should be specified with a unit (e.g., "60s", "2m") -> - Use `--enabled` flag to enable the operator on creation (default is disabled) -> - The script must be valid Python code with an `evaluate()` function - -Update an existing logical operator: - -```bash -# Update display name -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --display-name "Updated Operator Name" - -# Update script -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --script "def evaluate(a, b): return a != b" - -# Update multiple fields -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --display-name "Enhanced Operator" \ - --description "Updated with better logic" \ - --script-timeout "45s" \ - --enabled true - -# Update with custom update mask -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --display-name "New Name" \ - --description "New description" \ - --update-mask "displayName,description" -``` - -Delete a logical operator: - -```bash -secops integration logical-operators delete \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" -``` - -Test a logical operator: - -```bash -# Test an existing logical operator to verify it works correctly -secops integration logical-operators test \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" -``` - -Get logical operator template: - -```bash -# Get a boilerplate template for creating a new logical operator -secops integration logical-operators template --integration-name "MyIntegration" -``` - -Example workflow: Building conditional logic: - -```bash -# 1. Get a template to start with -secops integration logical-operators template \ - --integration-name "MyIntegration" - -# 2. Create a severity checker operator -secops integration logical-operators create \ - --integration-name "MyIntegration" \ - --display-name "Severity Level Check" \ - --script "def evaluate(severity, min_level): return severity >= min_level" \ - --script-timeout "30s" \ - --description "Checks if severity meets minimum threshold" - -# 3. Test the operator -secops integration logical-operators test \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" - -# 4. Enable the operator if test passes -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --enabled true - -# 5. List all operators to see what's available -secops integration logical-operators list \ - --integration-name "MyIntegration" \ - --as-list -``` - -#### Logical Operator Revisions - -List logical operator revisions: - -```bash -# List all revisions for a logical operator -secops integration logical-operator-revisions list \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" - -# List revisions as a direct list -secops integration logical-operator-revisions list \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --as-list - -# List with pagination -secops integration logical-operator-revisions list \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --page-size 10 - -# List with filtering and ordering -secops integration logical-operator-revisions list \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --filter-string "version = '1.0'" \ - --order-by "createTime desc" -``` - -Delete a logical operator revision: - -```bash -# Delete a specific revision -secops integration logical-operator-revisions delete \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --revision-id "rev-456" -``` - -Create a new revision: - -```bash -# Create a backup revision before making changes -secops integration logical-operator-revisions create \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --comment "Backup before refactoring evaluation logic" - -# Create a revision with descriptive comment -secops integration logical-operator-revisions create \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --comment "Version 2.0 - Enhanced comparison logic" -``` - -Rollback to a previous revision: - -```bash -# Rollback logical operator to a specific revision -secops integration logical-operator-revisions rollback \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --revision-id "rev-456" -``` - -Example workflow: Safe logical operator updates with revision control: - -```bash -# 1. Create a backup revision -secops integration logical-operator-revisions create \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --comment "Backup before updating conditional logic" - -# 2. Update the logical operator -secops integration logical-operators update \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --script "def evaluate(a, b): return a >= b" \ - --description "Updated with greater-than-or-equal logic" - -# 3. Test the updated logical operator -secops integration logical-operators test \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" - -# 4. If test fails, rollback to the backup revision -# First, list revisions to get the backup revision ID -secops integration logical-operator-revisions list \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --order-by "createTime desc" \ - --page-size 1 - -# Then rollback using the revision ID -secops integration logical-operator-revisions rollback \ - --integration-name "MyIntegration" \ - --logical-operator-id "lo1" \ - --revision-id "rev-backup-id" -``` - ### Rule Management List detection rules: diff --git a/README.md b/README.md index 9a49afe9..ccc72370 100644 --- a/README.md +++ b/README.md @@ -1911,280 +1911,6 @@ for watchlist in watchlists: ## Integration Management -### Marketplace Integrations - -List available marketplace integrations: - -```python -# Get all available marketplace integration -integrations = chronicle.list_marketplace_integrations() -for integration in integrations.get("marketplaceIntegrations", []): - integration_title = integration.get("title") - integration_id = integration.get("name", "").split("/")[-1] - integration_version = integration.get("version", "") - documentation_url = integration.get("documentationUri", "") - -# Get all integration as a list -integrations = chronicle.list_marketplace_integrations(as_list=True) - -# Get all currently installed integration -integrations = chronicle.list_marketplace_integrations(filter_string="installed = true") - -# Get all installed integration with updates available -integrations = chronicle.list_marketplace_integrations(filter_string="installed = true AND updateAvailable = true") - -# Specify use of V1 Alpha API version -integrations = chronicle.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) -``` - -Get a specific marketplace integration: - -```python -integration = chronicle.get_marketplace_integration("AWSSecurityHub") -``` - -Get the diff between the currently installed version and the latest -available version of an integration: - -```python -diff = chronicle.get_marketplace_integration_diff("AWSSecurityHub") -``` - -Install or update a marketplace integration: - -```python -# Install an integration with the default settings -integration_name = "AWSSecurityHub" -integration = chronicle.install_marketplace_integration(integration_name) - -# Install to staging environment and override any existing ontology mappings -integration = chronicle.install_marketplace_integration( - integration_name, - staging=True, - override_ontology_mappings=True -) - -# Installing a currently installed integration with no specified version -# number will update it to the latest version -integration = chronicle.install_marketplace_integration(integration_name) - -# Or you can specify a specific version to install -integration = chronicle.install_marketplace_integration( - integration_name, - version="5.0" -) -``` - -Uninstall a marketplace integration: - -```python -chronicle.uninstall_marketplace_integration("AWSSecurityHub") -``` - -### Integrations -List all installed integrations: - -```python -# Get all integrations -integrations = chronicle.list_integrations() -for i in integrations.get("integrations", []): - integration_id = i["identifier"] - integration_display_name = i["displayName"] - integration_type = i["type"] - -# Get all integrations as a list -integrations = chronicle.list_integrations(as_list=True) - -for i in integrations: - integration = chronicle.get_integration(i["identifier"]) - if integration.get("parameters"): - print(json.dumps(integration, indent=2)) - - -# Get integrations ordered by display name -integrations = chronicle.list_integrations(order_by="displayName", as_list=True) - ``` - -Get details of a specific integration: - -```python -integration = chronicle.get_integration("AWSSecurityHub") -``` - -Create an integration: - -```python -from secops.chronicle.models ( - IntegrationParam, - IntegrationParamType, - IntegrationType, - PythonVersion -) - -integration = chronicle.create_integration( - display_name="MyNewIntegration", - staging=True, - description="This is my integration", - python_version=PythonVersion.PYTHON_3_11, - parameters=[ - IntegrationParam( - display_name="AWS Access Key", - property_name="aws_access_key", - type=IntegrationParamType.STRING, - description="AWS access key for authentication", - mandatory=True, - ), - IntegrationParam( - display_name="AWS Secret Key", - property_name="aws_secret_key", - type=IntegrationParamType.PASSWORD, - description="AWS secret key for authentication", - mandatory=False, - ), - ], - categories=[ - "Cloud Security", - "Cloud", - "Security" - ], - integration_type=IntegrationType.RESPONSE, -) -``` - -Update an integration: - -```python -from secops.chronicle.models import IntegrationParam, IntegrationParamType - -updated_integration = chronicle.update_integration( - integration_name="MyNewIntegration", - display_name="Updated Integration Name", - description="Updated description", - parameters=[ - IntegrationParam( - display_name="AWS Region", - property_name="aws_region", - type=IntegrationParamType.STRING, - description="AWS region to use", - mandatory=True, - ), - ], - categories=[ - "Cloud Security", - "Cloud", - "Security" - ], -) -``` - -Delete an integration: - -```python -chronicle.delete_integration("MyNewIntegration") -``` - -Download an entire integration as a bytes object and save it as a .zip file -This includes all the integration details, parameters, and actions in a format that can be re-uploaded to Chronicle or used for backup purposes. - -```python -integration_bytes = chronicle.download_integration("MyIntegration") -with open("MyIntegration.zip", "wb") as f: - f.write(integration_bytes) -``` - -Export selected items from an integration (e.g. only actions) as a .zip file: - -```python -# Export only actions with IDs 1 and 2 from the integration - -export_bytes = chronicle.export_integration_items( - integration_name="AWSSecurityHub", - actions=["1", "2"] # IDs of the actions to export -) -with open("AWSSecurityHub_FullExport.zip", "wb") as f: - f.write(export_bytes) -``` - -Get dependencies for an integration: - -```python -dependencies = chronicle.get_integration_dependencies("AWSSecurityHub") -for dep in dependencies.get("dependencies", []): - parts = dep.split("-") - dependency_name = parts[0] - dependency_version = parts[1] if len(parts) > 1 else "latest" - print(f"Dependency: {dependency_name}, Version: {dependency_version}") -``` - -Force dependency update for an integration: - -```python -# Defining a version: -chronicle.download_integration_dependency( - "MyIntegration", - "boto3==1.42.44" -) - -# Install the latest version of a dependency: -chronicle.download_integration_dependency( - "MyIntegration", - "boto3" -) -``` - -Get remote agents that would be restricted from running an updated version of the integration - -```python -from secops.chronicle.models import PythonVersion - -agents = chronicle.get_integration_restricted_agents( - integration_name="AWSSecurityHub", - required_python_version=PythonVersion.PYTHON_3_11, -) -``` - -Get integration diff between two versions of an integration: - -```python -from secops.chronicle.models import DiffType - -# Get the diff between the commercial version of the integration and the current version in the environment. -diff = chronicle.get_integration_diff( - integration_name="AWSSecurityHub", - diff_type=DiffType.COMMERCIAL -) - -# Get the difference between the staging integration and its matching production version. -diff = chronicle.get_integration_diff( - integration_name="AWSSecurityHub", - diff_type=DiffType.PRODUCTION -) - -# Get the difference between the production integration and its corresponding staging version. -diff = chronicle.get_integration_diff( - integration_name="AWSSecurityHub", - diff_type=DiffType.STAGING -) -``` - -Transition an integration to staging or production environment: - -```python -from secops.chronicle.models import TargetMode - -# Transition to staging environment -chronicle.transition_integration_environment( - integration_name="AWSSecurityHub", - target_mode=TargetMode.STAGING -) - -# Transition to production environment -chronicle.transition_integration_environment( - integration_name="AWSSecurityHub", - target_mode=TargetMode.PRODUCTION -) -``` - ### Integration Actions List all available actions for an integration: @@ -2427,2784 +2153,185 @@ else: print("Test passed - changes saved") ``` -### Integration Connectors +### Integration Managers -List all available connectors for an integration: +List all available managers for an integration: ```python -# Get all connectors for an integration -connectors = chronicle.list_integration_connectors("AWSSecurityHub") +# Get all managers for an integration +managers = chronicle.list_integration_managers("MyIntegration") +for manager in managers.get("managers", []): + print(f"Manager: {manager.get('displayName')}, ID: {manager.get('name')}") -# Get all connectors as a list -connectors = chronicle.list_integration_connectors("AWSSecurityHub", as_list=True) +# Get all managers as a list +managers = chronicle.list_integration_managers("MyIntegration", as_list=True) -# Get only enabled connectors -connectors = chronicle.list_integration_connectors( - "AWSSecurityHub", - filter_string="enabled = true" +# Filter managers by display name +managers = chronicle.list_integration_managers( + "MyIntegration", + filter_string='displayName = "API Helper"' ) -# Exclude staging connectors -connectors = chronicle.list_integration_connectors( - "AWSSecurityHub", - exclude_staging=True +# Sort managers by display name +managers = chronicle.list_integration_managers( + "MyIntegration", + order_by="displayName" ) ``` -Get details of a specific connector: +Get details of a specific manager: ```python -connector = chronicle.get_integration_connector( - integration_name="AWSSecurityHub", - connector_id="123" +manager = chronicle.get_integration_manager( + integration_name="MyIntegration", + manager_id="123" ) ``` -Create an integration connector: +Create an integration manager: ```python -from secops.chronicle.models import ( - ConnectorParameter, - ParamType, - ConnectorParamMode, - ConnectorRule, - ConnectorRuleType -) +new_manager = chronicle.create_integration_manager( + integration_name="MyIntegration", + display_name="API Helper", + description="Shared utility functions for API calls", + script=""" +def make_api_request(url, headers=None): + '''Helper function to make API requests''' + import requests + return requests.get(url, headers=headers) -new_connector = chronicle.create_integration_connector( - integration_name="MyIntegration", - display_name="New Connector", - description="This is a new connector", - script="print('Fetching data...')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event_type", - parameters=[ - ConnectorParameter( - display_name="API Key", - type=ParamType.PASSWORD, - mode=ConnectorParamMode.CONNECTIVITY, - mandatory=True, - description="API key for authentication" - ) - ], - rules=[ - ConnectorRule( - display_name="Allow List", - type=ConnectorRuleType.ALLOW_LIST - ) - ] +def parse_response(response): + '''Parse API response''' + return response.json() +""" ) ``` -Update an integration connector: +Update an integration manager: ```python -from secops.chronicle.models import ( - ConnectorParameter, - ParamType, - ConnectorParamMode -) - -updated_connector = chronicle.update_integration_connector( - integration_name="MyIntegration", - connector_id="123", - display_name="Updated Connector Name", - description="Updated description", - enabled=False, - timeout_seconds=600, - parameters=[ - ConnectorParameter( - display_name="API Token", - type=ParamType.PASSWORD, - mode=ConnectorParamMode.CONNECTIVITY, - mandatory=True, - description="Updated authentication token" - ) - ], - script="print('Updated connector script')" +updated_manager = chronicle.update_integration_manager( + integration_name="MyIntegration", + manager_id="123", + display_name="Updated API Helper", + description="Updated shared utility functions", + script=""" +def make_api_request(url, headers=None, method='GET'): + '''Updated helper function with method parameter''' + import requests + if method == 'GET': + return requests.get(url, headers=headers) + elif method == 'POST': + return requests.post(url, headers=headers) +""" ) -``` - -Delete an integration connector: -```python -chronicle.delete_integration_connector( +# Update only specific fields +updated_manager = chronicle.update_integration_manager( integration_name="MyIntegration", - connector_id="123" + manager_id="123", + description="New description only" ) ``` -Execute a test run of an integration connector: +Delete an integration manager: ```python -# Test a connector before saving it -connector_config = { - "displayName": "Test Connector", - "script": "print('Testing connector')", - "enabled": True, - "timeoutSeconds": 300, - "productFieldName": "product", - "eventFieldName": "event_type" -} - -test_result = chronicle.execute_integration_connector_test( - integration_name="MyIntegration", - connector=connector_config -) - -print(f"Output: {test_result.get('outputMessage')}") -print(f"Debug: {test_result.get('debugOutputMessage')}") - -# Test with a specific agent for remote execution -test_result = chronicle.execute_integration_connector_test( +chronicle.delete_integration_manager( integration_name="MyIntegration", - connector=connector_config, - agent_identifier="agent-123" + manager_id="123" ) ``` -Get a template for creating a connector in an integration: +Get a template for creating a manager in an integration: ```python -template = chronicle.get_integration_connector_template("MyIntegration") +template = chronicle.get_integration_manager_template("MyIntegration") print(f"Template script: {template.get('script')}") ``` -### Integration Connector Revisions +### Integration Manager Revisions -List all revisions for a specific integration connector: +List all revisions for a specific manager: ```python -# Get all revisions for a connector -revisions = chronicle.list_integration_connector_revisions( +# Get all revisions for a manager +revisions = chronicle.list_integration_manager_revisions( integration_name="MyIntegration", - connector_id="c1" + manager_id="123" ) for revision in revisions.get("revisions", []): - print(f"Revision ID: {revision.get('name')}") - print(f"Comment: {revision.get('comment')}") - print(f"Created: {revision.get('createTime')}") + print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") # Get all revisions as a list -revisions = chronicle.list_integration_connector_revisions( +revisions = chronicle.list_integration_manager_revisions( integration_name="MyIntegration", - connector_id="c1", + manager_id="123", as_list=True ) -# Filter revisions with order -revisions = chronicle.list_integration_connector_revisions( +# Filter revisions +revisions = chronicle.list_integration_manager_revisions( integration_name="MyIntegration", - connector_id="c1", - order_by="createTime desc", - page_size=10 + manager_id="123", + filter_string='comment contains "backup"', + order_by="createTime desc" ) ``` -Delete a specific connector revision: +Get details of a specific revision: ```python -# Clean up old revision from version history -chronicle.delete_integration_connector_revision( +revision = chronicle.get_integration_manager_revision( integration_name="MyIntegration", - connector_id="c1", + manager_id="123", revision_id="r1" ) +print(f"Revision script: {revision.get('manager', {}).get('script')}") ``` -Create a new connector revision snapshot: +Create a new revision snapshot: ```python -# Get the current connector configuration -connector = chronicle.get_integration_connector( - integration_name="MyIntegration", - connector_id="c1" -) - -# Create a revision without comment -new_revision = chronicle.create_integration_connector_revision( - integration_name="MyIntegration", - connector_id="c1", - connector=connector -) - -# Create a revision with descriptive comment -new_revision = chronicle.create_integration_connector_revision( +# Get the current manager +manager = chronicle.get_integration_manager( integration_name="MyIntegration", - connector_id="c1", - connector=connector, - comment="Stable version before adding new field mapping" + manager_id="123" ) -print(f"Created revision: {new_revision.get('name')}") -``` - -Rollback a connector to a previous revision: - -```python -# Revert to a known good configuration -rolled_back = chronicle.rollback_integration_connector_revision( +# Create a revision before making changes +revision = chronicle.create_integration_manager_revision( integration_name="MyIntegration", - connector_id="c1", - revision_id="r1" + manager_id="123", + manager=manager, + comment="Backup before major refactor" ) - -print(f"Rolled back to revision: {rolled_back.get('name')}") -print(f"Connector script restored") +print(f"Created revision: {revision.get('name')}") ``` -Example workflow: Safe connector updates with revisions: +Rollback to a previous revision: ```python -# 1. Get current connector -connector = chronicle.get_integration_connector( - integration_name="MyIntegration", - connector_id="c1" -) - -# 2. Create backup revision before changes -backup = chronicle.create_integration_connector_revision( - integration_name="MyIntegration", - connector_id="c1", - connector=connector, - comment="Backup before timeout increase" -) -print(f"Backup created: {backup.get('name')}") - -# 3. Update the connector -updated_connector = chronicle.update_integration_connector( - integration_name="MyIntegration", - connector_id="c1", - timeout_seconds=600, - description="Increased timeout for large data pulls" -) - -# 4. Test the updated connector -test_result = chronicle.execute_integration_connector_test( +# Rollback to a previous working version +rollback_result = chronicle.rollback_integration_manager_revision( integration_name="MyIntegration", - connector=updated_connector + manager_id="123", + revision_id="acb123de-abcd-1234-ef00-1234567890ab" ) - -# 5. If test fails, rollback to the backup -if not test_result.get("outputMessage"): - print("Test failed, rolling back...") - chronicle.rollback_integration_connector_revision( - integration_name="MyIntegration", - connector_id="c1", - revision_id=backup.get("name").split("/")[-1] - ) - print("Rollback complete") -else: - print("Test passed, changes applied successfully") +print(f"Rolled back to: {rollback_result.get('name')}") ``` -### Connector Context Properties - -List all context properties for a specific connector: +Delete a revision: ```python -# Get all context properties for a connector -context_properties = chronicle.list_connector_context_properties( - integration_name="MyIntegration", - connector_id="c1" -) -for prop in context_properties.get("contextProperties", []): - print(f"Key: {prop.get('key')}, Value: {prop.get('value')}") - -# Get all context properties as a list -context_properties = chronicle.list_connector_context_properties( - integration_name="MyIntegration", - connector_id="c1", - as_list=True -) - -# Filter context properties -context_properties = chronicle.list_connector_context_properties( +chronicle.delete_integration_manager_revision( integration_name="MyIntegration", - connector_id="c1", - filter_string='key = "last_run_time"', - order_by="key" + manager_id="123", + revision_id="r1" ) ``` -Get a specific context property: - -```python -property_value = chronicle.get_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - context_property_id="last_run_time" -) -print(f"Value: {property_value.get('value')}") -``` - -Create a new context property: - -```python -# Create context property with auto-generated key -new_property = chronicle.create_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - value="2026-03-09T10:00:00Z" -) - -# Create context property with custom key -new_property = chronicle.create_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - value="2026-03-09T10:00:00Z", - key="last-sync-time" -) -print(f"Created property: {new_property.get('name')}") -``` - -Update an existing context property: - -```python -# Update property value -updated_property = chronicle.update_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - context_property_id="last-sync-time", - value="2026-03-09T11:00:00Z" -) -print(f"Updated value: {updated_property.get('value')}") -``` - -Delete a context property: - -```python -chronicle.delete_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - context_property_id="last-sync-time" -) -``` - -Delete all context properties: - -```python -# Clear all properties for a connector -chronicle.delete_all_connector_context_properties( - integration_name="MyIntegration", - connector_id="c1" -) - -# Clear all properties for a specific context ID -chronicle.delete_all_connector_context_properties( - integration_name="MyIntegration", - connector_id="c1", - context_id="my-context" -) -``` - -Example workflow: Track connector state with context properties: - -```python -# 1. Check if we have a last run time stored -try: - last_run = chronicle.get_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - context_property_id="last-run-time" - ) - print(f"Last run: {last_run.get('value')}") -except APIError: - print("No previous run time found") - # Create initial property - chronicle.create_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - value="2026-01-01T00:00:00Z", - key="last-run-time" - ) - -# 2. Run the connector and process data -# ... connector execution logic ... - -# 3. Update the last run time after successful execution -from datetime import datetime -current_time = datetime.utcnow().isoformat() + "Z" -chronicle.update_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - context_property_id="last-run-time", - value=current_time -) - -# 4. Store additional context like record count -chronicle.create_connector_context_property( - integration_name="MyIntegration", - connector_id="c1", - value="1500", - key="records-processed" -) - -# 5. List all context to see connector state -all_context = chronicle.list_connector_context_properties( - integration_name="MyIntegration", - connector_id="c1", - as_list=True -) -for prop in all_context: - print(f"{prop.get('key')}: {prop.get('value')}") -``` - -### Connector Instance Logs - -List all execution logs for a connector instance: - -```python -# Get all logs for a connector instance -logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1" -) -for log in logs.get("logs", []): - print(f"Log ID: {log.get('name')}, Severity: {log.get('severity')}") - print(f"Timestamp: {log.get('timestamp')}") - print(f"Message: {log.get('message')}") - -# Get all logs as a list -logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - as_list=True -) - -# Filter logs by severity -logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - filter_string='severity = "ERROR"', - order_by="timestamp desc" -) -``` - -Get a specific log entry: - -```python -log_entry = chronicle.get_connector_instance_log( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - log_id="log123" -) -print(f"Severity: {log_entry.get('severity')}") -print(f"Timestamp: {log_entry.get('timestamp')}") -print(f"Message: {log_entry.get('message')}") -``` - -Monitor connector execution and troubleshooting: - -```python -# Get recent logs for monitoring -recent_logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - order_by="timestamp desc", - page_size=10, - as_list=True -) - -# Check for errors -for log in recent_logs: - if log.get("severity") in ["ERROR", "CRITICAL"]: - print(f"Error at {log.get('timestamp')}") - log_details = chronicle.get_connector_instance_log( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - log_id=log.get("name").split("/")[-1] - ) - print(f"Error message: {log_details.get('message')}") -``` - -Analyze connector performance and reliability: - -```python -# Get all logs to calculate error rate -all_logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - as_list=True -) - -errors = sum(1 for log in all_logs if log.get("severity") in ["ERROR", "CRITICAL"]) -warnings = sum(1 for log in all_logs if log.get("severity") == "WARNING") -total = len(all_logs) - -if total > 0: - error_rate = (errors / total) * 100 - print(f"Error Rate: {error_rate:.2f}%") - print(f"Total Logs: {total}") - print(f"Errors: {errors}, Warnings: {warnings}") -``` - -### Connector Instances - -List all connector instances for a specific connector: - -```python -# Get all instances for a connector -instances = chronicle.list_connector_instances( - integration_name="MyIntegration", - connector_id="c1" -) -for instance in instances.get("connectorInstances", []): - print(f"Instance: {instance.get('displayName')}, Enabled: {instance.get('enabled')}") - -# Get all instances as a list -instances = chronicle.list_connector_instances( - integration_name="MyIntegration", - connector_id="c1", - as_list=True -) - -# Filter instances -instances = chronicle.list_connector_instances( - integration_name="MyIntegration", - connector_id="c1", - filter_string='enabled = true', - order_by="displayName" -) -``` - -Get a specific connector instance: - -```python -instance = chronicle.get_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1" -) -print(f"Display Name: {instance.get('displayName')}") -print(f"Environment: {instance.get('environment')}") -print(f"Interval: {instance.get('intervalSeconds')} seconds") -``` - -Create a new connector instance: - -```python -from secops.chronicle.models import ConnectorInstanceParameter - -# Create basic connector instance -new_instance = chronicle.create_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - environment="production", - display_name="Production Instance", - interval_seconds=3600, # Run every hour - timeout_seconds=300, # 5 minute timeout - enabled=True -) - -# Create instance with parameters -param = ConnectorInstanceParameter() -param.value = "my-api-key" - -new_instance = chronicle.create_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - environment="production", - display_name="Production Instance", - interval_seconds=3600, - timeout_seconds=300, - description="Main production connector instance", - parameters=[param], - enabled=True -) -print(f"Created instance: {new_instance.get('name')}") -``` - -Update an existing connector instance: - -```python -# Update display name -updated_instance = chronicle.update_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated Production Instance" -) - -# Update multiple fields including parameters -param = ConnectorInstanceParameter() -param.value = "new-api-key" - -updated_instance = chronicle.update_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated Instance", - interval_seconds=7200, # Change to every 2 hours - parameters=[param], - enabled=True -) -``` - -Delete a connector instance: - -```python -chronicle.delete_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1" -) -``` - -Refresh instance with latest connector definition: - -```python -# Fetch latest definition from marketplace -refreshed_instance = chronicle.get_connector_instance_latest_definition( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1" -) -print(f"Updated to latest definition") -``` - -Enable/disable logs collection for debugging: - -```python -# Enable logs collection -result = chronicle.set_connector_instance_logs_collection( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - enabled=True -) -print(f"Logs enabled until: {result.get('loggingEnabledUntilUnixMs')}") - -# Disable logs collection -chronicle.set_connector_instance_logs_collection( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - enabled=False -) -``` - -Run a connector instance on demand for testing: - -```python -# Get the current instance configuration -instance = chronicle.get_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1" -) - -# Run on demand to test configuration -test_result = chronicle.run_connector_instance_on_demand( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id="ci1", - connector_instance=instance -) - -if test_result.get("success"): - print("Test execution successful!") - print(f"Debug output: {test_result.get('debugOutput')}") -else: - print("Test execution failed") - print(f"Error: {test_result.get('debugOutput')}") -``` - -Example workflow: Deploy and test a new connector instance: - -```python -from secops.chronicle.models import ConnectorInstanceParameter - -# 1. Create a new connector instance -param = ConnectorInstanceParameter() -param.value = "test-api-key" - -new_instance = chronicle.create_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - environment="development", - display_name="Dev Test Instance", - interval_seconds=3600, - timeout_seconds=300, - description="Development testing instance", - parameters=[param], - enabled=False # Start disabled for testing -) - -instance_id = new_instance.get("name").split("/")[-1] -print(f"Created instance: {instance_id}") - -# 2. Enable logs collection for debugging -chronicle.set_connector_instance_logs_collection( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id=instance_id, - enabled=True -) - -# 3. Run on demand to test -test_result = chronicle.run_connector_instance_on_demand( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id=instance_id, - connector_instance=new_instance -) - -# 4. Check test results -if test_result.get("success"): - print("✓ Test passed - enabling instance") - # Enable the instance for scheduled runs - chronicle.update_connector_instance( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id=instance_id, - enabled=True - ) -else: - print("✗ Test failed - reviewing logs") - # Get logs to debug the issue - logs = chronicle.list_connector_instance_logs( - integration_name="MyIntegration", - connector_id="c1", - connector_instance_id=instance_id, - filter_string='severity = "ERROR"', - as_list=True - ) - for log in logs: - print(f"Error: {log.get('message')}") - -# 5. Monitor execution after enabling -instances = chronicle.list_connector_instances( - integration_name="MyIntegration", - connector_id="c1", - filter_string=f'name = "{new_instance.get("name")}"', - as_list=True -) -if instances: - print(f"Instance status: Enabled={instances[0].get('enabled')}") -``` - -### Integration Jobs - -List all available jobs for an integration: - -```python -# Get all jobs for an integration -jobs = chronicle.list_integration_jobs("MyIntegration") -for job in jobs.get("jobs", []): - print(f"Job: {job.get('displayName')}, ID: {job.get('name')}") - -# Get all jobs as a list -jobs = chronicle.list_integration_jobs("MyIntegration", as_list=True) - -# Get only custom jobs -jobs = chronicle.list_integration_jobs( - "MyIntegration", - filter_string="custom = true" -) - -# Exclude staging jobs -jobs = chronicle.list_integration_jobs( - "MyIntegration", - exclude_staging=True -) -``` - -Get details of a specific job: - -```python -job = chronicle.get_integration_job( - integration_name="MyIntegration", - job_id="123" -) -``` - -Create an integration job: - -```python -from secops.chronicle.models import JobParameter, ParamType - -new_job = chronicle.create_integration_job( - integration_name="MyIntegration", - display_name="Scheduled Sync Job", - description="Syncs data from external source", - script="print('Running scheduled job...')", - version=1, - enabled=True, - custom=True, - parameters=[ - JobParameter( - id=1, - display_name="Sync Interval", - description="Interval in minutes", - type=ParamType.INT, - mandatory=True, - default_value="60" - ) - ] -) -``` - -Update an integration job: - -```python -from secops.chronicle.models import JobParameter, ParamType - -updated_job = chronicle.update_integration_job( - integration_name="MyIntegration", - job_id="123", - display_name="Updated Job Name", - description="Updated description", - enabled=False, - version=2, - parameters=[ - JobParameter( - id=1, - display_name="New Parameter", - description="Updated parameter", - type=ParamType.STRING, - mandatory=True, - ) - ], - script="print('Updated job script')" -) -``` - -Delete an integration job: - -```python -chronicle.delete_integration_job( - integration_name="MyIntegration", - job_id="123" -) -``` - -Execute a test run of an integration job: - -```python -# Test a job before saving it -job = chronicle.get_integration_job( - integration_name="MyIntegration", - job_id="123" -) - -test_result = chronicle.execute_integration_job_test( - integration_name="MyIntegration", - job=job -) - -print(f"Output: {test_result.get('output')}") -print(f"Debug: {test_result.get('debugOutput')}") - -# Test with a specific agent for remote execution -test_result = chronicle.execute_integration_job_test( - integration_name="MyIntegration", - job=job, - agent_identifier="agent-123" -) -``` - -Get a template for creating a job in an integration: - -```python -template = chronicle.get_integration_job_template("MyIntegration") -print(f"Template script: {template.get('script')}") -``` - -### Integration Managers - -List all available managers for an integration: - -```python -# Get all managers for an integration -managers = chronicle.list_integration_managers("MyIntegration") -for manager in managers.get("managers", []): - print(f"Manager: {manager.get('displayName')}, ID: {manager.get('name')}") - -# Get all managers as a list -managers = chronicle.list_integration_managers("MyIntegration", as_list=True) - -# Filter managers by display name -managers = chronicle.list_integration_managers( - "MyIntegration", - filter_string='displayName = "API Helper"' -) - -# Sort managers by display name -managers = chronicle.list_integration_managers( - "MyIntegration", - order_by="displayName" -) -``` - -Get details of a specific manager: - -```python -manager = chronicle.get_integration_manager( - integration_name="MyIntegration", - manager_id="123" -) -``` - -Create an integration manager: - -```python -new_manager = chronicle.create_integration_manager( - integration_name="MyIntegration", - display_name="API Helper", - description="Shared utility functions for API calls", - script=""" -def make_api_request(url, headers=None): - '''Helper function to make API requests''' - import requests - return requests.get(url, headers=headers) - -def parse_response(response): - '''Parse API response''' - return response.json() -""" -) -``` - -Update an integration manager: - -```python -updated_manager = chronicle.update_integration_manager( - integration_name="MyIntegration", - manager_id="123", - display_name="Updated API Helper", - description="Updated shared utility functions", - script=""" -def make_api_request(url, headers=None, method='GET'): - '''Updated helper function with method parameter''' - import requests - if method == 'GET': - return requests.get(url, headers=headers) - elif method == 'POST': - return requests.post(url, headers=headers) -""" -) - -# Update only specific fields -updated_manager = chronicle.update_integration_manager( - integration_name="MyIntegration", - manager_id="123", - description="New description only" -) -``` - -Delete an integration manager: - -```python -chronicle.delete_integration_manager( - integration_name="MyIntegration", - manager_id="123" -) -``` - -Get a template for creating a manager in an integration: - -```python -template = chronicle.get_integration_manager_template("MyIntegration") -print(f"Template script: {template.get('script')}") -``` - -### Integration Manager Revisions - -List all revisions for a specific manager: - -```python -# Get all revisions for a manager -revisions = chronicle.list_integration_manager_revisions( - integration_name="MyIntegration", - manager_id="123" -) -for revision in revisions.get("revisions", []): - print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") - -# Get all revisions as a list -revisions = chronicle.list_integration_manager_revisions( - integration_name="MyIntegration", - manager_id="123", - as_list=True -) - -# Filter revisions -revisions = chronicle.list_integration_manager_revisions( - integration_name="MyIntegration", - manager_id="123", - filter_string='comment contains "backup"', - order_by="createTime desc" -) -``` - -Get details of a specific revision: - -```python -revision = chronicle.get_integration_manager_revision( - integration_name="MyIntegration", - manager_id="123", - revision_id="r1" -) -print(f"Revision script: {revision.get('manager', {}).get('script')}") -``` - -Create a new revision snapshot: - -```python -# Get the current manager -manager = chronicle.get_integration_manager( - integration_name="MyIntegration", - manager_id="123" -) - -# Create a revision before making changes -revision = chronicle.create_integration_manager_revision( - integration_name="MyIntegration", - manager_id="123", - manager=manager, - comment="Backup before major refactor" -) -print(f"Created revision: {revision.get('name')}") -``` - -Rollback to a previous revision: - -```python -# Rollback to a previous working version -rollback_result = chronicle.rollback_integration_manager_revision( - integration_name="MyIntegration", - manager_id="123", - revision_id="acb123de-abcd-1234-ef00-1234567890ab" -) -print(f"Rolled back to: {rollback_result.get('name')}") -``` - -Delete a revision: - -```python -chronicle.delete_integration_manager_revision( - integration_name="MyIntegration", - manager_id="123", - revision_id="r1" -) -``` - -### Integration Job Revisions - -List all revisions for a specific job: - -```python -# Get all revisions for a job -revisions = chronicle.list_integration_job_revisions( - integration_name="MyIntegration", - job_id="456" -) -for revision in revisions.get("revisions", []): - print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") - -# Get all revisions as a list -revisions = chronicle.list_integration_job_revisions( - integration_name="MyIntegration", - job_id="456", - as_list=True -) - -# Filter revisions by version -revisions = chronicle.list_integration_job_revisions( - integration_name="MyIntegration", - job_id="456", - filter_string='version = "2"', - order_by="createTime desc" -) -``` - -Delete a job revision: - -```python -chronicle.delete_integration_job_revision( - integration_name="MyIntegration", - job_id="456", - revision_id="r2" -) -``` - -Create a new job revision snapshot: - -```python -# Get the current job -job = chronicle.get_integration_job( - integration_name="MyIntegration", - job_id="456" -) - -# Create a revision before making changes -revision = chronicle.create_integration_job_revision( - integration_name="MyIntegration", - job_id="456", - job=job, - comment="Backup before scheduled update" -) -print(f"Created revision: {revision.get('name')}") -``` - -Rollback to a previous job revision: - -```python -# Rollback to a previous working version -rollback_result = chronicle.rollback_integration_job_revision( - integration_name="MyIntegration", - job_id="456", - revision_id="r2" -) -print(f"Rolled back to: {rollback_result.get('name')}") -``` - -### Integration Job Instances - -List all job instances for a specific job: - -```python -# Get all job instances for a job -job_instances = chronicle.list_integration_job_instances( - integration_name="MyIntegration", - job_id="456" -) -for instance in job_instances.get("jobInstances", []): - print(f"Instance: {instance.get('displayName')}, Enabled: {instance.get('enabled')}") - -# Get all job instances as a list -job_instances = chronicle.list_integration_job_instances( - integration_name="MyIntegration", - job_id="456", - as_list=True -) - -# Filter job instances -job_instances = chronicle.list_integration_job_instances( - integration_name="MyIntegration", - job_id="456", - filter_string="enabled = true", - order_by="displayName" -) -``` - -Get details of a specific job instance: - -```python -job_instance = chronicle.get_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1" -) -print(f"Interval: {job_instance.get('intervalSeconds')} seconds") -``` - -Create a new job instance: - -```python -from secops.chronicle.models import IntegrationJobInstanceParameter - -# Create a job instance with basic scheduling (interval-based) -new_job_instance = chronicle.create_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - display_name="Daily Data Sync", - description="Syncs data from external source daily", - interval_seconds=86400, # 24 hours - enabled=True, - advanced=False, - parameters=[ - IntegrationJobInstanceParameter(value="production"), - IntegrationJobInstanceParameter(value="https://api.example.com") - ] -) -``` - -Create a job instance with advanced scheduling: - -```python -from secops.chronicle.models import ( - AdvancedConfig, - ScheduleType, - DailyScheduleDetails, - Date, - TimeOfDay -) - -# Create with daily schedule -advanced_job_instance = chronicle.create_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - display_name="Daily Backup at 2 AM", - interval_seconds=86400, - enabled=True, - advanced=True, - advanced_config=AdvancedConfig( - time_zone="America/New_York", - schedule_type=ScheduleType.DAILY, - daily_schedule=DailyScheduleDetails( - start_date=Date(year=2025, month=1, day=1), - time=TimeOfDay(hours=2, minutes=0), - interval=1 # Every 1 day - ) - ), - agent="agent-123" # For remote execution -) -``` - -Create a job instance with weekly schedule: - -```python -from secops.chronicle.models import ( - AdvancedConfig, - ScheduleType, - WeeklyScheduleDetails, - DayOfWeek, - Date, - TimeOfDay -) - -# Run every Monday and Friday at 9 AM -weekly_job_instance = chronicle.create_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - display_name="Weekly Report", - interval_seconds=604800, # 1 week - enabled=True, - advanced=True, - advanced_config=AdvancedConfig( - time_zone="UTC", - schedule_type=ScheduleType.WEEKLY, - weekly_schedule=WeeklyScheduleDetails( - start_date=Date(year=2025, month=1, day=1), - days=[DayOfWeek.MONDAY, DayOfWeek.FRIDAY], - time=TimeOfDay(hours=9, minutes=0), - interval=1 # Every 1 week - ) - ) -) -``` - -Create a job instance with monthly schedule: - -```python -from secops.chronicle.models import ( - AdvancedConfig, - ScheduleType, - MonthlyScheduleDetails, - Date, - TimeOfDay -) - -# Run on the 1st of every month at midnight -monthly_job_instance = chronicle.create_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - display_name="Monthly Cleanup", - interval_seconds=2592000, # ~30 days - enabled=True, - advanced=True, - advanced_config=AdvancedConfig( - time_zone="America/Los_Angeles", - schedule_type=ScheduleType.MONTHLY, - monthly_schedule=MonthlyScheduleDetails( - start_date=Date(year=2025, month=1, day=1), - day=1, # Day of month (1-31) - time=TimeOfDay(hours=0, minutes=0), - interval=1 # Every 1 month - ) - ) -) -``` - -Create a one-time job instance: - -```python -from secops.chronicle.models import ( - AdvancedConfig, - ScheduleType, - OneTimeScheduleDetails, - Date, - TimeOfDay -) - -# Run once at a specific date and time -onetime_job_instance = chronicle.create_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - display_name="One-Time Migration", - interval_seconds=0, # Not used for one-time - enabled=True, - advanced=True, - advanced_config=AdvancedConfig( - time_zone="Europe/London", - schedule_type=ScheduleType.ONCE, - one_time_schedule=OneTimeScheduleDetails( - start_date=Date(year=2025, month=12, day=25), - time=TimeOfDay(hours=10, minutes=30) - ) - ) -) -``` - -Update a job instance: - -```python -from secops.chronicle.models import IntegrationJobInstanceParameter - -# Update scheduling and enable/disable -updated_instance = chronicle.update_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - display_name="Updated Sync Job", - interval_seconds=43200, # 12 hours - enabled=False -) - -# Update parameters -updated_instance = chronicle.update_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - parameters=[ - IntegrationJobInstanceParameter(value="staging"), - IntegrationJobInstanceParameter(value="https://staging-api.example.com") - ] -) - -# Update to use advanced scheduling -from secops.chronicle.models import ( - AdvancedConfig, - ScheduleType, - DailyScheduleDetails, - Date, - TimeOfDay -) - -updated_instance = chronicle.update_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - advanced=True, - advanced_config=AdvancedConfig( - time_zone="UTC", - schedule_type=ScheduleType.DAILY, - daily_schedule=DailyScheduleDetails( - start_date=Date(year=2025, month=1, day=1), - time=TimeOfDay(hours=12, minutes=0), - interval=1 - ) - ) -) - -# Update only specific fields -updated_instance = chronicle.update_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - enabled=True, - update_mask="enabled" -) -``` - -Delete a job instance: - -```python -chronicle.delete_integration_job_instance( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1" -) -``` - -Run a job instance on demand: - -```python -# Run immediately without waiting for schedule -result = chronicle.run_integration_job_instance_on_demand( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1" -) -print(f"Job execution started: {result}") - -# Run with parameter overrides -result = chronicle.run_integration_job_instance_on_demand( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - parameters=[ - IntegrationJobInstanceParameter(id=1, value="test-mode") - ] -) -``` - -### Job Context Properties - -List all context properties for a job: - -```python -# Get all context properties for a job -context_properties = chronicle.list_job_context_properties( - integration_name="MyIntegration", - job_id="456" -) -for prop in context_properties.get("contextProperties", []): - print(f"Key: {prop.get('key')}, Value: {prop.get('value')}") - -# Get all context properties as a list -context_properties = chronicle.list_job_context_properties( - integration_name="MyIntegration", - job_id="456", - as_list=True -) - -# Filter context properties -context_properties = chronicle.list_job_context_properties( - integration_name="MyIntegration", - job_id="456", - filter_string='key = "api-token"', - order_by="key" -) -``` - -Get a specific context property: - -```python -property_value = chronicle.get_job_context_property( - integration_name="MyIntegration", - job_id="456", - context_property_id="api-endpoint" -) -print(f"Value: {property_value.get('value')}") -``` - -Create a new context property: - -```python -# Create with auto-generated key -new_property = chronicle.create_job_context_property( - integration_name="MyIntegration", - job_id="456", - value="https://api.example.com/v2" -) -print(f"Created property: {new_property.get('key')}") - -# Create with custom key (must be 4-63 chars, match /[a-z][0-9]-/) -new_property = chronicle.create_job_context_property( - integration_name="MyIntegration", - job_id="456", - value="my-secret-token", - key="apitoken" -) -``` - -Update a context property: - -```python -# Update the value of an existing property -updated_property = chronicle.update_job_context_property( - integration_name="MyIntegration", - job_id="456", - context_property_id="api-endpoint", - value="https://api.example.com/v3" -) -print(f"Updated to: {updated_property.get('value')}") -``` - -Delete a context property: - -```python -chronicle.delete_job_context_property( - integration_name="MyIntegration", - job_id="456", - context_property_id="api-endpoint" -) -``` - -Delete all context properties: - -```python -# Clear all context properties for a job -chronicle.delete_all_job_context_properties( - integration_name="MyIntegration", - job_id="456" -) - -# Clear all properties for a specific context ID -chronicle.delete_all_job_context_properties( - integration_name="MyIntegration", - job_id="456", - context_id="mycontext" -) -``` - -### Job Instance Logs - -List all execution logs for a job instance: - -```python -# Get all logs for a job instance -logs = chronicle.list_job_instance_logs( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1" -) -for log in logs.get("logs", []): - print(f"Log ID: {log.get('name')}, Status: {log.get('status')}") - print(f"Start: {log.get('startTime')}, End: {log.get('endTime')}") - -# Get all logs as a list -logs = chronicle.list_job_instance_logs( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - as_list=True -) - -# Filter logs by status -logs = chronicle.list_job_instance_logs( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - filter_string="status = SUCCESS", - order_by="startTime desc" -) -``` - -Get a specific log entry: - -```python -log_entry = chronicle.get_job_instance_log( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - log_id="log123" -) -print(f"Status: {log_entry.get('status')}") -print(f"Start Time: {log_entry.get('startTime')}") -print(f"End Time: {log_entry.get('endTime')}") -print(f"Output: {log_entry.get('output')}") -``` - -Browse historical execution logs to monitor job performance: - -```python -# Get recent logs for monitoring -recent_logs = chronicle.list_job_instance_logs( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - order_by="startTime desc", - page_size=10, - as_list=True -) - -# Check for failures -for log in recent_logs: - if log.get("status") == "FAILED": - print(f"Failed execution at {log.get('startTime')}") - log_details = chronicle.get_job_instance_log( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - log_id=log.get("name").split("/")[-1] - ) - print(f"Error output: {log_details.get('output')}") -``` - -Monitor job reliability and performance: - -```python -# Get all logs to calculate success rate -all_logs = chronicle.list_job_instance_logs( - integration_name="MyIntegration", - job_id="456", - job_instance_id="ji1", - as_list=True -) - -successful = sum(1 for log in all_logs if log.get("status") == "SUCCESS") -failed = sum(1 for log in all_logs if log.get("status") == "FAILED") -total = len(all_logs) - -if total > 0: - success_rate = (successful / total) * 100 - print(f"Success Rate: {success_rate:.2f}%") - print(f"Total Executions: {total}") - print(f"Successful: {successful}, Failed: {failed}") -``` - -### Integration Instances - -List all instances for a specific integration: - -```python -# Get all instances for an integration -instances = chronicle.list_integration_instances("MyIntegration") -for instance in instances.get("integrationInstances", []): - print(f"Instance: {instance.get('displayName')}, ID: {instance.get('name')}") - print(f"Environment: {instance.get('environment')}") - -# Get all instances as a list -instances = chronicle.list_integration_instances("MyIntegration", as_list=True) - -# Get instances for a specific environment -instances = chronicle.list_integration_instances( - "MyIntegration", - filter_string="environment = 'production'" -) -``` - -Get details of a specific integration instance: - -```python -instance = chronicle.get_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1" -) -print(f"Display Name: {instance.get('displayName')}") -print(f"Environment: {instance.get('environment')}") -print(f"Agent: {instance.get('agent')}") -``` - -Create a new integration instance: - -```python -from secops.chronicle.models import IntegrationInstanceParameter - -# Create instance with required fields only -new_instance = chronicle.create_integration_instance( - integration_name="MyIntegration", - environment="production" -) - -# Create instance with all fields -new_instance = chronicle.create_integration_instance( - integration_name="MyIntegration", - environment="production", - display_name="Production Instance", - description="Main production integration instance", - parameters=[ - IntegrationInstanceParameter( - value="api_key_value" - ), - IntegrationInstanceParameter( - value="https://api.example.com" - ) - ], - agent="agent-123" -) -``` - -Update an existing integration instance: - -```python -from secops.chronicle.models import IntegrationInstanceParameter - -# Update instance display name -updated_instance = chronicle.update_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1", - display_name="Updated Production Instance" -) - -# Update multiple fields including parameters -updated_instance = chronicle.update_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1", - display_name="Updated Instance", - description="Updated description", - environment="staging", - parameters=[ - IntegrationInstanceParameter( - value="new_api_key" - ) - ] -) - -# Use custom update mask -updated_instance = chronicle.update_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1", - display_name="New Name", - update_mask="displayName" -) -``` - -Delete an integration instance: - -```python -chronicle.delete_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1" -) -``` - -Execute a connectivity test for an integration instance: - -```python -# Test if the instance can connect to the third-party service -test_result = chronicle.execute_integration_instance_test( - integration_name="MyIntegration", - integration_instance_id="ii1" -) -print(f"Test Successful: {test_result.get('successful')}") -print(f"Message: {test_result.get('message')}") -``` - -Get affected items (playbooks) that depend on an integration instance: - -```python -# Perform impact analysis before deleting or modifying an instance -affected_items = chronicle.get_integration_instance_affected_items( - integration_name="MyIntegration", - integration_instance_id="ii1" -) -for playbook in affected_items.get("affectedPlaybooks", []): - print(f"Playbook: {playbook.get('displayName')}") - print(f" ID: {playbook.get('name')}") -``` - -Get the default integration instance: - -```python -# Get the system default configuration for a commercial product -default_instance = chronicle.get_default_integration_instance( - integration_name="AWSSecurityHub" -) -print(f"Default Instance: {default_instance.get('displayName')}") -print(f"Environment: {default_instance.get('environment')}") -``` - -### Integration Transformers - -List all transformers for a specific integration: - -```python -# Get all transformers for an integration -transformers = chronicle.list_integration_transformers("MyIntegration") -for transformer in transformers.get("transformers", []): - print(f"Transformer: {transformer.get('displayName')}, ID: {transformer.get('name')}") - -# Get all transformers as a list -transformers = chronicle.list_integration_transformers("MyIntegration", as_list=True) - -# Get only enabled transformers -transformers = chronicle.list_integration_transformers( - "MyIntegration", - filter_string="enabled = true" -) - -# Exclude staging transformers -transformers = chronicle.list_integration_transformers( - "MyIntegration", - exclude_staging=True -) - -# Get transformers with expanded details -transformers = chronicle.list_integration_transformers( - "MyIntegration", - expand="parameters" -) -``` - -Get details of a specific transformer: - -```python -transformer = chronicle.get_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1" -) -print(f"Display Name: {transformer.get('displayName')}") -print(f"Script: {transformer.get('script')}") -print(f"Enabled: {transformer.get('enabled')}") - -# Get transformer with expanded parameters -transformer = chronicle.get_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - expand="parameters" -) -``` - -Create a new transformer: - -```python -# Create a basic transformer -new_transformer = chronicle.create_integration_transformer( - integration_name="MyIntegration", - display_name="JSON Parser", - script=""" -def transform(data): - import json - try: - return json.loads(data) - except Exception as e: - return {"error": str(e)} -""", - script_timeout="60s", - enabled=True -) - -# Create transformer with all fields -new_transformer = chronicle.create_integration_transformer( - integration_name="MyIntegration", - display_name="Advanced Data Transformer", - description="Transforms and enriches incoming data", - script=""" -def transform(data, api_key, endpoint_url): - import json - import requests - - # Parse input data - parsed = json.loads(data) - - # Enrich with external API call - response = requests.get( - endpoint_url, - headers={"Authorization": f"Bearer {api_key}"} - ) - parsed["enrichment"] = response.json() - - return parsed -""", - script_timeout="120s", - enabled=True, - parameters=[ - { - "name": "api_key", - "type": "STRING", - "displayName": "API Key", - "mandatory": True - }, - { - "name": "endpoint_url", - "type": "STRING", - "displayName": "Endpoint URL", - "mandatory": True - } - ], - usage_example="Used to enrich security events with external threat intelligence", - expected_input='{"event": "data", "timestamp": "2024-01-01T00:00:00Z"}', - expected_output='{"event": "data", "timestamp": "2024-01-01T00:00:00Z", "enrichment": {...}}' -) -``` - -Update an existing transformer: - -```python -# Update transformer display name -updated_transformer = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - display_name="Updated Transformer Name" -) - -# Update transformer script -updated_transformer = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - script=""" -def transform(data): - # Updated transformation logic - return data.upper() -""" -) - -# Update multiple fields including parameters -updated_transformer = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - display_name="Enhanced Transformer", - description="Updated with better error handling", - script=""" -def transform(data, timeout=30): - import json - try: - result = json.loads(data) - result["processed"] = True - return result - except Exception as e: - return {"error": str(e), "original": data} -""", - script_timeout="90s", - enabled=True, - parameters=[ - { - "name": "timeout", - "type": "INTEGER", - "displayName": "Processing Timeout", - "mandatory": False, - "defaultValue": "30" - } - ] -) - -# Use custom update mask -updated_transformer = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - display_name="New Name", - description="New Description", - update_mask="displayName,description" -) -``` - -Delete a transformer: - -```python -chronicle.delete_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1" -) -``` - -Execute a test run of a transformer: - -```python -# Get the transformer -transformer = chronicle.get_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1" -) - -# Test the transformer with sample data -test_result = chronicle.execute_integration_transformer_test( - integration_name="MyIntegration", - transformer=transformer -) -print(f"Output Message: {test_result.get('outputMessage')}") -print(f"Debug Output: {test_result.get('debugOutputMessage')}") -print(f"Result Value: {test_result.get('resultValue')}") - -# You can also test a transformer before creating it -test_transformer = { - "displayName": "Test Transformer", - "script": """ -def transform(data): - return {"transformed": data, "status": "success"} -""", - "scriptTimeout": "60s", - "enabled": True -} - -test_result = chronicle.execute_integration_transformer_test( - integration_name="MyIntegration", - transformer=test_transformer -) -``` - -Get a template for creating a transformer: - -```python -# Get a boilerplate template for a new transformer -template = chronicle.get_integration_transformer_template("MyIntegration") -print(f"Template Script: {template.get('script')}") -print(f"Template Display Name: {template.get('displayName')}") - -# Use the template as a starting point -new_transformer = chronicle.create_integration_transformer( - integration_name="MyIntegration", - display_name="My Custom Transformer", - script=template.get('script'), # Customize this - script_timeout="60s", - enabled=True -) -``` - -Example workflow: Safe transformer development with testing: - -```python -# 1. Get a template to start with -template = chronicle.get_integration_transformer_template("MyIntegration") - -# 2. Customize the script -custom_transformer = { - "displayName": "CSV to JSON Transformer", - "description": "Converts CSV data to JSON format", - "script": """ -def transform(data): - import csv - import json - from io import StringIO - - # Parse CSV - reader = csv.DictReader(StringIO(data)) - rows = list(reader) - - return json.dumps(rows) -""", - "scriptTimeout": "60s", - "enabled": False, # Start disabled for testing - "usageExample": "Input CSV with headers, output JSON array of objects" -} - -# 3. Test the transformer before creating it -test_result = chronicle.execute_integration_transformer_test( - integration_name="MyIntegration", - transformer=custom_transformer -) - -# 4. If test is successful, create the transformer -if test_result.get('resultValue'): - created_transformer = chronicle.create_integration_transformer( - integration_name="MyIntegration", - display_name=custom_transformer["displayName"], - description=custom_transformer["description"], - script=custom_transformer["script"], - script_timeout=custom_transformer["scriptTimeout"], - enabled=True, # Enable after successful testing - usage_example=custom_transformer["usageExample"] - ) - print(f"Transformer created: {created_transformer.get('name')}") -else: - print(f"Test failed: {test_result.get('debugOutputMessage')}") - -# 5. Continue testing and refining -transformer_id = created_transformer.get('name').split('/')[-1] -updated = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id=transformer_id, - script=""" -def transform(data, delimiter=','): - import csv - import json - from io import StringIO - - # Parse CSV with custom delimiter - reader = csv.DictReader(StringIO(data), delimiter=delimiter) - rows = list(reader) - - return json.dumps(rows, indent=2) -""", - parameters=[ - { - "name": "delimiter", - "type": "STRING", - "displayName": "CSV Delimiter", - "mandatory": False, - "defaultValue": "," - } - ] -) -``` - -### Integration Transformer Revisions - -List all revisions for a transformer: - -```python -# Get all revisions for a transformer -revisions = chronicle.list_integration_transformer_revisions( - integration_name="MyIntegration", - transformer_id="t1" -) -for revision in revisions.get("revisions", []): - print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") - -# Get all revisions as a list -revisions = chronicle.list_integration_transformer_revisions( - integration_name="MyIntegration", - transformer_id="t1", - as_list=True -) - -# Filter revisions -revisions = chronicle.list_integration_transformer_revisions( - integration_name="MyIntegration", - transformer_id="t1", - filter_string='version = "1.0"', - order_by="createTime desc" -) -``` - -Delete a specific transformer revision: - -```python -chronicle.delete_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - revision_id="rev-456" -) -``` - -Create a new revision before making changes: - -```python -# Get the current transformer -transformer = chronicle.get_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1" -) - -# Create a backup revision -new_revision = chronicle.create_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - transformer=transformer, - comment="Backup before major refactor" -) -print(f"Created revision: {new_revision.get('name')}") - -# Create revision with custom comment -new_revision = chronicle.create_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - transformer=transformer, - comment="Version 2.0 - Enhanced error handling" -) -``` - -Rollback to a previous revision: - -```python -# Rollback to a previous working version -rollback_result = chronicle.rollback_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - revision_id="rev-456" -) -print(f"Rolled back to: {rollback_result.get('name')}") -``` - -Example workflow: Safe transformer updates with revision control: - -```python -# 1. Get the current transformer -transformer = chronicle.get_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1" -) - -# 2. Create a backup revision -backup = chronicle.create_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - transformer=transformer, - comment="Backup before updating transformation logic" -) - -# 3. Make changes to the transformer -updated_transformer = chronicle.update_integration_transformer( - integration_name="MyIntegration", - transformer_id="t1", - display_name="Enhanced Transformer", - script=""" -def transform(data, enrichment_enabled=True): - import json - - try: - # Parse input data - parsed = json.loads(data) - - # Apply transformations - parsed["processed"] = True - parsed["timestamp"] = "2024-01-01T00:00:00Z" - - # Optional enrichment - if enrichment_enabled: - parsed["enriched"] = True - - return json.dumps(parsed) - except Exception as e: - return json.dumps({"error": str(e), "original": data}) -""" -) - -# 4. Test the updated transformer -test_result = chronicle.execute_integration_transformer_test( - integration_name="MyIntegration", - transformer=updated_transformer -) - -# 5. If test fails, rollback to backup -if not test_result.get("resultValue"): - print("Test failed - rolling back") - chronicle.rollback_integration_transformer_revision( - integration_name="MyIntegration", - transformer_id="t1", - revision_id=backup.get("name").split("/")[-1] - ) -else: - print("Test passed - transformer updated successfully") - -# 6. List all revisions to see history -all_revisions = chronicle.list_integration_transformer_revisions( - integration_name="MyIntegration", - transformer_id="t1", - as_list=True -) -print(f"Total revisions: {len(all_revisions)}") -for rev in all_revisions: - print(f" - {rev.get('comment', 'No comment')} (ID: {rev.get('name').split('/')[-1]})") -``` - -### Integration Logical Operators - -List all logical operators for a specific integration: - -```python -# Get all logical operators for an integration -logical_operators = chronicle.list_integration_logical_operators("MyIntegration") -for operator in logical_operators.get("logicalOperators", []): - print(f"Operator: {operator.get('displayName')}, ID: {operator.get('name')}") - -# Get all logical operators as a list -logical_operators = chronicle.list_integration_logical_operators( - "MyIntegration", - as_list=True -) - -# Get only enabled logical operators -logical_operators = chronicle.list_integration_logical_operators( - "MyIntegration", - filter_string="enabled = true" -) - -# Exclude staging logical operators -logical_operators = chronicle.list_integration_logical_operators( - "MyIntegration", - exclude_staging=True -) - -# Get logical operators with expanded details -logical_operators = chronicle.list_integration_logical_operators( - "MyIntegration", - expand="parameters" -) -``` - -Get details of a specific logical operator: - -```python -operator = chronicle.get_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1" -) -print(f"Display Name: {operator.get('displayName')}") -print(f"Script: {operator.get('script')}") -print(f"Enabled: {operator.get('enabled')}") - -# Get logical operator with expanded parameters -operator = chronicle.get_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - expand="parameters" -) -``` - -Create a new logical operator: - -```python -# Create a basic equality operator -new_operator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="Equals Operator", - script=""" -def evaluate(a, b): - return a == b -""", - script_timeout="60s", - enabled=True -) - -# Create a more complex conditional operator with parameters -new_operator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="Threshold Checker", - description="Checks if a value exceeds a threshold", - script=""" -def evaluate(value, threshold, inclusive=False): - if inclusive: - return value >= threshold - else: - return value > threshold -""", - script_timeout="30s", - enabled=True, - parameters=[ - { - "name": "value", - "type": "INTEGER", - "displayName": "Value to Check", - "mandatory": True - }, - { - "name": "threshold", - "type": "INTEGER", - "displayName": "Threshold Value", - "mandatory": True - }, - { - "name": "inclusive", - "type": "BOOLEAN", - "displayName": "Inclusive Comparison", - "mandatory": False, - "defaultValue": "false" - } - ] -) - -# Create a string matching operator -pattern_operator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="Pattern Matcher", - description="Matches strings against patterns", - script=""" -def evaluate(text, pattern, case_sensitive=True): - import re - flags = 0 if case_sensitive else re.IGNORECASE - return bool(re.search(pattern, text, flags)) -""", - script_timeout="60s", - enabled=True, - parameters=[ - { - "name": "text", - "type": "STRING", - "displayName": "Text to Match", - "mandatory": True - }, - { - "name": "pattern", - "type": "STRING", - "displayName": "Regex Pattern", - "mandatory": True - }, - { - "name": "case_sensitive", - "type": "BOOLEAN", - "displayName": "Case Sensitive", - "mandatory": False, - "defaultValue": "true" - } - ] -) -``` - -Update an existing logical operator: - -```python -# Update logical operator display name -updated_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - display_name="Updated Operator Name" -) - -# Update logical operator script -updated_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - script=""" -def evaluate(a, b): - # Updated logic with type checking - if type(a) != type(b): - return False - return a == b -""" -) - -# Update multiple fields including parameters -updated_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - display_name="Enhanced Operator", - description="Updated with better validation", - script=""" -def evaluate(value, min_value, max_value): - try: - return min_value <= value <= max_value - except Exception: - return False -""", - script_timeout="45s", - enabled=True, - parameters=[ - { - "name": "value", - "type": "INTEGER", - "displayName": "Value", - "mandatory": True - }, - { - "name": "min_value", - "type": "INTEGER", - "displayName": "Minimum Value", - "mandatory": True - }, - { - "name": "max_value", - "type": "INTEGER", - "displayName": "Maximum Value", - "mandatory": True - } - ] -) - -# Use custom update mask -updated_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - display_name="New Name", - description="New Description", - update_mask="displayName,description" -) -``` - -Delete a logical operator: - -```python -chronicle.delete_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1" -) -``` - -Execute a test run of a logical operator: - -```python -# Get the logical operator -operator = chronicle.get_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1" -) - -# Test the logical operator with sample data -test_result = chronicle.execute_integration_logical_operator_test( - integration_name="MyIntegration", - logical_operator=operator -) -print(f"Output Message: {test_result.get('outputMessage')}") -print(f"Debug Output: {test_result.get('debugOutputMessage')}") -print(f"Result Value: {test_result.get('resultValue')}") # True or False - -# You can also test a logical operator before creating it -test_operator = { - "displayName": "Test Equality Operator", - "script": """ -def evaluate(a, b): - return a == b -""", - "scriptTimeout": "30s", - "enabled": True -} - -test_result = chronicle.execute_integration_logical_operator_test( - integration_name="MyIntegration", - logical_operator=test_operator -) -``` - -Get a template for creating a logical operator: - -```python -# Get a boilerplate template for a new logical operator -template = chronicle.get_integration_logical_operator_template("MyIntegration") -print(f"Template Script: {template.get('script')}") -print(f"Template Display Name: {template.get('displayName')}") - -# Use the template as a starting point -new_operator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="My Custom Operator", - script=template.get('script'), # Customize this - script_timeout="60s", - enabled=True -) -``` - -Example workflow: Building conditional logic for integration workflows: - -```python -# 1. Get a template to start with -template = chronicle.get_integration_logical_operator_template("MyIntegration") - -# 2. Create a custom logical operator for severity checking -severity_operator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="Severity Level Check", - description="Checks if severity meets minimum threshold", - script=""" -def evaluate(severity, min_severity='MEDIUM'): - severity_levels = { - 'LOW': 1, - 'MEDIUM': 2, - 'HIGH': 3, - 'CRITICAL': 4 - } - - current_level = severity_levels.get(severity.upper(), 0) - min_level = severity_levels.get(min_severity.upper(), 0) - - return current_level >= min_level -""", - script_timeout="30s", - enabled=False, # Start disabled for testing - parameters=[ - { - "name": "severity", - "type": "STRING", - "displayName": "Event Severity", - "mandatory": True - }, - { - "name": "min_severity", - "type": "STRING", - "displayName": "Minimum Severity", - "mandatory": False, - "defaultValue": "MEDIUM" - } - ] -) - -# 3. Test the operator before enabling -test_result = chronicle.execute_integration_logical_operator_test( - integration_name="MyIntegration", - logical_operator=severity_operator -) - -# 4. If test is successful, enable the operator -if test_result.get('resultValue') is not None: - operator_id = severity_operator.get('name').split('/')[-1] - enabled_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id=operator_id, - enabled=True - ) - print(f"Operator enabled: {enabled_operator.get('name')}") -else: - print(f"Test failed: {test_result.get('debugOutputMessage')}") - -# 5. Create additional operators for workflow automation -# IP address validation operator -ip_validator = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="IP Address Validator", - description="Validates if a string is a valid IP address", - script=""" -def evaluate(ip_string): - import ipaddress - try: - ipaddress.ip_address(ip_string) - return True - except ValueError: - return False -""", - script_timeout="30s", - enabled=True -) - -# Time range checker -time_checker = chronicle.create_integration_logical_operator( - integration_name="MyIntegration", - display_name="Business Hours Checker", - description="Checks if timestamp falls within business hours", - script=""" -def evaluate(timestamp, start_hour=9, end_hour=17): - from datetime import datetime - - dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) - hour = dt.hour - - return start_hour <= hour < end_hour -""", - script_timeout="30s", - enabled=True, - parameters=[ - { - "name": "timestamp", - "type": "STRING", - "displayName": "Timestamp", - "mandatory": True - }, - { - "name": "start_hour", - "type": "INTEGER", - "displayName": "Business Day Start Hour", - "mandatory": False, - "defaultValue": "9" - }, - { - "name": "end_hour", - "type": "INTEGER", - "displayName": "Business Day End Hour", - "mandatory": False, - "defaultValue": "17" - } - ] -) - -# 6. List all logical operators for the integration -all_operators = chronicle.list_integration_logical_operators( - integration_name="MyIntegration", - as_list=True -) -print(f"Total logical operators: {len(all_operators)}") -for op in all_operators: - print(f" - {op.get('displayName')} (Enabled: {op.get('enabled')})") -``` - -### Integration Logical Operator Revisions - -List all revisions for a logical operator: - -```python -# Get all revisions for a logical operator -revisions = chronicle.list_integration_logical_operator_revisions( - integration_name="MyIntegration", - logical_operator_id="lo1" -) -for revision in revisions.get("revisions", []): - print(f"Revision: {revision.get('name')}, Comment: {revision.get('comment')}") - -# Get all revisions as a list -revisions = chronicle.list_integration_logical_operator_revisions( - integration_name="MyIntegration", - logical_operator_id="lo1", - as_list=True -) - -# Filter revisions -revisions = chronicle.list_integration_logical_operator_revisions( - integration_name="MyIntegration", - logical_operator_id="lo1", - filter_string='version = "1.0"', - order_by="createTime desc" -) -``` - -Delete a specific logical operator revision: - -```python -chronicle.delete_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - revision_id="rev-456" -) -``` - -Create a new revision before making changes: - -```python -# Get the current logical operator -logical_operator = chronicle.get_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1" -) - -# Create a backup revision -new_revision = chronicle.create_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - logical_operator=logical_operator, - comment="Backup before refactoring conditional logic" -) -print(f"Created revision: {new_revision.get('name')}") - -# Create revision with custom comment -new_revision = chronicle.create_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - logical_operator=logical_operator, - comment="Version 2.0 - Enhanced comparison logic" -) -``` - -Rollback to a previous revision: - -```python -# Rollback to a previous working version -rollback_result = chronicle.rollback_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - revision_id="rev-456" -) -print(f"Rolled back to: {rollback_result.get('name')}") -``` - -Example workflow: Safe logical operator updates with revision control: - -```python -# 1. Get the current logical operator -logical_operator = chronicle.get_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1" -) - -# 2. Create a backup revision -backup = chronicle.create_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - logical_operator=logical_operator, - comment="Backup before updating evaluation logic" -) - -# 3. Make changes to the logical operator -updated_operator = chronicle.update_integration_logical_operator( - integration_name="MyIntegration", - logical_operator_id="lo1", - display_name="Enhanced Conditional Operator", - script=""" -def evaluate(severity, threshold, include_medium=False): - severity_levels = { - 'LOW': 1, - 'MEDIUM': 2, - 'HIGH': 3, - 'CRITICAL': 4 - } - - current = severity_levels.get(severity.upper(), 0) - min_level = severity_levels.get(threshold.upper(), 0) - - if include_medium and current >= severity_levels['MEDIUM']: - return True - - return current >= min_level -""" -) - -# 4. Test the updated logical operator -test_result = chronicle.execute_integration_logical_operator_test( - integration_name="MyIntegration", - logical_operator=updated_operator -) - -# 5. If test fails, rollback to backup -if test_result.get("resultValue") is None or "error" in test_result.get("debugOutputMessage", "").lower(): - print("Test failed - rolling back") - chronicle.rollback_integration_logical_operator_revision( - integration_name="MyIntegration", - logical_operator_id="lo1", - revision_id=backup.get("name").split("/")[-1] - ) -else: - print("Test passed - logical operator updated successfully") - -# 6. List all revisions to see history -all_revisions = chronicle.list_integration_logical_operator_revisions( - integration_name="MyIntegration", - logical_operator_id="lo1", - as_list=True -) -print(f"Total revisions: {len(all_revisions)}") -for rev in all_revisions: - print(f" - {rev.get('comment', 'No comment')} (ID: {rev.get('name').split('/')[-1]})") -``` - - ## Rule Management The SDK provides comprehensive support for managing Chronicle detection rules: diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index b3d0b286..e9bd5b46 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -214,19 +214,6 @@ create_watchlist, update_watchlist, ) -from secops.chronicle.integration.integrations import ( - list_integrations, - get_integration, - delete_integration, - create_integration, - transition_integration, - update_integration, - update_custom_integration, - get_integration_affected_items, - get_integration_dependencies, - get_integration_diff, - get_integration_restricted_agents, -) from secops.chronicle.integration.actions import ( list_integration_actions, get_integration_action, @@ -243,52 +230,6 @@ create_integration_action_revision, rollback_integration_action_revision, ) -from secops.chronicle.integration.connectors import ( - list_integration_connectors, - get_integration_connector, - delete_integration_connector, - create_integration_connector, - update_integration_connector, - execute_integration_connector_test, - get_integration_connector_template, -) -from secops.chronicle.integration.connector_revisions import ( - list_integration_connector_revisions, - delete_integration_connector_revision, - create_integration_connector_revision, - rollback_integration_connector_revision, -) -from secops.chronicle.integration.connector_context_properties import ( - list_connector_context_properties, - get_connector_context_property, - delete_connector_context_property, - create_connector_context_property, - update_connector_context_property, - delete_all_connector_context_properties, -) -from secops.chronicle.integration.connector_instance_logs import ( - list_connector_instance_logs, - get_connector_instance_log, -) -from secops.chronicle.integration.connector_instances import ( - list_connector_instances, - get_connector_instance, - delete_connector_instance, - create_connector_instance, - update_connector_instance, - get_connector_instance_latest_definition, - set_connector_instance_logs_collection, - run_connector_instance_on_demand, -) -from secops.chronicle.integration.jobs import ( - list_integration_jobs, - get_integration_job, - delete_integration_job, - create_integration_job, - update_integration_job, - execute_integration_job_test, - get_integration_job_template, -) from secops.chronicle.integration.managers import ( list_integration_managers, get_integration_manager, @@ -304,79 +245,6 @@ create_integration_manager_revision, rollback_integration_manager_revision, ) -from secops.chronicle.integration.job_revisions import ( - list_integration_job_revisions, - delete_integration_job_revision, - create_integration_job_revision, - rollback_integration_job_revision, -) -from secops.chronicle.integration.job_instances import ( - list_integration_job_instances, - get_integration_job_instance, - delete_integration_job_instance, - create_integration_job_instance, - update_integration_job_instance, - run_integration_job_instance_on_demand, -) -from secops.chronicle.integration.job_context_properties import ( - list_job_context_properties, - get_job_context_property, - delete_job_context_property, - create_job_context_property, - update_job_context_property, - delete_all_job_context_properties, -) -from secops.chronicle.integration.job_instance_logs import ( - list_job_instance_logs, - get_job_instance_log, -) -from secops.chronicle.integration.integration_instances import ( - list_integration_instances, - get_integration_instance, - delete_integration_instance, - create_integration_instance, - update_integration_instance, - execute_integration_instance_test, - get_integration_instance_affected_items, - get_default_integration_instance, -) -from secops.chronicle.integration.transformers import ( - list_integration_transformers, - get_integration_transformer, - delete_integration_transformer, - create_integration_transformer, - update_integration_transformer, - execute_integration_transformer_test, - get_integration_transformer_template, -) -from secops.chronicle.integration.transformer_revisions import ( - list_integration_transformer_revisions, - delete_integration_transformer_revision, - create_integration_transformer_revision, - rollback_integration_transformer_revision, -) -from secops.chronicle.integration.logical_operators import ( - list_integration_logical_operators, - get_integration_logical_operator, - delete_integration_logical_operator, - create_integration_logical_operator, - update_integration_logical_operator, - execute_integration_logical_operator_test, - get_integration_logical_operator_template, -) -from secops.chronicle.integration.logical_operator_revisions import ( - list_integration_logical_operator_revisions, - delete_integration_logical_operator_revision, - create_integration_logical_operator_revision, - rollback_integration_logical_operator_revision, -) -from secops.chronicle.integration.marketplace_integrations import ( - list_marketplace_integrations, - get_marketplace_integration, - get_marketplace_integration_diff, - install_marketplace_integration, - uninstall_marketplace_integration, -) __all__ = [ # Client @@ -556,18 +424,6 @@ "delete_watchlist", "create_watchlist", "update_watchlist", - # Integrations - "list_integrations", - "get_integration", - "delete_integration", - "create_integration", - "transition_integration", - "update_integration", - "update_custom_integration", - "get_integration_affected_items", - "get_integration_dependencies", - "get_integration_diff", - "get_integration_restricted_agents", # Integration Actions "list_integration_actions", "get_integration_action", @@ -582,46 +438,6 @@ "delete_integration_action_revision", "create_integration_action_revision", "rollback_integration_action_revision", - # Integration Connectors - "list_integration_connectors", - "get_integration_connector", - "delete_integration_connector", - "create_integration_connector", - "update_integration_connector", - "execute_integration_connector_test", - "get_integration_connector_template", - # Integration Connector Revisions - "list_integration_connector_revisions", - "delete_integration_connector_revision", - "create_integration_connector_revision", - "rollback_integration_connector_revision", - # Connector Context Properties - "list_connector_context_properties", - "get_connector_context_property", - "delete_connector_context_property", - "create_connector_context_property", - "update_connector_context_property", - "delete_all_connector_context_properties", - # Connector Instance Logs - "list_connector_instance_logs", - "get_connector_instance_log", - # Connector Instances - "list_connector_instances", - "get_connector_instance", - "delete_connector_instance", - "create_connector_instance", - "update_connector_instance", - "get_connector_instance_latest_definition", - "set_connector_instance_logs_collection", - "run_connector_instance_on_demand", - # Integration Jobs - "list_integration_jobs", - "get_integration_job", - "delete_integration_job", - "create_integration_job", - "update_integration_job", - "execute_integration_job_test", - "get_integration_job_template", # Integration Managers "list_integration_managers", "get_integration_manager", @@ -635,67 +451,4 @@ "delete_integration_manager_revision", "create_integration_manager_revision", "rollback_integration_manager_revision", - # Integration Job Revisions - "list_integration_job_revisions", - "delete_integration_job_revision", - "create_integration_job_revision", - "rollback_integration_job_revision", - # Integration Job Instances - "list_integration_job_instances", - "get_integration_job_instance", - "delete_integration_job_instance", - "create_integration_job_instance", - "update_integration_job_instance", - "run_integration_job_instance_on_demand", - # Job Context Properties - "list_job_context_properties", - "get_job_context_property", - "delete_job_context_property", - "create_job_context_property", - "update_job_context_property", - "delete_all_job_context_properties", - # Job Instance Logs - "list_job_instance_logs", - "get_job_instance_log", - # Integration Instances - "list_integration_instances", - "get_integration_instance", - "delete_integration_instance", - "create_integration_instance", - "update_integration_instance", - "execute_integration_instance_test", - "get_integration_instance_affected_items", - "get_default_integration_instance", - # Integration Transformers - "list_integration_transformers", - "get_integration_transformer", - "delete_integration_transformer", - "create_integration_transformer", - "update_integration_transformer", - "execute_integration_transformer_test", - "get_integration_transformer_template", - # Integration Transformer Revisions - "list_integration_transformer_revisions", - "delete_integration_transformer_revision", - "create_integration_transformer_revision", - "rollback_integration_transformer_revision", - # Integration Logical Operators - "list_integration_logical_operators", - "get_integration_logical_operator", - "delete_integration_logical_operator", - "create_integration_logical_operator", - "update_integration_logical_operator", - "execute_integration_logical_operator_test", - "get_integration_logical_operator_template", - # Integration Logical Operator Revisions - "list_integration_logical_operator_revisions", - "delete_integration_logical_operator_revision", - "create_integration_logical_operator_revision", - "rollback_integration_logical_operator_revision", - # Marketplace Integrations - "list_marketplace_integrations", - "get_marketplace_integration", - "get_marketplace_integration_diff", - "install_marketplace_integration", - "uninstall_marketplace_integration", ] diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index e69a9fd2..4c5b13e9 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -130,30 +130,6 @@ is_valid_log_type as _is_valid_log_type, search_log_types as _search_log_types, ) -from secops.chronicle.integration.marketplace_integrations import ( - get_marketplace_integration as _get_marketplace_integration, - get_marketplace_integration_diff as _get_marketplace_integration_diff, - install_marketplace_integration as _install_marketplace_integration, - list_marketplace_integrations as _list_marketplace_integrations, - uninstall_marketplace_integration as _uninstall_marketplace_integration, -) -from secops.chronicle.integration.integrations import ( - create_integration as _create_integration, - delete_integration as _delete_integration, - download_integration as _download_integration, - download_integration_dependency as _download_integration_dependency, - export_integration_items as _export_integration_items, - get_agent_integrations as _get_agent_integrations, - get_integration as _get_integration, - get_integration_affected_items as _get_integration_affected_items, - get_integration_dependencies as _get_integration_dependencies, - get_integration_diff as _get_integration_diff, - get_integration_restricted_agents as _get_integration_restricted_agents, - list_integrations as _list_integrations, - transition_integration as _transition_integration, - update_custom_integration as _update_custom_integration, - update_integration as _update_integration, -) from secops.chronicle.integration.actions import ( create_integration_action as _create_integration_action, delete_integration_action as _delete_integration_action, @@ -170,52 +146,6 @@ list_integration_action_revisions as _list_integration_action_revisions, rollback_integration_action_revision as _rollback_integration_action_revision, ) -from secops.chronicle.integration.connectors import ( - create_integration_connector as _create_integration_connector, - delete_integration_connector as _delete_integration_connector, - execute_integration_connector_test as _execute_integration_connector_test, - get_integration_connector as _get_integration_connector, - get_integration_connector_template as _get_integration_connector_template, - list_integration_connectors as _list_integration_connectors, - update_integration_connector as _update_integration_connector, -) -from secops.chronicle.integration.connector_revisions import ( - create_integration_connector_revision as _create_integration_connector_revision, - delete_integration_connector_revision as _delete_integration_connector_revision, - list_integration_connector_revisions as _list_integration_connector_revisions, - rollback_integration_connector_revision as _rollback_integration_connector_revision, -) -from secops.chronicle.integration.connector_context_properties import ( - create_connector_context_property as _create_connector_context_property, - delete_all_connector_context_properties as _delete_all_connector_context_properties, - delete_connector_context_property as _delete_connector_context_property, - get_connector_context_property as _get_connector_context_property, - list_connector_context_properties as _list_connector_context_properties, - update_connector_context_property as _update_connector_context_property, -) -from secops.chronicle.integration.connector_instance_logs import ( - get_connector_instance_log as _get_connector_instance_log, - list_connector_instance_logs as _list_connector_instance_logs, -) -from secops.chronicle.integration.connector_instances import ( - create_connector_instance as _create_connector_instance, - delete_connector_instance as _delete_connector_instance, - get_connector_instance as _get_connector_instance, - get_connector_instance_latest_definition as _get_connector_instance_latest_definition, - list_connector_instances as _list_connector_instances, - run_connector_instance_on_demand as _run_connector_instance_on_demand, - set_connector_instance_logs_collection as _set_connector_instance_logs_collection, - update_connector_instance as _update_connector_instance, -) -from secops.chronicle.integration.jobs import ( - create_integration_job as _create_integration_job, - delete_integration_job as _delete_integration_job, - execute_integration_job_test as _execute_integration_job_test, - get_integration_job as _get_integration_job, - get_integration_job_template as _get_integration_job_template, - list_integration_jobs as _list_integration_jobs, - update_integration_job as _update_integration_job, -) from secops.chronicle.integration.managers import ( create_integration_manager as _create_integration_manager, delete_integration_manager as _delete_integration_manager, @@ -231,90 +161,14 @@ list_integration_manager_revisions as _list_integration_manager_revisions, rollback_integration_manager_revision as _rollback_integration_manager_revision, ) -from secops.chronicle.integration.job_revisions import ( - create_integration_job_revision as _create_integration_job_revision, - delete_integration_job_revision as _delete_integration_job_revision, - list_integration_job_revisions as _list_integration_job_revisions, - rollback_integration_job_revision as _rollback_integration_job_revision, -) -from secops.chronicle.integration.job_instances import ( - create_integration_job_instance as _create_integration_job_instance, - delete_integration_job_instance as _delete_integration_job_instance, - get_integration_job_instance as _get_integration_job_instance, - list_integration_job_instances as _list_integration_job_instances, - run_integration_job_instance_on_demand as _run_integration_job_instance_on_demand, - update_integration_job_instance as _update_integration_job_instance, -) -from secops.chronicle.integration.job_context_properties import ( - create_job_context_property as _create_job_context_property, - delete_all_job_context_properties as _delete_all_job_context_properties, - delete_job_context_property as _delete_job_context_property, - get_job_context_property as _get_job_context_property, - list_job_context_properties as _list_job_context_properties, - update_job_context_property as _update_job_context_property, -) -from secops.chronicle.integration.job_instance_logs import ( - get_job_instance_log as _get_job_instance_log, - list_job_instance_logs as _list_job_instance_logs, -) -from secops.chronicle.integration.integration_instances import ( - create_integration_instance as _create_integration_instance, - delete_integration_instance as _delete_integration_instance, - execute_integration_instance_test as _execute_integration_instance_test, - get_default_integration_instance as _get_default_integration_instance, - get_integration_instance as _get_integration_instance, - get_integration_instance_affected_items as _get_integration_instance_affected_items, - list_integration_instances as _list_integration_instances, - update_integration_instance as _update_integration_instance, -) -from secops.chronicle.integration.transformers import ( - create_integration_transformer as _create_integration_transformer, - delete_integration_transformer as _delete_integration_transformer, - execute_integration_transformer_test as _execute_integration_transformer_test, - get_integration_transformer as _get_integration_transformer, - get_integration_transformer_template as _get_integration_transformer_template, - list_integration_transformers as _list_integration_transformers, - update_integration_transformer as _update_integration_transformer, -) -from secops.chronicle.integration.transformer_revisions import ( - create_integration_transformer_revision as _create_integration_transformer_revision, - delete_integration_transformer_revision as _delete_integration_transformer_revision, - list_integration_transformer_revisions as _list_integration_transformer_revisions, - rollback_integration_transformer_revision as _rollback_integration_transformer_revision, -) -from secops.chronicle.integration.logical_operators import ( - create_integration_logical_operator as _create_integration_logical_operator, - delete_integration_logical_operator as _delete_integration_logical_operator, - execute_integration_logical_operator_test as _execute_integration_logical_operator_test, - get_integration_logical_operator as _get_integration_logical_operator, - get_integration_logical_operator_template as _get_integration_logical_operator_template, - list_integration_logical_operators as _list_integration_logical_operators, - update_integration_logical_operator as _update_integration_logical_operator, -) -from secops.chronicle.integration.logical_operator_revisions import ( - create_integration_logical_operator_revision as _create_integration_logical_operator_revision, - delete_integration_logical_operator_revision as _delete_integration_logical_operator_revision, - list_integration_logical_operator_revisions as _list_integration_logical_operator_revisions, - rollback_integration_logical_operator_revision as _rollback_integration_logical_operator_revision, -) from secops.chronicle.models import ( APIVersion, CaseList, - ConnectorParameter, - ConnectorRule, DashboardChart, DashboardQuery, - DiffType, EntitySummary, InputInterval, - IntegrationInstanceParameter, - IntegrationType, - JobParameter, - PythonVersion, - TargetMode, TileType, - IntegrationParam, - ConnectorInstanceParameter, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -853,4622 +707,407 @@ def update_watchlist( ) # ------------------------------------------------------------------------- - # Marketplace Integration methods + # Integration Action methods # ------------------------------------------------------------------------- - def list_marketplace_integrations( + def list_integration_actions( self, + integration_name: str, page_size: int | None = None, page_token: str | None = None, filter_string: str | None = None, order_by: str | None = None, + expand: str | None = None, api_version: APIVersion | None = APIVersion.V1BETA, as_list: bool = False, ) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of all marketplace integration. + """Get a list of actions for a given integration. Args: - page_size: Maximum number of integration to return per page - page_token: Token for the next page of results, if available - filter_string: Filter expression to filter marketplace integration - order_by: Field to sort the marketplace integration by - api_version: API version to use. Defaults to V1BETA - as_list: If True, return a list of integration instead of a dict - with integration list and nextPageToken. + integration_name: Name of the integration to get actions for + page_size: Number of results to return per page + page_token: Token for the page to retrieve + filter_string: Filter expression to filter actions + order_by: Field to sort the actions by + expand: Comma-separated list of fields to expand in the response + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of actions instead of a dict with + actions list and nextPageToken. Returns: - If as_list is True: List of marketplace integration. - If as_list is False: Dict with marketplace integration list and - nextPageToken. + If as_list is True: List of actions. + If as_list is False: Dict with actions list and nextPageToken. Raises: APIError: If the API request fails """ - return _list_marketplace_integrations( + return _list_integration_actions( self, - page_size, - page_token, - filter_string, - order_by, - api_version, - as_list, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + expand=expand, + api_version=api_version, + as_list=as_list, ) - def get_marketplace_integration( + def get_integration_action( self, integration_name: str, + action_id: str, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Get a specific marketplace integration by integration name. + """Get details of a specific action for a given integration. Args: - integration_name: name of the marketplace integration to retrieve - api_version: API version to use. Defaults to V1BETA + integration_name: Name of the integration the action belongs to + action_id: ID of the action to retrieve + api_version: API version to use for the request. Default is V1BETA. Returns: - Marketplace integration details + Dict containing details of the specified action. Raises: APIError: If the API request fails """ - return _get_marketplace_integration(self, integration_name, api_version) + return _get_integration_action( + self, + integration_name, + action_id, + api_version=api_version, + ) - def get_marketplace_integration_diff( + def delete_integration_action( self, integration_name: str, + action_id: str, api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get the differences between the currently installed version of - an integration and the commercial version available in the - marketplace. + ) -> None: + """Delete a specific action from a given integration. Args: - integration_name: name of the marketplace integration - api_version: API version to use. Defaults to V1BETA + integration_name: Name of the integration the action belongs to + action_id: ID of the action to delete + api_version: API version to use for the request. Default is V1BETA. Returns: - Marketplace integration diff details + None Raises: APIError: If the API request fails """ - return _get_marketplace_integration_diff( - self, integration_name, api_version + return _delete_integration_action( + self, + integration_name, + action_id, + api_version=api_version, ) - def install_marketplace_integration( + def create_integration_action( self, integration_name: str, - override_mapping: bool | None = None, - staging: bool | None = None, - version: str | None = None, - restore_from_snapshot: bool | None = None, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + script_result_name: str, + is_async: bool, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any]] | None = None, + ai_generated: bool | None = None, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Install a marketplace integration by integration name - - Args: - integration_name: Name of the marketplace integration to install - override_mapping: Optional. Determines if the integration should - override the ontology if already installed, if not provided, - set to false by default. - staging: Optional. Determines if the integration should be installed - as staging or production, - if not provided, installed as production. - version: Optional. Determines which version of the integration - should be installed. - restore_from_snapshot: Optional. Determines if the integration - should be installed from existing integration snapshot. + """Create a new custom action for a given integration. + + Args: + integration_name: Name of the integration to + create the action for. + display_name: Action's display name. + Maximum 150 characters. Required. + script: Action's Python script. Maximum size 5MB. Required. + timeout_seconds: Action timeout in seconds. Maximum 1200. Required. + enabled: Whether the action is enabled or disabled. Required. + script_result_name: Field name that holds the script result. + Maximum 100 characters. Required. + is_async: Whether the action is asynchronous. Required. + description: Action's description. Maximum 400 characters. Optional. + default_result_value: Action's default result value. + Maximum 1000 characters. Optional. + async_polling_interval_seconds: Polling interval + in seconds for async actions. + Cannot exceed total timeout. Optional. + async_total_timeout_seconds: Total async timeout in seconds. Maximum + 1209600 (14 days). Optional. + dynamic_results: List of dynamic result metadata dicts. + Max 50. Optional. + parameters: List of action parameter dicts. Max 50. Optional. + ai_generated: Whether the action was generated by AI. Optional. api_version: API version to use for the request. Default is V1BETA. Returns: - Installed marketplace integration details + Dict containing the newly created IntegrationAction resource. Raises: - APIError: If the API request fails + APIError: If the API request fails. """ - return _install_marketplace_integration( + return _create_integration_action( self, integration_name, - override_mapping, - staging, - version, - restore_from_snapshot, - api_version, + display_name, + script, + timeout_seconds, + enabled, + script_result_name, + is_async, + description=description, + default_result_value=default_result_value, + async_polling_interval_seconds=async_polling_interval_seconds, + async_total_timeout_seconds=async_total_timeout_seconds, + dynamic_results=dynamic_results, + parameters=parameters, + ai_generated=ai_generated, + api_version=api_version, ) - def uninstall_marketplace_integration( + def update_integration_action( self, integration_name: str, + action_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + script_result_name: str | None = None, + is_async: bool | None = None, + description: str | None = None, + default_result_value: str | None = None, + async_polling_interval_seconds: int | None = None, + async_total_timeout_seconds: int | None = None, + dynamic_results: list[dict[str, Any]] | None = None, + parameters: list[dict[str, Any]] | None = None, + ai_generated: bool | None = None, + update_mask: str | None = None, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Uninstall a marketplace integration by integration name + """Update an existing custom action for a given integration. + + Only custom actions can be updated; predefined commercial actions are + immutable. Args: - integration_name: Name of the marketplace integration to uninstall + integration_name: Name of the integration the action belongs to. + action_id: ID of the action to update. + display_name: Action's display name. Maximum 150 characters. + script: Action's Python script. Maximum size 5MB. + timeout_seconds: Action timeout in seconds. Maximum 1200. + enabled: Whether the action is enabled or disabled. + script_result_name: Field name that holds the script result. + Maximum 100 characters. + is_async: Whether the action is asynchronous. + description: Action's description. Maximum 400 characters. + default_result_value: Action's default result value. + Maximum 1000 characters. + async_polling_interval_seconds: Polling interval + in seconds for async actions. Cannot exceed total timeout. + async_total_timeout_seconds: Total async timeout in seconds. Maximum + 1209600 (14 days). + dynamic_results: List of dynamic result metadata dicts. Max 50. + parameters: List of action parameter dicts. Max 50. + ai_generated: Whether the action was generated by AI. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". api_version: API version to use for the request. Default is V1BETA. Returns: - Empty dictionary if uninstallation is successful + Dict containing the updated IntegrationAction resource. Raises: - APIError: If the API request fails + APIError: If the API request fails. """ - return _uninstall_marketplace_integration( - self, integration_name, api_version + return _update_integration_action( + self, + integration_name, + action_id, + display_name=display_name, + script=script, + timeout_seconds=timeout_seconds, + enabled=enabled, + script_result_name=script_result_name, + is_async=is_async, + description=description, + default_result_value=default_result_value, + async_polling_interval_seconds=async_polling_interval_seconds, + async_total_timeout_seconds=async_total_timeout_seconds, + dynamic_results=dynamic_results, + parameters=parameters, + ai_generated=ai_generated, + update_mask=update_mask, + api_version=api_version, ) - # ------------------------------------------------------------------------- - # Integration methods - # ------------------------------------------------------------------------- - - def list_integrations( + def execute_integration_action_test( self, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, + integration_name: str, + test_case_id: int, + action: dict[str, Any], + scope: str, + integration_instance_id: str, api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of all integrations. + ) -> dict[str, Any]: + """Execute a test run of an integration action's script. + + Use this method to verify custom action logic, connectivity, and data + parsing against a specified integration instance and test case before + making the action available in playbooks. Args: - page_size: Maximum number of integrations to return per page - page_token: Token for the next page of results, if available - filter_string: Filter expression to filter integrations. - Only supports "displayName:" prefix. - order_by: Field to sort the integrations by - api_version: API version to use. Defaults to V1BETA - as_list: If True, return a list of integrations instead of a dict - with integration list and nextPageToken. + integration_name: Name of the integration the action belongs to. + test_case_id: ID of the action test case. + action: Dict containing the IntegrationAction to test. + scope: The action test scope. + integration_instance_id: The integration instance ID to use. + api_version: API version to use for the request. Default is V1BETA. Returns: - If as_list is True: List of integrations. - If as_list is False: Dict with integration list and nextPageToken. + Dict with the test execution results with the following fields: + - output: The script output. + - debugOutput: The script debug output. + - resultJson: The result JSON if it exists (optional). + - resultName: The script result name (optional). Raises: - APIError: If the API request fails + APIError: If the API request fails. """ - return _list_integrations( + return _execute_integration_action_test( self, - page_size, - page_token, - filter_string, - order_by, - api_version, - as_list, + integration_name, + test_case_id, + action, + scope, + integration_instance_id, + api_version=api_version, ) - def get_integration( + def get_integration_actions_by_environment( self, integration_name: str, + environments: list[str], + include_widgets: bool, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Get a specific integration by integration name. - - Args: - integration_name: name of the integration to retrieve - api_version: API version to use. Defaults to V1BETA + """List actions executable within specified environments. - Returns: - Integration details - - Raises: - APIError: If the API request fails - """ - return _get_integration(self, integration_name, api_version) - - def delete_integration( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Deletes a specific custom integration. Commercial integrations - cannot be deleted via this method. - - Args: - integration_name: Name of the integration to delete - api_version: API version to use for the request. - Default is V1BETA. - - Raises: - APIError: If the API request fails - """ - _delete_integration(self, integration_name, api_version) - - def create_integration( - self, - display_name: str, - staging: bool, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[IntegrationParam | dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Creates a new custom SOAR integration. - - Args: - display_name: Required. The display name of the integration - (max 150 characters) - staging: Required. True if the integration is in staging mode - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a - base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50). - Each entry may be an IntegrationParam dataclass instance - or a plain dict with keys: id, defaultValue, - displayName, propertyName, type, description, mandatory. - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type - (response/extension) - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the details of the newly created integration - - Raises: - APIError: If the API request fails - """ - return _create_integration( - self, - display_name=display_name, - staging=staging, - description=description, - image_base64=image_base64, - svg_icon=svg_icon, - python_version=python_version, - parameters=parameters, - categories=categories, - integration_type=integration_type, - api_version=api_version, - ) - - def download_integration( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> bytes: - """Exports the entire integration package as a ZIP file. Includes - all scripts, definitions, and the manifest file. Use this method - for backup or sharing. - - Args: - integration_name: Name of the integration to download - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Bytes of the ZIP file containing the integration package - - Raises: - APIError: If the API request fails - """ - return _download_integration(self, integration_name, api_version) - - def download_integration_dependency( - self, - integration_name: str, - dependency_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Initiates the download of a Python dependency (e.g., a library - from PyPI) for a custom integration. - - Args: - integration_name: Name of the integration whose dependency - to download - dependency_name: The dependency name to download. It can - contain the version or the repository. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Empty dict if the download was successful, - or a dict containing error - details if the download failed - - Raises: - APIError: If the API request fails - """ - return _download_integration_dependency( - self, integration_name, dependency_name, api_version - ) - - def export_integration_items( - self, - integration_name: str, - actions: list[str] | str | None = None, - jobs: list[str] | str | None = None, - connectors: list[str] | str | None = None, - managers: list[str] | str | None = None, - transformers: list[str] | str | None = None, - logical_operators: list[str] | str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> bytes: - """Exports specific items from an integration into a ZIP folder. - Use this method to extract only a subset of capabilities (e.g., - just the connectors) for reuse. - - Args: - integration_name: Name of the integration to export items from - actions: Optional. IDs of the actions to export as a list or - comma-separated string. Format: [1,2,3] or "1,2,3" - jobs: Optional. IDs of the jobs to export as a list or - comma-separated string. - connectors: Optional. IDs of the connectors to export as a - list or comma-separated string. - managers: Optional. IDs of the managers to export as a list - or comma-separated string. - transformers: Optional. IDs of the transformers to export as - a list or comma-separated string. - logical_operators: Optional. IDs of the logical operators to - export as a list or comma-separated string. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Bytes of the ZIP file containing the exported items - - Raises: - APIError: If the API request fails - """ - return _export_integration_items( - self, - integration_name, - actions=actions, - jobs=jobs, - connectors=connectors, - managers=managers, - transformers=transformers, - logical_operators=logical_operators, - api_version=api_version, - ) - - def get_integration_affected_items( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Identifies all system items (e.g., connector instances, job - instances, playbooks) that would be affected by a change to or - deletion of this integration. Use this method to conduct impact - analysis before making breaking changes. - - Args: - integration_name: Name of the integration to check for - affected items - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the list of items affected by changes to - the specified integration - - Raises: - APIError: If the API request fails - """ - return _get_integration_affected_items( - self, integration_name, api_version - ) - - def get_agent_integrations( - self, - agent_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Returns the set of integrations currently installed and - configured on a specific agent. - - Args: - agent_id: The agent identifier - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the list of agent-based integrations - - Raises: - APIError: If the API request fails - """ - return _get_agent_integrations(self, agent_id, api_version) - - def get_integration_dependencies( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Returns the complete list of Python dependencies currently - associated with a custom integration. - - Args: - integration_name: Name of the integration to check for - dependencies - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the list of dependencies for the specified - integration - - Raises: - APIError: If the API request fails - """ - return _get_integration_dependencies( - self, integration_name, api_version - ) - - def get_integration_diff( - self, - integration_name: str, - diff_type: DiffType | None = DiffType.COMMERCIAL, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get the configuration diff of a specific integration. - - Args: - integration_name: ID of the integration to retrieve the diff for - diff_type: Type of diff to retrieve (Commercial, Production, or - Staging). Default is Commercial. - COMMERCIAL: Diff between the commercial version of the - integration and the current version in the environment. - PRODUCTION: Returns the difference between the staging - integration and its matching production version. - STAGING: Returns the difference between the production - integration and its corresponding staging version. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the configuration diff of the specified integration - - Raises: - APIError: If the API request fails - """ - return _get_integration_diff( - self, integration_name, diff_type, api_version - ) - - def get_integration_restricted_agents( - self, - integration_name: str, - required_python_version: PythonVersion, - push_request: bool = False, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Identifies remote agents that would be restricted from running - an updated version of the integration, typically due to environment - incompatibilities like unsupported Python versions. - - Args: - integration_name: Name of the integration to check for - restricted agents - required_python_version: Python version required for the - updated integration - push_request: Optional. Indicates whether the integration is - being pushed to a different mode (production/staging). - False by default. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the list of agents that would be restricted - from running the updated integration - - Raises: - APIError: If the API request fails - """ - return _get_integration_restricted_agents( - self, - integration_name, - required_python_version=required_python_version, - push_request=push_request, - api_version=api_version, - ) - - def transition_integration( - self, - integration_name: str, - target_mode: TargetMode, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Transitions an integration to a different environment - (e.g. staging to production). - - Args: - integration_name: Name of the integration to transition - target_mode: Target mode to transition the integration to. - PRODUCTION: Transition the integration to production. - STAGING: Transition the integration to staging. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the details of the transitioned integration - - Raises: - APIError: If the API request fails - """ - return _transition_integration( - self, integration_name, target_mode, api_version - ) - - def update_integration( - self, - integration_name: str, - display_name: str | None = None, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - staging: bool | None = None, - dependencies_to_remove: list[str] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Updates an existing integration's metadata. Use this method to - change the description or display image of a custom integration. - - Args: - integration_name: Name of the integration to update - display_name: Optional. The display name of the integration - (max 150 characters) - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a - base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50) - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type - (response/extension) - staging: Optional. True if the integration is in staging mode - dependencies_to_remove: Optional. List of dependencies to - remove from the integration - update_mask: Optional. Comma-separated list of fields to - update. If not provided, all non-None fields are updated. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing the details of the updated integration - - Raises: - APIError: If the API request fails - """ - return _update_integration( - self, - integration_name, - display_name=display_name, - description=description, - image_base64=image_base64, - svg_icon=svg_icon, - python_version=python_version, - parameters=parameters, - categories=categories, - integration_type=integration_type, - staging=staging, - dependencies_to_remove=dependencies_to_remove, - update_mask=update_mask, - api_version=api_version, - ) - - def update_custom_integration( - self, - integration_name: str, - display_name: str | None = None, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - staging: bool | None = None, - dependencies_to_remove: list[str] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Updates a custom integration definition, including its - parameters and dependencies. Use this method to refine the - operational behavior of a locally developed integration. - - Args: - integration_name: Name of the integration to update - display_name: Optional. The display name of the integration - (max 150 characters) - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a - base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50) - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type - (response/extension) - staging: Optional. True if the integration is in staging mode - dependencies_to_remove: Optional. List of dependencies to - remove from the integration - update_mask: Optional. Comma-separated list of fields to - update. If not provided, all non-None fields are updated. - api_version: API version to use for the request. - Default is V1BETA. - - Returns: - Dict containing: - - successful: Whether the integration was updated - successfully - - integration: The updated integration (if successful) - - dependencies: Dependency installation statuses - (if failed) - - Raises: - APIError: If the API request fails - """ - return _update_custom_integration( - self, - integration_name, - display_name=display_name, - description=description, - image_base64=image_base64, - svg_icon=svg_icon, - python_version=python_version, - parameters=parameters, - categories=categories, - integration_type=integration_type, - staging=staging, - dependencies_to_remove=dependencies_to_remove, - update_mask=update_mask, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Action methods - # ------------------------------------------------------------------------- - - def list_integration_actions( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of actions for a given integration. - - Args: - integration_name: Name of the integration to get actions for - page_size: Number of results to return per page - page_token: Token for the page to retrieve - filter_string: Filter expression to filter actions - order_by: Field to sort the actions by - expand: Comma-separated list of fields to expand in the response - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of actions instead of a dict with - actions list and nextPageToken. - - Returns: - If as_list is True: List of actions. - If as_list is False: Dict with actions list and nextPageToken. - - Raises: - APIError: If the API request fails - """ - return _list_integration_actions( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - expand=expand, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_action( - self, - integration_name: str, - action_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get details of a specific action for a given integration. - - Args: - integration_name: Name of the integration the action belongs to - action_id: ID of the action to retrieve - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified action. - - Raises: - APIError: If the API request fails - """ - return _get_integration_action( - self, - integration_name, - action_id, - api_version=api_version, - ) - - def delete_integration_action( - self, - integration_name: str, - action_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific action from a given integration. - - Args: - integration_name: Name of the integration the action belongs to - action_id: ID of the action to delete - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails - """ - return _delete_integration_action( - self, - integration_name, - action_id, - api_version=api_version, - ) - - def create_integration_action( - self, - integration_name: str, - display_name: str, - script: str, - timeout_seconds: int, - enabled: bool, - script_result_name: str, - is_async: bool, - description: str | None = None, - default_result_value: str | None = None, - async_polling_interval_seconds: int | None = None, - async_total_timeout_seconds: int | None = None, - dynamic_results: list[dict[str, Any]] | None = None, - parameters: list[dict[str, Any]] | None = None, - ai_generated: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new custom action for a given integration. - - Args: - integration_name: Name of the integration to - create the action for. - display_name: Action's display name. - Maximum 150 characters. Required. - script: Action's Python script. Maximum size 5MB. Required. - timeout_seconds: Action timeout in seconds. Maximum 1200. Required. - enabled: Whether the action is enabled or disabled. Required. - script_result_name: Field name that holds the script result. - Maximum 100 characters. Required. - is_async: Whether the action is asynchronous. Required. - description: Action's description. Maximum 400 characters. Optional. - default_result_value: Action's default result value. - Maximum 1000 characters. Optional. - async_polling_interval_seconds: Polling interval - in seconds for async actions. - Cannot exceed total timeout. Optional. - async_total_timeout_seconds: Total async timeout in seconds. Maximum - 1209600 (14 days). Optional. - dynamic_results: List of dynamic result metadata dicts. - Max 50. Optional. - parameters: List of action parameter dicts. Max 50. Optional. - ai_generated: Whether the action was generated by AI. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationAction resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_action( - self, - integration_name, - display_name, - script, - timeout_seconds, - enabled, - script_result_name, - is_async, - description=description, - default_result_value=default_result_value, - async_polling_interval_seconds=async_polling_interval_seconds, - async_total_timeout_seconds=async_total_timeout_seconds, - dynamic_results=dynamic_results, - parameters=parameters, - ai_generated=ai_generated, - api_version=api_version, - ) - - def update_integration_action( - self, - integration_name: str, - action_id: str, - display_name: str | None = None, - script: str | None = None, - timeout_seconds: int | None = None, - enabled: bool | None = None, - script_result_name: str | None = None, - is_async: bool | None = None, - description: str | None = None, - default_result_value: str | None = None, - async_polling_interval_seconds: int | None = None, - async_total_timeout_seconds: int | None = None, - dynamic_results: list[dict[str, Any]] | None = None, - parameters: list[dict[str, Any]] | None = None, - ai_generated: bool | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing custom action for a given integration. - - Only custom actions can be updated; predefined commercial actions are - immutable. - - Args: - integration_name: Name of the integration the action belongs to. - action_id: ID of the action to update. - display_name: Action's display name. Maximum 150 characters. - script: Action's Python script. Maximum size 5MB. - timeout_seconds: Action timeout in seconds. Maximum 1200. - enabled: Whether the action is enabled or disabled. - script_result_name: Field name that holds the script result. - Maximum 100 characters. - is_async: Whether the action is asynchronous. - description: Action's description. Maximum 400 characters. - default_result_value: Action's default result value. - Maximum 1000 characters. - async_polling_interval_seconds: Polling interval - in seconds for async actions. Cannot exceed total timeout. - async_total_timeout_seconds: Total async timeout in seconds. Maximum - 1209600 (14 days). - dynamic_results: List of dynamic result metadata dicts. Max 50. - parameters: List of action parameter dicts. Max 50. - ai_generated: Whether the action was generated by AI. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationAction resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_action( - self, - integration_name, - action_id, - display_name=display_name, - script=script, - timeout_seconds=timeout_seconds, - enabled=enabled, - script_result_name=script_result_name, - is_async=is_async, - description=description, - default_result_value=default_result_value, - async_polling_interval_seconds=async_polling_interval_seconds, - async_total_timeout_seconds=async_total_timeout_seconds, - dynamic_results=dynamic_results, - parameters=parameters, - ai_generated=ai_generated, - update_mask=update_mask, - api_version=api_version, - ) - - def execute_integration_action_test( - self, - integration_name: str, - test_case_id: int, - action: dict[str, Any], - scope: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Execute a test run of an integration action's script. - - Use this method to verify custom action logic, connectivity, and data - parsing against a specified integration instance and test case before - making the action available in playbooks. - - Args: - integration_name: Name of the integration the action belongs to. - test_case_id: ID of the action test case. - action: Dict containing the IntegrationAction to test. - scope: The action test scope. - integration_instance_id: The integration instance ID to use. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict with the test execution results with the following fields: - - output: The script output. - - debugOutput: The script debug output. - - resultJson: The result JSON if it exists (optional). - - resultName: The script result name (optional). - - Raises: - APIError: If the API request fails. - """ - return _execute_integration_action_test( - self, - integration_name, - test_case_id, - action, - scope, - integration_instance_id, - api_version=api_version, - ) - - def get_integration_actions_by_environment( - self, - integration_name: str, - environments: list[str], - include_widgets: bool, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """List actions executable within specified environments. - - Use this method to discover which automated tasks have active - integration instances configured for a particular - network or organizational context. - - Args: - integration_name: Name of the integration to fetch actions for. - environments: List of environments to filter actions by. - include_widgets: Whether to include widget actions in the response. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing a list of IntegrationAction objects that have - integration instances in one of the given environments. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_actions_by_environment( - self, - integration_name, - environments, - include_widgets, - api_version=api_version, - ) - - def get_integration_action_template( - self, - integration_name: str, - is_async: bool = False, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new - integration action. - - Use this method to jumpstart the development of a custom automated task - by providing boilerplate code for either synchronous or asynchronous - operations. - - Args: - integration_name: Name of the integration to fetch the template for. - is_async: Whether to fetch a template for an async action. Default - is False. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the IntegrationAction template. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_action_template( - self, - integration_name, - is_async=is_async, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Action Revisions methods - # ------------------------------------------------------------------------- - - def list_integration_action_revisions( - self, - integration_name: str, - action_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration action. - - Use this method to view the history of changes to an action, - enabling version control and the ability to rollback to - previous configurations. - - Args: - integration_name: Name of the integration the action - belongs to. - action_id: ID of the action to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of revisions instead of a - dict with revisions list and nextPageToken. - - Returns: - If as_list is True: List of action revisions. - If as_list is False: Dict with action revisions list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_action_revisions( - self, - integration_name, - action_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def delete_integration_action_revision( - self, - integration_name: str, - action_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific action revision. - - Use this method to permanently remove a revision from the - action's history. - - Args: - integration_name: Name of the integration the action - belongs to. - action_id: ID of the action the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_action_revision( - self, - integration_name, - action_id, - revision_id, - api_version=api_version, - ) - - def create_integration_action_revision( - self, - integration_name: str, - action_id: str, - action: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new revision for an integration action. - - Use this method to save a snapshot of the current action - configuration before making changes, enabling easy rollback if - needed. - - Args: - integration_name: Name of the integration the action - belongs to. - action_id: ID of the action to create a revision for. - action: The action object to save as a revision. - comment: Optional comment describing the revision. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created ActionRevision resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_action_revision( - self, - integration_name, - action_id, - action, - comment=comment, - api_version=api_version, - ) - - def rollback_integration_action_revision( - self, - integration_name: str, - action_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Rollback an integration action to a previous revision. - - Use this method to restore an action to a previously saved - state, reverting any changes made since that revision. - - Args: - integration_name: Name of the integration the action - belongs to. - action_id: ID of the action to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the rolled back IntegrationAction resource. - - Raises: - APIError: If the API request fails. - """ - return _rollback_integration_action_revision( - self, - integration_name, - action_id, - revision_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Connector methods - # ------------------------------------------------------------------------- - - def list_integration_connectors( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all connectors defined for a specific integration. - - Args: - integration_name: Name of the integration to list connectors - for. - page_size: Maximum number of connectors to return. Defaults - to 50, maximum is 1000. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter connectors. - order_by: Field to sort the connectors by. - exclude_staging: Whether to exclude staging connectors from - the response. By default, staging connectors are included. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of connectors instead of a - dict with connectors list and nextPageToken. - - Returns: - If as_list is True: List of connectors. - If as_list is False: Dict with connectors list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_connectors( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - exclude_staging=exclude_staging, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_connector( - self, - integration_name: str, - connector_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single connector for a given integration. - - Use this method to retrieve the Python script, configuration parameters, - and field mapping logic for a specific connector. - - Args: - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified IntegrationConnector. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_connector( - self, - integration_name, - connector_id, - api_version=api_version, - ) - - def delete_integration_connector( - self, - integration_name: str, - connector_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific custom connector from a given integration. - - Only custom connectors can be deleted; commercial connectors are - immutable. - - Args: - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_connector( - self, - integration_name, - connector_id, - api_version=api_version, - ) - - def create_integration_connector( - self, - integration_name: str, - display_name: str, - script: str, - timeout_seconds: int, - enabled: bool, - product_field_name: str, - event_field_name: str, - description: str | None = None, - parameters: list[dict[str, Any] | ConnectorParameter] | None = None, - rules: list[dict[str, Any] | ConnectorRule] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new custom connector for a given integration. - - Use this method to define how to fetch and parse alerts from a - unique or unofficial data source. Each connector must have a - unique display name and a functional Python script. - - Args: - integration_name: Name of the integration to create the - connector for. - display_name: Connector's display name. Required. - script: Connector's Python script. Required. - timeout_seconds: Timeout in seconds for a single script run. - Required. - enabled: Whether the connector is enabled or disabled. - Required. - product_field_name: Field name used to determine the device - product. Required. - event_field_name: Field name used to determine the event - name (sub-type). Required. - description: Connector's description. Optional. - parameters: List of ConnectorParameter instances or dicts. - Optional. - rules: List of ConnectorRule instances or dicts. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationConnector - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_connector( - self, - integration_name, - display_name, - script, - timeout_seconds, - enabled, - product_field_name, - event_field_name, - description=description, - parameters=parameters, - rules=rules, - api_version=api_version, - ) - - def update_integration_connector( - self, - integration_name: str, - connector_id: str, - display_name: str | None = None, - script: str | None = None, - timeout_seconds: int | None = None, - enabled: bool | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - description: str | None = None, - parameters: list[dict[str, Any] | ConnectorParameter] | None = None, - rules: list[dict[str, Any] | ConnectorRule] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing custom connector for a given integration. - - Only custom connectors can be updated; commercial connectors are - immutable. - - Args: - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to update. - display_name: Connector's display name. - script: Connector's Python script. - timeout_seconds: Timeout in seconds for a single script run. - enabled: Whether the connector is enabled or disabled. - product_field_name: Field name used to determine the device product. - event_field_name: Field name used to determine the event name - (sub-type). - description: Connector's description. - parameters: List of ConnectorParameter instances or dicts. - rules: List of ConnectorRule instances or dicts. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationConnector resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_connector( - self, - integration_name, - connector_id, - display_name=display_name, - script=script, - timeout_seconds=timeout_seconds, - enabled=enabled, - product_field_name=product_field_name, - event_field_name=event_field_name, - description=description, - parameters=parameters, - rules=rules, - update_mask=update_mask, - api_version=api_version, - ) - - def execute_integration_connector_test( - self, - integration_name: str, - connector: dict[str, Any], - agent_identifier: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Execute a test run of a connector's Python script. - - Use this method to verify data fetching logic, authentication, - and parsing logic before enabling the connector for production - ingestion. The full connector object is required as the test - can be run without saving the connector first. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector: Dict containing the IntegrationConnector to test. - agent_identifier: Agent identifier for remote testing. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the test execution results with the - following fields: - - outputMessage: Human-readable output message set by - the script. - - debugOutputMessage: The script debug output. - - resultJson: The result JSON if it exists (optional). - - Raises: - APIError: If the API request fails. - """ - return _execute_integration_connector_test( - self, - integration_name, - connector, - agent_identifier=agent_identifier, - api_version=api_version, - ) - - def get_integration_connector_template( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new - integration connector. - - Use this method to rapidly initialize the development of a new - connector. - - Args: - integration_name: Name of the integration to fetch the - template for. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the IntegrationConnector template. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_connector_template( - self, - integration_name, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Connector Revisions methods - # ------------------------------------------------------------------------- - - def list_integration_connector_revisions( - self, - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration connector. - - Use this method to browse the version history and identify - potential rollback targets. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of revisions instead of a - dict with revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_connector_revisions( - self, - integration_name, - connector_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def delete_integration_connector_revision( - self, - integration_name: str, - connector_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific revision for a given integration - connector. - - Use this method to clean up old or incorrect snapshots from the - version history. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_connector_revision( - self, - integration_name, - connector_id, - revision_id, - api_version=api_version, - ) - - def create_integration_connector_revision( - self, - integration_name: str, - connector_id: str, - connector: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new revision snapshot of the current integration - connector. - - Use this method to save a stable configuration before making - experimental changes. Only custom connectors can be versioned. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to create a revision for. - connector: Dict containing the IntegrationConnector to - snapshot. - comment: Comment describing the revision. Maximum 400 - characters. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created ConnectorRevision - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_connector_revision( - self, - integration_name, - connector_id, - connector, - comment=comment, - api_version=api_version, - ) - - def rollback_integration_connector_revision( - self, - integration_name: str, - connector_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Revert the current connector definition to a previously - saved revision. - - Use this method to quickly revert to a known good configuration - if an investigation or update is unsuccessful. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the ConnectorRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return _rollback_integration_connector_revision( - self, - integration_name, - connector_id, - revision_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Connector Context Properties methods - # ------------------------------------------------------------------------- - - def list_connector_context_properties( - self, - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all context properties for a specific integration - connector. - - Use this method to discover all custom data points associated - with a connector. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to list context - properties for. - page_size: Maximum number of context properties to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter context - properties. - order_by: Field to sort the context properties by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of context properties - instead of a dict with context properties list and - nextPageToken. - - Returns: - If as_list is True: List of context properties. - If as_list is False: Dict with context properties list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_connector_context_properties( - self, - integration_name, - connector_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_connector_context_property( - self, - integration_name: str, - connector_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single context property for a specific integration - connector. - - Use this method to retrieve the value of a specific key within - a connector's context. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the context property - belongs to. - context_property_id: ID of the context property to - retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified ContextProperty. - - Raises: - APIError: If the API request fails. - """ - return _get_connector_context_property( - self, - integration_name, - connector_id, - context_property_id, - api_version=api_version, - ) - - def delete_connector_context_property( - self, - integration_name: str, - connector_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific context property for a given integration - connector. - - Use this method to remove a custom data point that is no longer - relevant to the connector's context. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the context property - belongs to. - context_property_id: ID of the context property to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_connector_context_property( - self, - integration_name, - connector_id, - context_property_id, - api_version=api_version, - ) - - def create_connector_context_property( - self, - integration_name: str, - connector_id: str, - value: str, - key: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new context property for a specific integration - connector. - - Use this method to attach custom data to a connector's context. - Property keys must be unique within their context. Key values - must be 4-63 characters and match /[a-z][0-9]-/. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to create the context - property for. - value: The property value. Required. - key: The context property ID to use. Must be 4-63 - characters and match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - return _create_connector_context_property( - self, - integration_name, - connector_id, - value, - key=key, - api_version=api_version, - ) - - def update_connector_context_property( - self, - integration_name: str, - connector_id: str, - context_property_id: str, - value: str, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing context property for a given integration - connector. - - Use this method to modify the value of a previously saved key. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the context property - belongs to. - context_property_id: ID of the context property to update. - value: The new property value. Required. - update_mask: Comma-separated list of fields to update. Only - "value" is supported. If omitted, defaults to "value". - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - return _update_connector_context_property( - self, - integration_name, - connector_id, - context_property_id, - value, - update_mask=update_mask, - api_version=api_version, - ) - - def delete_all_connector_context_properties( - self, - integration_name: str, - connector_id: str, - context_id: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete all context properties for a specific integration - connector. - - Use this method to quickly clear all supplemental data from a - connector's context. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to clear context - properties from. - context_id: The context ID to remove context properties - from. Must be 4-63 characters and match /[a-z][0-9]-/. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_all_connector_context_properties( - self, - integration_name, - connector_id, - context_id=context_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Connector Instance Logs methods - # ------------------------------------------------------------------------- - - def list_connector_instance_logs( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all logs for a specific connector instance. - - Use this method to browse the execution history and diagnostic - output of a connector. Supports filtering and pagination to - efficiently navigate large volumes of log data. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to list - logs for. - page_size: Maximum number of logs to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter logs. - order_by: Field to sort the logs by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of logs instead of a dict - with logs list and nextPageToken. - - Returns: - If as_list is True: List of logs. - If as_list is False: Dict with logs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_connector_instance_logs( - self, - integration_name, - connector_id, - connector_instance_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_connector_instance_log( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - log_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single log entry for a specific connector instance. - - Use this method to retrieve a specific log entry from a - connector instance's execution, including its message, - timestamp, and severity level. Useful for auditing and detailed - troubleshooting of a specific connector run. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance the log - belongs to. - log_id: ID of the log entry to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified ConnectorLog. - - Raises: - APIError: If the API request fails. - """ - return _get_connector_instance_log( - self, - integration_name, - connector_id, - connector_instance_id, - log_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Connector Instance methods - # ------------------------------------------------------------------------- - - def list_connector_instances( - self, - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all instances for a specific integration connector. - - Use this method to discover all configured instances of a - connector. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to list instances for. - page_size: Maximum number of instances to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter instances. - order_by: Field to sort the instances by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of instances instead of a - dict with instances list and nextPageToken. - - Returns: - If as_list is True: List of connector instances. - If as_list is False: Dict with connector instances list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_connector_instances( - self, - integration_name, - connector_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_connector_instance( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single connector instance by ID. - - Use this method to retrieve the configuration of a specific - connector instance, including its parameters, schedule, and - runtime settings. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to - retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified ConnectorInstance. - - Raises: - APIError: If the API request fails. - """ - return _get_connector_instance( - self, - integration_name, - connector_id, - connector_instance_id, - api_version=api_version, - ) - - def delete_connector_instance( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific connector instance. - - Use this method to permanently remove a connector instance and - its configuration. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to - delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_connector_instance( - self, - integration_name, - connector_id, - connector_instance_id, - api_version=api_version, - ) - - def create_connector_instance( - self, - integration_name: str, - connector_id: str, - environment: str, - display_name: str, - interval_seconds: int, - timeout_seconds: int, - description: str | None = None, - parameters: list[ConnectorInstanceParameter | dict] | None = None, - agent: str | None = None, - allow_list: list[str] | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - integration_version: str | None = None, - version: str | None = None, - logging_enabled_until_unix_ms: str | None = None, - connector_instance_id: str | None = None, - enabled: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new connector instance. - - Use this method to configure a new instance of a connector with - specific parameters and schedule settings. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector to create an instance for. - environment: Environment for the instance (e.g., - "production"). - display_name: Display name for the instance. Required. - interval_seconds: Interval in seconds for recurring - execution. Required. - timeout_seconds: Timeout in seconds for execution. Required. - description: Description of the instance. Optional. - parameters: List of parameters for the instance. Optional. - agent: Agent identifier for remote execution. Optional. - allow_list: List of allowed IP addresses. Optional. - product_field_name: Product field name. Optional. - event_field_name: Event field name. Optional. - integration_version: Integration version. Optional. - version: Version. Optional. - logging_enabled_until_unix_ms: Logging enabled until - timestamp. Optional. - connector_instance_id: Custom ID for the instance. Optional. - enabled: Whether the instance is enabled. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created ConnectorInstance resource. - - Raises: - APIError: If the API request fails. - """ - return _create_connector_instance( - self, - integration_name, - connector_id, - environment, - display_name, - interval_seconds, - timeout_seconds, - description=description, - parameters=parameters, - agent=agent, - allow_list=allow_list, - product_field_name=product_field_name, - event_field_name=event_field_name, - integration_version=integration_version, - version=version, - logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, - connector_instance_id=connector_instance_id, - enabled=enabled, - api_version=api_version, - ) - - def update_connector_instance( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - display_name: str | None = None, - description: str | None = None, - interval_seconds: int | None = None, - timeout_seconds: int | None = None, - parameters: list[ConnectorInstanceParameter | dict] | None = None, - allow_list: list[str] | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - integration_version: str | None = None, - version: str | None = None, - logging_enabled_until_unix_ms: str | None = None, - enabled: bool | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing connector instance. - - Use this method to modify the configuration, parameters, or - schedule of a connector instance. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to - update. - display_name: Display name for the instance. Optional. - description: Description of the instance. Optional. - interval_seconds: Interval in seconds for recurring - execution. Optional. - timeout_seconds: Timeout in seconds for execution. Optional. - parameters: List of parameters for the instance. Optional. - agent: Agent identifier for remote execution. Optional. - allow_list: List of allowed IP addresses. Optional. - product_field_name: Product field name. Optional. - event_field_name: Event field name. Optional. - integration_version: Integration version. Optional. - version: Version. Optional. - logging_enabled_until_unix_ms: Logging enabled until - timestamp. Optional. - enabled: Whether the instance is enabled. Optional. - update_mask: Comma-separated list of fields to update. If - omitted, all provided fields will be updated. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated ConnectorInstance resource. - - Raises: - APIError: If the API request fails. - """ - return _update_connector_instance( - self, - integration_name, - connector_id, - connector_instance_id, - display_name=display_name, - description=description, - interval_seconds=interval_seconds, - timeout_seconds=timeout_seconds, - parameters=parameters, - allow_list=allow_list, - product_field_name=product_field_name, - event_field_name=event_field_name, - integration_version=integration_version, - version=version, - logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, - enabled=enabled, - update_mask=update_mask, - api_version=api_version, - ) - - def get_connector_instance_latest_definition( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Fetch the latest definition for a connector instance. - - Use this method to refresh a connector instance with the latest - connector definition from the marketplace. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to - refresh. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the refreshed ConnectorInstance with latest - definition. - - Raises: - APIError: If the API request fails. - """ - return _get_connector_instance_latest_definition( - self, - integration_name, - connector_id, - connector_instance_id, - api_version=api_version, - ) - - def set_connector_instance_logs_collection( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - enabled: bool, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Enable or disable logs collection for a connector instance. - - Use this method to control whether execution logs are collected - for a specific connector instance. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to - configure. - enabled: Whether to enable or disable logs collection. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated logs collection status. - - Raises: - APIError: If the API request fails. - """ - return _set_connector_instance_logs_collection( - self, - integration_name, - connector_id, - connector_instance_id, - enabled, - api_version=api_version, - ) - - def run_connector_instance_on_demand( - self, - integration_name: str, - connector_id: str, - connector_instance_id: str, - connector_instance: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Run a connector instance on demand for testing. - - Use this method to execute a connector instance immediately - without waiting for its scheduled run. Useful for testing - configuration changes. - - Args: - integration_name: Name of the integration the connector - belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to run. - connector_instance: The connector instance configuration to - test. Should include parameters and other settings. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the execution result, including success - status and debug output. - - Raises: - APIError: If the API request fails. - """ - return _run_connector_instance_on_demand( - self, - integration_name, - connector_id, - connector_instance_id, - connector_instance, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Job methods - # ------------------------------------------------------------------------- - - def list_integration_jobs( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all jobs defined for a specific integration. - - Use this method to browse the available background and scheduled - automation capabilities provided by a third-party connection. - - Args: - integration_name: Name of the integration to list jobs for. - page_size: Maximum number of jobs to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter jobs. Allowed - filters are: id, custom, system, author, version, - integration. - order_by: Field to sort the jobs by. - exclude_staging: Whether to exclude staging jobs from the - response. By default, staging jobs are included. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of jobs instead of a dict - with jobs list and nextPageToken. - - Returns: - If as_list is True: List of jobs. - If as_list is False: Dict with jobs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_jobs( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - exclude_staging=exclude_staging, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_job( - self, - integration_name: str, - job_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single job for a given integration. - - Use this method to retrieve the Python script, execution - parameters, and versioning information for a background - automation task. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified IntegrationJob. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_job( - self, - integration_name, - job_id, - api_version=api_version, - ) - - def delete_integration_job( - self, - integration_name: str, - job_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific custom job from a given integration. - - Only custom jobs can be deleted; commercial and system jobs - are immutable. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_job( - self, - integration_name, - job_id, - api_version=api_version, - ) - - def create_integration_job( - self, - integration_name: str, - display_name: str, - script: str, - version: int, - enabled: bool, - custom: bool, - description: str | None = None, - parameters: list[dict[str, Any] | JobParameter] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new custom job for a given integration. - - Each job must have a unique display name and a functional - Python script for its background execution. - - Args: - integration_name: Name of the integration to create the job - for. - display_name: Job's display name. Maximum 400 characters. - Required. - script: Job's Python script. Required. - version: Job's version. Required. - enabled: Whether the job is enabled. Required. - custom: Whether the job is custom or commercial. Required. - description: Job's description. Optional. - parameters: List of JobParameter instances or dicts. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationJob resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_job( - self, - integration_name, - display_name, - script, - version, - enabled, - custom, - description=description, - parameters=parameters, - api_version=api_version, - ) - - def update_integration_job( - self, - integration_name: str, - job_id: str, - display_name: str | None = None, - script: str | None = None, - version: int | None = None, - enabled: bool | None = None, - custom: bool | None = None, - description: str | None = None, - parameters: list[dict[str, Any] | JobParameter] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing custom job for a given integration. - - Use this method to modify the Python script or adjust the - parameter definitions for a job. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to update. - display_name: Job's display name. Maximum 400 characters. - script: Job's Python script. - version: Job's version. - enabled: Whether the job is enabled. - custom: Whether the job is custom or commercial. - description: Job's description. - parameters: List of JobParameter instances or dicts. - update_mask: Comma-separated list of fields to update. If - omitted, the mask is auto-generated from whichever - fields are provided. Example: "displayName,script". - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated IntegrationJob resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_job( - self, - integration_name, - job_id, - display_name=display_name, - script=script, - version=version, - enabled=enabled, - custom=custom, - description=description, - parameters=parameters, - update_mask=update_mask, - api_version=api_version, - ) - - def execute_integration_job_test( - self, - integration_name: str, - job: dict[str, Any], - agent_identifier: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Execute a test run of an integration job's Python script. - - Use this method to verify background automation logic and - connectivity before deploying the job to an instance for - recurring execution. - - Args: - integration_name: Name of the integration the job belongs - to. - job: Dict containing the IntegrationJob to test. - agent_identifier: Agent identifier for remote testing. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the test execution results with the - following fields: - - output: The script output. - - debugOutput: The script debug output. - - resultObjectJson: The result JSON if it exists - (optional). - - resultName: The script result name (optional). - - resultValue: The script result value (optional). - - Raises: - APIError: If the API request fails. - """ - return _execute_integration_job_test( - self, - integration_name, - job, - agent_identifier=agent_identifier, - api_version=api_version, - ) - - def get_integration_job_template( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new - integration job. - - Use this method to rapidly initialize the development of a new - job. - - Args: - integration_name: Name of the integration to fetch the - template for. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the IntegrationJob template. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_job_template( - self, - integration_name, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Manager methods - # ------------------------------------------------------------------------- - - def list_integration_managers( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all managers defined for a specific integration. - - Use this method to discover the library of managers available - within a particular integration's scope. - - Args: - integration_name: Name of the integration to list managers - for. - page_size: Maximum number of managers to return. Defaults to - 100, maximum is 100. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter managers. - order_by: Field to sort the managers by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of managers instead of a - dict with managers list and nextPageToken. - - Returns: - If as_list is True: List of managers. - If as_list is False: Dict with managers list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_managers( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_manager( - self, - integration_name: str, - manager_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single manager for a given integration. - - Use this method to retrieve the manager script and its metadata - for review or reference. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified IntegrationManager. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_manager( - self, - integration_name, - manager_id, - api_version=api_version, - ) - - def delete_integration_manager( - self, - integration_name: str, - manager_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific custom manager from a given integration. - - Note that deleting a manager may break components (actions, - jobs) that depend on its code. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_manager( - self, - integration_name, - manager_id, - api_version=api_version, - ) - - def create_integration_manager( - self, - integration_name: str, - display_name: str, - script: str, - description: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new custom manager for a given integration. - - Use this method to add a new shared code utility. Each manager - must have a unique display name and a script containing valid - Python logic for reuse across actions, jobs, and connectors. - - Args: - integration_name: Name of the integration to create the - manager for. - display_name: Manager's display name. Maximum 150 - characters. Required. - script: Manager's Python script. Maximum 5MB. Required. - description: Manager's description. Maximum 400 characters. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationManager - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_manager( - self, - integration_name, - display_name, - script, - description=description, - api_version=api_version, - ) - - def update_integration_manager( - self, - integration_name: str, - manager_id: str, - display_name: str | None = None, - script: str | None = None, - description: str | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing custom manager for a given integration. - - Use this method to modify the shared code, adjust its - description, or refine its logic across all components that - import it. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to update. - display_name: Manager's display name. Maximum 150 - characters. - script: Manager's Python script. Maximum 5MB. - description: Manager's description. Maximum 400 characters. - update_mask: Comma-separated list of fields to update. If - omitted, the mask is auto-generated from whichever - fields are provided. Example: "displayName,script". - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated IntegrationManager resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_manager( - self, - integration_name, - manager_id, - display_name=display_name, - script=script, - description=description, - update_mask=update_mask, - api_version=api_version, - ) - - def get_integration_manager_template( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new - integration manager. - - Use this method to quickly start developing new managers. - - Args: - integration_name: Name of the integration to fetch the - template for. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the IntegrationManager template. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_manager_template( - self, - integration_name, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Manager Revisions methods - # ------------------------------------------------------------------------- - - def list_integration_manager_revisions( - self, - integration_name: str, - manager_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration manager. - - Use this method to browse the version history and identify - previous functional states of a manager. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of revisions instead of a - dict with revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_manager_revisions( - self, - integration_name, - manager_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_manager_revision( - self, - integration_name: str, - manager_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single revision for a specific integration manager. - - Use this method to retrieve a specific snapshot of an - IntegrationManagerRevision for comparison or review. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager the revision belongs to. - revision_id: ID of the revision to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified - IntegrationManagerRevision. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_manager_revision( - self, - integration_name, - manager_id, - revision_id, - api_version=api_version, - ) - - def delete_integration_manager_revision( - self, - integration_name: str, - manager_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific revision for a given integration manager. - - Use this method to clean up obsolete snapshots and manage the - historical record of managers. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_manager_revision( - self, - integration_name, - manager_id, - revision_id, - api_version=api_version, - ) - - def create_integration_manager_revision( - self, - integration_name: str, - manager_id: str, - manager: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new revision snapshot of the current integration - manager. - - Use this method to establish a recovery point before making - significant updates to a manager. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to create a revision for. - manager: Dict containing the IntegrationManager to snapshot. - comment: Comment describing the revision. Maximum 400 - characters. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created - IntegrationManagerRevision resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_manager_revision( - self, - integration_name, - manager_id, - manager, - comment=comment, - api_version=api_version, - ) - - def rollback_integration_manager_revision( - self, - integration_name: str, - manager_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Revert the current manager definition to a previously saved - revision. - - Use this method to rapidly recover a functional state for - common code if an update causes operational issues in dependent - actions or jobs. - - Args: - integration_name: Name of the integration the manager - belongs to. - manager_id: ID of the manager to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the IntegrationManagerRevision rolled back - to. - - Raises: - APIError: If the API request fails. - """ - return _rollback_integration_manager_revision( - self, - integration_name, - manager_id, - revision_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Job Revisions methods - # ------------------------------------------------------------------------- - - def list_integration_job_revisions( - self, - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration job. - - Use this method to browse the version history of a job and - identify previous functional states. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of revisions instead of a - dict with revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_job_revisions( - self, - integration_name, - job_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def delete_integration_job_revision( - self, - integration_name: str, - job_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific revision for a given integration job. - - Use this method to clean up obsolete snapshots and manage the - historical record of jobs. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_job_revision( - self, - integration_name, - job_id, - revision_id, - api_version=api_version, - ) - - def create_integration_job_revision( - self, - integration_name: str, - job_id: str, - job: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new revision snapshot of the current integration - job. - - Use this method to establish a recovery point before making - significant updates to a job. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to create a revision for. - job: Dict containing the IntegrationJob to snapshot. - comment: Comment describing the revision. Maximum 400 - characters. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationJobRevision - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_job_revision( - self, - integration_name, - job_id, - job, - comment=comment, - api_version=api_version, - ) - - def rollback_integration_job_revision( - self, - integration_name: str, - job_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Revert the current job definition to a previously saved - revision. - - Use this method to rapidly recover a functional state if an - update causes operational issues in scheduled or background - automation. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the IntegrationJobRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return _rollback_integration_job_revision( - self, - integration_name, - job_id, - revision_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Job Instances methods - # ------------------------------------------------------------------------- - - def list_integration_job_instances( - self, - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all job instances for a specific integration job. - - Use this method to browse the active job instances and their - last execution status. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to list instances for. - page_size: Maximum number of job instances to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter job instances. - order_by: Field to sort the job instances by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of job instances instead of - a dict with job instances list and nextPageToken. - - Returns: - If as_list is True: List of job instances. - If as_list is False: Dict with job instances list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_job_instances( - self, - integration_name, - job_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_job_instance( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single job instance for a specific integration job. - - Use this method to retrieve configuration details and the - current schedule settings for a job instance. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified - IntegrationJobInstance. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_job_instance( - self, - integration_name, - job_id, - job_instance_id, - api_version=api_version, - ) - - def delete_integration_job_instance( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific job instance for a given integration job. - - Use this method to remove scheduled or configured job instances - that are no longer needed. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_job_instance( - self, - integration_name, - job_id, - job_instance_id, - api_version=api_version, - ) - - def create_integration_job_instance( - self, - integration_name: str, - job_id: str, - display_name: str, - interval_seconds: int, - enabled: bool, - advanced: bool, - description: str | None = None, - parameters: list[dict[str, Any]] | None = None, - agent: str | None = None, - advanced_config: dict[str, Any] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new job instance for a given integration job. - - Use this method to schedule a job to run at regular intervals - or with advanced cron-style scheduling. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to create an instance for. - display_name: Display name for the job instance. - interval_seconds: Interval in seconds between job runs. - enabled: Whether the job instance is enabled. - advanced: Whether advanced scheduling is used. - description: Description of the job instance. Optional. - parameters: List of parameter values for the job instance. - Optional. - agent: Agent identifier for remote execution. Optional. - advanced_config: Advanced scheduling configuration. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationJobInstance - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_job_instance( - self, - integration_name, - job_id, - display_name, - interval_seconds, - enabled, - advanced, - description=description, - parameters=parameters, - agent=agent, - advanced_config=advanced_config, - api_version=api_version, - ) - - def update_integration_job_instance( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - display_name: str | None = None, - description: str | None = None, - interval_seconds: int | None = None, - enabled: bool | None = None, - advanced: bool | None = None, - parameters: list[dict[str, Any]] | None = None, - advanced_config: dict[str, Any] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing job instance for a given integration job. - - Use this method to modify scheduling, parameters, or enable/ - disable a job instance. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to update. - display_name: Display name for the job instance. Optional. - description: Description of the job instance. Optional. - interval_seconds: Interval in seconds between job runs. - Optional. - enabled: Whether the job instance is enabled. Optional. - advanced: Whether advanced scheduling is used. Optional. - parameters: List of parameter values for the job instance. - Optional. - advanced_config: Advanced scheduling configuration. - Optional. - update_mask: Comma-separated field paths to update. If not - provided, will be auto-generated. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated IntegrationJobInstance. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_job_instance( - self, - integration_name, - job_id, - job_instance_id, - display_name=display_name, - description=description, - interval_seconds=interval_seconds, - enabled=enabled, - advanced=advanced, - parameters=parameters, - advanced_config=advanced_config, - update_mask=update_mask, - api_version=api_version, - ) - - def run_integration_job_instance_on_demand( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - parameters: list[dict[str, Any]] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Run a job instance immediately without waiting for the next - scheduled execution. - - Use this method to manually trigger a job instance for testing - or immediate data collection. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to run. - parameters: Optional parameter overrides for this run. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the result of the on-demand run. - - Raises: - APIError: If the API request fails. - """ - return _run_integration_job_instance_on_demand( - self, - integration_name, - job_id, - job_instance_id, - parameters=parameters, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Job Context Properties methods - # ------------------------------------------------------------------------- - - def list_job_context_properties( - self, - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all context properties for a specific integration job. - - Use this method to discover all custom data points associated - with a job. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to list context properties for. - page_size: Maximum number of context properties to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter context - properties. - order_by: Field to sort the context properties by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of context properties - instead of a dict with context properties list and - nextPageToken. - - Returns: - If as_list is True: List of context properties. - If as_list is False: Dict with context properties list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_job_context_properties( - self, - integration_name, - job_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_job_context_property( - self, - integration_name: str, - job_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single context property for a specific integration - job. - - Use this method to retrieve the value of a specific key within - a job's context. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to - retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified ContextProperty. - - Raises: - APIError: If the API request fails. - """ - return _get_job_context_property( - self, - integration_name, - job_id, - context_property_id, - api_version=api_version, - ) - - def delete_job_context_property( - self, - integration_name: str, - job_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific context property for a given integration - job. - - Use this method to remove a custom data point that is no longer - relevant to the job's context. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_job_context_property( - self, - integration_name, - job_id, - context_property_id, - api_version=api_version, - ) - - def create_job_context_property( - self, - integration_name: str, - job_id: str, - value: str, - key: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new context property for a specific integration - job. - - Use this method to attach custom data to a job's context. - Property keys must be unique within their context. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to create the context property for. - value: The property value. Required. - key: The context property ID to use. Must be 4-63 - characters and match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - return _create_job_context_property( - self, - integration_name, - job_id, - value, - key=key, - api_version=api_version, - ) - - def update_job_context_property( - self, - integration_name: str, - job_id: str, - context_property_id: str, - value: str, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing context property for a given integration - job. - - Use this method to modify the value of a previously saved key. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to update. - value: The new property value. Required. - update_mask: Comma-separated list of fields to update. Only - "value" is supported. If omitted, defaults to "value". - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - return _update_job_context_property( - self, - integration_name, - job_id, - context_property_id, - value, - update_mask=update_mask, - api_version=api_version, - ) - - def delete_all_job_context_properties( - self, - integration_name: str, - job_id: str, - context_id: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete all context properties for a specific integration - job. - - Use this method to quickly clear all supplemental data from a - job's context. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job to clear context properties from. - context_id: The context ID to remove context properties - from. Must be 4-63 characters and match /[a-z][0-9]-/. - Optional. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_all_job_context_properties( - self, - integration_name, - job_id, - context_id=context_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Job Instance Logs methods - # ------------------------------------------------------------------------- - - def list_job_instance_logs( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all execution logs for a specific job instance. - - Use this method to browse the historical performance and - reliability of a background automation schedule. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to list logs for. - page_size: Maximum number of logs to return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter logs. - order_by: Field to sort the logs by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of logs instead of a dict - with logs list and nextPageToken. - - Returns: - If as_list is True: List of logs. - If as_list is False: Dict with logs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_job_instance_logs( - self, - integration_name, - job_id, - job_instance_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_job_instance_log( - self, - integration_name: str, - job_id: str, - job_instance_id: str, - log_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single log entry for a specific job instance. - - Use this method to retrieve the detailed output message, - start/end times, and final status of a specific background task - execution. - - Args: - integration_name: Name of the integration the job belongs - to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance the log belongs to. - log_id: ID of the log entry to retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified JobInstanceLog. - - Raises: - APIError: If the API request fails. - """ - return _get_job_instance_log( - self, - integration_name, - job_id, - job_instance_id, - log_id, - api_version=api_version, - ) - - # ------------------------------------------------------------------------- - # Integration Instances methods - # ------------------------------------------------------------------------- - - def list_integration_instances( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all instances for a specific integration. - - Use this method to browse the configured integration instances - available for a custom or third-party product across different - environments. - - Args: - integration_name: Name of the integration to list instances - for. - page_size: Maximum number of integration instances to - return. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter integration - instances. - order_by: Field to sort the integration instances by. - api_version: API version to use for the request. Default is - V1BETA. - as_list: If True, return a list of integration instances - instead of a dict with integration instances list and - nextPageToken. - - Returns: - If as_list is True: List of integration instances. - If as_list is False: Dict with integration instances list - and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_instances( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - api_version=api_version, - as_list=as_list, - ) - - def get_integration_instance( - self, - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Get a single instance for a specific integration. - - Use this method to retrieve the specific configuration, - connection status, and environment mapping for an active - integration. - - Args: - integration_name: Name of the integration the instance - belongs to. - integration_instance_id: ID of the integration instance to - retrieve. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing details of the specified - IntegrationInstance. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_instance( - self, - integration_name, - integration_instance_id, - api_version=api_version, - ) - - def delete_integration_instance( - self, - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> None: - """Delete a specific integration instance. - - Use this method to permanently remove an integration instance - and stop all associated automated tasks (connectors or jobs) - using this instance. - - Args: - integration_name: Name of the integration the instance - belongs to. - integration_instance_id: ID of the integration instance to - delete. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_instance( - self, - integration_name, - integration_instance_id, - api_version=api_version, - ) - - def create_integration_instance( - self, - integration_name: str, - environment: str, - display_name: str | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationInstanceParameter] | None - ) = None, - agent: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Create a new integration instance for a specific - integration. - - Use this method to establish a new integration instance to a - custom or third-party security product for a specific - environment. All mandatory parameters required by the - integration definition must be provided. - - Args: - integration_name: Name of the integration to create the - instance for. - environment: The integration instance environment. Required. - display_name: The display name of the integration instance. - Automatically generated if not provided. Maximum 110 - characters. - description: The integration instance description. Maximum - 1500 characters. - parameters: List of IntegrationInstanceParameter instances - or dicts. - agent: Agent identifier for a remote integration instance. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the newly created IntegrationInstance - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_instance( - self, - integration_name, - environment, - display_name=display_name, - description=description, - parameters=parameters, - agent=agent, - api_version=api_version, - ) - - def update_integration_instance( - self, - integration_name: str, - integration_instance_id: str, - environment: str | None = None, - display_name: str | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationInstanceParameter] | None - ) = None, - agent: str | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Update an existing integration instance. - - Use this method to modify connection parameters (e.g., rotate - an API key), change the display name, or update the description - of a configured integration instance. - - Args: - integration_name: Name of the integration the instance - belongs to. - integration_instance_id: ID of the integration instance to - update. - environment: The integration instance environment. - display_name: The display name of the integration instance. - Maximum 110 characters. - description: The integration instance description. Maximum - 1500 characters. - parameters: List of IntegrationInstanceParameter instances - or dicts. - agent: Agent identifier for a remote integration instance. - update_mask: Comma-separated list of fields to update. If - omitted, the mask is auto-generated from whichever - fields are provided. Example: - "displayName,description". - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the updated IntegrationInstance resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_instance( - self, - integration_name, - integration_instance_id, - environment=environment, - display_name=display_name, - description=description, - parameters=parameters, - agent=agent, - update_mask=update_mask, - api_version=api_version, - ) - - def execute_integration_instance_test( - self, - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """Execute a connectivity test for a specific integration - instance. - - Use this method to verify that SecOps can successfully - communicate with the third-party security product using the - provided credentials. - - Args: - integration_name: Name of the integration the instance - belongs to. - integration_instance_id: ID of the integration instance to - test. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the test results with the following fields: - - successful: Indicates if the test was successful. - - message: Test result message (optional). - - Raises: - APIError: If the API request fails. - """ - return _execute_integration_instance_test( - self, - integration_name, - integration_instance_id, - api_version=api_version, - ) - - def get_integration_instance_affected_items( - self, - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, - ) -> dict[str, Any]: - """List all playbooks that depend on a specific integration - instance. - - Use this method to perform impact analysis before deleting or - significantly changing a connection configuration. + Use this method to discover which automated tasks have active + integration instances configured for a particular + network or organizational context. Args: - integration_name: Name of the integration the instance - belongs to. - integration_instance_id: ID of the integration instance to - fetch affected items for. - api_version: API version to use for the request. Default is - V1BETA. + integration_name: Name of the integration to fetch actions for. + environments: List of environments to filter actions by. + include_widgets: Whether to include widget actions in the response. + api_version: API version to use for the request. Default is V1BETA. Returns: - Dict containing a list of AffectedPlaybookResponse objects - that depend on the specified integration instance. + Dict containing a list of IntegrationAction objects that have + integration instances in one of the given environments. Raises: APIError: If the API request fails. """ - return _get_integration_instance_affected_items( + return _get_integration_actions_by_environment( self, integration_name, - integration_instance_id, + environments, + include_widgets, api_version=api_version, ) - def get_default_integration_instance( + def get_integration_action_template( self, integration_name: str, + is_async: bool = False, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Get the system default configuration for a specific - integration. - - Use this method to retrieve the baseline integration instance - details provided for a commercial product. - - Args: - integration_name: Name of the integration to fetch the - default instance for. - api_version: API version to use for the request. Default is - V1BETA. - - Returns: - Dict containing the default IntegrationInstance resource. - - Raises: - APIError: If the API request fails. - """ - return _get_default_integration_instance( - self, - integration_name, - api_version=api_version, - ) - - # -- Integration Transformers methods -- - - def list_integration_transformers( - self, - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all transformer definitions for a specific integration. - - Use this method to browse the available transformers. - - Args: - integration_name: Name of the integration to list transformers - for. - page_size: Maximum number of transformers to return. Defaults - to 100, maximum is 200. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter transformers. - order_by: Field to sort the transformers by. - exclude_staging: Whether to exclude staging transformers from - the response. By default, staging transformers are included. - expand: Expand the response with the full transformer details. - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - If as_list is True: List of transformers. - If as_list is False: Dict with transformers list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - return _list_integration_transformers( - self, - integration_name, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, - exclude_staging=exclude_staging, - expand=expand, - api_version=api_version, - ) - - def get_integration_transformer( - self, - integration_name: str, - transformer_id: str, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Get a single transformer definition for a specific integration. - - Use this method to retrieve the Python script, input parameters, - and expected input, output and usage example schema for a specific - data transformation logic within an integration. - - Args: - integration_name: Name of the integration the transformer - belongs to. - transformer_id: ID of the transformer to retrieve. - expand: Expand the response with the full transformer details. - Optional. - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - Dict containing details of the specified TransformerDefinition. - - Raises: - APIError: If the API request fails. - """ - return _get_integration_transformer( - self, - integration_name, - transformer_id, - expand=expand, - api_version=api_version, - ) - - def delete_integration_transformer( - self, - integration_name: str, - transformer_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> None: - """Delete a custom transformer definition from a given integration. - - Use this method to permanently remove an obsolete transformer from - an integration. - - Args: - integration_name: Name of the integration the transformer - belongs to. - transformer_id: ID of the transformer to delete. - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - return _delete_integration_transformer( - self, - integration_name, - transformer_id, - api_version=api_version, - ) - - def create_integration_transformer( - self, - integration_name: str, - display_name: str, - script: str, - script_timeout: str, - enabled: bool, - description: str | None = None, - parameters: list[dict[str, Any]] | None = None, - usage_example: str | None = None, - expected_output: str | None = None, - expected_input: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Create a new transformer definition for a given integration. - - Use this method to define a transformer, specifying its functional - Python script and necessary input parameters. - - Args: - integration_name: Name of the integration to create the - transformer for. - display_name: Transformer's display name. Maximum 150 characters. - Required. - script: Transformer's Python script. Required. - script_timeout: Timeout in seconds for a single script run. - Default is 60. Required. - enabled: Whether the transformer is enabled or disabled. - Required. - description: Transformer's description. Maximum 2050 characters. - Optional. - parameters: List of transformer parameter dicts. Optional. - usage_example: Transformer's usage example. Optional. - expected_output: Transformer's expected output. Optional. - expected_input: Transformer's expected input. Optional. - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - Dict containing the newly created TransformerDefinition - resource. - - Raises: - APIError: If the API request fails. - """ - return _create_integration_transformer( - self, - integration_name, - display_name, - script, - script_timeout, - enabled, - description=description, - parameters=parameters, - usage_example=usage_example, - expected_output=expected_output, - expected_input=expected_input, - api_version=api_version, - ) - - def update_integration_transformer( - self, - integration_name: str, - transformer_id: str, - display_name: str | None = None, - script: str | None = None, - script_timeout: str | None = None, - enabled: bool | None = None, - description: str | None = None, - parameters: list[dict[str, Any]] | None = None, - usage_example: str | None = None, - expected_output: str | None = None, - expected_input: str | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Update an existing transformer definition for a given - integration. - - Use this method to modify a transformation's Python script, adjust - its description, or refine its parameter definitions. - - Args: - integration_name: Name of the integration the transformer - belongs to. - transformer_id: ID of the transformer to update. - display_name: Transformer's display name. Maximum 150 - characters. - script: Transformer's Python script. - script_timeout: Timeout in seconds for a single script run. - enabled: Whether the transformer is enabled or disabled. - description: Transformer's description. Maximum 2050 characters. - parameters: List of transformer parameter dicts. When updating - existing parameters, id must be provided in each parameter. - usage_example: Transformer's usage example. - expected_output: Transformer's expected output. - expected_input: Transformer's expected input. - update_mask: Comma-separated list of fields to update. If - omitted, the mask is auto-generated from whichever fields - are provided. Example: "displayName,script". - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - Dict containing the updated TransformerDefinition resource. - - Raises: - APIError: If the API request fails. - """ - return _update_integration_transformer( - self, - integration_name, - transformer_id, - display_name=display_name, - script=script, - script_timeout=script_timeout, - enabled=enabled, - description=description, - parameters=parameters, - usage_example=usage_example, - expected_output=expected_output, - expected_input=expected_input, - update_mask=update_mask, - api_version=api_version, - ) - - def execute_integration_transformer_test( - self, - integration_name: str, - transformer: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Execute a test run of a transformer's Python script. - - Use this method to verify transformation logic and ensure data is - being parsed and formatted correctly before saving or deploying - the transformer. The full transformer object is required as the - test can be run without saving the transformer first. - - Args: - integration_name: Name of the integration the transformer - belongs to. - transformer: Dict containing the TransformerDefinition to test. - api_version: API version to use for the request. Default is - V1ALPHA. - - Returns: - Dict containing the test execution results with the following - fields: - - outputMessage: Human-readable output message set by the - script. - - debugOutputMessage: The script debug output. - - resultValue: The script result value. - - Raises: - APIError: If the API request fails. - """ - return _execute_integration_transformer_test( - self, - integration_name, - transformer, - api_version=api_version, - ) - - def get_integration_transformer_template( - self, - integration_name: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new transformer. + """Retrieve a default Python script template for a new + integration action. - Use this method to jumpstart the development of a custom data - transformation logic by providing boilerplate code. + Use this method to jumpstart the development of a custom automated task + by providing boilerplate code for either synchronous or asynchronous + operations. Args: - integration_name: Name of the integration to fetch the template - for. - api_version: API version to use for the request. Default is - V1ALPHA. + integration_name: Name of the integration to fetch the template for. + is_async: Whether to fetch a template for an async action. Default + is False. + api_version: API version to use for the request. Default is V1BETA. Returns: - Dict containing the TransformerDefinition template. + Dict containing the IntegrationAction template. Raises: APIError: If the API request fails. """ - return _get_integration_transformer_template( + return _get_integration_action_template( self, integration_name, + is_async=is_async, api_version=api_version, ) - # -- Integration Transformer Revisions methods -- + # ------------------------------------------------------------------------- + # Integration Action Revisions methods + # ------------------------------------------------------------------------- - def list_integration_transformer_revisions( + def list_integration_action_revisions( self, integration_name: str, - transformer_id: str, + action_id: str, page_size: int | None = None, page_token: str | None = None, filter_string: str | None = None, order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, as_list: bool = False, ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific transformer. + """List all revisions for a specific integration action. - Use this method to view the revision history of a transformer, - enabling you to track changes and potentially rollback to previous - versions. + Use this method to view the history of changes to an action, + enabling version control and the ability to rollback to + previous configurations. Args: - integration_name: Name of the integration the transformer + integration_name: Name of the integration the action belongs to. - transformer_id: ID of the transformer to list revisions for. - page_size: Maximum number of revisions to return. Defaults to - 100, maximum is 200. + action_id: ID of the action to list revisions for. + page_size: Maximum number of revisions to return. page_token: Page token from a previous call to retrieve the next page. filter_string: Filter expression to filter revisions. order_by: Field to sort the revisions by. api_version: API version to use for the request. Default is - V1ALPHA. - as_list: If True, automatically fetches all pages and returns - a list of revisions. If False, returns dict with revisions - and nextPageToken. + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. Returns: - If as_list is True: List of transformer revisions. - If as_list is False: Dict with revisions list and nextPageToken. + If as_list is True: List of action revisions. + If as_list is False: Dict with action revisions list and + nextPageToken. Raises: APIError: If the API request fails. """ - return _list_integration_transformer_revisions( + return _list_integration_action_revisions( self, integration_name, - transformer_id, + action_id, page_size=page_size, page_token=page_token, filter_string=filter_string, @@ -5477,25 +1116,25 @@ def list_integration_transformer_revisions( as_list=as_list, ) - def delete_integration_transformer_revision( + def delete_integration_action_revision( self, integration_name: str, - transformer_id: str, + action_id: str, revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> None: - """Delete a specific transformer revision. + """Delete a specific action revision. - Use this method to remove obsolete or incorrect revisions from - a transformer's history. + Use this method to permanently remove a revision from the + action's history. Args: - integration_name: Name of the integration the transformer + integration_name: Name of the integration the action belongs to. - transformer_id: ID of the transformer. + action_id: ID of the action the revision belongs to. revision_id: ID of the revision to delete. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: None @@ -5503,198 +1142,186 @@ def delete_integration_transformer_revision( Raises: APIError: If the API request fails. """ - return _delete_integration_transformer_revision( + return _delete_integration_action_revision( self, integration_name, - transformer_id, + action_id, revision_id, api_version=api_version, ) - def create_integration_transformer_revision( + def create_integration_action_revision( self, integration_name: str, - transformer_id: str, - transformer: dict[str, Any], + action_id: str, + action: dict[str, Any], comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Create a new revision for a transformer. + """Create a new revision for an integration action. - Use this method to create a snapshot of the transformer's current - state before making changes, enabling you to rollback if needed. + Use this method to save a snapshot of the current action + configuration before making changes, enabling easy rollback if + needed. Args: - integration_name: Name of the integration the transformer + integration_name: Name of the integration the action belongs to. - transformer_id: ID of the transformer to create a revision for. - transformer: Dict containing the TransformerDefinition to save - as a revision. - comment: Optional comment describing the revision or changes. + action_id: ID of the action to create a revision for. + action: The action object to save as a revision. + comment: Optional comment describing the revision. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the newly created TransformerRevision resource. + Dict containing the newly created ActionRevision resource. Raises: APIError: If the API request fails. """ - return _create_integration_transformer_revision( + return _create_integration_action_revision( self, integration_name, - transformer_id, - transformer, + action_id, + action, comment=comment, api_version=api_version, ) - def rollback_integration_transformer_revision( + def rollback_integration_action_revision( self, integration_name: str, - transformer_id: str, + action_id: str, revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Rollback a transformer to a previous revision. + """Rollback an integration action to a previous revision. - Use this method to restore a transformer to a previous working - state by rolling back to a specific revision. + Use this method to restore an action to a previously saved + state, reverting any changes made since that revision. Args: - integration_name: Name of the integration the transformer + integration_name: Name of the integration the action belongs to. - transformer_id: ID of the transformer to rollback. + action_id: ID of the action to rollback. revision_id: ID of the revision to rollback to. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the updated TransformerDefinition resource. + Dict containing the rolled back IntegrationAction resource. Raises: APIError: If the API request fails. """ - return _rollback_integration_transformer_revision( + return _rollback_integration_action_revision( self, integration_name, - transformer_id, + action_id, revision_id, api_version=api_version, ) - # -- Integration Logical Operators methods -- + # ------------------------------------------------------------------------- + # Integration Manager methods + # ------------------------------------------------------------------------- - def list_integration_logical_operators( + def list_integration_managers( self, integration_name: str, page_size: int | None = None, page_token: str | None = None, filter_string: str | None = None, order_by: str | None = None, - exclude_staging: bool | None = None, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, as_list: bool = False, ) -> dict[str, Any] | list[dict[str, Any]]: - """List all logical operator definitions for a specific integration. + """List all managers defined for a specific integration. - Use this method to browse the available logical operators that can - be used for conditional logic in your integration workflows. + Use this method to discover the library of managers available + within a particular integration's scope. Args: - integration_name: Name of the integration to list logical - operators for. - page_size: Maximum number of logical operators to return. - Defaults to 100, maximum is 200. + integration_name: Name of the integration to list managers + for. + page_size: Maximum number of managers to return. Defaults to + 100, maximum is 100. page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter logical operators. - order_by: Field to sort the logical operators by. - exclude_staging: Whether to exclude staging logical operators - from the response. By default, staging operators are included. - expand: Expand the response with the full logical operator - details. + filter_string: Filter expression to filter managers. + order_by: Field to sort the managers by. api_version: API version to use for the request. Default is - V1ALPHA. - as_list: If True, automatically fetches all pages and returns - a list. If False, returns dict with list and nextPageToken. + V1BETA. + as_list: If True, return a list of managers instead of a + dict with managers list and nextPageToken. Returns: - If as_list is True: List of logical operators. - If as_list is False: Dict with logicalOperators list and + If as_list is True: List of managers. + If as_list is False: Dict with managers list and nextPageToken. Raises: APIError: If the API request fails. """ - return _list_integration_logical_operators( + return _list_integration_managers( self, integration_name, page_size=page_size, page_token=page_token, filter_string=filter_string, order_by=order_by, - exclude_staging=exclude_staging, - expand=expand, api_version=api_version, as_list=as_list, ) - def get_integration_logical_operator( + def get_integration_manager( self, integration_name: str, - logical_operator_id: str, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Get a single logical operator definition for a specific integration. + """Get a single manager for a given integration. - Use this method to retrieve the Python script, input parameters, - and evaluation logic for a specific logical operator within an - integration. + Use this method to retrieve the manager script and its metadata + for review or reference. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to retrieve. - expand: Expand the response with the full logical operator - details. Optional. + manager_id: ID of the manager to retrieve. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing details of the specified LogicalOperator - definition. + Dict containing details of the specified IntegrationManager. Raises: APIError: If the API request fails. """ - return _get_integration_logical_operator( + return _get_integration_manager( self, integration_name, - logical_operator_id, - expand=expand, + manager_id, api_version=api_version, ) - def delete_integration_logical_operator( + def delete_integration_manager( self, integration_name: str, - logical_operator_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, + manager_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> None: - """Delete a custom logical operator definition from a given integration. + """Delete a specific custom manager from a given integration. - Use this method to permanently remove an obsolete logical operator - from an integration. + Note that deleting a manager may break components (actions, + jobs) that depend on its code. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to delete. + manager_id: ID of the manager to delete. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: None @@ -5702,264 +1329,237 @@ def delete_integration_logical_operator( Raises: APIError: If the API request fails. """ - return _delete_integration_logical_operator( + return _delete_integration_manager( self, integration_name, - logical_operator_id, + manager_id, api_version=api_version, ) - def create_integration_logical_operator( + def create_integration_manager( self, integration_name: str, display_name: str, script: str, - script_timeout: str, - enabled: bool, description: str | None = None, - parameters: list[dict[str, Any]] | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Create a new logical operator definition for a given integration. + """Create a new custom manager for a given integration. - Use this method to define a logical operator, specifying its - functional Python script and necessary input parameters for - conditional evaluations. + Use this method to add a new shared code utility. Each manager + must have a unique display name and a script containing valid + Python logic for reuse across actions, jobs, and connectors. Args: integration_name: Name of the integration to create the - logical operator for. - display_name: Logical operator's display name. Maximum 150 + manager for. + display_name: Manager's display name. Maximum 150 characters. Required. - script: Logical operator's Python script. Required. - script_timeout: Timeout in seconds for a single script run. - Default is 60. Required. - enabled: Whether the logical operator is enabled or disabled. - Required. - description: Logical operator's description. Maximum 2050 - characters. Optional. - parameters: List of logical operator parameter dicts. Optional. + script: Manager's Python script. Maximum 5MB. Required. + description: Manager's description. Maximum 400 characters. + Optional. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the newly created LogicalOperator resource. + Dict containing the newly created IntegrationManager + resource. Raises: APIError: If the API request fails. """ - return _create_integration_logical_operator( + return _create_integration_manager( self, integration_name, display_name, script, - script_timeout, - enabled, description=description, - parameters=parameters, api_version=api_version, ) - def update_integration_logical_operator( + def update_integration_manager( self, integration_name: str, - logical_operator_id: str, + manager_id: str, display_name: str | None = None, script: str | None = None, - script_timeout: str | None = None, - enabled: bool | None = None, description: str | None = None, - parameters: list[dict[str, Any]] | None = None, update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Update an existing logical operator definition for a given - integration. + """Update an existing custom manager for a given integration. - Use this method to modify a logical operator's Python script, - adjust its description, or refine its parameter definitions. + Use this method to modify the shared code, adjust its + description, or refine its logic across all components that + import it. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to update. - display_name: Logical operator's display name. Maximum 150 - characters. - script: Logical operator's Python script. - script_timeout: Timeout in seconds for a single script run. - enabled: Whether the logical operator is enabled or disabled. - description: Logical operator's description. Maximum 2050 + manager_id: ID of the manager to update. + display_name: Manager's display name. Maximum 150 characters. - parameters: List of logical operator parameter dicts. When - updating existing parameters, id must be provided in each - parameter. + script: Manager's Python script. Maximum 5MB. + description: Manager's description. Maximum 400 characters. update_mask: Comma-separated list of fields to update. If - omitted, the mask is auto-generated from whichever fields - are provided. Example: "displayName,script". + omitted, the mask is auto-generated from whichever + fields are provided. Example: "displayName,script". api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the updated LogicalOperator resource. + Dict containing the updated IntegrationManager resource. Raises: APIError: If the API request fails. """ - return _update_integration_logical_operator( + return _update_integration_manager( self, integration_name, - logical_operator_id, + manager_id, display_name=display_name, script=script, - script_timeout=script_timeout, - enabled=enabled, description=description, - parameters=parameters, update_mask=update_mask, api_version=api_version, ) - def execute_integration_logical_operator_test( + def get_integration_manager_template( self, integration_name: str, - logical_operator: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Execute a test run of a logical operator's Python script. + """Retrieve a default Python script template for a new + integration manager. - Use this method to verify logical operator evaluation logic and - ensure conditions are being assessed correctly before saving or - deploying the operator. The full logical operator object is - required as the test can be run without saving the operator first. + Use this method to quickly start developing new managers. Args: - integration_name: Name of the integration the logical operator - belongs to. - logical_operator: Dict containing the LogicalOperator - definition to test. + integration_name: Name of the integration to fetch the + template for. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the test execution results with the following - fields: - - outputMessage: Human-readable output message set by the - script. - - debugOutputMessage: The script debug output. - - resultValue: The script result value (True/False). + Dict containing the IntegrationManager template. Raises: APIError: If the API request fails. """ - return _execute_integration_logical_operator_test( + return _get_integration_manager_template( self, integration_name, - logical_operator, api_version=api_version, ) - def get_integration_logical_operator_template( + # ------------------------------------------------------------------------- + # Integration Manager Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_manager_revisions( self, integration_name: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, - ) -> dict[str, Any]: - """Retrieve a default Python script template for a new logical operator. + manager_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration manager. - Use this method to jumpstart the development of a custom - conditional logic by providing boilerplate code. + Use this method to browse the version history and identify + previous functional states of a manager. Args: - integration_name: Name of the integration to fetch the template - for. + integration_name: Name of the integration the manager + belongs to. + manager_id: ID of the manager to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. Returns: - Dict containing the LogicalOperator template. + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and + nextPageToken. Raises: APIError: If the API request fails. """ - return _get_integration_logical_operator_template( + return _list_integration_manager_revisions( self, integration_name, + manager_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, api_version=api_version, + as_list=as_list, ) - # -- Integration Logical Operator Revisions methods -- - - def list_integration_logical_operator_revisions( + def get_integration_manager_revision( self, integration_name: str, - logical_operator_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - as_list: bool = False, - ) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific logical operator. + manager_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single revision for a specific integration manager. - Use this method to view the revision history of a logical operator, - enabling you to track changes and potentially rollback to previous - versions. + Use this method to retrieve a specific snapshot of an + IntegrationManagerRevision for comparison or review. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to list - revisions for. - page_size: Maximum number of revisions to return. Defaults to - 100, maximum is 200. - page_token: Page token from a previous call to retrieve the - next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. + manager_id: ID of the manager the revision belongs to. + revision_id: ID of the revision to retrieve. api_version: API version to use for the request. Default is - V1ALPHA. - as_list: If True, automatically fetches all pages and returns - a list of revisions. If False, returns dict with revisions - and nextPageToken. + V1BETA. Returns: - If as_list is True: List of logical operator revisions. - If as_list is False: Dict with revisions list and nextPageToken. + Dict containing details of the specified + IntegrationManagerRevision. Raises: APIError: If the API request fails. """ - return _list_integration_logical_operator_revisions( + return _get_integration_manager_revision( self, integration_name, - logical_operator_id, - page_size=page_size, - page_token=page_token, - filter_string=filter_string, - order_by=order_by, + manager_id, + revision_id, api_version=api_version, - as_list=as_list, ) - def delete_integration_logical_operator_revision( + def delete_integration_manager_revision( self, integration_name: str, - logical_operator_id: str, + manager_id: str, revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> None: - """Delete a specific logical operator revision. + """Delete a specific revision for a given integration manager. - Use this method to remove obsolete or incorrect revisions from - a logical operator's history. + Use this method to clean up obsolete snapshots and manage the + historical record of managers. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator. + manager_id: ID of the manager the revision belongs to. revision_id: ID of the revision to delete. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: None @@ -5967,85 +1567,87 @@ def delete_integration_logical_operator_revision( Raises: APIError: If the API request fails. """ - return _delete_integration_logical_operator_revision( + return _delete_integration_manager_revision( self, integration_name, - logical_operator_id, + manager_id, revision_id, api_version=api_version, ) - def create_integration_logical_operator_revision( + def create_integration_manager_revision( self, integration_name: str, - logical_operator_id: str, - logical_operator: dict[str, Any], + manager_id: str, + manager: dict[str, Any], comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Create a new revision for a logical operator. + """Create a new revision snapshot of the current integration + manager. - Use this method to create a snapshot of the logical operator's - current state before making changes, enabling you to rollback if - needed. + Use this method to establish a recovery point before making + significant updates to a manager. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to create a - revision for. - logical_operator: Dict containing the LogicalOperator - definition to save as a revision. - comment: Optional comment describing the revision or changes. + manager_id: ID of the manager to create a revision for. + manager: Dict containing the IntegrationManager to snapshot. + comment: Comment describing the revision. Maximum 400 + characters. Optional. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the newly created LogicalOperatorRevision - resource. + Dict containing the newly created + IntegrationManagerRevision resource. Raises: APIError: If the API request fails. """ - return _create_integration_logical_operator_revision( + return _create_integration_manager_revision( self, integration_name, - logical_operator_id, - logical_operator, + manager_id, + manager, comment=comment, api_version=api_version, ) - def rollback_integration_logical_operator_revision( + def rollback_integration_manager_revision( self, integration_name: str, - logical_operator_id: str, + manager_id: str, revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, + api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Rollback a logical operator to a previous revision. + """Revert the current manager definition to a previously saved + revision. - Use this method to restore a logical operator to a previous - working state by rolling back to a specific revision. + Use this method to rapidly recover a functional state for + common code if an update causes operational issues in dependent + actions or jobs. Args: - integration_name: Name of the integration the logical operator + integration_name: Name of the integration the manager belongs to. - logical_operator_id: ID of the logical operator to rollback. + manager_id: ID of the manager to rollback. revision_id: ID of the revision to rollback to. api_version: API version to use for the request. Default is - V1ALPHA. + V1BETA. Returns: - Dict containing the updated LogicalOperator resource. + Dict containing the IntegrationManagerRevision rolled back + to. Raises: APIError: If the API request fails. """ - return _rollback_integration_logical_operator_revision( + return _rollback_integration_manager_revision( self, integration_name, - logical_operator_id, + manager_id, revision_id, api_version=api_version, ) diff --git a/src/secops/chronicle/integration/connector_context_properties.py b/src/secops/chronicle/integration/connector_context_properties.py deleted file mode 100644 index 24e59f66..00000000 --- a/src/secops/chronicle/integration/connector_context_properties.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright 2025 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. -# -"""Integration connector context properties functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_connector_context_properties( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all context properties for a specific integration connector. - - Use this method to discover all custom data points associated with a - connector. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to list context properties for. - page_size: Maximum number of context properties to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter context properties. - order_by: Field to sort the context properties by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of context properties instead of a - dict with context properties list and nextPageToken. - - Returns: - If as_list is True: List of context properties. - If as_list is False: Dict with context properties list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties" - ), - items_key="contextProperties", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_connector_context_property( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single context property for a specific integration connector. - - Use this method to retrieve the value of a specific key within a - connector's context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the context property belongs to. - context_property_id: ID of the context property to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified ContextProperty. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - ) - - -def delete_connector_context_property( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific context property for a given integration connector. - - Use this method to remove a custom data point that is no longer relevant - to the connector's context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the context property belongs to. - context_property_id: ID of the context property to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - ) - - -def create_connector_context_property( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - value: str, - key: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new context property for a specific integration connector. - - Use this method to attach custom data to a connector's context. Property - keys must be unique within their context. Key values must be 4-63 - characters and match /[a-z][0-9]-/. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to create the context property for. - value: The property value. Required. - key: The context property ID to use. Must be 4-63 characters and - match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - body = {"value": value} - - if key is not None: - body["key"] = key - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties" - ), - api_version=api_version, - json=body, - ) - - -def update_connector_context_property( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - context_property_id: str, - value: str, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing context property for a given integration connector. - - Use this method to modify the value of a previously saved key. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the context property belongs to. - context_property_id: ID of the context property to update. - value: The new property value. Required. - update_mask: Comma-separated list of fields to update. Only "value" - is supported. If omitted, defaults to "value". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - body, params = build_patch_body( - field_map=[ - ("value", "value", value), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def delete_all_connector_context_properties( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - context_id: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete all context properties for a specific integration connector. - - Use this method to quickly clear all supplemental data from a connector's - context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to clear context properties from. - context_id: The context ID to remove context properties from. Must be - 4-63 characters and match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - body = {} - - if context_id is not None: - body["contextId"] = context_id - - chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/contextProperties:clearAll" - ), - api_version=api_version, - json=body, - ) diff --git a/src/secops/chronicle/integration/connector_instance_logs.py b/src/secops/chronicle/integration/connector_instance_logs.py deleted file mode 100644 index 0be7bd25..00000000 --- a/src/secops/chronicle/integration/connector_instance_logs.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2025 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. -# -"""Integration connector instance logs functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import format_resource_id -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_connector_instance_logs( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all logs for a specific connector instance. - - Use this method to browse the execution history and diagnostic output of - a connector. Supports filtering and pagination to efficiently navigate - large volumes of log data. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to list logs for. - page_size: Maximum number of logs to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter logs. - order_by: Field to sort the logs by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of logs instead of a dict with logs - list and nextPageToken. - - Returns: - If as_list is True: List of logs. - If as_list is False: Dict with logs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}/logs" - ), - items_key="logs", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_connector_instance_log( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - log_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single log entry for a specific connector instance. - - Use this method to retrieve a specific log entry from a connector - instance's execution, including its message, timestamp, and severity - level. Useful for auditing and detailed troubleshooting of a specific - connector run. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance the log belongs to. - log_id: ID of the log entry to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified ConnectorLog. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}/logs/{log_id}" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/connector_instances.py b/src/secops/chronicle/integration/connector_instances.py deleted file mode 100644 index c6b563cc..00000000 --- a/src/secops/chronicle/integration/connector_instances.py +++ /dev/null @@ -1,489 +0,0 @@ -# Copyright 2025 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. -# -"""Integration connector instances functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import ( - APIVersion, - ConnectorInstanceParameter, -) -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_connector_instances( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all instances for a specific integration connector. - - Use this method to discover all configured instances of a connector. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to list instances for. - page_size: Maximum number of connector instances to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter connector instances. - order_by: Field to sort the connector instances by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of connector instances instead of a - dict with connector instances list and nextPageToken. - - Returns: - If as_list is True: List of connector instances. - If as_list is False: Dict with connector instances list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances" - ), - items_key="connectorInstances", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_connector_instance( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single instance for a specific integration connector. - - Use this method to retrieve the configuration and status of a specific - connector instance. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified ConnectorInstance. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}" - ), - api_version=api_version, - ) - - -def delete_connector_instance( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific connector instance. - - Use this method to permanently remove a data ingestion stream. For remote - connectors, the associated agent must be live and have no pending packages. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}" - ), - api_version=api_version, - ) - - -def create_connector_instance( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - environment: str, - display_name: str, - interval_seconds: int, - timeout_seconds: int, - description: str | None = None, - agent: str | None = None, - allow_list: list[str] | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - integration_version: str | None = None, - version: str | None = None, - logging_enabled_until_unix_ms: str | None = None, - parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, - connector_instance_id: str | None = None, - enabled: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new connector instance for a specific integration connector. - - Use this method to establish a new data ingestion stream from a security - product. Note that agent and remote cannot be patched after creation. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to create an instance for. - environment: Connector instance environment. Cannot be patched for - remote connectors. Required. - display_name: Connector instance display name. Required. - interval_seconds: Connector instance execution interval in seconds. - Required. - timeout_seconds: Timeout of a single Python script run. Required. - description: Connector instance description. Optional. - agent: Agent identifier for a remote connector instance. Cannot be - patched after creation. Optional. - allow_list: Connector instance allow list. Optional. - product_field_name: Connector's device product field. Optional. - event_field_name: Connector's event name field. Optional. - integration_version: The integration version. Optional. - version: The connector instance version. Optional. - logging_enabled_until_unix_ms: Timeout when log collecting will be - disabled. Optional. - parameters: List of ConnectorInstanceParameter instances or dicts. - Optional. - connector_instance_id: The connector instance id. Optional. - enabled: Whether the connector instance is enabled. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created ConnectorInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body = { - "environment": environment, - "displayName": display_name, - "intervalSeconds": interval_seconds, - "timeoutSeconds": timeout_seconds, - "description": description, - "agent": agent, - "allowList": allow_list, - "productFieldName": product_field_name, - "eventFieldName": event_field_name, - "integrationVersion": integration_version, - "version": version, - "loggingEnabledUntilUnixMs": logging_enabled_until_unix_ms, - "parameters": resolved_parameters, - "id": connector_instance_id, - "enabled": enabled, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances" - ), - api_version=api_version, - json=body, - ) - - -def update_connector_instance( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - display_name: str | None = None, - description: str | None = None, - interval_seconds: int | None = None, - timeout_seconds: int | None = None, - allow_list: list[str] | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - integration_version: str | None = None, - version: str | None = None, - logging_enabled_until_unix_ms: str | None = None, - parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, - enabled: bool | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing connector instance. - - Use this method to enable or disable a connector, change its display - name, or adjust its ingestion parameters. Note that agent, remote, and - environment cannot be patched after creation. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to update. - display_name: Connector instance display name. - description: Connector instance description. - interval_seconds: Connector instance execution interval in seconds. - timeout_seconds: Timeout of a single Python script run. - allow_list: Connector instance allow list. - product_field_name: Connector's device product field. - event_field_name: Connector's event name field. - integration_version: The integration version. Required on patch if - provided. - version: The connector instance version. - logging_enabled_until_unix_ms: Timeout when log collecting will be - disabled. - parameters: List of ConnectorInstanceParameter instances or dicts. - enabled: Whether the connector instance is enabled. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,intervalSeconds". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated ConnectorInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("description", "description", description), - ("intervalSeconds", "intervalSeconds", interval_seconds), - ("timeoutSeconds", "timeoutSeconds", timeout_seconds), - ("allowList", "allowList", allow_list), - ("productFieldName", "productFieldName", product_field_name), - ("eventFieldName", "eventFieldName", event_field_name), - ("integrationVersion", "integrationVersion", integration_version), - ("version", "version", version), - ( - "loggingEnabledUntilUnixMs", - "loggingEnabledUntilUnixMs", - logging_enabled_until_unix_ms, - ), - ("parameters", "parameters", resolved_parameters), - ("id", "id", connector_instance_id), - ("enabled", "enabled", enabled), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def get_connector_instance_latest_definition( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Refresh a connector instance with the latest definition. - - Use this method to discover new parameters or updated scripts for an - existing connector instance. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to refresh. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the refreshed ConnectorInstance resource. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}:fetchLatestDefinition" - ), - api_version=api_version, - ) - - -def set_connector_instance_logs_collection( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - enabled: bool, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Enable or disable debug log collection for a connector instance. - - When enabled is set to True, existing logs are cleared and a new - collection period is started. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to configure. - enabled: Whether logs collection is enabled for the connector - instance. Required. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the log enable expiration time in unix ms. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}:setLogsCollection" - ), - api_version=api_version, - json={"enabled": enabled}, - ) - - -def run_connector_instance_on_demand( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector_instance_id: str, - connector_instance: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Trigger an immediate, single execution of a connector instance. - - Use this method for testing configuration changes or manually - force-starting a data ingestion cycle. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the instance belongs to. - connector_instance_id: ID of the connector instance to run. - connector_instance: Dict containing the ConnectorInstance with - values to use for the run. Required. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the run results with the following fields: - - debugOutput: The execution debug output message. - - success: True if the execution was successful. - - sampleCases: List of alerts produced by the connector run. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/connectorInstances/" - f"{connector_instance_id}:runOnDemand" - ), - api_version=api_version, - json={"connectorInstance": connector_instance}, - ) diff --git a/src/secops/chronicle/integration/connector_revisions.py b/src/secops/chronicle/integration/connector_revisions.py deleted file mode 100644 index a5908864..00000000 --- a/src/secops/chronicle/integration/connector_revisions.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2025 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. -# -"""Integration connector revisions functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import format_resource_id -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_connector_revisions( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration connector. - - Use this method to browse the version history and identify potential - rollback targets. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of revisions instead of a dict with - revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/revisions" - ), - items_key="revisions", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def delete_integration_connector_revision( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific revision for a given integration connector. - - Use this method to clean up old or incorrect snapshots from the version - history. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/revisions/{revision_id}" - ), - api_version=api_version, - ) - - -def create_integration_connector_revision( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - connector: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new revision snapshot of the current integration connector. - - Use this method to save a stable configuration before making experimental - changes. Only custom connectors can be versioned. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to create a revision for. - connector: Dict containing the IntegrationConnector to snapshot. - comment: Comment describing the revision. Maximum 400 characters. - Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created ConnectorRevision resource. - - Raises: - APIError: If the API request fails. - """ - body = {"connector": connector} - - if comment is not None: - body["comment"] = comment - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/revisions" - ), - api_version=api_version, - json=body, - ) - - -def rollback_integration_connector_revision( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Revert the current connector definition to a previously saved revision. - - Use this method to quickly revert to a known good configuration if an - investigation or update is unsuccessful. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the ConnectorRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}/revisions/{revision_id}:rollback" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/connectors.py b/src/secops/chronicle/integration/connectors.py deleted file mode 100644 index b2c0ccd1..00000000 --- a/src/secops/chronicle/integration/connectors.py +++ /dev/null @@ -1,405 +0,0 @@ -# Copyright 2025 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. -# -"""Integration connectors functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import ( - APIVersion, - ConnectorParameter, - ConnectorRule, -) -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_connectors( - client: "ChronicleClient", - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all connectors defined for a specific integration. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to list connectors for. - page_size: Maximum number of connectors to return. Defaults to 50, - maximum is 1000. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter connectors. - order_by: Field to sort the connectors by. - exclude_staging: Whether to exclude staging connectors from the - response. By default, staging connectors are included. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of connectors instead of a dict with - connectors list and nextPageToken. - - Returns: - If as_list is True: List of connectors. - If as_list is False: Dict with connectors list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - "excludeStaging": exclude_staging, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=f"integrations/{format_resource_id(integration_name)}/connectors", - items_key="connectors", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_integration_connector( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single connector for a given integration. - - Use this method to retrieve the Python script, configuration parameters, - and field mapping logic for a specific connector. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified IntegrationConnector. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}" - ), - api_version=api_version, - ) - - -def delete_integration_connector( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific custom connector from a given integration. - - Only custom connectors can be deleted; commercial connectors are - immutable. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}" - ), - api_version=api_version, - ) - - -def create_integration_connector( - client: "ChronicleClient", - integration_name: str, - display_name: str, - script: str, - timeout_seconds: int, - enabled: bool, - product_field_name: str, - event_field_name: str, - description: str | None = None, - parameters: list[dict[str, Any] | ConnectorParameter] | None = None, - rules: list[dict[str, Any] | ConnectorRule] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new custom connector for a given integration. - - Use this method to define how to fetch and parse alerts from a unique or - unofficial data source. Each connector must have a unique display name - and a functional Python script. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to create the connector for. - display_name: Connector's display name. Required. - script: Connector's Python script. Required. - timeout_seconds: Timeout in seconds for a single script run. Required. - enabled: Whether the connector is enabled or disabled. Required. - product_field_name: Field name used to determine the device product. - Required. - event_field_name: Field name used to determine the event name - (sub-type). Required. - description: Connector's description. Optional. - parameters: List of ConnectorParameter instances or dicts. Optional. - rules: List of ConnectorRule instances or dicts. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationConnector resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, ConnectorParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - resolved_rules = ( - [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] - if rules is not None - else None - ) - - body = { - "displayName": display_name, - "script": script, - "timeoutSeconds": timeout_seconds, - "enabled": enabled, - "productFieldName": product_field_name, - "eventFieldName": event_field_name, - "description": description, - "parameters": resolved_parameters, - "rules": resolved_rules, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors" - ), - api_version=api_version, - json=body, - ) - - -def update_integration_connector( - client: "ChronicleClient", - integration_name: str, - connector_id: str, - display_name: str | None = None, - script: str | None = None, - timeout_seconds: int | None = None, - enabled: bool | None = None, - product_field_name: str | None = None, - event_field_name: str | None = None, - description: str | None = None, - parameters: list[dict[str, Any] | ConnectorParameter] | None = None, - rules: list[dict[str, Any] | ConnectorRule] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing custom connector for a given integration. - - Only custom connectors can be updated; commercial connectors are - immutable. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector_id: ID of the connector to update. - display_name: Connector's display name. - script: Connector's Python script. - timeout_seconds: Timeout in seconds for a single script run. - enabled: Whether the connector is enabled or disabled. - product_field_name: Field name used to determine the device product. - event_field_name: Field name used to determine the event name - (sub-type). - description: Connector's description. - parameters: List of ConnectorParameter instances or dicts. - rules: List of ConnectorRule instances or dicts. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationConnector resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, ConnectorParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - resolved_rules = ( - [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] - if rules is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("script", "script", script), - ("timeoutSeconds", "timeoutSeconds", timeout_seconds), - ("enabled", "enabled", enabled), - ("productFieldName", "productFieldName", product_field_name), - ("eventFieldName", "eventFieldName", event_field_name), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ("rules", "rules", resolved_rules), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors/{connector_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def execute_integration_connector_test( - client: "ChronicleClient", - integration_name: str, - connector: dict[str, Any], - agent_identifier: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Execute a test run of a connector's Python script. - - Use this method to verify data fetching logic, authentication, and parsing - logic before enabling the connector for production ingestion. The full - connector object is required as the test can be run without saving the - connector first. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the connector belongs to. - connector: Dict containing the IntegrationConnector to test. - agent_identifier: Agent identifier for remote testing. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the test execution results with the following fields: - - outputMessage: Human-readable output message set by the script. - - debugOutputMessage: The script debug output. - - resultJson: The result JSON if it exists (optional). - - Raises: - APIError: If the API request fails. - """ - body = {"connector": connector} - - if agent_identifier is not None: - body["agentIdentifier"] = agent_identifier - - return chronicle_request( - client, - method="POST", - endpoint_path=f"integrations/{format_resource_id(integration_name)}" - f"/connectors:executeTest", - api_version=api_version, - json=body, - ) - - -def get_integration_connector_template( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Retrieve a default Python script template for a - new integration connector. - - Use this method to rapidly initialize the development of a new connector. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to fetch the template for. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the IntegrationConnector template. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"connectors:fetchTemplate" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/integration_instances.py b/src/secops/chronicle/integration/integration_instances.py deleted file mode 100644 index c7e88dd7..00000000 --- a/src/secops/chronicle/integration/integration_instances.py +++ /dev/null @@ -1,403 +0,0 @@ -# Copyright 2025 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. -# -"""Integration instances functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion, IntegrationInstanceParameter -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_instances( - client: "ChronicleClient", - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all instances for a specific integration. - - Use this method to browse the configured integration instances available - for a custom or third-party product across different environments. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to list instances for. - page_size: Maximum number of integration instances to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter integration instances. - order_by: Field to sort the integration instances by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of integration instances instead of a - dict with integration instances list and nextPageToken. - - Returns: - If as_list is True: List of integration instances. - If as_list is False: Dict with integration instances list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances" - ), - items_key="integrationInstances", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_integration_instance( - client: "ChronicleClient", - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single instance for a specific integration. - - Use this method to retrieve the specific configuration, connection status, - and environment mapping for an active integration. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the instance belongs to. - integration_instance_id: ID of the integration instance to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified IntegrationInstance. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances/{integration_instance_id}" - ), - api_version=api_version, - ) - - -def delete_integration_instance( - client: "ChronicleClient", - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific integration instance. - - Use this method to permanently remove an integration instance and stop all - associated automated tasks (connectors or jobs) using this instance. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the instance belongs to. - integration_instance_id: ID of the integration instance to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances/{integration_instance_id}" - ), - api_version=api_version, - ) - - -def create_integration_instance( - client: "ChronicleClient", - integration_name: str, - environment: str, - display_name: str | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationInstanceParameter] | None - ) = None, - agent: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new integration instance for a specific integration. - - Use this method to establish a new integration instance to a custom or - third-party security product for a specific environment. All mandatory - parameters required by the integration definition must be provided. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to create the instance for. - environment: The integration instance environment. Required. - display_name: The display name of the integration instance. - Automatically generated if not provided. Maximum 110 characters. - description: The integration instance description. Maximum 1500 - characters. - parameters: List of IntegrationInstanceParameter instances or dicts. - agent: Agent identifier for a remote integration instance. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, IntegrationInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body = { - "environment": environment, - "displayName": display_name, - "description": description, - "parameters": resolved_parameters, - "agent": agent, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances" - ), - api_version=api_version, - json=body, - ) - - -def update_integration_instance( - client: "ChronicleClient", - integration_name: str, - integration_instance_id: str, - environment: str | None = None, - display_name: str | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationInstanceParameter] | None - ) = None, - agent: str | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing integration instance. - - Use this method to modify connection parameters (e.g., rotate an API - key), change the display name, or update the description of a configured - integration instance. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the instance belongs to. - integration_instance_id: ID of the integration instance to update. - environment: The integration instance environment. - display_name: The display name of the integration instance. Maximum - 110 characters. - description: The integration instance description. Maximum 1500 - characters. - parameters: List of IntegrationInstanceParameter instances or dicts. - agent: Agent identifier for a remote integration instance. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,description". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, IntegrationInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("environment", "environment", environment), - ("displayName", "displayName", display_name), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ("agent", "agent", agent), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances/{integration_instance_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def execute_integration_instance_test( - client: "ChronicleClient", - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Execute a connectivity test for a specific integration instance. - - Use this method to verify that SecOps can successfully communicate with - the third-party security product using the provided credentials. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the instance belongs to. - integration_instance_id: ID of the integration instance to test. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the test results with the following fields: - - successful: Indicates if the test was successful. - - message: Test result message (optional). - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances/{integration_instance_id}:executeTest" - ), - api_version=api_version, - ) - - -def get_integration_instance_affected_items( - client: "ChronicleClient", - integration_name: str, - integration_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """List all playbooks that depend on a specific integration instance. - - Use this method to perform impact analysis before deleting or - significantly changing a connection configuration. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the instance belongs to. - integration_instance_id: ID of the integration instance to fetch - affected items for. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing a list of AffectedPlaybookResponse objects that - depend on the specified integration instance. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances/{integration_instance_id}:fetchAffectedItems" - ), - api_version=api_version, - ) - - -def get_default_integration_instance( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get the system default configuration for a specific integration. - - Use this method to retrieve the baseline integration instance details - provided for a commercial product. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to fetch the default - instance for. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the default IntegrationInstance resource. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"integrationInstances:fetchDefaultInstance" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/integration/integrations.py deleted file mode 100644 index 4f72f5ea..00000000 --- a/src/secops/chronicle/integration/integrations.py +++ /dev/null @@ -1,686 +0,0 @@ -# Copyright 2025 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. -# -"""Integrations functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import ( - APIVersion, - DiffType, - IntegrationParam, - TargetMode, - PythonVersion, - IntegrationType, -) - -from secops.chronicle.utils.format_utils import build_patch_body -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request_bytes, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integrations( - client: "ChronicleClient", - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of integrations. - - Args: - client: ChronicleClient instance - page_size: Number of results to return per page - page_token: Token for the page to retrieve - filter_string: Filter expression to filter integrations - order_by: Field to sort the integrations by - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of integrations instead - of a dict with integrations list and nextPageToken. - - Returns: - If as_list is True: List of integrations. - If as_list is False: Dict with integrations list and - nextPageToken. - - Raises: - APIError: If the API request fails - """ - param_fields = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - param_fields = {k: v for k, v in param_fields.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path="integrations", - items_key="integrations", - page_size=page_size, - page_token=page_token, - extra_params=param_fields, - as_list=as_list, - ) - - -def get_integration( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get details of a specific integration. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to retrieve - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified integration - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}", - api_version=api_version, - ) - - -def delete_integration( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Deletes a specific custom Integration. Commercial integrations cannot - be deleted via this method. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to delete - api_version: API version to use for the request. Default is V1BETA. - - Raises: - APIError: If the API request fails - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=f"integrations/{integration_name}", - api_version=api_version, - ) - - -def create_integration( - client: "ChronicleClient", - display_name: str, - staging: bool, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[IntegrationParam | dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Creates a new custom SOAR Integration. - - Args: - client: ChronicleClient instance - display_name: Required. The display name of the integration - (max 150 characters) - staging: Required. True if the integration is in staging mode - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as - a base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50). Each entry may - be an IntegrationParam dataclass instance or a plain dict with - keys: id, defaultValue, displayName, propertyName, type, - description, mandatory. - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type (response/extension) - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the details of the newly created integration - - Raises: - APIError: If the API request fails - """ - serialised_params: list[dict[str, Any]] | None = None - if parameters is not None: - serialised_params = [ - p.to_dict() if isinstance(p, IntegrationParam) else p - for p in parameters - ] - - body_fields = { - "displayName": display_name, - "staging": staging, - "description": description, - "imageBase64": image_base64, - "svgIcon": svg_icon, - "pythonVersion": python_version, - "parameters": serialised_params, - "categories": categories, - "type": integration_type, - } - - # Remove keys with None values - body_fields = {k: v for k, v in body_fields.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path="integrations", - json=body_fields, - api_version=api_version, - ) - - -def download_integration( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> bytes: - """Exports the entire integration package as a ZIP file. Includes all - scripts, definitions, and the manifest file. Use this method for backup - or sharing. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to download - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Bytes of the ZIP file containing the integration package - - Raises: - APIError: If the API request fails - """ - return chronicle_request_bytes( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}:export", - api_version=api_version, - params={"alt": "media"}, - headers={"Accept": "application/zip"}, - ) - - -def download_integration_dependency( - client: "ChronicleClient", - integration_name: str, - dependency_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Initiates the download of a Python dependency (e.g., a library from - PyPI) for a custom integration. - - Args: - client: ChronicleClient instance - integration_name: name of the integration whose dependency to download - dependency_name: The dependency name to download. It can contain the - version or the repository. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Empty dict if the download was successful, or a dict containing error - details if the download failed - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="POST", - endpoint_path=f"integrations/{integration_name}:downloadDependency", - json={"dependency": dependency_name}, - api_version=api_version, - ) - - -def export_integration_items( - client: "ChronicleClient", - integration_name: str, - actions: list[str] | None = None, - jobs: list[str] | None = None, - connectors: list[str] | None = None, - managers: list[str] | None = None, - transformers: list[str] | None = None, - logical_operators: list[str] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> bytes: - """Exports specific items from an integration into a ZIP folder. Use - this method to extract only a subset of capabilities (e.g., just the - connectors) for reuse. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to export items from - actions: Optional. A list the ids of the actions to export. Format: - [1,2,3] - jobs: Optional. A list the ids of the jobs to export. Format: - [1,2,3] - connectors: Optional. A list the ids of the connectors to export. - Format: [1,2,3] - managers: Optional. A list the ids of the managers to export. Format: - [1,2,3] - transformers: Optional. A list the ids of the transformers to export. - Format: [1,2,3] - logical_operators: Optional. A list the ids of the logical - operators to export. Format: [1,2,3] - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Bytes of the ZIP file containing the exported integration items - - Raises: - APIError: If the API request fails - """ - export_items = { - "actions": ",".join(actions) if actions else None, - "jobs": jobs, - "connectors": connectors, - "managers": managers, - "transformers": transformers, - "logicalOperators": logical_operators, - "alt": "media", - } - - # Remove keys with None values - export_items = {k: v for k, v in export_items.items() if v is not None} - - return chronicle_request_bytes( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}:exportItems", - params=export_items, - api_version=api_version, - headers={"Accept": "application/zip"}, - ) - - -def get_integration_affected_items( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Identifies all system items (e.g., connector instances, job instances, - playbooks) that would be affected by a change to or deletion of this - integration. Use this method to conduct impact analysis before making - breaking changes. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to check for affected items - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the list of items affected by changes to the specified - integration - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}:fetchAffectedItems", - api_version=api_version, - ) - - -def get_agent_integrations( - client: "ChronicleClient", - agent_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Returns the set of integrations currently installed and configured on - a specific agent. - - Args: - client: ChronicleClient instance - agent_id: The agent identifier - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the list of agent-based integrations - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path="integrations:fetchAgentIntegrations", - params={"agentId": agent_id}, - api_version=api_version, - ) - - -def get_integration_dependencies( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Returns the complete list of Python dependencies currently associated - with a custom integration. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to check for dependencies - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the list of dependencies for the specified integration - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}:fetchDependencies", - api_version=api_version, - ) - - -def get_integration_restricted_agents( - client: "ChronicleClient", - integration_name: str, - required_python_version: PythonVersion, - push_request: bool = False, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Identifies remote agents that would be restricted from running an - updated version of the integration, typically due to environment - incompatibilities like unsupported Python versions. - - Args: - client: ChronicleClient instance - integration_name: name of the integration to check for restricted agents - required_python_version: Python version required for the updated - integration. - push_request: Optional. Indicates whether the integration is - pushed to a different mode (production/staging). False by default. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the list of agents that would be restricted from running - - Raises: - APIError: If the API request fails - """ - params_fields = { - "requiredPythonVersion": required_python_version.value, - "pushRequest": push_request, - } - - # Remove keys with None values - params_fields = {k: v for k, v in params_fields.items() if v is not None} - - return chronicle_request( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}:fetchRestrictedAgents", - params=params_fields, - api_version=api_version, - ) - - -def get_integration_diff( - client: "ChronicleClient", - integration_name: str, - diff_type: DiffType = DiffType.COMMERCIAL, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get the configuration diff of a specific integration. - - Args: - client: ChronicleClient instance - integration_name: ID of the integration to retrieve the diff for - diff_type: Type of diff to retrieve - (Commercial, Production, or Staging). Default is Commercial. - COMMERCIAL: Diff between the commercial version of the - integration and the current version in the environment. - PRODUCTION: Returns the difference between the staging - integration and its matching production version. - STAGING: Returns the difference between the production - integration and its corresponding staging version. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the configuration diff of the specified integration - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"integrations/{integration_name}" - f":fetch{diff_type.value}Diff", - api_version=api_version, - ) - - -def transition_integration( - client: "ChronicleClient", - integration_name: str, - target_mode: TargetMode, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Transition an integration to a different environment - (e.g. staging to production). - - Args: - client: ChronicleClient instance - integration_name: ID of the integration to transition - target_mode: Target mode to transition the integration to: - PRODUCTION: Transition the integration to production environment. - STAGING: Transition the integration to staging environment. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the details of the transitioned integration - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="POST", - endpoint_path=f"integrations/{integration_name}" - f":pushTo{target_mode.value}", - api_version=api_version, - ) - - -def update_integration( - client: "ChronicleClient", - integration_name: str, - display_name: str | None = None, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - staging: bool | None = None, - dependencies_to_remove: list[str] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing integration. - - Args: - client: ChronicleClient instance - integration_name: ID of the integration to update - display_name: Optional. The display name of the integration - (max 150 characters) - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a - base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50) - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type (response/extension) - staging: Optional. True if the integration is in staging mode - dependencies_to_remove: Optional. List of dependencies to - remove from the integration. - update_mask: Optional. Comma-separated list of fields to update. - If not provided, all non-None fields will be updated. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the details of the updated integration - - Raises: - APIError: If the API request fails - """ - body, params = build_patch_body( - field_map=[ - ("displayName", "display_name", display_name), - ("description", "description", description), - ("imageBase64", "image_base64", image_base64), - ("svgIcon", "svg_icon", svg_icon), - ("pythonVersion", "python_version", python_version), - ("parameters", "parameters", parameters), - ("categories", "categories", categories), - ("integrationType", "integration_type", integration_type), - ("staging", "staging", staging), - ], - update_mask=update_mask, - ) - - if dependencies_to_remove is not None: - params = params or {} - params["dependenciesToRemove"] = ",".join(dependencies_to_remove) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=f"integrations/{integration_name}", - json=body, - params=params, - api_version=api_version, - ) - - -def update_custom_integration( - client: "ChronicleClient", - integration_name: str, - display_name: str | None = None, - description: str | None = None, - image_base64: str | None = None, - svg_icon: str | None = None, - python_version: PythonVersion | None = None, - parameters: list[dict[str, Any]] | None = None, - categories: list[str] | None = None, - integration_type: IntegrationType | None = None, - staging: bool | None = None, - dependencies_to_remove: list[str] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Updates a custom integration definition, including its parameters and - dependencies. Use this method to refine the operational behavior of a - locally developed integration. - - Args: - client: ChronicleClient instance - integration_name: Name of the integration to update - display_name: Optional. The display name of the integration - (max 150 characters) - description: Optional. The integration's description - (max 1,500 characters) - image_base64: Optional. The integration's image encoded as a - base64 string (max 5 MB) - svg_icon: Optional. The integration's SVG icon (max 1 MB) - python_version: Optional. The integration's Python version - parameters: Optional. Integration parameters (max 50) - categories: Optional. Integration categories (max 50) - integration_type: Optional. The integration's type (response/extension) - staging: Optional. True if the integration is in staging mode - dependencies_to_remove: Optional. List of dependencies to remove from - the integration - update_mask: Optional. Comma-separated list of fields to update. - If not provided, all non-None fields will be updated. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing: - - successful: Whether the integration was updated successfully - - integration: The updated integration (populated if successful) - - dependencies: Dependency installation statuses - (populated if failed) - - Raises: - APIError: If the API request fails - """ - integration_fields = { - "name": integration_name, - "displayName": display_name, - "description": description, - "imageBase64": image_base64, - "svgIcon": svg_icon, - "pythonVersion": python_version, - "parameters": parameters, - "categories": categories, - "type": integration_type, - "staging": staging, - } - - # Remove keys with None values - integration_fields = { - k: v for k, v in integration_fields.items() if v is not None - } - - body = {"integration": integration_fields} - - if dependencies_to_remove is not None: - body["dependenciesToRemove"] = dependencies_to_remove - - params = {"updateMask": update_mask} if update_mask else None - - return chronicle_request( - client, - method="POST", - endpoint_path=f"integrations/" - f"{integration_name}:updateCustomIntegration", - json=body, - params=params, - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/job_context_properties.py b/src/secops/chronicle/integration/job_context_properties.py deleted file mode 100644 index de40a6f8..00000000 --- a/src/secops/chronicle/integration/job_context_properties.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright 2025 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. -# -"""Integration job context property functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_job_context_properties( - client: "ChronicleClient", - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all context properties for a specific integration job. - - Use this method to discover all custom data points associated with a job. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to list context properties for. - page_size: Maximum number of context properties to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter context properties. - order_by: Field to sort the context properties by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of context properties instead of a - dict with context properties list and nextPageToken. - - Returns: - If as_list is True: List of context properties. - If as_list is False: Dict with context properties list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties" - ), - items_key="contextProperties", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_job_context_property( - client: "ChronicleClient", - integration_name: str, - job_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single context property for a specific integration job. - - Use this method to retrieve the value of a specific key within a job's - context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified ContextProperty. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - ) - - -def delete_job_context_property( - client: "ChronicleClient", - integration_name: str, - job_id: str, - context_property_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific context property for a given integration job. - - Use this method to remove a custom data point that is no longer relevant - to the job's context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - ) - - -def create_job_context_property( - client: "ChronicleClient", - integration_name: str, - job_id: str, - value: str, - key: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new context property for a specific integration job. - - Use this method to attach custom data to a job's context. Property keys - must be unique within their context. Key values must be 4-63 characters - and match /[a-z][0-9]-/. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to create the context property for. - value: The property value. Required. - key: The context property ID to use. Must be 4-63 characters and - match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - body = {"value": value} - - if key is not None: - body["key"] = key - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties" - ), - api_version=api_version, - json=body, - ) - - -def update_job_context_property( - client: "ChronicleClient", - integration_name: str, - job_id: str, - context_property_id: str, - value: str, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing context property for a given integration job. - - Use this method to modify the value of a previously saved key. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the context property belongs to. - context_property_id: ID of the context property to update. - value: The new property value. Required. - update_mask: Comma-separated list of fields to update. Only "value" - is supported. If omitted, defaults to "value". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated ContextProperty resource. - - Raises: - APIError: If the API request fails. - """ - body, params = build_patch_body( - field_map=[ - ("value", "value", value), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties/{context_property_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def delete_all_job_context_properties( - client: "ChronicleClient", - integration_name: str, - job_id: str, - context_id: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete all context properties for a specific integration job. - - Use this method to quickly clear all supplemental data from a job's - context. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to clear context properties from. - context_id: The context ID to remove context properties from. Must be - 4-63 characters and match /[a-z][0-9]-/. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - body = {} - - if context_id is not None: - body["contextId"] = context_id - - chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/contextProperties:clearAll" - ), - api_version=api_version, - json=body, - ) diff --git a/src/secops/chronicle/integration/job_instance_logs.py b/src/secops/chronicle/integration/job_instance_logs.py deleted file mode 100644 index f58d568f..00000000 --- a/src/secops/chronicle/integration/job_instance_logs.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2025 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. -# -"""Integration job instances functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import format_resource_id -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_job_instance_logs( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all execution logs for a specific job instance. - - Use this method to browse the historical performance and reliability of a - background automation schedule. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to list logs for. - page_size: Maximum number of logs to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter logs. - order_by: Field to sort the logs by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of logs instead of a dict with logs - list and nextPageToken. - - Returns: - If as_list is True: List of logs. - If as_list is False: Dict with logs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/jobInstances/{job_instance_id}/logs" - ), - items_key="logs", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_job_instance_log( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - log_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single log entry for a specific job instance. - - Use this method to retrieve the detailed output message, start/end times, - and final status of a specific background task execution. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance the log belongs to. - log_id: ID of the log entry to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified JobInstanceLog. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/jobInstances/{job_instance_id}/logs/{log_id}" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/job_instances.py b/src/secops/chronicle/integration/job_instances.py deleted file mode 100644 index c64705ee..00000000 --- a/src/secops/chronicle/integration/job_instances.py +++ /dev/null @@ -1,399 +0,0 @@ -# Copyright 2025 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. -# -"""Integration job instances functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import ( - APIVersion, - AdvancedConfig, - IntegrationJobInstanceParameter, -) -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_job_instances( - client: "ChronicleClient", - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all job instances for a specific integration job. - - Use this method to browse the active job instances and their last - execution status. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to list instances for. - page_size: Maximum number of job instances to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter job instances. - order_by: Field to sort the job instances by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of job instances instead of a dict - with job instances list and nextPageToken. - - Returns: - If as_list is True: List of job instances. - If as_list is False: Dict with job instances list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/jobs/" - f"{job_id}/jobInstances" - ), - items_key="jobInstances", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_integration_job_instance( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single job instance for a specific integration job. - - Use this method to retrieve the execution status, last run time, and - active schedule for a specific background task. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified IntegrationJobInstance. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/jobs/" - f"{job_id}/jobInstances/{job_instance_id}" - ), - api_version=api_version, - ) - - -def delete_integration_job_instance( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific job instance for a given integration job. - - Use this method to permanently stop and remove a scheduled background task. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/jobs/" - f"{job_id}/jobInstances/{job_instance_id}" - ), - api_version=api_version, - ) - - -# pylint: disable=line-too-long -def create_integration_job_instance( - client: "ChronicleClient", - integration_name: str, - job_id: str, - display_name: str, - interval_seconds: int, - enabled: bool, - advanced: bool, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationJobInstanceParameter] | None - ) = None, - advanced_config: dict[str, Any] | AdvancedConfig | None = None, - agent: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - # pylint: enable=line-too-long - """Create a new job instance for a specific integration job. - - Use this method to schedule a new recurring background job. You must - provide a valid execution interval and any required script parameters. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to create an instance for. - display_name: Job instance display name. Required. - interval_seconds: Job execution interval in seconds. Minimum 60. - Required. - enabled: Whether the job instance is enabled. Required. - advanced: Whether the job instance uses advanced scheduling. Required. - description: Job instance description. Optional. - parameters: List of IntegrationJobInstanceParameter instances or - dicts. Optional. - advanced_config: Advanced scheduling configuration. Accepts an - AdvancedConfig instance or a raw dict. Optional. - agent: Agent identifier for remote job execution. Cannot be patched - after creation. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationJobInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - resolved_advanced_config = ( - advanced_config.to_dict() - if isinstance(advanced_config, AdvancedConfig) - else advanced_config - ) - - body = { - "displayName": display_name, - "intervalSeconds": interval_seconds, - "enabled": enabled, - "advanced": advanced, - "description": description, - "parameters": resolved_parameters, - "advancedConfig": resolved_advanced_config, - "agent": agent, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}" - f"/jobs/{job_id}/jobInstances" - ), - api_version=api_version, - json=body, - ) - - -# pylint: disable=line-too-long -def update_integration_job_instance( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - display_name: str | None = None, - interval_seconds: int | None = None, - enabled: bool | None = None, - advanced: bool | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationJobInstanceParameter] | None - ) = None, - advanced_config: dict[str, Any] | AdvancedConfig | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - # pylint: enable=line-too-long - """Update an existing job instance for a given integration job. - - Use this method to modify the execution interval, enable/disable the job - instance, or adjust the parameters passed to the background script. - - Note: The agent field cannot be updated after creation. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to update. - display_name: Job instance display name. - interval_seconds: Job execution interval in seconds. Minimum 60. - enabled: Whether the job instance is enabled. - advanced: Whether the job instance uses advanced scheduling. - description: Job instance description. - parameters: List of IntegrationJobInstanceParameter instances or - dicts. - advanced_config: Advanced scheduling configuration. Accepts an - AdvancedConfig instance or a raw dict. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,intervalSeconds". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationJobInstance resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - resolved_advanced_config = ( - advanced_config.to_dict() - if isinstance(advanced_config, AdvancedConfig) - else advanced_config - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("intervalSeconds", "intervalSeconds", interval_seconds), - ("enabled", "enabled", enabled), - ("advanced", "advanced", advanced), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ("advancedConfig", "advancedConfig", resolved_advanced_config), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/jobInstances/{job_instance_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -# pylint: disable=line-too-long -def run_integration_job_instance_on_demand( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job_instance_id: str, - parameters: ( - list[dict[str, Any] | IntegrationJobInstanceParameter] | None - ) = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - # pylint: enable=line-too-long - """Execute a job instance immediately, bypassing its normal schedule. - - Use this method to trigger an on-demand run of a job for synchronization - or troubleshooting purposes. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the instance belongs to. - job_instance_id: ID of the job instance to run on demand. - parameters: List of IntegrationJobInstanceParameter instances or - dicts. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing a success boolean indicating whether the job run - completed successfully. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, IntegrationJobInstanceParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body = {} - if resolved_parameters is not None: - body["parameters"] = resolved_parameters - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}" - f"/jobs/{job_id}/jobInstances/{job_instance_id}:runOnDemand" - ), - api_version=api_version, - json=body, - ) diff --git a/src/secops/chronicle/integration/job_revisions.py b/src/secops/chronicle/integration/job_revisions.py deleted file mode 100644 index 391daacb..00000000 --- a/src/secops/chronicle/integration/job_revisions.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright 2025 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. -# -"""Integration job revisions functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import ( - format_resource_id, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_job_revisions( - client: "ChronicleClient", - integration_name: str, - job_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration job. - - Use this method to browse the version history and identify previous - configurations of a recurring job. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of revisions instead of a dict with - revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/revisions" - ), - items_key="revisions", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def delete_integration_job_revision( - client: "ChronicleClient", - integration_name: str, - job_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific revision for a given integration job. - - Use this method to clean up obsolete snapshots and manage the historical - record of background automation tasks. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/revisions/{revision_id}" - ), - api_version=api_version, - ) - - -def create_integration_job_revision( - client: "ChronicleClient", - integration_name: str, - job_id: str, - job: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new revision snapshot of the current integration job. - - Use this method to establish a recovery point before making significant - changes to a background job's script or parameters. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to create a revision for. - job: Dict containing the IntegrationJob to snapshot. - comment: Comment describing the revision. Maximum 400 characters. - Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationJobRevision resource. - - Raises: - APIError: If the API request fails. - """ - body = {"job": job} - - if comment is not None: - body["comment"] = comment - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/revisions" - ), - api_version=api_version, - json=body, - ) - - -def rollback_integration_job_revision( - client: "ChronicleClient", - integration_name: str, - job_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Revert the current job definition to a previously saved revision. - - Use this method to rapidly recover a functional automation state if an - update causes operational issues. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the IntegrationJobRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}/revisions/{revision_id}:rollback" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/jobs.py b/src/secops/chronicle/integration/jobs.py deleted file mode 100644 index b7600a76..00000000 --- a/src/secops/chronicle/integration/jobs.py +++ /dev/null @@ -1,371 +0,0 @@ -# Copyright 2025 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. -# -"""Integration jobs functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion, JobParameter -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_jobs( - client: "ChronicleClient", - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all jobs defined for a specific integration. - - Use this method to browse the available background and scheduled automation - capabilities provided by a third-party connection. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to list jobs for. - page_size: Maximum number of jobs to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter jobs. Allowed filters are: - id, custom, system, author, version, integration. - order_by: Field to sort the jobs by. - exclude_staging: Whether to exclude staging jobs from the response. - By default, staging jobs are included. - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of jobs instead of a dict with jobs - list and nextPageToken. - - Returns: - If as_list is True: List of jobs. - If as_list is False: Dict with jobs list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - "excludeStaging": exclude_staging, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=f"integrations/{format_resource_id(integration_name)}/jobs", - items_key="jobs", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_integration_job( - client: "ChronicleClient", - integration_name: str, - job_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a single job for a given integration. - - Use this method to retrieve the Python script, execution parameters, and - versioning information for a background automation task. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to retrieve. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing details of the specified IntegrationJob. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}" - ), - api_version=api_version, - ) - - -def delete_integration_job( - client: "ChronicleClient", - integration_name: str, - job_id: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> None: - """Delete a specific custom job from a given integration. - - Only custom jobs can be deleted; commercial and system jobs are immutable. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to delete. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}" - ), - api_version=api_version, - ) - - -def create_integration_job( - client: "ChronicleClient", - integration_name: str, - display_name: str, - script: str, - version: int, - enabled: bool, - custom: bool, - description: str | None = None, - parameters: list[dict[str, Any] | JobParameter] | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Create a new custom job for a given integration. - - Each job must have a unique display name and a functional Python script - for its background execution. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to create the job for. - display_name: Job's display name. Maximum 400 characters. Required. - script: Job's Python script. Required. - version: Job's version. Required. - enabled: Whether the job is enabled. Required. - custom: Whether the job is custom or commercial. Required. - description: Job's description. Optional. - parameters: List of JobParameter instances or dicts. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the newly created IntegrationJob resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [p.to_dict() if isinstance(p, JobParameter) else p for p in parameters] - if parameters is not None - else None - ) - - body = { - "displayName": display_name, - "script": script, - "version": version, - "enabled": enabled, - "custom": custom, - "description": description, - "parameters": resolved_parameters, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/jobs" - ), - api_version=api_version, - json=body, - ) - - -def update_integration_job( - client: "ChronicleClient", - integration_name: str, - job_id: str, - display_name: str | None = None, - script: str | None = None, - version: int | None = None, - enabled: bool | None = None, - custom: bool | None = None, - description: str | None = None, - parameters: list[dict[str, Any] | JobParameter] | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Update an existing custom job for a given integration. - - Use this method to modify the Python script or adjust the parameter - definitions for a job. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job_id: ID of the job to update. - display_name: Job's display name. Maximum 400 characters. - script: Job's Python script. - version: Job's version. - enabled: Whether the job is enabled. - custom: Whether the job is custom or commercial. - description: Job's description. - parameters: List of JobParameter instances or dicts. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the updated IntegrationJob resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [p.to_dict() if isinstance(p, JobParameter) else p for p in parameters] - if parameters is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("script", "script", script), - ("version", "version", version), - ("enabled", "enabled", enabled), - ("custom", "custom", custom), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs/{job_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def execute_integration_job_test( - client: "ChronicleClient", - integration_name: str, - job: dict[str, Any], - agent_identifier: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Execute a test run of an integration job's Python script. - - Use this method to verify background automation logic and connectivity - before deploying the job to an instance for recurring execution. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the job belongs to. - job: Dict containing the IntegrationJob to test. - agent_identifier: Agent identifier for remote testing. Optional. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the test execution results with the following fields: - - output: The script output. - - debugOutput: The script debug output. - - resultObjectJson: The result JSON if it exists (optional). - - resultName: The script result name (optional). - - resultValue: The script result value (optional). - - Raises: - APIError: If the API request fails. - """ - body = {"job": job} - - if agent_identifier is not None: - body["agentIdentifier"] = agent_identifier - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs:executeTest" - ), - api_version=api_version, - json=body, - ) - - -def get_integration_job_template( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Retrieve a default Python script template for a new integration job. - - Use this method to rapidly initialize the development of a new job. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to fetch the template for. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Dict containing the IntegrationJob template. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"jobs:fetchTemplate" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/logical_operator_revisions.py b/src/secops/chronicle/integration/logical_operator_revisions.py deleted file mode 100644 index f7f00cee..00000000 --- a/src/secops/chronicle/integration/logical_operator_revisions.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright 2025 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. -# -"""Integration logical operator revisions functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import format_resource_id -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_logical_operator_revisions( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration logical operator. - - Use this method to browse through the version history of a custom logical - operator definition. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is V1ALPHA. - as_list: If True, return a list of revisions instead of a dict with - revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}/revisions" - ), - items_key="revisions", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def delete_integration_logical_operator_revision( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> None: - """Delete a specific revision for a given integration logical operator. - - Permanently removes the versioned snapshot from the logical operator's - history. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator the revision belongs - to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}/revisions/{revision_id}" - ), - api_version=api_version, - ) - - -def create_integration_logical_operator_revision( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - logical_operator: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Create a new revision snapshot of the current integration - logical operator. - - Use this method to save the current state of a logical operator - definition. Revisions can only be created for custom logical operators. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to create a revision - for. - logical_operator: Dict containing the IntegrationLogicalOperator to - snapshot. - comment: Comment describing the revision. Maximum 400 characters. - Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the newly created IntegrationLogicalOperatorRevision - resource. - - Raises: - APIError: If the API request fails. - """ - body = {"logicalOperator": logical_operator} - - if comment is not None: - body["comment"] = comment - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}/revisions" - ), - api_version=api_version, - json=body, - ) - - -def rollback_integration_logical_operator_revision( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Roll back the current logical operator to a previously saved revision. - - This updates the active logical operator definition with the configuration - stored in the specified revision. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the IntegrationLogicalOperatorRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}/revisions/" - f"{revision_id}:rollback" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/logical_operators.py b/src/secops/chronicle/integration/logical_operators.py deleted file mode 100644 index fe5da103..00000000 --- a/src/secops/chronicle/integration/logical_operators.py +++ /dev/null @@ -1,411 +0,0 @@ -# Copyright 2025 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. -# -"""Integration logical operators functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import ( - APIVersion, - IntegrationLogicalOperatorParameter, -) -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_logical_operators( - client: "ChronicleClient", - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all logical operator definitions for a specific integration. - - Use this method to discover the custom logic operators available for use - within playbook decision steps. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to list logical operators - for. - page_size: Maximum number of logical operators to return. Defaults - to 100, maximum is 200. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter logical operators. - order_by: Field to sort the logical operators by. - exclude_staging: Whether to exclude staging logical operators from - the response. By default, staging logical operators are included. - expand: Expand the response with the full logical operator details. - api_version: API version to use for the request. Default is V1ALPHA. - as_list: If True, return a list of logical operators instead of a - dict with logical operators list and nextPageToken. - - Returns: - If as_list is True: List of logical operators. - If as_list is False: Dict with logical operators list and - nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - "excludeStaging": exclude_staging, - "expand": expand, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators" - ), - items_key="logicalOperators", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def get_integration_logical_operator( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Get a single logical operator definition for a specific integration. - - Use this method to retrieve the Python script, evaluation parameters, - and description for a custom logical operator. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to retrieve. - expand: Expand the response with the full logical operator details. - Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing details of the specified IntegrationLogicalOperator. - - Raises: - APIError: If the API request fails. - """ - params = {} - if expand is not None: - params["expand"] = expand - - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}" - ), - api_version=api_version, - params=params if params else None, - ) - - -def delete_integration_logical_operator( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> None: - """Delete a specific custom logical operator from a given integration. - - Only custom logical operators can be deleted; predefined built-in - operators are immutable. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to delete. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}" - ), - api_version=api_version, - ) - - -def create_integration_logical_operator( - client: "ChronicleClient", - integration_name: str, - display_name: str, - script: str, - script_timeout: str, - enabled: bool, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationLogicalOperatorParameter] | None - ) = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Create a new custom logical operator for a given integration. - - Each operator must have a unique display name and a functional Python - script that returns a boolean result. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to create the logical - operator for. - display_name: Logical operator's display name. Maximum 150 - characters. Required. - script: Logical operator's Python script. Required. - script_timeout: Timeout in seconds for a single script run. Default - is 60. Required. - enabled: Whether the logical operator is enabled or disabled. - Required. - description: Logical operator's description. Maximum 2050 characters. - Optional. - parameters: List of IntegrationLogicalOperatorParameter instances or - dicts. Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the newly created IntegrationLogicalOperator resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - ( - p.to_dict() - if isinstance(p, IntegrationLogicalOperatorParameter) - else p - ) - for p in parameters - ] - if parameters is not None - else None - ) - - body = { - "displayName": display_name, - "script": script, - "scriptTimeout": script_timeout, - "enabled": enabled, - "description": description, - "parameters": resolved_parameters, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators" - ), - api_version=api_version, - json=body, - ) - - -def update_integration_logical_operator( - client: "ChronicleClient", - integration_name: str, - logical_operator_id: str, - display_name: str | None = None, - script: str | None = None, - script_timeout: str | None = None, - enabled: bool | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | IntegrationLogicalOperatorParameter] | None - ) = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Update an existing custom logical operator for a given integration. - - Use this method to modify the logical operator script, refine parameter - descriptions, or adjust the timeout for a logical operator. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator_id: ID of the logical operator to update. - display_name: Logical operator's display name. Maximum 150 characters. - script: Logical operator's Python script. - script_timeout: Timeout in seconds for a single script run. - enabled: Whether the logical operator is enabled or disabled. - description: Logical operator's description. Maximum 2050 characters. - parameters: List of IntegrationLogicalOperatorParameter instances or - dicts. When updating existing parameters, id must be provided - in each IntegrationLogicalOperatorParameter. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the updated IntegrationLogicalOperator resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - ( - p.to_dict() - if isinstance(p, IntegrationLogicalOperatorParameter) - else p - ) - for p in parameters - ] - if parameters is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("script", "script", script), - ("scriptTimeout", "scriptTimeout", script_timeout), - ("enabled", "enabled", enabled), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators/{logical_operator_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def execute_integration_logical_operator_test( - client: "ChronicleClient", - integration_name: str, - logical_operator: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Execute a test run of a logical operator's evaluation script. - - Use this method to verify decision logic and ensure it correctly handles - various input data before deployment in a playbook. The full logical - operator object is required as the test can be run without saving the - logical operator first. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the logical operator - belongs to. - logical_operator: Dict containing the IntegrationLogicalOperator to - test. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the test execution results with the following fields: - - outputMessage: Human-readable output message set by the script. - - debugOutputMessage: The script debug output. - - resultValue: The script result value. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators:executeTest" - ), - api_version=api_version, - json={"logicalOperator": logical_operator}, - ) - - -def get_integration_logical_operator_template( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Retrieve a default Python script template for a new logical operator. - - Use this method to rapidly initialize the development of a new logical - operator. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to fetch the template for. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the IntegrationLogicalOperator template. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"logicalOperators:fetchTemplate" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/marketplace_integrations.py b/src/secops/chronicle/integration/marketplace_integrations.py deleted file mode 100644 index fb9006cc..00000000 --- a/src/secops/chronicle/integration/marketplace_integrations.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright 2025 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. -# -"""Integrations functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_marketplace_integrations( - client: "ChronicleClient", - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """Get a list of marketplace integrations. - - Args: - client: ChronicleClient instance - page_size: Number of results to return per page - page_token: Token for the page to retrieve - filter_string: Filter expression to filter marketplace integrations - order_by: Field to sort the marketplace integrations by - api_version: API version to use for the request. Default is V1BETA. - as_list: If True, return a list of marketplace integrations instead - of a dict with marketplace integrations list and nextPageToken. - - Returns: - If as_list is True: List of marketplace integrations. - If as_list is False: Dict with marketplace integrations list and - nextPageToken. - - Raises: - APIError: If the API request fails - """ - field_map = { - "filter": filter_string, - "orderBy": order_by, - } - - return chronicle_paginated_request( - client, - api_version=api_version, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=page_size, - page_token=page_token, - extra_params={k: v for k, v in field_map.items() if v is not None}, - as_list=as_list, - ) - - -def get_marketplace_integration( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get a marketplace integration by integration name - - Args: - client: ChronicleClient instance - integration_name: Name of the marketplace integration to retrieve - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Marketplace integration details - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"marketplaceIntegrations/{integration_name}", - api_version=api_version, - ) - - -def get_marketplace_integration_diff( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Get the differences between the currently installed version of - an integration and the commercial version available in the marketplace. - - Args: - client: ChronicleClient instance - integration_name: Name of the marketplace integration to compare - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Marketplace integration diff details - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="GET", - endpoint_path=f"marketplaceIntegrations/{integration_name}" - f":fetchCommercialDiff", - api_version=api_version, - ) - - -def install_marketplace_integration( - client: "ChronicleClient", - integration_name: str, - override_mapping: bool | None = None, - staging: bool | None = None, - version: str | None = None, - restore_from_snapshot: bool | None = None, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Install a marketplace integration by integration name - - Args: - client: ChronicleClient instance - integration_name: Name of the marketplace integration to install - override_mapping: Optional. Determines if the integration should - override the ontology if already installed, if not provided, set to - false by default. - staging: Optional. Determines if the integration should be installed - as staging or production, if not provided, installed as production. - version: Optional. Determines which version of the integration - should be installed. - restore_from_snapshot: Optional. Determines if the integration - should be installed from existing integration snapshot. - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Installed marketplace integration details - - Raises: - APIError: If the API request fails - """ - field_map = { - "overrideMapping": override_mapping, - "staging": staging, - "version": version, - "restoreFromSnapshot": restore_from_snapshot, - } - - return chronicle_request( - client, - method="POST", - endpoint_path=f"marketplaceIntegrations/{integration_name}:install", - json={k: v for k, v in field_map.items() if v is not None}, - api_version=api_version, - ) - - -def uninstall_marketplace_integration( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1BETA, -) -> dict[str, Any]: - """Uninstall a marketplace integration by integration name - - Args: - client: ChronicleClient instance - integration_name: Name of the marketplace integration to uninstall - api_version: API version to use for the request. Default is V1BETA. - - Returns: - Empty dictionary if uninstallation is successful - - Raises: - APIError: If the API request fails - """ - return chronicle_request( - client, - method="POST", - endpoint_path=f"marketplaceIntegrations/{integration_name}:uninstall", - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/transformer_revisions.py b/src/secops/chronicle/integration/transformer_revisions.py deleted file mode 100644 index 397d3fbf..00000000 --- a/src/secops/chronicle/integration/transformer_revisions.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2025 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. -# -"""Integration transformer revisions functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion -from secops.chronicle.utils.format_utils import format_resource_id -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_transformer_revisions( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, - as_list: bool = False, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all revisions for a specific integration transformer. - - Use this method to browse through the version history of a custom - transformer definition. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to list revisions for. - page_size: Maximum number of revisions to return. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter revisions. - order_by: Field to sort the revisions by. - api_version: API version to use for the request. Default is V1ALPHA. - as_list: If True, return a list of revisions instead of a dict with - revisions list and nextPageToken. - - Returns: - If as_list is True: List of revisions. - If as_list is False: Dict with revisions list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}/revisions" - ), - items_key="revisions", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=as_list, - ) - - -def delete_integration_transformer_revision( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> None: - """Delete a specific revision for a given integration transformer. - - Permanently removes the versioned snapshot from the transformer's history. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer the revision belongs to. - revision_id: ID of the revision to delete. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}/revisions/{revision_id}" - ), - api_version=api_version, - ) - - -def create_integration_transformer_revision( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - transformer: dict[str, Any], - comment: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Create a new revision snapshot of the current integration transformer. - - Use this method to save the current state of a transformer definition. - Revisions can only be created for custom transformers. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to create a revision for. - transformer: Dict containing the TransformerDefinition to snapshot. - comment: Comment describing the revision. Maximum 400 characters. - Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the newly created TransformerRevision resource. - - Raises: - APIError: If the API request fails. - """ - body = {"transformer": transformer} - - if comment is not None: - body["comment"] = comment - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}/revisions" - ), - api_version=api_version, - json=body, - ) - - -def rollback_integration_transformer_revision( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - revision_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Roll back the current transformer definition to - a previously saved revision. - - This updates the active transformer definition with the configuration - stored in the specified revision. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to rollback. - revision_id: ID of the revision to rollback to. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the TransformerRevision rolled back to. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}/revisions/{revision_id}:rollback" - ), - api_version=api_version, - ) diff --git a/src/secops/chronicle/integration/transformers.py b/src/secops/chronicle/integration/transformers.py deleted file mode 100644 index a2a0b817..00000000 --- a/src/secops/chronicle/integration/transformers.py +++ /dev/null @@ -1,406 +0,0 @@ -# Copyright 2025 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. -# -"""Integration transformers functionality for Chronicle.""" - -from typing import Any, TYPE_CHECKING - -from secops.chronicle.models import APIVersion, TransformerDefinitionParameter -from secops.chronicle.utils.format_utils import ( - format_resource_id, - build_patch_body, -) -from secops.chronicle.utils.request_utils import ( - chronicle_paginated_request, - chronicle_request, -) - -if TYPE_CHECKING: - from secops.chronicle.client import ChronicleClient - - -def list_integration_transformers( - client: "ChronicleClient", - integration_name: str, - page_size: int | None = None, - page_token: str | None = None, - filter_string: str | None = None, - order_by: str | None = None, - exclude_staging: bool | None = None, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any] | list[dict[str, Any]]: - """List all transformer definitions for a specific integration. - - Use this method to browse the available transformers. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to list transformers for. - page_size: Maximum number of transformers to return. Defaults to 100, - maximum is 200. - page_token: Page token from a previous call to retrieve the next page. - filter_string: Filter expression to filter transformers. - order_by: Field to sort the transformers by. - exclude_staging: Whether to exclude staging transformers from the - response. By default, staging transformers are included. - expand: Expand the response with the full transformer details. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - If as_list is True: List of transformers. - If as_list is False: Dict with transformers list and nextPageToken. - - Raises: - APIError: If the API request fails. - """ - extra_params = { - "filter": filter_string, - "orderBy": order_by, - "excludeStaging": exclude_staging, - "expand": expand, - } - - # Remove keys with None values - extra_params = {k: v for k, v in extra_params.items() if v is not None} - - return chronicle_paginated_request( - client, - api_version=api_version, - path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers" - ), - items_key="transformers", - page_size=page_size, - page_token=page_token, - extra_params=extra_params, - as_list=False, - ) - - -def get_integration_transformer( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - expand: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Get a single transformer definition for a specific integration. - - Use this method to retrieve the Python script, input parameters, and - expected input, output and usage example schema for a specific data - transformation logic within an integration. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to retrieve. - expand: Expand the response with the full transformer details. - Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing details of the specified TransformerDefinition. - - Raises: - APIError: If the API request fails. - """ - params = {} - if expand is not None: - params["expand"] = expand - - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}" - ), - api_version=api_version, - params=params if params else None, - ) - - -def delete_integration_transformer( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> None: - """Delete a custom transformer definition from a given integration. - - Use this method to permanently remove an obsolete transformer from an - integration. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to delete. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - None - - Raises: - APIError: If the API request fails. - """ - chronicle_request( - client, - method="DELETE", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}" - ), - api_version=api_version, - ) - - -def create_integration_transformer( - client: "ChronicleClient", - integration_name: str, - display_name: str, - script: str, - script_timeout: str, - enabled: bool, - description: str | None = None, - parameters: ( - list[dict[str, Any] | TransformerDefinitionParameter] | None - ) = None, - usage_example: str | None = None, - expected_output: str | None = None, - expected_input: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Create a new transformer definition for a given integration. - - Use this method to define a transformer, specifying its functional Python - script and necessary input parameters. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to create the transformer - for. - display_name: Transformer's display name. Maximum 150 characters. - Required. - script: Transformer's Python script. Required. - script_timeout: Timeout in seconds for a single script run. Default - is 60. Required. - enabled: Whether the transformer is enabled or disabled. Required. - description: Transformer's description. Maximum 2050 characters. - Optional. - parameters: List of TransformerDefinitionParameter instances or - dicts. Optional. - usage_example: Transformer's usage example. Optional. - expected_output: Transformer's expected output. Optional. - expected_input: Transformer's expected input. Optional. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the newly created TransformerDefinition resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body = { - "displayName": display_name, - "script": script, - "scriptTimeout": script_timeout, - "enabled": enabled, - "description": description, - "parameters": resolved_parameters, - "usageExample": usage_example, - "expectedOutput": expected_output, - "expectedInput": expected_input, - } - - # Remove keys with None values - body = {k: v for k, v in body.items() if v is not None} - - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/transformers" - ), - api_version=api_version, - json=body, - ) - - -def update_integration_transformer( - client: "ChronicleClient", - integration_name: str, - transformer_id: str, - display_name: str | None = None, - script: str | None = None, - script_timeout: str | None = None, - enabled: bool | None = None, - description: str | None = None, - parameters: ( - list[dict[str, Any] | TransformerDefinitionParameter] | None - ) = None, - usage_example: str | None = None, - expected_output: str | None = None, - expected_input: str | None = None, - update_mask: str | None = None, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Update an existing transformer definition for a given integration. - - Use this method to modify a transformation's Python script, adjust its - description, or refine its parameter definitions. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer_id: ID of the transformer to update. - display_name: Transformer's display name. Maximum 150 characters. - script: Transformer's Python script. - script_timeout: Timeout in seconds for a single script run. - enabled: Whether the transformer is enabled or disabled. - description: Transformer's description. Maximum 2050 characters. - parameters: List of TransformerDefinitionParameter instances or - dicts. When updating existing parameters, id must be provided - in each TransformerDefinitionParameter. - usage_example: Transformer's usage example. - expected_output: Transformer's expected output. - expected_input: Transformer's expected input. - update_mask: Comma-separated list of fields to update. If omitted, - the mask is auto-generated from whichever fields are provided. - Example: "displayName,script". - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the updated TransformerDefinition resource. - - Raises: - APIError: If the API request fails. - """ - resolved_parameters = ( - [ - p.to_dict() if isinstance(p, TransformerDefinitionParameter) else p - for p in parameters - ] - if parameters is not None - else None - ) - - body, params = build_patch_body( - field_map=[ - ("displayName", "displayName", display_name), - ("script", "script", script), - ("scriptTimeout", "scriptTimeout", script_timeout), - ("enabled", "enabled", enabled), - ("description", "description", description), - ("parameters", "parameters", resolved_parameters), - ("usageExample", "usageExample", usage_example), - ("expectedOutput", "expectedOutput", expected_output), - ("expectedInput", "expectedInput", expected_input), - ], - update_mask=update_mask, - ) - - return chronicle_request( - client, - method="PATCH", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers/{transformer_id}" - ), - api_version=api_version, - json=body, - params=params, - ) - - -def execute_integration_transformer_test( - client: "ChronicleClient", - integration_name: str, - transformer: dict[str, Any], - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Execute a test run of a transformer's Python script. - - Use this method to verify transformation logic and ensure data is being - parsed and formatted correctly before saving or deploying the transformer. - The full transformer object is required as the test can be run without - saving the transformer first. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration the transformer belongs to. - transformer: Dict containing the TransformerDefinition to test. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the test execution results with the following fields: - - outputMessage: Human-readable output message set by the script. - - debugOutputMessage: The script debug output. - - resultValue: The script result value. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="POST", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers:executeTest" - ), - api_version=api_version, - json={"transformer": transformer}, - ) - - -def get_integration_transformer_template( - client: "ChronicleClient", - integration_name: str, - api_version: APIVersion | None = APIVersion.V1ALPHA, -) -> dict[str, Any]: - """Retrieve a default Python script template for a new transformer. - - Use this method to jumpstart the development of a custom data - transformation logic by providing boilerplate code. - - Args: - client: ChronicleClient instance. - integration_name: Name of the integration to fetch the template for. - api_version: API version to use for the request. Default is V1ALPHA. - - Returns: - Dict containing the TransformerDefinition template. - - Raises: - APIError: If the API request fails. - """ - return chronicle_request( - client, - method="GET", - endpoint_path=( - f"integrations/{format_resource_id(integration_name)}/" - f"transformers:fetchTemplate" - ), - api_version=api_version, - ) diff --git a/src/secops/cli/commands/integration/connector_context_properties.py b/src/secops/cli/commands/integration/connector_context_properties.py deleted file mode 100644 index 46b2d936..00000000 --- a/src/secops/cli/commands/integration/connector_context_properties.py +++ /dev/null @@ -1,375 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI connector context properties commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_connector_context_properties_command(subparsers): - """Setup connector context properties command""" - properties_parser = subparsers.add_parser( - "connector-context-properties", - help="Manage connector context properties", - ) - lvl1 = properties_parser.add_subparsers( - dest="connector_context_properties_command", - help="Connector context properties command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", help="List connector context properties" - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - list_parser.add_argument( - "--context-id", - type=str, - help="Context ID to filter properties", - dest="context_id", - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing properties", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing properties", - dest="order_by", - ) - list_parser.set_defaults( - func=handle_connector_context_properties_list_command, - ) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get a specific connector context property" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - get_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - get_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to get", - dest="property_id", - required=True, - ) - get_parser.set_defaults( - func=handle_connector_context_properties_get_command - ) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete a connector context property" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - delete_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - delete_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to delete", - dest="property_id", - required=True, - ) - delete_parser.set_defaults( - func=handle_connector_context_properties_delete_command - ) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new connector context property" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - create_parser.add_argument( - "--context-id", - type=str, - help="Context ID for the property", - dest="context_id", - required=True, - ) - create_parser.add_argument( - "--key", - type=str, - help="Key for the property", - dest="key", - required=True, - ) - create_parser.add_argument( - "--value", - type=str, - help="Value for the property", - dest="value", - required=True, - ) - create_parser.set_defaults( - func=handle_connector_context_properties_create_command - ) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update a connector context property" - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - update_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - update_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to update", - dest="property_id", - required=True, - ) - update_parser.add_argument( - "--value", - type=str, - help="New value for the property", - dest="value", - required=True, - ) - update_parser.set_defaults( - func=handle_connector_context_properties_update_command - ) - - # clear-all command - clear_parser = lvl1.add_parser( - "clear-all", help="Delete all connector context properties" - ) - clear_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - clear_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - clear_parser.add_argument( - "--context-id", - type=str, - help="Context ID to clear all properties for", - dest="context_id", - required=True, - ) - clear_parser.set_defaults( - func=handle_connector_context_properties_clear_command - ) - - -def handle_connector_context_properties_list_command(args, chronicle): - """Handle connector context properties list command""" - try: - out = chronicle.list_connector_context_properties( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error listing connector context properties: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_connector_context_properties_get_command(args, chronicle): - """Handle connector context property get command""" - try: - out = chronicle.get_connector_context_property( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - context_property_id=args.property_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting connector context property: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_context_properties_delete_command(args, chronicle): - """Handle connector context property delete command""" - try: - chronicle.delete_connector_context_property( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - context_property_id=args.property_id, - ) - print( - f"Connector context property " - f"{args.property_id} deleted successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error deleting connector context property: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_connector_context_properties_create_command(args, chronicle): - """Handle connector context property create command""" - try: - out = chronicle.create_connector_context_property( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - key=args.key, - value=args.value, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error creating connector context property: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_connector_context_properties_update_command(args, chronicle): - """Handle connector context property update command""" - try: - out = chronicle.update_connector_context_property( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - context_property_id=args.property_id, - value=args.value, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error updating connector context property: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_connector_context_properties_clear_command(args, chronicle): - """Handle clear all connector context properties command""" - try: - chronicle.delete_all_connector_context_properties( - integration_name=args.integration_name, - connector_id=args.connector_id, - context_id=args.context_id, - ) - print( - f"All connector context properties for context " - f"{args.context_id} cleared successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error clearing connector context properties: {e}", file=sys.stderr - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instance_logs.py b/src/secops/cli/commands/integration/connector_instance_logs.py deleted file mode 100644 index b67e35f2..00000000 --- a/src/secops/cli/commands/integration/connector_instance_logs.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI connector instance logs commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_connector_instance_logs_command(subparsers): - """Setup connector instance logs command""" - logs_parser = subparsers.add_parser( - "connector-instance-logs", - help="View connector instance logs", - ) - lvl1 = logs_parser.add_subparsers( - dest="connector_instance_logs_command", - help="Connector instance logs command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List connector instance logs") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - list_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance", - dest="connector_instance_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing logs", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing logs", - dest="order_by", - ) - list_parser.set_defaults(func=handle_connector_instance_logs_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get a specific connector instance log" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - get_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance", - dest="connector_instance_id", - required=True, - ) - get_parser.add_argument( - "--log-id", - type=str, - help="ID of the log to get", - dest="log_id", - required=True, - ) - get_parser.set_defaults(func=handle_connector_instance_logs_get_command) - - -def handle_connector_instance_logs_list_command(args, chronicle): - """Handle connector instance logs list command""" - try: - out = chronicle.list_connector_instance_logs( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing connector instance logs: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instance_logs_get_command(args, chronicle): - """Handle connector instance log get command""" - try: - out = chronicle.get_connector_instance_log( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - connector_instance_log_id=args.log_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting connector instance log: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instances.py b/src/secops/cli/commands/integration/connector_instances.py deleted file mode 100644 index df68bfde..00000000 --- a/src/secops/cli/commands/integration/connector_instances.py +++ /dev/null @@ -1,473 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI connector instances commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_connector_instances_command(subparsers): - """Setup connector instances command""" - instances_parser = subparsers.add_parser( - "connector-instances", - help="Manage connector instances", - ) - lvl1 = instances_parser.add_subparsers( - dest="connector_instances_command", - help="Connector instances command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List connector instances") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing instances", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing instances", - dest="order_by", - ) - list_parser.set_defaults(func=handle_connector_instances_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get a specific connector instance" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - get_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance to get", - dest="connector_instance_id", - required=True, - ) - get_parser.set_defaults(func=handle_connector_instances_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete a connector instance" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - delete_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance to delete", - dest="connector_instance_id", - required=True, - ) - delete_parser.set_defaults(func=handle_connector_instances_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new connector instance" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - create_parser.add_argument( - "--environment", - type=str, - help="Environment for the connector instance", - dest="environment", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the connector instance", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--interval-seconds", - type=int, - help="Interval in seconds for connector execution", - dest="interval_seconds", - ) - create_parser.add_argument( - "--timeout-seconds", - type=int, - help="Timeout in seconds for connector execution", - dest="timeout_seconds", - ) - create_parser.add_argument( - "--enabled", - action="store_true", - help="Enable the connector instance", - dest="enabled", - ) - create_parser.set_defaults(func=handle_connector_instances_create_command) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update a connector instance" - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - update_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance to update", - dest="connector_instance_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the connector instance", - dest="display_name", - ) - update_parser.add_argument( - "--interval-seconds", - type=int, - help="New interval in seconds for connector execution", - dest="interval_seconds", - ) - update_parser.add_argument( - "--timeout-seconds", - type=int, - help="New timeout in seconds for connector execution", - dest="timeout_seconds", - ) - update_parser.add_argument( - "--enabled", - type=str, - choices=["true", "false"], - help="Enable or disable the connector instance", - dest="enabled", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_connector_instances_update_command) - - # fetch-latest command - fetch_parser = lvl1.add_parser( - "fetch-latest", - help="Get the latest definition of a connector instance", - ) - fetch_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - fetch_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - fetch_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance", - dest="connector_instance_id", - required=True, - ) - fetch_parser.set_defaults( - func=handle_connector_instances_fetch_latest_command - ) - - # set-logs command - logs_parser = lvl1.add_parser( - "set-logs", - help="Enable or disable log collection for a connector instance", - ) - logs_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - logs_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - logs_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance", - dest="connector_instance_id", - required=True, - ) - logs_parser.add_argument( - "--enabled", - type=str, - choices=["true", "false"], - help="Enable or disable log collection", - dest="enabled", - required=True, - ) - logs_parser.set_defaults(func=handle_connector_instances_set_logs_command) - - # run-ondemand command - run_parser = lvl1.add_parser( - "run-ondemand", - help="Run a connector instance on demand", - ) - run_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - run_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - run_parser.add_argument( - "--connector-instance-id", - type=str, - help="ID of the connector instance to run", - dest="connector_instance_id", - required=True, - ) - run_parser.set_defaults( - func=handle_connector_instances_run_ondemand_command - ) - - -def handle_connector_instances_list_command(args, chronicle): - """Handle connector instances list command""" - try: - out = chronicle.list_connector_instances( - integration_name=args.integration_name, - connector_id=args.connector_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing connector instances: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_get_command(args, chronicle): - """Handle connector instance get command""" - try: - out = chronicle.get_connector_instance( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting connector instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_delete_command(args, chronicle): - """Handle connector instance delete command""" - try: - chronicle.delete_connector_instance( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - ) - print( - f"Connector instance {args.connector_instance_id}" - f" deleted successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting connector instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_create_command(args, chronicle): - """Handle connector instance create command""" - try: - out = chronicle.create_connector_instance( - integration_name=args.integration_name, - connector_id=args.connector_id, - environment=args.environment, - display_name=args.display_name, - interval_seconds=args.interval_seconds, - timeout_seconds=args.timeout_seconds, - enabled=args.enabled, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating connector instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_update_command(args, chronicle): - """Handle connector instance update command""" - try: - # Convert enabled string to boolean if provided - enabled = None - if args.enabled: - enabled = args.enabled.lower() == "true" - - out = chronicle.update_connector_instance( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - display_name=args.display_name, - interval_seconds=args.interval_seconds, - timeout_seconds=args.timeout_seconds, - enabled=enabled, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating connector instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_fetch_latest_command(args, chronicle): - """Handle fetch latest connector instance definition command""" - try: - out = chronicle.get_connector_instance_latest_definition( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error fetching latest connector instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_set_logs_command(args, chronicle): - """Handle set connector instance logs collection command""" - try: - enabled = args.enabled.lower() == "true" - out = chronicle.set_connector_instance_logs_collection( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - enabled=enabled, - ) - status = "enabled" if enabled else "disabled" - print(f"Log collection {status} for connector instance successfully") - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error setting connector instance logs: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_instances_run_ondemand_command(args, chronicle): - """Handle run connector instance on demand command""" - try: - # Get the connector instance first - connector_instance = chronicle.get_connector_instance( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - ) - out = chronicle.run_connector_instance_on_demand( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector_instance_id=args.connector_instance_id, - connector_instance=connector_instance, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error running connector instance on demand: {e}", file=sys.stderr - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_revisions.py b/src/secops/cli/commands/integration/connector_revisions.py deleted file mode 100644 index 779888c9..00000000 --- a/src/secops/cli/commands/integration/connector_revisions.py +++ /dev/null @@ -1,217 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration connector revisions commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_connector_revisions_command(subparsers): - """Setup integration connector revisions command""" - revisions_parser = subparsers.add_parser( - "connector-revisions", - help="Manage integration connector revisions", - ) - lvl1 = revisions_parser.add_subparsers( - dest="connector_revisions_command", - help="Integration connector revisions command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", help="List integration connector revisions" - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing revisions", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing revisions", - dest="order_by", - ) - list_parser.set_defaults(func=handle_connector_revisions_list_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete an integration connector revision" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - delete_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to delete", - dest="revision_id", - required=True, - ) - delete_parser.set_defaults(func=handle_connector_revisions_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new integration connector revision" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - create_parser.add_argument( - "--comment", - type=str, - help="Comment describing the revision", - dest="comment", - ) - create_parser.set_defaults(func=handle_connector_revisions_create_command) - - # rollback command - rollback_parser = lvl1.add_parser( - "rollback", help="Rollback connector to a previous revision" - ) - rollback_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - rollback_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector", - dest="connector_id", - required=True, - ) - rollback_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to rollback to", - dest="revision_id", - required=True, - ) - rollback_parser.set_defaults( - func=handle_connector_revisions_rollback_command, - ) - - -def handle_connector_revisions_list_command(args, chronicle): - """Handle integration connector revisions list command""" - try: - out = chronicle.list_integration_connector_revisions( - integration_name=args.integration_name, - connector_id=args.connector_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing connector revisions: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_revisions_delete_command(args, chronicle): - """Handle integration connector revision delete command""" - try: - chronicle.delete_integration_connector_revision( - integration_name=args.integration_name, - connector_id=args.connector_id, - revision_id=args.revision_id, - ) - print(f"Connector revision {args.revision_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting connector revision: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_revisions_create_command(args, chronicle): - """Handle integration connector revision create command""" - try: - # Get the current connector to create a revision - connector = chronicle.get_integration_connector( - integration_name=args.integration_name, - connector_id=args.connector_id, - ) - out = chronicle.create_integration_connector_revision( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector=connector, - comment=args.comment, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating connector revision: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connector_revisions_rollback_command(args, chronicle): - """Handle integration connector revision rollback command""" - try: - out = chronicle.rollback_integration_connector_revision( - integration_name=args.integration_name, - connector_id=args.connector_id, - revision_id=args.revision_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error rolling back connector revision: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/connectors.py b/src/secops/cli/commands/integration/connectors.py deleted file mode 100644 index fe8e03ef..00000000 --- a/src/secops/cli/commands/integration/connectors.py +++ /dev/null @@ -1,325 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration connectors commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_connectors_command(subparsers): - """Setup integration connectors command""" - connectors_parser = subparsers.add_parser( - "connectors", - help="Manage integration connectors", - ) - lvl1 = connectors_parser.add_subparsers( - dest="connectors_command", help="Integration connectors command" - ) - - # list command - list_parser = lvl1.add_parser("list", help="List integration connectors") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing connectors", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing connectors", - dest="order_by", - ) - list_parser.set_defaults( - func=handle_connectors_list_command, - ) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get integration connector details" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector to get", - dest="connector_id", - required=True, - ) - get_parser.set_defaults(func=handle_connectors_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete an integration connector" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector to delete", - dest="connector_id", - required=True, - ) - delete_parser.set_defaults(func=handle_connectors_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new integration connector" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the connector", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--code", - type=str, - help="Python code for the connector", - dest="code", - required=True, - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the connector", - dest="description", - ) - create_parser.add_argument( - "--connector-id", - type=str, - help="Custom ID for the connector", - dest="connector_id", - ) - create_parser.set_defaults(func=handle_connectors_create_command) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update an integration connector" - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector to update", - dest="connector_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the connector", - dest="display_name", - ) - update_parser.add_argument( - "--code", - type=str, - help="New Python code for the connector", - dest="code", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the connector", - dest="description", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_connectors_update_command) - - # test command - test_parser = lvl1.add_parser( - "test", help="Execute an integration connector test" - ) - test_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - test_parser.add_argument( - "--connector-id", - type=str, - help="ID of the connector to test", - dest="connector_id", - required=True, - ) - test_parser.set_defaults(func=handle_connectors_test_command) - - # template command - template_parser = lvl1.add_parser( - "template", - help="Get a template for creating a connector", - ) - template_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - template_parser.set_defaults(func=handle_connectors_template_command) - - -def handle_connectors_list_command(args, chronicle): - """Handle integration connectors list command""" - try: - out = chronicle.list_integration_connectors( - integration_name=args.integration_name, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing integration connectors: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_get_command(args, chronicle): - """Handle integration connector get command""" - try: - out = chronicle.get_integration_connector( - integration_name=args.integration_name, - connector_id=args.connector_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration connector: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_delete_command(args, chronicle): - """Handle integration connector delete command""" - try: - chronicle.delete_integration_connector( - integration_name=args.integration_name, - connector_id=args.connector_id, - ) - print(f"Connector {args.connector_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting integration connector: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_create_command(args, chronicle): - """Handle integration connector create command""" - try: - out = chronicle.create_integration_connector( - integration_name=args.integration_name, - display_name=args.display_name, - code=args.code, - description=args.description, - connector_id=args.connector_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating integration connector: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_update_command(args, chronicle): - """Handle integration connector update command""" - try: - out = chronicle.update_integration_connector( - integration_name=args.integration_name, - connector_id=args.connector_id, - display_name=args.display_name, - code=args.code, - description=args.description, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating integration connector: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_test_command(args, chronicle): - """Handle integration connector test command""" - try: - # First get the connector to test - connector = chronicle.get_integration_connector( - integration_name=args.integration_name, - connector_id=args.connector_id, - ) - out = chronicle.execute_integration_connector_test( - integration_name=args.integration_name, - connector_id=args.connector_id, - connector=connector, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error testing integration connector: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_connectors_template_command(args, chronicle): - """Handle get connector template command""" - try: - out = chronicle.get_integration_connector_template( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting connector template: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/integration.py b/src/secops/cli/commands/integration/integration.py deleted file mode 100644 index dd73a600..00000000 --- a/src/secops/cli/commands/integration/integration.py +++ /dev/null @@ -1,775 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration commands""" - -import sys - -from secops.chronicle.models import ( - DiffType, - IntegrationType, - PythonVersion, - TargetMode, -) -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_integrations_command(subparsers): - """Setup integrations command""" - integrations_parser = subparsers.add_parser( - "integrations", help="Manage SecOps integrations" - ) - lvl1 = integrations_parser.add_subparsers( - dest="integrations_command", help="Integrations command" - ) - - # list command - list_parser = lvl1.add_parser("list", help="List integrations") - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing integrations", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing integrations", - dest="order_by", - ) - list_parser.set_defaults(func=handle_integration_list_command) - - # get command - get_parser = lvl1.add_parser("get", help="Get integration details") - get_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to get details for", - dest="integration_id", - required=True, - ) - get_parser.set_defaults(func=handle_integration_get_command) - - # delete command - delete_parser = lvl1.add_parser("delete", help="Delete an integration") - delete_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to delete", - dest="integration_id", - required=True, - ) - delete_parser.set_defaults(func=handle_integration_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new custom integration" - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the integration (max 150 characters)", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--staging", - action="store_true", - help="Create the integration in staging mode", - dest="staging", - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the integration (max 1,500 characters)", - dest="description", - ) - create_parser.add_argument( - "--image-base64", - type=str, - help="Integration image encoded as a base64 string (max 5 MB)", - dest="image_base64", - ) - create_parser.add_argument( - "--svg-icon", - type=str, - help="Integration SVG icon string (max 1 MB)", - dest="svg_icon", - ) - create_parser.add_argument( - "--python-version", - type=str, - choices=[v.value for v in PythonVersion], - help="Python version for the integration", - dest="python_version", - ) - create_parser.add_argument( - "--integration-type", - type=str, - choices=[t.value for t in IntegrationType], - help="Integration type", - dest="integration_type", - ) - create_parser.set_defaults(func=handle_integration_create_command) - - # download command - download_parser = lvl1.add_parser( - "download", - help="Download an integration package as a ZIP file", - ) - download_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to download", - dest="integration_id", - required=True, - ) - download_parser.add_argument( - "--output-file", - type=str, - help="Path to write the downloaded ZIP file to", - dest="output_file", - required=True, - ) - download_parser.set_defaults(func=handle_integration_download_command) - - # download-dependency command - download_dep_parser = lvl1.add_parser( - "download-dependency", - help="Download a Python dependency for a custom integration", - ) - download_dep_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration", - dest="integration_id", - required=True, - ) - download_dep_parser.add_argument( - "--dependency-name", - type=str, - help=( - "Dependency name to download. Can include version or " - "repository, e.g. 'requests==2.31.0'" - ), - dest="dependency_name", - required=True, - ) - download_dep_parser.set_defaults( - func=handle_download_integration_dependency_command - ) - - # export-items command - export_items_parser = lvl1.add_parser( - "export-items", - help="Export specific items from an integration as a ZIP file", - ) - export_items_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to export items from", - dest="integration_id", - required=True, - ) - export_items_parser.add_argument( - "--output-file", - type=str, - help="Path to write the exported ZIP file to", - dest="output_file", - required=True, - ) - export_items_parser.add_argument( - "--actions", - type=str, - nargs="+", - help="IDs of actions to export", - dest="actions", - ) - export_items_parser.add_argument( - "--jobs", - type=str, - nargs="+", - help="IDs of jobs to export", - dest="jobs", - ) - export_items_parser.add_argument( - "--connectors", - type=str, - nargs="+", - help="IDs of connectors to export", - dest="connectors", - ) - export_items_parser.add_argument( - "--managers", - type=str, - nargs="+", - help="IDs of managers to export", - dest="managers", - ) - export_items_parser.add_argument( - "--transformers", - type=str, - nargs="+", - help="IDs of transformers to export", - dest="transformers", - ) - export_items_parser.add_argument( - "--logical-operators", - type=str, - nargs="+", - help="IDs of logical operators to export", - dest="logical_operators", - ) - export_items_parser.set_defaults( - func=handle_export_integration_items_command - ) - - # affected-items command - affected_parser = lvl1.add_parser( - "affected-items", - help="Get items affected by changes to an integration", - ) - affected_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to check", - dest="integration_id", - required=True, - ) - affected_parser.set_defaults( - func=handle_get_integration_affected_items_command - ) - - # agent-integrations command - agent_parser = lvl1.add_parser( - "agent-integrations", - help="Get integrations installed on a specific agent", - ) - agent_parser.add_argument( - "--agent-id", - type=str, - help="Identifier of the agent", - dest="agent_id", - required=True, - ) - agent_parser.set_defaults(func=handle_get_agent_integrations_command) - - # dependencies command - deps_parser = lvl1.add_parser( - "dependencies", - help="Get Python dependencies for a custom integration", - ) - deps_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration", - dest="integration_id", - required=True, - ) - deps_parser.set_defaults(func=handle_get_integration_dependencies_command) - - # restricted-agents command - restricted_parser = lvl1.add_parser( - "restricted-agents", - help="Get agents restricted from running an updated integration", - ) - restricted_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration", - dest="integration_id", - required=True, - ) - restricted_parser.add_argument( - "--required-python-version", - type=str, - choices=[v.value for v in PythonVersion], - help="Python version required for the updated integration", - dest="required_python_version", - required=True, - ) - restricted_parser.add_argument( - "--push-request", - action="store_true", - help="Indicates the integration is being pushed to a different mode", - dest="push_request", - ) - restricted_parser.set_defaults( - func=handle_get_integration_restricted_agents_command - ) - - # diff command - diff_parser = lvl1.add_parser( - "diff", help="Get the configuration diff for an integration" - ) - diff_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration", - dest="integration_id", - required=True, - ) - diff_parser.add_argument( - "--diff-type", - type=str, - choices=[d.value for d in DiffType], - help=( - "Type of diff to retrieve. " - "COMMERCIAL: diff against the marketplace version. " - "PRODUCTION: diff between staging and production. " - "STAGING: diff between production and staging." - ), - dest="diff_type", - default=DiffType.COMMERCIAL.value, - ) - diff_parser.set_defaults(func=handle_get_integration_diff_command) - - # transition command - transition_parser = lvl1.add_parser( - "transition", - help="Transition an integration to production or staging", - ) - transition_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to transition", - dest="integration_id", - required=True, - ) - transition_parser.add_argument( - "--target-mode", - type=str, - choices=[t.value for t in TargetMode], - help="Target mode to transition the integration to", - dest="target_mode", - required=True, - ) - transition_parser.set_defaults(func=handle_transition_integration_command) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update an existing integration's metadata" - ) - update_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to update", - dest="integration_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the integration (max 150 characters)", - dest="display_name", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the integration (max 1,500 characters)", - dest="description", - ) - update_parser.add_argument( - "--image-base64", - type=str, - help="New integration image encoded as a base64 string (max 5 MB)", - dest="image_base64", - ) - update_parser.add_argument( - "--svg-icon", - type=str, - help="New integration SVG icon string (max 1 MB)", - dest="svg_icon", - ) - update_parser.add_argument( - "--python-version", - type=str, - choices=[v.value for v in PythonVersion], - help="Python version for the integration", - dest="python_version", - ) - update_parser.add_argument( - "--integration-type", - type=str, - choices=[t.value for t in IntegrationType], - help="Integration type", - dest="integration_type", - ) - update_parser.add_argument( - "--staging", - action="store_true", - help="Set the integration to staging mode", - dest="staging", - ) - update_parser.add_argument( - "--dependencies-to-remove", - type=str, - nargs="+", - help="List of dependency names to remove from the integration", - dest="dependencies_to_remove", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help=( - "Comma-separated list of fields to update. " - "If not provided, all supplied fields are updated." - ), - dest="update_mask", - ) - update_parser.set_defaults(func=handle_update_integration_command) - - # update-custom command - update_custom_parser = lvl1.add_parser( - "update-custom", - help=( - "Update a custom integration definition including " - "parameters and dependencies" - ), - ) - update_custom_parser.add_argument( - "--integration-id", - type=str, - help="ID of the integration to update", - dest="integration_id", - required=True, - ) - update_custom_parser.add_argument( - "--display-name", - type=str, - help="New display name for the integration (max 150 characters)", - dest="display_name", - ) - update_custom_parser.add_argument( - "--description", - type=str, - help="New description for the integration (max 1,500 characters)", - dest="description", - ) - update_custom_parser.add_argument( - "--image-base64", - type=str, - help="New integration image encoded as a base64 string (max 5 MB)", - dest="image_base64", - ) - update_custom_parser.add_argument( - "--svg-icon", - type=str, - help="New integration SVG icon string (max 1 MB)", - dest="svg_icon", - ) - update_custom_parser.add_argument( - "--python-version", - type=str, - choices=[v.value for v in PythonVersion], - help="Python version for the integration", - dest="python_version", - ) - update_custom_parser.add_argument( - "--integration-type", - type=str, - choices=[t.value for t in IntegrationType], - help="Integration type", - dest="integration_type", - ) - update_custom_parser.add_argument( - "--staging", - action="store_true", - help="Set the integration to staging mode", - dest="staging", - ) - update_custom_parser.add_argument( - "--dependencies-to-remove", - type=str, - nargs="+", - help="List of dependency names to remove from the integration", - dest="dependencies_to_remove", - ) - update_custom_parser.add_argument( - "--update-mask", - type=str, - help=( - "Comma-separated list of fields to update. " - "If not provided, all supplied fields are updated." - ), - dest="update_mask", - ) - update_custom_parser.set_defaults( - func=handle_updated_custom_integration_command - ) - - -# --------------------------------------------------------------------------- -# Handlers -# --------------------------------------------------------------------------- - - -def handle_integration_list_command(args, chronicle): - """Handle list integrations command""" - try: - out = chronicle.list_integrations( - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing integrations: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_get_command(args, chronicle): - """Handle get integration command""" - try: - out = chronicle.get_integration( - integration_name=args.integration_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_delete_command(args, chronicle): - """Handle delete integration command""" - try: - chronicle.delete_integration( - integration_name=args.integration_id, - ) - print(f"Integration {args.integration_id} deleted successfully.") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_create_command(args, chronicle): - """Handle create integration command""" - try: - out = chronicle.create_integration( - display_name=args.display_name, - staging=args.staging, - description=args.description, - image_base64=args.image_base64, - svg_icon=args.svg_icon, - python_version=( - PythonVersion(args.python_version) - if args.python_version - else None - ), - integration_type=( - IntegrationType(args.integration_type) - if args.integration_type - else None - ), - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_download_command(args, chronicle): - """Handle download integration command""" - try: - zip_bytes = chronicle.download_integration( - integration_name=args.integration_id, - ) - with open(args.output_file, "wb") as f: - f.write(zip_bytes) - print( - f"Integration {args.integration_id} downloaded to " - f"{args.output_file}." - ) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error downloading integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_download_integration_dependency_command(args, chronicle): - """Handle download integration dependencies command""" - try: - out = chronicle.download_integration_dependency( - integration_name=args.integration_id, - dependency_name=args.dependency_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error downloading integration dependencies: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_export_integration_items_command(args, chronicle): - """Handle export integration items command""" - try: - zip_bytes = chronicle.export_integration_items( - integration_name=args.integration_id, - actions=args.actions, - jobs=args.jobs, - connectors=args.connectors, - managers=args.managers, - transformers=args.transformers, - logical_operators=args.logical_operators, - ) - with open(args.output_file, "wb") as f: - f.write(zip_bytes) - print(f"Integration items exported to {args.output_file}.") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error exporting integration items: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_get_integration_affected_items_command(args, chronicle): - """Handle get integration affected items command""" - try: - out = chronicle.get_integration_affected_items( - integration_name=args.integration_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration affected items: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_get_agent_integrations_command(args, chronicle): - """Handle get agent integration command""" - try: - out = chronicle.get_agent_integrations( - agent_id=args.agent_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting agent integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_get_integration_dependencies_command(args, chronicle): - """Handle get integration dependencies command""" - try: - out = chronicle.get_integration_dependencies( - integration_name=args.integration_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration dependencies: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_get_integration_restricted_agents_command(args, chronicle): - """Handle get integration restricted agent command""" - try: - out = chronicle.get_integration_restricted_agents( - integration_name=args.integration_id, - required_python_version=PythonVersion(args.required_python_version), - push_request=args.push_request, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting integration restricted agent: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_get_integration_diff_command(args, chronicle): - """Handle get integration diff command""" - try: - out = chronicle.get_integration_diff( - integration_name=args.integration_id, - diff_type=DiffType(args.diff_type), - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration diff: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_transition_integration_command(args, chronicle): - """Handle transition integration command""" - try: - out = chronicle.transition_integration( - integration_name=args.integration_id, - target_mode=TargetMode(args.target_mode), - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error transitioning integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_update_integration_command(args, chronicle): - """Handle update integration command""" - try: - out = chronicle.update_integration( - integration_name=args.integration_id, - display_name=args.display_name, - description=args.description, - image_base64=args.image_base64, - svg_icon=args.svg_icon, - python_version=( - PythonVersion(args.python_version) - if args.python_version - else None - ), - integration_type=( - IntegrationType(args.integration_type) - if args.integration_type - else None - ), - staging=args.staging or None, - dependencies_to_remove=args.dependencies_to_remove, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_updated_custom_integration_command(args, chronicle): - """Handle update custom integration command""" - try: - out = chronicle.update_custom_integration( - integration_name=args.integration_id, - display_name=args.display_name, - description=args.description, - image_base64=args.image_base64, - svg_icon=args.svg_icon, - python_version=( - PythonVersion(args.python_version) - if args.python_version - else None - ), - integration_type=( - IntegrationType(args.integration_type) - if args.integration_type - else None - ), - staging=args.staging or None, - dependencies_to_remove=args.dependencies_to_remove, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating custom integration: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py index d84bd383..581bfb29 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -15,27 +15,10 @@ """Top level arguments for integration commands""" from secops.cli.commands.integration import ( - marketplace_integration, - integration, actions, action_revisions, - connectors, - connector_revisions, - connector_context_properties, - connector_instance_logs, - connector_instances, - jobs, - job_revisions, - job_context_properties, - job_instance_logs, - job_instances, managers, manager_revisions, - integration_instances, - transformers, - transformer_revisions, - logical_operators, - logical_operator_revisions, ) @@ -49,26 +32,7 @@ def setup_integrations_command(subparsers): ) # Setup all subcommands under `integration` - integration.setup_integrations_command(lvl1) - integration_instances.setup_integration_instances_command(lvl1) - transformers.setup_transformers_command(lvl1) - transformer_revisions.setup_transformer_revisions_command(lvl1) - logical_operators.setup_logical_operators_command(lvl1) - logical_operator_revisions.setup_logical_operator_revisions_command(lvl1) actions.setup_actions_command(lvl1) action_revisions.setup_action_revisions_command(lvl1) - connectors.setup_connectors_command(lvl1) - connector_revisions.setup_connector_revisions_command(lvl1) - connector_context_properties.setup_connector_context_properties_command( - lvl1 - ) - connector_instance_logs.setup_connector_instance_logs_command(lvl1) - connector_instances.setup_connector_instances_command(lvl1) - jobs.setup_jobs_command(lvl1) - job_revisions.setup_job_revisions_command(lvl1) - job_context_properties.setup_job_context_properties_command(lvl1) - job_instance_logs.setup_job_instance_logs_command(lvl1) - job_instances.setup_job_instances_command(lvl1) managers.setup_managers_command(lvl1) manager_revisions.setup_manager_revisions_command(lvl1) - marketplace_integration.setup_marketplace_integrations_command(lvl1) diff --git a/src/secops/cli/commands/integration/integration_instances.py b/src/secops/cli/commands/integration/integration_instances.py deleted file mode 100644 index 2d375346..00000000 --- a/src/secops/cli/commands/integration/integration_instances.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration instances commands""" - -import json -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_integration_instances_command(subparsers): - """Setup integration instances command""" - instances_parser = subparsers.add_parser( - "instances", - help="Manage integration instances", - ) - lvl1 = instances_parser.add_subparsers( - dest="integration_instances_command", - help="Integration instances command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List integration instances") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing instances", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing instances", - dest="order_by", - ) - list_parser.set_defaults(func=handle_integration_instances_list_command) - - # get command - get_parser = lvl1.add_parser("get", help="Get integration instance details") - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--instance-id", - type=str, - help="ID of the instance to get", - dest="instance_id", - required=True, - ) - get_parser.set_defaults(func=handle_integration_instances_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete an integration instance" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--instance-id", - type=str, - help="ID of the instance to delete", - dest="instance_id", - required=True, - ) - delete_parser.set_defaults(func=handle_integration_instances_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new integration instance" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the instance", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--environment", - type=str, - help="Environment name for the instance", - dest="environment", - required=True, - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the instance", - dest="description", - ) - create_parser.add_argument( - "--instance-id", - type=str, - help="Custom ID for the instance", - dest="instance_id", - ) - create_parser.add_argument( - "--config", - type=str, - help="JSON string of instance configuration", - dest="config", - ) - create_parser.set_defaults(func=handle_integration_instances_create_command) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update an integration instance" - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--instance-id", - type=str, - help="ID of the instance to update", - dest="instance_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the instance", - dest="display_name", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the instance", - dest="description", - ) - update_parser.add_argument( - "--config", - type=str, - help="JSON string of new instance configuration", - dest="config", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_integration_instances_update_command) - - # test command - test_parser = lvl1.add_parser( - "test", help="Execute an integration instance test" - ) - test_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - test_parser.add_argument( - "--instance-id", - type=str, - help="ID of the instance to test", - dest="instance_id", - required=True, - ) - test_parser.set_defaults(func=handle_integration_instances_test_command) - - # get-affected-items command - affected_parser = lvl1.add_parser( - "get-affected-items", - help="Get items affected by an integration instance", - ) - affected_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - affected_parser.add_argument( - "--instance-id", - type=str, - help="ID of the instance", - dest="instance_id", - required=True, - ) - affected_parser.set_defaults( - func=handle_integration_instances_get_affected_items_command - ) - - # get-default command - default_parser = lvl1.add_parser( - "get-default", - help="Get the default integration instance", - ) - default_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - default_parser.set_defaults( - func=handle_integration_instances_get_default_command - ) - - -def handle_integration_instances_list_command(args, chronicle): - """Handle integration instances list command""" - try: - out = chronicle.list_integration_instances( - integration_name=args.integration_name, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing integration instances: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_get_command(args, chronicle): - """Handle integration instance get command""" - try: - out = chronicle.get_integration_instance( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_delete_command(args, chronicle): - """Handle integration instance delete command""" - try: - chronicle.delete_integration_instance( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - ) - print(f"Integration instance {args.instance_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting integration instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_create_command(args, chronicle): - """Handle integration instance create command""" - try: - # Parse config if provided - - config = None - if args.config: - config = json.loads(args.config) - - out = chronicle.create_integration_instance( - integration_name=args.integration_name, - display_name=args.display_name, - environment=args.environment, - description=args.description, - integration_instance_id=args.instance_id, - config=config, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing config JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating integration instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_update_command(args, chronicle): - """Handle integration instance update command""" - try: - # Parse config if provided - - config = None - if args.config: - config = json.loads(args.config) - - out = chronicle.update_integration_instance( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - display_name=args.display_name, - description=args.description, - config=config, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing config JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating integration instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_test_command(args, chronicle): - """Handle integration instance test command""" - try: - # Get the instance first - instance = chronicle.get_integration_instance( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - ) - - out = chronicle.execute_integration_instance_test( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - integration_instance=instance, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error testing integration instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_integration_instances_get_affected_items_command(args, chronicle): - """Handle get integration instance affected items command""" - try: - out = chronicle.get_integration_instance_affected_items( - integration_name=args.integration_name, - integration_instance_id=args.instance_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting integration instance affected items: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_integration_instances_get_default_command(args, chronicle): - """Handle get default integration instance command""" - try: - out = chronicle.get_default_integration_instance( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting default integration instance: {e}", file=sys.stderr - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_context_properties.py b/src/secops/cli/commands/integration/job_context_properties.py deleted file mode 100644 index 5da5cdb3..00000000 --- a/src/secops/cli/commands/integration/job_context_properties.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI job context properties commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_job_context_properties_command(subparsers): - """Setup job context properties command""" - properties_parser = subparsers.add_parser( - "job-context-properties", - help="Manage job context properties", - ) - lvl1 = properties_parser.add_subparsers( - dest="job_context_properties_command", - help="Job context properties command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List job context properties") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - list_parser.add_argument( - "--context-id", - type=str, - help="Context ID to filter properties", - dest="context_id", - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing properties", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing properties", - dest="order_by", - ) - list_parser.set_defaults(func=handle_job_context_properties_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get a specific job context property" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - get_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - get_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to get", - dest="property_id", - required=True, - ) - get_parser.set_defaults(func=handle_job_context_properties_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete a job context property" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - delete_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - delete_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to delete", - dest="property_id", - required=True, - ) - delete_parser.set_defaults( - func=handle_job_context_properties_delete_command - ) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new job context property" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - create_parser.add_argument( - "--context-id", - type=str, - help="Context ID for the property", - dest="context_id", - required=True, - ) - create_parser.add_argument( - "--key", - type=str, - help="Key for the property", - dest="key", - required=True, - ) - create_parser.add_argument( - "--value", - type=str, - help="Value for the property", - dest="value", - required=True, - ) - create_parser.set_defaults( - func=handle_job_context_properties_create_command - ) - - # update command - update_parser = lvl1.add_parser( - "update", help="Update a job context property" - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - update_parser.add_argument( - "--context-id", - type=str, - help="Context ID of the property", - dest="context_id", - required=True, - ) - update_parser.add_argument( - "--property-id", - type=str, - help="ID of the property to update", - dest="property_id", - required=True, - ) - update_parser.add_argument( - "--value", - type=str, - help="New value for the property", - dest="value", - required=True, - ) - update_parser.set_defaults( - func=handle_job_context_properties_update_command - ) - - # clear-all command - clear_parser = lvl1.add_parser( - "clear-all", help="Delete all job context properties" - ) - clear_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - clear_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - clear_parser.add_argument( - "--context-id", - type=str, - help="Context ID to clear all properties for", - dest="context_id", - required=True, - ) - clear_parser.set_defaults(func=handle_job_context_properties_clear_command) - - -def handle_job_context_properties_list_command(args, chronicle): - """Handle job context properties list command""" - try: - out = chronicle.list_job_context_properties( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing job context properties: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_context_properties_get_command(args, chronicle): - """Handle job context property get command""" - try: - out = chronicle.get_job_context_property( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - context_property_id=args.property_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting job context property: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_context_properties_delete_command(args, chronicle): - """Handle job context property delete command""" - try: - chronicle.delete_job_context_property( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - context_property_id=args.property_id, - ) - print(f"Job context property {args.property_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting job context property: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_context_properties_create_command(args, chronicle): - """Handle job context property create command""" - try: - out = chronicle.create_job_context_property( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - key=args.key, - value=args.value, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating job context property: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_context_properties_update_command(args, chronicle): - """Handle job context property update command""" - try: - out = chronicle.update_job_context_property( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - context_property_id=args.property_id, - value=args.value, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating job context property: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_context_properties_clear_command(args, chronicle): - """Handle clear all job context properties command""" - try: - chronicle.delete_all_job_context_properties( - integration_name=args.integration_name, - job_id=args.job_id, - context_id=args.context_id, - ) - print( - f"All job context properties for context " - f"{args.context_id} cleared successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error clearing job context properties: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_instance_logs.py b/src/secops/cli/commands/integration/job_instance_logs.py deleted file mode 100644 index d18e2ad4..00000000 --- a/src/secops/cli/commands/integration/job_instance_logs.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI job instance logs commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_job_instance_logs_command(subparsers): - """Setup job instance logs command""" - logs_parser = subparsers.add_parser( - "job-instance-logs", - help="View job instance logs", - ) - lvl1 = logs_parser.add_subparsers( - dest="job_instance_logs_command", - help="Job instance logs command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List job instance logs") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - list_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance", - dest="job_instance_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing logs", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing logs", - dest="order_by", - ) - list_parser.set_defaults(func=handle_job_instance_logs_list_command) - - # get command - get_parser = lvl1.add_parser("get", help="Get a specific job instance log") - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - get_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance", - dest="job_instance_id", - required=True, - ) - get_parser.add_argument( - "--log-id", - type=str, - help="ID of the log to get", - dest="log_id", - required=True, - ) - get_parser.set_defaults(func=handle_job_instance_logs_get_command) - - -def handle_job_instance_logs_list_command(args, chronicle): - """Handle job instance logs list command""" - try: - out = chronicle.list_job_instance_logs( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing job instance logs: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instance_logs_get_command(args, chronicle): - """Handle job instance log get command""" - try: - out = chronicle.get_job_instance_log( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - job_instance_log_id=args.log_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting job instance log: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_instances.py b/src/secops/cli/commands/integration/job_instances.py deleted file mode 100644 index 53c9a202..00000000 --- a/src/secops/cli/commands/integration/job_instances.py +++ /dev/null @@ -1,407 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration job instances commands""" - -import json -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_job_instances_command(subparsers): - """Setup integration job instances command""" - instances_parser = subparsers.add_parser( - "job-instances", - help="Manage job instances", - ) - lvl1 = instances_parser.add_subparsers( - dest="job_instances_command", - help="Job instances command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List job instances") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing instances", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing instances", - dest="order_by", - ) - list_parser.set_defaults(func=handle_job_instances_list_command) - - # get command - get_parser = lvl1.add_parser("get", help="Get a specific job instance") - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - get_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance to get", - dest="job_instance_id", - required=True, - ) - get_parser.set_defaults(func=handle_job_instances_get_command) - - # delete command - delete_parser = lvl1.add_parser("delete", help="Delete a job instance") - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - delete_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance to delete", - dest="job_instance_id", - required=True, - ) - delete_parser.set_defaults(func=handle_job_instances_delete_command) - - # create command - create_parser = lvl1.add_parser("create", help="Create a new job instance") - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - create_parser.add_argument( - "--environment", - type=str, - help="Environment for the job instance", - dest="environment", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the job instance", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--schedule", - type=str, - help="Cron schedule for the job instance", - dest="schedule", - ) - create_parser.add_argument( - "--timeout-seconds", - type=int, - help="Timeout in seconds for job execution", - dest="timeout_seconds", - ) - create_parser.add_argument( - "--enabled", - action="store_true", - help="Enable the job instance", - dest="enabled", - ) - create_parser.add_argument( - "--parameters", - type=str, - help="JSON string of job parameters", - dest="parameters", - ) - create_parser.set_defaults(func=handle_job_instances_create_command) - - # update command - update_parser = lvl1.add_parser("update", help="Update a job instance") - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - update_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance to update", - dest="job_instance_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the job instance", - dest="display_name", - ) - update_parser.add_argument( - "--schedule", - type=str, - help="New cron schedule for the job instance", - dest="schedule", - ) - update_parser.add_argument( - "--timeout-seconds", - type=int, - help="New timeout in seconds for job execution", - dest="timeout_seconds", - ) - update_parser.add_argument( - "--enabled", - type=str, - choices=["true", "false"], - help="Enable or disable the job instance", - dest="enabled", - ) - update_parser.add_argument( - "--parameters", - type=str, - help="JSON string of new job parameters", - dest="parameters", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_job_instances_update_command) - - # run-ondemand command - run_parser = lvl1.add_parser( - "run-ondemand", - help="Run a job instance on demand", - ) - run_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - run_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - run_parser.add_argument( - "--job-instance-id", - type=str, - help="ID of the job instance to run", - dest="job_instance_id", - required=True, - ) - run_parser.add_argument( - "--parameters", - type=str, - help="JSON string of parameters for this run", - dest="parameters", - ) - run_parser.set_defaults(func=handle_job_instances_run_ondemand_command) - - -def handle_job_instances_list_command(args, chronicle): - """Handle job instances list command""" - try: - out = chronicle.list_integration_job_instances( - integration_name=args.integration_name, - job_id=args.job_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing job instances: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instances_get_command(args, chronicle): - """Handle job instance get command""" - try: - out = chronicle.get_integration_job_instance( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting job instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instances_delete_command(args, chronicle): - """Handle job instance delete command""" - try: - chronicle.delete_integration_job_instance( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - ) - print(f"Job instance {args.job_instance_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting job instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instances_create_command(args, chronicle): - """Handle job instance create command""" - try: - # Parse parameters if provided - parameters = None - if args.parameters: - parameters = json.loads(args.parameters) - - out = chronicle.create_integration_job_instance( - integration_name=args.integration_name, - job_id=args.job_id, - environment=args.environment, - display_name=args.display_name, - schedule=args.schedule, - timeout_seconds=args.timeout_seconds, - enabled=args.enabled, - parameters=parameters, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing parameters JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating job instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instances_update_command(args, chronicle): - """Handle job instance update command""" - try: - # Parse parameters if provided - parameters = None - if args.parameters: - parameters = json.loads(args.parameters) - - # Convert enabled string to boolean if provided - enabled = None - if args.enabled: - enabled = args.enabled.lower() == "true" - - out = chronicle.update_integration_job_instance( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - display_name=args.display_name, - schedule=args.schedule, - timeout_seconds=args.timeout_seconds, - enabled=enabled, - parameters=parameters, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing parameters JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating job instance: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_instances_run_ondemand_command(args, chronicle): - """Handle run job instance on demand command""" - try: - # Parse parameters if provided - parameters = None - if args.parameters: - parameters = json.loads(args.parameters) - - # Get the job instance first - job_instance = chronicle.get_integration_job_instance( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - ) - - out = chronicle.run_integration_job_instance_on_demand( - integration_name=args.integration_name, - job_id=args.job_id, - job_instance_id=args.job_instance_id, - job_instance=job_instance, - parameters=parameters, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing parameters JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error running job instance on demand: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/job_revisions.py b/src/secops/cli/commands/integration/job_revisions.py deleted file mode 100644 index 36b24850..00000000 --- a/src/secops/cli/commands/integration/job_revisions.py +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration job revisions commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_job_revisions_command(subparsers): - """Setup integration job revisions command""" - revisions_parser = subparsers.add_parser( - "job-revisions", - help="Manage integration job revisions", - ) - lvl1 = revisions_parser.add_subparsers( - dest="job_revisions_command", - help="Integration job revisions command", - ) - - # list command - list_parser = lvl1.add_parser("list", help="List integration job revisions") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing revisions", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing revisions", - dest="order_by", - ) - list_parser.set_defaults(func=handle_job_revisions_list_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", help="Delete an integration job revision" - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - delete_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to delete", - dest="revision_id", - required=True, - ) - delete_parser.set_defaults(func=handle_job_revisions_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", help="Create a new integration job revision" - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - create_parser.add_argument( - "--comment", - type=str, - help="Comment describing the revision", - dest="comment", - ) - create_parser.set_defaults(func=handle_job_revisions_create_command) - - # rollback command - rollback_parser = lvl1.add_parser( - "rollback", help="Rollback job to a previous revision" - ) - rollback_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - rollback_parser.add_argument( - "--job-id", - type=str, - help="ID of the job", - dest="job_id", - required=True, - ) - rollback_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to rollback to", - dest="revision_id", - required=True, - ) - rollback_parser.set_defaults(func=handle_job_revisions_rollback_command) - - -def handle_job_revisions_list_command(args, chronicle): - """Handle integration job revisions list command""" - try: - out = chronicle.list_integration_job_revisions( - integration_name=args.integration_name, - job_id=args.job_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing job revisions: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_revisions_delete_command(args, chronicle): - """Handle integration job revision delete command""" - try: - chronicle.delete_integration_job_revision( - integration_name=args.integration_name, - job_id=args.job_id, - revision_id=args.revision_id, - ) - print(f"Job revision {args.revision_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting job revision: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_revisions_create_command(args, chronicle): - """Handle integration job revision create command""" - try: - # Get the current job to create a revision - job = chronicle.get_integration_job( - integration_name=args.integration_name, - job_id=args.job_id, - ) - out = chronicle.create_integration_job_revision( - integration_name=args.integration_name, - job_id=args.job_id, - job=job, - comment=args.comment, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating job revision: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_job_revisions_rollback_command(args, chronicle): - """Handle integration job revision rollback command""" - try: - out = chronicle.rollback_integration_job_revision( - integration_name=args.integration_name, - job_id=args.job_id, - revision_id=args.revision_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error rolling back job revision: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/jobs.py b/src/secops/cli/commands/integration/jobs.py deleted file mode 100644 index 4cd04e8c..00000000 --- a/src/secops/cli/commands/integration/jobs.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration jobs commands""" - -import json -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_jobs_command(subparsers): - """Setup integration jobs command""" - jobs_parser = subparsers.add_parser( - "jobs", - help="Manage integration jobs", - ) - lvl1 = jobs_parser.add_subparsers( - dest="jobs_command", help="Integration jobs command" - ) - - # list command - list_parser = lvl1.add_parser("list", help="List integration jobs") - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing jobs", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing jobs", - dest="order_by", - ) - list_parser.add_argument( - "--exclude-staging", - action="store_true", - help="Exclude staging jobs from the list", - dest="exclude_staging", - ) - list_parser.set_defaults(func=handle_jobs_list_command) - - # get command - get_parser = lvl1.add_parser("get", help="Get integration job details") - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--job-id", - type=str, - help="ID of the job to get", - dest="job_id", - required=True, - ) - get_parser.set_defaults(func=handle_jobs_get_command) - - # delete command - delete_parser = lvl1.add_parser("delete", help="Delete an integration job") - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--job-id", - type=str, - help="ID of the job to delete", - dest="job_id", - required=True, - ) - delete_parser.set_defaults(func=handle_jobs_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", - help="Create a new integration job", - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the job", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--code", - type=str, - help="Python code for the job", - dest="code", - required=True, - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the job", - dest="description", - ) - create_parser.add_argument( - "--job-id", - type=str, - help="Custom ID for the job", - dest="job_id", - ) - create_parser.add_argument( - "--parameters", - type=str, - help="JSON string of job parameters", - dest="parameters", - ) - create_parser.set_defaults(func=handle_jobs_create_command) - - # update command - update_parser = lvl1.add_parser("update", help="Update an integration job") - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--job-id", - type=str, - help="ID of the job to update", - dest="job_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the job", - dest="display_name", - ) - update_parser.add_argument( - "--code", - type=str, - help="New Python code for the job", - dest="code", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the job", - dest="description", - ) - update_parser.add_argument( - "--parameters", - type=str, - help="JSON string of new job parameters", - dest="parameters", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_jobs_update_command) - - # test command - test_parser = lvl1.add_parser( - "test", help="Execute an integration job test" - ) - test_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - test_parser.add_argument( - "--job-id", - type=str, - help="ID of the job to test", - dest="job_id", - required=True, - ) - test_parser.set_defaults(func=handle_jobs_test_command) - - # template command - template_parser = lvl1.add_parser( - "template", - help="Get a template for creating a job", - ) - template_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - template_parser.set_defaults(func=handle_jobs_template_command) - - -def handle_jobs_list_command(args, chronicle): - """Handle integration jobs list command""" - try: - out = chronicle.list_integration_jobs( - integration_name=args.integration_name, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - exclude_staging=args.exclude_staging, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing integration jobs: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_get_command(args, chronicle): - """Handle integration job get command""" - try: - out = chronicle.get_integration_job( - integration_name=args.integration_name, - job_id=args.job_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration job: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_delete_command(args, chronicle): - """Handle integration job delete command""" - try: - chronicle.delete_integration_job( - integration_name=args.integration_name, - job_id=args.job_id, - ) - print(f"Job {args.job_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting integration job: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_create_command(args, chronicle): - """Handle integration job create command""" - try: - # Parse parameters if provided - parameters = None - if args.parameters: - parameters = json.loads(args.parameters) - - out = chronicle.create_integration_job( - integration_name=args.integration_name, - display_name=args.display_name, - code=args.code, - description=args.description, - job_id=args.job_id, - parameters=parameters, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing parameters JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error creating integration job: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_update_command(args, chronicle): - """Handle integration job update command""" - try: - # Parse parameters if provided - parameters = None - if args.parameters: - parameters = json.loads(args.parameters) - - out = chronicle.update_integration_job( - integration_name=args.integration_name, - job_id=args.job_id, - display_name=args.display_name, - code=args.code, - description=args.description, - parameters=parameters, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except json.JSONDecodeError as e: - print(f"Error parsing parameters JSON: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error updating integration job: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_test_command(args, chronicle): - """Handle integration job test command""" - try: - # First get the job to test - job = chronicle.get_integration_job( - integration_name=args.integration_name, - job_id=args.job_id, - ) - out = chronicle.execute_integration_job_test( - integration_name=args.integration_name, - job_id=args.job_id, - job=job, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error testing integration job: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_jobs_template_command(args, chronicle): - """Handle get job template command""" - try: - out = chronicle.get_integration_job_template( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting job template: {e}", file=sys.stderr) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/logical_operator_revisions.py b/src/secops/cli/commands/integration/logical_operator_revisions.py deleted file mode 100644 index d09b9d70..00000000 --- a/src/secops/cli/commands/integration/logical_operator_revisions.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration logical operator revisions commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_logical_operator_revisions_command(subparsers): - """Setup integration logical operator revisions command""" - revisions_parser = subparsers.add_parser( - "logical-operator-revisions", - help="Manage integration logical operator revisions", - ) - lvl1 = revisions_parser.add_subparsers( - dest="logical_operator_revisions_command", - help="Integration logical operator revisions command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", - help="List integration logical operator revisions", - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator", - dest="logical_operator_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing revisions", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing revisions", - dest="order_by", - ) - list_parser.set_defaults( - func=handle_logical_operator_revisions_list_command, - ) - - # delete command - delete_parser = lvl1.add_parser( - "delete", - help="Delete an integration logical operator revision", - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator", - dest="logical_operator_id", - required=True, - ) - delete_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to delete", - dest="revision_id", - required=True, - ) - delete_parser.set_defaults( - func=handle_logical_operator_revisions_delete_command, - ) - - # create command - create_parser = lvl1.add_parser( - "create", - help="Create a new integration logical operator revision", - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator", - dest="logical_operator_id", - required=True, - ) - create_parser.add_argument( - "--comment", - type=str, - help="Comment describing the revision", - dest="comment", - ) - create_parser.set_defaults( - func=handle_logical_operator_revisions_create_command, - ) - - # rollback command - rollback_parser = lvl1.add_parser( - "rollback", - help="Rollback logical operator to a previous revision", - ) - rollback_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - rollback_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator", - dest="logical_operator_id", - required=True, - ) - rollback_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to rollback to", - dest="revision_id", - required=True, - ) - rollback_parser.set_defaults( - func=handle_logical_operator_revisions_rollback_command, - ) - - -def handle_logical_operator_revisions_list_command(args, chronicle): - """Handle integration logical operator revisions list command""" - try: - out = chronicle.list_integration_logical_operator_revisions( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing logical operator revisions: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_logical_operator_revisions_delete_command(args, chronicle): - """Handle integration logical operator revision delete command""" - try: - chronicle.delete_integration_logical_operator_revision( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - revision_id=args.revision_id, - ) - print( - f"Logical operator revision {args.revision_id} deleted successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error deleting logical operator revision: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_logical_operator_revisions_create_command(args, chronicle): - """Handle integration logical operator revision create command""" - try: - # Get the current logical operator to create a revision - logical_operator = chronicle.get_integration_logical_operator( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - ) - out = chronicle.create_integration_logical_operator_revision( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - logical_operator=logical_operator, - comment=args.comment, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error creating logical operator revision: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_logical_operator_revisions_rollback_command(args, chronicle): - """Handle integration logical operator revision rollback command""" - try: - out = chronicle.rollback_integration_logical_operator_revision( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - revision_id=args.revision_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error rolling back logical operator revision: {e}", - file=sys.stderr, - ) - sys.exit(1) - diff --git a/src/secops/cli/commands/integration/logical_operators.py b/src/secops/cli/commands/integration/logical_operators.py deleted file mode 100644 index 0bf65725..00000000 --- a/src/secops/cli/commands/integration/logical_operators.py +++ /dev/null @@ -1,395 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration logical operators commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_logical_operators_command(subparsers): - """Setup integration logical operators command""" - operators_parser = subparsers.add_parser( - "logical-operators", - help="Manage integration logical operators", - ) - lvl1 = operators_parser.add_subparsers( - dest="logical_operators_command", - help="Integration logical operators command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", - help="List integration logical operators", - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing logical operators", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing logical operators", - dest="order_by", - ) - list_parser.add_argument( - "--exclude-staging", - action="store_true", - help="Exclude staging logical operators from the response", - dest="exclude_staging", - ) - list_parser.add_argument( - "--expand", - type=str, - help="Expand the response with full logical operator details", - dest="expand", - ) - list_parser.set_defaults(func=handle_logical_operators_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", - help="Get integration logical operator details", - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator to get", - dest="logical_operator_id", - required=True, - ) - get_parser.add_argument( - "--expand", - type=str, - help="Expand the response with full logical operator details", - dest="expand", - ) - get_parser.set_defaults(func=handle_logical_operators_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", - help="Delete an integration logical operator", - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator to delete", - dest="logical_operator_id", - required=True, - ) - delete_parser.set_defaults(func=handle_logical_operators_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", - help="Create a new integration logical operator", - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the logical operator", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--script", - type=str, - help="Python script for the logical operator", - dest="script", - required=True, - ) - create_parser.add_argument( - "--script-timeout", - type=str, - help="Timeout for script execution (e.g., '60s')", - dest="script_timeout", - required=True, - ) - create_parser.add_argument( - "--enabled", - action="store_true", - help="Enable the logical operator (default: disabled)", - dest="enabled", - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the logical operator", - dest="description", - ) - create_parser.set_defaults(func=handle_logical_operators_create_command) - - # update command - update_parser = lvl1.add_parser( - "update", - help="Update an integration logical operator", - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator to update", - dest="logical_operator_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the logical operator", - dest="display_name", - ) - update_parser.add_argument( - "--script", - type=str, - help="New Python script for the logical operator", - dest="script", - ) - update_parser.add_argument( - "--script-timeout", - type=str, - help="New timeout for script execution", - dest="script_timeout", - ) - update_parser.add_argument( - "--enabled", - type=lambda x: x.lower() == "true", - help="Enable or disable the logical operator (true/false)", - dest="enabled", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the logical operator", - dest="description", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_logical_operators_update_command) - - # test command - test_parser = lvl1.add_parser( - "test", - help="Execute an integration logical operator test", - ) - test_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - test_parser.add_argument( - "--logical-operator-id", - type=str, - help="ID of the logical operator to test", - dest="logical_operator_id", - required=True, - ) - test_parser.set_defaults(func=handle_logical_operators_test_command) - - # template command - template_parser = lvl1.add_parser( - "template", - help="Get logical operator template", - ) - template_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - template_parser.set_defaults(func=handle_logical_operators_template_command) - - -def handle_logical_operators_list_command(args, chronicle): - """Handle integration logical operators list command""" - try: - out = chronicle.list_integration_logical_operators( - integration_name=args.integration_name, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - exclude_staging=args.exclude_staging, - expand=args.expand, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error listing integration logical operators: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_logical_operators_get_command(args, chronicle): - """Handle integration logical operator get command""" - try: - out = chronicle.get_integration_logical_operator( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - expand=args.expand, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting integration logical operator: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_logical_operators_delete_command(args, chronicle): - """Handle integration logical operator delete command""" - try: - chronicle.delete_integration_logical_operator( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - ) - print( - f"Logical operator {args.logical_operator_id} deleted successfully" - ) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error deleting integration logical operator: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_logical_operators_create_command(args, chronicle): - """Handle integration logical operator create command""" - try: - out = chronicle.create_integration_logical_operator( - integration_name=args.integration_name, - display_name=args.display_name, - script=args.script, - script_timeout=args.script_timeout, - enabled=args.enabled, - description=args.description, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error creating integration logical operator: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_logical_operators_update_command(args, chronicle): - """Handle integration logical operator update command""" - try: - out = chronicle.update_integration_logical_operator( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - display_name=args.display_name, - script=args.script, - script_timeout=args.script_timeout, - enabled=args.enabled, - description=args.description, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error updating integration logical operator: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_logical_operators_test_command(args, chronicle): - """Handle integration logical operator test command""" - try: - # Get the logical operator first - logical_operator = chronicle.get_integration_logical_operator( - integration_name=args.integration_name, - logical_operator_id=args.logical_operator_id, - ) - - out = chronicle.execute_integration_logical_operator_test( - integration_name=args.integration_name, - logical_operator=logical_operator, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error testing integration logical operator: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_logical_operators_template_command(args, chronicle): - """Handle integration logical operator template command""" - try: - out = chronicle.get_integration_logical_operator_template( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting logical operator template: {e}", - file=sys.stderr, - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/marketplace_integration.py b/src/secops/cli/commands/integration/marketplace_integration.py deleted file mode 100644 index f8b87aa2..00000000 --- a/src/secops/cli/commands/integration/marketplace_integration.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI marketplace integration commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_marketplace_integrations_command(subparsers): - """Setup marketplace integration command""" - mp_parser = subparsers.add_parser( - "marketplace", - help="Manage Chronicle marketplace integration", - ) - lvl1 = mp_parser.add_subparsers( - dest="mp_command", help="Marketplace integration command" - ) - - # list command - list_parser = lvl1.add_parser("list", help="List marketplace integrations") - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing marketplace integrations", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing marketplace integrations", - dest="order_by", - ) - list_parser.set_defaults(func=handle_mp_integration_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", help="Get marketplace integration details" - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the marketplace integration to get", - dest="integration_name", - required=True, - ) - get_parser.set_defaults(func=handle_mp_integration_get_command) - - # diff command - diff_parser = lvl1.add_parser( - "diff", - help="Get marketplace integration diff between " - "installed and latest version", - ) - diff_parser.add_argument( - "--integration-name", - type=str, - help="Name of the marketplace integration to diff", - dest="integration_name", - required=True, - ) - diff_parser.set_defaults(func=handle_mp_integration_diff_command) - - # install command - install_parser = lvl1.add_parser( - "install", help="Install or update a marketplace integration" - ) - install_parser.add_argument( - "--integration-name", - type=str, - help="Name of the marketplace integration to install or update", - dest="integration_name", - required=True, - ) - install_parser.add_argument( - "--override-mapping", - action="store_true", - help="Override existing mapping", - dest="override_mapping", - ) - install_parser.add_argument( - "--staging", - action="store_true", - help="Whether to install the integration in " - "staging environment (true/false)", - dest="staging", - ) - install_parser.add_argument( - "--version", - type=str, - help="Version of the marketplace integration to install", - dest="version", - ) - install_parser.add_argument( - "--restore-from-snapshot", - action="store_true", - help="Whether to restore the integration from existing snapshot " - "(true/false)", - dest="restore_from_snapshot", - ) - install_parser.set_defaults(func=handle_mp_integration_install_command) - - # uninstall command - uninstall_parser = lvl1.add_parser( - "uninstall", help="Uninstall a marketplace integration" - ) - uninstall_parser.add_argument( - "--integration-name", - type=str, - help="Name of the marketplace integration to uninstall", - dest="integration_name", - required=True, - ) - uninstall_parser.set_defaults(func=handle_mp_integration_uninstall_command) - - -def handle_mp_integration_list_command(args, chronicle): - """Handle marketplace integration list command""" - try: - out = chronicle.list_marketplace_integrations( - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing marketplace integrations: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_mp_integration_get_command(args, chronicle): - """Handle marketplace integration get command""" - try: - out = chronicle.get_marketplace_integration( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting marketplace integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_mp_integration_diff_command(args, chronicle): - """Handle marketplace integration diff command""" - try: - out = chronicle.get_marketplace_integration_diff( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting marketplace integration diff: {e}", file=sys.stderr - ) - sys.exit(1) - - -def handle_mp_integration_install_command(args, chronicle): - """Handle marketplace integration install command""" - try: - out = chronicle.install_marketplace_integration( - integration_name=args.integration_name, - override_mapping=args.override_mapping, - staging=args.staging, - version=args.version, - restore_from_snapshot=args.restore_from_snapshot, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error installing marketplace integration: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_mp_integration_uninstall_command(args, chronicle): - """Handle marketplace integration uninstall command""" - try: - out = chronicle.uninstall_marketplace_integration( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error uninstalling marketplace integration: {e}", file=sys.stderr - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/transformer_revisions.py b/src/secops/cli/commands/integration/transformer_revisions.py deleted file mode 100644 index 1075a696..00000000 --- a/src/secops/cli/commands/integration/transformer_revisions.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration transformer revisions commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_transformer_revisions_command(subparsers): - """Setup integration transformer revisions command""" - revisions_parser = subparsers.add_parser( - "transformer-revisions", - help="Manage integration transformer revisions", - ) - lvl1 = revisions_parser.add_subparsers( - dest="transformer_revisions_command", - help="Integration transformer revisions command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", - help="List integration transformer revisions", - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - list_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer", - dest="transformer_id", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing revisions", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing revisions", - dest="order_by", - ) - list_parser.set_defaults( - func=handle_transformer_revisions_list_command, - ) - - # delete command - delete_parser = lvl1.add_parser( - "delete", - help="Delete an integration transformer revision", - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer", - dest="transformer_id", - required=True, - ) - delete_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to delete", - dest="revision_id", - required=True, - ) - delete_parser.set_defaults( - func=handle_transformer_revisions_delete_command, - ) - - # create command - create_parser = lvl1.add_parser( - "create", - help="Create a new integration transformer revision", - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer", - dest="transformer_id", - required=True, - ) - create_parser.add_argument( - "--comment", - type=str, - help="Comment describing the revision", - dest="comment", - ) - create_parser.set_defaults( - func=handle_transformer_revisions_create_command, - ) - - # rollback command - rollback_parser = lvl1.add_parser( - "rollback", - help="Rollback transformer to a previous revision", - ) - rollback_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - rollback_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer", - dest="transformer_id", - required=True, - ) - rollback_parser.add_argument( - "--revision-id", - type=str, - help="ID of the revision to rollback to", - dest="revision_id", - required=True, - ) - rollback_parser.set_defaults( - func=handle_transformer_revisions_rollback_command, - ) - - -def handle_transformer_revisions_list_command(args, chronicle): - """Handle integration transformer revisions list command""" - try: - out = chronicle.list_integration_transformer_revisions( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing transformer revisions: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_transformer_revisions_delete_command(args, chronicle): - """Handle integration transformer revision delete command""" - try: - chronicle.delete_integration_transformer_revision( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - revision_id=args.revision_id, - ) - print(f"Transformer revision {args.revision_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error deleting transformer revision: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_transformer_revisions_create_command(args, chronicle): - """Handle integration transformer revision create command""" - try: - # Get the current transformer to create a revision - transformer = chronicle.get_integration_transformer( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - ) - out = chronicle.create_integration_transformer_revision( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - transformer=transformer, - comment=args.comment, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error creating transformer revision: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_transformer_revisions_rollback_command(args, chronicle): - """Handle integration transformer revision rollback command""" - try: - out = chronicle.rollback_integration_transformer_revision( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - revision_id=args.revision_id, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error rolling back transformer revision: {e}", - file=sys.stderr, - ) - sys.exit(1) diff --git a/src/secops/cli/commands/integration/transformers.py b/src/secops/cli/commands/integration/transformers.py deleted file mode 100644 index 65dcd32d..00000000 --- a/src/secops/cli/commands/integration/transformers.py +++ /dev/null @@ -1,387 +0,0 @@ -# Copyright 2025 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. -# -"""Google SecOps CLI integration transformers commands""" - -import sys - -from secops.cli.utils.formatters import output_formatter -from secops.cli.utils.common_args import ( - add_pagination_args, - add_as_list_arg, -) - - -def setup_transformers_command(subparsers): - """Setup integration transformers command""" - transformers_parser = subparsers.add_parser( - "transformers", - help="Manage integration transformers", - ) - lvl1 = transformers_parser.add_subparsers( - dest="transformers_command", - help="Integration transformers command", - ) - - # list command - list_parser = lvl1.add_parser( - "list", - help="List integration transformers", - ) - list_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - add_pagination_args(list_parser) - add_as_list_arg(list_parser) - list_parser.add_argument( - "--filter-string", - type=str, - help="Filter string for listing transformers", - dest="filter_string", - ) - list_parser.add_argument( - "--order-by", - type=str, - help="Order by string for listing transformers", - dest="order_by", - ) - list_parser.add_argument( - "--exclude-staging", - action="store_true", - help="Exclude staging transformers from the response", - dest="exclude_staging", - ) - list_parser.add_argument( - "--expand", - type=str, - help="Expand the response with full transformer details", - dest="expand", - ) - list_parser.set_defaults(func=handle_transformers_list_command) - - # get command - get_parser = lvl1.add_parser( - "get", - help="Get integration transformer details", - ) - get_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - get_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer to get", - dest="transformer_id", - required=True, - ) - get_parser.add_argument( - "--expand", - type=str, - help="Expand the response with full transformer details", - dest="expand", - ) - get_parser.set_defaults(func=handle_transformers_get_command) - - # delete command - delete_parser = lvl1.add_parser( - "delete", - help="Delete an integration transformer", - ) - delete_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - delete_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer to delete", - dest="transformer_id", - required=True, - ) - delete_parser.set_defaults(func=handle_transformers_delete_command) - - # create command - create_parser = lvl1.add_parser( - "create", - help="Create a new integration transformer", - ) - create_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - create_parser.add_argument( - "--display-name", - type=str, - help="Display name for the transformer", - dest="display_name", - required=True, - ) - create_parser.add_argument( - "--script", - type=str, - help="Python script for the transformer", - dest="script", - required=True, - ) - create_parser.add_argument( - "--script-timeout", - type=str, - help="Timeout for script execution (e.g., '60s')", - dest="script_timeout", - required=True, - ) - create_parser.add_argument( - "--enabled", - action="store_true", - help="Enable the transformer (default: disabled)", - dest="enabled", - ) - create_parser.add_argument( - "--description", - type=str, - help="Description of the transformer", - dest="description", - ) - create_parser.set_defaults(func=handle_transformers_create_command) - - # update command - update_parser = lvl1.add_parser( - "update", - help="Update an integration transformer", - ) - update_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - update_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer to update", - dest="transformer_id", - required=True, - ) - update_parser.add_argument( - "--display-name", - type=str, - help="New display name for the transformer", - dest="display_name", - ) - update_parser.add_argument( - "--script", - type=str, - help="New Python script for the transformer", - dest="script", - ) - update_parser.add_argument( - "--script-timeout", - type=str, - help="New timeout for script execution", - dest="script_timeout", - ) - update_parser.add_argument( - "--enabled", - type=lambda x: x.lower() == "true", - help="Enable or disable the transformer (true/false)", - dest="enabled", - ) - update_parser.add_argument( - "--description", - type=str, - help="New description for the transformer", - dest="description", - ) - update_parser.add_argument( - "--update-mask", - type=str, - help="Comma-separated list of fields to update", - dest="update_mask", - ) - update_parser.set_defaults(func=handle_transformers_update_command) - - # test command - test_parser = lvl1.add_parser( - "test", - help="Execute an integration transformer test", - ) - test_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - test_parser.add_argument( - "--transformer-id", - type=str, - help="ID of the transformer to test", - dest="transformer_id", - required=True, - ) - test_parser.set_defaults(func=handle_transformers_test_command) - - # template command - template_parser = lvl1.add_parser( - "template", - help="Get transformer template", - ) - template_parser.add_argument( - "--integration-name", - type=str, - help="Name of the integration", - dest="integration_name", - required=True, - ) - template_parser.set_defaults(func=handle_transformers_template_command) - - -def handle_transformers_list_command(args, chronicle): - """Handle integration transformers list command""" - try: - out = chronicle.list_integration_transformers( - integration_name=args.integration_name, - page_size=args.page_size, - page_token=args.page_token, - filter_string=args.filter_string, - order_by=args.order_by, - exclude_staging=args.exclude_staging, - expand=args.expand, - as_list=args.as_list, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error listing integration transformers: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_transformers_get_command(args, chronicle): - """Handle integration transformer get command""" - try: - out = chronicle.get_integration_transformer( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - expand=args.expand, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error getting integration transformer: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_transformers_delete_command(args, chronicle): - """Handle integration transformer delete command""" - try: - chronicle.delete_integration_transformer( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - ) - print(f"Transformer {args.transformer_id} deleted successfully") - except Exception as e: # pylint: disable=broad-exception-caught - print(f"Error deleting integration transformer: {e}", file=sys.stderr) - sys.exit(1) - - -def handle_transformers_create_command(args, chronicle): - """Handle integration transformer create command""" - try: - out = chronicle.create_integration_transformer( - integration_name=args.integration_name, - display_name=args.display_name, - script=args.script, - script_timeout=args.script_timeout, - enabled=args.enabled, - description=args.description, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error creating integration transformer: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_transformers_update_command(args, chronicle): - """Handle integration transformer update command""" - try: - out = chronicle.update_integration_transformer( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - display_name=args.display_name, - script=args.script, - script_timeout=args.script_timeout, - enabled=args.enabled, - description=args.description, - update_mask=args.update_mask, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error updating integration transformer: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_transformers_test_command(args, chronicle): - """Handle integration transformer test command""" - try: - # Get the transformer first - transformer = chronicle.get_integration_transformer( - integration_name=args.integration_name, - transformer_id=args.transformer_id, - ) - - out = chronicle.execute_integration_transformer_test( - integration_name=args.integration_name, - transformer=transformer, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error testing integration transformer: {e}", - file=sys.stderr, - ) - sys.exit(1) - - -def handle_transformers_template_command(args, chronicle): - """Handle integration transformer template command""" - try: - out = chronicle.get_integration_transformer_template( - integration_name=args.integration_name, - ) - output_formatter(out, getattr(args, "output", "json")) - except Exception as e: # pylint: disable=broad-exception-caught - print( - f"Error getting transformer template: {e}", - file=sys.stderr, - ) - sys.exit(1) diff --git a/tests/chronicle/integration/test_connector_context_properties.py b/tests/chronicle/integration/test_connector_context_properties.py deleted file mode 100644 index 33941087..00000000 --- a/tests/chronicle/integration/test_connector_context_properties.py +++ /dev/null @@ -1,561 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration connector context properties functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.connector_context_properties import ( - list_connector_context_properties, - get_connector_context_property, - delete_connector_context_property, - create_connector_context_property, - update_connector_context_property, - delete_all_connector_context_properties, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_connector_context_properties tests -- - - -def test_list_connector_context_properties_success(chronicle_client): - """Test list_connector_context_properties delegates to paginated request.""" - expected = { - "contextProperties": [{"key": "prop1"}, {"key": "prop2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.connector_context_properties.format_resource_id", - return_value="My Integration", - ): - result = list_connector_context_properties( - chronicle_client, - integration_name="My Integration", - connector_id="c1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "connectors/c1/contextProperties" in kwargs["path"] - assert kwargs["items_key"] == "contextProperties" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_connector_context_properties_default_args(chronicle_client): - """Test list_connector_context_properties with default args.""" - expected = {"contextProperties": []} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - return_value=expected, - ): - result = list_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - assert result == expected - - -def test_list_connector_context_properties_with_filters(chronicle_client): - """Test list_connector_context_properties with filter and order_by.""" - expected = {"contextProperties": [{"key": "prop1"}]} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - filter_string='key = "prop1"', - order_by="key", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'key = "prop1"', - "orderBy": "key", - } - - -def test_list_connector_context_properties_as_list(chronicle_client): - """Test list_connector_context_properties returns list when as_list=True.""" - expected = [{"key": "prop1"}, {"key": "prop2"}] - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_connector_context_properties_error(chronicle_client): - """Test list_connector_context_properties raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - side_effect=APIError("Failed to list context properties"), - ): - with pytest.raises(APIError) as exc_info: - list_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to list context properties" in str(exc_info.value) - - -# -- get_connector_context_property tests -- - - -def test_get_connector_context_property_success(chronicle_client): - """Test get_connector_context_property issues GET request.""" - expected = { - "name": "contextProperties/prop1", - "key": "prop1", - "value": "test-value", - } - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_connector_context_property_error(chronicle_client): - """Test get_connector_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - side_effect=APIError("Failed to get context property"), - ): - with pytest.raises(APIError) as exc_info: - get_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - ) - assert "Failed to get context property" in str(exc_info.value) - - -# -- delete_connector_context_property tests -- - - -def test_delete_connector_context_property_success(chronicle_client): - """Test delete_connector_context_property issues DELETE request.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_connector_context_property_error(chronicle_client): - """Test delete_connector_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - side_effect=APIError("Failed to delete context property"), - ): - with pytest.raises(APIError) as exc_info: - delete_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - ) - assert "Failed to delete context property" in str(exc_info.value) - - -# -- create_connector_context_property tests -- - - -def test_create_connector_context_property_required_fields_only(chronicle_client): - """Test create_connector_context_property with required fields only.""" - expected = {"name": "contextProperties/new", "value": "test-value"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - value="test-value", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/connectors/c1/contextProperties", - api_version=APIVersion.V1BETA, - json={"value": "test-value"}, - ) - - -def test_create_connector_context_property_with_key(chronicle_client): - """Test create_connector_context_property includes key when provided.""" - expected = {"name": "contextProperties/custom-key", "value": "test-value"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - value="test-value", - key="custom-key", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["value"] == "test-value" - assert kwargs["json"]["key"] == "custom-key" - - -def test_create_connector_context_property_error(chronicle_client): - """Test create_connector_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - side_effect=APIError("Failed to create context property"), - ): - with pytest.raises(APIError) as exc_info: - create_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - value="test-value", - ) - assert "Failed to create context property" in str(exc_info.value) - - -# -- update_connector_context_property tests -- - - -def test_update_connector_context_property_success(chronicle_client): - """Test update_connector_context_property updates value.""" - expected = { - "name": "contextProperties/prop1", - "value": "updated-value", - } - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - value="updated-value", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "PATCH" - assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] - assert kwargs["json"]["value"] == "updated-value" - assert kwargs["params"]["updateMask"] == "value" - - -def test_update_connector_context_property_with_custom_mask(chronicle_client): - """Test update_connector_context_property with custom update_mask.""" - expected = {"name": "contextProperties/prop1"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - value="updated-value", - update_mask="value", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["params"]["updateMask"] == "value" - - -def test_update_connector_context_property_error(chronicle_client): - """Test update_connector_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - side_effect=APIError("Failed to update context property"), - ): - with pytest.raises(APIError) as exc_info: - update_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - value="updated-value", - ) - assert "Failed to update context property" in str(exc_info.value) - - -# -- delete_all_connector_context_properties tests -- - - -def test_delete_all_connector_context_properties_success(chronicle_client): - """Test delete_all_connector_context_properties issues POST request.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_all_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "connectors/c1/contextProperties:clearAll" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - assert kwargs["json"] == {} - - -def test_delete_all_connector_context_properties_with_context_id(chronicle_client): - """Test delete_all_connector_context_properties with context_id.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_all_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_id="my-context", - ) - - _, kwargs = mock_request.call_args - assert kwargs["json"]["contextId"] == "my-context" - - -def test_delete_all_connector_context_properties_error(chronicle_client): - """Test delete_all_connector_context_properties raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - side_effect=APIError("Failed to clear context properties"), - ): - with pytest.raises(APIError) as exc_info: - delete_all_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to clear context properties" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_connector_context_properties_custom_api_version(chronicle_client): - """Test list_connector_context_properties with custom API version.""" - expected = {"contextProperties": []} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_connector_context_property_custom_api_version(chronicle_client): - """Test get_connector_context_property with custom API version.""" - expected = {"name": "contextProperties/prop1"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_delete_connector_context_property_custom_api_version(chronicle_client): - """Test delete_connector_context_property with custom API version.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - api_version=APIVersion.V1ALPHA, - ) - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_create_connector_context_property_custom_api_version(chronicle_client): - """Test create_connector_context_property with custom API version.""" - expected = {"name": "contextProperties/new"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - value="test-value", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_update_connector_context_property_custom_api_version(chronicle_client): - """Test update_connector_context_property with custom API version.""" - expected = {"name": "contextProperties/prop1"} - - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_context_property( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - context_property_id="prop1", - value="updated-value", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_delete_all_connector_context_properties_custom_api_version(chronicle_client): - """Test delete_all_connector_context_properties with custom API version.""" - with patch( - "secops.chronicle.integration.connector_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_all_connector_context_properties( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - api_version=APIVersion.V1ALPHA, - ) - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_connector_instance_logs.py b/tests/chronicle/integration/test_connector_instance_logs.py deleted file mode 100644 index 873264fc..00000000 --- a/tests/chronicle/integration/test_connector_instance_logs.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration connector instance logs functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.connector_instance_logs import ( - list_connector_instance_logs, - get_connector_instance_log, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_connector_instance_logs tests -- - - -def test_list_connector_instance_logs_success(chronicle_client): - """Test list_connector_instance_logs delegates to paginated request.""" - expected = { - "logs": [{"name": "log1"}, {"name": "log2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.connector_instance_logs.format_resource_id", - return_value="My Integration", - ): - result = list_connector_instance_logs( - chronicle_client, - integration_name="My Integration", - connector_id="c1", - connector_instance_id="ci1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "connectors/c1/connectorInstances/ci1/logs" in kwargs["path"] - assert kwargs["items_key"] == "logs" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_connector_instance_logs_default_args(chronicle_client): - """Test list_connector_instance_logs with default args.""" - expected = {"logs": []} - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - return_value=expected, - ): - result = list_connector_instance_logs( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - - assert result == expected - - -def test_list_connector_instance_logs_with_filters(chronicle_client): - """Test list_connector_instance_logs with filter and order_by.""" - expected = {"logs": [{"name": "log1"}]} - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instance_logs( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - filter_string='severity = "ERROR"', - order_by="timestamp desc", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'severity = "ERROR"', - "orderBy": "timestamp desc", - } - - -def test_list_connector_instance_logs_as_list(chronicle_client): - """Test list_connector_instance_logs returns list when as_list=True.""" - expected = [{"name": "log1"}, {"name": "log2"}] - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instance_logs( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_connector_instance_logs_error(chronicle_client): - """Test list_connector_instance_logs raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - side_effect=APIError("Failed to list connector instance logs"), - ): - with pytest.raises(APIError) as exc_info: - list_connector_instance_logs( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - assert "Failed to list connector instance logs" in str(exc_info.value) - - -# -- get_connector_instance_log tests -- - - -def test_get_connector_instance_log_success(chronicle_client): - """Test get_connector_instance_log issues GET request.""" - expected = { - "name": "logs/log1", - "message": "Test log message", - "severity": "INFO", - "timestamp": "2026-03-09T10:00:00Z", - } - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance_log( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - log_id="log1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "connectors/c1/connectorInstances/ci1/logs/log1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_connector_instance_log_error(chronicle_client): - """Test get_connector_instance_log raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_request", - side_effect=APIError("Failed to get connector instance log"), - ): - with pytest.raises(APIError) as exc_info: - get_connector_instance_log( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - log_id="log1", - ) - assert "Failed to get connector instance log" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_connector_instance_logs_custom_api_version(chronicle_client): - """Test list_connector_instance_logs with custom API version.""" - expected = {"logs": []} - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instance_logs( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_connector_instance_log_custom_api_version(chronicle_client): - """Test get_connector_instance_log with custom API version.""" - expected = {"name": "logs/log1"} - - with patch( - "secops.chronicle.integration.connector_instance_logs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance_log( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - log_id="log1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_connector_instances.py b/tests/chronicle/integration/test_connector_instances.py deleted file mode 100644 index 25bf3abe..00000000 --- a/tests/chronicle/integration/test_connector_instances.py +++ /dev/null @@ -1,845 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration connector instances functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import ( - APIVersion, - ConnectorInstanceParameter, -) -from secops.chronicle.integration.connector_instances import ( - list_connector_instances, - get_connector_instance, - delete_connector_instance, - create_connector_instance, - update_connector_instance, - get_connector_instance_latest_definition, - set_connector_instance_logs_collection, - run_connector_instance_on_demand, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_connector_instances tests -- - - -def test_list_connector_instances_success(chronicle_client): - """Test list_connector_instances delegates to chronicle_paginated_request.""" - expected = { - "connectorInstances": [{"name": "ci1"}, {"name": "ci2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.connector_instances.format_resource_id", - return_value="My Integration", - ): - result = list_connector_instances( - chronicle_client, - integration_name="My Integration", - connector_id="c1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "connectors/c1/connectorInstances" in kwargs["path"] - assert kwargs["items_key"] == "connectorInstances" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_connector_instances_default_args(chronicle_client): - """Test list_connector_instances with default args.""" - expected = {"connectorInstances": []} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - return_value=expected, - ): - result = list_connector_instances( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - assert result == expected - - -def test_list_connector_instances_with_filters(chronicle_client): - """Test list_connector_instances with filter and order_by.""" - expected = {"connectorInstances": [{"name": "ci1"}]} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instances( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - filter_string='enabled = true', - order_by="displayName", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'enabled = true', - "orderBy": "displayName", - } - - -def test_list_connector_instances_as_list(chronicle_client): - """Test list_connector_instances returns list when as_list=True.""" - expected = [{"name": "ci1"}, {"name": "ci2"}] - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instances( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_connector_instances_error(chronicle_client): - """Test list_connector_instances raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - side_effect=APIError("Failed to list connector instances"), - ): - with pytest.raises(APIError) as exc_info: - list_connector_instances( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to list connector instances" in str(exc_info.value) - - -# -- get_connector_instance tests -- - - -def test_get_connector_instance_success(chronicle_client): - """Test get_connector_instance issues GET request.""" - expected = { - "name": "connectorInstances/ci1", - "displayName": "Test Instance", - "enabled": True, - } - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_connector_instance_error(chronicle_client): - """Test get_connector_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to get connector instance"), - ): - with pytest.raises(APIError) as exc_info: - get_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - assert "Failed to get connector instance" in str(exc_info.value) - - -# -- delete_connector_instance tests -- - - -def test_delete_connector_instance_success(chronicle_client): - """Test delete_connector_instance issues DELETE request.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=None, - ) as mock_request: - delete_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_connector_instance_error(chronicle_client): - """Test delete_connector_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to delete connector instance"), - ): - with pytest.raises(APIError) as exc_info: - delete_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - assert "Failed to delete connector instance" in str(exc_info.value) - - -# -- create_connector_instance tests -- - - -def test_create_connector_instance_required_fields_only(chronicle_client): - """Test create_connector_instance with required fields only.""" - expected = {"name": "connectorInstances/new", "displayName": "New Instance"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - ) - - assert result == expected - - mock_request.assert_called_once() - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "connectors/c1/connectorInstances" in kwargs["endpoint_path"] - assert kwargs["json"]["environment"] == "production" - assert kwargs["json"]["displayName"] == "New Instance" - assert kwargs["json"]["intervalSeconds"] == 3600 - assert kwargs["json"]["timeoutSeconds"] == 300 - - -def test_create_connector_instance_with_optional_fields(chronicle_client): - """Test create_connector_instance includes optional fields when provided.""" - expected = {"name": "connectorInstances/new"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - description="Test description", - agent="agent-123", - allow_list=["192.168.1.0/24"], - product_field_name="product", - event_field_name="event", - integration_version="1.0.0", - version="2.0.0", - logging_enabled_until_unix_ms="1234567890000", - connector_instance_id="custom-id", - enabled=True, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["description"] == "Test description" - assert kwargs["json"]["agent"] == "agent-123" - assert kwargs["json"]["allowList"] == ["192.168.1.0/24"] - assert kwargs["json"]["productFieldName"] == "product" - assert kwargs["json"]["eventFieldName"] == "event" - assert kwargs["json"]["integrationVersion"] == "1.0.0" - assert kwargs["json"]["version"] == "2.0.0" - assert kwargs["json"]["loggingEnabledUntilUnixMs"] == "1234567890000" - assert kwargs["json"]["id"] == "custom-id" - assert kwargs["json"]["enabled"] is True - - -def test_create_connector_instance_with_parameters(chronicle_client): - """Test create_connector_instance with ConnectorInstanceParameter objects.""" - expected = {"name": "connectorInstances/new"} - - param = ConnectorInstanceParameter() - param.value = "secret-key" - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert len(kwargs["json"]["parameters"]) == 1 - assert kwargs["json"]["parameters"][0]["value"] == "secret-key" - - -def test_create_connector_instance_with_dict_parameters(chronicle_client): - """Test create_connector_instance with dict parameters.""" - expected = {"name": "connectorInstances/new"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - parameters=[{"displayName": "API Key", "value": "secret-key"}], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["parameters"][0]["displayName"] == "API Key" - - -def test_create_connector_instance_error(chronicle_client): - """Test create_connector_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to create connector instance"), - ): - with pytest.raises(APIError) as exc_info: - create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - ) - assert "Failed to create connector instance" in str(exc_info.value) - - -# -- update_connector_instance tests -- - - -def test_update_connector_instance_success(chronicle_client): - """Test update_connector_instance updates fields.""" - expected = {"name": "connectorInstances/ci1", "displayName": "Updated"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated", - enabled=True, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "PATCH" - assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] - assert kwargs["json"]["displayName"] == "Updated" - assert kwargs["json"]["enabled"] is True - # Check that update mask contains the expected fields - assert "displayName" in kwargs["params"]["updateMask"] - assert "enabled" in kwargs["params"]["updateMask"] - - -def test_update_connector_instance_with_custom_mask(chronicle_client): - """Test update_connector_instance with custom update_mask.""" - expected = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated", - update_mask="displayName", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["params"]["updateMask"] == "displayName" - - -def test_update_connector_instance_with_parameters(chronicle_client): - """Test update_connector_instance with parameters.""" - expected = {"name": "connectorInstances/ci1"} - - param = ConnectorInstanceParameter() - param.value = "new-key" - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert len(kwargs["json"]["parameters"]) == 1 - assert kwargs["json"]["parameters"][0]["value"] == "new-key" - - -def test_update_connector_instance_error(chronicle_client): - """Test update_connector_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to update connector instance"), - ): - with pytest.raises(APIError) as exc_info: - update_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated", - ) - assert "Failed to update connector instance" in str(exc_info.value) - - -# -- get_connector_instance_latest_definition tests -- - - -def test_get_connector_instance_latest_definition_success(chronicle_client): - """Test get_connector_instance_latest_definition issues GET request.""" - expected = { - "name": "connectorInstances/ci1", - "displayName": "Refreshed Instance", - } - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance_latest_definition( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "connectorInstances/ci1:fetchLatestDefinition" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_connector_instance_latest_definition_error(chronicle_client): - """Test get_connector_instance_latest_definition raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to fetch latest definition"), - ): - with pytest.raises(APIError) as exc_info: - get_connector_instance_latest_definition( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - ) - assert "Failed to fetch latest definition" in str(exc_info.value) - - -# -- set_connector_instance_logs_collection tests -- - - -def test_set_connector_instance_logs_collection_enable(chronicle_client): - """Test set_connector_instance_logs_collection enables logs.""" - expected = {"loggingEnabledUntilUnixMs": "1234567890000"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = set_connector_instance_logs_collection( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - enabled=True, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "connectorInstances/ci1:setLogsCollection" in kwargs["endpoint_path"] - assert kwargs["json"]["enabled"] is True - - -def test_set_connector_instance_logs_collection_disable(chronicle_client): - """Test set_connector_instance_logs_collection disables logs.""" - expected = {} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = set_connector_instance_logs_collection( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - enabled=False, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["enabled"] is False - - -def test_set_connector_instance_logs_collection_error(chronicle_client): - """Test set_connector_instance_logs_collection raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to set logs collection"), - ): - with pytest.raises(APIError) as exc_info: - set_connector_instance_logs_collection( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - enabled=True, - ) - assert "Failed to set logs collection" in str(exc_info.value) - - -# -- run_connector_instance_on_demand tests -- - - -def test_run_connector_instance_on_demand_success(chronicle_client): - """Test run_connector_instance_on_demand triggers execution.""" - expected = { - "debugOutput": "Execution completed", - "success": True, - "sampleCases": [], - } - - connector_instance = { - "name": "connectorInstances/ci1", - "displayName": "Test Instance", - "parameters": [{"displayName": "param1", "value": "value1"}], - } - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = run_connector_instance_on_demand( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - connector_instance=connector_instance, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "connectorInstances/ci1:runOnDemand" in kwargs["endpoint_path"] - assert kwargs["json"]["connectorInstance"] == connector_instance - - -def test_run_connector_instance_on_demand_error(chronicle_client): - """Test run_connector_instance_on_demand raises APIError on failure.""" - connector_instance = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - side_effect=APIError("Failed to run connector instance"), - ): - with pytest.raises(APIError) as exc_info: - run_connector_instance_on_demand( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - connector_instance=connector_instance, - ) - assert "Failed to run connector instance" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_connector_instances_custom_api_version(chronicle_client): - """Test list_connector_instances with custom API version.""" - expected = {"connectorInstances": []} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_connector_instances( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_connector_instance_custom_api_version(chronicle_client): - """Test get_connector_instance with custom API version.""" - expected = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_delete_connector_instance_custom_api_version(chronicle_client): - """Test delete_connector_instance with custom API version.""" - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=None, - ) as mock_request: - delete_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - api_version=APIVersion.V1ALPHA, - ) - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_create_connector_instance_custom_api_version(chronicle_client): - """Test create_connector_instance with custom API version.""" - expected = {"name": "connectorInstances/new"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - environment="production", - display_name="New Instance", - interval_seconds=3600, - timeout_seconds=300, - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_update_connector_instance_custom_api_version(chronicle_client): - """Test update_connector_instance with custom API version.""" - expected = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_connector_instance( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - display_name="Updated", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_connector_instance_latest_definition_custom_api_version(chronicle_client): - """Test get_connector_instance_latest_definition with custom API version.""" - expected = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_connector_instance_latest_definition( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_set_connector_instance_logs_collection_custom_api_version(chronicle_client): - """Test set_connector_instance_logs_collection with custom API version.""" - expected = {} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = set_connector_instance_logs_collection( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - enabled=True, - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_run_connector_instance_on_demand_custom_api_version(chronicle_client): - """Test run_connector_instance_on_demand with custom API version.""" - expected = {"success": True} - connector_instance = {"name": "connectorInstances/ci1"} - - with patch( - "secops.chronicle.integration.connector_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = run_connector_instance_on_demand( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector_instance_id="ci1", - connector_instance=connector_instance, - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - - - diff --git a/tests/chronicle/integration/test_connector_revisions.py b/tests/chronicle/integration/test_connector_revisions.py deleted file mode 100644 index 7b214bcb..00000000 --- a/tests/chronicle/integration/test_connector_revisions.py +++ /dev/null @@ -1,385 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration connector revisions functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.connector_revisions import ( - list_integration_connector_revisions, - delete_integration_connector_revision, - create_integration_connector_revision, - rollback_integration_connector_revision, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_connector_revisions tests -- - - -def test_list_integration_connector_revisions_success(chronicle_client): - """Test list_integration_connector_revisions delegates to chronicle_paginated_request.""" - expected = { - "revisions": [{"name": "r1"}, {"name": "r2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.connector_revisions.format_resource_id", - return_value="My Integration", - ): - result = list_integration_connector_revisions( - chronicle_client, - integration_name="My Integration", - connector_id="c1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "connectors/c1/revisions" in kwargs["path"] - assert kwargs["items_key"] == "revisions" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_integration_connector_revisions_default_args(chronicle_client): - """Test list_integration_connector_revisions with default args.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connector_revisions( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - assert result == expected - - -def test_list_integration_connector_revisions_with_filters(chronicle_client): - """Test list_integration_connector_revisions with filter and order_by.""" - expected = {"revisions": [{"name": "r1"}]} - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connector_revisions( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - filter_string='version = "1.0"', - order_by="createTime", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'version = "1.0"', - "orderBy": "createTime", - } - - -def test_list_integration_connector_revisions_as_list(chronicle_client): - """Test list_integration_connector_revisions returns list when as_list=True.""" - expected = [{"name": "r1"}, {"name": "r2"}] - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connector_revisions( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_connector_revisions_error(chronicle_client): - """Test list_integration_connector_revisions raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - side_effect=APIError("Failed to list connector revisions"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_connector_revisions( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to list connector revisions" in str(exc_info.value) - - -# -- delete_integration_connector_revision tests -- - - -def test_delete_integration_connector_revision_success(chronicle_client): - """Test delete_integration_connector_revision issues DELETE request.""" - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - revision_id="r1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "connectors/c1/revisions/r1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_integration_connector_revision_error(chronicle_client): - """Test delete_integration_connector_revision raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - side_effect=APIError("Failed to delete connector revision"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - revision_id="r1", - ) - assert "Failed to delete connector revision" in str(exc_info.value) - - -# -- create_integration_connector_revision tests -- - - -def test_create_integration_connector_revision_required_fields_only( - chronicle_client, -): - """Test create_integration_connector_revision with required fields only.""" - expected = { - "name": "revisions/new", - "connector": {"displayName": "My Connector"}, - } - connector_dict = { - "displayName": "My Connector", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector=connector_dict, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/connectors/c1/revisions" - ), - api_version=APIVersion.V1BETA, - json={"connector": connector_dict}, - ) - - -def test_create_integration_connector_revision_with_comment(chronicle_client): - """Test create_integration_connector_revision includes comment when provided.""" - expected = {"name": "revisions/new"} - connector_dict = { - "displayName": "My Connector", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector=connector_dict, - comment="Backup before major update", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["comment"] == "Backup before major update" - assert kwargs["json"]["connector"] == connector_dict - - -def test_create_integration_connector_revision_error(chronicle_client): - """Test create_integration_connector_revision raises APIError on failure.""" - connector_dict = { - "displayName": "My Connector", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - side_effect=APIError("Failed to create connector revision"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - connector=connector_dict, - ) - assert "Failed to create connector revision" in str(exc_info.value) - - -# -- rollback_integration_connector_revision tests -- - - -def test_rollback_integration_connector_revision_success(chronicle_client): - """Test rollback_integration_connector_revision issues POST request.""" - expected = { - "name": "revisions/r1", - "connector": { - "displayName": "My Connector", - "script": "print('hello')", - }, - } - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = rollback_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - revision_id="r1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "connectors/c1/revisions/r1:rollback" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_rollback_integration_connector_revision_error(chronicle_client): - """Test rollback_integration_connector_revision raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - side_effect=APIError("Failed to rollback connector revision"), - ): - with pytest.raises(APIError) as exc_info: - rollback_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - revision_id="r1", - ) - assert "Failed to rollback connector revision" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_integration_connector_revisions_custom_api_version( - chronicle_client, -): - """Test list_integration_connector_revisions with custom API version.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connector_revisions( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_delete_integration_connector_revision_custom_api_version( - chronicle_client, -): - """Test delete_integration_connector_revision with custom API version.""" - with patch( - "secops.chronicle.integration.connector_revisions.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_connector_revision( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - revision_id="r1", - api_version=APIVersion.V1ALPHA, - ) - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_connectors.py b/tests/chronicle/integration/test_connectors.py deleted file mode 100644 index 3aca859a..00000000 --- a/tests/chronicle/integration/test_connectors.py +++ /dev/null @@ -1,665 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration connectors functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import ( - APIVersion, - ConnectorParameter, - ParamType, - ConnectorParamMode, - ConnectorRule, - ConnectorRuleType, -) -from secops.chronicle.integration.connectors import ( - list_integration_connectors, - get_integration_connector, - delete_integration_connector, - create_integration_connector, - update_integration_connector, - execute_integration_connector_test, - get_integration_connector_template, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_connectors tests -- - - -def test_list_integration_connectors_success(chronicle_client): - """Test list_integration_connectors delegates to chronicle_paginated_request.""" - expected = {"connectors": [{"name": "c1"}, {"name": "c2"}], "nextPageToken": "t"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.connectors.format_resource_id", - return_value="My Integration", - ): - result = list_integration_connectors( - chronicle_client, - integration_name="My Integration", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="integrations/My Integration/connectors", - items_key="connectors", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_connectors_default_args(chronicle_client): - """Test list_integration_connectors with default args.""" - expected = {"connectors": []} - - with patch( - "secops.chronicle.integration.connectors.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connectors( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - -def test_list_integration_connectors_with_filters(chronicle_client): - """Test list_integration_connectors with filter and order_by.""" - expected = {"connectors": [{"name": "c1"}]} - - with patch( - "secops.chronicle.integration.connectors.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connectors( - chronicle_client, - integration_name="test-integration", - filter_string="enabled=true", - order_by="displayName", - exclude_staging=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": "enabled=true", - "orderBy": "displayName", - "excludeStaging": True, - } - - -def test_list_integration_connectors_as_list(chronicle_client): - """Test list_integration_connectors returns list when as_list=True.""" - expected = [{"name": "c1"}, {"name": "c2"}] - - with patch( - "secops.chronicle.integration.connectors.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_connectors( - chronicle_client, - integration_name="test-integration", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_connectors_error(chronicle_client): - """Test list_integration_connectors raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_paginated_request", - side_effect=APIError("Failed to list integration connectors"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_connectors( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to list integration connectors" in str(exc_info.value) - - -# -- get_integration_connector tests -- - - -def test_get_integration_connector_success(chronicle_client): - """Test get_integration_connector issues GET request.""" - expected = { - "name": "connectors/c1", - "displayName": "My Connector", - "script": "print('hello')", - } - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/connectors/c1", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_connector_error(chronicle_client): - """Test get_integration_connector raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to get integration connector"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to get integration connector" in str(exc_info.value) - - -# -- delete_integration_connector tests -- - - -def test_delete_integration_connector_success(chronicle_client): - """Test delete_integration_connector issues DELETE request.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path="integrations/test-integration/connectors/c1", - api_version=APIVersion.V1BETA, - ) - - -def test_delete_integration_connector_error(chronicle_client): - """Test delete_integration_connector raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to delete integration connector"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - ) - assert "Failed to delete integration connector" in str(exc_info.value) - - -# -- create_integration_connector tests -- - - -def test_create_integration_connector_required_fields_only(chronicle_client): - """Test create_integration_connector sends only required fields when optionals omitted.""" - expected = {"name": "connectors/new", "displayName": "My Connector"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector( - chronicle_client, - integration_name="test-integration", - display_name="My Connector", - script="print('hi')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/connectors", - api_version=APIVersion.V1BETA, - json={ - "displayName": "My Connector", - "script": "print('hi')", - "timeoutSeconds": 300, - "enabled": True, - "productFieldName": "product", - "eventFieldName": "event", - }, - ) - - -def test_create_integration_connector_with_optional_fields(chronicle_client): - """Test create_integration_connector includes optional fields when provided.""" - expected = {"name": "connectors/new"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector( - chronicle_client, - integration_name="test-integration", - display_name="My Connector", - script="print('hi')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event", - description="Test connector", - parameters=[{"name": "p1", "type": "STRING"}], - rules=[{"name": "r1", "type": "MAPPING"}], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["description"] == "Test connector" - assert kwargs["json"]["parameters"] == [{"name": "p1", "type": "STRING"}] - assert kwargs["json"]["rules"] == [{"name": "r1", "type": "MAPPING"}] - - -def test_create_integration_connector_with_dataclass_parameters(chronicle_client): - """Test create_integration_connector converts ConnectorParameter dataclasses.""" - expected = {"name": "connectors/new"} - - param = ConnectorParameter( - display_name="API Key", - type=ParamType.STRING, - mode=ConnectorParamMode.REGULAR, - mandatory=True, - description="API key for authentication", - ) - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector( - chronicle_client, - integration_name="test-integration", - display_name="My Connector", - script="print('hi')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["displayName"] == "API Key" - assert params_sent[0]["type"] == "STRING" - - -def test_create_integration_connector_with_dataclass_rules(chronicle_client): - """Test create_integration_connector converts ConnectorRule dataclasses.""" - expected = {"name": "connectors/new"} - - rule = ConnectorRule( - display_name="Mapping Rule", - type=ConnectorRuleType.ALLOW_LIST, - ) - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_connector( - chronicle_client, - integration_name="test-integration", - display_name="My Connector", - script="print('hi')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event", - rules=[rule], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - rules_sent = kwargs["json"]["rules"] - assert len(rules_sent) == 1 - assert rules_sent[0]["displayName"] == "Mapping Rule" - assert rules_sent[0]["type"] == "ALLOW_LIST" - - -def test_create_integration_connector_error(chronicle_client): - """Test create_integration_connector raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to create integration connector"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_connector( - chronicle_client, - integration_name="test-integration", - display_name="My Connector", - script="print('hi')", - timeout_seconds=300, - enabled=True, - product_field_name="product", - event_field_name="event", - ) - assert "Failed to create integration connector" in str(exc_info.value) - - -# -- update_integration_connector tests -- - - -def test_update_integration_connector_with_explicit_update_mask(chronicle_client): - """Test update_integration_connector passes through explicit update_mask.""" - expected = {"name": "connectors/c1", "displayName": "New Name"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - display_name="New Name", - update_mask="displayName", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path="integrations/test-integration/connectors/c1", - api_version=APIVersion.V1BETA, - json={"displayName": "New Name"}, - params={"updateMask": "displayName"}, - ) - - -def test_update_integration_connector_auto_update_mask(chronicle_client): - """Test update_integration_connector auto-generates updateMask based on fields.""" - expected = {"name": "connectors/c1"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - enabled=False, - timeout_seconds=600, - ) - - assert result == expected - - assert mock_request.call_count == 1 - _, kwargs = mock_request.call_args - - assert kwargs["method"] == "PATCH" - assert kwargs["endpoint_path"] == "integrations/test-integration/connectors/c1" - assert kwargs["api_version"] == APIVersion.V1BETA - - assert kwargs["json"] == {"enabled": False, "timeoutSeconds": 600} - - update_mask = kwargs["params"]["updateMask"] - assert set(update_mask.split(",")) == {"enabled", "timeoutSeconds"} - - -def test_update_integration_connector_with_parameters(chronicle_client): - """Test update_integration_connector with parameters field.""" - expected = {"name": "connectors/c1"} - - param = ConnectorParameter( - display_name="Auth Token", - type=ParamType.STRING, - mode=ConnectorParamMode.REGULAR, - mandatory=True, - ) - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["displayName"] == "Auth Token" - - -def test_update_integration_connector_with_rules(chronicle_client): - """Test update_integration_connector with rules field.""" - expected = {"name": "connectors/c1"} - - rule = ConnectorRule( - display_name="Filter Rule", - type=ConnectorRuleType.BLOCK_LIST, - ) - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - rules=[rule], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - rules_sent = kwargs["json"]["rules"] - assert len(rules_sent) == 1 - assert rules_sent[0]["displayName"] == "Filter Rule" - - -def test_update_integration_connector_error(chronicle_client): - """Test update_integration_connector raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to update integration connector"), - ): - with pytest.raises(APIError) as exc_info: - update_integration_connector( - chronicle_client, - integration_name="test-integration", - connector_id="c1", - display_name="New Name", - ) - assert "Failed to update integration connector" in str(exc_info.value) - - -# -- execute_integration_connector_test tests -- - - -def test_execute_integration_connector_test_success(chronicle_client): - """Test execute_integration_connector_test sends POST request with connector.""" - expected = { - "outputMessage": "Success", - "debugOutputMessage": "Debug info", - "resultJson": {"status": "ok"}, - } - - connector = { - "displayName": "Test Connector", - "script": "print('test')", - "enabled": True, - } - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = execute_integration_connector_test( - chronicle_client, - integration_name="test-integration", - connector=connector, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/connectors:executeTest", - api_version=APIVersion.V1BETA, - json={"connector": connector}, - ) - - -def test_execute_integration_connector_test_with_agent_identifier(chronicle_client): - """Test execute_integration_connector_test includes agent_identifier when provided.""" - expected = {"outputMessage": "Success"} - - connector = {"displayName": "Test", "script": "print('test')"} - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = execute_integration_connector_test( - chronicle_client, - integration_name="test-integration", - connector=connector, - agent_identifier="agent-123", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["agentIdentifier"] == "agent-123" - - -def test_execute_integration_connector_test_error(chronicle_client): - """Test execute_integration_connector_test raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to execute connector test"), - ): - with pytest.raises(APIError) as exc_info: - execute_integration_connector_test( - chronicle_client, - integration_name="test-integration", - connector={"displayName": "Test"}, - ) - assert "Failed to execute connector test" in str(exc_info.value) - - -# -- get_integration_connector_template tests -- - - -def test_get_integration_connector_template_success(chronicle_client): - """Test get_integration_connector_template issues GET request.""" - expected = { - "script": "# Template script\nprint('hello')", - "displayName": "Template Connector", - } - - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_connector_template( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/connectors:fetchTemplate", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_connector_template_error(chronicle_client): - """Test get_integration_connector_template raises APIError on failure.""" - with patch( - "secops.chronicle.integration.connectors.chronicle_request", - side_effect=APIError("Failed to get connector template"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_connector_template( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to get connector template" in str(exc_info.value) - diff --git a/tests/chronicle/integration/test_integration_instances.py b/tests/chronicle/integration/test_integration_instances.py deleted file mode 100644 index 153390ad..00000000 --- a/tests/chronicle/integration/test_integration_instances.py +++ /dev/null @@ -1,623 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration instances functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import ( - APIVersion, - IntegrationInstanceParameter, -) -from secops.chronicle.integration.integration_instances import ( - list_integration_instances, - get_integration_instance, - delete_integration_instance, - create_integration_instance, - update_integration_instance, - execute_integration_instance_test, - get_integration_instance_affected_items, - get_default_integration_instance, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_instances tests -- - - -def test_list_integration_instances_success(chronicle_client): - """Test list_integration_instances delegates to chronicle_paginated_request.""" - expected = { - "integrationInstances": [{"name": "ii1"}, {"name": "ii2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.integration_instances.format_resource_id", - return_value="My Integration", - ): - result = list_integration_instances( - chronicle_client, - integration_name="My Integration", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "integrationInstances" in kwargs["path"] - assert kwargs["items_key"] == "integrationInstances" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_integration_instances_default_args(chronicle_client): - """Test list_integration_instances with default args.""" - expected = {"integrationInstances": []} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", - return_value=expected, - ): - result = list_integration_instances( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - -def test_list_integration_instances_with_filters(chronicle_client): - """Test list_integration_instances with filter and order_by.""" - expected = {"integrationInstances": [{"name": "ii1"}]} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_instances( - chronicle_client, - integration_name="test-integration", - filter_string="environment = 'prod'", - order_by="displayName", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": "environment = 'prod'", - "orderBy": "displayName", - } - - -def test_list_integration_instances_as_list(chronicle_client): - """Test list_integration_instances returns list when as_list=True.""" - expected = [{"name": "ii1"}, {"name": "ii2"}] - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_instances( - chronicle_client, - integration_name="test-integration", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_instances_error(chronicle_client): - """Test list_integration_instances raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", - side_effect=APIError("Failed to list integration instances"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_instances( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to list integration instances" in str(exc_info.value) - - -# -- get_integration_instance tests -- - - -def test_get_integration_instance_success(chronicle_client): - """Test get_integration_instance issues GET request.""" - expected = { - "name": "integrationInstances/ii1", - "displayName": "My Instance", - "environment": "production", - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "integrationInstances/ii1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_integration_instance_error(chronicle_client): - """Test get_integration_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to get integration instance"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - assert "Failed to get integration instance" in str(exc_info.value) - - -# -- delete_integration_instance tests -- - - -def test_delete_integration_instance_success(chronicle_client): - """Test delete_integration_instance issues DELETE request.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "integrationInstances/ii1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_integration_instance_error(chronicle_client): - """Test delete_integration_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to delete integration instance"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - assert "Failed to delete integration instance" in str(exc_info.value) - - -# -- create_integration_instance tests -- - - -def test_create_integration_instance_required_fields_only(chronicle_client): - """Test create_integration_instance sends only required fields.""" - expected = {"name": "integrationInstances/new", "displayName": "My Instance"} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_instance( - chronicle_client, - integration_name="test-integration", - environment="production", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/integrationInstances", - api_version=APIVersion.V1BETA, - json={ - "environment": "production", - }, - ) - - -def test_create_integration_instance_with_optional_fields(chronicle_client): - """Test create_integration_instance includes optional fields when provided.""" - expected = {"name": "integrationInstances/new"} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_instance( - chronicle_client, - integration_name="test-integration", - environment="production", - display_name="My Instance", - description="Test instance", - parameters=[{"id": 1, "value": "test"}], - agent="agent-123", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["environment"] == "production" - assert kwargs["json"]["displayName"] == "My Instance" - assert kwargs["json"]["description"] == "Test instance" - assert kwargs["json"]["parameters"] == [{"id": 1, "value": "test"}] - assert kwargs["json"]["agent"] == "agent-123" - - -def test_create_integration_instance_with_dataclass_params(chronicle_client): - """Test create_integration_instance converts dataclass parameters.""" - expected = {"name": "integrationInstances/new"} - - param = IntegrationInstanceParameter(value="test-value") - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_instance( - chronicle_client, - integration_name="test-integration", - environment="production", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["value"] == "test-value" - - -def test_create_integration_instance_error(chronicle_client): - """Test create_integration_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to create integration instance"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_instance( - chronicle_client, - integration_name="test-integration", - environment="production", - ) - assert "Failed to create integration instance" in str(exc_info.value) - - -# -- update_integration_instance tests -- - - -def test_update_integration_instance_with_single_field(chronicle_client): - """Test update_integration_instance with single field updates updateMask.""" - expected = {"name": "integrationInstances/ii1", "displayName": "Updated"} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - display_name="Updated", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "PATCH" - assert "integrationInstances/ii1" in kwargs["endpoint_path"] - assert kwargs["json"]["displayName"] == "Updated" - assert kwargs["params"]["updateMask"] == "displayName" - - -def test_update_integration_instance_with_multiple_fields(chronicle_client): - """Test update_integration_instance with multiple fields.""" - expected = {"name": "integrationInstances/ii1"} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - display_name="Updated", - description="New description", - environment="staging", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["displayName"] == "Updated" - assert kwargs["json"]["description"] == "New description" - assert kwargs["json"]["environment"] == "staging" - assert "displayName" in kwargs["params"]["updateMask"] - assert "description" in kwargs["params"]["updateMask"] - assert "environment" in kwargs["params"]["updateMask"] - - -def test_update_integration_instance_with_custom_update_mask(chronicle_client): - """Test update_integration_instance with explicitly provided update_mask.""" - expected = {"name": "integrationInstances/ii1"} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - display_name="Updated", - update_mask="displayName,environment", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["params"]["updateMask"] == "displayName,environment" - - -def test_update_integration_instance_with_dataclass_params(chronicle_client): - """Test update_integration_instance converts dataclass parameters.""" - expected = {"name": "integrationInstances/ii1"} - - param = IntegrationInstanceParameter(value="test-value") - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["value"] == "test-value" - - -def test_update_integration_instance_error(chronicle_client): - """Test update_integration_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to update integration instance"), - ): - with pytest.raises(APIError) as exc_info: - update_integration_instance( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - display_name="Updated", - ) - assert "Failed to update integration instance" in str(exc_info.value) - - -# -- execute_integration_instance_test tests -- - - -def test_execute_integration_instance_test_success(chronicle_client): - """Test execute_integration_instance_test issues POST request.""" - expected = { - "successful": True, - "message": "Test successful", - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = execute_integration_instance_test( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "integrationInstances/ii1:executeTest" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_execute_integration_instance_test_failure(chronicle_client): - """Test execute_integration_instance_test when test fails.""" - expected = { - "successful": False, - "message": "Connection failed", - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ): - result = execute_integration_instance_test( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - assert result == expected - assert result["successful"] is False - - -def test_execute_integration_instance_test_error(chronicle_client): - """Test execute_integration_instance_test raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to execute test"), - ): - with pytest.raises(APIError) as exc_info: - execute_integration_instance_test( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - assert "Failed to execute test" in str(exc_info.value) - - -# -- get_integration_instance_affected_items tests -- - - -def test_get_integration_instance_affected_items_success(chronicle_client): - """Test get_integration_instance_affected_items issues GET request.""" - expected = { - "affectedPlaybooks": [ - {"name": "playbook1", "displayName": "Playbook 1"}, - {"name": "playbook2", "displayName": "Playbook 2"}, - ] - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_instance_affected_items( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "integrationInstances/ii1:fetchAffectedItems" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_integration_instance_affected_items_empty(chronicle_client): - """Test get_integration_instance_affected_items with no affected items.""" - expected = {"affectedPlaybooks": []} - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ): - result = get_integration_instance_affected_items( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - - assert result == expected - assert len(result["affectedPlaybooks"]) == 0 - - -def test_get_integration_instance_affected_items_error(chronicle_client): - """Test get_integration_instance_affected_items raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to fetch affected items"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_instance_affected_items( - chronicle_client, - integration_name="test-integration", - integration_instance_id="ii1", - ) - assert "Failed to fetch affected items" in str(exc_info.value) - - -# -- get_default_integration_instance tests -- - - -def test_get_default_integration_instance_success(chronicle_client): - """Test get_default_integration_instance issues GET request.""" - expected = { - "name": "integrationInstances/default", - "displayName": "Default Instance", - "environment": "default", - } - - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_default_integration_instance( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "integrationInstances:fetchDefaultInstance" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_default_integration_instance_error(chronicle_client): - """Test get_default_integration_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", - side_effect=APIError("Failed to get default instance"), - ): - with pytest.raises(APIError) as exc_info: - get_default_integration_instance( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to get default instance" in str(exc_info.value) - diff --git a/tests/chronicle/integration/test_integrations.py b/tests/chronicle/integration/test_integrations.py deleted file mode 100644 index 811ab052..00000000 --- a/tests/chronicle/integration/test_integrations.py +++ /dev/null @@ -1,909 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import ( - APIVersion, - DiffType, - TargetMode, - PythonVersion, -) -from secops.chronicle.integration.integrations import ( - list_integrations, - get_integration, - delete_integration, - create_integration, - download_integration, - download_integration_dependency, - export_integration_items, - get_integration_affected_items, - get_agent_integrations, - get_integration_dependencies, - get_integration_restricted_agents, - get_integration_diff, - transition_integration, - update_integration, - update_custom_integration, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -@pytest.fixture -def mock_response() -> Mock: - """Create a mock API response object.""" - mock = Mock() - mock.status_code = 200 - mock.json.return_value = {} - return mock - - -@pytest.fixture -def mock_error_response() -> Mock: - """Create a mock error API response object.""" - mock = Mock() - mock.status_code = 400 - mock.text = "Error message" - mock.raise_for_status.side_effect = Exception("API Error") - return mock - - -# -- list_integrations tests -- - - -def test_list_integrations_success(chronicle_client): - """Test list_integrations delegates to chronicle_paginated_request.""" - expected = {"integrations": [{"name": "i1"}, {"name": "i2"}]} - - with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integrations( - chronicle_client, - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="integrations", - items_key="integrations", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integrations_with_filter_and_order_by(chronicle_client): - """Test list_integrations passes filter_string and order_by in extra_params.""" - expected = {"integrations": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integrations( - chronicle_client, - filter_string='displayName = "My Integration"', - order_by="displayName", - as_list=True, - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="integrations", - items_key="integrations", - page_size=None, - page_token=None, - extra_params={ - "filter": 'displayName = "My Integration"', - "orderBy": "displayName", - }, - as_list=True, - ) - - -def test_list_integrations_error(chronicle_client): - """Test list_integrations propagates APIError from helper.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", - side_effect=APIError("Failed to list integrations"), - ): - with pytest.raises(APIError) as exc_info: - list_integrations(chronicle_client) - - assert "Failed to list integrations" in str(exc_info.value) - - -# -- get_integration tests -- - - -def test_get_integration_success(chronicle_client): - """Test get_integration returns expected result.""" - expected = {"name": "integrations/test-integration", "displayName": "Test"} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration(chronicle_client, "test-integration") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_error(chronicle_client): - """Test get_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to get integration"), - ): - with pytest.raises(APIError) as exc_info: - get_integration(chronicle_client, "test-integration") - - assert "Failed to get integration" in str(exc_info.value) - - -# -- delete_integration tests -- - - -def test_delete_integration_success(chronicle_client): - """Test delete_integration delegates to chronicle_request.""" - with patch("secops.chronicle.integration.integrations.chronicle_request") as mock_request: - delete_integration(chronicle_client, "test-integration") - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path="integrations/test-integration", - api_version=APIVersion.V1BETA, - ) - - -def test_delete_integration_error(chronicle_client): - """Test delete_integration propagates APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to delete integration"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration(chronicle_client, "test-integration") - - assert "Failed to delete integration" in str(exc_info.value) - - -# -- create_integration tests -- - - -def test_create_integration_required_fields_only(chronicle_client): - """Test create_integration with required fields only.""" - expected = {"name": "integrations/test", "displayName": "Test"} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration( - chronicle_client, - display_name="Test", - staging=True, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations", - json={"displayName": "Test", "staging": True}, - api_version=APIVersion.V1BETA, - ) - - -def test_create_integration_all_optional_fields(chronicle_client): - """Test create_integration with all optional fields.""" - expected = {"name": "integrations/test"} - - python_version = list(PythonVersion)[0] - integration_type = Mock(name="integration_type") - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration( - chronicle_client, - display_name="Test", - staging=False, - description="desc", - image_base64="b64", - svg_icon="", - python_version=python_version, - parameters=[{"id": "p1"}], - categories=["cat"], - integration_type=integration_type, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations", - json={ - "displayName": "Test", - "staging": False, - "description": "desc", - "imageBase64": "b64", - "svgIcon": "", - "pythonVersion": python_version, - "parameters": [{"id": "p1"}], - "categories": ["cat"], - "type": integration_type, - }, - api_version=APIVersion.V1BETA, - ) - - -def test_create_integration_none_fields_excluded(chronicle_client): - """Test that None optional fields are excluded from create_integration body.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value={"name": "integrations/test"}, - ) as mock_request: - create_integration( - chronicle_client, - display_name="Test", - staging=True, - description=None, - image_base64=None, - svg_icon=None, - python_version=None, - parameters=None, - categories=None, - integration_type=None, - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations", - json={"displayName": "Test", "staging": True}, - api_version=APIVersion.V1BETA, - ) - - -def test_create_integration_error(chronicle_client): - """Test create_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to create integration"), - ): - with pytest.raises(APIError) as exc_info: - create_integration(chronicle_client, display_name="Test", staging=True) - - assert "Failed to create integration" in str(exc_info.value) - - -# -- download_integration tests -- - - -def test_download_integration_success(chronicle_client): - """Test download_integration uses chronicle_request_bytes with alt=media and zip accept.""" - expected = b"ZIPBYTES" - - with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", - return_value=expected, - ) as mock_bytes: - result = download_integration(chronicle_client, "test-integration") - - assert result == expected - - mock_bytes.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:export", - api_version=APIVersion.V1BETA, - params={"alt": "media"}, - headers={"Accept": "application/zip"}, - ) - - -def test_download_integration_error(chronicle_client): - """Test download_integration propagates APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", - side_effect=APIError("Failed to download integration"), - ): - with pytest.raises(APIError) as exc_info: - download_integration(chronicle_client, "test-integration") - - assert "Failed to download integration" in str(exc_info.value) - - -# -- download_integration_dependency tests -- - - -def test_download_integration_dependency_success(chronicle_client): - """Test download_integration_dependency posts dependency name.""" - expected = {"dependency": "requests"} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = download_integration_dependency( - chronicle_client, - integration_name="test-integration", - dependency_name="requests==2.32.0", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration:downloadDependency", - json={"dependency": "requests==2.32.0"}, - api_version=APIVersion.V1BETA, - ) - - -def test_download_integration_dependency_error(chronicle_client): - """Test download_integration_dependency raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to download dependency"), - ): - with pytest.raises(APIError) as exc_info: - download_integration_dependency( - chronicle_client, - integration_name="test-integration", - dependency_name="requests", - ) - - assert "Failed to download dependency" in str(exc_info.value) - - -# -- export_integration_items tests -- - - -def test_export_integration_items_success_some_fields(chronicle_client): - """Test export_integration_items builds params correctly and uses chronicle_request_bytes.""" - expected = b"ZIPBYTES" - - with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", - return_value=expected, - ) as mock_bytes: - result = export_integration_items( - chronicle_client, - integration_name="test-integration", - actions=["1", "2"], - connectors=["10"], - logical_operators=["7"], - ) - - assert result == expected - - mock_bytes.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:exportItems", - params={ - "actions": "1,2", - "connectors": ["10"], - "logicalOperators": ["7"], - "alt": "media", - }, - api_version=APIVersion.V1BETA, - headers={"Accept": "application/zip"}, - ) - - -def test_export_integration_items_no_fields(chronicle_client): - """Test export_integration_items always includes alt=media.""" - expected = b"ZIPBYTES" - - with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", - return_value=expected, - ) as mock_bytes: - result = export_integration_items( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_bytes.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:exportItems", - params={"alt": "media"}, - api_version=APIVersion.V1BETA, - headers={"Accept": "application/zip"}, - ) - - -def test_export_integration_items_error(chronicle_client): - """Test export_integration_items propagates APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", - side_effect=APIError("Failed to export integration items"), - ): - with pytest.raises(APIError) as exc_info: - export_integration_items(chronicle_client, "test-integration") - - assert "Failed to export integration items" in str(exc_info.value) - - -# -- get_integration_affected_items tests -- - - -def test_get_integration_affected_items_success(chronicle_client): - """Test get_integration_affected_items delegates to chronicle_request.""" - expected = {"affectedItems": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_affected_items(chronicle_client, "test-integration") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:fetchAffectedItems", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_affected_items_error(chronicle_client): - """Test get_integration_affected_items raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to fetch affected items"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_affected_items(chronicle_client, "test-integration") - - assert "Failed to fetch affected items" in str(exc_info.value) - - -# -- get_agent_integrations tests -- - - -def test_get_agent_integrations_success(chronicle_client): - """Test get_agent_integrations passes agentId parameter.""" - expected = {"integrations": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_agent_integrations(chronicle_client, agent_id="agent-123") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations:fetchAgentIntegrations", - params={"agentId": "agent-123"}, - api_version=APIVersion.V1BETA, - ) - - -def test_get_agent_integrations_error(chronicle_client): - """Test get_agent_integrations raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to fetch agent integrations"), - ): - with pytest.raises(APIError) as exc_info: - get_agent_integrations(chronicle_client, agent_id="agent-123") - - assert "Failed to fetch agent integrations" in str(exc_info.value) - - -# -- get_integration_dependencies tests -- - - -def test_get_integration_dependencies_success(chronicle_client): - """Test get_integration_dependencies delegates to chronicle_request.""" - expected = {"dependencies": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_dependencies(chronicle_client, "test-integration") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:fetchDependencies", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_dependencies_error(chronicle_client): - """Test get_integration_dependencies raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to fetch dependencies"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_dependencies(chronicle_client, "test-integration") - - assert "Failed to fetch dependencies" in str(exc_info.value) - - -# -- get_integration_restricted_agents tests -- - - -def test_get_integration_restricted_agents_success(chronicle_client): - """Test get_integration_restricted_agents passes required python version and pushRequest.""" - expected = {"restrictedAgents": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_restricted_agents( - chronicle_client, - integration_name="test-integration", - required_python_version=PythonVersion.PYTHON_3_11, - push_request=True, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:fetchRestrictedAgents", - params={ - "requiredPythonVersion": PythonVersion.PYTHON_3_11.value, - "pushRequest": True, - }, - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_restricted_agents_default_push_request(chronicle_client): - """Test get_integration_restricted_agents default push_request=False is sent.""" - expected = {"restrictedAgents": []} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - get_integration_restricted_agents( - chronicle_client, - integration_name="test-integration", - required_python_version=PythonVersion.PYTHON_3_11, - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:fetchRestrictedAgents", - params={ - "requiredPythonVersion": PythonVersion.PYTHON_3_11.value, - "pushRequest": False, - }, - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_restricted_agents_error(chronicle_client): - """Test get_integration_restricted_agents raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to fetch restricted agents"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_restricted_agents( - chronicle_client, - integration_name="test-integration", - required_python_version=PythonVersion.PYTHON_3_11, - ) - - assert "Failed to fetch restricted agents" in str(exc_info.value) - - -# -- get_integration_diff tests -- - - -def test_get_integration_diff_success(chronicle_client): - """Test get_integration_diff builds endpoint with diff type.""" - expected = {"diff": {}} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_diff( - chronicle_client, - integration_name="test-integration", - diff_type=DiffType.PRODUCTION, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration:fetchProductionDiff", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_diff_error(chronicle_client): - """Test get_integration_diff raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to fetch diff"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_diff(chronicle_client, "test-integration") - - assert "Failed to fetch diff" in str(exc_info.value) - - -# -- transition_integration tests -- - - -def test_transition_integration_success(chronicle_client): - """Test transition_integration posts to pushTo{TargetMode}.""" - expected = {"name": "integrations/test"} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = transition_integration( - chronicle_client, - integration_name="test-integration", - target_mode=TargetMode.PRODUCTION, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration:pushToProduction", - api_version=APIVersion.V1BETA, - ) - - -def test_transition_integration_error(chronicle_client): - """Test transition_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to transition integration"), - ): - with pytest.raises(APIError) as exc_info: - transition_integration( - chronicle_client, - integration_name="test-integration", - target_mode=TargetMode.STAGING, - ) - - assert "Failed to transition integration" in str(exc_info.value) - - -# -- update_integration tests -- - - -def test_update_integration_uses_build_patch_body_and_passes_dependencies_to_remove( - chronicle_client, -): - """Test update_integration uses build_patch_body and adds dependenciesToRemove.""" - body = {"displayName": "New"} - params = {"updateMask": "displayName"} - - with patch( - "secops.chronicle.integration.integrations.build_patch_body", - return_value=(body, params), - ) as mock_build_patch, patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value={"name": "integrations/test"}, - ) as mock_request: - result = update_integration( - chronicle_client, - integration_name="test-integration", - display_name="New", - dependencies_to_remove=["dep1", "dep2"], - update_mask="displayName", - ) - - assert result == {"name": "integrations/test"} - - mock_build_patch.assert_called_once() - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path="integrations/test-integration", - json=body, - params={"updateMask": "displayName", "dependenciesToRemove": "dep1,dep2"}, - api_version=APIVersion.V1BETA, - ) - - -def test_update_integration_when_build_patch_body_returns_no_params(chronicle_client): - """Test update_integration handles params=None from build_patch_body.""" - body = {"description": "New"} - - with patch( - "secops.chronicle.integration.integrations.build_patch_body", - return_value=(body, None), - ), patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value={"name": "integrations/test"}, - ) as mock_request: - update_integration( - chronicle_client, - integration_name="test-integration", - description="New", - dependencies_to_remove=["dep1"], - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path="integrations/test-integration", - json=body, - params={"dependenciesToRemove": "dep1"}, - api_version=APIVersion.V1BETA, - ) - - -def test_update_integration_error(chronicle_client): - """Test update_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.build_patch_body", - return_value=({}, None), - ), patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to update integration"), - ): - with pytest.raises(APIError) as exc_info: - update_integration(chronicle_client, "test-integration") - - assert "Failed to update integration" in str(exc_info.value) - - -# -- update_custom_integration tests -- - - -def test_update_custom_integration_builds_body_and_params(chronicle_client): - """Test update_custom_integration builds nested integration body and updateMask param.""" - expected = {"successful": True, "integration": {"name": "integrations/test"}} - - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_custom_integration( - chronicle_client, - integration_name="test-integration", - display_name="New", - staging=False, - dependencies_to_remove=["dep1"], - update_mask="displayName,staging", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration:updateCustomIntegration", - json={ - "integration": { - "name": "test-integration", - "displayName": "New", - "staging": False, - }, - "dependenciesToRemove": ["dep1"], - }, - params={"updateMask": "displayName,staging"}, - api_version=APIVersion.V1BETA, - ) - - -def test_update_custom_integration_excludes_none_fields(chronicle_client): - """Test update_custom_integration excludes None fields from integration object.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - return_value={"successful": True}, - ) as mock_request: - update_custom_integration( - chronicle_client, - integration_name="test-integration", - display_name=None, - description=None, - image_base64=None, - svg_icon=None, - python_version=None, - parameters=None, - categories=None, - integration_type=None, - staging=None, - dependencies_to_remove=None, - update_mask=None, - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration:updateCustomIntegration", - json={"integration": {"name": "test-integration"}}, - params=None, - api_version=APIVersion.V1BETA, - ) - - -def test_update_custom_integration_error(chronicle_client): - """Test update_custom_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.integrations.chronicle_request", - side_effect=APIError("Failed to update custom integration"), - ): - with pytest.raises(APIError) as exc_info: - update_custom_integration(chronicle_client, "test-integration") - - assert "Failed to update custom integration" in str(exc_info.value) diff --git a/tests/chronicle/integration/test_job_context_properties.py b/tests/chronicle/integration/test_job_context_properties.py deleted file mode 100644 index 5fdce61c..00000000 --- a/tests/chronicle/integration/test_job_context_properties.py +++ /dev/null @@ -1,506 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration job context properties functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.job_context_properties import ( - list_job_context_properties, - get_job_context_property, - delete_job_context_property, - create_job_context_property, - update_job_context_property, - delete_all_job_context_properties, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_job_context_properties tests -- - - -def test_list_job_context_properties_success(chronicle_client): - """Test list_job_context_properties delegates to paginated request.""" - expected = { - "contextProperties": [{"key": "prop1"}, {"key": "prop2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.job_context_properties.format_resource_id", - return_value="My Integration", - ): - result = list_job_context_properties( - chronicle_client, - integration_name="My Integration", - job_id="j1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "jobs/j1/contextProperties" in kwargs["path"] - assert kwargs["items_key"] == "contextProperties" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_job_context_properties_default_args(chronicle_client): - """Test list_job_context_properties with default args.""" - expected = {"contextProperties": []} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - return_value=expected, - ): - result = list_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - assert result == expected - - -def test_list_job_context_properties_with_filters(chronicle_client): - """Test list_job_context_properties with filter and order_by.""" - expected = {"contextProperties": [{"key": "prop1"}]} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - filter_string='key = "prop1"', - order_by="key", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'key = "prop1"', - "orderBy": "key", - } - - -def test_list_job_context_properties_as_list(chronicle_client): - """Test list_job_context_properties returns list when as_list=True.""" - expected = [{"key": "prop1"}, {"key": "prop2"}] - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_job_context_properties_error(chronicle_client): - """Test list_job_context_properties raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - side_effect=APIError("Failed to list context properties"), - ): - with pytest.raises(APIError) as exc_info: - list_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to list context properties" in str(exc_info.value) - - -# -- get_job_context_property tests -- - - -def test_get_job_context_property_success(chronicle_client): - """Test get_job_context_property issues GET request.""" - expected = { - "name": "contextProperties/prop1", - "key": "prop1", - "value": "test-value", - } - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "jobs/j1/contextProperties/prop1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_job_context_property_error(chronicle_client): - """Test get_job_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - side_effect=APIError("Failed to get context property"), - ): - with pytest.raises(APIError) as exc_info: - get_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - ) - assert "Failed to get context property" in str(exc_info.value) - - -# -- delete_job_context_property tests -- - - -def test_delete_job_context_property_success(chronicle_client): - """Test delete_job_context_property issues DELETE request.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "jobs/j1/contextProperties/prop1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_job_context_property_error(chronicle_client): - """Test delete_job_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - side_effect=APIError("Failed to delete context property"), - ): - with pytest.raises(APIError) as exc_info: - delete_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - ) - assert "Failed to delete context property" in str(exc_info.value) - - -# -- create_job_context_property tests -- - - -def test_create_job_context_property_value_only(chronicle_client): - """Test create_job_context_property with value only.""" - expected = {"name": "contextProperties/new", "value": "test-value"} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - value="test-value", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/jobs/j1/contextProperties" - ), - api_version=APIVersion.V1BETA, - json={"value": "test-value"}, - ) - - -def test_create_job_context_property_with_key(chronicle_client): - """Test create_job_context_property with key specified.""" - expected = {"name": "contextProperties/mykey", "value": "test-value"} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - value="test-value", - key="mykey", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["value"] == "test-value" - assert kwargs["json"]["key"] == "mykey" - - -def test_create_job_context_property_error(chronicle_client): - """Test create_job_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - side_effect=APIError("Failed to create context property"), - ): - with pytest.raises(APIError) as exc_info: - create_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - value="test-value", - ) - assert "Failed to create context property" in str(exc_info.value) - - -# -- update_job_context_property tests -- - - -def test_update_job_context_property_success(chronicle_client): - """Test update_job_context_property issues PATCH request.""" - expected = {"name": "contextProperties/prop1", "value": "updated-value"} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_context_properties.build_patch_body", - return_value=( - {"value": "updated-value"}, - {"updateMask": "value"}, - ), - ): - result = update_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - value="updated-value", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path=( - "integrations/test-integration/jobs/j1/contextProperties/prop1" - ), - api_version=APIVersion.V1BETA, - json={"value": "updated-value"}, - params={"updateMask": "value"}, - ) - - -def test_update_job_context_property_with_update_mask(chronicle_client): - """Test update_job_context_property with explicit update_mask.""" - expected = {"name": "contextProperties/prop1", "value": "updated-value"} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_context_properties.build_patch_body", - return_value=( - {"value": "updated-value"}, - {"updateMask": "value"}, - ), - ): - result = update_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - value="updated-value", - update_mask="value", - ) - - assert result == expected - - -def test_update_job_context_property_error(chronicle_client): - """Test update_job_context_property raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - side_effect=APIError("Failed to update context property"), - ), patch( - "secops.chronicle.integration.job_context_properties.build_patch_body", - return_value=({"value": "updated"}, {"updateMask": "value"}), - ): - with pytest.raises(APIError) as exc_info: - update_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - value="updated", - ) - assert "Failed to update context property" in str(exc_info.value) - - -# -- delete_all_job_context_properties tests -- - - -def test_delete_all_job_context_properties_success(chronicle_client): - """Test delete_all_job_context_properties issues POST request.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_all_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/" - "jobs/j1/contextProperties:clearAll" - ), - api_version=APIVersion.V1BETA, - json={}, - ) - - -def test_delete_all_job_context_properties_with_context_id(chronicle_client): - """Test delete_all_job_context_properties with context_id specified.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=None, - ) as mock_request: - delete_all_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_id="mycontext", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "contextProperties:clearAll" in kwargs["endpoint_path"] - assert kwargs["json"]["contextId"] == "mycontext" - - -def test_delete_all_job_context_properties_error(chronicle_client): - """Test delete_all_job_context_properties raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - side_effect=APIError("Failed to delete all context properties"), - ): - with pytest.raises(APIError) as exc_info: - delete_all_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to delete all context properties" in str( - exc_info.value - ) - - -# -- API version tests -- - - -def test_list_job_context_properties_custom_api_version(chronicle_client): - """Test list_job_context_properties with custom API version.""" - expected = {"contextProperties": []} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_context_properties( - chronicle_client, - integration_name="test-integration", - job_id="j1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_job_context_property_custom_api_version(chronicle_client): - """Test get_job_context_property with custom API version.""" - expected = {"name": "contextProperties/prop1"} - - with patch( - "secops.chronicle.integration.job_context_properties.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_job_context_property( - chronicle_client, - integration_name="test-integration", - job_id="j1", - context_property_id="prop1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_job_instance_logs.py b/tests/chronicle/integration/test_job_instance_logs.py deleted file mode 100644 index ad456e79..00000000 --- a/tests/chronicle/integration/test_job_instance_logs.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration job instance logs functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.job_instance_logs import ( - list_job_instance_logs, - get_job_instance_log, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_job_instance_logs tests -- - - -def test_list_job_instance_logs_success(chronicle_client): - """Test list_job_instance_logs delegates to paginated request.""" - expected = { - "logs": [{"name": "log1"}, {"name": "log2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.job_instance_logs.format_resource_id", - return_value="My Integration", - ): - result = list_job_instance_logs( - chronicle_client, - integration_name="My Integration", - job_id="j1", - job_instance_id="ji1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "jobs/j1/jobInstances/ji1/logs" in kwargs["path"] - assert kwargs["items_key"] == "logs" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_job_instance_logs_default_args(chronicle_client): - """Test list_job_instance_logs with default args.""" - expected = {"logs": []} - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - return_value=expected, - ): - result = list_job_instance_logs( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - - assert result == expected - - -def test_list_job_instance_logs_with_filters(chronicle_client): - """Test list_job_instance_logs with filter and order_by.""" - expected = {"logs": [{"name": "log1"}]} - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_instance_logs( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - filter_string="status = SUCCESS", - order_by="startTime desc", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": "status = SUCCESS", - "orderBy": "startTime desc", - } - - -def test_list_job_instance_logs_as_list(chronicle_client): - """Test list_job_instance_logs returns list when as_list=True.""" - expected = [{"name": "log1"}, {"name": "log2"}] - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_instance_logs( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_job_instance_logs_error(chronicle_client): - """Test list_job_instance_logs raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - side_effect=APIError("Failed to list job instance logs"), - ): - with pytest.raises(APIError) as exc_info: - list_job_instance_logs( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - assert "Failed to list job instance logs" in str(exc_info.value) - - -# -- get_job_instance_log tests -- - - -def test_get_job_instance_log_success(chronicle_client): - """Test get_job_instance_log issues GET request.""" - expected = { - "name": "logs/log1", - "status": "SUCCESS", - "startTime": "2026-03-08T10:00:00Z", - "endTime": "2026-03-08T10:05:00Z", - } - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_job_instance_log( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - log_id="log1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "jobs/j1/jobInstances/ji1/logs/log1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_job_instance_log_error(chronicle_client): - """Test get_job_instance_log raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_request", - side_effect=APIError("Failed to get job instance log"), - ): - with pytest.raises(APIError) as exc_info: - get_job_instance_log( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - log_id="log1", - ) - assert "Failed to get job instance log" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_job_instance_logs_custom_api_version(chronicle_client): - """Test list_job_instance_logs with custom API version.""" - expected = {"logs": []} - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_job_instance_logs( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_job_instance_log_custom_api_version(chronicle_client): - """Test get_job_instance_log with custom API version.""" - expected = {"name": "logs/log1"} - - with patch( - "secops.chronicle.integration.job_instance_logs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_job_instance_log( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - log_id="log1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_job_instances.py b/tests/chronicle/integration/test_job_instances.py deleted file mode 100644 index 3e8ca386..00000000 --- a/tests/chronicle/integration/test_job_instances.py +++ /dev/null @@ -1,733 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration job instances functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import ( - APIVersion, - IntegrationJobInstanceParameter, - AdvancedConfig, - ScheduleType, - DailyScheduleDetails, - Date, - TimeOfDay, -) -from secops.chronicle.integration.job_instances import ( - list_integration_job_instances, - get_integration_job_instance, - delete_integration_job_instance, - create_integration_job_instance, - update_integration_job_instance, - run_integration_job_instance_on_demand, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_job_instances tests -- - - -def test_list_integration_job_instances_success(chronicle_client): - """Test list_integration_job_instances delegates to chronicle_paginated_request.""" - expected = { - "jobInstances": [{"name": "ji1"}, {"name": "ji2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.job_instances.format_resource_id", - return_value="My Integration", - ): - result = list_integration_job_instances( - chronicle_client, - integration_name="My Integration", - job_id="j1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "jobs/j1/jobInstances" in kwargs["path"] - assert kwargs["items_key"] == "jobInstances" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_integration_job_instances_default_args(chronicle_client): - """Test list_integration_job_instances with default args.""" - expected = {"jobInstances": []} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_instances( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - assert result == expected - - -def test_list_integration_job_instances_with_filters(chronicle_client): - """Test list_integration_job_instances with filter and order_by.""" - expected = {"jobInstances": [{"name": "ji1"}]} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_instances( - chronicle_client, - integration_name="test-integration", - job_id="j1", - filter_string="enabled = true", - order_by="displayName", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": "enabled = true", - "orderBy": "displayName", - } - - -def test_list_integration_job_instances_as_list(chronicle_client): - """Test list_integration_job_instances returns list when as_list=True.""" - expected = [{"name": "ji1"}, {"name": "ji2"}] - - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_instances( - chronicle_client, - integration_name="test-integration", - job_id="j1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_job_instances_error(chronicle_client): - """Test list_integration_job_instances raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - side_effect=APIError("Failed to list job instances"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_job_instances( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to list job instances" in str(exc_info.value) - - -# -- get_integration_job_instance tests -- - - -def test_get_integration_job_instance_success(chronicle_client): - """Test get_integration_job_instance issues GET request.""" - expected = { - "name": "jobInstances/ji1", - "displayName": "My Job Instance", - "intervalSeconds": 300, - "enabled": True, - } - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "GET" - assert "jobs/j1/jobInstances/ji1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_get_integration_job_instance_error(chronicle_client): - """Test get_integration_job_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - side_effect=APIError("Failed to get job instance"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - assert "Failed to get job instance" in str(exc_info.value) - - -# -- delete_integration_job_instance tests -- - - -def test_delete_integration_job_instance_success(chronicle_client): - """Test delete_integration_job_instance issues DELETE request.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "jobs/j1/jobInstances/ji1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_integration_job_instance_error(chronicle_client): - """Test delete_integration_job_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - side_effect=APIError("Failed to delete job instance"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - assert "Failed to delete job instance" in str(exc_info.value) - - -# -- create_integration_job_instance tests -- - - -def test_create_integration_job_instance_required_fields_only(chronicle_client): - """Test create_integration_job_instance sends only required fields.""" - expected = {"name": "jobInstances/new", "displayName": "My Job Instance"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="My Job Instance", - interval_seconds=300, - enabled=True, - advanced=False, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/jobs/j1/jobInstances", - api_version=APIVersion.V1BETA, - json={ - "displayName": "My Job Instance", - "intervalSeconds": 300, - "enabled": True, - "advanced": False, - }, - ) - - -def test_create_integration_job_instance_with_optional_fields(chronicle_client): - """Test create_integration_job_instance includes optional fields when provided.""" - expected = {"name": "jobInstances/new"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="My Job Instance", - interval_seconds=300, - enabled=True, - advanced=False, - description="Test job instance", - parameters=[{"id": 1, "value": "test"}], - agent="agent-123", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["description"] == "Test job instance" - assert kwargs["json"]["parameters"] == [{"id": 1, "value": "test"}] - assert kwargs["json"]["agent"] == "agent-123" - - -def test_create_integration_job_instance_with_dataclass_params(chronicle_client): - """Test create_integration_job_instance converts dataclass parameters.""" - expected = {"name": "jobInstances/new"} - - param = IntegrationJobInstanceParameter(value="test-value") - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="My Job Instance", - interval_seconds=300, - enabled=True, - advanced=False, - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["value"] == "test-value" - - -def test_create_integration_job_instance_with_advanced_config(chronicle_client): - """Test create_integration_job_instance with AdvancedConfig dataclass.""" - expected = {"name": "jobInstances/new"} - - advanced_config = AdvancedConfig( - time_zone="America/New_York", - schedule_type=ScheduleType.DAILY, - daily_schedule=DailyScheduleDetails( - start_date=Date(year=2026, month=3, day=8), - time=TimeOfDay(hours=2, minutes=0), - interval=1 - ) - ) - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="My Job Instance", - interval_seconds=300, - enabled=True, - advanced=True, - advanced_config=advanced_config, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - config_sent = kwargs["json"]["advancedConfig"] - assert config_sent["timeZone"] == "America/New_York" - assert config_sent["scheduleType"] == "DAILY" - assert "dailySchedule" in config_sent - - -def test_create_integration_job_instance_error(chronicle_client): - """Test create_integration_job_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - side_effect=APIError("Failed to create job instance"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="My Job Instance", - interval_seconds=300, - enabled=True, - advanced=False, - ) - assert "Failed to create job instance" in str(exc_info.value) - - -# -- update_integration_job_instance tests -- - - -def test_update_integration_job_instance_single_field(chronicle_client): - """Test update_integration_job_instance updates a single field.""" - expected = {"name": "jobInstances/ji1", "displayName": "Updated Instance"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=( - {"displayName": "Updated Instance"}, - {"updateMask": "displayName"}, - ), - ) as mock_build_patch: - result = update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - display_name="Updated Instance", - ) - - assert result == expected - - mock_build_patch.assert_called_once() - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path=( - "integrations/test-integration/jobs/j1/jobInstances/ji1" - ), - api_version=APIVersion.V1BETA, - json={"displayName": "Updated Instance"}, - params={"updateMask": "displayName"}, - ) - - -def test_update_integration_job_instance_multiple_fields(chronicle_client): - """Test update_integration_job_instance updates multiple fields.""" - expected = {"name": "jobInstances/ji1"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=( - { - "displayName": "Updated", - "intervalSeconds": 600, - "enabled": False, - }, - {"updateMask": "displayName,intervalSeconds,enabled"}, - ), - ): - result = update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - display_name="Updated", - interval_seconds=600, - enabled=False, - ) - - assert result == expected - - -def test_update_integration_job_instance_with_dataclass_params(chronicle_client): - """Test update_integration_job_instance converts dataclass parameters.""" - expected = {"name": "jobInstances/ji1"} - - param = IntegrationJobInstanceParameter(value="updated-value") - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=( - {"parameters": [{"value": "updated-value"}]}, - {"updateMask": "parameters"}, - ), - ): - result = update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - parameters=[param], - ) - - assert result == expected - - -def test_update_integration_job_instance_with_advanced_config(chronicle_client): - """Test update_integration_job_instance with AdvancedConfig dataclass.""" - expected = {"name": "jobInstances/ji1"} - - advanced_config = AdvancedConfig( - time_zone="UTC", - schedule_type=ScheduleType.DAILY, - daily_schedule=DailyScheduleDetails( - start_date=Date(year=2026, month=3, day=8), - time=TimeOfDay(hours=0, minutes=0), - interval=1 - ) - ) - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=( - { - "advancedConfig": { - "timeZone": "UTC", - "scheduleType": "DAILY", - "dailySchedule": { - "startDate": {"year": 2026, "month": 3, "day": 8}, - "time": {"hours": 0, "minutes": 0}, - "interval": 1 - } - } - }, - {"updateMask": "advancedConfig"}, - ), - ): - result = update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - advanced_config=advanced_config, - ) - - assert result == expected - - -def test_update_integration_job_instance_with_update_mask(chronicle_client): - """Test update_integration_job_instance respects explicit update_mask.""" - expected = {"name": "jobInstances/ji1"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=( - {"displayName": "Updated"}, - {"updateMask": "displayName"}, - ), - ): - result = update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - display_name="Updated", - update_mask="displayName", - ) - - assert result == expected - - -def test_update_integration_job_instance_error(chronicle_client): - """Test update_integration_job_instance raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - side_effect=APIError("Failed to update job instance"), - ), patch( - "secops.chronicle.integration.job_instances.build_patch_body", - return_value=({"enabled": False}, {"updateMask": "enabled"}), - ): - with pytest.raises(APIError) as exc_info: - update_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - enabled=False, - ) - assert "Failed to update job instance" in str(exc_info.value) - - -# -- run_integration_job_instance_on_demand tests -- - - -def test_run_integration_job_instance_on_demand_success(chronicle_client): - """Test run_integration_job_instance_on_demand issues POST request.""" - expected = {"success": True} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = run_integration_job_instance_on_demand( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/jobs/j1/jobInstances/ji1:runOnDemand" - ), - api_version=APIVersion.V1BETA, - json={}, - ) - - -def test_run_integration_job_instance_on_demand_with_params(chronicle_client): - """Test run_integration_job_instance_on_demand with parameters.""" - expected = {"success": True} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = run_integration_job_instance_on_demand( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - parameters=[{"id": 1, "value": "override"}], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["parameters"] == [{"id": 1, "value": "override"}] - - -def test_run_integration_job_instance_on_demand_with_dataclass(chronicle_client): - """Test run_integration_job_instance_on_demand converts dataclass parameters.""" - expected = {"success": True} - - param = IntegrationJobInstanceParameter(value="test") - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = run_integration_job_instance_on_demand( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["value"] == "test" - - -def test_run_integration_job_instance_on_demand_error(chronicle_client): - """Test run_integration_job_instance_on_demand raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - side_effect=APIError("Failed to run job instance on demand"), - ): - with pytest.raises(APIError) as exc_info: - run_integration_job_instance_on_demand( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - ) - assert "Failed to run job instance on demand" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_integration_job_instances_custom_api_version(chronicle_client): - """Test list_integration_job_instances with custom API version.""" - expected = {"jobInstances": []} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_instances( - chronicle_client, - integration_name="test-integration", - job_id="j1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_get_integration_job_instance_custom_api_version(chronicle_client): - """Test get_integration_job_instance with custom API version.""" - expected = {"name": "jobInstances/ji1"} - - with patch( - "secops.chronicle.integration.job_instances.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_job_instance( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job_instance_id="ji1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_job_revisions.py b/tests/chronicle/integration/test_job_revisions.py deleted file mode 100644 index 3a81682c..00000000 --- a/tests/chronicle/integration/test_job_revisions.py +++ /dev/null @@ -1,378 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration job revisions functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.job_revisions import ( - list_integration_job_revisions, - delete_integration_job_revision, - create_integration_job_revision, - rollback_integration_job_revision, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_job_revisions tests -- - - -def test_list_integration_job_revisions_success(chronicle_client): - """Test list_integration_job_revisions delegates to chronicle_paginated_request.""" - expected = { - "revisions": [{"name": "r1"}, {"name": "r2"}], - "nextPageToken": "t", - } - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.job_revisions.format_resource_id", - return_value="My Integration", - ): - result = list_integration_job_revisions( - chronicle_client, - integration_name="My Integration", - job_id="j1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert "jobs/j1/revisions" in kwargs["path"] - assert kwargs["items_key"] == "revisions" - assert kwargs["page_size"] == 10 - assert kwargs["page_token"] == "next-token" - - -def test_list_integration_job_revisions_default_args(chronicle_client): - """Test list_integration_job_revisions with default args.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_revisions( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - assert result == expected - - -def test_list_integration_job_revisions_with_filters(chronicle_client): - """Test list_integration_job_revisions with filter and order_by.""" - expected = {"revisions": [{"name": "r1"}]} - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_revisions( - chronicle_client, - integration_name="test-integration", - job_id="j1", - filter_string='version = "1.0"', - order_by="createTime", - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": 'version = "1.0"', - "orderBy": "createTime", - } - - -def test_list_integration_job_revisions_as_list(chronicle_client): - """Test list_integration_job_revisions returns list when as_list=True.""" - expected = [{"name": "r1"}, {"name": "r2"}] - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_revisions( - chronicle_client, - integration_name="test-integration", - job_id="j1", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_job_revisions_error(chronicle_client): - """Test list_integration_job_revisions raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - side_effect=APIError("Failed to list job revisions"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_job_revisions( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to list job revisions" in str(exc_info.value) - - -# -- delete_integration_job_revision tests -- - - -def test_delete_integration_job_revision_success(chronicle_client): - """Test delete_integration_job_revision issues DELETE request.""" - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - revision_id="r1", - ) - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "DELETE" - assert "jobs/j1/revisions/r1" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_delete_integration_job_revision_error(chronicle_client): - """Test delete_integration_job_revision raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - side_effect=APIError("Failed to delete job revision"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - revision_id="r1", - ) - assert "Failed to delete job revision" in str(exc_info.value) - - -# -- create_integration_job_revision tests -- - - -def test_create_integration_job_revision_required_fields_only( - chronicle_client, -): - """Test create_integration_job_revision with required fields only.""" - expected = {"name": "revisions/new", "job": {"displayName": "My Job"}} - job_dict = { - "displayName": "My Job", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job=job_dict, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/jobs/j1/revisions" - ), - api_version=APIVersion.V1BETA, - json={"job": job_dict}, - ) - - -def test_create_integration_job_revision_with_comment(chronicle_client): - """Test create_integration_job_revision includes comment when provided.""" - expected = {"name": "revisions/new"} - job_dict = { - "displayName": "My Job", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job=job_dict, - comment="Backup before major update", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["comment"] == "Backup before major update" - assert kwargs["json"]["job"] == job_dict - - -def test_create_integration_job_revision_error(chronicle_client): - """Test create_integration_job_revision raises APIError on failure.""" - job_dict = { - "displayName": "My Job", - "script": "print('hello')", - "version": 1, - "enabled": True, - "custom": True, - } - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - side_effect=APIError("Failed to create job revision"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - job=job_dict, - ) - assert "Failed to create job revision" in str(exc_info.value) - - -# -- rollback_integration_job_revision tests -- - - -def test_rollback_integration_job_revision_success(chronicle_client): - """Test rollback_integration_job_revision issues POST request.""" - expected = { - "name": "revisions/r1", - "job": { - "displayName": "My Job", - "script": "print('hello')", - }, - } - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = rollback_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - revision_id="r1", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["method"] == "POST" - assert "jobs/j1/revisions/r1:rollback" in kwargs["endpoint_path"] - assert kwargs["api_version"] == APIVersion.V1BETA - - -def test_rollback_integration_job_revision_error(chronicle_client): - """Test rollback_integration_job_revision raises APIError on failure.""" - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - side_effect=APIError("Failed to rollback job revision"), - ): - with pytest.raises(APIError) as exc_info: - rollback_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - revision_id="r1", - ) - assert "Failed to rollback job revision" in str(exc_info.value) - - -# -- API version tests -- - - -def test_list_integration_job_revisions_custom_api_version(chronicle_client): - """Test list_integration_job_revisions with custom API version.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.job_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_job_revisions( - chronicle_client, - integration_name="test-integration", - job_id="j1", - api_version=APIVersion.V1ALPHA, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - - -def test_delete_integration_job_revision_custom_api_version(chronicle_client): - """Test delete_integration_job_revision with custom API version.""" - with patch( - "secops.chronicle.integration.job_revisions.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_job_revision( - chronicle_client, - integration_name="test-integration", - job_id="j1", - revision_id="r1", - api_version=APIVersion.V1ALPHA, - ) - - _, kwargs = mock_request.call_args - assert kwargs["api_version"] == APIVersion.V1ALPHA - diff --git a/tests/chronicle/integration/test_jobs.py b/tests/chronicle/integration/test_jobs.py deleted file mode 100644 index a318a890..00000000 --- a/tests/chronicle/integration/test_jobs.py +++ /dev/null @@ -1,594 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration jobs functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion, JobParameter, ParamType -from secops.chronicle.integration.jobs import ( - list_integration_jobs, - get_integration_job, - delete_integration_job, - create_integration_job, - update_integration_job, - execute_integration_job_test, - get_integration_job_template, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -# -- list_integration_jobs tests -- - - -def test_list_integration_jobs_success(chronicle_client): - """Test list_integration_jobs delegates to chronicle_paginated_request.""" - expected = {"jobs": [{"name": "j1"}, {"name": "j2"}], "nextPageToken": "t"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.jobs.format_resource_id", - return_value="My Integration", - ): - result = list_integration_jobs( - chronicle_client, - integration_name="My Integration", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="integrations/My Integration/jobs", - items_key="jobs", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_jobs_default_args(chronicle_client): - """Test list_integration_jobs with default args.""" - expected = {"jobs": []} - - with patch( - "secops.chronicle.integration.jobs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_jobs( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - -def test_list_integration_jobs_with_filters(chronicle_client): - """Test list_integration_jobs with filter and order_by.""" - expected = {"jobs": [{"name": "j1"}]} - - with patch( - "secops.chronicle.integration.jobs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_jobs( - chronicle_client, - integration_name="test-integration", - filter_string="custom=true", - order_by="displayName", - exclude_staging=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["extra_params"] == { - "filter": "custom=true", - "orderBy": "displayName", - "excludeStaging": True, - } - - -def test_list_integration_jobs_as_list(chronicle_client): - """Test list_integration_jobs returns list when as_list=True.""" - expected = [{"name": "j1"}, {"name": "j2"}] - - with patch( - "secops.chronicle.integration.jobs.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_jobs( - chronicle_client, - integration_name="test-integration", - as_list=True, - ) - - assert result == expected - - _, kwargs = mock_paginated.call_args - assert kwargs["as_list"] is True - - -def test_list_integration_jobs_error(chronicle_client): - """Test list_integration_jobs raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_paginated_request", - side_effect=APIError("Failed to list integration jobs"), - ): - with pytest.raises(APIError) as exc_info: - list_integration_jobs( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to list integration jobs" in str(exc_info.value) - - -# -- get_integration_job tests -- - - -def test_get_integration_job_success(chronicle_client): - """Test get_integration_job issues GET request.""" - expected = { - "name": "jobs/j1", - "displayName": "My Job", - "script": "print('hello')", - } - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/jobs/j1", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_job_error(chronicle_client): - """Test get_integration_job raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to get integration job"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to get integration job" in str(exc_info.value) - - -# -- delete_integration_job tests -- - - -def test_delete_integration_job_success(chronicle_client): - """Test delete_integration_job issues DELETE request.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=None, - ) as mock_request: - delete_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path="integrations/test-integration/jobs/j1", - api_version=APIVersion.V1BETA, - ) - - -def test_delete_integration_job_error(chronicle_client): - """Test delete_integration_job raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to delete integration job"), - ): - with pytest.raises(APIError) as exc_info: - delete_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - ) - assert "Failed to delete integration job" in str(exc_info.value) - - -# -- create_integration_job tests -- - - -def test_create_integration_job_required_fields_only(chronicle_client): - """Test create_integration_job sends only required fields when optionals omitted.""" - expected = {"name": "jobs/new", "displayName": "My Job"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job( - chronicle_client, - integration_name="test-integration", - display_name="My Job", - script="print('hi')", - version=1, - enabled=True, - custom=True, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/jobs", - api_version=APIVersion.V1BETA, - json={ - "displayName": "My Job", - "script": "print('hi')", - "version": 1, - "enabled": True, - "custom": True, - }, - ) - - -def test_create_integration_job_with_optional_fields(chronicle_client): - """Test create_integration_job includes optional fields when provided.""" - expected = {"name": "jobs/new"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job( - chronicle_client, - integration_name="test-integration", - display_name="My Job", - script="print('hi')", - version=1, - enabled=True, - custom=True, - description="Test job", - parameters=[{"id": 1, "displayName": "p1", "type": "STRING"}], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["description"] == "Test job" - assert kwargs["json"]["parameters"] == [ - {"id": 1, "displayName": "p1", "type": "STRING"} - ] - - -def test_create_integration_job_with_dataclass_parameters(chronicle_client): - """Test create_integration_job converts JobParameter dataclasses.""" - expected = {"name": "jobs/new"} - - param = JobParameter( - id=1, - display_name="API Key", - description="API key for authentication", - type=ParamType.STRING, - mandatory=True, - ) - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_job( - chronicle_client, - integration_name="test-integration", - display_name="My Job", - script="print('hi')", - version=1, - enabled=True, - custom=True, - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["id"] == 1 - assert params_sent[0]["displayName"] == "API Key" - assert params_sent[0]["type"] == "STRING" - - -def test_create_integration_job_error(chronicle_client): - """Test create_integration_job raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to create integration job"), - ): - with pytest.raises(APIError) as exc_info: - create_integration_job( - chronicle_client, - integration_name="test-integration", - display_name="My Job", - script="print('hi')", - version=1, - enabled=True, - custom=True, - ) - assert "Failed to create integration job" in str(exc_info.value) - - -# -- update_integration_job tests -- - - -def test_update_integration_job_with_explicit_update_mask(chronicle_client): - """Test update_integration_job passes through explicit update_mask.""" - expected = {"name": "jobs/j1", "displayName": "New Name"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="New Name", - update_mask="displayName", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="PATCH", - endpoint_path="integrations/test-integration/jobs/j1", - api_version=APIVersion.V1BETA, - json={"displayName": "New Name"}, - params={"updateMask": "displayName"}, - ) - - -def test_update_integration_job_auto_update_mask(chronicle_client): - """Test update_integration_job auto-generates updateMask based on fields.""" - expected = {"name": "jobs/j1"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - enabled=False, - version=2, - ) - - assert result == expected - - assert mock_request.call_count == 1 - _, kwargs = mock_request.call_args - - assert kwargs["method"] == "PATCH" - assert kwargs["endpoint_path"] == "integrations/test-integration/jobs/j1" - assert kwargs["api_version"] == APIVersion.V1BETA - - assert kwargs["json"] == {"enabled": False, "version": 2} - - update_mask = kwargs["params"]["updateMask"] - assert set(update_mask.split(",")) == {"enabled", "version"} - - -def test_update_integration_job_with_parameters(chronicle_client): - """Test update_integration_job with parameters field.""" - expected = {"name": "jobs/j1"} - - param = JobParameter( - id=2, - display_name="Auth Token", - description="Authentication token", - type=ParamType.PASSWORD, - mandatory=True, - ) - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = update_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - parameters=[param], - ) - - assert result == expected - - _, kwargs = mock_request.call_args - params_sent = kwargs["json"]["parameters"] - assert len(params_sent) == 1 - assert params_sent[0]["id"] == 2 - assert params_sent[0]["displayName"] == "Auth Token" - - -def test_update_integration_job_error(chronicle_client): - """Test update_integration_job raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to update integration job"), - ): - with pytest.raises(APIError) as exc_info: - update_integration_job( - chronicle_client, - integration_name="test-integration", - job_id="j1", - display_name="New Name", - ) - assert "Failed to update integration job" in str(exc_info.value) - - -# -- execute_integration_job_test tests -- - - -def test_execute_integration_job_test_success(chronicle_client): - """Test execute_integration_job_test sends POST request with job.""" - expected = { - "output": "Success", - "debugOutput": "Debug info", - "resultObjectJson": {"status": "ok"}, - } - - job = { - "displayName": "Test Job", - "script": "print('test')", - "enabled": True, - } - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = execute_integration_job_test( - chronicle_client, - integration_name="test-integration", - job=job, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/jobs:executeTest", - api_version=APIVersion.V1BETA, - json={"job": job}, - ) - - -def test_execute_integration_job_test_with_agent_identifier(chronicle_client): - """Test execute_integration_job_test includes agent_identifier when provided.""" - expected = {"output": "Success"} - - job = {"displayName": "Test", "script": "print('test')"} - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = execute_integration_job_test( - chronicle_client, - integration_name="test-integration", - job=job, - agent_identifier="agent-123", - ) - - assert result == expected - - _, kwargs = mock_request.call_args - assert kwargs["json"]["agentIdentifier"] == "agent-123" - - -def test_execute_integration_job_test_error(chronicle_client): - """Test execute_integration_job_test raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to execute job test"), - ): - with pytest.raises(APIError) as exc_info: - execute_integration_job_test( - chronicle_client, - integration_name="test-integration", - job={"displayName": "Test"}, - ) - assert "Failed to execute job test" in str(exc_info.value) - - -# -- get_integration_job_template tests -- - - -def test_get_integration_job_template_success(chronicle_client): - """Test get_integration_job_template issues GET request.""" - expected = { - "script": "# Template script\nprint('hello')", - "displayName": "Template Job", - } - - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_job_template( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/jobs:fetchTemplate", - api_version=APIVersion.V1BETA, - ) - - -def test_get_integration_job_template_error(chronicle_client): - """Test get_integration_job_template raises APIError on failure.""" - with patch( - "secops.chronicle.integration.jobs.chronicle_request", - side_effect=APIError("Failed to get job template"), - ): - with pytest.raises(APIError) as exc_info: - get_integration_job_template( - chronicle_client, - integration_name="test-integration", - ) - assert "Failed to get job template" in str(exc_info.value) - diff --git a/tests/chronicle/integration/test_logical_operator_revisions.py b/tests/chronicle/integration/test_logical_operator_revisions.py deleted file mode 100644 index 29e912e6..00000000 --- a/tests/chronicle/integration/test_logical_operator_revisions.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration logical operator revisions functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.logical_operator_revisions import ( - list_integration_logical_operator_revisions, - delete_integration_logical_operator_revision, - create_integration_logical_operator_revision, - rollback_integration_logical_operator_revision, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1ALPHA, - ) - - -# -- list_integration_logical_operator_revisions tests -- - - -def test_list_integration_logical_operator_revisions_success(chronicle_client): - """Test list_integration_logical_operator_revisions delegates to paginated request.""" - expected = { - "revisions": [{"name": "r1"}, {"name": "r2"}], - "nextPageToken": "token", - } - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.logical_operator_revisions.format_resource_id", - return_value="My Integration", - ): - result = list_integration_logical_operator_revisions( - chronicle_client, - integration_name="My Integration", - logical_operator_id="lo1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/My Integration/logicalOperators/lo1/revisions", - items_key="revisions", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_logical_operator_revisions_default_args(chronicle_client): - """Test list_integration_logical_operator_revisions with default args.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_logical_operator_revisions( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/logicalOperators/lo1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={}, - as_list=False, - ) - - -def test_list_integration_logical_operator_revisions_with_filter_order( - chronicle_client, -): - """Test list passes filter/orderBy in extra_params.""" - expected = {"revisions": [{"name": "r1"}]} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_logical_operator_revisions( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - filter_string='version = "1.0"', - order_by="createTime desc", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/logicalOperators/lo1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={ - "filter": 'version = "1.0"', - "orderBy": "createTime desc", - }, - as_list=False, - ) - - -def test_list_integration_logical_operator_revisions_as_list(chronicle_client): - """Test list_integration_logical_operator_revisions with as_list=True.""" - expected = [{"name": "r1"}, {"name": "r2"}] - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_logical_operator_revisions( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - as_list=True, - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/logicalOperators/lo1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={}, - as_list=True, - ) - - -# -- delete_integration_logical_operator_revision tests -- - - -def test_delete_integration_logical_operator_revision_success(chronicle_client): - """Test delete_integration_logical_operator_revision delegates to chronicle_request.""" - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - return_value=None, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operator_revisions.format_resource_id", - return_value="test-integration", - ): - delete_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - revision_id="rev1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path=( - "integrations/test-integration/logicalOperators/lo1/revisions/rev1" - ), - api_version=APIVersion.V1ALPHA, - ) - - -# -- create_integration_logical_operator_revision tests -- - - -def test_create_integration_logical_operator_revision_minimal(chronicle_client): - """Test create_integration_logical_operator_revision with minimal fields.""" - logical_operator = { - "displayName": "Test Operator", - "script": "def evaluate(a, b): return a == b", - } - expected = {"name": "rev1", "comment": ""} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operator_revisions.format_resource_id", - return_value="test-integration", - ): - result = create_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - logical_operator=logical_operator, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/logicalOperators/lo1/revisions" - ), - api_version=APIVersion.V1ALPHA, - json={"logicalOperator": logical_operator}, - ) - - -def test_create_integration_logical_operator_revision_with_comment(chronicle_client): - """Test create_integration_logical_operator_revision with comment.""" - logical_operator = { - "displayName": "Test Operator", - "script": "def evaluate(a, b): return a == b", - } - expected = {"name": "rev1", "comment": "Version 2.0"} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - logical_operator=logical_operator, - comment="Version 2.0", - ) - - assert result == expected - - call_kwargs = mock_request.call_args[1] - assert call_kwargs["json"]["logicalOperator"] == logical_operator - assert call_kwargs["json"]["comment"] == "Version 2.0" - - -# -- rollback_integration_logical_operator_revision tests -- - - -def test_rollback_integration_logical_operator_revision_success(chronicle_client): - """Test rollback_integration_logical_operator_revision delegates to chronicle_request.""" - expected = {"name": "rev1", "comment": "Rolled back"} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operator_revisions.format_resource_id", - return_value="test-integration", - ): - result = rollback_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - revision_id="rev1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/logicalOperators/lo1/" - "revisions/rev1:rollback" - ), - api_version=APIVersion.V1ALPHA, - ) - - -# -- Error handling tests -- - - -def test_list_integration_logical_operator_revisions_api_error(chronicle_client): - """Test list_integration_logical_operator_revisions handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_paginated_request", - side_effect=APIError("API Error"), - ): - with pytest.raises(APIError, match="API Error"): - list_integration_logical_operator_revisions( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - ) - - -def test_delete_integration_logical_operator_revision_api_error(chronicle_client): - """Test delete_integration_logical_operator_revision handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - side_effect=APIError("Delete failed"), - ): - with pytest.raises(APIError, match="Delete failed"): - delete_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - revision_id="rev1", - ) - - -def test_create_integration_logical_operator_revision_api_error(chronicle_client): - """Test create_integration_logical_operator_revision handles API errors.""" - logical_operator = {"displayName": "Test"} - - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - side_effect=APIError("Creation failed"), - ): - with pytest.raises(APIError, match="Creation failed"): - create_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - logical_operator=logical_operator, - ) - - -def test_rollback_integration_logical_operator_revision_api_error(chronicle_client): - """Test rollback_integration_logical_operator_revision handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operator_revisions.chronicle_request", - side_effect=APIError("Rollback failed"), - ): - with pytest.raises(APIError, match="Rollback failed"): - rollback_integration_logical_operator_revision( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - revision_id="rev1", - ) - diff --git a/tests/chronicle/integration/test_logical_operators.py b/tests/chronicle/integration/test_logical_operators.py deleted file mode 100644 index df495750..00000000 --- a/tests/chronicle/integration/test_logical_operators.py +++ /dev/null @@ -1,547 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration logical operators functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.logical_operators import ( - list_integration_logical_operators, - get_integration_logical_operator, - delete_integration_logical_operator, - create_integration_logical_operator, - update_integration_logical_operator, - execute_integration_logical_operator_test, - get_integration_logical_operator_template, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1ALPHA, - ) - - -# -- list_integration_logical_operators tests -- - - -def test_list_integration_logical_operators_success(chronicle_client): - """Test list_integration_logical_operators delegates to paginated request.""" - expected = { - "logicalOperators": [{"name": "lo1"}, {"name": "lo2"}], - "nextPageToken": "token", - } - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="My Integration", - ): - result = list_integration_logical_operators( - chronicle_client, - integration_name="My Integration", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/My Integration/logicalOperators", - items_key="logicalOperators", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_logical_operators_default_args(chronicle_client): - """Test list_integration_logical_operators with default args.""" - expected = {"logicalOperators": []} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_logical_operators( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/logicalOperators", - items_key="logicalOperators", - page_size=None, - page_token=None, - extra_params={}, - as_list=False, - ) - - -def test_list_integration_logical_operators_with_filter_order_expand( - chronicle_client, -): - """Test list passes filter/orderBy/excludeStaging/expand in extra_params.""" - expected = {"logicalOperators": [{"name": "lo1"}]} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_logical_operators( - chronicle_client, - integration_name="test-integration", - filter_string='displayName = "My Operator"', - order_by="displayName", - exclude_staging=True, - expand="parameters", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/logicalOperators", - items_key="logicalOperators", - page_size=None, - page_token=None, - extra_params={ - "filter": 'displayName = "My Operator"', - "orderBy": "displayName", - "excludeStaging": True, - "expand": "parameters", - }, - as_list=False, - ) - - -# -- get_integration_logical_operator tests -- - - -def test_get_integration_logical_operator_success(chronicle_client): - """Test get_integration_logical_operator delegates to chronicle_request.""" - expected = {"name": "lo1", "displayName": "My Operator"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="test-integration", - ): - result = get_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/logicalOperators/lo1", - api_version=APIVersion.V1ALPHA, - params=None, - ) - - -def test_get_integration_logical_operator_with_expand(chronicle_client): - """Test get_integration_logical_operator with expand parameter.""" - expected = {"name": "lo1"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - expand="parameters", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/logicalOperators/lo1", - api_version=APIVersion.V1ALPHA, - params={"expand": "parameters"}, - ) - - -# -- delete_integration_logical_operator tests -- - - -def test_delete_integration_logical_operator_success(chronicle_client): - """Test delete_integration_logical_operator delegates to chronicle_request.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=None, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="test-integration", - ): - delete_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path="integrations/test-integration/logicalOperators/lo1", - api_version=APIVersion.V1ALPHA, - ) - - -# -- create_integration_logical_operator tests -- - - -def test_create_integration_logical_operator_minimal(chronicle_client): - """Test create_integration_logical_operator with minimal required fields.""" - expected = {"name": "lo1", "displayName": "New Operator"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="test-integration", - ): - result = create_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - display_name="New Operator", - script="def evaluate(a, b): return a == b", - script_timeout="60s", - enabled=True, - ) - - assert result == expected - - mock_request.assert_called_once() - call_kwargs = mock_request.call_args[1] - assert call_kwargs["method"] == "POST" - assert ( - call_kwargs["endpoint_path"] - == "integrations/test-integration/logicalOperators" - ) - assert call_kwargs["api_version"] == APIVersion.V1ALPHA - assert call_kwargs["json"]["displayName"] == "New Operator" - assert call_kwargs["json"]["script"] == "def evaluate(a, b): return a == b" - assert call_kwargs["json"]["scriptTimeout"] == "60s" - assert call_kwargs["json"]["enabled"] is True - - -def test_create_integration_logical_operator_with_all_fields(chronicle_client): - """Test create_integration_logical_operator with all optional fields.""" - expected = {"name": "lo1"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - display_name="Full Operator", - script="def evaluate(a, b): return a > b", - script_timeout="120s", - enabled=False, - description="Test logical operator description", - parameters=[{"name": "param1", "type": "STRING"}], - ) - - assert result == expected - - call_kwargs = mock_request.call_args[1] - body = call_kwargs["json"] - assert body["displayName"] == "Full Operator" - assert body["description"] == "Test logical operator description" - assert body["parameters"] == [{"name": "param1", "type": "STRING"}] - - -# -- update_integration_logical_operator tests -- - - -def test_update_integration_logical_operator_display_name(chronicle_client): - """Test update_integration_logical_operator updates display name.""" - expected = {"name": "lo1", "displayName": "Updated Name"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.build_patch_body", - return_value=({"displayName": "Updated Name"}, {"updateMask": "displayName"}), - ) as mock_build: - result = update_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - display_name="Updated Name", - ) - - assert result == expected - - mock_build.assert_called_once() - mock_request.assert_called_once() - call_kwargs = mock_request.call_args[1] - assert call_kwargs["method"] == "PATCH" - assert ( - call_kwargs["endpoint_path"] - == "integrations/test-integration/logicalOperators/lo1" - ) - - -def test_update_integration_logical_operator_with_update_mask(chronicle_client): - """Test update_integration_logical_operator with explicit update mask.""" - expected = {"name": "lo1"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.build_patch_body", - return_value=( - {"displayName": "New Name", "enabled": True}, - {"updateMask": "displayName,enabled"}, - ), - ): - result = update_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - display_name="New Name", - enabled=True, - update_mask="displayName,enabled", - ) - - assert result == expected - - -def test_update_integration_logical_operator_all_fields(chronicle_client): - """Test update_integration_logical_operator with all fields.""" - expected = {"name": "lo1"} - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.build_patch_body", - return_value=( - { - "displayName": "Updated", - "script": "new script", - "scriptTimeout": "90s", - "enabled": False, - "description": "Updated description", - "parameters": [{"name": "p1"}], - }, - {"updateMask": "displayName,script,scriptTimeout,enabled,description"}, - ), - ): - result = update_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - display_name="Updated", - script="new script", - script_timeout="90s", - enabled=False, - description="Updated description", - parameters=[{"name": "p1"}], - ) - - assert result == expected - - -# -- execute_integration_logical_operator_test tests -- - - -def test_execute_integration_logical_operator_test_success(chronicle_client): - """Test execute_integration_logical_operator_test delegates to chronicle_request.""" - logical_operator = { - "displayName": "Test Operator", - "script": "def evaluate(a, b): return a == b", - } - expected = { - "outputMessage": "Success", - "debugOutputMessage": "Debug info", - "resultValue": True, - } - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="test-integration", - ): - result = execute_integration_logical_operator_test( - chronicle_client, - integration_name="test-integration", - logical_operator=logical_operator, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/logicalOperators:executeTest", - api_version=APIVersion.V1ALPHA, - json={"logicalOperator": logical_operator}, - ) - - -# -- get_integration_logical_operator_template tests -- - - -def test_get_integration_logical_operator_template_success(chronicle_client): - """Test get_integration_logical_operator_template delegates to chronicle_request.""" - expected = { - "script": "def evaluate(a, b):\n # Template code\n return True", - "displayName": "Template Operator", - } - - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.logical_operators.format_resource_id", - return_value="test-integration", - ): - result = get_integration_logical_operator_template( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path=( - "integrations/test-integration/logicalOperators:fetchTemplate" - ), - api_version=APIVersion.V1ALPHA, - ) - - -# -- Error handling tests -- - - -def test_list_integration_logical_operators_api_error(chronicle_client): - """Test list_integration_logical_operators handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_paginated_request", - side_effect=APIError("API Error"), - ): - with pytest.raises(APIError, match="API Error"): - list_integration_logical_operators( - chronicle_client, - integration_name="test-integration", - ) - - -def test_get_integration_logical_operator_api_error(chronicle_client): - """Test get_integration_logical_operator handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - side_effect=APIError("Not found"), - ): - with pytest.raises(APIError, match="Not found"): - get_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="nonexistent", - ) - - -def test_create_integration_logical_operator_api_error(chronicle_client): - """Test create_integration_logical_operator handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - side_effect=APIError("Creation failed"), - ): - with pytest.raises(APIError, match="Creation failed"): - create_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - display_name="New Operator", - script="def evaluate(a, b): return a == b", - script_timeout="60s", - enabled=True, - ) - - -def test_update_integration_logical_operator_api_error(chronicle_client): - """Test update_integration_logical_operator handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - side_effect=APIError("Update failed"), - ), patch( - "secops.chronicle.integration.logical_operators.build_patch_body", - return_value=({"displayName": "Updated"}, {"updateMask": "displayName"}), - ): - with pytest.raises(APIError, match="Update failed"): - update_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - display_name="Updated", - ) - - -def test_delete_integration_logical_operator_api_error(chronicle_client): - """Test delete_integration_logical_operator handles API errors.""" - with patch( - "secops.chronicle.integration.logical_operators.chronicle_request", - side_effect=APIError("Delete failed"), - ): - with pytest.raises(APIError, match="Delete failed"): - delete_integration_logical_operator( - chronicle_client, - integration_name="test-integration", - logical_operator_id="lo1", - ) - diff --git a/tests/chronicle/integration/test_marketplace_integrations.py b/tests/chronicle/integration/test_marketplace_integrations.py deleted file mode 100644 index 15216fd9..00000000 --- a/tests/chronicle/integration/test_marketplace_integrations.py +++ /dev/null @@ -1,522 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle marketplace integration functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.marketplace_integrations import ( - list_marketplace_integrations, - get_marketplace_integration, - get_marketplace_integration_diff, - install_marketplace_integration, - uninstall_marketplace_integration, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1BETA, - ) - - -@pytest.fixture -def mock_response() -> Mock: - """Create a mock API response object.""" - mock = Mock() - mock.status_code = 200 - mock.json.return_value = {} - return mock - - -@pytest.fixture -def mock_error_response() -> Mock: - """Create a mock error API response object.""" - mock = Mock() - mock.status_code = 400 - mock.text = "Error message" - mock.raise_for_status.side_effect = Exception("API Error") - return mock - - -# -- list_marketplace_integrations tests -- - - -def test_list_marketplace_integrations_success(chronicle_client): - """Test list_marketplace_integrations delegates to chronicle_paginated_request.""" - expected = { - "marketplaceIntegrations": [ - {"name": "integration1"}, - {"name": "integration2"}, - ] - } - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations( - chronicle_client, - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_marketplace_integrations_default_args(chronicle_client): - """Test list_marketplace_integrations with default args.""" - expected = {"marketplaceIntegrations": []} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations(chronicle_client) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=None, - page_token=None, - extra_params={}, - as_list=False, - ) - - -def test_list_marketplace_integrations_with_filter(chronicle_client): - """Test list_marketplace_integrations passes filter_string in extra_params.""" - expected = {"marketplaceIntegrations": [{"name": "integration1"}]} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations( - chronicle_client, - filter_string='displayName = "My Integration"', - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=None, - page_token=None, - extra_params={"filter": 'displayName = "My Integration"'}, - as_list=False, - ) - - -def test_list_marketplace_integrations_with_order_by(chronicle_client): - """Test list_marketplace_integrations passes order_by in extra_params.""" - expected = {"marketplaceIntegrations": []} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations( - chronicle_client, - order_by="displayName", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=None, - page_token=None, - extra_params={"orderBy": "displayName"}, - as_list=False, - ) - - -def test_list_marketplace_integrations_with_filter_and_order_by(chronicle_client): - """Test list_marketplace_integrations with both filter_string and order_by.""" - expected = {"marketplaceIntegrations": []} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations( - chronicle_client, - filter_string='displayName = "My Integration"', - order_by="displayName", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=None, - page_token=None, - extra_params={ - "filter": 'displayName = "My Integration"', - "orderBy": "displayName", - }, - as_list=False, - ) - - -def test_list_marketplace_integrations_as_list(chronicle_client): - """Test list_marketplace_integrations with as_list=True.""" - expected = [{"name": "integration1"}, {"name": "integration2"}] - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_marketplace_integrations(chronicle_client, as_list=True) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1BETA, - path="marketplaceIntegrations", - items_key="marketplaceIntegrations", - page_size=None, - page_token=None, - extra_params={}, - as_list=True, - ) - - -def test_list_marketplace_integrations_error(chronicle_client): - """Test list_marketplace_integrations propagates APIError from helper.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", - side_effect=APIError("Failed to list marketplace integrations"), - ): - with pytest.raises(APIError) as exc_info: - list_marketplace_integrations(chronicle_client) - - assert "Failed to list marketplace integrations" in str(exc_info.value) - - -# -- get_marketplace_integration tests -- - - -def test_get_marketplace_integration_success(chronicle_client): - """Test get_marketplace_integration returns expected result.""" - expected = { - "name": "test-integration", - "displayName": "Test Integration", - "version": "1.0.0", - } - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_marketplace_integration(chronicle_client, "test-integration") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="marketplaceIntegrations/test-integration", - api_version=APIVersion.V1BETA, - ) - - -def test_get_marketplace_integration_error(chronicle_client): - """Test get_marketplace_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - side_effect=APIError("Failed to get marketplace integration test-integration"), - ): - with pytest.raises(APIError) as exc_info: - get_marketplace_integration(chronicle_client, "test-integration") - - assert "Failed to get marketplace integration" in str(exc_info.value) - - -# -- get_marketplace_integration_diff tests -- - - -def test_get_marketplace_integration_diff_success(chronicle_client): - """Test get_marketplace_integration_diff returns expected result.""" - expected = { - "name": "test-integration", - "diff": {"added": [], "removed": [], "modified": []}, - } - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_marketplace_integration_diff(chronicle_client, "test-integration") - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path=( - "marketplaceIntegrations/test-integration" - ":fetchCommercialDiff" - ), - api_version=APIVersion.V1BETA, - ) - - -def test_get_marketplace_integration_diff_error(chronicle_client): - """Test get_marketplace_integration_diff raises APIError on failure.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - side_effect=APIError("Failed to get marketplace integration diff"), - ): - with pytest.raises(APIError) as exc_info: - get_marketplace_integration_diff(chronicle_client, "test-integration") - - assert "Failed to get marketplace integration diff" in str(exc_info.value) - - -# -- install_marketplace_integration tests -- - - -def test_install_marketplace_integration_no_optional_fields(chronicle_client): - """Test install_marketplace_integration with no optional fields sends empty body.""" - expected = { - "name": "test-integration", - "displayName": "Test Integration", - "installedVersion": "1.0.0", - } - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:install", - json={}, - api_version=APIVersion.V1BETA, - ) - - -def test_install_marketplace_integration_all_fields(chronicle_client): - """Test install_marketplace_integration with all optional fields.""" - expected = { - "name": "test-integration", - "displayName": "Test Integration", - "installedVersion": "2.0.0", - } - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - override_mapping=True, - staging=False, - version="2.0.0", - restore_from_snapshot=True, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:install", - json={ - "overrideMapping": True, - "staging": False, - "version": "2.0.0", - "restoreFromSnapshot": True, - }, - api_version=APIVersion.V1BETA, - ) - - -def test_install_marketplace_integration_override_mapping_only(chronicle_client): - """Test install_marketplace_integration with only override_mapping set.""" - expected = {"name": "test-integration"} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - override_mapping=True, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:install", - json={"overrideMapping": True}, - api_version=APIVersion.V1BETA, - ) - - -def test_install_marketplace_integration_version_only(chronicle_client): - """Test install_marketplace_integration with only version set.""" - expected = {"name": "test-integration"} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - version="1.2.3", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:install", - json={"version": "1.2.3"}, - api_version=APIVersion.V1BETA, - ) - - -def test_install_marketplace_integration_none_fields_excluded(chronicle_client): - """Test that None optional fields are not included in the request body.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value={"name": "test-integration"}, - ) as mock_request: - install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - override_mapping=None, - staging=None, - version=None, - restore_from_snapshot=None, - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:install", - json={}, - api_version=APIVersion.V1BETA, - ) - - -def test_install_marketplace_integration_error(chronicle_client): - """Test install_marketplace_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - side_effect=APIError("Failed to install marketplace integration"), - ): - with pytest.raises(APIError) as exc_info: - install_marketplace_integration( - chronicle_client, - integration_name="test-integration", - ) - - assert "Failed to install marketplace integration" in str(exc_info.value) - - -# -- uninstall_marketplace_integration tests -- - - -def test_uninstall_marketplace_integration_success(chronicle_client): - """Test uninstall_marketplace_integration returns expected result.""" - expected = {} - - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - return_value=expected, - ) as mock_request: - result = uninstall_marketplace_integration( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="marketplaceIntegrations/test-integration:uninstall", - api_version=APIVersion.V1BETA, - ) - - -def test_uninstall_marketplace_integration_error(chronicle_client): - """Test uninstall_marketplace_integration raises APIError on failure.""" - with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_request", - side_effect=APIError("Failed to uninstall marketplace integration"), - ): - with pytest.raises(APIError) as exc_info: - uninstall_marketplace_integration( - chronicle_client, - integration_name="test-integration", - ) - - assert "Failed to uninstall marketplace integration" in str(exc_info.value) \ No newline at end of file diff --git a/tests/chronicle/integration/test_transformer_revisions.py b/tests/chronicle/integration/test_transformer_revisions.py deleted file mode 100644 index 8107891e..00000000 --- a/tests/chronicle/integration/test_transformer_revisions.py +++ /dev/null @@ -1,366 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration transformer revisions functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.transformer_revisions import ( - list_integration_transformer_revisions, - delete_integration_transformer_revision, - create_integration_transformer_revision, - rollback_integration_transformer_revision, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1ALPHA, - ) - - -# -- list_integration_transformer_revisions tests -- - - -def test_list_integration_transformer_revisions_success(chronicle_client): - """Test list_integration_transformer_revisions delegates to paginated request.""" - expected = { - "revisions": [{"name": "r1"}, {"name": "r2"}], - "nextPageToken": "token", - } - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.transformer_revisions.format_resource_id", - return_value="My Integration", - ): - result = list_integration_transformer_revisions( - chronicle_client, - integration_name="My Integration", - transformer_id="t1", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/My Integration/transformers/t1/revisions", - items_key="revisions", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_transformer_revisions_default_args(chronicle_client): - """Test list_integration_transformer_revisions with default args.""" - expected = {"revisions": []} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_transformer_revisions( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/transformers/t1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={}, - as_list=False, - ) - - -def test_list_integration_transformer_revisions_with_filter_order( - chronicle_client, -): - """Test list passes filter/orderBy in extra_params.""" - expected = {"revisions": [{"name": "r1"}]} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_transformer_revisions( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - filter_string='version = "1.0"', - order_by="createTime desc", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/transformers/t1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={ - "filter": 'version = "1.0"', - "orderBy": "createTime desc", - }, - as_list=False, - ) - - -def test_list_integration_transformer_revisions_as_list(chronicle_client): - """Test list_integration_transformer_revisions with as_list=True.""" - expected = [{"name": "r1"}, {"name": "r2"}] - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_transformer_revisions( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - as_list=True, - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/transformers/t1/revisions", - items_key="revisions", - page_size=None, - page_token=None, - extra_params={}, - as_list=True, - ) - - -# -- delete_integration_transformer_revision tests -- - - -def test_delete_integration_transformer_revision_success(chronicle_client): - """Test delete_integration_transformer_revision delegates to chronicle_request.""" - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - return_value=None, - ) as mock_request, patch( - "secops.chronicle.integration.transformer_revisions.format_resource_id", - return_value="test-integration", - ): - delete_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - revision_id="rev1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path=( - "integrations/test-integration/transformers/t1/revisions/rev1" - ), - api_version=APIVersion.V1ALPHA, - ) - - -# -- create_integration_transformer_revision tests -- - - -def test_create_integration_transformer_revision_minimal(chronicle_client): - """Test create_integration_transformer_revision with minimal fields.""" - transformer = { - "displayName": "Test Transformer", - "script": "def transform(data): return data", - } - expected = {"name": "rev1", "comment": ""} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformer_revisions.format_resource_id", - return_value="test-integration", - ): - result = create_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - transformer=transformer, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/transformers/t1/revisions" - ), - api_version=APIVersion.V1ALPHA, - json={"transformer": transformer}, - ) - - -def test_create_integration_transformer_revision_with_comment(chronicle_client): - """Test create_integration_transformer_revision with comment.""" - transformer = { - "displayName": "Test Transformer", - "script": "def transform(data): return data", - } - expected = {"name": "rev1", "comment": "Version 2.0"} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - transformer=transformer, - comment="Version 2.0", - ) - - assert result == expected - - call_kwargs = mock_request.call_args[1] - assert call_kwargs["json"]["transformer"] == transformer - assert call_kwargs["json"]["comment"] == "Version 2.0" - - -# -- rollback_integration_transformer_revision tests -- - - -def test_rollback_integration_transformer_revision_success(chronicle_client): - """Test rollback_integration_transformer_revision delegates to chronicle_request.""" - expected = {"name": "rev1", "comment": "Rolled back"} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformer_revisions.format_resource_id", - return_value="test-integration", - ): - result = rollback_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - revision_id="rev1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path=( - "integrations/test-integration/transformers/t1/revisions/rev1:rollback" - ), - api_version=APIVersion.V1ALPHA, - ) - - -# -- Error handling tests -- - - -def test_list_integration_transformer_revisions_api_error(chronicle_client): - """Test list_integration_transformer_revisions handles API errors.""" - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_paginated_request", - side_effect=APIError("API Error"), - ): - with pytest.raises(APIError, match="API Error"): - list_integration_transformer_revisions( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - ) - - -def test_delete_integration_transformer_revision_api_error(chronicle_client): - """Test delete_integration_transformer_revision handles API errors.""" - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - side_effect=APIError("Delete failed"), - ): - with pytest.raises(APIError, match="Delete failed"): - delete_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - revision_id="rev1", - ) - - -def test_create_integration_transformer_revision_api_error(chronicle_client): - """Test create_integration_transformer_revision handles API errors.""" - transformer = {"displayName": "Test"} - - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - side_effect=APIError("Creation failed"), - ): - with pytest.raises(APIError, match="Creation failed"): - create_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - transformer=transformer, - ) - - -def test_rollback_integration_transformer_revision_api_error(chronicle_client): - """Test rollback_integration_transformer_revision handles API errors.""" - with patch( - "secops.chronicle.integration.transformer_revisions.chronicle_request", - side_effect=APIError("Rollback failed"), - ): - with pytest.raises(APIError, match="Rollback failed"): - rollback_integration_transformer_revision( - chronicle_client, - integration_name="test-integration", - transformer_id="t1", - revision_id="rev1", - ) - diff --git a/tests/chronicle/integration/test_transformers.py b/tests/chronicle/integration/test_transformers.py deleted file mode 100644 index 43d8687e..00000000 --- a/tests/chronicle/integration/test_transformers.py +++ /dev/null @@ -1,555 +0,0 @@ -# Copyright 2025 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. -# -"""Tests for Chronicle integration transformers functions.""" - -from unittest.mock import Mock, patch - -import pytest - -from secops.chronicle.client import ChronicleClient -from secops.chronicle.models import APIVersion -from secops.chronicle.integration.transformers import ( - list_integration_transformers, - get_integration_transformer, - delete_integration_transformer, - create_integration_transformer, - update_integration_transformer, - execute_integration_transformer_test, - get_integration_transformer_template, -) -from secops.exceptions import APIError - - -@pytest.fixture -def chronicle_client(): - """Create a Chronicle client for testing.""" - with patch("secops.auth.SecOpsAuth") as mock_auth: - mock_session = Mock() - mock_session.headers = {} - mock_auth.return_value.session = mock_session - return ChronicleClient( - customer_id="test-customer", - project_id="test-project", - default_api_version=APIVersion.V1ALPHA, - ) - - -# -- list_integration_transformers tests -- - - -def test_list_integration_transformers_success(chronicle_client): - """Test list_integration_transformers delegates to chronicle_paginated_request.""" - expected = { - "transformers": [{"name": "t1"}, {"name": "t2"}], - "nextPageToken": "token", - } - - with patch( - "secops.chronicle.integration.transformers.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="My Integration", - ): - result = list_integration_transformers( - chronicle_client, - integration_name="My Integration", - page_size=10, - page_token="next-token", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/My Integration/transformers", - items_key="transformers", - page_size=10, - page_token="next-token", - extra_params={}, - as_list=False, - ) - - -def test_list_integration_transformers_default_args(chronicle_client): - """Test list_integration_transformers with default args.""" - expected = {"transformers": []} - - with patch( - "secops.chronicle.integration.transformers.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_transformers( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/transformers", - items_key="transformers", - page_size=None, - page_token=None, - extra_params={}, - as_list=False, - ) - - -def test_list_integration_transformers_with_filter_order_expand(chronicle_client): - """Test list passes filter/orderBy/excludeStaging/expand in extra_params.""" - expected = {"transformers": [{"name": "t1"}]} - - with patch( - "secops.chronicle.integration.transformers.chronicle_paginated_request", - return_value=expected, - ) as mock_paginated: - result = list_integration_transformers( - chronicle_client, - integration_name="test-integration", - filter_string='displayName = "My Transformer"', - order_by="displayName", - exclude_staging=True, - expand="parameters", - ) - - assert result == expected - - mock_paginated.assert_called_once_with( - chronicle_client, - api_version=APIVersion.V1ALPHA, - path="integrations/test-integration/transformers", - items_key="transformers", - page_size=None, - page_token=None, - extra_params={ - "filter": 'displayName = "My Transformer"', - "orderBy": "displayName", - "excludeStaging": True, - "expand": "parameters", - }, - as_list=False, - ) - - -# -- get_integration_transformer tests -- - - -def test_get_integration_transformer_success(chronicle_client): - """Test get_integration_transformer delegates to chronicle_request.""" - expected = {"name": "transformer1", "displayName": "My Transformer"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="test-integration", - ): - result = get_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/transformers/transformer1", - api_version=APIVersion.V1ALPHA, - params=None, - ) - - -def test_get_integration_transformer_with_expand(chronicle_client): - """Test get_integration_transformer with expand parameter.""" - expected = {"name": "transformer1"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request: - result = get_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - expand="parameters", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/transformers/transformer1", - api_version=APIVersion.V1ALPHA, - params={"expand": "parameters"}, - ) - - -# -- delete_integration_transformer tests -- - - -def test_delete_integration_transformer_success(chronicle_client): - """Test delete_integration_transformer delegates to chronicle_request.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=None, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="test-integration", - ): - delete_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - ) - - mock_request.assert_called_once_with( - chronicle_client, - method="DELETE", - endpoint_path="integrations/test-integration/transformers/transformer1", - api_version=APIVersion.V1ALPHA, - ) - - -# -- create_integration_transformer tests -- - - -def test_create_integration_transformer_minimal(chronicle_client): - """Test create_integration_transformer with minimal required fields.""" - expected = {"name": "transformer1", "displayName": "New Transformer"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="test-integration", - ): - result = create_integration_transformer( - chronicle_client, - integration_name="test-integration", - display_name="New Transformer", - script="def transform(data): return data", - script_timeout="60s", - enabled=True, - ) - - assert result == expected - - mock_request.assert_called_once() - call_kwargs = mock_request.call_args[1] - assert call_kwargs["method"] == "POST" - assert ( - call_kwargs["endpoint_path"] - == "integrations/test-integration/transformers" - ) - assert call_kwargs["api_version"] == APIVersion.V1ALPHA - assert call_kwargs["json"]["displayName"] == "New Transformer" - assert call_kwargs["json"]["script"] == "def transform(data): return data" - assert call_kwargs["json"]["scriptTimeout"] == "60s" - assert call_kwargs["json"]["enabled"] is True - - -def test_create_integration_transformer_with_all_fields(chronicle_client): - """Test create_integration_transformer with all optional fields.""" - expected = {"name": "transformer1"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request: - result = create_integration_transformer( - chronicle_client, - integration_name="test-integration", - display_name="Full Transformer", - script="def transform(data): return data", - script_timeout="120s", - enabled=False, - description="Test transformer description", - parameters=[{"name": "param1", "type": "STRING"}], - usage_example="Example usage", - expected_output="Output format", - expected_input="Input format", - ) - - assert result == expected - - call_kwargs = mock_request.call_args[1] - body = call_kwargs["json"] - assert body["displayName"] == "Full Transformer" - assert body["description"] == "Test transformer description" - assert body["parameters"] == [{"name": "param1", "type": "STRING"}] - assert body["usageExample"] == "Example usage" - assert body["expectedOutput"] == "Output format" - assert body["expectedInput"] == "Input format" - - -# -- update_integration_transformer tests -- - - -def test_update_integration_transformer_display_name(chronicle_client): - """Test update_integration_transformer updates display name.""" - expected = {"name": "transformer1", "displayName": "Updated Name"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.build_patch_body", - return_value=({"displayName": "Updated Name"}, {"updateMask": "displayName"}), - ) as mock_build: - result = update_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - display_name="Updated Name", - ) - - assert result == expected - - mock_build.assert_called_once() - mock_request.assert_called_once() - call_kwargs = mock_request.call_args[1] - assert call_kwargs["method"] == "PATCH" - assert ( - call_kwargs["endpoint_path"] - == "integrations/test-integration/transformers/transformer1" - ) - - -def test_update_integration_transformer_with_update_mask(chronicle_client): - """Test update_integration_transformer with explicit update mask.""" - expected = {"name": "transformer1"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.build_patch_body", - return_value=( - {"displayName": "New Name", "enabled": True}, - {"updateMask": "displayName,enabled"}, - ), - ): - result = update_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - display_name="New Name", - enabled=True, - update_mask="displayName,enabled", - ) - - assert result == expected - - -def test_update_integration_transformer_all_fields(chronicle_client): - """Test update_integration_transformer with all fields.""" - expected = {"name": "transformer1"} - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.build_patch_body", - return_value=( - { - "displayName": "Updated", - "script": "new script", - "scriptTimeout": "90s", - "enabled": False, - "description": "Updated description", - "parameters": [{"name": "p1"}], - "usageExample": "New example", - "expectedOutput": "New output", - "expectedInput": "New input", - }, - {"updateMask": "displayName,script,scriptTimeout,enabled,description"}, - ), - ): - result = update_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - display_name="Updated", - script="new script", - script_timeout="90s", - enabled=False, - description="Updated description", - parameters=[{"name": "p1"}], - usage_example="New example", - expected_output="New output", - expected_input="New input", - ) - - assert result == expected - - -# -- execute_integration_transformer_test tests -- - - -def test_execute_integration_transformer_test_success(chronicle_client): - """Test execute_integration_transformer_test delegates to chronicle_request.""" - transformer = { - "displayName": "Test Transformer", - "script": "def transform(data): return data", - } - expected = { - "outputMessage": "Success", - "debugOutputMessage": "Debug info", - "resultValue": {"status": "ok"}, - } - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="test-integration", - ): - result = execute_integration_transformer_test( - chronicle_client, - integration_name="test-integration", - transformer=transformer, - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="POST", - endpoint_path="integrations/test-integration/transformers:executeTest", - api_version=APIVersion.V1ALPHA, - json={"transformer": transformer}, - ) - - -# -- get_integration_transformer_template tests -- - - -def test_get_integration_transformer_template_success(chronicle_client): - """Test get_integration_transformer_template delegates to chronicle_request.""" - expected = { - "script": "def transform(data):\n # Template code\n return data", - "displayName": "Template Transformer", - } - - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - return_value=expected, - ) as mock_request, patch( - "secops.chronicle.integration.transformers.format_resource_id", - return_value="test-integration", - ): - result = get_integration_transformer_template( - chronicle_client, - integration_name="test-integration", - ) - - assert result == expected - - mock_request.assert_called_once_with( - chronicle_client, - method="GET", - endpoint_path="integrations/test-integration/transformers:fetchTemplate", - api_version=APIVersion.V1ALPHA, - ) - - -# -- Error handling tests -- - - -def test_list_integration_transformers_api_error(chronicle_client): - """Test list_integration_transformers handles API errors.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_paginated_request", - side_effect=APIError("API Error"), - ): - with pytest.raises(APIError, match="API Error"): - list_integration_transformers( - chronicle_client, - integration_name="test-integration", - ) - - -def test_get_integration_transformer_api_error(chronicle_client): - """Test get_integration_transformer handles API errors.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - side_effect=APIError("Not found"), - ): - with pytest.raises(APIError, match="Not found"): - get_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="nonexistent", - ) - - -def test_create_integration_transformer_api_error(chronicle_client): - """Test create_integration_transformer handles API errors.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - side_effect=APIError("Creation failed"), - ): - with pytest.raises(APIError, match="Creation failed"): - create_integration_transformer( - chronicle_client, - integration_name="test-integration", - display_name="New Transformer", - script="def transform(data): return data", - script_timeout="60s", - enabled=True, - ) - - -def test_update_integration_transformer_api_error(chronicle_client): - """Test update_integration_transformer handles API errors.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - side_effect=APIError("Update failed"), - ), patch( - "secops.chronicle.integration.transformers.build_patch_body", - return_value=({"displayName": "Updated"}, {"updateMask": "displayName"}), - ): - with pytest.raises(APIError, match="Update failed"): - update_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - display_name="Updated", - ) - - -def test_delete_integration_transformer_api_error(chronicle_client): - """Test delete_integration_transformer handles API errors.""" - with patch( - "secops.chronicle.integration.transformers.chronicle_request", - side_effect=APIError("Delete failed"), - ): - with pytest.raises(APIError, match="Delete failed"): - delete_integration_transformer( - chronicle_client, - integration_name="test-integration", - transformer_id="transformer1", - ) -