From 44a19f2e3ac4f9f0d48bfeba62a380fce21c7d4f Mon Sep 17 00:00:00 2001 From: Mihir Vala <179564180+mihirvala-crestdata@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:05:31 +0530 Subject: [PATCH 1/2] chore: added support for featured content rule list --- examples/featured_content_rules_example.py | 106 ++++++++++++++++++ src/secops/chronicle/__init__.py | 5 + src/secops/chronicle/client.py | 37 ++++++ .../chronicle/featured_content_rules.py | 67 +++++++++++ 4 files changed, 215 insertions(+) create mode 100644 examples/featured_content_rules_example.py create mode 100644 src/secops/chronicle/featured_content_rules.py diff --git a/examples/featured_content_rules_example.py b/examples/featured_content_rules_example.py new file mode 100644 index 0000000..58cc161 --- /dev/null +++ b/examples/featured_content_rules_example.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# 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. +# +"""Example script demonstrating Chronicle Featured Content Rules.""" + +import argparse + +from secops.chronicle.client import ChronicleClient + + +def get_client(project_id: str, customer_id: str, region: str): + """Initialize Chronicle client.""" + return ChronicleClient( + project_id=project_id, customer_id=customer_id, region=region + ) + + +def featured_content_rules_list_example(chronicle): + """Demonstrate featured content rules functionality.""" + try: + print("\n[1] List All Featured Content Rules") + print("-" * 70) + result = chronicle.list_featured_content_rules() + rules = result.get("featuredContentRules", []) + print(f"Total rules found: {len(rules)}") + + if rules: + print("\nFirst 3 rules:") + for i, rule in enumerate(rules[:3], 1): + name = rule.get("name", "") + rule_id_extracted = name.split("/")[-1] if name else "N/A" + content_metadata = rule.get("contentMetadata", {}) + display_name = content_metadata.get("displayName", "Unknown") + severity = rule.get("severity", "UNSPECIFIED") + print( + f" {i}. {display_name} " + f"[{rule_id_extracted}] - {severity}" + ) + + print("\n[2] Paginated List (5 rules per page)") + print("-" * 70) + result = chronicle.list_featured_content_rules(page_size=5) + featured_rules = result.get("featuredContentRules", []) + next_token = result.get("nextPageToken") + print(f"Rules in first page: {len(featured_rules)}") + print(f"More pages available: {bool(next_token)}") + + print("\n[3] Filter expression") + print("-" * 70) + filter_expr = 'rule_precision:"Precise"' + combined_rules_result = chronicle.list_featured_content_rules( + filter_expression=filter_expr + ) + combined_rules = combined_rules_result.get("featuredContentRules", []) + print(f"Rules matching filter expression: {len(combined_rules)}") + except Exception as e: + print(f"\nError: {e}") + + +def main(): + """Main function to demonstrate featured content rules usage.""" + parser = argparse.ArgumentParser( + description="Chronicle Featured Content Rules Example" + ) + parser.add_argument( + "--project_id", + required=True, + help="Google Cloud Project ID", + ) + parser.add_argument( + "--customer_id", + required=True, + help="Chronicle Customer ID (UUID)", + ) + parser.add_argument( + "--region", + default="us", + help="Chronicle region (default: us)", + ) + + args = parser.parse_args() + + chronicle = get_client(args.project_id, args.customer_id, args.region) + + print( + f"\nConnected to Chronicle (Project: {args.project_id}, " + f"Customer: {args.customer_id}, Region: {args.region})\n" + ) + + featured_content_rules_list_example(chronicle) + + +if __name__ == "__main__": + main() diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index b874c10..8f250b1 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -162,6 +162,9 @@ search_curated_detections, update_curated_rule_set_deployment, ) +from secops.chronicle.featured_content_rules import ( + list_featured_content_rules, +) from secops.chronicle.rule_validation import ValidationResult from secops.chronicle.search import search_udm from secops.chronicle.stats import get_stats @@ -274,6 +277,8 @@ "get_curated_rule_by_name", "update_curated_rule_set_deployment", "search_curated_detections", + # Featured content rules operations + "list_featured_content_rules", # Native Dashboard "add_chart", "create_dashboard", diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index c96aedc..8195357 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -290,6 +290,9 @@ 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 @@ -2543,6 +2546,40 @@ def get_curated_rule_set(self, rule_set_id: str) -> dict[str, Any]: """ return _get_curated_rule_set(self, rule_set_id) + def list_featured_content_rules( + self, + page_size: int | None = None, + page_token: str | None = None, + filter_expression: str | None = None, + ) -> list[dict[str, Any]] | dict[str, Any]: + """List featured content rules from Chronicle Content Hub. + + Args: + page_size: Maximum number of featured content rules to return. + If unspecified, at most 100 rules will be returned. + Maximum value is 1000. If provided, returns dict with + nextPageToken. + page_token: Token for retrieving the next page of results. + filter_expression: Optional filter expression. Supported: + - category_name:"" (OR for multiple) + - policy_name:"" (OR for multiple) + - rule_id:"ur_" (OR for multiple) + - rule_precision:"" (Precise or Broad) + - search_rule_name_or_description=~"" + Multiple filters can be combined with AND operator. + + Returns: + If page_size is None: List of all featured content rules. + If page_size is provided: Dict with featuredContentRules + list and nextPageToken. + + Raises: + APIError: If the API request fails + """ + return _list_featured_content_rules( + self, page_size, page_token, filter_expression + ) + def search_curated_detections( self, rule_id: str, diff --git a/src/secops/chronicle/featured_content_rules.py b/src/secops/chronicle/featured_content_rules.py new file mode 100644 index 0000000..59fd1f7 --- /dev/null +++ b/src/secops/chronicle/featured_content_rules.py @@ -0,0 +1,67 @@ +# 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. +# +"""Featured content rules functionality for Chronicle.""" + +from typing import Any + +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, +) + + +def list_featured_content_rules( + client, + page_size: int | None = None, + page_token: str | None = None, + filter_expression: str | None = None, +) -> list[dict[str, Any]] | dict[str, Any]: + """List featured content rules from Chronicle Content Hub. + + Args: + client: ChronicleClient instance + page_size: Maximum number of featured content rules to return. + If unspecified, at most 100 rules will be returned. + Maximum value is 1000; values above 1000 will be coerced + to 1000. If provided, returns dict with nextPageToken. + page_token: Token for retrieving the next page of results. + filter_expression: Optional filter expression. Supported filters: + - category_name:"" (OR operator for multiple) + - policy_name:"" (OR operator for multiple) + - rule_id:"ur_" (OR operator for multiple) + - rule_precision:"" (Precise or Broad) + - search_rule_name_or_description=~"" + Multiple filters can be combined with AND operator. + + Returns: + If page_size is None: List of all featured content rules. + If page_size is provided: Dict with featuredContentRules list + and nextPageToken. + + Raises: + APIError: If the API request fails + """ + extra_params = {} + if filter_expression: + extra_params["filter"] = filter_expression + + return chronicle_paginated_request( + client, + base_url=client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=page_size, + page_token=page_token, + extra_params=extra_params if extra_params else None, + ) From 8cea44faf646cb7fb4474e81422eb56ef57e8452 Mon Sep 17 00:00:00 2001 From: Mihir Vala <179564180+mihirvala-crestdata@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:31:47 +0530 Subject: [PATCH 2/2] chore: added CLI. added unit tests. added integration test. added documentation and mapping. --- CLI.md | 27 ++ README.md | 34 ++ api_module_mapping.md | 1 + src/secops/cli/cli_client.py | 4 + .../cli/commands/featured_content_rules.py | 60 ++++ .../chronicle/test_featured_content_rules.py | 338 ++++++++++++++++++ ...test_featured_content_rules_integration.py | 94 +++++ ...test_featured_content_rules_integration.py | 93 +++++ 8 files changed, 651 insertions(+) create mode 100644 src/secops/cli/commands/featured_content_rules.py create mode 100644 tests/chronicle/test_featured_content_rules.py create mode 100644 tests/chronicle/test_featured_content_rules_integration.py create mode 100644 tests/cli/test_featured_content_rules_integration.py diff --git a/CLI.md b/CLI.md index 65945ac..ffe7455 100644 --- a/CLI.md +++ b/CLI.md @@ -1244,6 +1244,33 @@ secops reference-list update \ --entries-file "/path/to/updated_domains.txt" ``` +### Featured Content Rules + +Featured content rules are pre-built detection rules available in the Chronicle Content Hub. These curated rules can be listed and filtered to help you discover and deploy detections. + +#### List all featured content rules: + +```bash +secops featured-content-rules list +``` + +#### List with pagination: + +```bash +# Get first page with 10 rules +secops featured-content-rules list --page-size 10 + +# Get next page using token from previous response +secops featured-content-rules list --page-size 10 --page-token "token123" +``` + +#### Get filtered list: + +```bash +secops featured-content-rules list \ + --filter 'category_name:"Threat Detection" AND rule_precision:"Precise"' +``` + ## Examples ### Search for Recent Network Connections diff --git a/README.md b/README.md index 61d7b74..559c683 100644 --- a/README.md +++ b/README.md @@ -2286,6 +2286,40 @@ activity = chronicle.compute_rule_exclusion_activity( ) ``` +### Featured Content Rules + +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 +# 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 with pagination +result = chronicle.list_featured_content_rules(page_size=10) +rules = result.get("featuredContentRules", []) +next_page_token = result.get("nextPageToken") + +if next_page_token: + next_page = chronicle.list_featured_content_rules( + page_size=10, + page_token=next_page_token + ) + +# Filter list +filtered_rules = chronicle.list_featured_content_rules( + filter_expression=( + 'category_name:"Threat Detection" AND ' + 'rule_precision:"Precise"' + ) +) +``` + ## Data Tables and Reference Lists 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. diff --git a/api_module_mapping.md b/api_module_mapping.md index 726b930..21207be 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -84,6 +84,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ | 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 | diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index e318512..37dd23b 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -12,6 +12,9 @@ from secops.cli.commands.config import setup_config_command from secops.cli.commands.curated_rule import setup_curated_rules_command from secops.cli.commands.dashboard import setup_dashboard_command +from secops.cli.commands.featured_content_rules import ( + setup_featured_content_rules_command, +) from secops.cli.commands.dashboard_query import setup_dashboard_query_command from secops.cli.commands.data_table import setup_data_table_command from secops.cli.commands.entity import setup_entity_command @@ -176,6 +179,7 @@ def build_parser() -> argparse.ArgumentParser: setup_rule_exclusion_command(subparsers) setup_forwarder_command(subparsers) setup_curated_rules_command(subparsers) + setup_featured_content_rules_command(subparsers) setup_config_command(subparsers) setup_help_command(subparsers) setup_dashboard_command(subparsers) diff --git a/src/secops/cli/commands/featured_content_rules.py b/src/secops/cli/commands/featured_content_rules.py new file mode 100644 index 0000000..22d8940 --- /dev/null +++ b/src/secops/cli/commands/featured_content_rules.py @@ -0,0 +1,60 @@ +# 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 featured content rules commands""" + +import sys + +from secops.cli.utils.common_args import add_pagination_args +from secops.cli.utils.formatters import output_formatter + + +def setup_featured_content_rules_command(subparsers): + """Set up the featured-content-rules command group.""" + parser = subparsers.add_parser( + "featured-content-rules", + help="Manage featured content rules from Chronicle Content Hub", + ) + subparser = parser.add_subparsers(dest="featured_content_rules_cmd") + parser.set_defaults(func=lambda args, _: parser.print_help()) + + list_parser = subparser.add_parser( + "list", help="List featured content rules" + ) + add_pagination_args(list_parser) + list_parser.add_argument( + "--filter", + "--filter-expression", + dest="filter_expression", + help=( + "Filter expression. Supported filters: " + "category_name, policy_name, rule_id, rule_precision, " + "search_rule_name_or_description" + ), + ) + list_parser.set_defaults(func=handle_featured_content_rules_list_command) + + +def handle_featured_content_rules_list_command(args, chronicle): + """List featured content rules.""" + try: + out = chronicle.list_featured_content_rules( + page_size=getattr(args, "page_size", None), + page_token=getattr(args, "page_token", None), + filter_expression=getattr(args, "filter_expression", None), + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: + print(f"Error listing featured content rules: {e}", file=sys.stderr) + sys.exit(1) diff --git a/tests/chronicle/test_featured_content_rules.py b/tests/chronicle/test_featured_content_rules.py new file mode 100644 index 0000000..84044cd --- /dev/null +++ b/tests/chronicle/test_featured_content_rules.py @@ -0,0 +1,338 @@ +# 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 featured content rules functions.""" + +from typing import Any, Dict +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.featured_content_rules import ( + list_featured_content_rules, +) +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", + ) + + +@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 + + +def test_list_featured_content_rules_success_without_params( + chronicle_client, +): + """Test list_featured_content_rules without parameters.""" + expected: Dict[str, Any] = { + "featuredContentRules": [ + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_123", + "severity": "HIGH", + "contentMetadata": { + "displayName": "Test Rule 1", + "description": "Test description", + }, + }, + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_456", + "severity": "MEDIUM", + "contentMetadata": { + "displayName": "Test Rule 2", + "description": "Another test rule", + }, + }, + ] + } + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules(chronicle_client) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=None, + page_token=None, + extra_params=None, + ) + + +def test_list_featured_content_rules_with_page_size(chronicle_client): + """Test list_featured_content_rules with page_size parameter.""" + expected: Dict[str, Any] = { + "featuredContentRules": [ + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_123", + "severity": "HIGH", + }, + ], + "nextPageToken": "token-abc-123", + } + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules(chronicle_client, page_size=10) + + assert result == expected + assert "nextPageToken" in result + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=10, + page_token=None, + extra_params=None, + ) + + +def test_list_featured_content_rules_with_page_token(chronicle_client): + """Test list_featured_content_rules with page_token parameter.""" + expected: Dict[str, Any] = { + "featuredContentRules": [ + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_789", + "severity": "LOW", + }, + ] + } + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules( + chronicle_client, page_token="token-xyz-789" + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=None, + page_token="token-xyz-789", + extra_params=None, + ) + + +def test_list_featured_content_rules_with_filter_expression( + chronicle_client, +): + """Test list_featured_content_rules with filter_expression.""" + expected: Dict[str, Any] = { + "featuredContentRules": [ + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_precise_1", + "severity": "HIGH", + "rulePrecision": "Precise", + }, + ] + } + + filter_expr = 'rule_precision:"Precise"' + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules( + chronicle_client, filter_expression=filter_expr + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=None, + page_token=None, + extra_params={"filter": filter_expr}, + ) + + +def test_list_featured_content_rules_with_all_parameters( + chronicle_client, +): + """Test list_featured_content_rules with all parameters.""" + expected: Dict[str, Any] = { + "featuredContentRules": [ + { + "name": "projects/test/locations/us/instances/test/" + "contentHub/featuredContentRules/ur_filtered", + "severity": "CRITICAL", + }, + ], + "nextPageToken": "next-token-123", + } + + filter_expr = ( + 'category_name:"Threat Detection" AND ' 'rule_precision:"Precise"' + ) + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules( + chronicle_client, + page_size=5, + page_token="current-token", + filter_expression=filter_expr, + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=5, + page_token="current-token", + extra_params={"filter": filter_expr}, + ) + + +def test_list_featured_content_rules_empty_results(chronicle_client): + """Test list_featured_content_rules with empty results.""" + expected: Dict[str, Any] = {"featuredContentRules": []} + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules(chronicle_client) + + assert result == expected + assert result["featuredContentRules"] == [] + + mock_paginated.assert_called_once() + + +def test_list_featured_content_rules_api_error(chronicle_client): + """Test list_featured_content_rules raises APIError on failure.""" + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + side_effect=APIError("Failed to list featuredContentRules"), + ): + with pytest.raises(APIError) as exc_info: + list_featured_content_rules(chronicle_client) + + assert "Failed to list featuredContentRules" in str(exc_info.value) + + +def test_list_featured_content_rules_invalid_filter_error( + chronicle_client, +): + """Test list_featured_content_rules with invalid filter.""" + error_msg = ( + "invalid filter. The request only supports the following " + "filters: 'category_name', 'policy_name', 'rule_id', " + "'rule_precision', 'search_rule_name_or_description'" + ) + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + side_effect=APIError(error_msg), + ): + with pytest.raises(APIError) as exc_info: + list_featured_content_rules( + chronicle_client, + filter_expression='invalid_field:"value"', + ) + + assert "invalid filter" in str(exc_info.value) + + +def test_list_featured_content_rules_max_page_size(chronicle_client): + """Test list_featured_content_rules with maximum page size.""" + expected: Dict[str, Any] = { + "featuredContentRules": [{"name": f"rule_{i}"} for i in range(1000)], + "nextPageToken": "token-for-next-1000", + } + + with patch( + "secops.chronicle.featured_content_rules." + "chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_featured_content_rules(chronicle_client, page_size=1000) + + assert result == expected + assert len(result["featuredContentRules"]) == 1000 + + mock_paginated.assert_called_once_with( + chronicle_client, + base_url=chronicle_client.base_url, + path="contentHub/featuredContentRules", + items_key="featuredContentRules", + page_size=1000, + page_token=None, + extra_params=None, + ) diff --git a/tests/chronicle/test_featured_content_rules_integration.py b/tests/chronicle/test_featured_content_rules_integration.py new file mode 100644 index 0000000..a70bad3 --- /dev/null +++ b/tests/chronicle/test_featured_content_rules_integration.py @@ -0,0 +1,94 @@ +# 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 tests for featured content rules in Chronicle API. + +These tests require valid credentials and API access. +""" +import pytest + +from secops import SecOpsClient + +from ..config import CHRONICLE_CONFIG, SERVICE_ACCOUNT_JSON + + +@pytest.fixture(scope="module") +def chronicle(): + """Fixture to create a Chronicle client for testing.""" + client = SecOpsClient(service_account_info=SERVICE_ACCOUNT_JSON) + return client.chronicle(**CHRONICLE_CONFIG) + + +@pytest.mark.integration +def test_list_featured_content_rules_basic(chronicle): + """Test basic listing of featured content rules.""" + rules = chronicle.list_featured_content_rules() + assert isinstance(rules, dict) + assert "featuredContentRules" in rules + assert isinstance(rules["featuredContentRules"], list) + + print(f"\nFound {len(rules['featuredContentRules'])} featured rules") + + if rules["featuredContentRules"]: + first_rule = rules["featuredContentRules"][0] + assert "name" in first_rule + print(f"First rule: {first_rule.get('name')}") + + +@pytest.mark.integration +def test_list_featured_content_rules_with_pagination(chronicle): + """Test listing featured content rules with pagination.""" + page_size = 5 + result = chronicle.list_featured_content_rules(page_size=page_size) + + assert isinstance(result, dict) + assert "featuredContentRules" in result + assert isinstance(result["featuredContentRules"], list) + assert len(result["featuredContentRules"]) <= page_size + + print( + f"\nPaginated result: {len(result['featuredContentRules'])} " + f"rules (page_size={page_size})" + ) + + if "nextPageToken" in result: + print("Next page token available") + next_page = chronicle.list_featured_content_rules( + page_size=page_size, page_token=result["nextPageToken"] + ) + assert isinstance(next_page, dict) + assert "featuredContentRules" in next_page + print(f"Next page: {len(next_page['featuredContentRules'])} rules") + + +@pytest.mark.integration +def test_list_featured_content_rules_with_filter(chronicle): + """Test listing featured content rules with filter expression.""" + filter_expr = 'rule_precision:"Precise"' + result = chronicle.list_featured_content_rules( + filter_expression=filter_expr + ) + + assert isinstance(result, dict) + assert "featuredContentRules" in result + assert isinstance(result["featuredContentRules"], list) + + print( + f"\nFiltered by precision: " + f"{len(result['featuredContentRules'])} rules" + ) + + if result["featuredContentRules"]: + for rule in result["featuredContentRules"][:3]: + print(f" - {rule.get('name')}") diff --git a/tests/cli/test_featured_content_rules_integration.py b/tests/cli/test_featured_content_rules_integration.py new file mode 100644 index 0000000..e9ee150 --- /dev/null +++ b/tests/cli/test_featured_content_rules_integration.py @@ -0,0 +1,93 @@ +# 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 tests for featured content rules CLI commands.""" + +import json +import subprocess + +import pytest + + +@pytest.mark.integration +def test_cli_featured_content_rules_list_with_filter(cli_env, common_args): + """Test featured-content-rules list command with filter.""" + cmd = ( + ["secops"] + + common_args + + [ + "featured-content-rules", + "list", + "--filter", + 'rule_precision:"Precise"', + ] + ) + + result = subprocess.run(cmd, env=cli_env, capture_output=True, text=True) + + assert ( + result.returncode == 0 + ), f"Command failed with stderr: {result.stderr}" + + try: + output = json.loads(result.stdout) + assert isinstance(output, dict) + assert "featuredContentRules" in output + assert isinstance(output["featuredContentRules"], list) + + print( + f"\nFiltered result: " + f"{len(output['featuredContentRules'])} rules" + ) + + except json.JSONDecodeError: + assert "Error:" not in result.stdout + + +@pytest.mark.integration +def test_cli_featured_content_rules_list_with_page_size(cli_env, common_args): + """Test featured-content-rules list command with page size.""" + cmd = ( + ["secops"] + + common_args + + [ + "featured-content-rules", + "list", + "--page-size", + "5", + ] + ) + + result = subprocess.run(cmd, env=cli_env, capture_output=True, text=True) + + assert ( + result.returncode == 0 + ), f"Command failed with stderr: {result.stderr}" + + try: + output = json.loads(result.stdout) + assert isinstance(output, dict) + assert "featuredContentRules" in output + assert len(output["featuredContentRules"]) <= 5 + + print( + f"\nPaginated result: " + f"{len(output['featuredContentRules'])} rules" + ) + + if "nextPageToken" in output: + print("Next page token available") + + except json.JSONDecodeError: + assert "Error:" not in result.stdout