{"id":42656341,"url":"https://github.com/google/secops-wrapper","last_synced_at":"2026-03-03T14:03:31.859Z","repository":{"id":281516874,"uuid":"929870652","full_name":"google/secops-wrapper","owner":"google","description":"A helper SDK to wrap the Google SecOps API for common security use cases","archived":false,"fork":false,"pushed_at":"2026-01-29T06:27:28.000Z","size":1594,"stargazers_count":59,"open_issues_count":7,"forks_count":33,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-29T22:32:46.976Z","etag":null,"topics":["chronicle","google","siem"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/secops/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/google.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-02-09T15:46:21.000Z","updated_at":"2026-01-29T06:26:24.000Z","dependencies_parsed_at":"2025-03-25T14:36:24.257Z","dependency_job_id":"a8de7452-04c8-466e-9dc7-25819eb16a79","html_url":"https://github.com/google/secops-wrapper","commit_stats":null,"previous_names":["google/secops-wrapper"],"tags_count":66,"template":false,"template_full_name":null,"purl":"pkg:github/google/secops-wrapper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Fsecops-wrapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Fsecops-wrapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Fsecops-wrapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Fsecops-wrapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/google","download_url":"https://codeload.github.com/google/secops-wrapper/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/google%2Fsecops-wrapper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29039341,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-03T08:41:49.363Z","status":"ssl_error","status_checked_at":"2026-02-03T08:40:19.255Z","response_time":96,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chronicle","google","siem"],"created_at":"2026-01-29T08:06:55.047Z","updated_at":"2026-03-03T14:03:31.850Z","avatar_url":"https://github.com/google.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Google SecOps SDK for Python\n\n[![PyPI version](https://img.shields.io/pypi/v/secops.svg)](https://pypi.org/project/secops/)\n\n\nA Python SDK for interacting with Google Security Operations products, currently supporting Chronicle/SecOps SIEM.\nThis wraps the API for common use cases, including UDM searches, entity lookups, IoCs, alert management, case management, and detection rule management.\n\n## Prerequisites\n\nFollow these steps to ensure your environment is properly configured:\n\n1. **Configure a Google Cloud Project for Google SecOps**\n   - Your Google Cloud project must be linked to your Google SecOps instance.\n   - Chronicle API needs to be enabled in your Google Cloud project.\n   - The project used for authentication must be the same project that was set up during your SecOps onboarding.\n   - For detailed instructions, see [Configure a Google Cloud project for Google SecOps](https://cloud.google.com/chronicle/docs/onboard/configure-cloud-project).\n\n2. **Set up IAM Permissions**\n   - The service account or user credentials you use must have appropriate permissions\n   - The recommended predefined role is **Chronicle API Admin** (`roles/chronicle.admin`)\n   - For more granular access control, you can create custom roles with specific permissions\n   - See [Access control using IAM](https://cloud.google.com/chronicle/docs/onboard/configure-feature-access) for detailed permission information\n\n3. **Required Information**\n   - Your Chronicle instance ID (customer_id)\n   - Your Google Cloud project number (project_id)\n   - Your preferred region (e.g., \"us\", \"europe\", \"asia\")\n\n\n\u003e **Note:** Using a Google Cloud project that is not linked to your SecOps instance will result in authentication failures, even if the service account/user has the correct IAM roles assigned.\n\n## Installation\n\n```bash\npip install secops\n```\n\n## Command Line Interface\n\nThe SDK also provides a comprehensive command-line interface (CLI) that makes it easy to interact with Google Security Operations products from your terminal:\n\n```bash\n# Save your credentials\nsecops config set --customer-id \"your-instance-id\" --project-id \"your-project-id\" --region \"us\"\n\n# Now use commands without specifying credentials each time\nsecops search --query \"metadata.event_type = \\\"NETWORK_CONNECTION\\\"\"\n```\n\nFor detailed CLI documentation and examples, see the [CLI Documentation](https://github.com/google/secops-wrapper/blob/main/CLI.md).\n\n\n## Authentication\n\nThe SDK supports two main authentication methods:\n\n### 1. Application Default Credentials (ADC)\n\nThe simplest and recommended way to authenticate the SDK. Application Default Credentials provide a consistent authentication method that works across different Google Cloud environments and local development.\n\nThere are several ways to use ADC:\n\n#### a. Using `gcloud` CLI (Recommended for Local Development)\n\n```bash\n# Login and set up application-default credentials\ngcloud auth application-default login\n```\n\nThen in your code:\n```python\nfrom secops import SecOpsClient\n\n# Initialize with default credentials - no explicit configuration needed\nclient = SecOpsClient()\n```\n\n#### b. Using Environment Variable\n\nSet the environment variable pointing to your service account key:\n```bash\nexport GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/service-account.json\"\n```\n\nThen in your code:\n```python\nfrom secops import SecOpsClient\n\n# Initialize with default credentials - will automatically use the credentials file\nclient = SecOpsClient()\n```\n\n#### c. Google Cloud Environment (Automatic)\n\nWhen running on Google Cloud services (Compute Engine, Cloud Functions, Cloud Run, etc.), ADC works automatically without any configuration:\n\n```python\nfrom secops import SecOpsClient\n\n# Initialize with default credentials - will automatically use the service account \n# assigned to your Google Cloud resource\nclient = SecOpsClient()\n```\n\nADC will automatically try these authentication methods in order:\n1. Environment variable `GOOGLE_APPLICATION_CREDENTIALS`\n2. Google Cloud SDK credentials (set by `gcloud auth application-default login`)\n3. Google Cloud-provided service account credentials\n4. Local service account impersonation credentials\n\n### 2. Service Account Authentication\n\nFor more explicit control, you can authenticate using a service account that is created in the Google Cloud project associated with Google SecOps.\n\n**Important Note on Permissions:**\n* This service account needs to be granted the appropriate Identity and Access Management (IAM) role to interact with the Google Secops (Chronicle) API. The recommended predefined role is **Chronicle API Admin** (`roles/chronicle.admin`). Alternatively, if your security policies require more granular control, you can create a custom IAM role with the specific permissions needed for the operations you intend to use (e.g., `chronicle.instances.get`, `chronicle.events.create`, `chronicle.rules.list`, etc.). \n\nOnce the service account is properly permissioned, you can authenticate using it in two ways: \n\n#### a. Using a Service Account JSON File\n\n```python\nfrom secops import SecOpsClient\n\n# Initialize with service account JSON file\nclient = SecOpsClient(service_account_path=\"/path/to/service-account.json\")\n```\n\n#### b. Using Service Account Info Dictionary\n\nIf you prefer to manage credentials programmatically without a file, you can create a dictionary containing the service account key's contents.\n\n```python\nfrom secops import SecOpsClient\n\n# Service account details as a dictionary\nservice_account_info = {\n    \"type\": \"service_account\",\n    \"project_id\": \"your-project-id\",\n    \"private_key_id\": \"key-id\",\n    \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n...\",\n    \"client_email\": \"service-account@project.iam.gserviceaccount.com\",\n    \"client_id\": \"client-id\",\n    \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n    \"token_uri\": \"https://oauth2.googleapis.com/token\",\n    \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n    \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/...\"\n}\n\n# Initialize with service account info\nclient = SecOpsClient(service_account_info=service_account_info)\n```\n\n### Impersonate Service Account\n\nBoth [Application Default Credentials](#1-application-default-credentials-adc) and [Service Account Authentication](#2-service-account-authentication) supports impersonating a Service Account leveraging the corresponding `impersonate_service_account` parameter as per the following configuration:\n\n```python\nfrom secops import SecOpsClient\n\n# Initialize with default credentials and impersonate service account\nclient = SecOpsClient(impersonate_service_account=\"secops@test-project.iam.gserviceaccount.com\")\n```\n\n### Retry Configuration\n\nThe SDK provides built-in retry functionality that automatically handles transient errors such as rate limiting (429), server errors (500, 502, 503, 504), and network issues. You can customize the retry behavior when initializing the client:\n\n```python\nfrom secops import SecOpsClient\nfrom secops.auth import RetryConfig\n\n# Define retry configurations\nretry_config = RetryConfig(\n    total=3,                     # Maximum number of retries (default: 5)\n    retry_status_codes=[429, 500, 502, 503, 504],  # HTTP status codes to retry\n    allowed_methods=[\"GET\", \"DELETE\"],  # HTTP methods to retry\n    backoff_factor=0.5           # Backoff factor (default: 0.3)\n)\n\n# Initialize with custom retry config\nclient = SecOpsClient(retry_config=retry_config)\n\n\n# Disable retry completely by marking retry config as False\nclient = SecOpsClient(retry_config=False)\n```\n\n## Using the Chronicle API\n\n### Initializing the Chronicle Client\n\nAfter creating a SecOpsClient, you need to initialize the Chronicle-specific client:\n\n```python\n# Initialize Chronicle client\nchronicle = client.chronicle(\n    customer_id=\"your-chronicle-instance-id\",  # Your Chronicle instance ID\n    project_id=\"your-project-id\",             # Your GCP project ID\n    region=\"us\"                               # Chronicle API region \n)\n```\n[See available regions](https://github.com/google/secops-wrapper/blob/main/regions.md)\n\n#### API Version Control\n\nThe SDK supports flexible API version selection:\n\n- **Default Version**: Set `default_api_version` during client initialization (default is `v1alpha`)\n- **Per-Method Override**: Many methods accept an `api_version` parameter to override the default for specific calls\n\n**Supported API versions:**\n- `v1` - Stable production API\n- `v1beta` - Beta API with newer features\n- `v1alpha` - Alpha API with experimental features\n\n**Example with per-method version override:**\n```python\nfrom secops.chronicle.models import APIVersion\n\n# Client defaults to v1alpha\nchronicle = client.chronicle(\n    customer_id=\"your-chronicle-instance-id\",\n    project_id=\"your-project-id\",\n    region=\"us\",\n    default_api_version=\"v1alpha\"\n)\n\n# Use v1 for a specific rule operation\nrule = chronicle.get_rule(\n    rule_id=\"ru_12345678-1234-1234-1234-123456789abc\",\n    api_version=APIVersion.V1  # Override to use v1 for this call\n)\n```\n\n### Log Ingestion\n\nIngest raw logs directly into Chronicle:\n\n```python\nfrom datetime import datetime, timezone\nimport json\n\n# Create a sample log (this is an OKTA log)\ncurrent_time = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')\nokta_log = {\n    \"actor\": {\n        \"alternateId\": \"mark.taylor@cymbal-investments.org\",\n        \"displayName\": \"Mark Taylor\",\n        \"id\": \"00u4j7xcb5N6zfiRP5d8\",\n        \"type\": \"User\"\n    },\n    \"client\": {\n        \"userAgent\": {\n            \"rawUserAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36\",\n            \"os\": \"Windows 10\",\n            \"browser\": \"CHROME\"\n        },\n        \"ipAddress\": \"96.6.127.53\",\n        \"geographicalContext\": {\n            \"city\": \"New York\",\n            \"state\": \"New York\",\n            \"country\": \"United States\",\n            \"postalCode\": \"10118\",\n            \"geolocation\": {\"lat\": 40.7123, \"lon\": -74.0068}\n        }\n    },\n    \"displayMessage\": \"Max sign in attempts exceeded\",\n    \"eventType\": \"user.account.lock\",\n    \"outcome\": {\"result\": \"FAILURE\", \"reason\": \"LOCKED_OUT\"},\n    \"published\": \"2025-06-19T21:51:50.116Z\",\n    \"securityContext\": {\n        \"asNumber\": 20940,\n        \"asOrg\": \"akamai technologies inc.\",\n        \"isp\": \"akamai international b.v.\",\n        \"domain\": \"akamaitechnologies.com\",\n        \"isProxy\": false\n    },\n    \"severity\": \"DEBUG\",\n    \"legacyEventType\": \"core.user_auth.account_locked\",\n    \"uuid\": \"5b90a94a-d7ba-11ea-834a-85c24a1b2121\",\n    \"version\": \"0\"\n    # ... additional OKTA log fields may be included\n}\n\n# Ingest a single log using the default forwarder\nresult = chronicle.ingest_log(\n    log_type=\"OKTA\",  # Chronicle log type\n    log_message=json.dumps(okta_log)  # JSON string of the log\n)\n\nprint(f\"Operation: {result.get('operation')}\")\n\n# Batch ingestion: Ingest multiple logs in a single request\nbatch_logs = [\n    json.dumps({\"actor\": {\"displayName\": \"User 1\"}, \"eventType\": \"user.session.start\"}),\n    json.dumps({\"actor\": {\"displayName\": \"User 2\"}, \"eventType\": \"user.session.start\"}),\n    json.dumps({\"actor\": {\"displayName\": \"User 3\"}, \"eventType\": \"user.session.start\"})\n]\n\n# Ingest multiple logs in a single API call\nbatch_result = chronicle.ingest_log(\n    log_type=\"OKTA\",\n    log_message=batch_logs  # List of log message strings\n)\n\nprint(f\"Batch operation: {batch_result.get('operation')}\")\n\n# Add custom labels to your logs\nlabeled_result = chronicle.ingest_log(\n    log_type=\"OKTA\",\n    log_message=json.dumps(okta_log),\n    labels={\"environment\": \"production\", \"app\": \"web-portal\", \"team\": \"security\"}\n)\n```\nThe SDK also supports non-JSON log formats. Here's an example with XML for Windows Event logs:\n\n```python\n# Create a Windows Event XML log\nxml_content = \"\"\"\u003cEvent xmlns='http://schemas.microsoft.com/win/2004/08/events/event'\u003e\n  \u003cSystem\u003e\n    \u003cProvider Name='Microsoft-Windows-Security-Auditing' Guid='{54849625-5478-4994-A5BA-3E3B0328C30D}'/\u003e\n    \u003cEventID\u003e4624\u003c/EventID\u003e\n    \u003cVersion\u003e1\u003c/Version\u003e\n    \u003cLevel\u003e0\u003c/Level\u003e\n    \u003cTask\u003e12544\u003c/Task\u003e\n    \u003cOpcode\u003e0\u003c/Opcode\u003e\n    \u003cKeywords\u003e0x8020000000000000\u003c/Keywords\u003e\n    \u003cTimeCreated SystemTime='2024-05-10T14:30:00Z'/\u003e\n    \u003cEventRecordID\u003e202117513\u003c/EventRecordID\u003e\n    \u003cCorrelation/\u003e\n    \u003cExecution ProcessID='656' ThreadID='700'/\u003e\n    \u003cChannel\u003eSecurity\u003c/Channel\u003e\n    \u003cComputer\u003eWIN-SERVER.xyz.net\u003c/Computer\u003e\n    \u003cSecurity/\u003e\n  \u003c/System\u003e\n  \u003cEventData\u003e\n    \u003cData Name='SubjectUserSid'\u003eS-1-0-0\u003c/Data\u003e\n    \u003cData Name='SubjectUserName'\u003e-\u003c/Data\u003e\n    \u003cData Name='TargetUserName'\u003esvcUser\u003c/Data\u003e\n    \u003cData Name='WorkstationName'\u003eCLIENT-PC\u003c/Data\u003e\n    \u003cData Name='LogonType'\u003e3\u003c/Data\u003e\n  \u003c/EventData\u003e\n\u003c/Event\u003e\"\"\"\n\n# Ingest the XML log - no json.dumps() needed for XML\nresult = chronicle.ingest_log(\n    log_type=\"WINEVTLOG_XML\",  # Windows Event Log XML format\n    log_message=xml_content    # Raw XML content\n)\n\nprint(f\"Operation: {result.get('operation')}\")\n```\nThe SDK supports all log types available in Chronicle. You can:\n\n1. View available log types:\n```python\n# Get all available log types\nlog_types = chronicle.get_all_log_types()\nfor lt in log_types[:5]:  # Show first 5\n    print(f\"{lt.id}: {lt.description}\")\n\n\n# Fetch only first 50 log types (single page)\nlog_types_page = chronicle.get_all_log_types(page_size=50)\n\n# Fetch specific page using token\nlog_types_next = chronicle.get_all_log_types(\n    page_size=50, \n    page_token=\"next_page_token\"\n)\n```\n\n2. Search for specific log types:\n```python\n# Search for log types related to firewalls\nfirewall_types = chronicle.search_log_types(\"firewall\")\nfor lt in firewall_types:\n    print(f\"{lt.id}: {lt.description}\")\n```\n\n3. Validate log types:\n```python\n# Check if a log type is valid\nif chronicle.is_valid_log_type(\"OKTA\"):\n    print(\"Valid log type\")\nelse:\n    print(\"Invalid log type\")\n```\n\n4. Classify logs to predict log type:\n```python\n# Classify a raw log to determine its type\nokta_log = '{\"eventType\": \"user.session.start\", \"actor\": {\"alternateId\": \"user@example.com\"}}'\npredictions = chronicle.classify_logs(log_data=okta_log)\n\n# Display predictions sorted by confidence score\nfor prediction in predictions:\n    print(f\"Log Type: {prediction['logType']}, Score: {prediction['score']}\")\n```\n\n\u003e **Note:** Confidence scores are provided by the API as guidance only and may not always accurately reflect classification certainty. Use scores for relative ranking rather than absolute confidence.\n\n5. Use custom forwarders:\n```python\n# Create or get a custom forwarder\nforwarder = chronicle.get_or_create_forwarder(display_name=\"MyCustomForwarder\")\nforwarder_id = forwarder[\"name\"].split(\"/\")[-1]\n\n# Use the custom forwarder for log ingestion\nresult = chronicle.ingest_log(\n    log_type=\"WINDOWS\",\n    log_message=json.dumps(windows_log),\n    forwarder_id=forwarder_id\n)\n```\n\n### Forwarder Management\n\nChronicle log forwarders are essential for handling log ingestion with specific configurations. The SDK provides comprehensive methods for creating and managing forwarders:\n\n#### Create a new forwarder\n\n```python\n# Create a basic forwarder with just a display name\nforwarder = chronicle.create_forwarder(display_name=\"MyAppForwarder\")\n\n# Create a forwarder with optional configuration\nforwarder = chronicle.create_forwarder(\n    display_name=\"ProductionForwarder\",\n    metadata={\"labels\": {\"env\": \"prod\"}},\n    upload_compression=True,  # Enable upload compression for efficiency\n    enable_server=False  # Server functionality disabled,\n    http_settings={\n        \"port\":8080,\n        \"host\":\"192.168.0.100\",\n        \"routeSettings\":{\n            \"availableStatusCode\": 200,\n            \"readyStatusCode\": 200,\n            \"unreadyStatusCode\": 500\n        }\n    }\n)\n\nprint(f\"Created forwarder with ID: {forwarder['name'].split('/')[-1]}\")\n```\n\n#### List all forwarders\n\nRetrieve all forwarders in your Chronicle environment with pagination support:\n\n```python\n# Get the default page size (50)\nforwarders = chronicle.list_forwarders()\n\n# Get forwarders with custom page size\nforwarders = chronicle.list_forwarders(page_size=100)\n\n# Process the forwarders\nfor forwarder in forwarders.get(\"forwarders\", []):\n    forwarder_id = forwarder.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = forwarder.get(\"displayName\", \"\")\n    create_time = forwarder.get(\"createTime\", \"\")\n    print(f\"Forwarder ID: {forwarder_id}, Name: {display_name}, Created: {create_time}\")\n```\n\n#### Get forwarder details\n\nRetrieve details about a specific forwarder using its ID:\n\n```python\n# Get a specific forwarder using its ID\nforwarder_id = \"1234567890\"\nforwarder = chronicle.get_forwarder(forwarder_id=forwarder_id)\n\n# Access forwarder properties\ndisplay_name = forwarder.get(\"displayName\", \"\")\nmetadata = forwarder.get(\"metadata\", {})\nserver_enabled = forwarder.get(\"enableServer\", False)\n\nprint(f\"Forwarder {display_name} details:\")\nprint(f\"  Metadata: {metadata}\")\nprint(f\"  Server enabled: {server_enabled}\")\n```\n\n#### Get or create a forwarder\n\nRetrieve an existing forwarder by display name or create a new one if it doesn't exist:\n\n```python\n# Try to find a forwarder with the specified display name\n# If not found, create a new one with that display name\nforwarder = chronicle.get_or_create_forwarder(display_name=\"ApplicationLogForwarder\")\n\n# Extract the forwarder ID for use in log ingestion\nforwarder_id = forwarder[\"name\"].split(\"/\")[-1]\n```\n\n#### Update a forwarder\n\nUpdate an existing forwarder's configuration with specific properties:\n\n```python\n# Update a forwarder with new properties\nforwarder = chronicle.update_forwarder(\n    forwarder_id=\"1234567890\",\n    display_name=\"UpdatedForwarderName\",\n    metadata={\"labels\": {\"env\": \"prod\"}},\n    upload_compression=True\n)\n\n# Update specific fields using update mask\nforwarder = chronicle.update_forwarder(\n    forwarder_id=\"1234567890\",\n    display_name=\"ProdForwarder\",\n    update_mask=[\"display_name\"]\n)\n\nprint(f\"Updated forwarder: {forwarder['name']}\")\n```\n\n#### Delete a forwarder\n\nDelete an existing forwarder by its ID:\n\n```python\n# Delete a forwarder by ID\nchronicle.delete_forwarder(forwarder_id=\"1234567890\")\n\nprint(\"Forwarder deleted successfully\")\n```\n\n### Log Processing Pipelines\n\nChronicle log processing pipelines allow you to transform, filter, and enrich log data before it is stored in Chronicle. Common use cases include removing empty key-value pairs, redacting sensitive data, adding ingestion labels, filtering logs by field values, and extracting host information. Pipelines can be associated with log types (with optional collector IDs) and feeds, providing flexible control over your data ingestion workflow.\n\nThe SDK provides comprehensive methods for managing pipelines, associating streams, testing configurations, and fetching sample logs.\n\n#### List pipelines\n\nRetrieve all log processing pipelines in your Chronicle instance:\n\n```python\n# Get all pipelines\nresult = chronicle.list_log_processing_pipelines()\npipelines = result.get(\"logProcessingPipelines\", [])\n\nfor pipeline in pipelines:\n    pipeline_id = pipeline[\"name\"].split(\"/\")[-1]\n    print(f\"Pipeline: {pipeline['displayName']} (ID: {pipeline_id})\")\n\n# List with pagination\nresult = chronicle.list_log_processing_pipelines(\n    page_size=50,\n    page_token=\"next_page_token\"\n)\n```\n\n#### Get pipeline details\n\nRetrieve details about a specific pipeline:\n\n```python\n# Get pipeline by ID\npipeline_id = \"1234567890\"\npipeline = chronicle.get_log_processing_pipeline(pipeline_id)\n\nprint(f\"Name: {pipeline['displayName']}\")\nprint(f\"Description: {pipeline.get('description', 'N/A')}\")\nprint(f\"Processors: {len(pipeline.get('processors', []))}\")\n```\n\n#### Create a pipeline\n\nCreate a new log processing pipeline with processors:\n\n```python\n# Define pipeline configuration\npipeline_config = {\n    \"displayName\": \"My Custom Pipeline\",\n    \"description\": \"Filters and transforms application logs\",\n    \"processors\": [\n        {\n            \"filterProcessor\": {\n                \"include\": {\n                    \"logMatchType\": \"REGEXP\",\n                    \"logBodies\": [\".*error.*\", \".*warning.*\"],\n                },\n                \"errorMode\": \"IGNORE\",\n            }\n        }\n    ],\n    \"customMetadata\": [\n        {\"key\": \"environment\", \"value\": \"production\"},\n        {\"key\": \"team\", \"value\": \"security\"}\n    ]\n}\n\n# Create the pipeline (server generates ID)\ncreated_pipeline = chronicle.create_log_processing_pipeline(\n    pipeline=pipeline_config\n)\n\npipeline_id = created_pipeline[\"name\"].split(\"/\")[-1]\nprint(f\"Created pipeline with ID: {pipeline_id}\")\n```\n\n#### Update a pipeline\n\nUpdate an existing pipeline's configuration:\n\n```python\n# Get the existing pipeline first\npipeline = chronicle.get_log_processing_pipeline(pipeline_id)\n\n# Update specific fields\nupdated_config = {\n    \"name\": pipeline[\"name\"], \n    \"description\": \"Updated description\",\n    \"processors\": pipeline[\"processors\"]\n}\n\n# Patch with update mask\nupdated_pipeline = chronicle.update_log_processing_pipeline(\n    pipeline_id=pipeline_id,\n    pipeline=updated_config,\n    update_mask=\"description\"\n)\n\nprint(f\"Updated: {updated_pipeline['displayName']}\")\n```\n\n#### Delete a pipeline\n\nDelete an existing pipeline:\n\n```python\n# Delete by ID\nchronicle.delete_log_processing_pipeline(pipeline_id)\nprint(\"Pipeline deleted successfully\")\n\n# Delete with etag for concurrency control\nchronicle.delete_log_processing_pipeline(\n    pipeline_id=pipeline_id,\n    etag=\"etag_value\"\n)\n```\n\n#### Associate streams with a pipeline\n\nAssociate log streams (by log type or feed) with a pipeline:\n\n```python\n# Associate by log type\nstreams = [\n    {\"logType\": \"WINEVTLOG\"},\n    {\"logType\": \"LINUX\"}\n]\n\nchronicle.associate_streams(\n    pipeline_id=pipeline_id,\n    streams=streams\n)\nprint(\"Streams associated successfully\")\n\n# Associate by feed ID\nfeed_streams = [\n    {\"feed\": \"feed-uuid-1\"},\n    {\"feed\": \"feed-uuid-2\"}\n]\n\nchronicle.associate_streams(\n    pipeline_id=pipeline_id,\n    streams=feed_streams\n)\n```\n\n#### Dissociate streams from a pipeline\n\nRemove stream associations from a pipeline:\n\n```python\n# Dissociate streams\nstreams = [{\"logType\": \"WINEVTLOG\"}]\n\nchronicle.dissociate_streams(\n    pipeline_id=pipeline_id,\n    streams=streams\n)\nprint(\"Streams dissociated successfully\")\n```\n\n#### Fetch associated pipeline\n\nFind which pipeline is associated with a specific stream:\n\n```python\n# Find pipeline for a log type\nstream_query = {\"logType\": \"WINEVTLOG\"}\nassociated = chronicle.fetch_associated_pipeline(stream=stream_query)\n\nif associated:\n    print(f\"Associated pipeline: {associated['name']}\")\nelse:\n    print(\"No pipeline associated with this stream\")\n\n# Find pipeline for a feed\nfeed_query = {\"feed\": \"feed-uuid\"}\nassociated = chronicle.fetch_associated_pipeline(stream=feed_query)\n```\n\n#### Fetch sample logs\n\nRetrieve sample logs for specific streams:\n\n```python\n# Fetch sample logs for log types\nstreams = [\n    {\"logType\": \"WINEVTLOG\"},\n    {\"logType\": \"LINUX\"}\n]\n\nresult = chronicle.fetch_sample_logs_by_streams(\n    streams=streams,\n    sample_logs_count=10\n)\n\nfor log in result.get(\"logs\", []):\n    print(f\"Log: {log}\")\n```\n\n#### Test a pipeline\n\nTest a pipeline configuration against sample logs before deployment:\n\n```python\nimport base64\nfrom datetime import datetime, timezone\n\n# Define pipeline to test\npipeline_config = {\n    \"displayName\": \"Test Pipeline\",\n    \"processors\": [\n        {\n            \"filterProcessor\": {\n                \"include\": {\n                    \"logMatchType\": \"REGEXP\",\n                    \"logBodies\": [\".*\"],\n                },\n                \"errorMode\": \"IGNORE\",\n            }\n        }\n    ]\n}\n\n# Create test logs with base64-encoded data\ncurrent_time = datetime.now(timezone.utc).isoformat()\nlog_data = base64.b64encode(b\"Sample log entry\").decode(\"utf-8\")\n\ninput_logs = [\n    {\n        \"data\": log_data,\n        \"logEntryTime\": current_time,\n        \"collectionTime\": current_time,\n    }\n]\n\n# Test the pipeline\nresult = chronicle.test_pipeline(\n    pipeline=pipeline_config,\n    input_logs=input_logs\n)\n\nprint(f\"Processed {len(result.get('logs', []))} logs\")\nfor processed_log in result.get(\"logs\", []):\n    print(f\"Result: {processed_log}\")\n```\n\n5. Use custom timestamps:\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Define custom timestamps\nlog_entry_time = datetime.now(timezone.utc) - timedelta(hours=1)\ncollection_time = datetime.now(timezone.utc)\n\nresult = chronicle.ingest_log(\n    log_type=\"OKTA\",\n    log_message=json.dumps(okta_log),\n    log_entry_time=log_entry_time,  # When the log was generated\n    collection_time=collection_time  # When the log was collected\n)\n```\n\nIngest UDM events directly into Chronicle:\n\n```python\nimport uuid\nfrom datetime import datetime, timezone\n\n# Generate a unique ID\nevent_id = str(uuid.uuid4())\n\n# Get current time in ISO 8601 format\ncurrent_time = datetime.now(timezone.utc).isoformat().replace(\"+00:00\", \"Z\")\n\n# Create a UDM event for a network connection\nnetwork_event = {\n    \"metadata\": {\n        \"id\": event_id,\n        \"event_timestamp\": current_time,\n        \"event_type\": \"NETWORK_CONNECTION\",\n        \"product_name\": \"My Security Product\", \n        \"vendor_name\": \"My Company\"\n    },\n    \"principal\": {\n        \"hostname\": \"workstation-1\",\n        \"ip\": \"192.168.1.100\",\n        \"port\": 12345\n    },\n    \"target\": {\n        \"ip\": \"203.0.113.10\",\n        \"port\": 443\n    },\n    \"network\": {\n        \"application_protocol\": \"HTTPS\",\n        \"direction\": \"OUTBOUND\"\n    }\n}\n\n# Ingest a single UDM event\nresult = chronicle.ingest_udm(udm_events=network_event)\nprint(f\"Ingested event with ID: {event_id}\")\n\n# Create a second event\nprocess_event = {\n    \"metadata\": {\n        # No ID - one will be auto-generated\n        \"event_timestamp\": current_time,\n        \"event_type\": \"PROCESS_LAUNCH\",\n        \"product_name\": \"My Security Product\", \n        \"vendor_name\": \"My Company\"\n    },\n    \"principal\": {\n        \"hostname\": \"workstation-1\",\n        \"process\": {\n            \"command_line\": \"ping 8.8.8.8\",\n            \"pid\": 1234\n        },\n        \"user\": {\n            \"userid\": \"user123\"\n        }\n    }\n}\n\n# Ingest multiple UDM events in a single call\nresult = chronicle.ingest_udm(udm_events=[network_event, process_event])\nprint(\"Multiple events ingested successfully\")\n```\n\nImport entities into Chronicle:\n\n```python\n# Create a sample entity\nentity = {\n    \"metadata\": {\n        \"collected_timestamp\": \"2025-01-01T00:00:00Z\",\n        \"vendor_name\": \"TestVendor\",\n        \"product_name\": \"TestProduct\",\n        \"entity_type\": \"USER\",\n    },\n    \"entity\": {\n        \"user\": {\n            \"userid\": \"testuser\",\n        }\n    },\n}\n\n# Import a single entity\nresult = chronicle.import_entities(entities=entity, log_type=\"TEST_LOG_TYPE\")\nprint(f\"Imported entity: {result}\")\n\n# Import multiple entities\nentity2 = {\n    \"metadata\": {\n        \"collected_timestamp\": \"2025-01-01T00:00:00Z\",\n        \"vendor_name\": \"TestVendor\",\n        \"product_name\": \"TestProduct\",\n        \"entity_type\": \"ASSET\",\n    },\n    \"entity\": {\n        \"asset\": {\n            \"hostname\": \"testhost\",\n        }\n    },\n}\nentities = [entity, entity2]\nresult = chronicle.import_entities(entities=entities, log_type=\"TEST_LOG_TYPE\")\nprint(f\"Imported entities: {result}\")\n```\n\n### Data Export\n\n\u003e **Note**: The Data Export API features are currently under test and review. We welcome your feedback and encourage you to submit any issues or unexpected behavior to the issue tracker so we can improve this functionality.\n\nYou can export Chronicle logs to Google Cloud Storage using the Data Export API:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Set time range for export\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(days=1)  # Last 24 hours\n\n# Get available log types for export\navailable_log_types = chronicle.fetch_available_log_types(\n    start_time=start_time,\n    end_time=end_time\n)\n\n# Print available log types\nfor log_type in available_log_types[\"available_log_types\"]:\n    print(f\"{log_type.display_name} ({log_type.log_type.split('/')[-1]})\")\n    print(f\"  Available from {log_type.start_time} to {log_type.end_time}\")\n\n# Create a data export for a single log type (legacy method)\nexport = chronicle.create_data_export(\n    gcs_bucket=\"projects/my-project/buckets/my-export-bucket\",\n    start_time=start_time,\n    end_time=end_time,\n    log_type=\"GCP_DNS\"  # Single log type to export\n)\n\n# Create a data export for multiple log types\nexport_multiple = chronicle.create_data_export(\n    gcs_bucket=\"projects/my-project/buckets/my-export-bucket\",\n    start_time=start_time,\n    end_time=end_time,\n    log_types=[\"WINDOWS\", \"LINUX\", \"GCP_DNS\"]  # Multiple log types to export\n)\n\n# Get the export ID\nexport_id = export[\"name\"].split(\"/\")[-1]\nprint(f\"Created export with ID: {export_id}\")\n    print(f\"Status: {export['data_export_status']['stage']}\")\n\n# List recent exports\nrecent_exports = chronicle.list_data_export(page_size=10)\nprint(f\"Found {len(recent_exports.get('dataExports', []))} recent exports\")\n\n# Print details of recent exports\nfor item in recent_exports.get(\"dataExports\", []):\n    item_id = item[\"name\"].split(\"/\")[-1]\n    if \"dataExportStatus\" in item:\n        status = item[\"dataExportStatus\"][\"stage\"]\n    else:\n        status = item[\"data_export_status\"][\"stage\"]\n    print(f\"Export ID: {item_id}, Status: {status}\")\n\n# Check export status\nstatus = chronicle.get_data_export(export_id)\n\n# Update an export that is in IN_QUEUE state\nif status.get(\"dataExportStatus\", {}).get(\"stage\") == \"IN_QUEUE\":\n    # Update with a new start time\n    updated_start = start_time + timedelta(hours=2)\n    update_result = chronicle.update_data_export(\n        data_export_id=export_id,\n        start_time=updated_start,\n        # Optionally update other parameters like end_time, gcs_bucket, or log_types\n    )\n    print(\"Export updated successfully\")\n\n# Cancel an export if needed\nif status.get(\"dataExportStatus\", {}).get(\"stage\") in [\"IN_QUEUE\", \"PROCESSING\"]:\n    cancelled = chronicle.cancel_data_export(export_id)\n    print(f\"Export has been cancelled. New status: {cancelled['data_export_status']['stage']}\")\n\n# Export all log types at once\nexport_all = chronicle.create_data_export(\n    gcs_bucket=\"projects/my-project/buckets/my-export-bucket\",\n    start_time=start_time,\n    end_time=end_time,\n    export_all_logs=True\n)\n\nprint(f\"Created export for all logs. Status: {export_all['data_export_status']['stage']}\")\n```\n\nThe Data Export API supports:\n- Exporting one, multiple, or all log types to Google Cloud Storage\n- Listing recent exports and filtering results\n- Checking export status and progress\n- Updating exports that are in the queue\n- Cancelling exports in progress\n- Fetching available log types for a specific time range\n\nIf you encounter any issues with the Data Export functionality, please submit them to our issue tracker with detailed information about the problem and steps to reproduce.\n\n### Basic UDM Search\n\nSearch for network connection events:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Set time range for queries\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(hours=24)  # Last 24 hours\n\n# Perform UDM search\nresults = chronicle.search_udm(\n    query=\"\"\"\n    metadata.event_type = \"NETWORK_CONNECTION\"\n    ip != \"\"\n    \"\"\",\n    start_time=start_time,\n    end_time=end_time,\n    max_events=5\n)\n\n# Example response:\n{\n    \"events\": [\n        {\n            \"name\": \"projects/my-project/locations/us/instances/my-instance/events/encoded-event-id\",\n            \"udm\": {\n                \"metadata\": {\n                    \"eventTimestamp\": \"2024-02-09T10:30:00Z\",\n                    \"eventType\": \"NETWORK_CONNECTION\"\n                },\n                \"target\": {\n                    \"ip\": [\"192.168.1.100\"],\n                    \"port\": 443\n                },\n                \"principal\": {\n                    \"hostname\": \"workstation-1\"\n                }\n            }\n        }\n    ],\n    \"total_events\": 1,\n    \"more_data_available\": false\n}\n```\n\n### UDM Search View\n\nRetrieve UDM search results with additional contextual information, including detection data:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Set time range for queries\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(hours=24)  # Last 24 hours\n\n# Fetch UDM search view results\nresults = chronicle.fetch_udm_search_view(\n    query='metadata.event_type = \"NETWORK_CONNECTION\"',\n    start_time=start_time,\n    end_time=end_time,\n    max_events=5,  # Limit to 5 events\n    max_detections=10,  # Get up to 10 detections\n    snapshot_query='feedback_summary.status = \"OPEN\"',  # Filter for open alerts\n    case_insensitive=True  # Case-insensitive search\n)\n```\n\n\u003e **Note:** The `fetch_udm_search_view` method is synchronous and returns all results at once, not as a streaming response since the parameter passed to endpoint(legacyFetchUDMSearchView) provides synchronous response.\n\n### Fetch UDM Field Values\n\nSearch for ingested UDM field values that match a query:\n\n```python\n# Search for fields containing \"source\"\nresults = chronicle.find_udm_field_values(\n    query=\"source\",\n    page_size=10\n)\n\n# Example response:\n{\n    \"valueMatches\": [\n        {\n            \"fieldPath\": \"metadata.ingestion_labels.key\",\n            \"value\": \"source\",\n            \"ingestionTime\": \"2025-08-18T08:00:11.670673Z\",\n            \"matchEnd\": 6\n        },\n        {\n            \"fieldPath\": \"additional.fields.key\",\n            \"value\": \"source\",\n            \"ingestionTime\": \"2025-02-18T19:45:01.811426Z\",\n            \"matchEnd\": 6\n        }\n    ],\n    \"fieldMatches\": [\n        {\n            \"fieldPath\": \"about.labels.value\"\n        },\n        {\n            \"fieldPath\": \"additional.fields.value.string_value\"\n        }\n    ],\n    \"fieldMatchRegex\": \"source\"\n}\n```\n\n### Statistics Queries\n\nGet statistics about network connections grouped by hostname:\n\n```python\nstats = chronicle.get_stats(\n    query=\"\"\"metadata.event_type = \"NETWORK_CONNECTION\"\nmatch:\n    target.hostname\noutcome:\n    $count = count(metadata.id)\norder:\n    $count desc\"\"\",\n    start_time=start_time,\n    end_time=end_time,\n    max_events=1000,\n    max_values=10,\n    timeout=180\n)\n\n# Example response:\n{\n    \"columns\": [\"hostname\", \"count\"],\n    \"rows\": [\n        {\"hostname\": \"server-1\", \"count\": 1500},\n        {\"hostname\": \"server-2\", \"count\": 1200}\n    ],\n    \"total_rows\": 2\n}\n```\n\n### CSV Export\n\nExport specific fields to CSV format:\n\n```python\ncsv_data = chronicle.fetch_udm_search_csv(\n    query='metadata.event_type = \"NETWORK_CONNECTION\"',\n    start_time=start_time,\n    end_time=end_time,\n    fields=[\"timestamp\", \"user\", \"hostname\", \"process name\"]\n)\n\n# Example response:\n\"\"\"\nmetadata.eventTimestamp,principal.hostname,target.ip,target.port\n2024-02-09T10:30:00Z,workstation-1,192.168.1.100,443\n2024-02-09T10:31:00Z,workstation-2,192.168.1.101,80\n\"\"\"\n```\n\n### Query Validation\n\nValidate a UDM query before execution:\n\n```python\nquery = 'target.ip != \"\" and principal.hostname = \"test-host\"'\nvalidation = chronicle.validate_query(query)\n\n# Example response:\n{\n    \"isValid\": true,\n    \"queryType\": \"QUERY_TYPE_UDM_QUERY\",\n    \"suggestedFields\": [\n        \"target.ip\",\n        \"principal.hostname\"\n    ]\n}\n```\n\n### Natural Language Search\n\nSearch for events using natural language instead of UDM query syntax:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Set time range for queries\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(hours=24)  # Last 24 hours\n\n# Option 1: Translate natural language to UDM query\nudm_query = chronicle.translate_nl_to_udm(\"show me network connections\")\nprint(f\"Translated query: {udm_query}\")\n# Example output: 'metadata.event_type=\"NETWORK_CONNECTION\"'\n\n# Then run the query manually if needed\nresults = chronicle.search_udm(\n    query=udm_query,\n    start_time=start_time,\n    end_time=end_time\n)\n\n# Option 2: Perform complete search with natural language\nresults = chronicle.nl_search(\n    text=\"show me failed login attempts\",\n    start_time=start_time,\n    end_time=end_time,\n    max_events=100\n)\n\n# Example response (same format as search_udm):\n{\n    \"events\": [\n        {\n            \"event\": {\n                \"metadata\": {\n                    \"eventTimestamp\": \"2024-02-09T10:30:00Z\",\n                    \"eventType\": \"USER_LOGIN\"\n                },\n                \"principal\": {\n                    \"user\": {\n                        \"userid\": \"jdoe\"\n                    }\n                },\n                \"securityResult\": {\n                    \"action\": \"BLOCK\",\n                    \"summary\": \"Failed login attempt\"\n                }\n            }\n        }\n    ],\n    \"total_events\": 1\n}\n```\n\nThe natural language search feature supports various query patterns:\n- \"Show me network connections\"\n- \"Find suspicious processes\"\n- \"Show login failures in the last hour\"\n- \"Display connections to IP address 192.168.1.100\"\n\nIf the natural language cannot be translated to a valid UDM query, an `APIError` will be raised with a message indicating that no valid query could be generated.\n\n### Entity Summary\n\nGet detailed information about specific entities like IP addresses, domains, or file hashes. The function automatically detects the entity type based on the provided value and fetches a comprehensive summary including related entities, alerts, timeline, prevalence, and more.\n\n```python\n# IP address summary\nip_summary = chronicle.summarize_entity(\n    value=\"8.8.8.8\",\n    start_time=start_time,\n    end_time=end_time\n)\n\n# Domain summary\ndomain_summary = chronicle.summarize_entity(\n    value=\"google.com\",\n    start_time=start_time,\n    end_time=end_time\n)\n\n# File hash summary (SHA256)\nfile_hash = \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\" \nfile_summary = chronicle.summarize_entity(\n    value=file_hash,\n    start_time=start_time,\n    end_time=end_time\n)\n\n# Optionally hint the preferred type if auto-detection might be ambiguous\nuser_summary = chronicle.summarize_entity(\n    value=\"jdoe\",\n    start_time=start_time,\n    end_time=end_time,\n    preferred_entity_type=\"USER\"\n)\n\n\n# Example response structure (EntitySummary object):\n# Access attributes like: ip_summary.primary_entity, ip_summary.related_entities,\n# ip_summary.alert_counts, ip_summary.timeline, ip_summary.prevalence, etc.\n\n# Example fields within the EntitySummary object:\n# primary_entity: {\n#     \"name\": \"entities/...\",\n#     \"metadata\": {\n#         \"entityType\": \"ASSET\",  # Or FILE, DOMAIN_NAME, USER, etc.\n#         \"interval\": { \"startTime\": \"...\", \"endTime\": \"...\" }\n#     },\n#     \"metric\": { \"firstSeen\": \"...\", \"lastSeen\": \"...\" },\n#     \"entity\": {  # Contains specific details like 'asset', 'file', 'domain'\n#         \"asset\": { \"ip\": [\"8.8.8.8\"] }\n#     }\n# }\n# related_entities: [ { ... similar to primary_entity ... } ]\n# alert_counts: [ { \"rule\": \"Rule Name\", \"count\": 5 } ]\n# timeline: { \"buckets\": [ { \"alertCount\": 1, \"eventCount\": 10 } ], \"bucketSize\": \"3600s\" }\n# prevalence: [ { \"prevalenceTime\": \"...\", \"count\": 100 } ]\n# file_metadata_and_properties: {  # Only for FILE entities\n#     \"metadata\": [ { \"key\": \"...\", \"value\": \"...\" } ],\n#     \"properties\": [ { \"title\": \"...\", \"properties\": [ { \"key\": \"...\", \"value\": \"...\" } ] } ]\n# }\n```\n\n### List IoCs (Indicators of Compromise)\n\nRetrieve IoC matches against ingested events:\n\n```python\niocs = chronicle.list_iocs(\n    start_time=start_time,\n    end_time=end_time,\n    max_matches=1000,\n    add_mandiant_attributes=True,\n    prioritized_only=False\n)\n\n# Process the results\nfor ioc in iocs['matches']:\n    ioc_type = next(iter(ioc['artifactIndicator'].keys()))\n    ioc_value = next(iter(ioc['artifactIndicator'].values()))\n    print(f\"IoC Type: {ioc_type}, Value: {ioc_value}\")\n    print(f\"Sources: {', '.join(ioc['sources'])}\")\n```\n\nThe IoC response includes:\n- The indicator itself (domain, IP, hash, etc.)\n- Sources and categories\n- Affected assets in your environment\n- First and last seen timestamps\n- Confidence scores and severity ratings\n- Associated threat actors and malware families (with Mandiant attributes)\n\n### Alerts and Case Management\n\nRetrieve alerts and their associated cases:\n\n```python\n# Get non-closed alerts\nalerts = chronicle.get_alerts(\n    start_time=start_time,\n    end_time=end_time,\n    snapshot_query='feedback_summary.status != \"CLOSED\"',\n    max_alerts=100\n)\n\n# Get alerts from the response\nalert_list = alerts.get('alerts', {}).get('alerts', [])\n\n# Extract case IDs from alerts\ncase_ids = {alert.get('caseName') for alert in alert_list if alert.get('caseName')}\n\n# Get case details using the batch API\nif case_ids:\n    cases = chronicle.get_cases(list(case_ids))\n    \n    # Process cases\n    for case in cases.cases:\n        print(f\"Case: {case.display_name}\")\n        print(f\"Priority: {case.priority}\")\n        print(f\"Status: {case.status}\")\n        print(f\"Stage: {case.stage}\")\n        \n        # Access SOAR platform information if available\n        if case.soar_platform_info:\n            print(f\"SOAR Case ID: {case.soar_platform_info.case_id}\")\n            print(f\"SOAR Platform: {case.soar_platform_info.platform_type}\")\n```\n\nThe alerts response includes:\n- Progress status and completion status\n- Alert counts (baseline and filtered)\n- Alert details (rule information, detection details, etc.)\n- Case associations\n\nYou can filter alerts using the snapshot query parameter with fields like:\n- `detection.rule_name`\n- `detection.alert_state`\n- `feedback_summary.verdict`\n- `feedback_summary.priority`\n- `feedback_summary.status`\n\n### Case Management Helpers\n\nThe `CaseList` class provides helper methods for working with cases:\n\n```python\n# Get details for specific cases (uses the batch API)\ncases = chronicle.get_cases([\"case-id-1\", \"case-id-2\"])\n\n# Filter cases by priority\nhigh_priority = cases.filter_by_priority(\"PRIORITY_HIGH\")\n\n# Filter cases by status\nopen_cases = cases.filter_by_status(\"STATUS_OPEN\")\n\n# Look up a specific case\ncase = cases.get_case(\"case-id-1\")\n```\n\n\u003e **Note**: The case management API uses the `legacy:legacyBatchGetCases` endpoint to retrieve multiple cases in a single request. You can retrieve up to 1000 cases in a single batch.\n\n### Investigation Management\n\nChronicle investigations provide automated analysis and recommendations for alerts and cases. The SDK provides methods to list, retrieve, trigger, and fetch associated investigations.\n\n#### List investigations\n\nRetrieve all investigations in your Chronicle instance:\n\n```python\n# List all investigations\nresult = chronicle.list_investigations()\ninvestigations = result.get(\"investigations\", [])\n\nfor inv in investigations:\n    print(f\"Investigation: {inv['displayName']}\")\n    print(f\"  Status: {inv.get('status', 'N/A')}\")\n    print(f\"  Verdict: {inv.get('verdict', 'N/A')}\")\n\n# List with pagination\nresult = chronicle.list_investigations(page_size=50, page_token=\"token\")\n```\n\n#### Get investigation details\n\nRetrieve a specific investigation by its ID:\n\n```python\n# Get investigation by ID\ninvestigation = chronicle.get_investigation(investigation_id=\"inv_123\")\n\nprint(f\"Name: {investigation['displayName']}\")\nprint(f\"Status: {investigation.get('status')}\")\nprint(f\"Verdict: {investigation.get('verdict')}\")\nprint(f\"Confidence: {investigation.get('confidence')}\")\n```\n\n#### Trigger investigation for an alert\n\nCreate a new investigation for a specific alert:\n\n```python\n# Trigger investigation for an alert\ninvestigation = chronicle.trigger_investigation(alert_id=\"alert_123\")\n\nprint(f\"Investigation created: {investigation['name']}\")\nprint(f\"Status: {investigation.get('status')}\")\nprint(f\"Trigger type: {investigation.get('triggerType')}\")\n```\n\n#### Fetch associated investigations\n\nRetrieve investigations associated with alerts or cases:\n\n```python\nfrom secops.chronicle import DetectionType\n\n# Fetch investigations for specific alerts\nresult = chronicle.fetch_associated_investigations(\n    detection_type=DetectionType.ALERT,\n    alert_ids=[\"alert_123\", \"alert_456\"],\n    association_limit_per_detection=5\n)\n\n# Process associations\nassociations_list = result.get(\"associationsList\", {})\nfor alert_id, data in associations_list.items():\n    investigations = data.get(\"investigations\", [])\n    print(f\"Alert {alert_id}: {len(investigations)} investigation(s)\")\n    \n    for inv in investigations:\n        print(f\"  - {inv['displayName']}: {inv.get('verdict', 'N/A')}\")\n\n# Fetch investigations for cases\ncase_result = chronicle.fetch_associated_investigations(\n    detection_type=DetectionType.CASE,\n    case_ids=[\"case_123\"],\n    association_limit_per_detection=3\n)\n\n# You can also use string values for detection_type\nresult = chronicle.fetch_associated_investigations(\n    detection_type=\"ALERT\",  # or \"DETECTION_TYPE_ALERT\"\n    alert_ids=[\"alert_123\"]\n)\n```\n\n### Generating UDM Key/Value Mapping\nChronicle provides a feature to generate UDM key-value mapping for a given row log.\n\n```python\nmapping = chronicle.generate_udm_key_value_mappings(\n    log_format=\"JSON\",\n    log='{\"events\":[{\"id\":\"123\",\"user\":\"test_user\",\"source_ip\":\"192.168.1.10\"}]}',\n    use_array_bracket_notation=True,\n    compress_array_fields=False,\n)\n\nprint(f\"Generated UDM key/value mapping: {mapping}\")\n```\n\n```python\n# Generate UDM key-value mapping\nudm_mapping = chronicle.generate_udm_mapping(log_type=\"WINDOWS_AD\")\nprint(udm_mapping)\n```\n\n## Parser Management\n\nChronicle 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.\n\nThe SDK provides comprehensive support for managing Chronicle parsers:\n\n### Creating Parsers\n\nCreate new parser:\n\n```python\nparser_text = \"\"\"\nfilter {\n    mutate {\n      replace =\u003e {\n        \"event1.idm.read_only_udm.metadata.event_type\" =\u003e \"GENERIC_EVENT\"\n        \"event1.idm.read_only_udm.metadata.vendor_name\" =\u003e  \"ACME Labs\"\n      }\n    }\n    grok {\n      match =\u003e {\n        \"message\" =\u003e [\"^(?P\u003c_firstWord\u003e[^\\s]+)\\s.*$\"]\n      }\n      on_error =\u003e \"_grok_message_failed\"\n    }\n    if ![_grok_message_failed] {\n      mutate {\n        replace =\u003e {\n          \"event1.idm.read_only_udm.metadata.description\" =\u003e \"%{_firstWord}\"\n        }\n      }\n    }\n    mutate {\n      merge =\u003e {\n        \"@output\" =\u003e \"event1\"\n      }\n    }\n}\n\"\"\"\n\nlog_type = \"WINDOWS_AD\"\n\n# Create the parser\nparser = chronicle.create_parser(\n    log_type=log_type, \n    parser_code=parser_text,\n    validated_on_empty_logs=True  # Whether to validate parser on empty logs\n)\nparser_id = parser.get(\"name\", \"\").split(\"/\")[-1]\nprint(f\"Parser ID: {parser_id}\")\n```\n\n### Managing Parsers\n\nRetrieve, list, copy, activate/deactivate, and delete parsers:\n\n```python\n# List all parsers (returns complete list)\nparsers = chronicle.list_parsers()\nfor parser in parsers:\n    parser_id = parser.get(\"name\", \"\").split(\"/\")[-1]\n    state = parser.get(\"state\")\n    print(f\"Parser ID: {parser_id}, State: {state}\")\n\n# Manual pagination: get raw API response with nextPageToken\nresponse = chronicle.list_parsers(page_size=50)\nparsers = response.get(\"parsers\", [])\nnext_token = response.get(\"nextPageToken\")\n# Use next_token for subsequent calls:\n# response = chronicle.list_parsers(page_size=50, page_token=next_token)\n\nlog_type = \"WINDOWS_AD\"\n    \n# Get specific parser\nparser = chronicle.get_parser(log_type=log_type, id=parser_id)\nprint(f\"Parser content: {parser.get('text')}\")\n\n# Activate/Deactivate parser\nchronicle.activate_parser(log_type=log_type, id=parser_id)\nchronicle.deactivate_parser(log_type=log_type, id=parser_id)\n\n# Copy an existing parser as a starting point\ncopied_parser = chronicle.copy_parser(log_type=log_type, id=\"pa_existing_parser\")\n\n# Delete parser\nchronicle.delete_parser(log_type=log_type, id=parser_id)\n\n# Force delete an active parser\nchronicle.delete_parser(log_type=log_type, id=parser_id, force=True)\n\n# Activate a release candidate parser\nchronicle.activate_release_candidate_parser(log_type=log_type, id=\"pa_release_candidate\")\n```\n\n\u003e **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.\n\n### Run Parser against sample logs\n\nRun the parser on one or more sample logs:\n\n```python\n# Sample parser code that extracts fields from logs\nparser_text = \"\"\"\nfilter {\n    mutate {\n      replace =\u003e {\n        \"event1.idm.read_only_udm.metadata.event_type\" =\u003e \"GENERIC_EVENT\"\n        \"event1.idm.read_only_udm.metadata.vendor_name\" =\u003e  \"ACME Labs\"\n      }\n    }\n    grok {\n      match =\u003e {\n        \"message\" =\u003e [\"^(?P\u003c_firstWord\u003e[^\\s]+)\\s.*$\"]\n      }\n      on_error =\u003e \"_grok_message_failed\"\n    }\n    if ![_grok_message_failed] {\n      mutate {\n        replace =\u003e {\n          \"event1.idm.read_only_udm.metadata.description\" =\u003e \"%{_firstWord}\"\n        }\n      }\n    }\n    mutate {\n      merge =\u003e {\n        \"@output\" =\u003e \"event1\"\n      }\n    }\n}\n\"\"\"\n\nlog_type = \"WINDOWS_AD\"\n\n# Sample log entries to test\nsample_logs = [\n    '{\"message\": \"ERROR: Failed authentication attempt\"}',\n    '{\"message\": \"WARNING: Suspicious activity detected\"}',\n    '{\"message\": \"INFO: User logged in successfully\"}'\n]\n\n# Run parser evaluation\nresult = chronicle.run_parser(\n    log_type=log_type, \n    parser_code=parser_text,\n    parser_extension_code=None,  # Optional parser extension\n    logs=sample_logs,\n    statedump_allowed=False  # Enable if using statedump filters\n)\n\n# Check the results\nif \"runParserResults\" in result:\n    for i, parser_result in enumerate(result[\"runParserResults\"]):\n        print(f\"\\nLog {i+1} parsing result:\")\n        if \"parsedEvents\" in parser_result:\n            print(f\"  Parsed events: {parser_result['parsedEvents']}\")\n        if \"errors\" in parser_result:\n            print(f\"  Errors: {parser_result['errors']}\")\n\n# Run parser with statedump for debugging\n# Statedump provides internal parser state useful for troubleshooting\nresult_with_statedump = chronicle.run_parser(\n    log_type=log_type, \n    parser_code=parser_text,\n    parser_extension_code=None,\n    logs=sample_logs,\n    statedump_allowed=True,  # Enable statedump in parser output\n    parse_statedump=True     # Parse statedump string into structured format\n)\n\n# Check statedump results (useful for parser debugging)\nif \"runParserResults\" in result_with_statedump:\n    for i, parser_result in enumerate(result_with_statedump[\"runParserResults\"]):\n        if \"statedumpResults\" in parser_result:\n            for dump in parser_result[\"statedumpResults\"]:\n                statedump = dump.get(\"statedumpResult\", {})\n                print(f\"\\nParser state for log {i+1}:\")\n                print(f\"  Info: {statedump.get('info', '')}\")\n                print(f\"  State: {statedump.get('state', {})}\")\n```\n\nThe `run_parser` function includes comprehensive validation:\n- Validates log type and parser code are provided\n- Ensures logs are provided as a list of strings\n- Enforces size limits (10MB per log, 50MB total, max 1000 logs)\n- Provides detailed error messages for different failure scenarios\n\n### Complete Parser Workflow Example\n\nHere's a complete example that demonstrates retrieving a parser, running it against a log, and ingesting the parsed UDM event:\n\n```python\n# Step 1: List and retrieve an OKTA parser\nparsers = chronicle.list_parsers(log_type=\"OKTA\")\nparser_id = parsers[0][\"name\"].split(\"/\")[-1]\nparser_details = chronicle.get_parser(log_type=\"OKTA\", id=parser_id)\n\n# Extract and decode parser code\nimport base64\nparser_code = base64.b64decode(parser_details[\"cbn\"]).decode('utf-8')\n\n# Step 2: Run the parser against a sample log\nokta_log = {\n    \"actor\": {\"alternateId\": \"user@example.com\", \"displayName\": \"Test User\"},\n    \"eventType\": \"user.account.lock\",\n    \"outcome\": {\"result\": \"FAILURE\", \"reason\": \"LOCKED_OUT\"},\n    \"published\": \"2025-06-19T21:51:50.116Z\"\n    # ... other OKTA log fields\n}\n\nresult = chronicle.run_parser(\n    log_type=\"OKTA\",\n    parser_code=parser_code,\n    parser_extension_code=None,\n    logs=[json.dumps(okta_log)]\n)\n\n# Step 3: Extract and ingest the parsed UDM event\nif result[\"runParserResults\"][0][\"parsedEvents\"]:\n    # parsedEvents is a dict with 'events' key containing the actual events list\n    parsed_events_data = result[\"runParserResults\"][0][\"parsedEvents\"]\n    if isinstance(parsed_events_data, dict) and \"events\" in parsed_events_data:\n        events = parsed_events_data[\"events\"]\n        if events and len(events) \u003e 0:\n            # Extract the first event\n            if \"event\" in events[0]:\n                udm_event = events[0][\"event\"]\n            else:\n                udm_event = events[0]\n            \n            # Ingest the parsed UDM event back into Chronicle\n            ingest_result = chronicle.ingest_udm(udm_events=udm_event)\n            print(f\"UDM event ingested: {ingest_result}\")\n```\n\nThis workflow is useful for:\n- Testing parsers before deployment\n- Understanding how logs are transformed to UDM format\n- Re-processing logs with updated parsers\n- Debugging parsing issues\n\n## Parser Extension\n\nParser 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.\n\nThe SDK provides comprehensive support for managing Chronicle parser extensions:\n\n### List Parser Extensions\n\nList parser extensions for a log type:\n\n```python\nlog_type = \"OKTA\"\n\nextensions = chronicle.list_parser_extensions(log_type)\nprint(f\"Found {len(extensions[\"parserExtensions\"])} parser extensions for log type: {log_type}\")\n```\n\n### Create a new parser extension\n\nCreate new parser extension using either CBN snippet, field extractor or dynamic parsing:\n\n```python\nlog_type = \"OKTA\"\nfield_extractor = {\n    \"extractors\": [\n        {\n            \"preconditionPath\": \"severity\",\n            \"preconditionValue\": \"Info\",\n            \"preconditionOp\": \"EQUALS\",\n            \"fieldPath\": \"displayMessage\",\n            \"destinationPath\": \"udm.metadata.description\",\n        }\n    ],\n    \"logFormat\": \"JSON\",\n    \"appendRepeatedFields\": True,\n}\n\nchronicle.create_parser_extension(log_type, field_extractor=field_extractor)\n```\n\n### Get parser extension\n\nGet parser extension details:\n\n```python\nlog_type = \"OKTA\"\nextension_id = \"1234567890\"\n\nextension = chronicle.get_parser_extension(log_type, extension_id)\n\nprint(extension)\n```\n\n### Activate Parser Extension\n\nActivate parser extension:\n\n```python\nlog_type = \"OKTA\"\nextension_id = \"1234567890\"\n\nchronicle.activate_parser_extension(log_type, extension_id)\n```\n\n### Delete Parser Extension\n\nDelete parser extension:\n\n```python\nlog_type = \"OKTA\"\nextension_id = \"1234567890\"\n\nchronicle.delete_parser_extension(log_type, extension_id)\n```\n\n## Watchlist Management\n\n### Creating a Watchlist\n\nCreate a new watchlist:\n\n```python\nwatchlist = chronicle.create_watchlist(\n    name=\"my_watchlist\",\n    display_name=\"my_watchlist\",\n    multiplying_factor=1.5,\n    description=\"My new watchlist\"\n)\n```\n\n### Updating a Watchlist\n\nUpdate a watchlist by ID:\n\n```python\nupdated_watchlist = chronicle.update_watchlist(\n    watchlist_id=\"abc-123-def\",\n    display_name=\"Updated Watchlist Name\",\n    description=\"Updated description\",\n    multiplying_factor=2.0,\n    entity_population_mechanism={\"manual\": {}},\n    watchlist_user_preferences={\"pinned\": True}\n)\n```\n\n### Deleting a Watchlist\n\nDelete a watchlist by ID:\n\n```python\nchronicle.delete_watchlist(\"acb-123-def\", force=True)\n```\n\n### Getting a Watchlist\n\nGet a watchlist by ID:\n\n```python\nwatchlist = chronicle.get_watchlist(\"acb-123-def\")\n```\n\n### List all Watchlists\n\nList all watchlists:\n\n```python\n# List watchlists (returns dict with pagination metadata)\nwatchlists = chronicle.list_watchlists()\nfor watchlist in watchlists.get(\"watchlists\", []):\n    print(f\"Watchlist: {watchlist.get('displayName')}\")\n\n# List watchlists as a direct list (automatically fetches all pages)\nwatchlists = chronicle.list_watchlists(as_list=True)\nfor watchlist in watchlists:\n    print(f\"Watchlist: {watchlist.get('displayName')}\")\n```\n\n## Rule Management\n\nThe SDK provides comprehensive support for managing Chronicle detection rules:\n\n### Creating Rules\n\nCreate new detection rules using YARA-L 2.0 syntax:\n\n```python\nrule_text = \"\"\"\nrule simple_network_rule {\n    meta:\n        description = \"Example rule to detect network connections\"\n        author = \"SecOps SDK Example\"\n        severity = \"Medium\"\n        priority = \"Medium\"\n        yara_version = \"YL2.0\"\n        rule_version = \"1.0\"\n    events:\n        $e.metadata.event_type = \"NETWORK_CONNECTION\"\n        $e.principal.hostname != \"\"\n    condition:\n        $e\n}\n\"\"\"\n\n# Create the rule\nrule = chronicle.create_rule(rule_text)\nrule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\nprint(f\"Rule ID: {rule_id}\")\n```\n\n### Managing Rules\n\nRetrieve, list, update, enable/disable, and delete rules:\n\n```python\n# List all rules\nrules = chronicle.list_rules()\nfor rule in rules.get(\"rules\", []):\n    rule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\n    enabled = rule.get(\"deployment\", {}).get(\"enabled\", False)\n    print(f\"Rule ID: {rule_id}, Enabled: {enabled}\")\n\n# List paginated rules and `REVISION_METADATA_ONLY` view\nrules = chronicle.list_rules(view=\"REVISION_METADATA_ONLY\",page_size=50)\nprint(f\"Fetched {len(rules.get(\"rules\"))} rules\")\n\n# Get specific rule\nrule = chronicle.get_rule(rule_id)\nprint(f\"Rule content: {rule.get('text')}\")\n\n# Update rule\nupdated_rule = chronicle.update_rule(rule_id, updated_rule_text)\n\n# Enable/disable rule\ndeployment = chronicle.enable_rule(rule_id, enabled=True)  # Enable\ndeployment = chronicle.enable_rule(rule_id, enabled=False) # Disable\n\n# Delete rule\nchronicle.delete_rule(rule_id)\n```\n\n### Rule Deployment\n\nManage a rule's deployment (enabled/alerting/archive state and run frequency):\n\n```python\n# Get current deployment for a rule\ndeployment = chronicle.get_rule_deployment(rule_id)\n\n# List deployments (paginated)\npage = chronicle.list_rule_deployments(page_size=10)\n\n# List deployments with filter\nfiltered = chronicle.list_rule_deployments(filter_query=\"enabled=true\")\n\n# Update deployment fields (partial updates supported)\nchronicle.update_rule_deployment(\n    rule_id=rule_id,\n    enabled=True,          # continuously execute\n    alerting=False,        # detections do not generate alerts\n    run_frequency=\"LIVE\" # LIVE | HOURLY | DAILY\n)\n\n# Archive a rule (must set enabled to False when archived=True)\nchronicle.update_rule_deployment(\n    rule_id=rule_id,\n    archived=True\n)\n```\n\n### Searching Rules\n\nSearch for rules using regular expressions:\n\n```python\n# Search for rules containing specific patterns\nresults = chronicle.search_rules(\"suspicious process\")\nfor rule in results.get(\"rules\", []):\n    rule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\n    print(f\"Rule ID: {rule_id}, contains: 'suspicious process'\")\n    \n# Find rules mentioning a specific MITRE technique\nmitre_rules = chronicle.search_rules(\"T1055\")\nprint(f\"Found {len(mitre_rules.get('rules', []))} rules mentioning T1055 technique\")\n```\n\n### Testing Rules\n\nTest rules against historical data to validate their effectiveness before deployment:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Define time range for testing\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(days=7)  # Test against last 7 days\n\n# Rule to test\nrule_text = \"\"\"\nrule test_rule {\n    meta:\n        description = \"Test rule for validation\"\n        author = \"Test Author\"\n        severity = \"Low\"\n        yara_version = \"YL2.0\"\n        rule_version = \"1.0\"\n    events:\n        $e.metadata.event_type = \"NETWORK_CONNECTION\"\n    condition:\n        $e\n}\n\"\"\"\n\n# Test the rule\ntest_results = chronicle.run_rule_test(\n    rule_text=rule_text,\n    start_time=start_time,\n    end_time=end_time,\n    max_results=100\n)\n\n# Process streaming results\ndetection_count = 0\nfor result in test_results:\n    result_type = result.get(\"type\")\n    \n    if result_type == \"progress\":\n        # Progress update\n        percent_done = result.get(\"percentDone\", 0)\n        print(f\"Progress: {percent_done}%\")\n    \n    elif result_type == \"detection\":\n        # Detection result\n        detection_count += 1\n        detection = result.get(\"detection\", {})\n        print(f\"Detection {detection_count}:\")\n        \n        # Process detection details\n        if \"rule_id\" in detection:\n            print(f\"  Rule ID: {detection['rule_id']}\")\n        if \"data\" in detection:\n            print(f\"  Data: {detection['data']}\")\n            \n    elif result_type == \"error\":\n        # Error information\n        print(f\"Error: {result.get('message', 'Unknown error')}\")\n\nprint(f\"Finished testing. Found {detection_count} detection(s).\")\n```\n\n# Extract just the UDM events for programmatic processing\n```python\nudm_events = []\nfor result in chronicle.run_rule_test(rule_text, start_time, end_time, max_results=100):\n    if result.get(\"type\") == \"detection\":\n        detection = result.get(\"detection\", {})\n        result_events = detection.get(\"resultEvents\", {})\n        \n        for var_name, var_data in result_events.items():\n            event_samples = var_data.get(\"eventSamples\", [])\n            for sample in event_samples:\n                event = sample.get(\"event\")\n                if event:\n                    udm_events.append(event)\n\n# Process the UDM events\nfor event in udm_events:\n    # Process each UDM event\n    metadata = event.get(\"metadata\", {})\n    print(f\"Event type: {metadata.get('eventType')}\")\n```\n\n### Retrohunts\n\nRun rules against historical data to find past matches:\n\n```python\nfrom datetime import datetime, timedelta, timezone\n\n# Set time range for retrohunt\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(days=7)  # Search past 7 days\n\n# Create retrohunt\nretrohunt = chronicle.create_retrohunt(rule_id, start_time, end_time)\noperation_id = retrohunt.get(\"name\", \"\").split(\"/\")[-1]\n\n# Check retrohunt status\nretrohunt_status = chronicle.get_retrohunt(rule_id, operation_id)\nstate = retrohunt_status.get(\"state\", \"\")\n\n# List retrohunts for a rule\nretrohunts = chronicle.list_retrohunts(rule_id)\n```\n\n### Detections and Errors\n\nMonitor rule detections and execution errors:\n\n```python\nfrom datetime import datetime, timedelta\n\nstart_time = datetime.now(timezone.utc)\nend_time = start_time - timedelta(days=7)\n# List detections for a rule\ndetections = chronicle.list_detections(\n    rule_id=rule_id,\n    start_time=start_time,\n    end_time=end_time,\n    list_basis=\"CREATED_TIME\"\n)\nfor detection in detections.get(\"detections\", []):\n    detection_id = detection.get(\"id\", \"\")\n    event_time = detection.get(\"eventTime\", \"\")\n    alerting = detection.get(\"alertState\", \"\") == \"ALERTING\"\n    print(f\"Detection: {detection_id}, Time: {event_time}, Alerting: {alerting}\")\n\n# List execution errors for a rule\nerrors = chronicle.list_errors(rule_id)\nfor error in errors.get(\"ruleExecutionErrors\", []):\n    error_message = error.get(\"error_message\", \"\")\n    create_time = error.get(\"create_time\", \"\")\n    print(f\"Error: {error_message}, Time: {create_time}\")\n```\n\n### Rule Alerts\n\nSearch for alerts generated by rules:\n\n```python\n# Set time range for alert search\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(days=7)  # Search past 7 days\n\n# Search for rule alerts\nalerts_response = chronicle.search_rule_alerts(\n    start_time=start_time,\n    end_time=end_time,\n    page_size=10\n)\n\n# The API returns a nested structure where alerts are grouped by rule\n# Extract and process all alerts from this structure\nall_alerts = []\ntoo_many_alerts = alerts_response.get('tooManyAlerts', False)\n\n# Process the nested response structure - alerts are grouped by rule\nfor rule_alert in alerts_response.get('ruleAlerts', []):\n    # Extract rule metadata\n    rule_metadata = rule_alert.get('ruleMetadata', {})\n    rule_id = rule_metadata.get('properties', {}).get('ruleId', 'Unknown')\n    rule_name = rule_metadata.get('properties', {}).get('name', 'Unknown')\n    \n    # Get alerts for this rule\n    rule_alerts = rule_alert.get('alerts', [])\n    \n    # Process each alert\n    for alert in rule_alerts:\n        # Extract important fields\n        alert_id = alert.get(\"id\", \"\")\n        detection_time = alert.get(\"detectionTimestamp\", \"\")\n        commit_time = alert.get(\"commitTimestamp\", \"\")\n        alerting_type = alert.get(\"alertingType\", \"\")\n        \n        print(f\"Alert ID: {alert_id}\")\n        print(f\"Rule ID: {rule_id}\")\n        print(f\"Rule Name: {rule_name}\")\n        print(f\"Detection Time: {detection_time}\")\n        \n        # Extract events from the alert\n        if 'resultEvents' in alert:\n            for var_name, event_data in alert.get('resultEvents', {}).items():\n                if 'eventSamples' in event_data:\n                    for sample in event_data.get('eventSamples', []):\n                        if 'event' in sample:\n                            event = sample['event']\n                            # Process event data\n                            event_type = event.get('metadata', {}).get('eventType', 'Unknown')\n                            print(f\"Event Type: {event_type}\")\n```\n\nIf `tooManyAlerts` is True in the response, consider narrowing your search criteria using a smaller time window or more specific filters.\n\n### Curated Rule Sets\n\nQuery curated rules:\n\n```python\n# List all curated rules (returns dict with pagination metadata)\nresult = chronicle.list_curated_rules()\nfor rule in result.get(\"curatedRules\", []):\n    rule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule.get(\"description\")\n    description = rule.get(\"description\")\n    print(f\"Rule: {display_name}, Description: {description}\")\n\n# List all curated rules as a direct list\nrules = chronicle.list_curated_rules(as_list=True)\nfor rule in rules:\n    rule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule.get(\"description\")\n    print(f\"Rule: {display_name}\")\n\n# Get a curated rule\nrule = chronicle.get_curated_rule(\"ur_ttp_lol_Atbroker\")\n\n# Get a curated rule set by display name\n# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets.\nrule_set = chronicle.get_curated_rule_by_name(\"Atbroker.exe Abuse\")\n```\n\nSearch for curated rules detections:\n\n```python\nfrom datetime import datetime, timedelta, timezone\nfrom secops.chronicle.models import AlertState, ListBasis\n\n# Search for detections from a specific curated rule\nend_time = datetime.now(timezone.utc)\nstart_time = end_time - timedelta(days=7)\n\nresult = chronicle.search_curated_detections(\n    rule_id=\"ur_ttp_GCP_MassSecretDeletion\",\n    start_time=start_time,\n    end_time=end_time,\n    list_basis=ListBasis.DETECTION_TIME,\n    alert_state=AlertState.ALERTING,\n    page_size=100\n)\n\ndetections = result.get(\"curatedDetections\", [])\nprint(f\"Found {len(detections)} detections\")\n\n# Check if more results are available\nif \"nextPageToken\" in result:\n    # Retrieve next page\n    next_result = chronicle.search_curated_detections(\n        rule_id=\"ur_ttp_GCP_MassSecretDeletion\",\n        start_time=start_time,\n        end_time=end_time,\n        list_basis=ListBasis.DETECTION_TIME,\n        page_token=result[\"nextPageToken\"],\n        page_size=100\n    )\n```\n\nQuery curated rule sets:\n\n```python\n# List all curated rule sets (returns dict with pagination metadata)\nresult = chronicle.list_curated_rule_sets()\nfor rule_set in result.get(\"curatedRuleSets\", []):\n    rule_set_id = rule_set.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule_set.get(\"displayName\")\n    print(f\"Rule Set: {display_name}, ID: {rule_set_id}\")\n\n# List all curated rule sets as a direct list\nrule_sets = chronicle.list_curated_rule_sets(as_list=True)\nfor rule_set in rule_sets:\n    rule_set_id = rule_set.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule_set.get(\"displayName\")\n    print(f\"Rule Set: {display_name}, ID: {rule_set_id}\")\n\n# Get a curated rule set by ID\nrule_set = chronicle.get_curated_rule_set(\"00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93\")\n```\n\nQuery curated rule set categories:\n\n```python\n# List all curated rule set categories (returns dict with pagination metadata)\nresult = chronicle.list_curated_rule_set_categories()\nfor rule_set_category in result.get(\"curatedRuleSetCategories\", []):\n    rule_set_category_id = rule_set_category.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule_set_category.get(\"displayName\")\n    print(f\"Rule Set Category: {display_name}, ID: {rule_set_category_id}\")\n\n# List all curated rule set categories as a direct list\nrule_set_categories = chronicle.list_curated_rule_set_categories(as_list=True)\nfor rule_set_category in rule_set_categories:\n    rule_set_category_id = rule_set_category.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rule_set_category.get(\"displayName\")\n    print(f\"Rule Set Category: {display_name}, ID: {rule_set_category_id}\")\n\n# Get a curated rule set category by ID\nrule_set_category = chronicle.get_curated_rule_set_category(\"110fa43d-7165-2355-1985-a63b7cdf90e8\")\n```\n\nManage curated rule set deployments (turn alerting on or off (either precise or broad) for curated rule sets):\n\n```python\n# List all curated rule set deployments (returns dict with pagination metadata)\nresult = chronicle.list_curated_rule_set_deployments()\nfor rs_deployment in result.get(\"curatedRuleSetDeployments\", []):\n    rule_set_id = rs_deployment.get(\"name\", \"\").split(\"/\")[-3]\n    category_id = rs_deployment.get(\"name\", \"\").split(\"/\")[-5]\n    deployment_status = rs_deployment.get(\"name\", \"\").split(\"/\")[-1]\n    display_name = rs_deployment.get(\"displayName\")\n    alerting = rs_deployment.get(\"alerting\", False)\n    print(\n        f\"Rule Set: {display_name},\"\n        f\"Rule Set ID: {rule_set_id}\",\n        f\"Category ID: {category_id}\",\n        f\"Precision: {deployment_status}\",\n        f\"Alerting: {alerting}\",\n    )\n\n# List all curated rule set deployments as a direct list\nrule_set_deployments = chronicle.list_curated_rule_set_deployments(as_list=True)\nfor rs_deployment in rule_set_deployments:\n    rule_set_id = rs_deployment.get(\"name\", \"\").split(\"/\")[-3]\n    display_name = rs_deployment.get(\"displayName\")\n    print(f\"Rule Set: {display_name}, ID: {rule_set_id}\")\n\n# Get curated rule set deployment by ID\nrule_set_deployment = chronicle.get_curated_rule_set_deployment(\"00ad672e-ebb3-0dd1-2a4d-99bd7c5e5f93\")\n\n# Get curated rule set deployment by rule set display name\n# NOTE: This is a linear scan of all curated rules which may be inefficient for large rule sets.\nrule_set_deployment = chronicle.get_curated_rule_set_deployment_by_name(\"Azure - Network\")\n    \n# Update multiple curated rule set deployments\n# Define deployments for rule sets\ndeployments = [\n    {\n        \"category_id\": \"category-uuid\",\n        \"rule_set_id\": \"ruleset-uuid\",\n        \"precision\": \"broad\",\n        \"enabled\": True,\n        \"alerting\": False\n    }\n]\n\nchronicle.batch_update_curated_rule_set_deployments(deployments)\n\n# Update a single curated rule set deployment\nchronicle.update_curated_rule_set_deployment(\n    category_id=\"category-uuid\",\n    rule_set_id=\"ruleset-uuid\",\n    precision=\"broad\",\n    enabled=True,\n    alerting=False\n)\n\n```\n\n### Rule Validation\n\nValidate a YARA-L2 rule before creating or updating it:\n\n```python\n# Example rule\nrule_text = \"\"\"\nrule test_rule {\n    meta:\n        description = \"Test rule for validation\"\n        author = \"Test Author\"\n        severity = \"Low\"\n        yara_version = \"YL2.0\"\n        rule_version = \"1.0\"\n    events:\n        $e.metadata.event_type = \"NETWORK_CONNECTION\"\n    condition:\n        $e\n}\n\"\"\"\n\n# Validate the rule\nresult = chronicle.validate_rule(rule_text)\n\nif result.success:\n    print(\"Rule is valid\")\nelse:\n    print(f\"Rule is invalid: {result.message}\")\n    if result.position:\n        print(f\"Error at line {result.position['startLine']}, column {result.position['startColumn']}\")\n```\n\n### Rule Exclusions\n\nRule 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.\n\n```python\nfrom secops.chronicle.rule_exclusion import RuleExclusionType\nfrom datetime import datetime, timedelta\n\n# Create a new rule exclusion\nexclusion = chronicle.create_rule_exclusion(\n    display_name=\"Exclusion Display Name\",\n    refinement_type=RuleExclusionType.DETECTION_EXCLUSION,\n    query='(domain = \"google.com\")'\n)\n\n# Get exclusion id from name\nexclusion_id = exclusion[\"name\"].split(\"/\")[-1]\n\n# Get details of a specific rule exclusion\nexclusion_details = chronicle.get_rule_exclusion(exclusion_id)\nprint(f\"Exclusion: {exclusion_details.get('display_name')}\")\nprint(f\"Query: {exclusion_details.get('query')}\")\n\n# List all rule exclusions with pagination\nexclusions = chronicle.list_rule_exclusions(page_size=10)\nfor exclusion in exclusions.get(\"findingsRefinements\", []):\n    print(f\"- {exclusion.get('display_name')}: {exclusion.get('name')}\")\n\n# Update an existing exclusion\nupdated = chronicle.patch_rule_exclusion(\n    exclusion_id=exclusion_id,\n    display_name=\"Updated Exclusion\",\n    query='(ip = \"8.8.8.8\")',\n    update_mask=\"display_name,query\"\n)\n\n# Manage deployment settings\nchronicle.update_rule_exclusion_deployment(\n    exclusion_id,\n    enabled=True,\n    archived=False,\n    detection_exclusion_application={\n        \"curatedRules\": [],\n        \"curatedRuleSets\": [],\n        \"rules\": [],\n    }\n)\n\n# Compute rule exclusion Activity for provided time period\nend_time = datetime.utcnow()\nstart_time = end_time - timedelta(days=7)\n\nactivity = chronicle.compute_rule_exclusion_activity(\n    exclusion_id,\n    start_time=start_time,\n    end_time=end_time\n)\n```\n\n### Featured Content Rules\n\nFeatured 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.\n\n```python\n# List all featured content rules\nrules = chronicle.list_featured_content_rules()\nfor rule in rules.get(\"featuredContentRules\", []):\n    rule_id = rule.get(\"name\", \"\").split(\"/\")[-1]\n    content_metadata = rule.get(\"contentMetadata\", {})\n    display_name = content_metadata.get(\"displayName\", \"Unknown\")\n    severity = rule.get(\"severity\", \"UNSPECIFIED\")\n    print(f\"Rule: {display_name} [{rule_id}] - Severity: {severity}\")\n\n# List with pagination\nresult = chronicle.list_featured_content_rules(page_size=10)\nrules = result.get(\"featuredContentRules\", [])\nnext_page_token = result.get(\"nextPageToken\")\n\nif next_page_token:\n    next_page = chronicle.list_featured_content_rules(\n        page_size=10,\n        page_token=next_page_token\n    )\n\n# Filter list\nfiltered_rules = chronicle.list_featured_content_rules(\n    filter_expression=(\n        'category_name:\"Threat Detection\" AND '\n        'rule_precision:\"Precise\"'\n    )\n)\n```\n\n## Data Tables and Reference Lists\n\nChronicle 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.\n\n### Data Tables\n\nData 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.\n\n#### Creating Data Tables\n\n```python\nfrom secops.chronicle.data_table import DataTableColumnType\n\n# Create a data table with different column types\ndata_table = chronicle.create_data_table(\n    name=\"suspicious_ips\",\n    description=\"Known suspicious IP addresses with context\",\n    header={\n        \"ip_address\": DataTableColumnType.CIDR,\n        # Alternately, you can map a column to an entity proto field\n        # See: https://cloud.google.com/chronicle/docs/investigation/data-tables#map_column_names_to_entity_fields_optional\n        # \"ip_address\": \"entity.asset.ip\"\n        \"port\": DataTableColumnType.NUMBER,\n        \"severity\": DataTableColumnType.STRING,\n        \"description\": DataTableColumnType.STRING\n    },\n    # Optional: Set additional column options (valid options: repeatedValues, keyColumns)\n    column_options: {\"ip_address\": {\"repeatedValues\": True}},\n    # Optional: Add initial rows\n    rows=[\n        [\"192.168.1.100\", 3232, \"High\", \"Scanning activity\"],\n        [\"10.0.0.5\", 9000, \"Medium\", \"Suspicious login attempts\"]\n    ]\n)\n\nprint(f\"Created table: {data_table['name']}\")\n```\n\n#### Managing Data Tables\n\n```python\n# List all data tables\ntables = chronicle.list_data_tables()\nfor table in tables:\n    table_id = table[\"name\"].split(\"/\")[-1]\n    print(f\"Table: {table_id}, Created: {table.get('createTime')}\")\n\n# Get a specific data table's details\ntable_details = chronicle.get_data_table(\"suspicious_ips\")\nprint(f\"Column count: {len(table_details.get('columnInfo', []))}\")\n\n# Update a data table's properties\nupdated_table = chronicle.update_data_table(\n    \"suspicious_ips\",\n    description=\"Updated description for suspicious IPs\",\n    row_time_to_live=\"72h\"  # Set TTL for rows to 72 hours\n    update_mask=[\"description\", \"row_time_to_live\"]\n)\nprint(f\"Updated data table: {updated_table['name']}\")\n\n# Add rows to a data table\nchronicle.create_data_table_rows(\n    \"suspicious_ips\",\n    [\n        [\"172.16.0.1\", \"Low\", \"Unusual outbound connection\"],\n        [\"192.168.2.200\", \"Critical\", \"Data exfiltration attempt\"]\n    ]\n)\n\n# List rows in a data table\nrows = chronicle.list_data_table_rows(\"suspicious_ips\")\nfor row in rows:\n    row_id = row[\"name\"].split(\"/\")[-1]\n    values = row.get(\"values\", [])\n    print(f\"Row {row_id}: {values}\")\n\n# Delete specific rows by ID\nrow_ids = [rows[0][\"name\"].split(\"/\")[-1], rows[1][\"name\"].split(\"/\")[-1]]\nchronicle.delete_data_table_rows(\"suspicious_ips\", row_ids)\n\n# Replace all rows in a data table with new rows\nchronicle.replace_data_table_rows(\n    name=\"suspicious_ips\", # Data table Name\n    rows=[\n        [\"192.168.100.1\", \"Critical\", \"Active scanning\"],\n        [\"10.1.1.5\", \"High\", \"Brute force attempts\"],\n        [\"172.16.5.10\", \"Medium\", \"Suspicious traffic\"]\n    ]\n)\n\n# Bulk update rows in a data table\nrow_updates = [\n    {\n        \"name\": \"projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row123\",  # Full resource name\n        \"values\": [\"192.168.100.1\", \"Critical\", \"Updated description\"]\n    },\n    {\n        \"name\": \"projects/my-project/locations/us/instances/my-instance/dataTables/suspicious_ips/dataTableRows/row456\",  # Full resource name\n        \"values\": [\"10.1.1.5\", \"High\", \"Updated brute force info\"],\n        \"update_mask\": \"values\"  # Optional: only update values field\n    }\n]\n\n# Execute bulk update\nchronicle.update_data_table_rows(\n    name=\"suspicious_ips\",\n    row_updates=row_updates\n)\n\n# Delete a data table\nchronicle.delete_data_table(\"suspicious_ips\", force=True)  # force=True deletes even if it has rows\n```\n\n### Reference Lists\n\nReference 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.\n\n#### Creating Reference Lists\n\n```python\nfrom secops.chronicle.reference_list import ReferenceListSyntaxType, ReferenceListView\n\n# Create a reference list with string entries\nstring_list = chronicle.create_reference_list(\n    name=\"admin_accounts\",\n    description=\"Administrative user accounts\",\n    entries=[\"admin\", \"administrator\", \"root\", \"system\"],\n    syntax_type=ReferenceListSyntaxType.STRING\n)\n\nprint(f\"Created reference list: {string_list['name']}\")\n\n# Create a reference list with CIDR entries\ncidr_list = chronicle.create_reference_list(\n    name=\"trusted_networks\",\n    description=\"Internal network ranges\",\n    entries=[\"10.0.0.0/8\", \"192.168.0.0/16\", \"172.16.0.0/12\"],\n    syntax_type=ReferenceListSyntaxType.CIDR\n)\n\n# Create a reference list with regex patterns\nregex_list = chronicle.create_reference_list(\n    name=\"email_patterns\",\n    description=\"Email patterns to watch for\",\n    entries=[\".*@suspicious\\\\.com\", \"malicious_.*@.*\\\\.org\"],\n    syntax_type=ReferenceListSyntaxType.REGEX\n)\n```\n\n#### Managing Reference Lists\n\n```python\n# List all reference lists (basic view without entries)\nlists = chronicle.list_reference_lists(view=ReferenceListView.BASIC)\nfor ref_list in lists:\n    list_id = ref_list[\"name\"].split(\"/\")[-1]\n    print(f\"List: {list_id}, Description: {ref_list.get('description')}\")\n\n# Get a specific reference list including all entries\nadmin_list = chronicle.get_reference_list(\"admin_accounts\", view=ReferenceListView.FULL)\nentries = [entry.get(\"value\") for entry in admin_list.get(\"entries\", [])]\nprint(f\"Admin accounts: {entries}\")\n\n# Update reference list entries\nchronicle.update_reference_list(\n    name=\"admin_accounts\",\n    entries=[\"admin\", \"administrator\", \"root\", \"system\", \"superuser\"]\n)\n\n# Update reference list description\nchronicle.update_reference_list(\n    name=\"admin_accounts\",\n    description=\"Updated administrative user accounts list\"\n)\n\n```\n\n### Using in YARA-L Rules\n\nBoth Data Tables and Reference Lists can be referenced in YARA-L detection rules.\n\n#### Using Data Tables in Rules\n\n```\nrule detect_with_data_table {\n    meta:\n        description = \"Detect connections to suspicious IPs\"\n        author = \"SecOps SDK Example\"\n        severity = \"Medium\"\n        yara_version = \"YL2.0\"\n    events:\n        $e.metadata.event_type = \"NETWORK_CONNECTION\"\n        $e.target.ip != \"\"\n        $lookup in data_table.suspicious_ips\n        $lookup.ip_address = $e.target.ip\n        $severity = $lookup.severity\n        \n    condition:\n        $e and $lookup and $severity = \"High\"\n}\n```\n\n#### Using Reference Lists in Rules\n\n```\nrule detect_with_reference_list {\n    meta:\n        description = \"Detect admin account usage from untrusted networks\"\n        author = \"SecOps SDK Example\" \n        severity = \"High\"\n        yara_version = \"YL2.0\"\n    events:\n        $login.metadata.event_type = \"USER_LOGIN\"\n        $login.principal.user.userid in reference_list.admin_accounts\n        not $login.principal.ip in reference_list.trusted_networks\n        \n    condition:\n        $login\n}\n```\n\n## Gemini AI\n\nYou can use Chronicle's Gemini AI to get security insights, generate detection rules, explain security concepts, and more:\n\n\u003e **Note:** Only enterprise tier users have access to Advanced Gemini features. Users must opt-in to use Gemini in Chronicle before accessing this functionality. \nThe 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, \nyou'll see an error message that includes \"users must opt-in before using Gemini.\"\n\n```python\n# Query Gemini with a security question\nresponse = chronicle.gemini(\"What is Windows event ID 4625?\")\n\n# Get text content (combines TEXT blocks and stripped HTML content)\ntext_explanation = response.get_text_content()\nprint(\"Explanation:\", text_explanation)\n\n# Work with different content blocks\nfor block in response.blocks:\n    print(f\"Block type: {block.block_type}\")\n    if block.block_type == \"TEXT\":\n        print(\"Text content:\", block.content)\n    elif block.block_type == \"CODE\":\n        print(f\"Code ({block.title}):\", block.content)\n    elif block.block_type == \"HTML\":\n        print(\"HTML content (with tags):\", block.content)\n\n# Get all code blocks\ncode_blocks = response.get_code_blocks()\nfor code_block in code_blocks:\n    print(f\"Code block ({code_block.title}):\", code_block.content)\n\n# Get all HTML blocks (with HTML tags preserved)\nhtml_blocks = response.get_html_blocks()\nfor html_block in html_blocks:\n    print(f\"HTML block (with tags):\", html_block.content)\n\n# Check for references\nif response.references:\n    print(f\"Found {len(response.references)} references\")\n\n# Check for suggested actions\nfor action in response.suggested_actions:\n    print(f\"Suggested action: {action.display_text} ({action.action_type})\")\n    if action.navigation:\n        print(f\"Action URI: {action.navigation.target_uri}\")\n```\n\n### Response Content Methods\n\nThe `GeminiResponse` class provides several methods to work with response content:\n\n- `get_text_content()`: Returns a combined string of all TEXT blocks plus the text content from HTML blocks with HTML tags removed\n- `get_code_blocks()`: Returns a list of blocks with `block_type == \"CODE\"`\n- `get_html_blocks()`: Returns a list of blocks with `block_type == \"HTML\"` (HTML tags preserved)\n- `get_raw_response()`: Returns the complete, unprocessed API response as a dictionary\n\nThese methods help you work with different types of content in a structured way.\n\n### Accessing Raw API Response\n\nFor advanced use cases or debugging, you can access the raw API response:\n\n```python\n# Get the complete raw API response\nresponse = chronicle.gemini(\"What is Windows event ID 4625?\")\nraw_response = response.get_raw_response()\n\n# Now you can access any part of the original JSON structure\nprint(json.dumps(raw_response, indent=2))\n\n# Example of navigating the raw response structure\nif \"responses\" in raw_response:\n    for resp in raw_response[\"responses\"]:\n        if \"blocks\" in resp:\n            print(f\"Found {len(resp['blocks'])} blocks in raw response\")\n```\n\nThis gives you direct access to the original API response format, which can be useful for accessing advanced features or troubleshooting.\n\n### Manual Opt-In\n\nIf your account has sufficient permissions, you can manually opt-in to Gemini before using it:\n\n```python\n# Manually opt-in to Gemini\nopt_success = chronicle.opt_in_to_gemini()\nif opt_success:\n    print(\"Successfully opted in to Gemini\")\nelse:\n    print(\"Unable to opt-in due to permission issues\")\n\n# Then use Gemini as normal\nresponse = chronicle.gemini(\"What is Windows event ID 4625?\")\n```\n\nThis can be useful in environments where you want to explicitly control when the opt-in happens.\n\n### Generate Detection Rules\n\nChronicle Gemini can generate YARA-L rules for detection:\n\n```python\n# Generate a rule to detect potential security issues\nrule_response = chronicle.gemini(\"Write a rule to detect powershell downloading a file called gdp.zip\")\n\n# Extract the generated rule(s)\ncode_blocks = rule_response.get_code_blocks()\nif code_blocks:\n    rule = code_blocks[0].content\n    print(\"Generated rule:\", rule)\n    \n    # Check for rule editor action\n    for action in rule_response.suggested_actions:\n        if action.display_text == \"Open in Rule Editor\" and action.action_type == \"NAVIGATION\":\n            rule_editor_url = action.navigation.target_uri\n            print(\"Rule can be opened in editor:\", rule_editor_url)\n```\n\n### Get Intel Information\n\nGet detailed information about malware, threat actors, files, vulnerabilities:\n\n```python\n# Ask about a CVE\ncve_response = chronicle.gemini(\"tell me about CVE-2021-44228\")\n\n# Get the explanation\ncve_explanation = cve_response.get_text_content()\nprint(\"CVE explanation:\", cve_explanation)\n```\n\n### Maintain Conversation Context\n\nYou can maintain conversation context by reusing the same conversation ID:\n\n```python\n# Start a conversation\ninitial_response = chronicle.gemini(\"What is a DDoS attack?\")\n\n# Get the conversation ID from the response\nconversation_id = initial_response.name.split('/')[-3]  # Extract from format: .../conversations/{id}/messages/{id}\n\n# Ask a follow-up question in the same conversation context\nfollowup_response = chronicle.gemini(\n    \"What are the most common mitigation techniques?\",\n    conversation_id=conversation_id\n)\n\n# Gemini will remember the context of the previous question about DDoS\n```\n\n## Feed Management\n\nFeeds are used to ingest data into Chronicle. The SDK provides methods to manage feeds.\n\n```python\nimport json\n\n# List existing feeds\nfeeds = chronicle.list_feeds()\nprint(f\"Found {len(feeds)} feeds\")\n\n# Create a new feed\nfeed_details = {\n    \"logType\": f\"projects/your-project-id/locations/us/instances/your-chronicle-instance-id/logTypes/WINEVTLOG\",\n    \"feedSourceType\": \"HTTP\",\n    \"httpSettings\": {\n        \"uri\": \"https://example.com/example_feed\",\n        \"sourceType\": \"FILES\",\n    },\n    \"labels\": {\"environment\": \"production\", \"created_by\": \"secops_sdk\"}\n}\n\ncreated_feed = chronicle.create_feed(\n    display_name=\"My New Feed\",\n    details=feed_details\n)\n\n# Get feed ID from name\nfeed_id = created_feed[\"name\"].split(\"/\")[-1]\nprint(f\"Feed created with ID: {feed_id}\")\n\n# Get feed details\nfeed_details = chronicle.get_feed(feed_id)\nprint(f\"Feed state: {feed_details.get('state')}\")\n\n# Update feed\nupdated_details = {\n    \"httpSettings\": {\n        \"uri\": \"https://example.com/updated_feed_url\",\n        \"sourceType\": \"FILES\"\n    },\n    \"labels\": {\"environment\": \"production\", \"updated\": \"true\"}\n}\n\nupdated_feed = chronicle.update_feed(\n    feed_id=feed_id,\n    display_name=\"Updated Feed Name\",\n    details=updated_details\n)\n\n# Disable feed\ndisabled_feed = chronicle.disable_feed(feed_id)\nprint(f\"Feed disabled. State: {disabled_feed.get('state')}\")\n\n# Enable feed\nenabled_feed = chronicle.enable_feed(feed_id)\nprint(f\"Feed enabled. State: {enabled_feed.get('state')}\")\n\n# Generate secret for feed (for supported feed types)\ntry:\n    secret_result = chronicle.generate_secret(feed_id)\n    print(f\"Generated secret: {secret_result.get('secret')}\")\nexcept Exception as e:\n    print(f\"Error generating secret for feed: {e}\")\n\n# Delete feed\nchronicle.delete_feed(feed_id)\nprint(\"Feed deleted successfully\")\n```\n\nThe 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.\n\n\u003e **Note**: Secret generation is only available for certain feed types that require authentication.\n\n## Chronicle Dashboard\n\nThe Chronicle Dashboard API provides methods to manage native dashboards and dashboard charts in Chronicle.\n\n### Create Dashboard\n```python\n# Create a dashboard\ndashboard = chronicle.create_dashboard(\n    display_name=\"Security Overview\",\n    description=\"Dashboard showing security metrics\",\n    access_type=\"PRIVATE\"  # \"PRIVATE\" or \"PUBLIC\"\n)\ndashboard_id = dashboard[\"name\"].split(\"/\")[-1]\nprint(f\"Created dashboard with ID: {dashboard_id}\")\n```\n\n### Get Specific Dashboard Details\n```python\n# Get a specific dashboard\ndashboard = chronicle.get_dashboard(\n    dashboard_id=\"dashboard-id-here\",\n    view=\"FULL\"  # Optional: \"BASIC\" or \"FULL\" \n)\nprint(f\"Dashboard Details: {dashboard}\")\n```\n\n### List Dashboards\n```python\ndashboards = chronicle.list_dashboards()\nfor dashboard in dashboards.get(\"nativeDashboards\", []):\n    print(f\"- {dashboard.get('displayName')}\")\n\n# List dashboards with pagination(first page)\ndashboards = chronicle.list_dashboards(page_size=10)\nfor dashboard in dashboards.get(\"nativeDashboards\", []):\n    print(f\"- {dashboard.get('displayName')}\")\n\n# Get next page if available\nif \"nextPageToken\" in dashboards:\n    next_page = chronicle.list_dashboards(\n        page_size=10,\n        page_token=dashboards[\"nextPageToken\"]\n    )\n```\n\n### Update existing dashboard details\n```python\nfilters = [\n    {\n        \"id\": \"GlobalTimeFilter\",\n        \"dataSource\": \"GLOBAL\",\n        \"filterOperatorAndFieldValues\": [\n            {\"filterOperator\": \"PAST\", \"fieldValues\": [\"7\", \"DAY\"]}\n        ],\n        \"displayName\": \"Global Time Filter\",\n        \"chartIds\": [],\n        \"isStandardTimeRangeFilter\": True,\n        \"isStandardTimeRangeFilterEnabled\": True,\n    }\n]\ncharts = [\n    {\n        \"dashboardChart\": \"projects/\u003cproject_id\u003e/locations/\u003clocation\u003e/instances/\u003cinstacne_id\u003e/dashboardCharts/\u003cchart_id\u003e\",\n        \"chartLayout\": {\"startX\": 0, \"spanX\": 48, \"startY\": 0, \"spanY\": 26},\n        \"filtersIds\": [\"GlobalTimeFilter\"],\n    }\n]\n# Update a dashboard\nupdated_dashboard = chronicle.update_dashboard(\n    dashboard_id=\"dashboard-id-here\",\n    display_name=\"Updated Security Dashboard\",\n    filters=filters,\n    charts=charts,\n)\nprint(f\"Updated dashboard: {updated_dashboard['displayName']}\")\n```\n\n### Duplicate existing dashboard\n```python\n# Duplicate a dashboard\nduplicate = chronicle.duplicate_dashboard(\n    dashboard_id=\"dashboard-id-here\",\n    display_name=\"Copy of Security Dashboard\",\n    access_type=\"PRIVATE\"\n)\nduplicate_id = duplicate[\"name\"].split(\"/\")[-1]\nprint(f\"Created duplicate dashboard with ID: {duplicate_id}\")\n```\n\n#### Import Dashboard\nImports a dashboard from a JSON file.\n\n```python\nimport os\nfrom secops.chronicle import client\n\n# Assumes the CHRONICLE_SA_KEY environment variable is set with service account JSON\nchronicle_client = client.Client()\n\n# Path to the dashboard file\ndashboard = {\n    \"dashboard\": {...}\n    \"dashboardCharts\": [...],\n    \"dashboardQueries\": [...]\n}\n\n# Import the dashboard\ntry:\n    new_dashboard = chronicle_client.import_dashboard(dashboard)\n    print(new_dashboard)\nexcept Exception as e:\n    print(f\"An error occurred: {e}\")\n\n```\n\n#### Export Dashboards\n\nExport dashboard to a dictionary.\n\n```python\n# Export a dashboard\ndashboards = chronicle.export_dashboard(dashboard_names=[\"\u003cdashboard_id\u003e\"])\n```\n\n\n### Add Chart to existing dashboard\n```python\n# Define chart configuration\nquery = \"\"\"\nmetadata.event_type = \"NETWORK_DNS\"\nmatch:\n  principal.hostname\noutcome:\n  $dns_query_count = count(metadata.id)\norder:\n  principal.hostname asc\n\"\"\"\n\nchart_layout = {\n    \"startX\": 0, \n    \"spanX\": 12, \n    \"startY\": 0, \n    \"spanY\": 8\n}\n\nchart_datasource = {\n    \"dataSources\": [\"UDM\"]\n}\n\ninterval = {\n    \"relativeTime\": {\n        \"timeUnit\": \"DAY\",\n        \"startTimeVal\": \"1\"\n    }\n}\n\n# Add chart to dashboard\nchart = chronicle.add_chart(\n    dashboard_id=\"dashboard-id-here\",\n    display_name=\"DNS Query Metrics\",\n    query=query,\n    chart_layout=chart_layout,\n    chart_datasource=chart_datasource,\n    interval=interval,\n    tile_type=\"VISUALIZATION\" # Option: \"VISUALIZATION\" or \"BUTTOn\"\n)\n```\n\n### Get Dashboard Chart Details\n```python\n# Get dashboard chart details\ndashboard_chart = chronicle.get_chart(\n    chart_id=\"chart-id-here\"\n)\nprint(f\"Dashboard Chart Details: {dashboard_chart}\")\n```\n\n### Edit Dashboard Chart\n```python\n# Dashboard Query updated details\nupdated_dashboard_query={\n    \"name\": \"project/\u003cprojectNumber\u003e/instance/\u003cinstanceNumber\u003e/dashboardQueries/\u003cquery_id\u003e\",\n    \"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',\n    \"input\": {\"relativeTime\": {\"timeUnit\": \"DAY\", \"startTimeVal\": \"1\"}},\n    \"etag\": \"123456\", # Latest etag from server\n}\n\n# Dashboard Chart updated details\nupdated_dashboard_chart={\n    \"name\": \"project/\u003cprojectNumber\u003e/instance/\u003cinstanceNumber\u003e/dashboardCharts/\u003cchart_id\u003e\",\n    \"display_name\": \"Updated chart display Name\",\n    \"description\": \"Updated chart description\",\n    \"etag\": \"12345466\", # latest etag from server\n    \"visualization\": {},\n    \"chart_datasource\":{\"data_sources\":[]}\n}\n\nupdated_chart = chronicle.edit_chart(\n    dashboard_id=\"dashboard-id-here\",\n    dashboard_chart=updated_dashboard_chart,\n    dashboard_query=updated_dashboard_query\n)\nprint(f\"Updated dashboard chart: {updated_chart}\")\n```\n\n### Remove Chart from existing dashboard\n```python\n# Remove chart from dashboard\nchronicle.remove_chart(\n    dashboard_id=\"dashboard-id-here\",\n    chart_id=\"chart-id-here\"\n)\n```\n\n### Delete existing dashboard\n```python\n# Delete a dashboard\nchronicle.delete_dashboard(dashboard_id=\"dashboard-id-here\")\nprint(\"Dashboard deleted successfully\")\n```\n\n## Dashboard Query\n\nThe Chronicle Dashboard Query API provides methods to execute dashboard queries without creating a dashboard and get details of dashboard query.\n\n### Execute Dashboard Query\n```python\n# Define query and time interval\nquery = \"\"\"\nmetadata.event_type = \"USER_LOGIN\"\nmatch:\n  principal.user.userid\noutcome:\n  $logon_count = count(metadata.id)\norder:\n  $logon_count desc\nlimit: 10\n\"\"\"\n\ninterval = {\n    \"relativeTime\": {\n        \"timeUnit\": \"DAY\",\n        \"startTimeVal\": \"7\"\n    }\n}\n\n# Execute the query\nresults = chronicle.execute_dashboard_query(\n    query=query,\n    interval=interval\n)\n\n# Process results\nfor result in results.get(\"results\", []):\n    print(result)\n```\n\n### Get Dashboard Query details\n```python\n# Get dashboard query details\ndashboard_query = chronicle.get_dashboard_query(\n    query_id=\"query-id-here\"\n)\nprint(f\"Dashboard Query Details: {dashboard_query}\")\n```\n\n## Error Handling\n\nThe SDK defines several custom exceptions:\n\n```python\nfrom secops.exceptions import SecOpsError, AuthenticationError, APIError\n\ntry:\n    results = chronicle.search_udm(...)\nexcept AuthenticationError as e:\n    print(f\"Authentication failed: {e}\")\nexcept APIError as e:\n    print(f\"API request failed: {e}\")\nexcept SecOpsError as e:\n    print(f\"General error: {e}\")\n```\n\n## Value Type Detection\n\nThe SDK automatically detects the most common entity types when using the `summarize_entity` function:\n- IP addresses (IPv4 and IPv6)\n- MD5/SHA1/SHA256 hashes\n- Domain names\n- Email addresses\n- MAC addresses\n- Hostnames\n\nThis detection happens internally within `summarize_entity`, simplifying its usage. You only need to provide the `value` argument.\n\n```python\n# The SDK automatically determines how to query for these values\nip_summary = chronicle.summarize_entity(value=\"192.168.1.100\", ...)\ndomain_summary = chronicle.summarize_entity(value=\"example.com\", ...)\nhash_summary = chronicle.summarize_entity(value=\"e17dd4eef8b4978673791ef4672f4f6a\", ...)\n```\n\nYou can optionally provide a `preferred_entity_type` hint to `summarize_entity` if the automatic detection might be ambiguous (e.g., a string could be a username or a hostname).\n\n## License\n\nThis project is licensed under the Apache License 2.0 - [see the LICENSE file for details.](https://github.com/google/secops-wrapper/blob/main/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoogle%2Fsecops-wrapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgoogle%2Fsecops-wrapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoogle%2Fsecops-wrapper/lists"}