secops 0.35.0
Python SDK for wrapping the Google SecOps API for common use cases
Requires Python: >=3.10
Keywords: chronicle, google, secops, security

# Google SecOps SDK for Python

[![PyPI version](https://img.shields.io/pypi/v/secops.svg)](https://pypi.org/project/secops/)


A Python SDK for interacting with Google Security Operations products, currently supporting Chronicle/SecOps SIEM.
This wraps the API for common use cases, including UDM searches, entity lookups, IoCs, alert management, case management, and detection rule management.

## Prerequisites

Follow these steps to ensure your environment is properly configured:

1. **Configure a Google Cloud Project for Google SecOps**
   - Your Google Cloud project must be linked to your Google SecOps instance.
   - Chronicle API needs to be enabled in your Google Cloud project.
   - The project used for authentication must be the same project that was set up during your SecOps onboarding.
   - For detailed instructions, see [Configure a Google Cloud project for Google SecOps](https://cloud.google.com/chronicle/docs/onboard/configure-cloud-project).

2. **Set up IAM Permissions**
   - The service account or user credentials you use must have appropriate permissions
   - The recommended predefined role is **Chronicle API Admin** (`roles/chronicle.admin`)
   - For more granular access control, you can create custom roles with specific permissions
   - See [Access control using IAM](https://cloud.google.com/chronicle/docs/onboard/configure-feature-access) for detailed permission information

3. **Required Information**
   - Your Chronicle instance ID (customer_id)
   - Your Google Cloud project number (project_id)
   - Your preferred region (e.g., "us", "europe", "asia")


> **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.

## Installation

```bash
pip install secops
```

## Command Line Interface

The SDK also provides a comprehensive command-line interface (CLI) that makes it easy to interact with Google Security Operations products from your terminal:

```bash
# Save your credentials
secops config set --customer-id "your-instance-id" --project-id "your-project-id" --region "us"

# Now use commands without specifying credentials each time
secops search --query "metadata.event_type = \"NETWORK_CONNECTION\""
```

For detailed CLI documentation and examples, see the [CLI Documentation](https://github.com/google/secops-wrapper/blob/main/CLI.md).


## Authentication

The SDK supports two main authentication methods:

### 1. Application Default Credentials (ADC)

The 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.

There are several ways to use ADC:

#### a. Using `gcloud` CLI (Recommended for Local Development)

```bash
# Login and set up application-default credentials
gcloud auth application-default login
```

Then in your code:
```python
from secops import SecOpsClient

# Initialize with default credentials - no explicit configuration needed
client = SecOpsClient()
```

#### b. Using Environment Variable

Set the environment variable pointing to your service account key:
```bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
```

Then in your code:
```python
from secops import SecOpsClient

# Initialize with default credentials - will automatically use the credentials file
client = SecOpsClient()
```

#### c. Google Cloud Environment (Automatic)

When running on Google Cloud services (Compute Engine, Cloud Functions, Cloud Run, etc.), ADC works automatically without any configuration:

```python
from secops import SecOpsClient

# Initialize with default credentials - will automatically use the service account 
# assigned to your Google Cloud resource
client = SecOpsClient()
```

ADC will automatically try these authentication methods in order:
1. Environment variable `GOOGLE_APPLICATION_CREDENTIALS`
2. Google Cloud SDK credentials (set by `gcloud auth application-default login`)
3. Google Cloud-provided service account credentials
4. Local service account impersonation credentials

### 2. Service Account Authentication

For more explicit control, you can authenticate using a service account that is created in the Google Cloud project associated with Google SecOps.

**Important Note on Permissions:**
* 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.). 

Once the service account is properly permissioned, you can authenticate using it in two ways: 

#### a. Using a Service Account JSON File

```python
from secops import SecOpsClient

# Initialize with service account JSON file
client = SecOpsClient(service_account_path="/path/to/service-account.json")
```

#### b. Using Service Account Info Dictionary

If you prefer to manage credentials programmatically without a file, you can create a dictionary containing the service account key's contents.

```python
from secops import SecOpsClient

# Service account details as a dictionary
service_account_info = {
    "type": "service_account",
    "project_id": "your-project-id",
    "private_key_id": "key-id",
    "private_key": "-----BEGIN PRIVATE KEY-----\n...",
    "client_email": "service-account@project.iam.gserviceaccount.com",
    "client_id": "client-id",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..."
}

# Initialize with service account info
client = SecOpsClient(service_account_info=service_account_info)
```

### Impersonate Service Account

Both [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:

```python
from secops import SecOpsClient

# Initialize with default credentials and impersonate service account
client = SecOpsClient(impersonate_service_account="secops@test-project.iam.gserviceaccount.com")
```

### Retry Configuration

The 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:

```python
from secops import SecOpsClient
from secops.auth import RetryConfig

# Define retry configurations
retry_config = RetryConfig(
    total=3,                     # Maximum number of retries (default: 5)
    retry_status_codes=[429, 500, 502, 503, 504],  # HTTP status codes to retry
    allowed_methods=["GET", "DELETE"],  # HTTP methods to retry
    backoff_factor=0.5           # Backoff factor (default: 0.3)
)

# Initialize with custom retry config
client = SecOpsClient(retry_config=retry_config)


# Disable retry completely by marking retry config as False
client = SecOpsClient(retry_config=False)
```

## Using the Chronicle API

### Initializing the Chronicle Client

After creating a SecOpsClient, you need to initialize the Chronicle-specific client:

```python
# Initialize Chronicle client
chronicle = client.chronicle(
    customer_id="your-chronicle-instance-id",  # Your Chronicle instance ID
    project_id="your-project-id",             # Your GCP project ID
    region="us"                               # Chronicle API region 
)
```
[See available regions](https://github.com/google/secops-wrapper/blob/main/regions.md)

#### API Version Control

The SDK supports flexible API version selection:

- **Default Version**: Set `default_api_version` during client initialization (default is `v1alpha`)
- **Per-Method Override**: Many methods accept an `api_version` parameter to override the default for specific calls

**Supported API versions:**
- `v1` - Stable production API
- `v1beta` - Beta API with newer features
- `v1alpha` - Alpha API with experimental features

**Example with per-method version override:**
```python
from secops.chronicle.models import APIVersion

# Client defaults to v1alpha
chronicle = client.chronicle(
    customer_id="your-chronicle-instance-id",
    project_id="your-project-id",
    region="us",
    default_api_version="v1alpha"
)

# Use v1 for a specific rule operation
rule = chronicle.get_rule(
    rule_id="ru_12345678-1234-1234-1234-123456789abc",
    api_version=APIVersion.V1  # Override to use v1 for this call
)
```

### Log Ingestion

Ingest raw logs directly into Chronicle:

```python
from datetime import datetime, timezone
import json

# Create a sample log (this is an OKTA log)
current_time = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
okta_log = {
    "actor": {
        "alternateId": "mark.taylor@cymbal-investments.org",
        "displayName": "Mark Taylor",
        "id": "00u4j7xcb5N6zfiRP5d8",
        "type": "User"
    },
    "client": {
        "userAgent": {
            "rawUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
            "os": "Windows 10",
            "browser": "CHROME"
        },
        "ipAddress": "96.6.127.53",
        "geographicalContext": {
            "city": "New York",
            "state": "New York",
            "country": "United States",
            "postalCode": "10118",
            "geolocation": {"lat": 40.7123, "lon": -74.0068}
        }
    },
    "displayMessage": "Max sign in attempts exceeded",
    "eventType": "user.account.lock",
    "outcome": {"result": "FAILURE", "reason": "LOCKED_OUT"},
    "published": "2025-06-19T21:51:50.116Z",
    "securityContext": {
        "asNumber": 20940,
        "asOrg": "akamai technologies inc.",
        "isp": "akamai international b.v.",
        "domain": "akamaitechnologies.com",
        "isProxy": false
    },
    "severity": "DEBUG",
    "legacyEventType": "core.user_auth.account_locked",
    "uuid": "5b90a94a-d7ba-11ea-834a-85c24a1b2121",
    "version": "0"
    # ... additional OKTA log fields may be included
}

# Ingest a single log using the default forwarder
result = chronicle.ingest_log(
    log_type="OKTA",  # Chronicle log type
    log_message=json.dumps(okta_log)  # JSON string of the log
)

print(f"Operation: {result.get('operation')}")

# Batch ingestion: Ingest multiple logs in a single request
batch_logs = [
    json.dumps({"actor": {"displayName": "User 1"}, "eventType": "user.session.start"}),
    json.dumps({"actor": {"displayName": "User 2"}, "eventType": "user.session.start"}),
    json.dumps({"actor": {"displayName": "User 3"}, "eventType": "user.session.start"})
]

# Ingest multiple logs in a single API call
batch_result = chronicle.ingest_log(
    log_type="OKTA",
    log_message=batch_logs  # List of log message strings
)

print(f"Batch operation: {batch_result.get('operation')}")

# Add custom labels to your logs
labeled_result = chronicle.ingest_log(
    log_type="OKTA",
    log_message=json.dumps(okta_log),
    labels={"environment": "production", "app": "web-portal", "team": "security"}
)
```
The SDK also supports non-JSON log formats. Here's an example with XML for Windows Event logs:

```python
# Create a Windows Event XML log
xml_content = """<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>
  <System>
    <Provider Name='Microsoft-Windows-Security-Auditing' Guid='{54849625-5478-4994-A5BA-3E3B0328C30D}'/>
    <EventID>4624</EventID>
    <Version>1</Version>
    <Level>0</Level>
    <Task>12544</Task>
    <Opcode>0</Opcode>
    <Keywords>0x8020000000000000</Keywords>
    <TimeCreated SystemTime='2024-05-10T14:30:00Z'/>
    <EventRecordID>202117513</EventRecordID>
    <Correlation/>
    <Execution ProcessID='656' ThreadID='700'/>
    <Channel>Security</Channel>
    <Computer>WIN-SERVER.xyz.net</Computer>
    <Security/>
  </System>
  <EventData>
    <Data Name='SubjectUserSid'>S-1-0-0</Data>
    <Data Name='SubjectUserName'>-</Data>
    <Data Name='TargetUserName'>svcUser</Data>
    <Data Name='WorkstationName'>CLIENT-PC</Data>
    <Data Name='LogonType'>3</Data>
  </EventData>
</Event>"""

# Ingest the XML log - no json.dumps() needed for XML
result = chronicle.ingest_log(
    log_type="WINEVTLOG_XML",  # Windows Event Log XML format
    log_message=xml_content    # Raw XML content
)

print(f"Operation: {result.get('operation')}")
```
The SDK supports all log types available in Chronicle. You can:

1. View available log types:
```python
# Get all available log types
log_types = chronicle.get_all_log_types()
for lt in log_types[:5]:  # Show first 5
    print(f"{lt.id}: {lt.description}")


# Fetch only first 50 log types (single page)
log_types_page = chronicle.get_all_log_types(page_size=50)

# Fetch specific page using token
log_types_next = chronicle.get_all_log_types(
    page_size=50, 
    page_token="next_page_token"
)
```

2. Search for specific log types:
```python
# Search for log types related to firewalls
firewall_types = chronicle.search_log_types("firewall")
for lt in firewall_types:
    print(f"{lt.id}: {lt.description}")
```

3. Validate log types:
```python
# Check if a log type is valid
if chronicle.is_valid_log_type("OKTA"):
    print("Valid log type")
else:
    print("Invalid log type")
```

4. Classify logs to predict log type:
```python
# Classify a raw log to determine its type
okta_log = '{"eventType": "user.session.start", "actor": {"alternateId": "user@example.com"}}'
predictions = chronicle.classify_logs(log_data=okta_log)

# Display predictions sorted by confidence score
for prediction in predictions:
    print(f"Log Type: {prediction['logType']}, Score: {prediction['score']}")
```

> **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.

5. Use custom forwarders:
```python
# Create or get a custom forwarder
forwarder = chronicle.get_or_create_forwarder(display_name="MyCustomForwarder")
forwarder_id = forwarder["name"].split("/")[-1]

# Use the custom forwarder for log ingestion
result = chronicle.ingest_log(
    log_type="WINDOWS",
    log_message=json.dumps(windows_log),
    forwarder_id=forwarder_id
)
```

### Forwarder Management

Chronicle log forwarders are essential for handling log ingestion with specific configurations. The SDK provides comprehensive methods for creating and managing forwarders:

#### Create a new forwarder

```python
# Create a basic forwarder with just a display name
forwarder = chronicle.create_forwarder(display_name="MyAppForwarder")

# Create a forwarder with optional configuration
forwarder = chronicle.create_forwarder(
    display_name="ProductionForwarder",
    metadata={"labels": {"env": "prod"}},
    upload_compression=True,  # Enable upload compression for efficiency
    enable_server=False  # Server functionality disabled,
    http_settings={
        "port":8080,
        "host":"192.168.0.100",
        "routeSettings":{
            "availableStatusCode": 200,
            "readyStatusCode": 200,
            "unreadyStatusCode": 500
        }
    }
)

print(f"Created forwarder with ID: {forwarder['name'].split('/')[-1]}")
```

#### List all forwarders

Retrieve all forwarders in your Chronicle environment with pagination support:

```python
# Get the default page size (50)
forwarders = chronicle.list_forwarders()

# Get forwarders with custom page size
forwarders = chronicle.list_forwarders(page_size=100)

# Process the forwarders
for forwarder in forwarders.get("forwarders", []):
    forwarder_id = forwarder.get("name", "").split("/")[-1]
    display_name = forwarder.get("displayName", "")
    create_time = forwarder.get("createTime", "")
    print(f"Forwarder ID: {forwarder_id}, Name: {display_name}, Created: {create_time}")
```

#### Get forwarder details

Retrieve details about a specific forwarder using its ID:

```python
# Get a specific forwarder using its ID
forwarder_id = "1234567890"
forwarder = chronicle.get_forwarder(forwarder_id=forwarder_id)

# Access forwarder properties
display_name = forwarder.get("displayName", "")
metadata = forwarder.get("metadata", {})
server_enabled = forwarder.get("enableServer", False)

print(f"Forwarder {display_name} details:")
print(f"  Metadata: {metadata}")
print(f"  Server enabled: {server_enabled}")
```

#### Get or create a forwarder

Retrieve an existing forwarder by display name or create a new one if it doesn't exist:

```python
# Try to find a forwarder with the specified display name
# If not found, create a new one with that display name
forwarder = chronicle.get_or_create_forwarder(display_name="ApplicationLogForwarder")

# Extract the forwarder ID for use in log ingestion
forwarder_id = forwarder["name"].split("/")[-1]
```

#### Update a forwarder

Update an existing forwarder's configuration with specific properties:

```python
# Update a forwarder with new properties
forwarder = chronicle.update_forwarder(
    forwarder_id="1234567890",
    display_name="UpdatedForwarderName",
    metadata={"labels": {"env": "prod"}},
    upload_compression=True
)

# Update specific fields using update mask
forwarder = chronicle.update_forwarder(
    forwarder_id="1234567890",
    display_name="ProdForwarder",
    update_mask=["display_name"]
)

print(f"Updated forwarder: {forwarder['name']}")
```

#### Delete a forwarder

Delete an existing forwarder by its ID:

```python
# Delete a forwarder by ID
chronicle.delete_forwarder(forwarder_id="1234567890")

print("Forwarder deleted successfully")
```

### Log Processing Pipelines

Chronicle 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.

The SDK provides comprehensive methods for managing pipelines, associating streams, testing configurations, and fetching sample logs.

#### List pipelines

Retrieve all log processing pipelines in your Chronicle instance:

```python
# Get all pipelines
result = chronicle.list_log_processing_pipelines()
pipelines = result.get("logProcessingPipelines", [])

for pipeline in pipelines:
    pipeline_id = pipeline["name"].split("/")[-1]
    print(f"Pipeline: {pipeline['displayName']} (ID: {pipeline_id})")

# List with pagination
result = chronicle.list_log_processing_pipelines(
    page_size=50,
    page_token="next_page_token"
)
```

#### Get pipeline details

Retrieve details about a specific pipeline:

```python
# Get pipeline by ID
pipeline_id = "1234567890"
pipeline = chronicle.get_log_processing_pipeline(pipeline_id)

print(f"Name: {pipeline['displayName']}")
print(f"Description: {pipeline.get('description', 'N/A')}")
print(f"Processors: {len(pipeline.get('processors', []))}")
```

#### Create a pipeline

Create a new log processing pipeline with processors:

```python
# Define pipeline configuration
pipeline_config = {
    "displayName": "My Custom Pipeline",
    "description": "Filters and transforms application logs",
    "processors": [
        {
            "filterProcessor": {
                "include": {
                    "logMatchType": "REGEXP",
                    "logBodies": [".*error.*", ".*warning.*"],
                },
                "errorMode": "IGNORE",
            }
        }
    ],
    "customMetadata": [
        {"key": "environment", "value": "production"},
        {"key": "team", "value": "security"}
    ]
}

# Create the pipeline (server generates ID)
created_pipeline = chronicle.create_log_processing_pipeline(
    pipeline=pipeline_config
)

pipeline_id = created_pipeline["name"].split("/")[-1]
print(f"Created pipeline with ID: {pipeline_id}")
```

#### Update a pipeline

Update an existing pipeline's configuration:

```python
# Get the existing pipeline first
pipeline = chronicle.get_log_processing_pipeline(pipeline_id)

# Update specific fields
updated_config = {
    "name": pipeline["name"], 
    "description": "Updated description",
    "processors": pipeline["processors"]
}

# Patch with update mask
updated_pipeline = chronicle.update_log_processing_pipeline(
    pipeline_id=pipeline_id,
    pipeline=updated_config,
    update_mask="description"
)

print(f"Updated: {updated_pipeline['displayName']}")
```

#### Delete a pipeline

Delete an existing pipeline:

```python
# Delete by ID
chronicle.delete_log_processing_pipeline(pipeline_id)
print("Pipeline deleted successfully")

# Delete with etag for concurrency control
chronicle.delete_log_processing_pipeline(
    pipeline_id=pipeline_id,
    etag="etag_value"
)
```

#### Associate streams with a pipeline

Associate log streams (by log type or feed) with a pipeline:

```python
# Associate by log type
streams = [
    {"logType": "WINEVTLOG"},
    {"logType": "LINUX"}
]

chronicle.associate_streams(
    pipeline_id=pipeline_id,
    streams=streams
)
print("Streams associated successfully")

# Associate by feed ID
feed_streams = [
    {"feed": "feed-uuid-1"},
    {"feed": "feed-uuid-2"}
]

chronicle.associate_streams(
    pipeline_id=pipeline_id,
    streams=feed_streams
)
```

#### Dissociate streams from a pipeline

Remove stream associations from a pipeline:

```python
# Dissociate streams
streams = [{"logType": "WINEVTLOG"}]

chronicle.dissociate_streams(
    pipeline_id=pipeline_id,
    streams=streams
)
print("Streams dissociated successfully")
```

#### Fetch associated pipeline

Find which pipeline is associated with a specific stream:

```python
# Find pipeline for a log type
stream_query = {"logType": "WINEVTLOG"}
associated = chronicle.fetch_associated_pipeline(stream=stream_query)

if associated:
    print(f"Associated pipeline: {associated['name']}")
else:
    print("No pipeline associated with this stream")

# Find pipeline for a feed
feed_query = {"feed": "feed-uuid"}
associated = chronicle.fetch_associated_pipeline(stream=feed_query)
```

#### Fetch sample logs

Retrieve sample logs for specific streams:

```python
# Fetch sample logs for log types
streams = [
    {"logType": "WINEVTLOG"},
    {"logType": "LINUX"}
]

result = chronicle.fetch_sample_logs_by_streams(
    streams=streams,
    sample_logs_count=10
)

for log in result.get("logs", []):
    print(f"Log: {log}")
```

#### Test a pipeline

Test a pipeline configuration against sample logs before deployment:

```python
import base64
from datetime import datetime, timezone

# Define pipeline to test
pipeline_config = {
    "displayName": "Test Pipeline",
    "processors": [
        {
            "filterProcessor": {
                "include": {
                    "logMatchType": "REGEXP",
                    "logBodies": [".*"],
                },
                "errorMode": "IGNORE",
            }
        }
    ]
}

# Create test logs with base64-encoded data
current_time = datetime.now(timezone.utc).isoformat()
log_data = base64.b64encode(b"Sample log entry").decode("utf-8")

input_logs = [
    {
        "data": log_data,
        "logEntryTime": current_time,
        "collectionTime": current_time,
    }
]

# Test the pipeline
result = chronicle.test_pipeline(
    pipeline=pipeline_config,
    input_logs=input_logs
)

print(f"Processed {len(result.get('logs', []))} logs")
for processed_log in result.get("logs", []):
    print(f"Result: {processed_log}")
```

5. Use custom timestamps:
```python
from datetime import datetime, timedelta, timezone

# Define custom timestamps
log_entry_time = datetime.now(timezone.utc) - timedelta(hours=1)
collection_time = datetime.now(timezone.utc)

result = chronicle.ingest_log(
    log_type="OKTA",
    log_message=json.dumps(okta_log),
    log_entry_time=log_entry_time,  # When the log was generated
    collection_time=collection_time  # When the log was collected
)
```

Ingest UDM events directly into Chronicle:

```python
import uuid
from datetime import datetime, timezone

# Generate a unique ID
event_id = str(uuid.uuid4())

# Get current time in ISO 8601 format
current_time = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

# Create a UDM event for a network connection
network_event = {
    "metadata": {
        "id": event_id,
        "event_timestamp": current_time,
        "event_type": "NETWORK_CONNECTION",
        "product_name": "My Security Product", 
        "vendor_name": "My Company"
    },
    "principal": {
        "hostname": "workstation-1",
        "ip": "192.168.1.100",
        "port": 12345
    },
    "target": {
        "ip": "203.0.113.10",
        "port": 443
    },
    "network": {
        "application_protocol": "HTTPS",
        "direction": "OUTBOUND"
    }
}

# Ingest a single UDM event
result = chronicle.ingest_udm(udm_events=network_event)
print(f"Ingested event with ID: {event_id}")

# Create a second event
process_event = {
    "metadata": {
        # No ID - one will be auto-generated
        "event_timestamp": current_time,
        "event_type": "PROCESS_LAUNCH",
        "product_name": "My Security Product", 
        "vendor_name": "My Company"
    },
    "principal": {
        "hostname": "workstation-1",
        "process": {
            "command_line": "ping 8.8.8.8",
            "pid": 1234
        },
        "user": {
            "userid": "user123"
        }
    }
}

# Ingest multiple UDM events in a single call
result = chronicle.ingest_udm(udm_events=[network_event, process_event])
print("Multiple events ingested successfully")
```

Import entities into Chronicle:

```python
# Create a sample entity
entity = {
    "metadata": {
        "collected_timestamp": "2025-01-01T00:00:00Z",
        "vendor_name": "TestVendor",
        "product_name": "TestProduct",
        "entity_type": "USER",
    },
    "entity": {
        "user": {
            "userid": "testuser",
        }
    },
}

# Import a single entity
result = chronicle.import_entities(entities=entity, log_type="TEST_LOG_TYPE")
print(f"Imported entity: {result}")

# Import multiple entities
entity2 = {
    "metadata": {
        "collected_timestamp": "2025-01-01T00:00:00Z",
        "vendor_name": "TestVendor",
        "product_name": "TestProduct",
        "entity_type": "ASSET",
    },
    "entity": {
        "asset": {
            "hostname": "testhost",
        }
    },
}
entities = [entity, entity2]
result = chronicle.import_entities(entities=entities, log_type="TEST_LOG_TYPE")
print(f"Imported entities: {result}")
```

### Data Export

> **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.

You can export Chronicle logs to Google Cloud Storage using the Data Export API:

```python
from datetime import datetime, timedelta, timezone

# Set time range for export
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=1)  # Last 24 hours

# Get available log types for export
available_log_types = chronicle.fetch_available_log_types(
    start_time=start_time,
    end_time=end_time
)

# Print available log types
for log_type in available_log_types["available_log_types"]:
    print(f"{log_type.display_name} ({log_type.log_type.split('/')[-1]})")
    print(f"  Available from {log_type.start_time} to {log_type.end_time}")

# Create a data export for a single log type (legacy method)
export = chronicle.create_data_export(
    gcs_bucket="projects/my-project/buckets/my-export-bucket",
    start_time=start_time,
    end_time=end_time,
    log_type="GCP_DNS"  # Single log type to export
)

# Create a data export for multiple log types
export_multiple = chronicle.create_data_export(
    gcs_bucket="projects/my-project/buckets/my-export-bucket",
    start_time=start_time,
    end_time=end_time,
    log_types=["WINDOWS", "LINUX", "GCP_DNS"]  # Multiple log types to export
)

# Get the export ID
export_id = export["name"].split("/")[-1]
print(f"Created export with ID: {export_id}")
    print(f"Status: {export['data_export_status']['stage']}")

# List recent exports
recent_exports = chronicle.list_data_export(page_size=10)
print(f"Found {len(recent_exports.get('dataExports', []))} recent exports")

# Print details of recent exports
for item in recent_exports.get("dataExports", []):
    item_id = item["name"].split("/")[-1]
    if "dataExportStatus" in item:
        status = item["dataExportStatus"]["stage"]
    else:
        status = item["data_export_status"]["stage"]
    print(f"Export ID: {item_id}, Status: {status}")

# Check export status
status = chronicle.get_data_export(export_id)

# Update an export that is in IN_QUEUE state
if status.get("dataExportStatus", {}).get("stage") == "IN_QUEUE":
    # Update with a new start time
    updated_start = start_time + timedelta(hours=2)
    update_result = chronicle.update_data_export(
        data_export_id=export_id,
        start_time=updated_start,
        # Optionally update other parameters like end_time, gcs_bucket, or log_types
    )
    print("Export updated successfully")

# Cancel an export if needed
if status.get("dataExportStatus", {}).get("stage") in ["IN_QUEUE", "PROCESSING"]:
    cancelled = chronicle.cancel_data_export(export_id)
    print(f"Export has been cancelled. New status: {cancelled['data_export_status']['stage']}")

# Export all log types at once
export_all = chronicle.create_data_export(
    gcs_bucket="projects/my-project/buckets/my-export-bucket",
    start_time=start_time,
    end_time=end_time,
    export_all_logs=True
)

print(f"Created export for all logs. Status: {export_all['data_export_status']['stage']}")
```

The Data Export API supports:
- Exporting one, multiple, or all log types to Google Cloud Storage
- Listing recent exports and filtering results
- Checking export status and progress
- Updating exports that are in the queue
- Cancelling exports in progress
- Fetching available log types for a specific time range

If 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.

### Basic UDM Search

Search for network connection events:

```python
from datetime import datetime, timedelta, timezone

# Set time range for queries
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)  # Last 24 hours

# Perform UDM search
results = chronicle.search_udm(
    query="""
    metadata.event_type = "NETWORK_CONNECTION"
    ip != ""
    """,
    start_time=start_time,
    end_time=end_time,
    max_events=5
)

# Example response:
{
    "events": [
        {
            "name": "projects/my-project/locations/us/instances/my-instance/events/encoded-event-id",
            "udm": {
                "metadata": {
                    "eventTimestamp": "2024-02-09T10:30:00Z",
                    "eventType": "NETWORK_CONNECTION"
                },
                "target": {
                    "ip": ["192.168.1.100"],
                    "port": 443
                },
                "principal": {
                    "hostname": "workstation-1"
                }
            }
        }
    ],
    "total_events": 1,
    "more_data_available": false
}
```

### UDM Search View

Retrieve UDM search results with additional contextual information, including detection data:

```python
from datetime import datetime, timedelta, timezone

# Set time range for queries
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)  # Last 24 hours

# Fetch UDM search view results
results = chronicle.fetch_udm_search_view(
    query='metadata.event_type = "NETWORK_CONNECTION"',
    start_time=start_time,
    end_time=end_time,
    max_events=5,  # Limit to 5 events
    max_detections=10,  # Get up to 10 detections
    snapshot_query='feedback_summary.status = "OPEN"',  # Filter for open alerts
    case_insensitive=True  # Case-insensitive search
)
```

> **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.

### Fetch UDM Field Values

Search for ingested UDM field values that match a query:

```python
# Search for fields containing "source"
results = chronicle.find_udm_field_values(
    query="source",
    page_size=10
)

# Example response:
{
    "valueMatches": [
        {
            "fieldPath": "metadata.ingestion_labels.key",
            "value": "source",
            "ingestionTime": "2025-08-18T08:00:11.670673Z",
            "matchEnd": 6
        },
        {
            "fieldPath": "additional.fields.key",
            "value": "source",
            "ingestionTime": "2025-02-18T19:45:01.811426Z",
            "matchEnd": 6
        }
    ],
    "fieldMatches": [
        {
            "fieldPath": "about.labels.value"
        },
        {
            "fieldPath": "additional.fields.value.string_value"
        }
    ],
    "fieldMatchRegex": "source"
}
```

### Statistics Queries

Get statistics about network connections grouped by hostname:

```python
stats = chronicle.get_stats(
    query="""metadata.event_type = "NETWORK_CONNECTION"
match:
    target.hostname
outcome:
    $count = count(metadata.id)
order:
    $count desc""",
    start_time=start_time,
    end_time=end_time,
    max_events=1000,
    max_values=10,
    timeout=180
)

# Example response:
{
    "columns": ["hostname", "count"],
    "rows": [
        {"hostname": "server-1", "count": 1500},
        {"hostname": "server-2", "count": 1200}
    ],
    "total_rows": 2
}
```

### CSV Export

Export specific fields to CSV format:

```python
csv_data = chronicle.fetch_udm_search_csv(
    query='metadata.event_type = "NETWORK_CONNECTION"',
    start_time=start_time,
    end_time=end_time,
    fields=["timestamp", "user", "hostname", "process name"]
)

# Example response:
"""
metadata.eventTimestamp,principal.hostname,target.ip,target.port
2024-02-09T10:30:00Z,workstation-1,192.168.1.100,443
2024-02-09T10:31:00Z,workstation-2,192.168.1.101,80
"""
```

### Query Validation

Validate a UDM query before execution:

```python
query = 'target.ip != "" and principal.hostname = "test-host"'
validation = chronicle.validate_query(query)

# Example response:
{
    "isValid": true,
    "queryType": "QUERY_TYPE_UDM_QUERY",
    "suggestedFields": [
        "target.ip",
        "principal.hostname"
    ]
}
```

### Natural Language Search

Search for events using natural language instead of UDM query syntax:

```python
from datetime import datetime, timedelta, timezone

# Set time range for queries
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(hours=24)  # Last 24 hours

# Option 1: Translate natural language to UDM query
udm_query = chronicle.translate_nl_to_udm("show me network connections")
print(f"Translated query: {udm_query}")
# Example output: 'metadata.event_type="NETWORK_CONNECTION"'

# Then run the query manually if needed
results = chronicle.search_udm(
    query=udm_query,
    start_time=start_time,
    end_time=end_time
)

# Option 2: Perform complete search with natural language
results = chronicle.nl_search(
    text="show me failed login attempts",
    start_time=start_time,
    end_time=end_time,
    max_events=100
)

# Example response (same format as search_udm):
{
    "events": [
        {
            "event": {
                "metadata": {
                    "eventTimestamp": "2024-02-09T10:30:00Z",
                    "eventType": "USER_LOGIN"
                },
                "principal": {
                    "user": {
                        "userid": "jdoe"
                    }
                },
                "securityResult": {
                    "action": "BLOCK",
                    "summary": "Failed login attempt"
                }
            }
        }
    ],
    "total_events": 1
}
```

The natural language search feature supports various query patterns:
- "Show me network connections"
- "Find suspicious processes"
- "Show login failures in the last hour"
- "Display connections to IP address 192.168.1.100"

If 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.

### Entity Summary

Get 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.

```python
# IP address summary
ip_summary = chronicle.summarize_entity(
    value="8.8.8.8",
    start_time=start_time,
    end_time=end_time
)

# Domain summary
domain_summary = chronicle.summarize_entity(
    value="google.com",
    start_time=start_time,
    end_time=end_time
)

# File hash summary (SHA256)
file_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 
file_summary = chronicle.summarize_entity(
    value=file_hash,
    start_time=start_time,
    end_time=end_time
)

# Optionally hint the preferred type if auto-detection might be ambiguous
user_summary = chronicle.summarize_entity(
    value="jdoe",
    start_time=start_time,
    end_time=end_time,
    preferred_entity_type="USER"
)


# Example response structure (EntitySummary object):
# Access attributes like: ip_summary.primary_entity, ip_summary.related_entities,
# ip_summary.alert_counts, ip_summary.timeline, ip_summary.prevalence, etc.

# Example fields within the EntitySummary object:
# primary_entity: {
#     "name": "entities/...",
#     "metadata": {
#         "entityType": "ASSET",  # Or FILE, DOMAIN_NAME, USER, etc.
#         "interval": { "startTime": "...", "endTime": "..." }
#     },
#     "metric": { "firstSeen": "...", "lastSeen": "..." },
#     "entity": {  # Contains specific details like 'asset', 'file', 'domain'
#         "asset": { "ip": ["8.8.8.8"] }
#     }
# }
# related_entities: [ { ... similar to primary_entity ... } ]
# alert_counts: [ { "rule": "Rule Name", "count": 5 } ]
# timeline: { "buckets": [ { "alertCount": 1, "eventCount": 10 } ], "bucketSize": "3600s" }
# prevalence: [ { "prevalenceTime": "...", "count": 100 } ]
# file_metadata_and_properties: {  # Only for FILE entities
#     "metadata": [ { "key": "...", "value": "..." } ],
#     "properties": [ { "title": "...", "properties": [ { "key": "...", "value": "..." } ] } ]
# }
```

### List IoCs (Indicators of Compromise)

Retrieve IoC matches against ingested events:

```python
iocs = chronicle.list_iocs(
    start_time=start_time,
    end_time=end_time,
    max_matches=1000,
    add_mandiant_attributes=True,
    prioritized_only=False
)

# Process the results
for ioc in iocs['matches']:
    ioc_type = next(iter(ioc['artifactIndicator'].keys()))
    ioc_value = next(iter(ioc['artifactIndicator'].values()))
    print(f"IoC Type: {ioc_type}, Value: {ioc_value}")
    print(f"Sources: {', '.join(ioc['sources'])}")
```

The IoC response includes:
- The indicator itself (domain, IP, hash, etc.)
- Sources and categories
- Affected assets in