From 2002aa284576c4a65594ddc5fc726262d13dd122 Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Fri, 27 Feb 2026 20:24:09 +0000 Subject: [PATCH 01/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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 49f7b3d3148101915b8a919974cdcdf3524944ff Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 14:47:09 +0000 Subject: [PATCH 45/47] chore: refactor for split PR --- CLI.md | 1759 +------ README.md | 3028 +---------- src/secops/chronicle/__init__.py | 267 - src/secops/chronicle/client.py | 4666 +---------------- .../chronicle/integration/action_revisions.py | 201 - src/secops/chronicle/integration/actions.py | 452 -- .../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/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/manager_revisions.py | 243 - src/secops/chronicle/integration/managers.py | 285 - .../integration/transformer_revisions.py | 202 - .../chronicle/integration/transformers.py | 406 -- .../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 | 76 +- .../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 -- .../commands/integration/manager_revisions.py | 254 - .../cli/commands/integration/managers.py | 283 - .../integration/transformer_revisions.py | 236 - .../cli/commands/integration/transformers.py | 387 -- .../integration/test_action_revisions.py | 409 -- tests/chronicle/integration/test_actions.py | 666 --- .../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 --- .../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 -- .../integration/test_manager_revisions.py | 417 -- tests/chronicle/integration/test_managers.py | 460 -- .../integration/test_transformer_revisions.py | 366 -- .../integration/test_transformers.py | 555 -- 59 files changed, 460 insertions(+), 29029 deletions(-) delete mode 100644 src/secops/chronicle/integration/action_revisions.py delete mode 100644 src/secops/chronicle/integration/actions.py 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/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/manager_revisions.py delete mode 100644 src/secops/chronicle/integration/managers.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/action_revisions.py delete mode 100644 src/secops/cli/commands/integration/actions.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/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/manager_revisions.py delete mode 100644 src/secops/cli/commands/integration/managers.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_action_revisions.py delete mode 100644 tests/chronicle/integration/test_actions.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_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_manager_revisions.py delete mode 100644 tests/chronicle/integration/test_managers.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..ed99978b 100644 --- a/CLI.md +++ b/CLI.md @@ -794,1181 +794,258 @@ Uninstall a marketplace integration: secops integration marketplace uninstall --integration-name "AWSSecurityHub" ``` -#### Integration Actions +#### Integrations -List integration actions: +List integrations: ```bash -# List all actions for an integration -secops integration actions list --integration-name "MyIntegration" +# List all integrations +secops integration integrations list -# List actions as a direct list (fetches all pages automatically) -secops integration actions list --integration-name "MyIntegration" --as-list +# List integrations as a direct list +secops integration integrations list --as-list # List with pagination -secops integration actions list --integration-name "MyIntegration" --page-size 50 +secops integration integrations list --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" -``` +secops integration integrations list --filter-string "displayName = 'CrowdStrike'" -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" +# List with ordering +secops integration integrations list --order-by "displayName" ``` -Create a revision backup: +Get integration details: ```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" +secops integration integrations get --integration-id "MyIntegration" ``` -Rollback to a previous revision: +Create a new custom integration: ```bash -secops integration connector-revisions rollback \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --revision-id "r456" -``` +# Create a basic integration +secops integration integrations create --display-name "My Custom Integration" -Delete an old revision: +# Create in staging mode +secops integration integrations create \ + --display-name "My Custom Integration" \ + --staging -```bash -secops integration connector-revisions delete \ - --integration-name "MyIntegration" \ - --connector-id "c1" \ - --revision-id "r789" +# Create with all options +secops integration integrations create \ + --display-name "My Custom Integration" \ + --description "Custom integration for internal tooling" \ + --python-version "V3_11" \ + --integration-type "RESPONSE" \ + --staging ``` -#### 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: +Update an integration: ```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 +secops integration integrations update \ + --integration-id "MyIntegration" \ + --display-name "Updated Integration Name" -# 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" \ +# Update multiple fields +secops integration integrations update \ + --integration-id "MyIntegration" \ + --display-name "Updated Name" \ + --description "Updated description" \ + --python-version "V3_11" + +# Update with explicit update mask +secops integration integrations update \ + --integration-id "MyIntegration" \ --display-name "New Name" \ - --schedule "0 6 * * *" \ - --update-mask "displayName,schedule" + --update-mask "displayName" -# 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"}]' -``` +# Remove dependencies during update +secops integration integrations update \ + --integration-id "MyIntegration" \ + --dependencies-to-remove "old-package" "unused-lib" -#### Integration Managers +# Set integration to staging mode +secops integration integrations update \ + --integration-id "MyIntegration" \ + --staging +``` -List integration managers: +Update a custom integration definition: ```bash -# List all managers for an integration -secops integration managers list --integration-name "MyIntegration" +# Update a custom integration (supports parameters and dependencies) +secops integration integrations update-custom \ + --integration-id "MyIntegration" \ + --display-name "Updated Custom Integration" \ + --description "Updated custom integration" -# List managers as a direct list (fetches all pages automatically) -secops integration managers list --integration-name "MyIntegration" --as-list +# Update with all options +secops integration integrations update-custom \ + --integration-id "MyIntegration" \ + --display-name "Updated Custom Integration" \ + --python-version "V3_11" \ + --integration-type "EXTENSION" \ + --dependencies-to-remove "old-dep" \ + --staging +``` -# List with pagination -secops integration managers list --integration-name "MyIntegration" --page-size 50 +Delete an integration: -# List with filtering -secops integration managers list --integration-name "MyIntegration" --filter-string "enabled = true" +```bash +secops integration integrations delete --integration-id "MyIntegration" ``` -Get manager details: +Download an integration package: ```bash -secops integration managers get --integration-name "MyIntegration" --manager-id "mgr1" +# Download integration as a ZIP file +secops integration integrations download \ + --integration-id "MyIntegration" \ + --output-file "/tmp/my-integration.zip" ``` -Create a new manager: +Download a Python dependency for a custom integration: ```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" +# Download a specific dependency +secops integration integrations download-dependency \ + --integration-id "MyIntegration" \ + --dependency-name "requests==2.31.0" ``` -Update an existing manager: +Export specific items from an integration: ```bash -# Update display name -secops integration managers update \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --display-name "Updated Manager Name" +# Export specific actions and connectors +secops integration integrations export-items \ + --integration-id "MyIntegration" \ + --output-file "/tmp/export.zip" \ + --actions "action1" "action2" \ + --connectors "connector1" -# Update code -secops integration managers update \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --code "def manage(context): return {'status': 'updated'}" +# Export jobs and managers +secops integration integrations export-items \ + --integration-id "MyIntegration" \ + --output-file "/tmp/export.zip" \ + --jobs "job1" "job2" \ + --managers "manager1" -# 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" +# Export transformers and logical operators +secops integration integrations export-items \ + --integration-id "MyIntegration" \ + --output-file "/tmp/export.zip" \ + --transformers "t1" \ + --logical-operators "lo1" "lo2" ``` -Delete a manager: +Get items affected by changes to an integration: ```bash -secops integration managers delete --integration-name "MyIntegration" --manager-id "mgr1" +secops integration integrations affected-items --integration-id "MyIntegration" ``` -Get manager template: +Get integrations installed on a specific agent: ```bash -secops integration managers template --integration-name "MyIntegration" +secops integration integrations agent-integrations --agent-id "my-agent-id" ``` -#### Manager Revisions - -List manager revisions: +Get Python dependencies for a custom integration: ```bash -# List all revisions for a manager -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" +secops integration integrations dependencies --integration-id "MyIntegration" +``` -# List revisions as a direct list -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --as-list +Get agents restricted from running an updated integration: -# List with pagination -secops integration manager-revisions list \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --page-size 10 +```bash +# Check restricted agents for a Python version upgrade +secops integration integrations restricted-agents \ + --integration-id "MyIntegration" \ + --required-python-version "V3_11" -# 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" +# Check restricted agents for a push request +secops integration integrations restricted-agents \ + --integration-id "MyIntegration" \ + --required-python-version "V3_11" \ + --push-request ``` -Get a specific revision: +Get the configuration diff for an integration: ```bash -secops integration manager-revisions get \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r456" +# Get diff against marketplace version (default) +secops integration integrations diff --integration-id "MyIntegration" + +# Get diff between staging and production +secops integration integrations diff \ + --integration-id "MyIntegration" \ + --diff-type "Production" + +# Get diff between production and staging +secops integration integrations diff \ + --integration-id "MyIntegration" \ + --diff-type "Staging" ``` -Create a revision backup: +Transition an integration between staging and production: ```bash -# Create revision with comment -secops integration manager-revisions create \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --comment "Backup before major refactor" +# Push integration to production +secops integration integrations transition \ + --integration-id "MyIntegration" \ + --target-mode "Production" -# Create revision without comment -secops integration manager-revisions create \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" +# Push integration to staging +secops integration integrations transition \ + --integration-id "MyIntegration" \ + --target-mode "Staging" ``` -Rollback to a previous revision: +Example workflow: Create, configure, test, and deploy a custom integration: ```bash -secops integration manager-revisions rollback \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r456" -``` +# 1. Create a new custom integration in staging +secops integration integrations create \ + --display-name "My Custom SIEM Connector" \ + --description "Custom connector for internal SIEM" \ + --python-version "V3_11" \ + --integration-type "RESPONSE" \ + --staging -Delete an old revision: +# 2. Check its dependencies +secops integration integrations dependencies \ + --integration-id "MyCustomSIEMConnector" -```bash -secops integration manager-revisions delete \ - --integration-name "MyIntegration" \ - --manager-id "mgr1" \ - --revision-id "r789" +# 3. View the diff before pushing to production +secops integration integrations diff \ + --integration-id "MyCustomSIEMConnector" \ + --diff-type "Production" + +# 4. Check for restricted agents +secops integration integrations restricted-agents \ + --integration-id "MyCustomSIEMConnector" \ + --required-python-version "V3_11" \ + --push-request + +# 5. Push to production +secops integration integrations transition \ + --integration-id "MyCustomSIEMConnector" \ + --target-mode "Production" + +# 6. Download a backup +secops integration integrations download \ + --integration-id "MyCustomSIEMConnector" \ + --output-file "/tmp/my-siem-connector-backup.zip" + +# 7. Export specific items for sharing +secops integration integrations export-items \ + --integration-id "MyCustomSIEMConnector" \ + --output-file "/tmp/siem-actions.zip" \ + --actions "PingAction" "FetchEvents" ``` #### Integration Instances @@ -2080,495 +1157,7 @@ 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 diff --git a/README.md b/README.md index 9a49afe9..e57f285e 100644 --- a/README.md +++ b/README.md @@ -2185,3026 +2185,148 @@ chronicle.transition_integration_environment( ) ``` -### 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") -``` - -### 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: - -```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, - 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=ParamType.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, - 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')" -) -``` - -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')}") -``` - -### 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") -``` - -### 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')}") -``` - -### 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 +### Integration Instances -List all logical operators for a specific integration: +List all instances 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" -) +# 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')}") -# Exclude staging logical operators -logical_operators = chronicle.list_integration_logical_operators( - "MyIntegration", - exclude_staging=True -) +# Get all instances as a list +instances = chronicle.list_integration_instances("MyIntegration", as_list=True) -# Get logical operators with expanded details -logical_operators = chronicle.list_integration_logical_operators( +# Get instances for a specific environment +instances = chronicle.list_integration_instances( "MyIntegration", - expand="parameters" + filter_string="environment = 'production'" ) ``` -Get details of a specific logical operator: +Get details of a specific integration instance: ```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( +instance = chronicle.get_integration_instance( integration_name="MyIntegration", - logical_operator_id="lo1", - expand="parameters" + 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 logical operator: +Create a new integration instance: ```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 -) +from secops.chronicle.models import IntegrationInstanceParameter -# Create a more complex conditional operator with parameters -new_operator = chronicle.create_integration_logical_operator( +# Create instance with required fields only +new_instance = chronicle.create_integration_instance( 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" - } - ] + environment="production" ) -# Create a string matching operator -pattern_operator = chronicle.create_integration_logical_operator( +# Create instance with all fields +new_instance = chronicle.create_integration_instance( 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, + environment="production", + display_name="Production Instance", + description="Main production integration instance", 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" - } - ] + IntegrationInstanceParameter( + value="api_key_value" + ), + IntegrationInstanceParameter( + value="https://api.example.com" + ) + ], + agent="agent-123" ) ``` -Update an existing logical operator: +Update an existing integration instance: ```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" -) +from secops.chronicle.models import IntegrationInstanceParameter -# Update logical operator script -updated_operator = chronicle.update_integration_logical_operator( +# Update instance display name +updated_instance = chronicle.update_integration_instance( 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 -""" + integration_instance_id="ii1", + display_name="Updated Production Instance" ) # Update multiple fields including parameters -updated_operator = chronicle.update_integration_logical_operator( +updated_instance = chronicle.update_integration_instance( 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, + integration_instance_id="ii1", + display_name="Updated Instance", + description="Updated description", + environment="staging", 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 - } + IntegrationInstanceParameter( + value="new_api_key" + ) ] ) # Use custom update mask -updated_operator = chronicle.update_integration_logical_operator( +updated_instance = chronicle.update_integration_instance( integration_name="MyIntegration", - logical_operator_id="lo1", + integration_instance_id="ii1", 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" + update_mask="displayName" ) ``` -Delete a specific logical operator revision: +Delete an integration instance: ```python -chronicle.delete_integration_logical_operator_revision( +chronicle.delete_integration_instance( integration_name="MyIntegration", - logical_operator_id="lo1", - revision_id="rev-456" + integration_instance_id="ii1" ) ``` -Create a new revision before making changes: +Execute a connectivity test for an integration instance: ```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( +# Test if the instance can connect to the third-party service +test_result = chronicle.execute_integration_instance_test( integration_name="MyIntegration", - logical_operator_id="lo1", - logical_operator=logical_operator, - comment="Version 2.0 - Enhanced comparison logic" + integration_instance_id="ii1" ) +print(f"Test Successful: {test_result.get('successful')}") +print(f"Message: {test_result.get('message')}") ``` -Rollback to a previous revision: +Get affected items (playbooks) that depend on an integration instance: ```python -# Rollback to a previous working version -rollback_result = chronicle.rollback_integration_logical_operator_revision( +# Perform impact analysis before deleting or modifying an instance +affected_items = chronicle.get_integration_instance_affected_items( integration_name="MyIntegration", - logical_operator_id="lo1", - revision_id="rev-456" + integration_instance_id="ii1" ) -print(f"Rolled back to: {rollback_result.get('name')}") +for playbook in affected_items.get("affectedPlaybooks", []): + print(f"Playbook: {playbook.get('displayName')}") + print(f" ID: {playbook.get('name')}") ``` -Example workflow: Safe logical operator updates with revision control: +Get the default integration instance: ```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 +# Get the system default configuration for a commercial product +default_instance = chronicle.get_default_integration_instance( + integration_name="AWSSecurityHub" ) -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]})") +print(f"Default Instance: {default_instance.get('displayName')}") +print(f"Environment: {default_instance.get('environment')}") ``` - ## 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..9f302080 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -227,149 +227,6 @@ 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.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, - 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, - 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.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, @@ -568,130 +425,6 @@ "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 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", - "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", - "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", - # 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", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index e69a9fd2..5e8b4bb6 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -154,109 +154,6 @@ 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.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, - 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, - 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.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.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, @@ -267,41 +164,9 @@ 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, @@ -309,12 +174,10 @@ InputInterval, IntegrationInstanceParameter, IntegrationType, - JobParameter, PythonVersion, TargetMode, TileType, IntegrationParam, - ConnectorInstanceParameter, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -1535,4518 +1398,335 @@ def update_custom_integration( ) # ------------------------------------------------------------------------- - # Integration Action methods + # Integration Instances methods # ------------------------------------------------------------------------- - def list_integration_actions( + 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, - 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. + """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 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. + 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 actions. - If as_list is False: Dict with actions list and nextPageToken. + 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 + APIError: If the API request fails. """ - return _list_integration_actions( + return _list_integration_instances( 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( + def get_integration_instance( self, integration_name: str, - action_id: str, + integration_instance_id: str, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """Get details of a specific action for a given integration. + """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 action belongs to - action_id: ID of the action to retrieve - api_version: API version to use for the request. Default is V1BETA. + 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 action. + Dict containing details of the specified + IntegrationInstance. Raises: - APIError: If the API request fails + APIError: If the API request fails. """ - return _get_integration_action( + return _get_integration_instance( self, integration_name, - action_id, + integration_instance_id, api_version=api_version, ) - def delete_integration_action( + def delete_integration_instance( self, integration_name: str, - action_id: str, + integration_instance_id: str, api_version: APIVersion | None = APIVersion.V1BETA, ) -> None: - """Delete a specific action from a given integration. + """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 action belongs to - action_id: ID of the action to delete - api_version: API version to use for the request. Default is V1BETA. + 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 + APIError: If the API request fails. """ - return _delete_integration_action( + return _delete_integration_instance( self, integration_name, - action_id, + integration_instance_id, api_version=api_version, ) - def create_integration_action( + def create_integration_instance( self, integration_name: str, - display_name: str, - script: str, - timeout_seconds: int, - enabled: bool, - script_result_name: str, - is_async: bool, + environment: str, + display_name: str | 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, + parameters: ( + list[dict[str, Any] | IntegrationInstanceParameter] | None + ) = None, + agent: str | 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. + """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 IntegrationAction resource. + Dict containing the newly created IntegrationInstance + resource. Raises: APIError: If the API request fails. """ - return _create_integration_action( + return _create_integration_instance( self, integration_name, - display_name, - script, - timeout_seconds, - enabled, - script_result_name, - is_async, + environment, + display_name=display_name, 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, + agent=agent, api_version=api_version, ) - def update_integration_action( + def update_integration_instance( self, integration_name: str, - action_id: str, + integration_instance_id: str, + environment: str | None = None, 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, + 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 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. + """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 IntegrationAction resource. + Dict containing the updated IntegrationInstance resource. Raises: APIError: If the API request fails. """ - return _update_integration_action( + return _update_integration_instance( self, integration_name, - action_id, + integration_instance_id, + environment=environment, 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, + agent=agent, update_mask=update_mask, api_version=api_version, ) - def execute_integration_action_test( + def execute_integration_instance_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. + """Execute a connectivity test for a specific integration + instance. - 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. + 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 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. + 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 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). + 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_action_test( + return _execute_integration_instance_test( self, integration_name, - test_case_id, - action, - scope, integration_instance_id, api_version=api_version, ) - def get_integration_actions_by_environment( + def get_integration_instance_affected_items( self, integration_name: str, - environments: list[str], - include_widgets: bool, + integration_instance_id: str, api_version: APIVersion | None = APIVersion.V1BETA, ) -> dict[str, Any]: - """List actions executable within specified environments. + """List all playbooks that depend on a specific integration + instance. - Use this method to discover which automated tasks have active - integration instances configured for a particular - network or organizational context. + Use this method to perform impact analysis before deleting or + significantly changing a connection configuration. 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. + 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 IntegrationAction objects that have - integration instances in one of the given environments. + Dict containing a list of AffectedPlaybookResponse objects + that depend on the specified integration instance. Raises: APIError: If the API request fails. """ - return _get_integration_actions_by_environment( + return _get_integration_instance_affected_items( self, integration_name, - environments, - include_widgets, + integration_instance_id, api_version=api_version, ) - def get_integration_action_template( + def get_default_integration_instance( 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. + """Get the system default configuration for a specific + integration. - Use this method to view the history of changes to an action, - enabling version control and the ability to rollback to - previous configurations. + Use this method to retrieve the baseline integration instance + details provided for a commercial product. 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. + integration_name: Name of the integration to fetch the + default instance for. 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. + Dict containing the default IntegrationInstance resource. 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. - - 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, - ) - - # -- 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, - ) - - # -- 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, - ) - - # -- 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( + return _get_default_integration_instance( self, integration_name, - logical_operator_id, - revision_id, api_version=api_version, ) diff --git a/src/secops/chronicle/integration/action_revisions.py b/src/secops/chronicle/integration/action_revisions.py deleted file mode 100644 index b5229b3c..00000000 --- a/src/secops/chronicle/integration/action_revisions.py +++ /dev/null @@ -1,201 +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 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/actions.py b/src/secops/chronicle/integration/actions.py deleted file mode 100644 index d52ba28b..00000000 --- a/src/secops/chronicle/integration/actions.py +++ /dev/null @@ -1,452 +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 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/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/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/manager_revisions.py b/src/secops/chronicle/integration/manager_revisions.py deleted file mode 100644 index 644a8490..00000000 --- a/src/secops/chronicle/integration/manager_revisions.py +++ /dev/null @@ -1,243 +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 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/src/secops/chronicle/integration/managers.py b/src/secops/chronicle/integration/managers.py deleted file mode 100644 index ced5b199..00000000 --- a/src/secops/chronicle/integration/managers.py +++ /dev/null @@ -1,285 +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 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/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/action_revisions.py b/src/secops/cli/commands/integration/action_revisions.py deleted file mode 100644 index d6999bac..00000000 --- a/src/secops/cli/commands/integration/action_revisions.py +++ /dev/null @@ -1,215 +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 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 deleted file mode 100644 index a389c8aa..00000000 --- a/src/secops/cli/commands/integration/actions.py +++ /dev/null @@ -1,382 +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 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 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_client.py b/src/secops/cli/commands/integration/integration_client.py index d84bd383..916c28f3 100644 --- a/src/secops/cli/commands/integration/integration_client.py +++ b/src/secops/cli/commands/integration/integration_client.py @@ -17,25 +17,25 @@ 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, + # 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, + # transformers, + # transformer_revisions, + # logical_operators, + # logical_operator_revisions, ) @@ -51,24 +51,24 @@ 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) + # 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/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/manager_revisions.py b/src/secops/cli/commands/integration/manager_revisions.py deleted file mode 100644 index 82116abe..00000000 --- a/src/secops/cli/commands/integration/manager_revisions.py +++ /dev/null @@ -1,254 +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 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 deleted file mode 100644 index e5f202a0..00000000 --- a/src/secops/cli/commands/integration/managers.py +++ /dev/null @@ -1,283 +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 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) 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_action_revisions.py b/tests/chronicle/integration/test_action_revisions.py deleted file mode 100644 index f9abd9bc..00000000 --- a/tests/chronicle/integration/test_action_revisions.py +++ /dev/null @@ -1,409 +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 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 - diff --git a/tests/chronicle/integration/test_actions.py b/tests/chronicle/integration/test_actions.py deleted file mode 100644 index 6cd0a9ac..00000000 --- a/tests/chronicle/integration/test_actions.py +++ /dev/null @@ -1,666 +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 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/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_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_manager_revisions.py b/tests/chronicle/integration/test_manager_revisions.py deleted file mode 100644 index 7076bd54..00000000 --- a/tests/chronicle/integration/test_manager_revisions.py +++ /dev/null @@ -1,417 +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 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 - diff --git a/tests/chronicle/integration/test_managers.py b/tests/chronicle/integration/test_managers.py deleted file mode 100644 index c6bf5c4a..00000000 --- a/tests/chronicle/integration/test_managers.py +++ /dev/null @@ -1,460 +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 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) - 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", - ) - From a58146c864d40d836e07e1db4e8c31fdd8c28ddb Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Tue, 10 Mar 2026 15:07:00 +0000 Subject: [PATCH 46/47] 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 053e5d2833c173a833f447ec86d72361e0e3be6d Mon Sep 17 00:00:00 2001 From: PaperMtn Date: Thu, 19 Mar 2026 20:25:02 +0000 Subject: [PATCH 47/47] feat: refactor SOARService as a feature namespace in ChronicleClient --- README.md | 2722 +++++++++-------- api_module_mapping.md | 2064 ++++++------- src/secops/chronicle/__init__.py | 38 - src/secops/chronicle/client.py | 1059 +------ src/secops/chronicle/soar/__init__.py | 74 + src/secops/chronicle/soar/client.py | 1093 +++++++ .../{ => soar}/integration/__init__.py | 0 .../integration/integration_instances.py | 0 .../{ => soar}/integration/integrations.py | 0 .../integration/marketplace_integrations.py | 0 .../cli/commands/integration/integration.py | 26 +- .../integration/integration_instances.py | 18 +- .../integration/marketplace_integration.py | 10 +- .../{integration => soar}/__init__.py | 0 .../integration/test_integration_instances.py | 56 +- .../integration/test_integrations.py | 82 +- .../test_marketplace_integrations.py | 40 +- 17 files changed, 3691 insertions(+), 3591 deletions(-) create mode 100644 src/secops/chronicle/soar/__init__.py create mode 100644 src/secops/chronicle/soar/client.py rename src/secops/chronicle/{ => soar}/integration/__init__.py (100%) rename src/secops/chronicle/{ => soar}/integration/integration_instances.py (100%) rename src/secops/chronicle/{ => soar}/integration/integrations.py (100%) rename src/secops/chronicle/{ => soar}/integration/marketplace_integrations.py (100%) rename tests/chronicle/{integration => soar}/__init__.py (100%) rename tests/chronicle/{ => soar}/integration/test_integration_instances.py (88%) rename tests/chronicle/{ => soar}/integration/test_integrations.py (89%) rename tests/chronicle/{ => soar}/integration/test_marketplace_integrations.py (89%) diff --git a/README.md b/README.md index 2229eeed..dba1b272 100644 --- a/README.md +++ b/README.md @@ -1660,13 +1660,13 @@ udm_mapping = chronicle.generate_udm_mapping(log_type="WINDOWS_AD") print(udm_mapping) ``` -## Parser Management +### Parser Management Chronicle parsers are used to process and normalize raw log data into Chronicle's Unified Data Model (UDM) format. Parsers transform various log formats (JSON, XML, CEF, etc.) into a standardized structure that enables consistent querying and analysis across different data sources. The SDK provides comprehensive support for managing Chronicle parsers: -### Creating Parsers +#### Creating Parsers Create new parser: @@ -1712,7 +1712,7 @@ parser_id = parser.get("name", "").split("/")[-1] print(f"Parser ID: {parser_id}") ``` -### Managing Parsers +#### Managing Parsers Retrieve, list, copy, activate/deactivate, and delete parsers: @@ -1756,7 +1756,7 @@ chronicle.activate_release_candidate_parser(log_type=log_type, id="pa_release_ca > **Note:** Parsers work in conjunction with log ingestion. When you ingest logs using `chronicle.ingest_log()`, Chronicle automatically applies the appropriate parser based on the log type to transform your raw logs into UDM format. If you're working with custom log formats, you may need to create or configure custom parsers first. -### Run Parser against sample logs +#### Run Parser against sample logs Run the parser on one or more sample logs: @@ -1846,7 +1846,7 @@ The `run_parser` function includes comprehensive validation: - Enforces size limits (10MB per log, 50MB total, max 1000 logs) - Provides detailed error messages for different failure scenarios -### Complete Parser Workflow Example +#### Complete Parser Workflow Example Here's a complete example that demonstrates retrieving a parser, running it against a log, and ingesting the parsed UDM event: @@ -1900,13 +1900,13 @@ This workflow is useful for: - Re-processing logs with updated parsers - Debugging parsing issues -## Parser Extension +### Parser Extension Parser extensions provide a flexible way to extend the capabilities of existing default (or custom) parsers without replacing them. The extensions let you customize the parser pipeline by adding new parsing logic, extracting and transforming fields, and updating or removing UDM field mappings. The SDK provides comprehensive support for managing Chronicle parser extensions: -### List Parser Extensions +#### List Parser Extensions List parser extensions for a log type: @@ -1917,7 +1917,7 @@ extensions = chronicle.list_parser_extensions(log_type) print(f"Found {len(extensions["parserExtensions"])} parser extensions for log type: {log_type}") ``` -### Create a new parser extension +#### Create a new parser extension Create new parser extension using either CBN snippet, field extractor or dynamic parsing: @@ -1940,7 +1940,7 @@ field_extractor = { chronicle.create_parser_extension(log_type, field_extractor=field_extractor) ``` -### Get parser extension +#### Get parser extension Get parser extension details: @@ -1953,7 +1953,7 @@ extension = chronicle.get_parser_extension(log_type, extension_id) print(extension) ``` -### Activate Parser Extension +#### Activate Parser Extension Activate parser extension: @@ -1964,7 +1964,7 @@ extension_id = "1234567890" chronicle.activate_parser_extension(log_type, extension_id) ``` -### Delete Parser Extension +#### Delete Parser Extension Delete parser extension: @@ -1975,9 +1975,9 @@ extension_id = "1234567890" chronicle.delete_parser_extension(log_type, extension_id) ``` -## Watchlist Management +### Watchlist Management -### Creating a Watchlist +#### Creating a Watchlist Create a new watchlist: @@ -1990,7 +1990,7 @@ watchlist = chronicle.create_watchlist( ) ``` -### Updating a Watchlist +#### Updating a Watchlist Update a watchlist by ID: @@ -2005,7 +2005,7 @@ updated_watchlist = chronicle.update_watchlist( ) ``` -### Deleting a Watchlist +#### Deleting a Watchlist Delete a watchlist by ID: @@ -2013,7 +2013,7 @@ Delete a watchlist by ID: chronicle.delete_watchlist("acb-123-def", force=True) ``` -### Getting a Watchlist +#### Getting a Watchlist Get a watchlist by ID: @@ -2021,7 +2021,7 @@ Get a watchlist by ID: watchlist = chronicle.get_watchlist("acb-123-def") ``` -### List all Watchlists +#### List all Watchlists List all watchlists: @@ -2037,1727 +2037,1751 @@ for watchlist in watchlists: print(f"Watchlist: {watchlist.get('displayName')}") ``` -## Integration Management +### Rule Management -### Marketplace Integrations - -List available marketplace integrations: +The SDK provides comprehensive support for managing Chronicle detection rules: -```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", "") +#### Creating Rules -# 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") +Create new detection rules using YARA-L 2.0 syntax: -# Get all installed integration with updates available -integrations = chronicle.list_marketplace_integrations(filter_string="installed = true AND updateAvailable = true") +```python +rule_text = """ +rule simple_network_rule { + meta: + description = "Example rule to detect network connections" + author = "SecOps SDK Example" + severity = "Medium" + priority = "Medium" + yara_version = "YL2.0" + rule_version = "1.0" + events: + $e.metadata.event_type = "NETWORK_CONNECTION" + $e.principal.hostname != "" + condition: + $e +} +""" -# Specify use of V1 Alpha API version -integrations = chronicle.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) +# Create the rule +rule = chronicle.create_rule(rule_text) +rule_id = rule.get("name", "").split("/")[-1] +print(f"Rule ID: {rule_id}") ``` -Get a specific marketplace integration: - -```python -integration = chronicle.get_marketplace_integration("AWSSecurityHub") -``` +#### Managing Rules -Get the diff between the currently installed version and the latest -available version of an integration: +Retrieve, list, update, enable/disable, and delete rules: ```python -diff = chronicle.get_marketplace_integration_diff("AWSSecurityHub") -``` +# List all rules +rules = chronicle.list_rules() +for rule in rules.get("rules", []): + rule_id = rule.get("name", "").split("/")[-1] + enabled = rule.get("deployment", {}).get("enabled", False) + print(f"Rule ID: {rule_id}, Enabled: {enabled}") -Install or update a marketplace integration: +# List paginated rules and `REVISION_METADATA_ONLY` view +rules = chronicle.list_rules(view="REVISION_METADATA_ONLY",page_size=50) +print(f"Fetched {len(rules.get("rules"))} rules") -```python -# Install an integration with the default settings -integration_name = "AWSSecurityHub" -integration = chronicle.install_marketplace_integration(integration_name) +# Get specific rule +rule = chronicle.get_rule(rule_id) +print(f"Rule content: {rule.get('text')}") -# Install to staging environment and override any existing ontology mappings -integration = chronicle.install_marketplace_integration( - integration_name, - staging=True, - override_ontology_mappings=True -) +# Update rule +updated_rule = chronicle.update_rule(rule_id, updated_rule_text) -# Installing a currently installed integration with no specified version -# number will update it to the latest version -integration = chronicle.install_marketplace_integration(integration_name) +# Enable/disable rule +deployment = chronicle.enable_rule(rule_id, enabled=True) # Enable +deployment = chronicle.enable_rule(rule_id, enabled=False) # Disable -# Or you can specify a specific version to install -integration = chronicle.install_marketplace_integration( - integration_name, - version="5.0" -) +# Delete rule +chronicle.delete_rule(rule_id) ``` -Uninstall a marketplace integration: - -```python -chronicle.uninstall_marketplace_integration("AWSSecurityHub") -``` +#### Rule Deployment -### Integrations -List all installed integrations: +Manage a rule's deployment (enabled/alerting/archive state and run frequency): ```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 current deployment for a rule +deployment = chronicle.get_rule_deployment(rule_id) -# Get all integrations as a list -integrations = chronicle.list_integrations(as_list=True) +# List deployments (paginated) +page = chronicle.list_rule_deployments(page_size=10) -for i in integrations: - integration = chronicle.get_integration(i["identifier"]) - if integration.get("parameters"): - print(json.dumps(integration, indent=2)) +# List deployments with filter +filtered = chronicle.list_rule_deployments(filter_query="enabled=true") +# Update deployment fields (partial updates supported) +chronicle.update_rule_deployment( + rule_id=rule_id, + enabled=True, # continuously execute + alerting=False, # detections do not generate alerts + run_frequency="LIVE" # LIVE | HOURLY | DAILY +) -# Get integrations ordered by display name -integrations = chronicle.list_integrations(order_by="displayName", as_list=True) - ``` +# Archive a rule (must set enabled to False when archived=True) +chronicle.update_rule_deployment( + rule_id=rule_id, + archived=True +) +``` -Get details of a specific integration: +#### Searching Rules + +Search for rules using regular expressions: ```python -integration = chronicle.get_integration("AWSSecurityHub") +# Search for rules containing specific patterns +results = chronicle.search_rules("suspicious process") +for rule in results.get("rules", []): + rule_id = rule.get("name", "").split("/")[-1] + print(f"Rule ID: {rule_id}, contains: 'suspicious process'") + +# Find rules mentioning a specific MITRE technique +mitre_rules = chronicle.search_rules("T1055") +print(f"Found {len(mitre_rules.get('rules', []))} rules mentioning T1055 technique") ``` -Create an integration: +#### Testing Rules -```python -from secops.chronicle.models ( - IntegrationParam, - IntegrationParamType, - IntegrationType, - PythonVersion -) +Test rules against historical data to validate their effectiveness before deployment: -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, -) -``` +```python +from datetime import datetime, timedelta, timezone -Update an integration: +# Define time range for testing +end_time = datetime.now(timezone.utc) +start_time = end_time - timedelta(days=7) # Test against last 7 days -```python -from secops.chronicle.models import IntegrationParam, IntegrationParamType +# Rule to test +rule_text = """ +rule test_rule { + meta: + description = "Test rule for validation" + author = "Test Author" + severity = "Low" + yara_version = "YL2.0" + rule_version = "1.0" + events: + $e.metadata.event_type = "NETWORK_CONNECTION" + condition: + $e +} +""" -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" - ], +# Test the rule +test_results = chronicle.run_rule_test( + rule_text=rule_text, + start_time=start_time, + end_time=end_time, + max_results=100 ) -``` -Delete an integration: +# Process streaming results +detection_count = 0 +for result in test_results: + result_type = result.get("type") + + if result_type == "progress": + # Progress update + percent_done = result.get("percentDone", 0) + print(f"Progress: {percent_done}%") + + elif result_type == "detection": + # Detection result + detection_count += 1 + detection = result.get("detection", {}) + print(f"Detection {detection_count}:") + + # Process detection details + if "rule_id" in detection: + print(f" Rule ID: {detection['rule_id']}") + if "data" in detection: + print(f" Data: {detection['data']}") + + elif result_type == "error": + # Error information + print(f"Error: {result.get('message', 'Unknown error')}") -```python -chronicle.delete_integration("MyNewIntegration") +print(f"Finished testing. Found {detection_count} detection(s).") ``` -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. - +#### Extract just the UDM events for programmatic processing ```python -integration_bytes = chronicle.download_integration("MyIntegration") -with open("MyIntegration.zip", "wb") as f: - f.write(integration_bytes) +udm_events = [] +for result in chronicle.run_rule_test(rule_text, start_time, end_time, max_results=100): + if result.get("type") == "detection": + detection = result.get("detection", {}) + result_events = detection.get("resultEvents", {}) + + for var_name, var_data in result_events.items(): + event_samples = var_data.get("eventSamples", []) + for sample in event_samples: + event = sample.get("event") + if event: + udm_events.append(event) + +# Process the UDM events +for event in udm_events: + # Process each UDM event + metadata = event.get("metadata", {}) + print(f"Event type: {metadata.get('eventType')}") ``` -Export selected items from an integration (e.g. only actions) as a .zip file: +#### Retrohunts + +Run rules against historical data to find past matches: ```python -# Export only actions with IDs 1 and 2 from the integration +from datetime import datetime, timedelta, timezone -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) -``` +# Set time range for retrohunt +end_time = datetime.now(timezone.utc) +start_time = end_time - timedelta(days=7) # Search past 7 days -Get dependencies for an integration: +# Create retrohunt +retrohunt = chronicle.create_retrohunt(rule_id, start_time, end_time) +operation_id = retrohunt.get("name", "").split("/")[-1] -```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}") +# Check retrohunt status +retrohunt_status = chronicle.get_retrohunt(rule_id, operation_id) +state = retrohunt_status.get("state", "") + +# List retrohunts for a rule +retrohunts = chronicle.list_retrohunts(rule_id) ``` -Force dependency update for an integration: +#### Detections and Errors + +Monitor rule detections and execution errors: ```python -# Defining a version: -chronicle.download_integration_dependency( - "MyIntegration", - "boto3==1.42.44" -) +from datetime import datetime, timedelta -# Install the latest version of a dependency: -chronicle.download_integration_dependency( - "MyIntegration", - "boto3" +start_time = datetime.now(timezone.utc) +end_time = start_time - timedelta(days=7) +# List detections for a rule +detections = chronicle.list_detections( + rule_id=rule_id, + start_time=start_time, + end_time=end_time, + list_basis="CREATED_TIME" ) -``` +for detection in detections.get("detections", []): + detection_id = detection.get("id", "") + event_time = detection.get("eventTime", "") + alerting = detection.get("alertState", "") == "ALERTING" + print(f"Detection: {detection_id}, Time: {event_time}, Alerting: {alerting}") -Get remote agents that would be restricted from running an updated version of the integration +# List execution errors for a rule +errors = chronicle.list_errors(rule_id) +for error in errors.get("ruleExecutionErrors", []): + error_message = error.get("error_message", "") + create_time = error.get("create_time", "") + print(f"Error: {error_message}, Time: {create_time}") +``` -```python -from secops.chronicle.models import PythonVersion +#### Rule Alerts -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: +Search for alerts generated by rules: ```python -from secops.chronicle.models import DiffType +# Set time range for alert search +end_time = datetime.now(timezone.utc) +start_time = end_time - timedelta(days=7) # Search past 7 days -# 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 +# Search for rule alerts +alerts_response = chronicle.search_rule_alerts( + start_time=start_time, + end_time=end_time, + page_size=10 ) -# Get the difference between the staging integration and its matching production version. -diff = chronicle.get_integration_diff( - integration_name="AWSSecurityHub", - diff_type=DiffType.PRODUCTION -) +# The API returns a nested structure where alerts are grouped by rule +# Extract and process all alerts from this structure +all_alerts = [] +too_many_alerts = alerts_response.get('tooManyAlerts', False) -# Get the difference between the production integration and its corresponding staging version. -diff = chronicle.get_integration_diff( - integration_name="AWSSecurityHub", - diff_type=DiffType.STAGING -) +# Process the nested response structure - alerts are grouped by rule +for rule_alert in alerts_response.get('ruleAlerts', []): + # Extract rule metadata + rule_metadata = rule_alert.get('ruleMetadata', {}) + rule_id = rule_metadata.get('properties', {}).get('ruleId', 'Unknown') + rule_name = rule_metadata.get('properties', {}).get('name', 'Unknown') + + # Get alerts for this rule + rule_alerts = rule_alert.get('alerts', []) + + # Process each alert + for alert in rule_alerts: + # Extract important fields + alert_id = alert.get("id", "") + detection_time = alert.get("detectionTimestamp", "") + commit_time = alert.get("commitTimestamp", "") + alerting_type = alert.get("alertingType", "") + + print(f"Alert ID: {alert_id}") + print(f"Rule ID: {rule_id}") + print(f"Rule Name: {rule_name}") + print(f"Detection Time: {detection_time}") + + # Extract events from the alert + if 'resultEvents' in alert: + for var_name, event_data in alert.get('resultEvents', {}).items(): + if 'eventSamples' in event_data: + for sample in event_data.get('eventSamples', []): + if 'event' in sample: + event = sample['event'] + # Process event data + event_type = event.get('metadata', {}).get('eventType', 'Unknown') + print(f"Event Type: {event_type}") ``` -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 -) -``` +If `tooManyAlerts` is True in the response, consider narrowing your search criteria using a smaller time window or more specific filters. -### Integration Instances +#### Curated Rule Sets -List all instances for a specific integration: +Query curated rules: ```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) +# List all curated rules (returns dict with pagination metadata) +result = chronicle.list_curated_rules() +for rule in result.get("curatedRules", []): + rule_id = rule.get("name", "").split("/")[-1] + display_name = rule.get("description") + description = rule.get("description") + print(f"Rule: {display_name}, Description: {description}") -# Get instances for a specific environment -instances = chronicle.list_integration_instances( - "MyIntegration", - filter_string="environment = 'production'" -) -``` +# List all curated rules as a direct list +rules = chronicle.list_curated_rules(as_list=True) +for rule in rules: + rule_id = rule.get("name", "").split("/")[-1] + display_name = rule.get("description") + print(f"Rule: {display_name}") -Get details of a specific integration instance: +# Get a curated rule +rule = chronicle.get_curated_rule("ur_ttp_lol_Atbroker") -```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')}") +# Get a curated rule set by display name +# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets. +rule_set = chronicle.get_curated_rule_by_name("Atbroker.exe Abuse") ``` -Create a new integration instance: +Search for curated rules detections: ```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: +from datetime import datetime, timedelta, timezone +from secops.chronicle.models import AlertState, ListBasis -```python -from secops.chronicle.models import IntegrationInstanceParameter +# Search for detections from a specific curated rule +end_time = datetime.now(timezone.utc) +start_time = end_time - timedelta(days=7) -# Update instance display name -updated_instance = chronicle.update_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1", - display_name="Updated Production Instance" +result = chronicle.search_curated_detections( + rule_id="ur_ttp_GCP_MassSecretDeletion", + start_time=start_time, + end_time=end_time, + list_basis=ListBasis.DETECTION_TIME, + alert_state=AlertState.ALERTING, + page_size=100 ) -# 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" - ) - ] -) +detections = result.get("curatedDetections", []) +print(f"Found {len(detections)} detections") -# Use custom update mask -updated_instance = chronicle.update_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1", - display_name="New Name", - update_mask="displayName" -) +# Check if more results are available +if "nextPageToken" in result: + # Retrieve next page + next_result = chronicle.search_curated_detections( + rule_id="ur_ttp_GCP_MassSecretDeletion", + start_time=start_time, + end_time=end_time, + list_basis=ListBasis.DETECTION_TIME, + page_token=result["nextPageToken"], + page_size=100 + ) ``` -Delete an integration instance: +Query curated rule sets: ```python -chronicle.delete_integration_instance( - integration_name="MyIntegration", - integration_instance_id="ii1" -) -``` +# List all curated rule sets (returns dict with pagination metadata) +result = chronicle.list_curated_rule_sets() +for rule_set in result.get("curatedRuleSets", []): + rule_set_id = rule_set.get("name", "").split("/")[-1] + display_name = rule_set.get("displayName") + print(f"Rule Set: {display_name}, ID: {rule_set_id}") -Execute a connectivity test for an integration instance: +# List all curated rule sets as a direct list +rule_sets = chronicle.list_curated_rule_sets(as_list=True) +for rule_set in rule_sets: + rule_set_id = rule_set.get("name", "").split("/")[-1] + display_name = rule_set.get("displayName") + print(f"Rule Set: {display_name}, ID: {rule_set_id}") -```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 a curated rule set by ID +rule_set = chronicle.get_curated_rule_set("00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93") ``` -Get affected items (playbooks) that depend on an integration instance: +Query curated rule set categories: ```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')}") -``` +# List all curated rule set categories (returns dict with pagination metadata) +result = chronicle.list_curated_rule_set_categories() +for rule_set_category in result.get("curatedRuleSetCategories", []): + rule_set_category_id = rule_set_category.get("name", "").split("/")[-1] + display_name = rule_set_category.get("displayName") + print(f"Rule Set Category: {display_name}, ID: {rule_set_category_id}") -Get the default integration instance: +# List all curated rule set categories as a direct list +rule_set_categories = chronicle.list_curated_rule_set_categories(as_list=True) +for rule_set_category in rule_set_categories: + rule_set_category_id = rule_set_category.get("name", "").split("/")[-1] + display_name = rule_set_category.get("displayName") + print(f"Rule Set Category: {display_name}, ID: {rule_set_category_id}") -```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')}") +# Get a curated rule set category by ID +rule_set_category = chronicle.get_curated_rule_set_category("110fa43d-7165-2355-1985-a63b7cdf90e8") ``` -## Rule Management - -The SDK provides comprehensive support for managing Chronicle detection rules: +Manage curated rule set deployments (turn alerting on or off (either precise or broad) for curated rule sets): -### Creating Rules +```python +# List all curated rule set deployments (returns dict with pagination metadata) +result = chronicle.list_curated_rule_set_deployments() +for rs_deployment in result.get("curatedRuleSetDeployments", []): + rule_set_id = rs_deployment.get("name", "").split("/")[-3] + category_id = rs_deployment.get("name", "").split("/")[-5] + deployment_status = rs_deployment.get("name", "").split("/")[-1] + display_name = rs_deployment.get("displayName") + alerting = rs_deployment.get("alerting", False) + print( + f"Rule Set: {display_name}," + f"Rule Set ID: {rule_set_id}", + f"Category ID: {category_id}", + f"Precision: {deployment_status}", + f"Alerting: {alerting}", + ) -Create new detection rules using YARA-L 2.0 syntax: +# List all curated rule set deployments as a direct list +rule_set_deployments = chronicle.list_curated_rule_set_deployments(as_list=True) +for rs_deployment in rule_set_deployments: + rule_set_id = rs_deployment.get("name", "").split("/")[-3] + display_name = rs_deployment.get("displayName") + print(f"Rule Set: {display_name}, ID: {rule_set_id}") + +# Get curated rule set deployment by ID +rule_set_deployment = chronicle.get_curated_rule_set_deployment("00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93") + +# Get curated rule set deployment by rule set display name +# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets. +rule_set_deployment = chronicle.get_curated_rule_set_deployment_by_name("Azure - Network") + +# Update multiple curated rule set deployments +# Define deployments for rule sets +deployments = [ + { + "category_id": "category-uuid", + "rule_set_id": "ruleset-uuid", + "precision": "broad", + "enabled": True, + "alerting": False + } +] + +chronicle.batch_update_curated_rule_set_deployments(deployments) + +# Update a single curated rule set deployment +chronicle.update_curated_rule_set_deployment( + category_id="category-uuid", + rule_set_id="ruleset-uuid", + precision="broad", + enabled=True, + alerting=False +) + +``` + +#### Rule Validation + +Validate a YARA-L2 rule before creating or updating it: ```python +# Example rule rule_text = """ -rule simple_network_rule { +rule test_rule { meta: - description = "Example rule to detect network connections" - author = "SecOps SDK Example" - severity = "Medium" - priority = "Medium" + description = "Test rule for validation" + author = "Test Author" + severity = "Low" yara_version = "YL2.0" rule_version = "1.0" events: $e.metadata.event_type = "NETWORK_CONNECTION" - $e.principal.hostname != "" condition: $e } """ -# Create the rule -rule = chronicle.create_rule(rule_text) -rule_id = rule.get("name", "").split("/")[-1] -print(f"Rule ID: {rule_id}") +# Validate the rule +result = chronicle.validate_rule(rule_text) + +if result.success: + print("Rule is valid") +else: + print(f"Rule is invalid: {result.message}") + if result.position: + print(f"Error at line {result.position['startLine']}, column {result.position['startColumn']}") ``` -### Managing Rules +#### Rule Exclusions -Retrieve, list, update, enable/disable, and delete rules: +Rule Exclusions allow you to exclude specific events from triggering detections in Chronicle. They are useful for filtering out known false positives or excluding test/development traffic from production detections. ```python -# List all rules -rules = chronicle.list_rules() -for rule in rules.get("rules", []): - rule_id = rule.get("name", "").split("/")[-1] - enabled = rule.get("deployment", {}).get("enabled", False) - print(f"Rule ID: {rule_id}, Enabled: {enabled}") +from secops.chronicle.rule_exclusion import RuleExclusionType +from datetime import datetime, timedelta -# List paginated rules and `REVISION_METADATA_ONLY` view -rules = chronicle.list_rules(view="REVISION_METADATA_ONLY",page_size=50) -print(f"Fetched {len(rules.get("rules"))} rules") +# Create a new rule exclusion +exclusion = chronicle.create_rule_exclusion( + display_name="Exclusion Display Name", + refinement_type=RuleExclusionType.DETECTION_EXCLUSION, + query='(domain = "google.com")' +) -# Get specific rule -rule = chronicle.get_rule(rule_id) -print(f"Rule content: {rule.get('text')}") +# Get exclusion id from name +exclusion_id = exclusion["name"].split("/")[-1] -# Update rule -updated_rule = chronicle.update_rule(rule_id, updated_rule_text) +# Get details of a specific rule exclusion +exclusion_details = chronicle.get_rule_exclusion(exclusion_id) +print(f"Exclusion: {exclusion_details.get('display_name')}") +print(f"Query: {exclusion_details.get('query')}") -# Enable/disable rule -deployment = chronicle.enable_rule(rule_id, enabled=True) # Enable -deployment = chronicle.enable_rule(rule_id, enabled=False) # Disable +# List all rule exclusions with pagination +exclusions = chronicle.list_rule_exclusions(page_size=10) +for exclusion in exclusions.get("findingsRefinements", []): + print(f"- {exclusion.get('display_name')}: {exclusion.get('name')}") -# Delete rule -chronicle.delete_rule(rule_id) +# Update an existing exclusion +updated = chronicle.patch_rule_exclusion( + exclusion_id=exclusion_id, + display_name="Updated Exclusion", + query='(ip = "8.8.8.8")', + update_mask="display_name,query" +) + +# Manage deployment settings +chronicle.update_rule_exclusion_deployment( + exclusion_id, + enabled=True, + archived=False, + detection_exclusion_application={ + "curatedRules": [], + "curatedRuleSets": [], + "rules": [], + } +) + +# Compute rule exclusion Activity for provided time period +end_time = datetime.utcnow() +start_time = end_time - timedelta(days=7) + +activity = chronicle.compute_rule_exclusion_activity( + exclusion_id, + start_time=start_time, + end_time=end_time +) ``` -### Rule Deployment +#### Featured Content Rules -Manage a rule's deployment (enabled/alerting/archive state and run frequency): +Featured content rules are pre-built detection rules available in the Chronicle Content Hub. These curated rules help you quickly deploy detections without writing custom rules. ```python -# Get current deployment for a rule -deployment = chronicle.get_rule_deployment(rule_id) - -# List deployments (paginated) -page = chronicle.list_rule_deployments(page_size=10) +# List all featured content rules +rules = chronicle.list_featured_content_rules() +for rule in rules.get("featuredContentRules", []): + rule_id = rule.get("name", "").split("/")[-1] + content_metadata = rule.get("contentMetadata", {}) + display_name = content_metadata.get("displayName", "Unknown") + severity = rule.get("severity", "UNSPECIFIED") + print(f"Rule: {display_name} [{rule_id}] - Severity: {severity}") -# List deployments with filter -filtered = chronicle.list_rule_deployments(filter_query="enabled=true") +# List with pagination +result = chronicle.list_featured_content_rules(page_size=10) +rules = result.get("featuredContentRules", []) +next_page_token = result.get("nextPageToken") -# Update deployment fields (partial updates supported) -chronicle.update_rule_deployment( - rule_id=rule_id, - enabled=True, # continuously execute - alerting=False, # detections do not generate alerts - run_frequency="LIVE" # LIVE | HOURLY | DAILY -) +if next_page_token: + next_page = chronicle.list_featured_content_rules( + page_size=10, + page_token=next_page_token + ) -# Archive a rule (must set enabled to False when archived=True) -chronicle.update_rule_deployment( - rule_id=rule_id, - archived=True +# Filter list +filtered_rules = chronicle.list_featured_content_rules( + filter_expression=( + 'category_name:"Threat Detection" AND ' + 'rule_precision:"Precise"' + ) ) ``` -### Searching Rules +### Data Tables and Reference Lists -Search for rules using regular expressions: +Chronicle provides two ways to manage and reference structured data in detection rules: Data Tables and Reference Lists. These can be used to maintain lists of trusted/suspicious entities, mappings of contextual information, or any other structured data useful for detection. -```python -# Search for rules containing specific patterns -results = chronicle.search_rules("suspicious process") -for rule in results.get("rules", []): - rule_id = rule.get("name", "").split("/")[-1] - print(f"Rule ID: {rule_id}, contains: 'suspicious process'") - -# Find rules mentioning a specific MITRE technique -mitre_rules = chronicle.search_rules("T1055") -print(f"Found {len(mitre_rules.get('rules', []))} rules mentioning T1055 technique") -``` +#### Data Tables -### Testing Rules +Data Tables are collections of structured data with defined columns and data types. They can be referenced in detection rules to enhance your detections with additional context. -Test rules against historical data to validate their effectiveness before deployment: +#### Creating Data Tables ```python -from datetime import datetime, timedelta, timezone +from secops.chronicle.data_table import DataTableColumnType -# Define time range for testing -end_time = datetime.now(timezone.utc) -start_time = end_time - timedelta(days=7) # Test against last 7 days +# Create a data table with different column types +data_table = chronicle.create_data_table( + name="suspicious_ips", + description="Known suspicious IP addresses with context", + header={ + "ip_address": DataTableColumnType.CIDR, + # Alternately, you can map a column to an entity proto field + # See: https://cloud.google.com/chronicle/docs/investigation/data-tables#map_column_names_to_entity_fields_optional + # "ip_address": "entity.asset.ip" + "port": DataTableColumnType.NUMBER, + "severity": DataTableColumnType.STRING, + "description": DataTableColumnType.STRING + }, + # Optional: Set additional column options (valid options: repeatedValues, keyColumns) + column_options: {"ip_address": {"repeatedValues": True}}, + # Optional: Add initial rows + rows=[ + ["192.168.1.100", 3232, "High", "Scanning activity"], + ["10.0.0.5", 9000, "Medium", "Suspicious login attempts"] + ] +) -# Rule to test -rule_text = """ -rule test_rule { - meta: - description = "Test rule for validation" - author = "Test Author" - severity = "Low" - yara_version = "YL2.0" - rule_version = "1.0" - events: - $e.metadata.event_type = "NETWORK_CONNECTION" - condition: - $e -} -""" +print(f"Created table: {data_table['name']}") +``` -# Test the rule -test_results = chronicle.run_rule_test( - rule_text=rule_text, - start_time=start_time, - end_time=end_time, - max_results=100 -) - -# Process streaming results -detection_count = 0 -for result in test_results: - result_type = result.get("type") - - if result_type == "progress": - # Progress update - percent_done = result.get("percentDone", 0) - print(f"Progress: {percent_done}%") - - elif result_type == "detection": - # Detection result - detection_count += 1 - detection = result.get("detection", {}) - print(f"Detection {detection_count}:") - - # Process detection details - if "rule_id" in detection: - print(f" Rule ID: {detection['rule_id']}") - if "data" in detection: - print(f" Data: {detection['data']}") - - elif result_type == "error": - # Error information - print(f"Error: {result.get('message', 'Unknown error')}") - -print(f"Finished testing. Found {detection_count} detection(s).") -``` - -# Extract just the UDM events for programmatic processing -```python -udm_events = [] -for result in chronicle.run_rule_test(rule_text, start_time, end_time, max_results=100): - if result.get("type") == "detection": - detection = result.get("detection", {}) - result_events = detection.get("resultEvents", {}) - - for var_name, var_data in result_events.items(): - event_samples = var_data.get("eventSamples", []) - for sample in event_samples: - event = sample.get("event") - if event: - udm_events.append(event) - -# Process the UDM events -for event in udm_events: - # Process each UDM event - metadata = event.get("metadata", {}) - print(f"Event type: {metadata.get('eventType')}") -``` - -### Retrohunts - -Run rules against historical data to find past matches: +#### Managing Data Tables ```python -from datetime import datetime, timedelta, timezone - -# Set time range for retrohunt -end_time = datetime.now(timezone.utc) -start_time = end_time - timedelta(days=7) # Search past 7 days - -# Create retrohunt -retrohunt = chronicle.create_retrohunt(rule_id, start_time, end_time) -operation_id = retrohunt.get("name", "").split("/")[-1] - -# Check retrohunt status -retrohunt_status = chronicle.get_retrohunt(rule_id, operation_id) -state = retrohunt_status.get("state", "") - -# List retrohunts for a rule -retrohunts = chronicle.list_retrohunts(rule_id) -``` - -### Detections and Errors +# List all data tables +tables = chronicle.list_data_tables() +for table in tables: + table_id = table["name"].split("/")[-1] + print(f"Table: {table_id}, Created: {table.get('createTime')}") -Monitor rule detections and execution errors: +# Get a specific data table's details +table_details = chronicle.get_data_table("suspicious_ips") +print(f"Column count: {len(table_details.get('columnInfo', []))}") -```python -from datetime import datetime, timedelta +# Update a data table's properties +updated_table = chronicle.update_data_table( + "suspicious_ips", + description="Updated description for suspicious IPs", + row_time_to_live="72h" # Set TTL for rows to 72 hours + update_mask=["description", "row_time_to_live"] +) +print(f"Updated data table: {updated_table['name']}") -start_time = datetime.now(timezone.utc) -end_time = start_time - timedelta(days=7) -# List detections for a rule -detections = chronicle.list_detections( - rule_id=rule_id, - start_time=start_time, - end_time=end_time, - list_basis="CREATED_TIME" +# Add rows to a data table +chronicle.create_data_table_rows( + "suspicious_ips", + [ + ["172.16.0.1", "Low", "Unusual outbound connection"], + ["192.168.2.200", "Critical", "Data exfiltration attempt"] + ] ) -for detection in detections.get("detections", []): - detection_id = detection.get("id", "") - event_time = detection.get("eventTime", "") - alerting = detection.get("alertState", "") == "ALERTING" - print(f"Detection: {detection_id}, Time: {event_time}, Alerting: {alerting}") -# List execution errors for a rule -errors = chronicle.list_errors(rule_id) -for error in errors.get("ruleExecutionErrors", []): - error_message = error.get("error_message", "") - create_time = error.get("create_time", "") - print(f"Error: {error_message}, Time: {create_time}") -``` +# List rows in a data table +rows = chronicle.list_data_table_rows("suspicious_ips") +for row in rows: + row_id = row["name"].split("/")[-1] + values = row.get("values", []) + print(f"Row {row_id}: {values}") -### Rule Alerts +# Delete specific rows by ID +row_ids = [rows[0]["name"].split("/")[-1], rows[1]["name"].split("/")[-1]] +chronicle.delete_data_table_rows("suspicious_ips", row_ids) -Search for alerts generated by rules: +# Replace all rows in a data table with new rows +chronicle.replace_data_table_rows( + name="suspicious_ips", # Data table Name + rows=[ + ["192.168.100.1", "Critical", "Active scanning"], + ["10.1.1.5", "High", "Brute force attempts"], + ["172.16.5.10", "Medium", "Suspicious traffic"] + ] +) -```python -# Set time range for alert search -end_time = datetime.now(timezone.utc) -start_time = end_time - timedelta(days=7) # Search past 7 days +# Bulk update rows in a data table +row_updates = [ + { + "name": "projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row123", # Full resource name + "values": ["192.168.100.1", "Critical", "Updated description"] + }, + { + "name": "projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row456", # Full resource name + "values": ["10.1.1.5", "High", "Updated brute force info"], + "update_mask": "values" # Optional: only update values field + } +] -# Search for rule alerts -alerts_response = chronicle.search_rule_alerts( - start_time=start_time, - end_time=end_time, - page_size=10 +# Execute bulk update +chronicle.update_data_table_rows( + name="suspicious_ips", + row_updates=row_updates ) -# The API returns a nested structure where alerts are grouped by rule -# Extract and process all alerts from this structure -all_alerts = [] -too_many_alerts = alerts_response.get('tooManyAlerts', False) - -# Process the nested response structure - alerts are grouped by rule -for rule_alert in alerts_response.get('ruleAlerts', []): - # Extract rule metadata - rule_metadata = rule_alert.get('ruleMetadata', {}) - rule_id = rule_metadata.get('properties', {}).get('ruleId', 'Unknown') - rule_name = rule_metadata.get('properties', {}).get('name', 'Unknown') - - # Get alerts for this rule - rule_alerts = rule_alert.get('alerts', []) - - # Process each alert - for alert in rule_alerts: - # Extract important fields - alert_id = alert.get("id", "") - detection_time = alert.get("detectionTimestamp", "") - commit_time = alert.get("commitTimestamp", "") - alerting_type = alert.get("alertingType", "") - - print(f"Alert ID: {alert_id}") - print(f"Rule ID: {rule_id}") - print(f"Rule Name: {rule_name}") - print(f"Detection Time: {detection_time}") - - # Extract events from the alert - if 'resultEvents' in alert: - for var_name, event_data in alert.get('resultEvents', {}).items(): - if 'eventSamples' in event_data: - for sample in event_data.get('eventSamples', []): - if 'event' in sample: - event = sample['event'] - # Process event data - event_type = event.get('metadata', {}).get('eventType', 'Unknown') - print(f"Event Type: {event_type}") +# Delete a data table +chronicle.delete_data_table("suspicious_ips", force=True) # force=True deletes even if it has rows ``` -If `tooManyAlerts` is True in the response, consider narrowing your search criteria using a smaller time window or more specific filters. - -### Curated Rule Sets - -Query curated rules: - -```python -# List all curated rules (returns dict with pagination metadata) -result = chronicle.list_curated_rules() -for rule in result.get("curatedRules", []): - rule_id = rule.get("name", "").split("/")[-1] - display_name = rule.get("description") - description = rule.get("description") - print(f"Rule: {display_name}, Description: {description}") - -# List all curated rules as a direct list -rules = chronicle.list_curated_rules(as_list=True) -for rule in rules: - rule_id = rule.get("name", "").split("/")[-1] - display_name = rule.get("description") - print(f"Rule: {display_name}") - -# Get a curated rule -rule = chronicle.get_curated_rule("ur_ttp_lol_Atbroker") +#### Reference Lists -# Get a curated rule set by display name -# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets. -rule_set = chronicle.get_curated_rule_by_name("Atbroker.exe Abuse") -``` +Reference Lists are simple lists of values (strings, CIDR blocks, or regex patterns) that can be referenced in detection rules. They are useful for maintaining whitelists, blacklists, or any other categorized sets of values. -Search for curated rules detections: +##### Creating Reference Lists ```python -from datetime import datetime, timedelta, timezone -from secops.chronicle.models import AlertState, ListBasis - -# Search for detections from a specific curated rule -end_time = datetime.now(timezone.utc) -start_time = end_time - timedelta(days=7) +from secops.chronicle.reference_list import ReferenceListSyntaxType, ReferenceListView -result = chronicle.search_curated_detections( - rule_id="ur_ttp_GCP_MassSecretDeletion", - start_time=start_time, - end_time=end_time, - list_basis=ListBasis.DETECTION_TIME, - alert_state=AlertState.ALERTING, - page_size=100 +# Create a reference list with string entries +string_list = chronicle.create_reference_list( + name="admin_accounts", + description="Administrative user accounts", + entries=["admin", "administrator", "root", "system"], + syntax_type=ReferenceListSyntaxType.STRING ) -detections = result.get("curatedDetections", []) -print(f"Found {len(detections)} detections") - -# Check if more results are available -if "nextPageToken" in result: - # Retrieve next page - next_result = chronicle.search_curated_detections( - rule_id="ur_ttp_GCP_MassSecretDeletion", - start_time=start_time, - end_time=end_time, - list_basis=ListBasis.DETECTION_TIME, - page_token=result["nextPageToken"], - page_size=100 - ) -``` - -Query curated rule sets: - -```python -# List all curated rule sets (returns dict with pagination metadata) -result = chronicle.list_curated_rule_sets() -for rule_set in result.get("curatedRuleSets", []): - rule_set_id = rule_set.get("name", "").split("/")[-1] - display_name = rule_set.get("displayName") - print(f"Rule Set: {display_name}, ID: {rule_set_id}") - -# List all curated rule sets as a direct list -rule_sets = chronicle.list_curated_rule_sets(as_list=True) -for rule_set in rule_sets: - rule_set_id = rule_set.get("name", "").split("/")[-1] - display_name = rule_set.get("displayName") - print(f"Rule Set: {display_name}, ID: {rule_set_id}") - -# Get a curated rule set by ID -rule_set = chronicle.get_curated_rule_set("00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93") -``` - -Query curated rule set categories: - -```python -# List all curated rule set categories (returns dict with pagination metadata) -result = chronicle.list_curated_rule_set_categories() -for rule_set_category in result.get("curatedRuleSetCategories", []): - rule_set_category_id = rule_set_category.get("name", "").split("/")[-1] - display_name = rule_set_category.get("displayName") - print(f"Rule Set Category: {display_name}, ID: {rule_set_category_id}") +print(f"Created reference list: {string_list['name']}") -# List all curated rule set categories as a direct list -rule_set_categories = chronicle.list_curated_rule_set_categories(as_list=True) -for rule_set_category in rule_set_categories: - rule_set_category_id = rule_set_category.get("name", "").split("/")[-1] - display_name = rule_set_category.get("displayName") - print(f"Rule Set Category: {display_name}, ID: {rule_set_category_id}") +# Create a reference list with CIDR entries +cidr_list = chronicle.create_reference_list( + name="trusted_networks", + description="Internal network ranges", + entries=["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"], + syntax_type=ReferenceListSyntaxType.CIDR +) -# Get a curated rule set category by ID -rule_set_category = chronicle.get_curated_rule_set_category("110fa43d-7165-2355-1985-a63b7cdf90e8") +# Create a reference list with regex patterns +regex_list = chronicle.create_reference_list( + name="email_patterns", + description="Email patterns to watch for", + entries=[".*@suspicious\\.com", "malicious_.*@.*\\.org"], + syntax_type=ReferenceListSyntaxType.REGEX +) ``` -Manage curated rule set deployments (turn alerting on or off (either precise or broad) for curated rule sets): +##### Managing Reference Lists ```python -# List all curated rule set deployments (returns dict with pagination metadata) -result = chronicle.list_curated_rule_set_deployments() -for rs_deployment in result.get("curatedRuleSetDeployments", []): - rule_set_id = rs_deployment.get("name", "").split("/")[-3] - category_id = rs_deployment.get("name", "").split("/")[-5] - deployment_status = rs_deployment.get("name", "").split("/")[-1] - display_name = rs_deployment.get("displayName") - alerting = rs_deployment.get("alerting", False) - print( - f"Rule Set: {display_name}," - f"Rule Set ID: {rule_set_id}", - f"Category ID: {category_id}", - f"Precision: {deployment_status}", - f"Alerting: {alerting}", - ) - -# List all curated rule set deployments as a direct list -rule_set_deployments = chronicle.list_curated_rule_set_deployments(as_list=True) -for rs_deployment in rule_set_deployments: - rule_set_id = rs_deployment.get("name", "").split("/")[-3] - display_name = rs_deployment.get("displayName") - print(f"Rule Set: {display_name}, ID: {rule_set_id}") - -# Get curated rule set deployment by ID -rule_set_deployment = chronicle.get_curated_rule_set_deployment("00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93") +# List all reference lists (basic view without entries) +lists = chronicle.list_reference_lists(view=ReferenceListView.BASIC) +for ref_list in lists: + list_id = ref_list["name"].split("/")[-1] + print(f"List: {list_id}, Description: {ref_list.get('description')}") -# Get curated rule set deployment by rule set display name -# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets. -rule_set_deployment = chronicle.get_curated_rule_set_deployment_by_name("Azure - Network") - -# Update multiple curated rule set deployments -# Define deployments for rule sets -deployments = [ - { - "category_id": "category-uuid", - "rule_set_id": "ruleset-uuid", - "precision": "broad", - "enabled": True, - "alerting": False - } -] +# Get a specific reference list including all entries +admin_list = chronicle.get_reference_list("admin_accounts", view=ReferenceListView.FULL) +entries = [entry.get("value") for entry in admin_list.get("entries", [])] +print(f"Admin accounts: {entries}") -chronicle.batch_update_curated_rule_set_deployments(deployments) +# Update reference list entries +chronicle.update_reference_list( + name="admin_accounts", + entries=["admin", "administrator", "root", "system", "superuser"] +) -# Update a single curated rule set deployment -chronicle.update_curated_rule_set_deployment( - category_id="category-uuid", - rule_set_id="ruleset-uuid", - precision="broad", - enabled=True, - alerting=False +# Update reference list description +chronicle.update_reference_list( + name="admin_accounts", + description="Updated administrative user accounts list" ) ``` -### Rule Validation +#### Using in YARA-L Rules -Validate a YARA-L2 rule before creating or updating it: +Both Data Tables and Reference Lists can be referenced in YARA-L detection rules. -```python -# Example rule -rule_text = """ -rule test_rule { +##### Using Data Tables in Rules + +``` +rule detect_with_data_table { meta: - description = "Test rule for validation" - author = "Test Author" - severity = "Low" + description = "Detect connections to suspicious IPs" + author = "SecOps SDK Example" + severity = "Medium" yara_version = "YL2.0" - rule_version = "1.0" events: $e.metadata.event_type = "NETWORK_CONNECTION" + $e.target.ip != "" + $lookup in data_table.suspicious_ips + $lookup.ip_address = $e.target.ip + $severity = $lookup.severity + condition: - $e + $e and $lookup and $severity = "High" } -""" +``` -# Validate the rule -result = chronicle.validate_rule(rule_text) +#### Using Reference Lists in Rules -if result.success: - print("Rule is valid") -else: - print(f"Rule is invalid: {result.message}") - if result.position: - print(f"Error at line {result.position['startLine']}, column {result.position['startColumn']}") +``` +rule detect_with_reference_list { + meta: + description = "Detect admin account usage from untrusted networks" + author = "SecOps SDK Example" + severity = "High" + yara_version = "YL2.0" + events: + $login.metadata.event_type = "USER_LOGIN" + $login.principal.user.userid in reference_list.admin_accounts + not $login.principal.ip in reference_list.trusted_networks + + condition: + $login +} ``` -### Rule Exclusions +### Gemini AI -Rule Exclusions allow you to exclude specific events from triggering detections in Chronicle. They are useful for filtering out known false positives or excluding test/development traffic from production detections. +You can use Chronicle's Gemini AI to get security insights, generate detection rules, explain security concepts, and more: + +> **Note:** Only enterprise tier users have access to Advanced Gemini features. Users must opt-in to use Gemini in Chronicle before accessing this functionality. +The SDK will automatically attempt to opt you in when you first use the Gemini functionality. If the automatic opt-in fails due to permission issues, +you'll see an error message that includes "users must opt-in before using Gemini." ```python -from secops.chronicle.rule_exclusion import RuleExclusionType -from datetime import datetime, timedelta +# Query Gemini with a security question +response = chronicle.gemini("What is Windows event ID 4625?") -# Create a new rule exclusion -exclusion = chronicle.create_rule_exclusion( - display_name="Exclusion Display Name", - refinement_type=RuleExclusionType.DETECTION_EXCLUSION, - query='(domain = "google.com")' -) +# Get text content (combines TEXT blocks and stripped HTML content) +text_explanation = response.get_text_content() +print("Explanation:", text_explanation) -# Get exclusion id from name -exclusion_id = exclusion["name"].split("/")[-1] +# Work with different content blocks +for block in response.blocks: + print(f"Block type: {block.block_type}") + if block.block_type == "TEXT": + print("Text content:", block.content) + elif block.block_type == "CODE": + print(f"Code ({block.title}):", block.content) + elif block.block_type == "HTML": + print("HTML content (with tags):", block.content) -# Get details of a specific rule exclusion -exclusion_details = chronicle.get_rule_exclusion(exclusion_id) -print(f"Exclusion: {exclusion_details.get('display_name')}") -print(f"Query: {exclusion_details.get('query')}") +# Get all code blocks +code_blocks = response.get_code_blocks() +for code_block in code_blocks: + print(f"Code block ({code_block.title}):", code_block.content) -# List all rule exclusions with pagination -exclusions = chronicle.list_rule_exclusions(page_size=10) -for exclusion in exclusions.get("findingsRefinements", []): - print(f"- {exclusion.get('display_name')}: {exclusion.get('name')}") +# Get all HTML blocks (with HTML tags preserved) +html_blocks = response.get_html_blocks() +for html_block in html_blocks: + print(f"HTML block (with tags):", html_block.content) -# Update an existing exclusion -updated = chronicle.patch_rule_exclusion( - exclusion_id=exclusion_id, - display_name="Updated Exclusion", - query='(ip = "8.8.8.8")', - update_mask="display_name,query" -) +# Check for references +if response.references: + print(f"Found {len(response.references)} references") -# Manage deployment settings -chronicle.update_rule_exclusion_deployment( - exclusion_id, - enabled=True, - archived=False, - detection_exclusion_application={ - "curatedRules": [], - "curatedRuleSets": [], - "rules": [], - } -) +# Check for suggested actions +for action in response.suggested_actions: + print(f"Suggested action: {action.display_text} ({action.action_type})") + if action.navigation: + print(f"Action URI: {action.navigation.target_uri}") +``` -# Compute rule exclusion Activity for provided time period -end_time = datetime.utcnow() -start_time = end_time - timedelta(days=7) +#### Response Content Methods -activity = chronicle.compute_rule_exclusion_activity( - exclusion_id, - start_time=start_time, - end_time=end_time -) +The `GeminiResponse` class provides several methods to work with response content: + +- `get_text_content()`: Returns a combined string of all TEXT blocks plus the text content from HTML blocks with HTML tags removed +- `get_code_blocks()`: Returns a list of blocks with `block_type == "CODE"` +- `get_html_blocks()`: Returns a list of blocks with `block_type == "HTML"` (HTML tags preserved) +- `get_raw_response()`: Returns the complete, unprocessed API response as a dictionary + +These methods help you work with different types of content in a structured way. + +#### Accessing Raw API Response + +For advanced use cases or debugging, you can access the raw API response: + +```python +# Get the complete raw API response +response = chronicle.gemini("What is Windows event ID 4625?") +raw_response = response.get_raw_response() + +# Now you can access any part of the original JSON structure +print(json.dumps(raw_response, indent=2)) + +# Example of navigating the raw response structure +if "responses" in raw_response: + for resp in raw_response["responses"]: + if "blocks" in resp: + print(f"Found {len(resp['blocks'])} blocks in raw response") ``` -### Featured Content Rules +This gives you direct access to the original API response format, which can be useful for accessing advanced features or troubleshooting. -Featured content rules are pre-built detection rules available in the Chronicle Content Hub. These curated rules help you quickly deploy detections without writing custom rules. +#### Manual Opt-In + +If your account has sufficient permissions, you can manually opt-in to Gemini before using it: ```python -# List all featured content rules -rules = chronicle.list_featured_content_rules() -for rule in rules.get("featuredContentRules", []): - rule_id = rule.get("name", "").split("/")[-1] - content_metadata = rule.get("contentMetadata", {}) - display_name = content_metadata.get("displayName", "Unknown") - severity = rule.get("severity", "UNSPECIFIED") - print(f"Rule: {display_name} [{rule_id}] - Severity: {severity}") +# Manually opt-in to Gemini +opt_success = chronicle.opt_in_to_gemini() +if opt_success: + print("Successfully opted in to Gemini") +else: + print("Unable to opt-in due to permission issues") -# List with pagination -result = chronicle.list_featured_content_rules(page_size=10) -rules = result.get("featuredContentRules", []) -next_page_token = result.get("nextPageToken") +# Then use Gemini as normal +response = chronicle.gemini("What is Windows event ID 4625?") +``` + +This can be useful in environments where you want to explicitly control when the opt-in happens. + +#### Generate Detection Rules + +Chronicle Gemini can generate YARA-L rules for detection: -if next_page_token: - next_page = chronicle.list_featured_content_rules( - page_size=10, - page_token=next_page_token - ) +```python +# Generate a rule to detect potential security issues +rule_response = chronicle.gemini("Write a rule to detect powershell downloading a file called gdp.zip") -# Filter list -filtered_rules = chronicle.list_featured_content_rules( - filter_expression=( - 'category_name:"Threat Detection" AND ' - 'rule_precision:"Precise"' - ) -) +# Extract the generated rule(s) +code_blocks = rule_response.get_code_blocks() +if code_blocks: + rule = code_blocks[0].content + print("Generated rule:", rule) + + # Check for rule editor action + for action in rule_response.suggested_actions: + if action.display_text == "Open in Rule Editor" and action.action_type == "NAVIGATION": + rule_editor_url = action.navigation.target_uri + print("Rule can be opened in editor:", rule_editor_url) ``` -## Data Tables and Reference Lists +#### Get Intel Information -Chronicle provides two ways to manage and reference structured data in detection rules: Data Tables and Reference Lists. These can be used to maintain lists of trusted/suspicious entities, mappings of contextual information, or any other structured data useful for detection. +Get detailed information about malware, threat actors, files, vulnerabilities: -### Data Tables +```python +# Ask about a CVE +cve_response = chronicle.gemini("tell me about CVE-2021-44228") -Data Tables are collections of structured data with defined columns and data types. They can be referenced in detection rules to enhance your detections with additional context. +# Get the explanation +cve_explanation = cve_response.get_text_content() +print("CVE explanation:", cve_explanation) +``` -#### Creating Data Tables +#### Maintain Conversation Context + +You can maintain conversation context by reusing the same conversation ID: ```python -from secops.chronicle.data_table import DataTableColumnType +# Start a conversation +initial_response = chronicle.gemini("What is a DDoS attack?") -# Create a data table with different column types -data_table = chronicle.create_data_table( - name="suspicious_ips", - description="Known suspicious IP addresses with context", - header={ - "ip_address": DataTableColumnType.CIDR, - # Alternately, you can map a column to an entity proto field - # See: https://cloud.google.com/chronicle/docs/investigation/data-tables#map_column_names_to_entity_fields_optional - # "ip_address": "entity.asset.ip" - "port": DataTableColumnType.NUMBER, - "severity": DataTableColumnType.STRING, - "description": DataTableColumnType.STRING - }, - # Optional: Set additional column options (valid options: repeatedValues, keyColumns) - column_options: {"ip_address": {"repeatedValues": True}}, - # Optional: Add initial rows - rows=[ - ["192.168.1.100", 3232, "High", "Scanning activity"], - ["10.0.0.5", 9000, "Medium", "Suspicious login attempts"] - ] +# Get the conversation ID from the response +conversation_id = initial_response.name.split('/')[-3] # Extract from format: .../conversations/{id}/messages/{id} + +# Ask a follow-up question in the same conversation context +followup_response = chronicle.gemini( + "What are the most common mitigation techniques?", + conversation_id=conversation_id ) -print(f"Created table: {data_table['name']}") +# Gemini will remember the context of the previous question about DDoS ``` -#### Managing Data Tables +### Feed Management + +Feeds are used to ingest data into Chronicle. The SDK provides methods to manage feeds. ```python -# List all data tables -tables = chronicle.list_data_tables() -for table in tables: - table_id = table["name"].split("/")[-1] - print(f"Table: {table_id}, Created: {table.get('createTime')}") +import json -# Get a specific data table's details -table_details = chronicle.get_data_table("suspicious_ips") -print(f"Column count: {len(table_details.get('columnInfo', []))}") +# List existing feeds +feeds = chronicle.list_feeds() +print(f"Found {len(feeds)} feeds") -# Update a data table's properties -updated_table = chronicle.update_data_table( - "suspicious_ips", - description="Updated description for suspicious IPs", - row_time_to_live="72h" # Set TTL for rows to 72 hours - update_mask=["description", "row_time_to_live"] -) -print(f"Updated data table: {updated_table['name']}") +# Create a new feed +feed_details = { + "logType": f"projects/your-project-id/locations/us/instances/your-chronicle-instance-id/logTypes/WINEVTLOG", + "feedSourceType": "HTTP", + "httpSettings": { + "uri": "https://example.com/example_feed", + "sourceType": "FILES", + }, + "labels": {"environment": "production", "created_by": "secops_sdk"} +} -# Add rows to a data table -chronicle.create_data_table_rows( - "suspicious_ips", - [ - ["172.16.0.1", "Low", "Unusual outbound connection"], - ["192.168.2.200", "Critical", "Data exfiltration attempt"] - ] +created_feed = chronicle.create_feed( + display_name="My New Feed", + details=feed_details ) -# List rows in a data table -rows = chronicle.list_data_table_rows("suspicious_ips") -for row in rows: - row_id = row["name"].split("/")[-1] - values = row.get("values", []) - print(f"Row {row_id}: {values}") - -# Delete specific rows by ID -row_ids = [rows[0]["name"].split("/")[-1], rows[1]["name"].split("/")[-1]] -chronicle.delete_data_table_rows("suspicious_ips", row_ids) +# Get feed ID from name +feed_id = created_feed["name"].split("/")[-1] +print(f"Feed created with ID: {feed_id}") -# Replace all rows in a data table with new rows -chronicle.replace_data_table_rows( - name="suspicious_ips", # Data table Name - rows=[ - ["192.168.100.1", "Critical", "Active scanning"], - ["10.1.1.5", "High", "Brute force attempts"], - ["172.16.5.10", "Medium", "Suspicious traffic"] - ] -) +# Get feed details +feed_details = chronicle.get_feed(feed_id) +print(f"Feed state: {feed_details.get('state')}") -# Bulk update rows in a data table -row_updates = [ - { - "name": "projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row123", # Full resource name - "values": ["192.168.100.1", "Critical", "Updated description"] +# Update feed +updated_details = { + "httpSettings": { + "uri": "https://example.com/updated_feed_url", + "sourceType": "FILES" }, - { - "name": "projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row456", # Full resource name - "values": ["10.1.1.5", "High", "Updated brute force info"], - "update_mask": "values" # Optional: only update values field - } -] + "labels": {"environment": "production", "updated": "true"} +} -# Execute bulk update -chronicle.update_data_table_rows( - name="suspicious_ips", - row_updates=row_updates +updated_feed = chronicle.update_feed( + feed_id=feed_id, + display_name="Updated Feed Name", + details=updated_details ) -# Delete a data table -chronicle.delete_data_table("suspicious_ips", force=True) # force=True deletes even if it has rows +# Disable feed +disabled_feed = chronicle.disable_feed(feed_id) +print(f"Feed disabled. State: {disabled_feed.get('state')}") + +# Enable feed +enabled_feed = chronicle.enable_feed(feed_id) +print(f"Feed enabled. State: {enabled_feed.get('state')}") + +# Generate secret for feed (for supported feed types) +try: + secret_result = chronicle.generate_secret(feed_id) + print(f"Generated secret: {secret_result.get('secret')}") +except Exception as e: + print(f"Error generating secret for feed: {e}") + +# Delete feed +chronicle.delete_feed(feed_id) +print("Feed deleted successfully") ``` -### Reference Lists +The Feed API supports different feed types such as HTTP, HTTPS Push, and S3 bucket data sources etc. Each feed type has specific configuration options that can be specified in the `details` dictionary. -Reference Lists are simple lists of values (strings, CIDR blocks, or regex patterns) that can be referenced in detection rules. They are useful for maintaining whitelists, blacklists, or any other categorized sets of values. +> **Note**: Secret generation is only available for certain feed types that require authentication. -#### Creating Reference Lists +### Chronicle Dashboard +The Chronicle Dashboard API provides methods to manage native dashboards and dashboard charts in Chronicle. + +#### Create Dashboard ```python -from secops.chronicle.reference_list import ReferenceListSyntaxType, ReferenceListView +# Create a dashboard +dashboard = chronicle.create_dashboard( + display_name="Security Overview", + description="Dashboard showing security metrics", + access_type="PRIVATE" # "PRIVATE" or "PUBLIC" +) +dashboard_id = dashboard["name"].split("/")[-1] +print(f"Created dashboard with ID: {dashboard_id}") +``` -# Create a reference list with string entries -string_list = chronicle.create_reference_list( - name="admin_accounts", - description="Administrative user accounts", - entries=["admin", "administrator", "root", "system"], - syntax_type=ReferenceListSyntaxType.STRING +#### Get Specific Dashboard Details +```python +# Get a specific dashboard +dashboard = chronicle.get_dashboard( + dashboard_id="dashboard-id-here", + view="FULL" # Optional: "BASIC" or "FULL" ) +print(f"Dashboard Details: {dashboard}") +``` -print(f"Created reference list: {string_list['name']}") +#### List Dashboards +```python +dashboards = chronicle.list_dashboards() +for dashboard in dashboards.get("nativeDashboards", []): + print(f"- {dashboard.get('displayName')}") -# Create a reference list with CIDR entries -cidr_list = chronicle.create_reference_list( - name="trusted_networks", - description="Internal network ranges", - entries=["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"], - syntax_type=ReferenceListSyntaxType.CIDR +# List dashboards with pagination(first page) +dashboards = chronicle.list_dashboards(page_size=10) +for dashboard in dashboards.get("nativeDashboards", []): + print(f"- {dashboard.get('displayName')}") + +# Get next page if available +if "nextPageToken" in dashboards: + next_page = chronicle.list_dashboards( + page_size=10, + page_token=dashboards["nextPageToken"] + ) +``` + +#### Update existing dashboard details +```python +filters = [ + { + "id": "GlobalTimeFilter", + "dataSource": "GLOBAL", + "filterOperatorAndFieldValues": [ + {"filterOperator": "PAST", "fieldValues": ["7", "DAY"]} + ], + "displayName": "Global Time Filter", + "chartIds": [], + "isStandardTimeRangeFilter": True, + "isStandardTimeRangeFilterEnabled": True, + } +] +charts = [ + { + "dashboardChart": "projects//locations//instances//dashboardCharts/", + "chartLayout": {"startX": 0, "spanX": 48, "startY": 0, "spanY": 26}, + "filtersIds": ["GlobalTimeFilter"], + } +] +# Update a dashboard +updated_dashboard = chronicle.update_dashboard( + dashboard_id="dashboard-id-here", + display_name="Updated Security Dashboard", + filters=filters, + charts=charts, ) +print(f"Updated dashboard: {updated_dashboard['displayName']}") +``` -# Create a reference list with regex patterns -regex_list = chronicle.create_reference_list( - name="email_patterns", - description="Email patterns to watch for", - entries=[".*@suspicious\\.com", "malicious_.*@.*\\.org"], - syntax_type=ReferenceListSyntaxType.REGEX +#### Duplicate existing dashboard +```python +# Duplicate a dashboard +duplicate = chronicle.duplicate_dashboard( + dashboard_id="dashboard-id-here", + display_name="Copy of Security Dashboard", + access_type="PRIVATE" ) +duplicate_id = duplicate["name"].split("/")[-1] +print(f"Created duplicate dashboard with ID: {duplicate_id}") ``` -#### Managing Reference Lists +#### Import Dashboard +Imports a dashboard from a JSON file. ```python -# List all reference lists (basic view without entries) -lists = chronicle.list_reference_lists(view=ReferenceListView.BASIC) -for ref_list in lists: - list_id = ref_list["name"].split("/")[-1] - print(f"List: {list_id}, Description: {ref_list.get('description')}") +import os +from secops.chronicle import client -# Get a specific reference list including all entries -admin_list = chronicle.get_reference_list("admin_accounts", view=ReferenceListView.FULL) -entries = [entry.get("value") for entry in admin_list.get("entries", [])] -print(f"Admin accounts: {entries}") +# Assumes the CHRONICLE_SA_KEY environment variable is set with service account JSON +chronicle_client = client.Client() -# Update reference list entries -chronicle.update_reference_list( - name="admin_accounts", - entries=["admin", "administrator", "root", "system", "superuser"] -) +# Path to the dashboard file +dashboard = { + "dashboard": {...} + "dashboardCharts": [...], + "dashboardQueries": [...] +} -# Update reference list description -chronicle.update_reference_list( - name="admin_accounts", - description="Updated administrative user accounts list" -) +# Import the dashboard +try: + new_dashboard = chronicle_client.import_dashboard(dashboard) + print(new_dashboard) +except Exception as e: + print(f"An error occurred: {e}") ``` -### Using in YARA-L Rules - -Both Data Tables and Reference Lists can be referenced in YARA-L detection rules. +#### Export Dashboards -#### Using Data Tables in Rules +Export dashboard to a dictionary. -``` -rule detect_with_data_table { - meta: - description = "Detect connections to suspicious IPs" - author = "SecOps SDK Example" - severity = "Medium" - yara_version = "YL2.0" - events: - $e.metadata.event_type = "NETWORK_CONNECTION" - $e.target.ip != "" - $lookup in data_table.suspicious_ips - $lookup.ip_address = $e.target.ip - $severity = $lookup.severity - - condition: - $e and $lookup and $severity = "High" -} +```python +# Export a dashboard +dashboards = chronicle.export_dashboard(dashboard_names=[""]) ``` -#### Using Reference Lists in Rules -``` -rule detect_with_reference_list { - meta: - description = "Detect admin account usage from untrusted networks" - author = "SecOps SDK Example" - severity = "High" - yara_version = "YL2.0" - events: - $login.metadata.event_type = "USER_LOGIN" - $login.principal.user.userid in reference_list.admin_accounts - not $login.principal.ip in reference_list.trusted_networks - - condition: - $login +#### Add Chart to existing dashboard +```python +# Define chart configuration +query = """ +metadata.event_type = "NETWORK_DNS" +match: + principal.hostname +outcome: + $dns_query_count = count(metadata.id) +order: + principal.hostname asc +""" + +chart_layout = { + "startX": 0, + "spanX": 12, + "startY": 0, + "spanY": 8 } -``` -## Gemini AI +chart_datasource = { + "dataSources": ["UDM"] +} -You can use Chronicle's Gemini AI to get security insights, generate detection rules, explain security concepts, and more: +interval = { + "relativeTime": { + "timeUnit": "DAY", + "startTimeVal": "1" + } +} -> **Note:** Only enterprise tier users have access to Advanced Gemini features. Users must opt-in to use Gemini in Chronicle before accessing this functionality. -The SDK will automatically attempt to opt you in when you first use the Gemini functionality. If the automatic opt-in fails due to permission issues, -you'll see an error message that includes "users must opt-in before using Gemini." +# Add chart to dashboard +chart = chronicle.add_chart( + dashboard_id="dashboard-id-here", + display_name="DNS Query Metrics", + query=query, + chart_layout=chart_layout, + chart_datasource=chart_datasource, + interval=interval, + tile_type="VISUALIZATION" # Option: "VISUALIZATION" or "BUTTOn" +) +``` +#### Get Dashboard Chart Details ```python -# Query Gemini with a security question -response = chronicle.gemini("What is Windows event ID 4625?") - -# Get text content (combines TEXT blocks and stripped HTML content) -text_explanation = response.get_text_content() -print("Explanation:", text_explanation) +# Get dashboard chart details +dashboard_chart = chronicle.get_chart( + chart_id="chart-id-here" +) +print(f"Dashboard Chart Details: {dashboard_chart}") +``` -# Work with different content blocks -for block in response.blocks: - print(f"Block type: {block.block_type}") - if block.block_type == "TEXT": - print("Text content:", block.content) - elif block.block_type == "CODE": - print(f"Code ({block.title}):", block.content) - elif block.block_type == "HTML": - print("HTML content (with tags):", block.content) +#### Edit Dashboard Chart +```python +# Dashboard Query updated details +updated_dashboard_query={ + "name": "project//instance//dashboardQueries/", + "query": 'metadata.event_type = "USER_LOGIN"\nmatch:\n principal.user.userid\noutcome:\n $logon_count = count(metadata.id)\norder:\n $logon_count desc\nlimit: 10', + "input": {"relativeTime": {"timeUnit": "DAY", "startTimeVal": "1"}}, + "etag": "123456", # Latest etag from server +} -# Get all code blocks -code_blocks = response.get_code_blocks() -for code_block in code_blocks: - print(f"Code block ({code_block.title}):", code_block.content) +# Dashboard Chart updated details +updated_dashboard_chart={ + "name": "project//instance//dashboardCharts/", + "display_name": "Updated chart display Name", + "description": "Updated chart description", + "etag": "12345466", # latest etag from server + "visualization": {}, + "chart_datasource":{"data_sources":[]} +} -# Get all HTML blocks (with HTML tags preserved) -html_blocks = response.get_html_blocks() -for html_block in html_blocks: - print(f"HTML block (with tags):", html_block.content) +updated_chart = chronicle.edit_chart( + dashboard_id="dashboard-id-here", + dashboard_chart=updated_dashboard_chart, + dashboard_query=updated_dashboard_query +) +print(f"Updated dashboard chart: {updated_chart}") +``` -# Check for references -if response.references: - print(f"Found {len(response.references)} references") +#### Remove Chart from existing dashboard +```python +# Remove chart from dashboard +chronicle.remove_chart( + dashboard_id="dashboard-id-here", + chart_id="chart-id-here" +) +``` -# Check for suggested actions -for action in response.suggested_actions: - print(f"Suggested action: {action.display_text} ({action.action_type})") - if action.navigation: - print(f"Action URI: {action.navigation.target_uri}") +#### Delete existing dashboard +```python +# Delete a dashboard +chronicle.delete_dashboard(dashboard_id="dashboard-id-here") +print("Dashboard deleted successfully") ``` -### Response Content Methods +### Dashboard Query -The `GeminiResponse` class provides several methods to work with response content: +The Chronicle Dashboard Query API provides methods to execute dashboard queries without creating a dashboard and get details of dashboard query. -- `get_text_content()`: Returns a combined string of all TEXT blocks plus the text content from HTML blocks with HTML tags removed -- `get_code_blocks()`: Returns a list of blocks with `block_type == "CODE"` -- `get_html_blocks()`: Returns a list of blocks with `block_type == "HTML"` (HTML tags preserved) -- `get_raw_response()`: Returns the complete, unprocessed API response as a dictionary +#### Execute Dashboard Query +```python +# Define query and time interval +query = """ +metadata.event_type = "USER_LOGIN" +match: + principal.user.userid +outcome: + $logon_count = count(metadata.id) +order: + $logon_count desc +limit: 10 +""" -These methods help you work with different types of content in a structured way. +interval = { + "relativeTime": { + "timeUnit": "DAY", + "startTimeVal": "7" + } +} -### Accessing Raw API Response +# Execute the query +results = chronicle.execute_dashboard_query( + query=query, + interval=interval +) -For advanced use cases or debugging, you can access the raw API response: +# Process results +for result in results.get("results", []): + print(result) +``` +#### Get Dashboard Query details ```python -# Get the complete raw API response -response = chronicle.gemini("What is Windows event ID 4625?") -raw_response = response.get_raw_response() +# Get dashboard query details +dashboard_query = chronicle.get_dashboard_query( + query_id="query-id-here" +) +print(f"Dashboard Query Details: {dashboard_query}") +``` -# Now you can access any part of the original JSON structure -print(json.dumps(raw_response, indent=2)) +## SOAR Modules -# Example of navigating the raw response structure -if "responses" in raw_response: - for resp in raw_response["responses"]: - if "blocks" in resp: - print(f"Found {len(resp['blocks'])} blocks in raw response") -``` +The SDK provides functions to interact with SOAR modules in Google SecOps. These are available under the `soar` namespace on the `ChronicleClient` instance, e.g. `chronicle.soar.(...)`. This covers the full lifecycle of integrations, integration instances, actions, connectors, jobs, managers, and playbooks. -This gives you direct access to the original API response format, which can be useful for accessing advanced features or troubleshooting. +### Integration Management -### Manual Opt-In +#### Marketplace Integrations -If your account has sufficient permissions, you can manually opt-in to Gemini before using it: +List available marketplace integrations: ```python -# Manually opt-in to Gemini -opt_success = chronicle.opt_in_to_gemini() -if opt_success: - print("Successfully opted in to Gemini") -else: - print("Unable to opt-in due to permission issues") +# Get all available marketplace integrations +integrations = chronicle.soar.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", "") -# Then use Gemini as normal -response = chronicle.gemini("What is Windows event ID 4625?") -``` +# Get all integrations as a list +integrations = chronicle.soar.list_marketplace_integrations(as_list=True) -This can be useful in environments where you want to explicitly control when the opt-in happens. +# Get all currently installed integrations +integrations = chronicle.soar.list_marketplace_integrations(filter_string="installed = true") -### Generate Detection Rules +# Get all installed integrations with updates available +integrations = chronicle.soar.list_marketplace_integrations( + filter_string="installed = true AND updateAvailable = true" +) -Chronicle Gemini can generate YARA-L rules for detection: +# Specify use of V1 Alpha API version +integrations = chronicle.soar.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) +``` -```python -# Generate a rule to detect potential security issues -rule_response = chronicle.gemini("Write a rule to detect powershell downloading a file called gdp.zip") +Get a specific marketplace integration: -# Extract the generated rule(s) -code_blocks = rule_response.get_code_blocks() -if code_blocks: - rule = code_blocks[0].content - print("Generated rule:", rule) - - # Check for rule editor action - for action in rule_response.suggested_actions: - if action.display_text == "Open in Rule Editor" and action.action_type == "NAVIGATION": - rule_editor_url = action.navigation.target_uri - print("Rule can be opened in editor:", rule_editor_url) +```python +integration = chronicle.soar.get_marketplace_integration("AWSSecurityHub") ``` -### Get Intel Information - -Get detailed information about malware, threat actors, files, vulnerabilities: +Get the diff between the currently installed version and the latest +available version of an integration: ```python -# Ask about a CVE -cve_response = chronicle.gemini("tell me about CVE-2021-44228") - -# Get the explanation -cve_explanation = cve_response.get_text_content() -print("CVE explanation:", cve_explanation) +diff = chronicle.soar.get_marketplace_integration_diff("AWSSecurityHub") ``` -### Maintain Conversation Context - -You can maintain conversation context by reusing the same conversation ID: +Install or update a marketplace integration: ```python -# Start a conversation -initial_response = chronicle.gemini("What is a DDoS attack?") - -# Get the conversation ID from the response -conversation_id = initial_response.name.split('/')[-3] # Extract from format: .../conversations/{id}/messages/{id} +# Install an integration with the default settings +integration_name = "AWSSecurityHub" +integration = chronicle.soar.install_marketplace_integration(integration_name) -# Ask a follow-up question in the same conversation context -followup_response = chronicle.gemini( - "What are the most common mitigation techniques?", - conversation_id=conversation_id +# Install to staging environment and override any existing ontology mappings +integration = chronicle.soar.install_marketplace_integration( + integration_name, + staging=True, + override_mapping=True, ) -# Gemini will remember the context of the previous question about DDoS -``` +# Installing a currently installed integration with no specified version +# number will update it to the latest version +integration = chronicle.soar.install_marketplace_integration(integration_name) -## Feed Management +# Or you can specify a specific version to install +integration = chronicle.soar.install_marketplace_integration( + integration_name, + version="5.0", +) +``` -Feeds are used to ingest data into Chronicle. The SDK provides methods to manage feeds. +Uninstall a marketplace integration: ```python -import json - -# List existing feeds -feeds = chronicle.list_feeds() -print(f"Found {len(feeds)} feeds") - -# Create a new feed -feed_details = { - "logType": f"projects/your-project-id/locations/us/instances/your-chronicle-instance-id/logTypes/WINEVTLOG", - "feedSourceType": "HTTP", - "httpSettings": { - "uri": "https://example.com/example_feed", - "sourceType": "FILES", - }, - "labels": {"environment": "production", "created_by": "secops_sdk"} -} - -created_feed = chronicle.create_feed( - display_name="My New Feed", - details=feed_details -) +chronicle.soar.uninstall_marketplace_integration("AWSSecurityHub") +``` -# Get feed ID from name -feed_id = created_feed["name"].split("/")[-1] -print(f"Feed created with ID: {feed_id}") +#### Integrations -# Get feed details -feed_details = chronicle.get_feed(feed_id) -print(f"Feed state: {feed_details.get('state')}") +List all installed integrations: -# Update feed -updated_details = { - "httpSettings": { - "uri": "https://example.com/updated_feed_url", - "sourceType": "FILES" - }, - "labels": {"environment": "production", "updated": "true"} -} +```python +# Get all integrations +integrations = chronicle.soar.list_integrations() +for i in integrations.get("integrations", []): + integration_id = i["identifier"] + integration_display_name = i["displayName"] + integration_type = i["type"] -updated_feed = chronicle.update_feed( - feed_id=feed_id, - display_name="Updated Feed Name", - details=updated_details -) +# Get all integrations as a list +integrations = chronicle.soar.list_integrations(as_list=True) -# Disable feed -disabled_feed = chronicle.disable_feed(feed_id) -print(f"Feed disabled. State: {disabled_feed.get('state')}") +for i in integrations: + integration = chronicle.soar.get_integration(i["identifier"]) + if integration.get("parameters"): + print(json.dumps(integration, indent=2)) -# Enable feed -enabled_feed = chronicle.enable_feed(feed_id) -print(f"Feed enabled. State: {enabled_feed.get('state')}") +# Get integrations ordered by display name +integrations = chronicle.soar.list_integrations(order_by="displayName", as_list=True) +``` -# Generate secret for feed (for supported feed types) -try: - secret_result = chronicle.generate_secret(feed_id) - print(f"Generated secret: {secret_result.get('secret')}") -except Exception as e: - print(f"Error generating secret for feed: {e}") +Get details of a specific integration: -# Delete feed -chronicle.delete_feed(feed_id) -print("Feed deleted successfully") +```python +integration = chronicle.soar.get_integration("AWSSecurityHub") ``` -The Feed API supports different feed types such as HTTP, HTTPS Push, and S3 bucket data sources etc. Each feed type has specific configuration options that can be specified in the `details` dictionary. +Create an integration: -> **Note**: Secret generation is only available for certain feed types that require authentication. +```python +from secops.chronicle.models import ( + IntegrationParam, + IntegrationParamType, + IntegrationType, + PythonVersion, +) -## Chronicle Dashboard +integration = chronicle.soar.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, +) +``` -The Chronicle Dashboard API provides methods to manage native dashboards and dashboard charts in Chronicle. +Update an integration: -### Create Dashboard ```python -# Create a dashboard -dashboard = chronicle.create_dashboard( - display_name="Security Overview", - description="Dashboard showing security metrics", - access_type="PRIVATE" # "PRIVATE" or "PUBLIC" +from secops.chronicle.models import IntegrationParam, IntegrationParamType + +updated_integration = chronicle.soar.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"], ) -dashboard_id = dashboard["name"].split("/")[-1] -print(f"Created dashboard with ID: {dashboard_id}") ``` -### Get Specific Dashboard Details +Update a custom integration (including its scripts and dependencies): + ```python -# Get a specific dashboard -dashboard = chronicle.get_dashboard( - dashboard_id="dashboard-id-here", - view="FULL" # Optional: "BASIC" or "FULL" +result = chronicle.soar.update_custom_integration( + integration_name="MyNewIntegration", + display_name="Updated Integration Name", + description="Updated description", + dependencies_to_remove=["old-lib==1.0.0"], ) -print(f"Dashboard Details: {dashboard}") +# result contains "successful", "integration", and optionally "dependencies" ``` -### List Dashboards +Delete an integration: + ```python -dashboards = chronicle.list_dashboards() -for dashboard in dashboards.get("nativeDashboards", []): - print(f"- {dashboard.get('displayName')}") +chronicle.soar.delete_integration("MyNewIntegration") +``` -# List dashboards with pagination(first page) -dashboards = chronicle.list_dashboards(page_size=10) -for dashboard in dashboards.get("nativeDashboards", []): - print(f"- {dashboard.get('displayName')}") +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: -# Get next page if available -if "nextPageToken" in dashboards: - next_page = chronicle.list_dashboards( - page_size=10, - page_token=dashboards["nextPageToken"] - ) +```python +integration_bytes = chronicle.soar.download_integration("MyIntegration") +with open("MyIntegration.zip", "wb") as f: + f.write(integration_bytes) ``` -### Update existing dashboard details +Export selected items from an integration (e.g. only specific actions) as a `.zip` file: + ```python -filters = [ - { - "id": "GlobalTimeFilter", - "dataSource": "GLOBAL", - "filterOperatorAndFieldValues": [ - {"filterOperator": "PAST", "fieldValues": ["7", "DAY"]} - ], - "displayName": "Global Time Filter", - "chartIds": [], - "isStandardTimeRangeFilter": True, - "isStandardTimeRangeFilterEnabled": True, - } -] -charts = [ - { - "dashboardChart": "projects//locations//instances//dashboardCharts/", - "chartLayout": {"startX": 0, "spanX": 48, "startY": 0, "spanY": 26}, - "filtersIds": ["GlobalTimeFilter"], - } -] -# Update a dashboard -updated_dashboard = chronicle.update_dashboard( - dashboard_id="dashboard-id-here", - display_name="Updated Security Dashboard", - filters=filters, - charts=charts, +# Export only actions with IDs 1 and 2 from the integration +export_bytes = chronicle.soar.export_integration_items( + integration_name="AWSSecurityHub", + actions=["1", "2"], # IDs of the actions to export +) +with open("AWSSecurityHub_Export.zip", "wb") as f: + f.write(export_bytes) + +# Export multiple item types at once +export_bytes = chronicle.soar.export_integration_items( + integration_name="AWSSecurityHub", + actions=["1", "2"], + connectors=["3"], + jobs=["4", "5"], ) -print(f"Updated dashboard: {updated_dashboard['displayName']}") ``` -### Duplicate existing dashboard +Get dependencies for an integration: + ```python -# Duplicate a dashboard -duplicate = chronicle.duplicate_dashboard( - dashboard_id="dashboard-id-here", - display_name="Copy of Security Dashboard", - access_type="PRIVATE" -) -duplicate_id = duplicate["name"].split("/")[-1] -print(f"Created duplicate dashboard with ID: {duplicate_id}") +dependencies = chronicle.soar.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}") ``` -#### Import Dashboard -Imports a dashboard from a JSON file. +Force a dependency download for an integration: ```python -import os -from secops.chronicle import client +# Install a specific version of a dependency +chronicle.soar.download_integration_dependency( + "MyIntegration", + "boto3==1.42.44", +) -# Assumes the CHRONICLE_SA_KEY environment variable is set with service account JSON -chronicle_client = client.Client() +# Install the latest version of a dependency +chronicle.soar.download_integration_dependency( + "MyIntegration", + "boto3", +) +``` -# Path to the dashboard file -dashboard = { - "dashboard": {...} - "dashboardCharts": [...], - "dashboardQueries": [...] -} +Get remote agents that would be restricted from running an updated version of the integration: -# Import the dashboard -try: - new_dashboard = chronicle_client.import_dashboard(dashboard) - print(new_dashboard) -except Exception as e: - print(f"An error occurred: {e}") +```python +from secops.chronicle.models import PythonVersion +agents = chronicle.soar.get_integration_restricted_agents( + integration_name="AWSSecurityHub", + required_python_version=PythonVersion.PYTHON_3_11, +) ``` -#### Export Dashboards - -Export dashboard to a dictionary. +Get the integrations currently installed on a specific agent: ```python -# Export a dashboard -dashboards = chronicle.export_dashboard(dashboard_names=[""]) +agent_integrations = chronicle.soar.get_agent_integrations(agent_id="my-agent-id") ``` +Get items (connector instances, job instances, playbooks) affected by changes to an integration: -### Add Chart to existing dashboard ```python -# Define chart configuration -query = """ -metadata.event_type = "NETWORK_DNS" -match: - principal.hostname -outcome: - $dns_query_count = count(metadata.id) -order: - principal.hostname asc -""" +affected = chronicle.soar.get_integration_affected_items("AWSSecurityHub") +``` -chart_layout = { - "startX": 0, - "spanX": 12, - "startY": 0, - "spanY": 8 -} +Get integration diff between two versions of an integration: -chart_datasource = { - "dataSources": ["UDM"] -} +```python +from secops.chronicle.models import DiffType -interval = { - "relativeTime": { - "timeUnit": "DAY", - "startTimeVal": "1" - } -} +# Get the diff between the commercial version and the current version in the environment +diff = chronicle.soar.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.COMMERCIAL, +) -# Add chart to dashboard -chart = chronicle.add_chart( - dashboard_id="dashboard-id-here", - display_name="DNS Query Metrics", - query=query, - chart_layout=chart_layout, - chart_datasource=chart_datasource, - interval=interval, - tile_type="VISUALIZATION" # Option: "VISUALIZATION" or "BUTTOn" +# Get the difference between the staging integration and its matching production version +diff = chronicle.soar.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.PRODUCTION, +) + +# Get the difference between the production integration and its corresponding staging version +diff = chronicle.soar.get_integration_diff( + integration_name="AWSSecurityHub", + diff_type=DiffType.STAGING, ) ``` -### Get Dashboard Chart Details +Transition an integration to staging or production environment: + ```python -# Get dashboard chart details -dashboard_chart = chronicle.get_chart( - chart_id="chart-id-here" +from secops.chronicle.models import TargetMode + +# Transition to staging environment +chronicle.soar.transition_integration( + integration_name="AWSSecurityHub", + target_mode=TargetMode.STAGING, +) + +# Transition to production environment +chronicle.soar.transition_integration( + integration_name="AWSSecurityHub", + target_mode=TargetMode.PRODUCTION, ) -print(f"Dashboard Chart Details: {dashboard_chart}") ``` -### Edit Dashboard Chart +#### Integration Instances + +List all instances for a specific integration: + ```python -# Dashboard Query updated details -updated_dashboard_query={ - "name": "project//instance//dashboardQueries/", - "query": 'metadata.event_type = "USER_LOGIN"\nmatch:\n principal.user.userid\noutcome:\n $logon_count = count(metadata.id)\norder:\n $logon_count desc\nlimit: 10', - "input": {"relativeTime": {"timeUnit": "DAY", "startTimeVal": "1"}}, - "etag": "123456", # Latest etag from server -} +# Get all instances for an integration +instances = chronicle.soar.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')}") -# Dashboard Chart updated details -updated_dashboard_chart={ - "name": "project//instance//dashboardCharts/", - "display_name": "Updated chart display Name", - "description": "Updated chart description", - "etag": "12345466", # latest etag from server - "visualization": {}, - "chart_datasource":{"data_sources":[]} -} +# Get all instances as a list +instances = chronicle.soar.list_integration_instances("MyIntegration", as_list=True) -updated_chart = chronicle.edit_chart( - dashboard_id="dashboard-id-here", - dashboard_chart=updated_dashboard_chart, - dashboard_query=updated_dashboard_query +# Get instances for a specific environment +instances = chronicle.soar.list_integration_instances( + "MyIntegration", + filter_string="environment = 'production'", ) -print(f"Updated dashboard chart: {updated_chart}") ``` -### Remove Chart from existing dashboard +Get details of a specific integration instance: + ```python -# Remove chart from dashboard -chronicle.remove_chart( - dashboard_id="dashboard-id-here", - chart_id="chart-id-here" +instance = chronicle.soar.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')}") ``` -### Delete existing dashboard +Create a new integration instance: + ```python -# Delete a dashboard -chronicle.delete_dashboard(dashboard_id="dashboard-id-here") -print("Dashboard deleted successfully") +from secops.chronicle.models import IntegrationInstanceParameter + +# Create instance with required fields only +new_instance = chronicle.soar.create_integration_instance( + integration_name="MyIntegration", + environment="production", +) + +# Create instance with all fields +new_instance = chronicle.soar.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", +) ``` -## Dashboard Query +Update an existing integration instance: -The Chronicle Dashboard Query API provides methods to execute dashboard queries without creating a dashboard and get details of dashboard query. +```python +from secops.chronicle.models import IntegrationInstanceParameter + +# Update instance display name +updated_instance = chronicle.soar.update_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", + display_name="Updated Production Instance", +) + +# Update multiple fields including parameters +updated_instance = chronicle.soar.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 a custom update mask to restrict which fields are patched +updated_instance = chronicle.soar.update_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", + display_name="New Name", + update_mask="displayName", +) +``` + +Delete an integration instance: -### Execute Dashboard Query ```python -# Define query and time interval -query = """ -metadata.event_type = "USER_LOGIN" -match: - principal.user.userid -outcome: - $logon_count = count(metadata.id) -order: - $logon_count desc -limit: 10 -""" +chronicle.soar.delete_integration_instance( + integration_name="MyIntegration", + integration_instance_id="ii1", +) +``` -interval = { - "relativeTime": { - "timeUnit": "DAY", - "startTimeVal": "7" - } -} +Execute a connectivity test for an integration instance: -# Execute the query -results = chronicle.execute_dashboard_query( - query=query, - interval=interval +```python +# Test if the instance can connect to the third-party service +test_result = chronicle.soar.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')}") +``` -# Process results -for result in results.get("results", []): - print(result) +Get affected items (playbooks) that depend on an integration instance: + +```python +# Perform impact analysis before deleting or modifying an instance +affected_items = chronicle.soar.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 Dashboard Query details +Get the default integration instance: + ```python -# Get dashboard query details -dashboard_query = chronicle.get_dashboard_query( - query_id="query-id-here" +# Get the system default configuration for a commercial product +default_instance = chronicle.soar.get_default_integration_instance( + integration_name="AWSSecurityHub", ) -print(f"Dashboard Query Details: {dashboard_query}") +print(f"Default Instance: {default_instance.get('displayName')}") +print(f"Environment: {default_instance.get('environment')}") ``` ## Error Handling @@ -3777,7 +3801,7 @@ except SecOpsError as e: print(f"General error: {e}") ``` -## Value Type Detection +### Value Type Detection The SDK automatically detects the most common entity types when using the `summarize_entity` function: - IP addresses (IPv4 and IPv6) diff --git a/api_module_mapping.md b/api_module_mapping.md index 09d2f8ae..13fe821e 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -12,1035 +12,1035 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Endpoint Mapping -| REST Resource | Version | secops-wrapper module | CLI Command | -|--------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| -| dataAccessLabels.create | v1 | | | -| dataAccessLabels.delete | v1 | | | -| dataAccessLabels.get | v1 | | | -| dataAccessLabels.list | v1 | | | -| dataAccessLabels.patch | v1 | | | -| dataAccessScopes.create | v1 | | | -| dataAccessScopes.delete | v1 | | | -| dataAccessScopes.get | v1 | | | -| dataAccessScopes.list | v1 | | | -| dataAccessScopes.patch | v1 | | | -| get | v1 | | | -| operations.cancel | v1 | | | -| operations.delete | v1 | | | -| operations.get | v1 | | | -| operations.list | v1 | | | -| referenceLists.create | v1 | chronicle.reference_list.create_reference_list | secops reference-list create | -| referenceLists.get | v1 | chronicle.reference_list.get_reference_list | secops reference-list get | -| referenceLists.list | v1 | chronicle.reference_list.list_reference_lists | secops reference-list list | -| referenceLists.patch | v1 | chronicle.reference_list.update_reference_list | secops reference-list update | -| rules.create | v1 | chronicle.rule.create_rule | secops rule create | -| rules.delete | v1 | chronicle.rule.delete_rule | secops rule delete | -| rules.deployments.list | v1 | | | -| rules.get | v1 | chronicle.rule.get_rule | secops rule get | -| rules.getDeployment | v1 | | | -| rules.list | v1 | chronicle.rule.list_rules | secops rule list | -| rules.listRevisions | v1 | | | -| rules.patch | v1 | chronicle.rule.update_rule | secops rule update | -| rules.retrohunts.create | v1 | chronicle.rule_retrohunt.create_retrohunt | secops rule-retrohunt create | -| rules.retrohunts.get | v1 | chronicle.rule_retrohunt.get_retrohunt | secops rule-retrohunt get | -| rules.retrohunts.list | v1 | chronicle.rule_retrohunt.list_retrohunts | secops rule-retrohunt list | -| rules.updateDeployment | v1 | chronicle.rule.enable_rule | secops rule enable | -| watchlists.create | v1 | chronicle.watchlist.create_watchlist | secops watchlist create | -| 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 | -| dataAccessLabels.create | v1beta | | | -| dataAccessLabels.delete | v1beta | | | -| dataAccessLabels.get | v1beta | | | -| dataAccessLabels.list | v1beta | | | -| dataAccessLabels.patch | v1beta | | | -| dataAccessScopes.create | v1beta | | | -| dataAccessScopes.delete | v1beta | | | -| dataAccessScopes.get | v1beta | | | -| dataAccessScopes.list | v1beta | | | -| dataAccessScopes.patch | v1beta | | | -| get | v1beta | | | -| integrations.create | v1beta | | | -| 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 | secops integration integrations list | -| integrations.patch | v1beta | | | -| 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 | 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 | 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 | -| 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 | | | -| operations.list | v1beta | | | -| referenceLists.create | v1beta | | | -| referenceLists.get | v1beta | | | -| referenceLists.list | v1beta | | | -| referenceLists.patch | v1beta | | | -| rules.create | v1beta | | | -| rules.delete | v1beta | | | -| rules.deployments.list | v1beta | | | -| rules.get | v1beta | | | -| rules.getDeployment | v1beta | | | -| rules.list | v1beta | | | -| rules.listRevisions | v1beta | | | -| rules.patch | v1beta | | | -| rules.retrohunts.create | v1beta | | | -| rules.retrohunts.get | v1beta | | | -| rules.retrohunts.list | v1beta | | | -| rules.updateDeployment | v1beta | | | -| watchlists.create | v1beta | | | -| watchlists.delete | v1beta | | | -| watchlists.get | v1beta | | | -| watchlists.list | v1beta | | | -| watchlists.patch | v1beta | | | -| analytics.entities.analyticValues.list | v1alpha | | | -| analytics.list | v1alpha | | | -| batchValidateWatchlistEntities | v1alpha | | | -| bigQueryAccess.provide | v1alpha | | | -| bigQueryExport.provision | v1alpha | | | -| cases.countPriorities | v1alpha | | | -| contentHub.featuredContentRules.list | v1alpha | chronicle.featured_content_rules.list_featured_content_rules | secops featured-content-rules list | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.batchUpdate | v1alpha | chronicle.rule_set.batch_update_curated_rule_set_deployments | | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.patch | v1alpha | chronicle.rule_set.update_curated_rule_set_deployment | secops curated-rule rule-set-deployment update | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.list | v1alpha | chronicle.rule_set.list_curated_rule_set_deployments | secops curated-rule rule-set-deployment list | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.get | v1alpha | chronicle.rule_set.get_curated_rule_set_deployment
chronicle.rule_set.get_curated_rule_set_deployment_by_name | secops curated-rule rule-set-deployment get | -| curatedRuleSetCategories.curatedRuleSets.get | v1alpha | chronicle.rule_set.get_curated_rule_set | secops curated-rule rule-set get | -| curatedRuleSetCategories.curatedRuleSets.list | v1alpha | chronicle.rule_set.list_curated_rule_sets | secops curated-rule rule-set list | -| curatedRuleSetCategories.get | v1alpha | chronicle.rule_set.get_curated_rule_set_category | secops curated-rule rule-set-category get | -| 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 | | | -| integrations.create | v1alpha | | | -| 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) | secops integration integrations list | -| integrations.patch | v1alpha | | | -| 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) | 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) | 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 | -| 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) | 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 | -| 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 | | | -| REST Resource | Version | secops-wrapper module | CLI Command | -|--------------------------------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------| -| dataAccessLabels.create | v1 | | | -| dataAccessLabels.delete | v1 | | | -| dataAccessLabels.get | v1 | | | -| dataAccessLabels.list | v1 | | | -| dataAccessLabels.patch | v1 | | | -| dataAccessScopes.create | v1 | | | -| dataAccessScopes.delete | v1 | | | -| dataAccessScopes.get | v1 | | | -| dataAccessScopes.list | v1 | | | -| dataAccessScopes.patch | v1 | | | -| get | v1 | | | -| operations.cancel | v1 | | | -| operations.delete | v1 | | | -| operations.get | v1 | | | -| operations.list | v1 | | | -| referenceLists.create | v1 | chronicle.reference_list.create_reference_list | secops reference-list create | -| referenceLists.get | v1 | chronicle.reference_list.get_reference_list | secops reference-list get | -| referenceLists.list | v1 | chronicle.reference_list.list_reference_lists | secops reference-list list | -| referenceLists.patch | v1 | chronicle.reference_list.update_reference_list | secops reference-list update | -| rules.create | v1 | chronicle.rule.create_rule | secops rule create | -| rules.delete | v1 | chronicle.rule.delete_rule | secops rule delete | -| rules.deployments.list | v1 | | | -| rules.get | v1 | chronicle.rule.get_rule | secops rule get | -| rules.getDeployment | v1 | | | -| rules.list | v1 | chronicle.rule.list_rules | secops rule list | -| rules.listRevisions | v1 | | | -| rules.patch | v1 | chronicle.rule.update_rule | secops rule update | -| rules.retrohunts.create | v1 | chronicle.rule_retrohunt.create_retrohunt | secops rule-retrohunt create | -| rules.retrohunts.get | v1 | chronicle.rule_retrohunt.get_retrohunt | secops rule-retrohunt get | -| rules.retrohunts.list | v1 | chronicle.rule_retrohunt.list_retrohunts | secops rule-retrohunt list | -| rules.updateDeployment | v1 | chronicle.rule.enable_rule | secops rule enable | -| watchlists.create | v1 | chronicle.watchlist.create_watchlist | secops watchlist create | -| 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 | -| dataAccessLabels.create | v1beta | | | -| dataAccessLabels.delete | v1beta | | | -| dataAccessLabels.get | v1beta | | | -| dataAccessLabels.list | v1beta | | | -| dataAccessLabels.patch | v1beta | | | -| dataAccessScopes.create | v1beta | | | -| dataAccessScopes.delete | v1beta | | | -| dataAccessScopes.get | v1beta | | | -| dataAccessScopes.list | v1beta | | | -| dataAccessScopes.patch | v1beta | | | -| get | v1beta | | | -| operations.cancel | v1beta | | | -| operations.delete | v1beta | | | -| operations.get | v1beta | | | -| operations.list | v1beta | | | -| referenceLists.create | v1beta | | | -| referenceLists.get | v1beta | | | -| referenceLists.list | v1beta | | | -| referenceLists.patch | v1beta | | | -| rules.create | v1beta | | | -| rules.delete | v1beta | | | -| rules.deployments.list | v1beta | | | -| rules.get | v1beta | | | -| rules.getDeployment | v1beta | | | -| rules.list | v1beta | | | -| rules.listRevisions | v1beta | | | -| rules.patch | v1beta | | | -| rules.retrohunts.create | v1beta | | | -| rules.retrohunts.get | v1beta | | | -| rules.retrohunts.list | v1beta | | | -| rules.updateDeployment | v1beta | | | -| watchlists.create | v1beta | | | -| watchlists.delete | v1beta | | | -| watchlists.get | v1beta | | | -| watchlists.list | v1beta | | | -| watchlists.patch | v1beta | | | -| cases.executeBulkAddTag | v1beta | chronicle.case.execute_bulk_add_tag | secops case bulk-add-tag | -| cases.executeBulkAssign | v1beta | chronicle.case.execute_bulk_assign | secops case bulk-assign | -| cases.executeBulkChangePriority | v1beta | chronicle.case.execute_bulk_change_priority | secops case bulk-change-priority | -| cases.executeBulkChangeStage | v1beta | chronicle.case.execute_bulk_change_stage | secops case bulk-change-stage | -| cases.executeBulkClose | v1beta | chronicle.case.execute_bulk_close | secops case bulk-close | -| cases.executeBulkReopen | v1beta | chronicle.case.execute_bulk_reopen | secops case bulk-reopen | -| cases.get | v1beta | chronicle.case.get_case | secops case get | -| cases.list | v1beta | chronicle.case.list_cases | secops case list | -| cases.merge | v1beta | chronicle.case.merge_cases | secops case merge | -| cases.patch | v1beta | chronicle.case.patch_case | secops case update | -| analytics.entities.analyticValues.list | v1alpha | | | -| analytics.list | v1alpha | | | -| batchValidateWatchlistEntities | v1alpha | | | -| bigQueryAccess.provide | v1alpha | | | -| bigQueryExport.provision | v1alpha | | | -| cases.countPriorities | v1alpha | | | -| contentHub.featuredContentRules.list | v1alpha | chronicle.featured_content_rules.list_featured_content_rules | secops featured-content-rules list | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.batchUpdate | v1alpha | chronicle.rule_set.batch_update_curated_rule_set_deployments | | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.patch | v1alpha | chronicle.rule_set.update_curated_rule_set_deployment | secops curated-rule rule-set-deployment update | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.list | v1alpha | chronicle.rule_set.list_curated_rule_set_deployments | secops curated-rule rule-set-deployment list | -| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.get | v1alpha | chronicle.rule_set.get_curated_rule_set_deployment
chronicle.rule_set.get_curated_rule_set_deployment_by_name | secops curated-rule rule-set-deployment get | -| curatedRuleSetCategories.curatedRuleSets.get | v1alpha | chronicle.rule_set.get_curated_rule_set | secops curated-rule rule-set get | -| curatedRuleSetCategories.curatedRuleSets.list | v1alpha | chronicle.rule_set.list_curated_rule_sets | secops curated-rule rule-set list | -| curatedRuleSetCategories.get | v1alpha | chronicle.rule_set.get_curated_rule_set_category | secops curated-rule rule-set-category get | -| 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|chronicle.log_search.search_raw_logs |secops search raw-logs | -|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| | | +| REST Resource | Version | secops-wrapper module | CLI Command | +|----------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| dataAccessLabels.create | v1 | | | +| dataAccessLabels.delete | v1 | | | +| dataAccessLabels.get | v1 | | | +| dataAccessLabels.list | v1 | | | +| dataAccessLabels.patch | v1 | | | +| dataAccessScopes.create | v1 | | | +| dataAccessScopes.delete | v1 | | | +| dataAccessScopes.get | v1 | | | +| dataAccessScopes.list | v1 | | | +| dataAccessScopes.patch | v1 | | | +| get | v1 | | | +| operations.cancel | v1 | | | +| operations.delete | v1 | | | +| operations.get | v1 | | | +| operations.list | v1 | | | +| referenceLists.create | v1 | chronicle.reference_list.create_reference_list | secops reference-list create | +| referenceLists.get | v1 | chronicle.reference_list.get_reference_list | secops reference-list get | +| referenceLists.list | v1 | chronicle.reference_list.list_reference_lists | secops reference-list list | +| referenceLists.patch | v1 | chronicle.reference_list.update_reference_list | secops reference-list update | +| rules.create | v1 | chronicle.rule.create_rule | secops rule create | +| rules.delete | v1 | chronicle.rule.delete_rule | secops rule delete | +| rules.deployments.list | v1 | | | +| rules.get | v1 | chronicle.rule.get_rule | secops rule get | +| rules.getDeployment | v1 | | | +| rules.list | v1 | chronicle.rule.list_rules | secops rule list | +| rules.listRevisions | v1 | | | +| rules.patch | v1 | chronicle.rule.update_rule | secops rule update | +| rules.retrohunts.create | v1 | chronicle.rule_retrohunt.create_retrohunt | secops rule-retrohunt create | +| rules.retrohunts.get | v1 | chronicle.rule_retrohunt.get_retrohunt | secops rule-retrohunt get | +| rules.retrohunts.list | v1 | chronicle.rule_retrohunt.list_retrohunts | secops rule-retrohunt list | +| rules.updateDeployment | v1 | chronicle.rule.enable_rule | secops rule enable | +| watchlists.create | v1 | chronicle.watchlist.create_watchlist | secops watchlist create | +| 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 | +| dataAccessLabels.create | v1beta | | | +| dataAccessLabels.delete | v1beta | | | +| dataAccessLabels.get | v1beta | | | +| dataAccessLabels.list | v1beta | | | +| dataAccessLabels.patch | v1beta | | | +| dataAccessScopes.create | v1beta | | | +| dataAccessScopes.delete | v1beta | | | +| dataAccessScopes.get | v1beta | | | +| dataAccessScopes.list | v1beta | | | +| dataAccessScopes.patch | v1beta | | | +| get | v1beta | | | +| integrations.create | v1beta | | | +| integrations.delete | v1beta | chronicle.soar.integration.integrations.delete_integration | secops integration integrations delete | +| integrations.download | v1beta | chronicle.soar.integration.integrations.download_integration | secops integration integrations download | +| integrations.downloadDependency | v1beta | chronicle.soar.integration.integrations.download_integration_dependency | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1beta | chronicle.soar.integration.integrations.export_integration_items | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1beta | chronicle.soar.integration.integrations.get_integration_affected_items | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1beta | chronicle.soar.integration.integrations.get_agent_integrations | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1beta | chronicle.soar.integration.integrations.get_integration_diff | secops integration integrations get-diff | +| integrations.fetchDependencies | v1beta | chronicle.soar.integration.integrations.get_integration_dependencies | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1beta | chronicle.soar.integration.integrations.get_integration_restricted_agents | secops integration integrations get-restricted-agents | +| integrations.get | v1beta | chronicle.soar.integration.integrations.get_integration | secops integration integrations get | +| integrations.getFetchProductionDiff | v1beta | chronicle.soar.integration.integrations.get_integration_diff(diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1beta | chronicle.soar.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.soar.integration.integrations.list_integrations | secops integration integrations list | +| integrations.patch | v1beta | | | +| integrations.pushToProduction | v1beta | chronicle.soar.integration.integrations.transition_integration(target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1beta | chronicle.soar.integration.integrations.transition_integration(target_mode=TargetMode.STAGING) | secops integration integrations transition | +| integrations.updateCustomIntegration | v1beta | | | +| integrations.upload | v1beta | | | +| integrations.actions.create | v1beta | chronicle.soar.integration.actions.create_integration_action | secops integration actions create | +| integrations.actions.delete | v1beta | chronicle.soar.integration.actions.delete_integration_action | secops integration actions delete | +| integrations.actions.executeTest | v1beta | chronicle.soar.integration.actions.execute_integration_action_test | secops integration actions test | +| integrations.actions.fetchActionsByEnvironment | v1beta | chronicle.soar.integration.actions.get_integration_actions_by_environment | | +| integrations.actions.fetchTemplate | v1beta | chronicle.soar.integration.actions.get_integration_action_template | secops integration actions template | +| integrations.actions.get | v1beta | chronicle.soar.integration.actions.get_integration_action | secops integration actions get | +| integrations.actions.list | v1beta | chronicle.soar.integration.actions.list_integration_actions | secops integration actions list | +| integrations.actions.patch | v1beta | chronicle.soar.integration.actions.update_integration_action | secops integration actions update | +| integrations.actions.revisions.create | v1beta | chronicle.soar.integration.action_revisions.create_integration_action_revision | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1beta | chronicle.soar.integration.action_revisions.delete_integration_action_revision | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1beta | chronicle.soar.integration.action_revisions.list_integration_action_revisions | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1beta | chronicle.soar.integration.action_revisions.rollback_integration_action_revision | secops integration action-revisions rollback | +| integrations.connectors.create | v1beta | chronicle.soar.integration.connectors.create_integration_connector | secops integration connectors create | +| integrations.connectors.delete | v1beta | chronicle.soar.integration.connectors.delete_integration_connector | secops integration connectors delete | +| integrations.connectors.executeTest | v1beta | chronicle.soar.integration.connectors.execute_integration_connector_test | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1beta | chronicle.soar.integration.connectors.get_integration_connector_template | secops integration connectors template | +| integrations.connectors.get | v1beta | chronicle.soar.integration.connectors.get_integration_connector | secops integration connectors get | +| integrations.connectors.list | v1beta | chronicle.soar.integration.connectors.list_integration_connectors | secops integration connectors list | +| integrations.connectors.patch | v1beta | chronicle.soar.integration.connectors.update_integration_connector | secops integration connectors update | +| integrations.connectors.revisions.create | v1beta | chronicle.soar.integration.connector_revisions.create_integration_connector_revision | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1beta | chronicle.soar.integration.connector_revisions.delete_integration_connector_revision | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1beta | chronicle.soar.integration.connector_revisions.list_integration_connector_revisions | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1beta | chronicle.soar.integration.connector_revisions.rollback_integration_connector_revision | secops integration connector-revisions rollback | +| integrations.connectors.contextProperties.clearAll | v1beta | chronicle.soar.integration.connector_context_properties.delete_all_connector_context_properties | secops integration connector-context-properties delete-all | +| integrations.connectors.contextProperties.create | v1beta | chronicle.soar.integration.connector_context_properties.create_connector_context_property | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1beta | chronicle.soar.integration.connector_context_properties.delete_connector_context_property | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1beta | chronicle.soar.integration.connector_context_properties.get_connector_context_property | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1beta | chronicle.soar.integration.connector_context_properties.list_connector_context_properties | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1beta | chronicle.soar.integration.connector_context_properties.update_connector_context_property | secops integration connector-context-properties update | +| integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.soar.integration.connector_instance_logs.get_connector_instance_log | secops integration connector-instance-logs get | +| integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.soar.integration.connector_instance_logs.list_connector_instance_logs | secops integration connector-instance-logs list | +| integrations.connectors.connectorInstances.create | v1beta | chronicle.soar.integration.connector_instances.create_connector_instance | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1beta | chronicle.soar.integration.connector_instances.delete_connector_instance | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1beta | chronicle.soar.integration.connector_instances.get_connector_instance_latest_definition | secops integration connector-instances get-latest-definition | +| integrations.connectors.connectorInstances.get | v1beta | chronicle.soar.integration.connector_instances.get_connector_instance | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1beta | chronicle.soar.integration.connector_instances.list_connector_instances | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1beta | chronicle.soar.integration.connector_instances.update_connector_instance | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1beta | chronicle.soar.integration.connector_instances.run_connector_instance_on_demand | secops integration connector-instances run-on-demand | +| integrations.connectors.connectorInstances.setLogsCollection | v1beta | chronicle.soar.integration.connector_instances.set_connector_instance_logs_collection | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1beta | chronicle.soar.integration.integration_instances.create_integration_instance | secops integration instances create | +| integrations.integrationInstances.delete | v1beta | chronicle.soar.integration.integration_instances.delete_integration_instance | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1beta | chronicle.soar.integration.integration_instances.execute_integration_instance_test | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1beta | chronicle.soar.integration.integration_instances.get_integration_instance_affected_items | secops integration instances get-affected-items | +| integrations.integrationInstances.fetchDefaultInstance | v1beta | chronicle.soar.integration.integration_instances.get_default_integration_instance | secops integration instances get-default | +| integrations.integrationInstances.get | v1beta | chronicle.soar.integration.integration_instances.get_integration_instance | secops integration instances get | +| integrations.integrationInstances.list | v1beta | chronicle.soar.integration.integration_instances.list_integration_instances | secops integration instances list | +| integrations.integrationInstances.patch | v1beta | chronicle.soar.integration.integration_instances.update_integration_instance | secops integration instances update | +| integrations.jobs.create | v1beta | chronicle.soar.integration.jobs.create_integration_job | secops integration jobs create | +| integrations.jobs.delete | v1beta | chronicle.soar.integration.jobs.delete_integration_job | secops integration jobs delete | +| integrations.jobs.executeTest | v1beta | chronicle.soar.integration.jobs.execute_integration_job_test | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1beta | chronicle.soar.integration.jobs.get_integration_job_template | secops integration jobs template | +| integrations.jobs.get | v1beta | chronicle.soar.integration.jobs.get_integration_job | secops integration jobs get | +| integrations.jobs.list | v1beta | chronicle.soar.integration.jobs.list_integration_jobs | secops integration jobs list | +| integrations.jobs.patch | v1beta | chronicle.soar.integration.jobs.update_integration_job | secops integration jobs update | +| integrations.managers.create | v1beta | chronicle.soar.integration.managers.create_integration_manager | secops integration managers create | +| integrations.managers.delete | v1beta | chronicle.soar.integration.managers.delete_integration_manager | secops integration managers delete | +| integrations.managers.fetchTemplate | v1beta | chronicle.soar.integration.managers.get_integration_manager_template | secops integration managers template | +| integrations.managers.get | v1beta | chronicle.soar.integration.managers.get_integration_manager | secops integration managers get | +| integrations.managers.list | v1beta | chronicle.soar.integration.managers.list_integration_managers | secops integration managers list | +| integrations.managers.patch | v1beta | chronicle.soar.integration.managers.update_integration_manager | secops integration managers update | +| integrations.managers.revisions.create | v1beta | chronicle.soar.integration.manager_revisions.create_integration_manager_revision | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1beta | chronicle.soar.integration.manager_revisions.delete_integration_manager_revision | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1beta | chronicle.soar.integration.manager_revisions.get_integration_manager_revision | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1beta | chronicle.soar.integration.manager_revisions.list_integration_manager_revisions | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1beta | chronicle.soar.integration.manager_revisions.rollback_integration_manager_revision | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1beta | chronicle.soar.integration.job_revisions.create_integration_job_revision | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1beta | chronicle.soar.integration.job_revisions.delete_integration_job_revision | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1beta | chronicle.soar.integration.job_revisions.list_integration_job_revisions | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1beta | chronicle.soar.integration.job_revisions.rollback_integration_job_revision | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1beta | chronicle.soar.integration.job_instances.create_integration_job_instance | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1beta | chronicle.soar.integration.job_instances.delete_integration_job_instance | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1beta | chronicle.soar.integration.job_instances.get_integration_job_instance | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1beta | chronicle.soar.integration.job_instances.list_integration_job_instances | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1beta | chronicle.soar.integration.job_instances.update_integration_job_instance | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.soar.integration.job_instances.run_integration_job_instance_on_demand | secops integration job-instances run-on-demand | +| integrations.jobs.contextProperties.clearAll | v1beta | chronicle.soar.integration.job_context_properties.delete_all_job_context_properties | secops integration job-context-properties delete-all | +| integrations.jobs.contextProperties.create | v1beta | chronicle.soar.integration.job_context_properties.create_job_context_property | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1beta | chronicle.soar.integration.job_context_properties.delete_job_context_property | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1beta | chronicle.soar.integration.job_context_properties.get_job_context_property | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1beta | chronicle.soar.integration.job_context_properties.list_job_context_properties | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1beta | chronicle.soar.integration.job_context_properties.update_job_context_property | secops integration job-context-properties update | +| integrations.jobs.jobInstances.logs.get | v1beta | chronicle.soar.integration.job_instance_logs.get_job_instance_log | secops integration job-instance-logs get | +| integrations.jobs.jobInstances.logs.list | v1beta | chronicle.soar.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 | +| 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 | | | +| operations.list | v1beta | | | +| referenceLists.create | v1beta | | | +| referenceLists.get | v1beta | | | +| referenceLists.list | v1beta | | | +| referenceLists.patch | v1beta | | | +| rules.create | v1beta | | | +| rules.delete | v1beta | | | +| rules.deployments.list | v1beta | | | +| rules.get | v1beta | | | +| rules.getDeployment | v1beta | | | +| rules.list | v1beta | | | +| rules.listRevisions | v1beta | | | +| rules.patch | v1beta | | | +| rules.retrohunts.create | v1beta | | | +| rules.retrohunts.get | v1beta | | | +| rules.retrohunts.list | v1beta | | | +| rules.updateDeployment | v1beta | | | +| watchlists.create | v1beta | | | +| watchlists.delete | v1beta | | | +| watchlists.get | v1beta | | | +| watchlists.list | v1beta | | | +| watchlists.patch | v1beta | | | +| analytics.entities.analyticValues.list | v1alpha | | | +| analytics.list | v1alpha | | | +| batchValidateWatchlistEntities | v1alpha | | | +| bigQueryAccess.provide | v1alpha | | | +| bigQueryExport.provision | v1alpha | | | +| cases.countPriorities | v1alpha | | | +| contentHub.featuredContentRules.list | v1alpha | chronicle.featured_content_rules.list_featured_content_rules | secops featured-content-rules list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.batchUpdate | v1alpha | chronicle.rule_set.batch_update_curated_rule_set_deployments | | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.patch | v1alpha | chronicle.rule_set.update_curated_rule_set_deployment | secops curated-rule rule-set-deployment update | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.list | v1alpha | chronicle.rule_set.list_curated_rule_set_deployments | secops curated-rule rule-set-deployment list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.get | v1alpha | chronicle.rule_set.get_curated_rule_set_deployment
chronicle.rule_set.get_curated_rule_set_deployment_by_name | secops curated-rule rule-set-deployment get | +| curatedRuleSetCategories.curatedRuleSets.get | v1alpha | chronicle.rule_set.get_curated_rule_set | secops curated-rule rule-set get | +| curatedRuleSetCategories.curatedRuleSets.list | v1alpha | chronicle.rule_set.list_curated_rule_sets | secops curated-rule rule-set list | +| curatedRuleSetCategories.get | v1alpha | chronicle.rule_set.get_curated_rule_set_category | secops curated-rule rule-set-category get | +| 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 | | | +| integrations.create | v1alpha | | | +| integrations.delete | v1alpha | chronicle.soar.integration.integrations.delete_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations delete | +| integrations.download | v1alpha | chronicle.soar.integration.integrations.download_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations download | +| integrations.downloadDependency | v1alpha | chronicle.soar.integration.integrations.download_integration_dependency(api_version=APIVersion.V1ALPHA) | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1alpha | chronicle.soar.integration.integrations.export_integration_items(api_version=APIVersion.V1ALPHA) | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1alpha | chronicle.soar.integration.integrations.get_integration_affected_items(api_version=APIVersion.V1ALPHA) | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1alpha | chronicle.soar.integration.integrations.get_agent_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1alpha | chronicle.soar.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA) | secops integration integrations get-diff | +| integrations.fetchDependencies | v1alpha | chronicle.soar.integration.integrations.get_integration_dependencies(api_version=APIVersion.V1ALPHA) | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1alpha | chronicle.soar.integration.integrations.get_integration_restricted_agents(api_version=APIVersion.V1ALPHA) | secops integration integrations get-restricted-agents | +| integrations.get | v1alpha | chronicle.soar.integration.integrations.get_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations get | +| integrations.getFetchProductionDiff | v1alpha | chronicle.soar.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA, diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1alpha | chronicle.soar.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.soar.integration.integrations.list_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations list | +| integrations.patch | v1alpha | | | +| integrations.pushToProduction | v1alpha | chronicle.soar.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1alpha | chronicle.soar.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.soar.integration.actions.create_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions create | +| integrations.actions.delete | v1alpha | chronicle.soar.integration.actions.delete_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions delete | +| integrations.actions.executeTest | v1alpha | chronicle.soar.integration.actions.execute_integration_action_test(api_version=APIVersion.V1ALPHA) | secops integration actions test | +| integrations.actions.fetchActionsByEnvironment | v1alpha | chronicle.soar.integration.actions.get_integration_actions_by_environment(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.fetchTemplate | v1alpha | chronicle.soar.integration.actions.get_integration_action_template(api_version=APIVersion.V1ALPHA) | secops integration actions template | +| integrations.actions.get | v1alpha | chronicle.soar.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions get | +| integrations.actions.list | v1alpha | chronicle.soar.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | secops integration actions list | +| integrations.actions.patch | v1alpha | chronicle.soar.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions update | +| integrations.actions.revisions.create | v1alpha | chronicle.soar.integration.action_revisions.create_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1alpha | chronicle.soar.integration.action_revisions.delete_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1alpha | chronicle.soar.integration.action_revisions.list_integration_action_revisions(api_version=APIVersion.V1ALPHA) | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1alpha | chronicle.soar.integration.action_revisions.rollback_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions rollback | +| integrations.connectors.create | v1alpha | chronicle.soar.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors create | +| integrations.connectors.delete | v1alpha | chronicle.soar.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors delete | +| integrations.connectors.executeTest | v1alpha | chronicle.soar.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1alpha | chronicle.soar.integration.connectors.get_integration_connector_template(api_version=APIVersion.V1ALPHA) | secops integration connectors template | +| integrations.connectors.get | v1alpha | chronicle.soar.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors get | +| integrations.connectors.list | v1alpha | chronicle.soar.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | secops integration connectors list | +| integrations.connectors.patch | v1alpha | chronicle.soar.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors update | +| integrations.connectors.revisions.create | v1alpha | chronicle.soar.integration.connector_revisions.create_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1alpha | chronicle.soar.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1alpha | chronicle.soar.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1alpha | chronicle.soar.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions rollback | +| integrations.connectors.contextProperties.clearAll | v1alpha | chronicle.soar.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.soar.integration.connector_context_properties.create_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1alpha | chronicle.soar.integration.connector_context_properties.delete_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1alpha | chronicle.soar.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1alpha | chronicle.soar.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1alpha | chronicle.soar.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.soar.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.soar.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | secops integration connector-instance-logs list | +| integrations.connectors.connectorInstances.create | v1alpha | chronicle.soar.integration.connector_instances.create_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1alpha | chronicle.soar.integration.connector_instances.delete_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1alpha | chronicle.soar.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.soar.integration.connector_instances.get_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1alpha | chronicle.soar.integration.connector_instances.list_connector_instances(api_version=APIVersion.V1ALPHA) | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1alpha | chronicle.soar.integration.connector_instances.update_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1alpha | chronicle.soar.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.soar.integration.connector_instances.set_connector_instance_logs_collection(api_version=APIVersion.V1ALPHA) | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1alpha | chronicle.soar.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances create | +| integrations.integrationInstances.delete | v1alpha | chronicle.soar.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1alpha | chronicle.soar.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1alpha | chronicle.soar.integration.integration_instances.get_integration_instance_affected_items(api_version=APIVersion.V1ALPHA) | secops integration instances get-affected-items | +| integrations.integrationInstances.fetchDefaultInstance | v1alpha | chronicle.soar.integration.integration_instances.get_default_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get-default | +| integrations.integrationInstances.get | v1alpha | chronicle.soar.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get | +| integrations.integrationInstances.list | v1alpha | chronicle.soar.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | secops integration instances list | +| integrations.integrationInstances.patch | v1alpha | chronicle.soar.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances update | +| integrations.transformers.create | v1alpha | chronicle.soar.integration.transformers.create_integration_transformer | secops integration transformers create | +| integrations.transformers.delete | v1alpha | chronicle.soar.integration.transformers.delete_integration_transformer | secops integration transformers delete | +| integrations.transformers.executeTest | v1alpha | chronicle.soar.integration.transformers.execute_integration_transformer_test | secops integration transformers test | +| integrations.transformers.fetchTemplate | v1alpha | chronicle.soar.integration.transformers.get_integration_transformer_template | secops integration transformers template | +| integrations.transformers.get | v1alpha | chronicle.soar.integration.transformers.get_integration_transformer | secops integration transformers get | +| integrations.transformers.list | v1alpha | chronicle.soar.integration.transformers.list_integration_transformers | secops integration transformers list | +| integrations.transformers.patch | v1alpha | chronicle.soar.integration.transformers.update_integration_transformer | secops integration transformers update | +| integrations.transformers.revisions.create | v1alpha | chronicle.soar.integration.transformer_revisions.create_integration_transformer_revision | secops integration transformer-revisions create | +| integrations.transformers.revisions.delete | v1alpha | chronicle.soar.integration.transformer_revisions.delete_integration_transformer_revision | secops integration transformer-revisions delete | +| integrations.transformers.revisions.list | v1alpha | chronicle.soar.integration.transformer_revisions.list_integration_transformer_revisions | secops integration transformer-revisions list | +| integrations.transformers.revisions.rollback | v1alpha | chronicle.soar.integration.transformer_revisions.rollback_integration_transformer_revision | secops integration transformer-revisions rollback | +| integrations.logicalOperators.create | v1alpha | chronicle.soar.integration.logical_operators.create_integration_logical_operator | secops integration logical-operators create | +| integrations.logicalOperators.delete | v1alpha | chronicle.soar.integration.logical_operators.delete_integration_logical_operator | secops integration logical-operators delete | +| integrations.logicalOperators.executeTest | v1alpha | chronicle.soar.integration.logical_operators.execute_integration_logical_operator_test | secops integration logical-operators test | +| integrations.logicalOperators.fetchTemplate | v1alpha | chronicle.soar.integration.logical_operators.get_integration_logical_operator_template | secops integration logical-operators template | +| integrations.logicalOperators.get | v1alpha | chronicle.soar.integration.logical_operators.get_integration_logical_operator | secops integration logical-operators get | +| integrations.logicalOperators.list | v1alpha | chronicle.soar.integration.logical_operators.list_integration_logical_operators | secops integration logical-operators list | +| integrations.logicalOperators.patch | v1alpha | chronicle.soar.integration.logical_operators.update_integration_logical_operator | secops integration logical-operators update | +| integrations.logicalOperators.revisions.create | v1alpha | chronicle.soar.integration.logical_operator_revisions.create_integration_logical_operator_revision | secops integration logical-operator-revisions create | +| integrations.logicalOperators.revisions.delete | v1alpha | chronicle.soar.integration.logical_operator_revisions.delete_integration_logical_operator_revision | secops integration logical-operator-revisions delete | +| integrations.logicalOperators.revisions.list | v1alpha | chronicle.soar.integration.logical_operator_revisions.list_integration_logical_operator_revisions | secops integration logical-operator-revisions list | +| integrations.logicalOperators.revisions.rollback | v1alpha | chronicle.soar.integration.logical_operator_revisions.rollback_integration_logical_operator_revision | secops integration logical-operator-revisions rollback | +| integrations.jobs.create | v1alpha | chronicle.soar.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs create | +| integrations.jobs.delete | v1alpha | chronicle.soar.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs delete | +| integrations.jobs.executeTest | v1alpha | chronicle.soar.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1alpha | chronicle.soar.integration.jobs.get_integration_job_template(api_version=APIVersion.V1ALPHA) | secops integration jobs template | +| integrations.jobs.get | v1alpha | chronicle.soar.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs get | +| integrations.jobs.list | v1alpha | chronicle.soar.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | secops integration jobs list | +| integrations.jobs.patch | v1alpha | chronicle.soar.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs update | +| integrations.managers.create | v1alpha | chronicle.soar.integration.managers.create_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers create | +| integrations.managers.delete | v1alpha | chronicle.soar.integration.managers.delete_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers delete | +| integrations.managers.fetchTemplate | v1alpha | chronicle.soar.integration.managers.get_integration_manager_template(api_version=APIVersion.V1ALPHA) | secops integration managers template | +| integrations.managers.get | v1alpha | chronicle.soar.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers get | +| integrations.managers.list | v1alpha | chronicle.soar.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | secops integration managers list | +| integrations.managers.patch | v1alpha | chronicle.soar.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers update | +| integrations.managers.revisions.create | v1alpha | chronicle.soar.integration.manager_revisions.create_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1alpha | chronicle.soar.integration.manager_revisions.delete_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1alpha | chronicle.soar.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1alpha | chronicle.soar.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1alpha | chronicle.soar.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1alpha | chronicle.soar.integration.job_revisions.create_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1alpha | chronicle.soar.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1alpha | chronicle.soar.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1alpha | chronicle.soar.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1alpha | chronicle.soar.integration.job_instances.create_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1alpha | chronicle.soar.integration.job_instances.delete_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1alpha | chronicle.soar.integration.job_instances.get_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1alpha | chronicle.soar.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1alpha | chronicle.soar.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.soar.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.soar.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.soar.integration.job_context_properties.create_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1alpha | chronicle.soar.integration.job_context_properties.delete_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1alpha | chronicle.soar.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1alpha | chronicle.soar.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1alpha | chronicle.soar.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.soar.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.soar.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 | +| 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) | 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 | +| 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 | | | +| REST Resource | Version | secops-wrapper module | CLI Command | +| -------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| dataAccessLabels.create | v1 | | | +| dataAccessLabels.delete | v1 | | | +| dataAccessLabels.get | v1 | | | +| dataAccessLabels.list | v1 | | | +| dataAccessLabels.patch | v1 | | | +| dataAccessScopes.create | v1 | | | +| dataAccessScopes.delete | v1 | | | +| dataAccessScopes.get | v1 | | | +| dataAccessScopes.list | v1 | | | +| dataAccessScopes.patch | v1 | | | +| get | v1 | | | +| operations.cancel | v1 | | | +| operations.delete | v1 | | | +| operations.get | v1 | | | +| operations.list | v1 | | | +| referenceLists.create | v1 | chronicle.reference_list.create_reference_list | secops reference-list create | +| referenceLists.get | v1 | chronicle.reference_list.get_reference_list | secops reference-list get | +| referenceLists.list | v1 | chronicle.reference_list.list_reference_lists | secops reference-list list | +| referenceLists.patch | v1 | chronicle.reference_list.update_reference_list | secops reference-list update | +| rules.create | v1 | chronicle.rule.create_rule | secops rule create | +| rules.delete | v1 | chronicle.rule.delete_rule | secops rule delete | +| rules.deployments.list | v1 | | | +| rules.get | v1 | chronicle.rule.get_rule | secops rule get | +| rules.getDeployment | v1 | | | +| rules.list | v1 | chronicle.rule.list_rules | secops rule list | +| rules.listRevisions | v1 | | | +| rules.patch | v1 | chronicle.rule.update_rule | secops rule update | +| rules.retrohunts.create | v1 | chronicle.rule_retrohunt.create_retrohunt | secops rule-retrohunt create | +| rules.retrohunts.get | v1 | chronicle.rule_retrohunt.get_retrohunt | secops rule-retrohunt get | +| rules.retrohunts.list | v1 | chronicle.rule_retrohunt.list_retrohunts | secops rule-retrohunt list | +| rules.updateDeployment | v1 | chronicle.rule.enable_rule | secops rule enable | +| watchlists.create | v1 | chronicle.watchlist.create_watchlist | secops watchlist create | +| 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 | +| dataAccessLabels.create | v1beta | | | +| dataAccessLabels.delete | v1beta | | | +| dataAccessLabels.get | v1beta | | | +| dataAccessLabels.list | v1beta | | | +| dataAccessLabels.patch | v1beta | | | +| dataAccessScopes.create | v1beta | | | +| dataAccessScopes.delete | v1beta | | | +| dataAccessScopes.get | v1beta | | | +| dataAccessScopes.list | v1beta | | | +| dataAccessScopes.patch | v1beta | | | +| get | v1beta | | | +| operations.cancel | v1beta | | | +| operations.delete | v1beta | | | +| operations.get | v1beta | | | +| operations.list | v1beta | | | +| referenceLists.create | v1beta | | | +| referenceLists.get | v1beta | | | +| referenceLists.list | v1beta | | | +| referenceLists.patch | v1beta | | | +| rules.create | v1beta | | | +| rules.delete | v1beta | | | +| rules.deployments.list | v1beta | | | +| rules.get | v1beta | | | +| rules.getDeployment | v1beta | | | +| rules.list | v1beta | | | +| rules.listRevisions | v1beta | | | +| rules.patch | v1beta | | | +| rules.retrohunts.create | v1beta | | | +| rules.retrohunts.get | v1beta | | | +| rules.retrohunts.list | v1beta | | | +| rules.updateDeployment | v1beta | | | +| watchlists.create | v1beta | | | +| watchlists.delete | v1beta | | | +| watchlists.get | v1beta | | | +| watchlists.list | v1beta | | | +| watchlists.patch | v1beta | | | +| cases.executeBulkAddTag | v1beta | chronicle.case.execute_bulk_add_tag | secops case bulk-add-tag | +| cases.executeBulkAssign | v1beta | chronicle.case.execute_bulk_assign | secops case bulk-assign | +| cases.executeBulkChangePriority | v1beta | chronicle.case.execute_bulk_change_priority | secops case bulk-change-priority | +| cases.executeBulkChangeStage | v1beta | chronicle.case.execute_bulk_change_stage | secops case bulk-change-stage | +| cases.executeBulkClose | v1beta | chronicle.case.execute_bulk_close | secops case bulk-close | +| cases.executeBulkReopen | v1beta | chronicle.case.execute_bulk_reopen | secops case bulk-reopen | +| cases.get | v1beta | chronicle.case.get_case | secops case get | +| cases.list | v1beta | chronicle.case.list_cases | secops case list | +| cases.merge | v1beta | chronicle.case.merge_cases | secops case merge | +| cases.patch | v1beta | chronicle.case.patch_case | secops case update | +| analytics.entities.analyticValues.list | v1alpha | | | +| analytics.list | v1alpha | | | +| batchValidateWatchlistEntities | v1alpha | | | +| bigQueryAccess.provide | v1alpha | | | +| bigQueryExport.provision | v1alpha | | | +| cases.countPriorities | v1alpha | | | +| contentHub.featuredContentRules.list | v1alpha | chronicle.featured_content_rules.list_featured_content_rules | secops featured-content-rules list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.batchUpdate | v1alpha | chronicle.rule_set.batch_update_curated_rule_set_deployments | | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.patch | v1alpha | chronicle.rule_set.update_curated_rule_set_deployment | secops curated-rule rule-set-deployment update | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.list | v1alpha | chronicle.rule_set.list_curated_rule_set_deployments | secops curated-rule rule-set-deployment list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.get | v1alpha | chronicle.rule_set.get_curated_rule_set_deployment
chronicle.rule_set.get_curated_rule_set_deployment_by_name | secops curated-rule rule-set-deployment get | +| curatedRuleSetCategories.curatedRuleSets.get | v1alpha | chronicle.rule_set.get_curated_rule_set | secops curated-rule rule-set get | +| curatedRuleSetCategories.curatedRuleSets.list | v1alpha | chronicle.rule_set.list_curated_rule_sets | secops curated-rule rule-set list | +| curatedRuleSetCategories.get | v1alpha | chronicle.rule_set.get_curated_rule_set_category | secops curated-rule rule-set-category get | +| 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 | chronicle.log_search.search_raw_logs | secops search raw-logs | +| 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 | | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 258f3717..76d4150f 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -227,26 +227,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.marketplace_integrations import ( - list_marketplace_integrations, - get_marketplace_integration, - get_marketplace_integration_diff, - install_marketplace_integration, - uninstall_marketplace_integration, -) __all__ = [ # Client @@ -438,22 +418,4 @@ "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", - # 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 2a78e83c..da7c9311 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -26,6 +26,7 @@ # pylint: disable=line-too-long from secops import auth as secops_auth from secops.auth import RetryConfig +from secops.chronicle.soar import SOARService from secops.chronicle.alert import get_alerts as _get_alerts from secops.chronicle.case import ( execute_bulk_add_tag as _execute_bulk_add_tag, @@ -38,7 +39,7 @@ get_cases_from_list, list_cases as _list_cases, merge_cases as _merge_cases, - patch_case as _patch_case + patch_case as _patch_case, ) from secops.chronicle.dashboard import ( DashboardAccessType, @@ -142,40 +143,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.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, CaseCloseReason, @@ -183,15 +150,9 @@ CasePriority, DashboardChart, DashboardQuery, - DiffType, EntitySummary, InputInterval, - IntegrationInstanceParameter, - IntegrationType, - PythonVersion, - TargetMode, TileType, - IntegrationParam, ) from secops.chronicle.nl_search import ( nl_search as _nl_search, @@ -465,6 +426,7 @@ def __init__( self.default_api_version = APIVersion(default_api_version) self._default_forwarder_display_name: str = "Wrapper-SDK-Forwarder" self._cached_default_forwarder_id: str | None = None + self.soar = SOARService(self) # Format the instance ID to match the expected format if region in ["dev", "staging"]: @@ -729,1021 +691,6 @@ def update_watchlist( update_mask, ) - # ------------------------------------------------------------------------- - # Marketplace Integration methods - # ------------------------------------------------------------------------- - - 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 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. - - Returns: - If as_list is True: List of marketplace integration. - If as_list is False: Dict with marketplace integration 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: - Empty dictionary if uninstallation is successful - - Raises: - APIError: If the API request fails - """ - return _uninstall_marketplace_integration( - self, integration_name, api_version - ) - - # ------------------------------------------------------------------------- - # Integration methods - # ------------------------------------------------------------------------- - - 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] | list[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. - 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 integrations. - 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 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 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/soar/__init__.py b/src/secops/chronicle/soar/__init__.py new file mode 100644 index 00000000..0e972623 --- /dev/null +++ b/src/secops/chronicle/soar/__init__.py @@ -0,0 +1,74 @@ +# 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. +# +"""Chronicle SOAR API specific functionality.""" + +from secops.chronicle.soar.client import SOARService + +from secops.chronicle.soar.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.soar.integration.integration_instances import ( + list_integration_instances, + get_integration_instance, + delete_integration_instance, + create_integration_instance, + update_integration_instance, +) +from secops.chronicle.soar.integration.marketplace_integrations import ( + list_marketplace_integrations, + get_marketplace_integration, + get_marketplace_integration_diff, + install_marketplace_integration, + uninstall_marketplace_integration, +) + +__all__ = [ + # client + "SOARService", + # 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", + # Marketplace Integrations + "list_marketplace_integrations", + "get_marketplace_integration", + "get_marketplace_integration_diff", + "install_marketplace_integration", + "uninstall_marketplace_integration", + # Integration Instances + "list_integration_instances", + "get_integration_instance", + "delete_integration_instance", + "create_integration_instance", + "update_integration_instance", +] diff --git a/src/secops/chronicle/soar/client.py b/src/secops/chronicle/soar/client.py new file mode 100644 index 00000000..0f3d27a8 --- /dev/null +++ b/src/secops/chronicle/soar/client.py @@ -0,0 +1,1093 @@ +# 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. +# +"""Chronicle SOAR API client.""" + +from typing import TYPE_CHECKING, Any + +# pylint: disable=line-too-long +from secops.chronicle.models import ( + APIVersion, + DiffType, + IntegrationInstanceParameter, + IntegrationType, + PythonVersion, + TargetMode, + IntegrationParam, +) +from secops.chronicle.soar.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.soar.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.soar.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, +) + +# pylint: enable=line-too-long + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +class SOARService: + """Namespace for all SOAR-related operations in Google SecOps.""" + + def __init__(self, client: "ChronicleClient"): + self._client = client + + # ------------------------------------------------------------------------- + # Marketplace Integration methods + # ------------------------------------------------------------------------- + + 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 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. + + Returns: + If as_list is True: List of marketplace integration. + If as_list is False: Dict with marketplace integration list and + nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_marketplace_integrations( + self._client, + 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._client, 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._client, 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._client, + 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: + Empty dictionary if uninstallation is successful + + Raises: + APIError: If the API request fails + """ + return _uninstall_marketplace_integration( + self._client, integration_name, api_version + ) + + # ------------------------------------------------------------------------- + # Integration methods + # ------------------------------------------------------------------------- + + 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] | list[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. + 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 integrations. + If as_list is False: Dict with integration list and nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_integrations( + self._client, + 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._client, 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._client, 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._client, + 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._client, 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._client, 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._client, + 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._client, 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._client, 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._client, 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._client, 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._client, + 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._client, 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._client, + 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._client, + 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 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._client, + 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._client, + 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._client, + 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._client, + 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._client, + 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._client, + 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._client, + 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._client, + integration_name, + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/__init__.py b/src/secops/chronicle/soar/integration/__init__.py similarity index 100% rename from src/secops/chronicle/integration/__init__.py rename to src/secops/chronicle/soar/integration/__init__.py diff --git a/src/secops/chronicle/integration/integration_instances.py b/src/secops/chronicle/soar/integration/integration_instances.py similarity index 100% rename from src/secops/chronicle/integration/integration_instances.py rename to src/secops/chronicle/soar/integration/integration_instances.py diff --git a/src/secops/chronicle/integration/integrations.py b/src/secops/chronicle/soar/integration/integrations.py similarity index 100% rename from src/secops/chronicle/integration/integrations.py rename to src/secops/chronicle/soar/integration/integrations.py diff --git a/src/secops/chronicle/integration/marketplace_integrations.py b/src/secops/chronicle/soar/integration/marketplace_integrations.py similarity index 100% rename from src/secops/chronicle/integration/marketplace_integrations.py rename to src/secops/chronicle/soar/integration/marketplace_integrations.py diff --git a/src/secops/cli/commands/integration/integration.py b/src/secops/cli/commands/integration/integration.py index dd73a600..55c94f05 100644 --- a/src/secops/cli/commands/integration/integration.py +++ b/src/secops/cli/commands/integration/integration.py @@ -524,7 +524,7 @@ def setup_integrations_command(subparsers): def handle_integration_list_command(args, chronicle): """Handle list integrations command""" try: - out = chronicle.list_integrations( + out = chronicle.soar.list_integrations( page_size=args.page_size, page_token=args.page_token, filter_string=args.filter_string, @@ -540,7 +540,7 @@ def handle_integration_list_command(args, chronicle): def handle_integration_get_command(args, chronicle): """Handle get integration command""" try: - out = chronicle.get_integration( + out = chronicle.soar.get_integration( integration_name=args.integration_id, ) output_formatter(out, getattr(args, "output", "json")) @@ -552,7 +552,7 @@ def handle_integration_get_command(args, chronicle): def handle_integration_delete_command(args, chronicle): """Handle delete integration command""" try: - chronicle.delete_integration( + chronicle.soar.delete_integration( integration_name=args.integration_id, ) print(f"Integration {args.integration_id} deleted successfully.") @@ -564,7 +564,7 @@ def handle_integration_delete_command(args, chronicle): def handle_integration_create_command(args, chronicle): """Handle create integration command""" try: - out = chronicle.create_integration( + out = chronicle.soar.create_integration( display_name=args.display_name, staging=args.staging, description=args.description, @@ -607,7 +607,7 @@ def handle_integration_download_command(args, chronicle): def handle_download_integration_dependency_command(args, chronicle): """Handle download integration dependencies command""" try: - out = chronicle.download_integration_dependency( + out = chronicle.soar.download_integration_dependency( integration_name=args.integration_id, dependency_name=args.dependency_name, ) @@ -642,7 +642,7 @@ def handle_export_integration_items_command(args, chronicle): def handle_get_integration_affected_items_command(args, chronicle): """Handle get integration affected items command""" try: - out = chronicle.get_integration_affected_items( + out = chronicle.soar.get_integration_affected_items( integration_name=args.integration_id, ) output_formatter(out, getattr(args, "output", "json")) @@ -654,7 +654,7 @@ def handle_get_integration_affected_items_command(args, chronicle): def handle_get_agent_integrations_command(args, chronicle): """Handle get agent integration command""" try: - out = chronicle.get_agent_integrations( + out = chronicle.soar.get_agent_integrations( agent_id=args.agent_id, ) output_formatter(out, getattr(args, "output", "json")) @@ -666,7 +666,7 @@ def handle_get_agent_integrations_command(args, chronicle): def handle_get_integration_dependencies_command(args, chronicle): """Handle get integration dependencies command""" try: - out = chronicle.get_integration_dependencies( + out = chronicle.soar.get_integration_dependencies( integration_name=args.integration_id, ) output_formatter(out, getattr(args, "output", "json")) @@ -678,7 +678,7 @@ def handle_get_integration_dependencies_command(args, chronicle): def handle_get_integration_restricted_agents_command(args, chronicle): """Handle get integration restricted agent command""" try: - out = chronicle.get_integration_restricted_agents( + out = chronicle.soar.get_integration_restricted_agents( integration_name=args.integration_id, required_python_version=PythonVersion(args.required_python_version), push_request=args.push_request, @@ -694,7 +694,7 @@ def handle_get_integration_restricted_agents_command(args, chronicle): def handle_get_integration_diff_command(args, chronicle): """Handle get integration diff command""" try: - out = chronicle.get_integration_diff( + out = chronicle.soar.get_integration_diff( integration_name=args.integration_id, diff_type=DiffType(args.diff_type), ) @@ -707,7 +707,7 @@ def handle_get_integration_diff_command(args, chronicle): def handle_transition_integration_command(args, chronicle): """Handle transition integration command""" try: - out = chronicle.transition_integration( + out = chronicle.soar.transition_integration( integration_name=args.integration_id, target_mode=TargetMode(args.target_mode), ) @@ -720,7 +720,7 @@ def handle_transition_integration_command(args, chronicle): def handle_update_integration_command(args, chronicle): """Handle update integration command""" try: - out = chronicle.update_integration( + out = chronicle.soar.update_integration( integration_name=args.integration_id, display_name=args.display_name, description=args.description, @@ -749,7 +749,7 @@ def handle_update_integration_command(args, chronicle): def handle_updated_custom_integration_command(args, chronicle): """Handle update custom integration command""" try: - out = chronicle.update_custom_integration( + out = chronicle.soar.update_custom_integration( integration_name=args.integration_id, display_name=args.display_name, description=args.description, diff --git a/src/secops/cli/commands/integration/integration_instances.py b/src/secops/cli/commands/integration/integration_instances.py index 2d375346..e9e78d26 100644 --- a/src/secops/cli/commands/integration/integration_instances.py +++ b/src/secops/cli/commands/integration/integration_instances.py @@ -250,7 +250,7 @@ def setup_integration_instances_command(subparsers): def handle_integration_instances_list_command(args, chronicle): """Handle integration instances list command""" try: - out = chronicle.list_integration_instances( + out = chronicle.soar.list_integration_instances( integration_name=args.integration_name, page_size=args.page_size, page_token=args.page_token, @@ -267,7 +267,7 @@ def handle_integration_instances_list_command(args, chronicle): def handle_integration_instances_get_command(args, chronicle): """Handle integration instance get command""" try: - out = chronicle.get_integration_instance( + out = chronicle.soar.get_integration_instance( integration_name=args.integration_name, integration_instance_id=args.instance_id, ) @@ -280,7 +280,7 @@ def handle_integration_instances_get_command(args, chronicle): def handle_integration_instances_delete_command(args, chronicle): """Handle integration instance delete command""" try: - chronicle.delete_integration_instance( + chronicle.soar.delete_integration_instance( integration_name=args.integration_name, integration_instance_id=args.instance_id, ) @@ -299,7 +299,7 @@ def handle_integration_instances_create_command(args, chronicle): if args.config: config = json.loads(args.config) - out = chronicle.create_integration_instance( + out = chronicle.soar.create_integration_instance( integration_name=args.integration_name, display_name=args.display_name, environment=args.environment, @@ -325,7 +325,7 @@ def handle_integration_instances_update_command(args, chronicle): if args.config: config = json.loads(args.config) - out = chronicle.update_integration_instance( + out = chronicle.soar.update_integration_instance( integration_name=args.integration_name, integration_instance_id=args.instance_id, display_name=args.display_name, @@ -346,12 +346,12 @@ def handle_integration_instances_test_command(args, chronicle): """Handle integration instance test command""" try: # Get the instance first - instance = chronicle.get_integration_instance( + instance = chronicle.soar.get_integration_instance( integration_name=args.integration_name, integration_instance_id=args.instance_id, ) - out = chronicle.execute_integration_instance_test( + out = chronicle.soar.execute_integration_instance_test( integration_name=args.integration_name, integration_instance_id=args.instance_id, integration_instance=instance, @@ -365,7 +365,7 @@ def handle_integration_instances_test_command(args, chronicle): 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( + out = chronicle.soar.get_integration_instance_affected_items( integration_name=args.integration_name, integration_instance_id=args.instance_id, ) @@ -381,7 +381,7 @@ def handle_integration_instances_get_affected_items_command(args, chronicle): def handle_integration_instances_get_default_command(args, chronicle): """Handle get default integration instance command""" try: - out = chronicle.get_default_integration_instance( + out = chronicle.soar.get_default_integration_instance( integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) diff --git a/src/secops/cli/commands/integration/marketplace_integration.py b/src/secops/cli/commands/integration/marketplace_integration.py index f8b87aa2..5ce0467c 100644 --- a/src/secops/cli/commands/integration/marketplace_integration.py +++ b/src/secops/cli/commands/integration/marketplace_integration.py @@ -135,7 +135,7 @@ def setup_marketplace_integrations_command(subparsers): def handle_mp_integration_list_command(args, chronicle): """Handle marketplace integration list command""" try: - out = chronicle.list_marketplace_integrations( + out = chronicle.soar.list_marketplace_integrations( page_size=args.page_size, page_token=args.page_token, filter_string=args.filter_string, @@ -151,7 +151,7 @@ def handle_mp_integration_list_command(args, chronicle): def handle_mp_integration_get_command(args, chronicle): """Handle marketplace integration get command""" try: - out = chronicle.get_marketplace_integration( + out = chronicle.soar.get_marketplace_integration( integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) @@ -163,7 +163,7 @@ def handle_mp_integration_get_command(args, chronicle): def handle_mp_integration_diff_command(args, chronicle): """Handle marketplace integration diff command""" try: - out = chronicle.get_marketplace_integration_diff( + out = chronicle.soar.get_marketplace_integration_diff( integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) @@ -177,7 +177,7 @@ def handle_mp_integration_diff_command(args, chronicle): def handle_mp_integration_install_command(args, chronicle): """Handle marketplace integration install command""" try: - out = chronicle.install_marketplace_integration( + out = chronicle.soar.install_marketplace_integration( integration_name=args.integration_name, override_mapping=args.override_mapping, staging=args.staging, @@ -193,7 +193,7 @@ def handle_mp_integration_install_command(args, chronicle): def handle_mp_integration_uninstall_command(args, chronicle): """Handle marketplace integration uninstall command""" try: - out = chronicle.uninstall_marketplace_integration( + out = chronicle.soar.uninstall_marketplace_integration( integration_name=args.integration_name, ) output_formatter(out, getattr(args, "output", "json")) diff --git a/tests/chronicle/integration/__init__.py b/tests/chronicle/soar/__init__.py similarity index 100% rename from tests/chronicle/integration/__init__.py rename to tests/chronicle/soar/__init__.py diff --git a/tests/chronicle/integration/test_integration_instances.py b/tests/chronicle/soar/integration/test_integration_instances.py similarity index 88% rename from tests/chronicle/integration/test_integration_instances.py rename to tests/chronicle/soar/integration/test_integration_instances.py index 153390ad..fa65ecad 100644 --- a/tests/chronicle/integration/test_integration_instances.py +++ b/tests/chronicle/soar/integration/test_integration_instances.py @@ -23,7 +23,7 @@ APIVersion, IntegrationInstanceParameter, ) -from secops.chronicle.integration.integration_instances import ( +from secops.chronicle.soar.integration.integration_instances import ( list_integration_instances, get_integration_instance, delete_integration_instance, @@ -61,10 +61,10 @@ def test_list_integration_instances_success(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_paginated_request", return_value=expected, ) as mock_paginated, patch( - "secops.chronicle.integration.integration_instances.format_resource_id", + "secops.chronicle.soar.integration.integration_instances.format_resource_id", return_value="My Integration", ): result = list_integration_instances( @@ -88,7 +88,7 @@ def test_list_integration_instances_default_args(chronicle_client): expected = {"integrationInstances": []} with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_paginated_request", return_value=expected, ): result = list_integration_instances( @@ -104,7 +104,7 @@ def test_list_integration_instances_with_filters(chronicle_client): expected = {"integrationInstances": [{"name": "ii1"}]} with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_integration_instances( @@ -128,7 +128,7 @@ def test_list_integration_instances_as_list(chronicle_client): expected = [{"name": "ii1"}, {"name": "ii2"}] with patch( - "secops.chronicle.integration.integration_instances.chronicle_paginated_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_integration_instances( @@ -146,7 +146,7 @@ def test_list_integration_instances_as_list(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_paginated_request", side_effect=APIError("Failed to list integration instances"), ): with pytest.raises(APIError) as exc_info: @@ -169,7 +169,7 @@ def test_get_integration_instance_success(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_instance( @@ -189,7 +189,7 @@ def test_get_integration_instance_success(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to get integration instance"), ): with pytest.raises(APIError) as exc_info: @@ -207,7 +207,7 @@ def test_get_integration_instance_error(chronicle_client): def test_delete_integration_instance_success(chronicle_client): """Test delete_integration_instance issues DELETE request.""" with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=None, ) as mock_request: delete_integration_instance( @@ -225,7 +225,7 @@ def test_delete_integration_instance_success(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to delete integration instance"), ): with pytest.raises(APIError) as exc_info: @@ -245,7 +245,7 @@ def test_create_integration_instance_required_fields_only(chronicle_client): expected = {"name": "integrationInstances/new", "displayName": "My Instance"} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = create_integration_instance( @@ -272,7 +272,7 @@ def test_create_integration_instance_with_optional_fields(chronicle_client): expected = {"name": "integrationInstances/new"} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = create_integration_instance( @@ -302,7 +302,7 @@ def test_create_integration_instance_with_dataclass_params(chronicle_client): param = IntegrationInstanceParameter(value="test-value") with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = create_integration_instance( @@ -323,7 +323,7 @@ def test_create_integration_instance_with_dataclass_params(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to create integration instance"), ): with pytest.raises(APIError) as exc_info: @@ -343,7 +343,7 @@ def test_update_integration_instance_with_single_field(chronicle_client): expected = {"name": "integrationInstances/ii1", "displayName": "Updated"} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = update_integration_instance( @@ -367,7 +367,7 @@ def test_update_integration_instance_with_multiple_fields(chronicle_client): expected = {"name": "integrationInstances/ii1"} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = update_integration_instance( @@ -395,7 +395,7 @@ def test_update_integration_instance_with_custom_update_mask(chronicle_client): expected = {"name": "integrationInstances/ii1"} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = update_integration_instance( @@ -419,7 +419,7 @@ def test_update_integration_instance_with_dataclass_params(chronicle_client): param = IntegrationInstanceParameter(value="test-value") with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = update_integration_instance( @@ -440,7 +440,7 @@ def test_update_integration_instance_with_dataclass_params(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to update integration instance"), ): with pytest.raises(APIError) as exc_info: @@ -464,7 +464,7 @@ def test_execute_integration_instance_test_success(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = execute_integration_instance_test( @@ -489,7 +489,7 @@ def test_execute_integration_instance_test_failure(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ): result = execute_integration_instance_test( @@ -505,7 +505,7 @@ def test_execute_integration_instance_test_failure(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to execute test"), ): with pytest.raises(APIError) as exc_info: @@ -530,7 +530,7 @@ def test_get_integration_instance_affected_items_success(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_instance_affected_items( @@ -552,7 +552,7 @@ def test_get_integration_instance_affected_items_empty(chronicle_client): expected = {"affectedPlaybooks": []} with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ): result = get_integration_instance_affected_items( @@ -568,7 +568,7 @@ def test_get_integration_instance_affected_items_empty(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to fetch affected items"), ): with pytest.raises(APIError) as exc_info: @@ -592,7 +592,7 @@ def test_get_default_integration_instance_success(chronicle_client): } with patch( - "secops.chronicle.integration.integration_instances.chronicle_request", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", return_value=expected, ) as mock_request: result = get_default_integration_instance( @@ -611,7 +611,7 @@ def test_get_default_integration_instance_success(chronicle_client): 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", + "secops.chronicle.soar.integration.integration_instances.chronicle_request", side_effect=APIError("Failed to get default instance"), ): with pytest.raises(APIError) as exc_info: diff --git a/tests/chronicle/integration/test_integrations.py b/tests/chronicle/soar/integration/test_integrations.py similarity index 89% rename from tests/chronicle/integration/test_integrations.py rename to tests/chronicle/soar/integration/test_integrations.py index 811ab052..0c508260 100644 --- a/tests/chronicle/integration/test_integrations.py +++ b/tests/chronicle/soar/integration/test_integrations.py @@ -25,7 +25,7 @@ TargetMode, PythonVersion, ) -from secops.chronicle.integration.integrations import ( +from secops.chronicle.soar.integration.integrations import ( list_integrations, get_integration, delete_integration, @@ -86,7 +86,7 @@ def test_list_integrations_success(chronicle_client): expected = {"integrations": [{"name": "i1"}, {"name": "i2"}]} with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", + "secops.chronicle.soar.integration.integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_integrations( @@ -114,7 +114,7 @@ def test_list_integrations_with_filter_and_order_by(chronicle_client): expected = {"integrations": []} with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", + "secops.chronicle.soar.integration.integrations.chronicle_paginated_request", return_value=expected, ) as mock_paginated: result = list_integrations( @@ -144,7 +144,7 @@ def test_list_integrations_with_filter_and_order_by(chronicle_client): def test_list_integrations_error(chronicle_client): """Test list_integrations propagates APIError from helper.""" with patch( - "secops.chronicle.integration.integrations.chronicle_paginated_request", + "secops.chronicle.soar.integration.integrations.chronicle_paginated_request", side_effect=APIError("Failed to list integrations"), ): with pytest.raises(APIError) as exc_info: @@ -161,7 +161,7 @@ def test_get_integration_success(chronicle_client): expected = {"name": "integrations/test-integration", "displayName": "Test"} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_integration(chronicle_client, "test-integration") @@ -179,7 +179,7 @@ def test_get_integration_success(chronicle_client): def test_get_integration_error(chronicle_client): """Test get_integration raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to get integration"), ): with pytest.raises(APIError) as exc_info: @@ -193,7 +193,7 @@ def test_get_integration_error(chronicle_client): 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: + with patch("secops.chronicle.soar.integration.integrations.chronicle_request") as mock_request: delete_integration(chronicle_client, "test-integration") mock_request.assert_called_once_with( @@ -207,7 +207,7 @@ def test_delete_integration_success(chronicle_client): def test_delete_integration_error(chronicle_client): """Test delete_integration propagates APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to delete integration"), ): with pytest.raises(APIError) as exc_info: @@ -224,7 +224,7 @@ def test_create_integration_required_fields_only(chronicle_client): expected = {"name": "integrations/test", "displayName": "Test"} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = create_integration( @@ -252,7 +252,7 @@ def test_create_integration_all_optional_fields(chronicle_client): integration_type = Mock(name="integration_type") with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = create_integration( @@ -292,7 +292,7 @@ def test_create_integration_all_optional_fields(chronicle_client): 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", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value={"name": "integrations/test"}, ) as mock_request: create_integration( @@ -320,7 +320,7 @@ def test_create_integration_none_fields_excluded(chronicle_client): def test_create_integration_error(chronicle_client): """Test create_integration raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to create integration"), ): with pytest.raises(APIError) as exc_info: @@ -337,7 +337,7 @@ def test_download_integration_success(chronicle_client): expected = b"ZIPBYTES" with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", + "secops.chronicle.soar.integration.integrations.chronicle_request_bytes", return_value=expected, ) as mock_bytes: result = download_integration(chronicle_client, "test-integration") @@ -357,7 +357,7 @@ def test_download_integration_success(chronicle_client): def test_download_integration_error(chronicle_client): """Test download_integration propagates APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", + "secops.chronicle.soar.integration.integrations.chronicle_request_bytes", side_effect=APIError("Failed to download integration"), ): with pytest.raises(APIError) as exc_info: @@ -374,7 +374,7 @@ def test_download_integration_dependency_success(chronicle_client): expected = {"dependency": "requests"} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = download_integration_dependency( @@ -397,7 +397,7 @@ def test_download_integration_dependency_success(chronicle_client): def test_download_integration_dependency_error(chronicle_client): """Test download_integration_dependency raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to download dependency"), ): with pytest.raises(APIError) as exc_info: @@ -418,7 +418,7 @@ def test_export_integration_items_success_some_fields(chronicle_client): expected = b"ZIPBYTES" with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", + "secops.chronicle.soar.integration.integrations.chronicle_request_bytes", return_value=expected, ) as mock_bytes: result = export_integration_items( @@ -451,7 +451,7 @@ def test_export_integration_items_no_fields(chronicle_client): expected = b"ZIPBYTES" with patch( - "secops.chronicle.integration.integrations.chronicle_request_bytes", + "secops.chronicle.soar.integration.integrations.chronicle_request_bytes", return_value=expected, ) as mock_bytes: result = export_integration_items( @@ -474,7 +474,7 @@ def test_export_integration_items_no_fields(chronicle_client): 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", + "secops.chronicle.soar.integration.integrations.chronicle_request_bytes", side_effect=APIError("Failed to export integration items"), ): with pytest.raises(APIError) as exc_info: @@ -491,7 +491,7 @@ def test_get_integration_affected_items_success(chronicle_client): expected = {"affectedItems": []} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_affected_items(chronicle_client, "test-integration") @@ -509,7 +509,7 @@ def test_get_integration_affected_items_success(chronicle_client): 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", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to fetch affected items"), ): with pytest.raises(APIError) as exc_info: @@ -526,7 +526,7 @@ def test_get_agent_integrations_success(chronicle_client): expected = {"integrations": []} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_agent_integrations(chronicle_client, agent_id="agent-123") @@ -545,7 +545,7 @@ def test_get_agent_integrations_success(chronicle_client): def test_get_agent_integrations_error(chronicle_client): """Test get_agent_integrations raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to fetch agent integrations"), ): with pytest.raises(APIError) as exc_info: @@ -562,7 +562,7 @@ def test_get_integration_dependencies_success(chronicle_client): expected = {"dependencies": []} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_dependencies(chronicle_client, "test-integration") @@ -580,7 +580,7 @@ def test_get_integration_dependencies_success(chronicle_client): def test_get_integration_dependencies_error(chronicle_client): """Test get_integration_dependencies raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to fetch dependencies"), ): with pytest.raises(APIError) as exc_info: @@ -597,7 +597,7 @@ def test_get_integration_restricted_agents_success(chronicle_client): expected = {"restrictedAgents": []} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_restricted_agents( @@ -626,7 +626,7 @@ def test_get_integration_restricted_agents_default_push_request(chronicle_client expected = {"restrictedAgents": []} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: get_integration_restricted_agents( @@ -650,7 +650,7 @@ def test_get_integration_restricted_agents_default_push_request(chronicle_client 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", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to fetch restricted agents"), ): with pytest.raises(APIError) as exc_info: @@ -671,7 +671,7 @@ def test_get_integration_diff_success(chronicle_client): expected = {"diff": {}} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = get_integration_diff( @@ -693,7 +693,7 @@ def test_get_integration_diff_success(chronicle_client): def test_get_integration_diff_error(chronicle_client): """Test get_integration_diff raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to fetch diff"), ): with pytest.raises(APIError) as exc_info: @@ -710,7 +710,7 @@ def test_transition_integration_success(chronicle_client): expected = {"name": "integrations/test"} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = transition_integration( @@ -732,7 +732,7 @@ def test_transition_integration_success(chronicle_client): def test_transition_integration_error(chronicle_client): """Test transition_integration raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to transition integration"), ): with pytest.raises(APIError) as exc_info: @@ -756,10 +756,10 @@ def test_update_integration_uses_build_patch_body_and_passes_dependencies_to_rem params = {"updateMask": "displayName"} with patch( - "secops.chronicle.integration.integrations.build_patch_body", + "secops.chronicle.soar.integration.integrations.build_patch_body", return_value=(body, params), ) as mock_build_patch, patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value={"name": "integrations/test"}, ) as mock_request: result = update_integration( @@ -788,10 +788,10 @@ def test_update_integration_when_build_patch_body_returns_no_params(chronicle_cl body = {"description": "New"} with patch( - "secops.chronicle.integration.integrations.build_patch_body", + "secops.chronicle.soar.integration.integrations.build_patch_body", return_value=(body, None), ), patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value={"name": "integrations/test"}, ) as mock_request: update_integration( @@ -814,10 +814,10 @@ def test_update_integration_when_build_patch_body_returns_no_params(chronicle_cl def test_update_integration_error(chronicle_client): """Test update_integration raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.build_patch_body", + "secops.chronicle.soar.integration.integrations.build_patch_body", return_value=({}, None), ), patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to update integration"), ): with pytest.raises(APIError) as exc_info: @@ -834,7 +834,7 @@ def test_update_custom_integration_builds_body_and_params(chronicle_client): expected = {"successful": True, "integration": {"name": "integrations/test"}} with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value=expected, ) as mock_request: result = update_custom_integration( @@ -868,7 +868,7 @@ def test_update_custom_integration_builds_body_and_params(chronicle_client): 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", + "secops.chronicle.soar.integration.integrations.chronicle_request", return_value={"successful": True}, ) as mock_request: update_custom_integration( @@ -900,7 +900,7 @@ def test_update_custom_integration_excludes_none_fields(chronicle_client): def test_update_custom_integration_error(chronicle_client): """Test update_custom_integration raises APIError on failure.""" with patch( - "secops.chronicle.integration.integrations.chronicle_request", + "secops.chronicle.soar.integration.integrations.chronicle_request", side_effect=APIError("Failed to update custom integration"), ): with pytest.raises(APIError) as exc_info: diff --git a/tests/chronicle/integration/test_marketplace_integrations.py b/tests/chronicle/soar/integration/test_marketplace_integrations.py similarity index 89% rename from tests/chronicle/integration/test_marketplace_integrations.py rename to tests/chronicle/soar/integration/test_marketplace_integrations.py index 15216fd9..0c508c20 100644 --- a/tests/chronicle/integration/test_marketplace_integrations.py +++ b/tests/chronicle/soar/integration/test_marketplace_integrations.py @@ -20,7 +20,7 @@ from secops.chronicle.client import ChronicleClient from secops.chronicle.models import APIVersion -from secops.chronicle.integration.marketplace_integrations import ( +from secops.chronicle.soar.integration.marketplace_integrations import ( list_marketplace_integrations, get_marketplace_integration, get_marketplace_integration_diff, @@ -76,7 +76,7 @@ def test_list_marketplace_integrations_success(chronicle_client): } with patch( - "secops.chronicle.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_paginated_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.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.integration.marketplace_integrations.chronicle_request", + "secops.chronicle.soar.integration.marketplace_integrations.chronicle_request", side_effect=APIError("Failed to uninstall marketplace integration"), ): with pytest.raises(APIError) as exc_info: