diff --git a/CLI.md b/CLI.md index 27bf625b..c7f0f43b 100644 --- a/CLI.md +++ b/CLI.md @@ -745,6 +745,434 @@ Delete a watchlist: secops watchlist delete --watchlist-id "abc-123-def" ``` +### Integration Management + +#### Integration Connectors + +List integration connectors: + +```bash +# List all connectors for an integration +secops integration connectors list --integration-name "MyIntegration" + +# List connectors as a direct list (fetches all pages automatically) +secops integration connectors list --integration-name "MyIntegration" --as-list + +# List with pagination +secops integration connectors list --integration-name "MyIntegration" --page-size 50 + +# List with filtering +secops integration connectors list --integration-name "MyIntegration" --filter-string "enabled = true" +``` + +Get connector details: + +```bash +secops integration connectors get --integration-name "MyIntegration" --connector-id "c1" +``` + +Create a new connector: + +```bash +secops integration connectors create \ + --integration-name "MyIntegration" \ + --display-name "Data Ingestion" \ + --code "def fetch_data(context): return []" + +# Create with description and custom ID +secops integration connectors create \ + --integration-name "MyIntegration" \ + --display-name "My Connector" \ + --code "def fetch_data(context): return []" \ + --description "Connector description" \ + --connector-id "custom-connector-id" +``` + +Update an existing connector: + +```bash +# Update display name +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --display-name "Updated Connector Name" + +# Update code +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --code "def fetch_data(context): return updated_data()" + +# Update multiple fields with update mask +secops integration connectors update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --display-name "New Name" \ + --description "New description" \ + --update-mask "displayName,description" +``` + +Delete a connector: + +```bash +secops integration connectors delete --integration-name "MyIntegration" --connector-id "c1" +``` + +Test a connector: + +```bash +secops integration connectors test --integration-name "MyIntegration" --connector-id "c1" +``` + +Get connector template: + +```bash +secops integration connectors template --integration-name "MyIntegration" +``` + +#### Connector Revisions + +List connector revisions: + +```bash +# List all revisions for a connector +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" + +# List revisions as a direct list +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --as-list + +# List with pagination +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --page-size 10 + +# List with filtering and ordering +secops integration connector-revisions list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --filter-string 'version = "1.0"' \ + --order-by "createTime desc" +``` + +Create a revision backup: + +```bash +# Create revision with comment +secops integration connector-revisions create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --comment "Backup before field mapping changes" + +# Create revision without comment +secops integration connector-revisions create \ + --integration-name "MyIntegration" \ + --connector-id "c1" +``` + +Rollback to a previous revision: + +```bash +secops integration connector-revisions rollback \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --revision-id "r456" +``` + +Delete an old revision: + +```bash +secops integration connector-revisions delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --revision-id "r789" +``` + +#### Connector Context Properties + +List connector context properties: + +```bash +# List all properties for a connector context +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" + +# List properties as a direct list +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --as-list + +# List with pagination +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --page-size 50 + +# List with filtering +secops integration connector-context-properties list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --filter-string 'key = "last_run_time"' +``` + +Get a specific context property: + +```bash +secops integration connector-context-properties get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Create a new context property: + +```bash +# Store last run time +secops integration connector-context-properties create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --key "last_run_time" \ + --value "2026-03-09T10:00:00Z" + +# Store checkpoint for incremental sync +secops integration connector-context-properties create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --key "checkpoint" \ + --value "page_token_xyz123" +``` + +Update a context property: + +```bash +# Update last run time +secops integration connector-context-properties update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" \ + --value "2026-03-09T11:00:00Z" +``` + +Delete a context property: + +```bash +secops integration connector-context-properties delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" \ + --property-id "prop123" +``` + +Clear all context properties: + +```bash +# Clear all properties for a specific context +secops integration connector-context-properties clear-all \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --context-id "mycontext" +``` + +#### Connector Instance Logs + +List connector instance logs: + +```bash +# List all logs for a connector instance +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" + +# List logs as a direct list +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --as-list + +# List with pagination +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --page-size 50 + +# List with filtering (filter by severity or timestamp) +secops integration connector-instance-logs list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --filter-string 'severity = "ERROR"' \ + --order-by "createTime desc" +``` + +Get a specific log entry: + +```bash +secops integration connector-instance-logs get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --log-id "log456" +``` + +#### Connector Instances + +List connector instances: + +```bash +# List all instances for a connector +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" + +# List instances as a direct list +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --as-list + +# List with pagination +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --page-size 50 + +# List with filtering +secops integration connector-instances list \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --filter-string 'enabled = true' +``` + +Get connector instance details: + +```bash +secops integration connector-instances get \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Create a new connector instance: + +```bash +# Create basic connector instance +secops integration connector-instances create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --environment "production" \ + --display-name "Production Data Collector" + +# Create with schedule and timeout +secops integration connector-instances create \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --environment "production" \ + --display-name "Hourly Sync" \ + --interval-seconds 3600 \ + --timeout-seconds 300 \ + --enabled +``` + +Update a connector instance: + +```bash +# Update display name +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --display-name "Updated Display Name" + +# Update interval and timeout +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --interval-seconds 7200 \ + --timeout-seconds 600 + +# Enable or disable instance +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled true + +# Update multiple fields with update mask +secops integration connector-instances update \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --display-name "New Name" \ + --interval-seconds 3600 \ + --update-mask "displayName,intervalSeconds" +``` + +Delete a connector instance: + +```bash +secops integration connector-instances delete \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Fetch latest definition: + +```bash +# Get the latest definition of a connector instance +secops integration connector-instances fetch-latest \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + +Enable or disable log collection: + +```bash +# Enable log collection for debugging +secops integration connector-instances set-logs \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled true + +# Disable log collection +secops integration connector-instances set-logs \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" \ + --enabled false +``` + +Run connector instance on demand: + +```bash +# Trigger an immediate execution for testing +secops integration connector-instances run-ondemand \ + --integration-name "MyIntegration" \ + --connector-id "c1" \ + --connector-instance-id "inst123" +``` + ### Rule Management List detection rules: @@ -896,7 +1324,6 @@ secops curated-rule search-detections \ --end-time "2024-01-31T23:59:59Z" \ --list-basis "DETECTION_TIME" \ --page-size 50 - ``` List all curated rule sets: @@ -1543,39 +1970,7 @@ secops reference-list create \ secops parser list # Get details of a specific parser -secops parser get --log-type "WINDOWS" --id "pa_12345" - -# Create a custom parser for a new log format -secops parser create \ - --log-type "CUSTOM_APPLICATION" \ - --parser-code-file "/path/to/custom_parser.conf" \ - --validated-on-empty-logs - -# Copy an existing parser as a starting point -secops parser copy --log-type "OKTA" --id "pa_okta_base" - -# Activate your custom parser -secops parser activate --log-type "CUSTOM_APPLICATION" --id "pa_new_custom" - -# If needed, deactivate and delete old parser -secops parser deactivate --log-type "CUSTOM_APPLICATION" --id "pa_old_custom" -secops parser delete --log-type "CUSTOM_APPLICATION" --id "pa_old_custom" -``` - -### Complete Parser Workflow Example: Retrieve, Run, and Ingest - -This example demonstrates the complete workflow of retrieving an OKTA parser, running it against a sample log, and ingesting the parsed UDM event: - -```bash -# Step 1: List OKTA parsers to find an active one -secops parser list --log-type "OKTA" > okta_parsers.json - -# Extract the first parser ID (you can use jq or grep) -PARSER_ID=$(cat okta_parsers.json | jq -r '.[0].name' | awk -F'/' '{print $NF}') -echo "Using parser: $PARSER_ID" - -# Step 2: Get the parser details and save to a file -secops parser get --log-type "OKTA" --id "$PARSER_ID" > parser_details.json +secops parser get --log-type "WINDOWS" --id "$PARSER_ID" > parser_details.json # Extract and decode the parser code (base64 encoded in 'cbn' field) cat parser_details.json | jq -r '.cbn' | base64 -d > okta_parser.conf @@ -1713,7 +2108,7 @@ secops feed update --id "feed-123" --display-name "Updated Feed Name" secops feed update --id "feed-123" --details '{"httpSettings":{"uri":"https://example.com/updated-feed","sourceType":"FILES"}}' # Update both display name and details -secops feed update --id "feed-123" --display-name "Updated Name" --details '{"httpSettings":{"uri":"https://example.com/updated-feed"}}' +secops feed update --id "feed-123" --display-name "New Name" --details '{"httpSettings":{"uri":"https://example.com/updated-feed"}}' ``` Enable and disable feeds: @@ -1854,4 +2249,5 @@ secops dashboard-query get --id query-id ## Conclusion -The SecOps CLI provides a powerful way to interact with Google Security Operations products directly from your terminal. For more detailed information about the SDK capabilities, refer to the [main README](README.md). \ No newline at end of file +The SecOps CLI provides a powerful way to interact with Google Security Operations products directly from your terminal. For more detailed information about the SDK capabilities, refer to the [main README](README.md). + diff --git a/README.md b/README.md index d746ad5a..f7f1d6c3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +from tests.chronicle.test_rule_integration import chronicle + # Google SecOps SDK for Python [![PyPI version](https://img.shields.io/pypi/v/secops.svg)](https://pypi.org/project/secops/) @@ -1907,6 +1909,774 @@ for watchlist in watchlists: print(f"Watchlist: {watchlist.get('displayName')}") ``` +## Integration Management + +### Integration Connectors + +List all available connectors for an integration: + +```python +# Get all connectors for an integration +connectors = chronicle.list_integration_connectors("AWSSecurityHub") + +# Get all connectors as a list +connectors = chronicle.list_integration_connectors("AWSSecurityHub", as_list=True) + +# Get only enabled connectors +connectors = chronicle.list_integration_connectors( + "AWSSecurityHub", + filter_string="enabled = true" +) + +# Exclude staging connectors +connectors = chronicle.list_integration_connectors( + "AWSSecurityHub", + exclude_staging=True +) +``` + +Get details of a specific connector: + +```python +connector = chronicle.get_integration_connector( + integration_name="AWSSecurityHub", + connector_id="123" +) +``` + +Create an integration connector: + +```python +from secops.chronicle.models import ( + ConnectorParameter, + ParamType, + ConnectorParamMode, + ConnectorRule, + ConnectorRuleType +) + +new_connector = chronicle.create_integration_connector( + integration_name="MyIntegration", + display_name="New Connector", + description="This is a new connector", + script="print('Fetching data...')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event_type", + parameters=[ + ConnectorParameter( + display_name="API Key", + type=ParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="API key for authentication" + ) + ], + rules=[ + ConnectorRule( + display_name="Allow List", + type=ConnectorRuleType.ALLOW_LIST + ) + ] +) +``` + +Update an integration connector: + +```python +from secops.chronicle.models import ( + ConnectorParameter, + ParamType, + ConnectorParamMode +) + +updated_connector = chronicle.update_integration_connector( + integration_name="MyIntegration", + connector_id="123", + display_name="Updated Connector Name", + description="Updated description", + enabled=False, + timeout_seconds=600, + parameters=[ + ConnectorParameter( + display_name="API Token", + type=ParamType.PASSWORD, + mode=ConnectorParamMode.CONNECTIVITY, + mandatory=True, + description="Updated authentication token" + ) + ], + script="print('Updated connector script')" +) +``` + +Delete an integration connector: + +```python +chronicle.delete_integration_connector( + integration_name="MyIntegration", + connector_id="123" +) +``` + +Execute a test run of an integration connector: + +```python +# Test a connector before saving it +connector_config = { + "displayName": "Test Connector", + "script": "print('Testing connector')", + "enabled": True, + "timeoutSeconds": 300, + "productFieldName": "product", + "eventFieldName": "event_type" +} + +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=connector_config +) + +print(f"Output: {test_result.get('outputMessage')}") +print(f"Debug: {test_result.get('debugOutputMessage')}") + +# Test with a specific agent for remote execution +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=connector_config, + agent_identifier="agent-123" +) +``` + +Get a template for creating a connector in an integration: + +```python +template = chronicle.get_integration_connector_template("MyIntegration") +print(f"Template script: {template.get('script')}") +``` + +### Integration Connector Revisions + +List all revisions for a specific integration connector: + +```python +# Get all revisions for a connector +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1" +) +for revision in revisions.get("revisions", []): + print(f"Revision ID: {revision.get('name')}") + print(f"Comment: {revision.get('comment')}") + print(f"Created: {revision.get('createTime')}") + +# Get all revisions as a list +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter revisions with order +revisions = chronicle.list_integration_connector_revisions( + integration_name="MyIntegration", + connector_id="c1", + order_by="createTime desc", + page_size=10 +) +``` + +Delete a specific connector revision: + +```python +# Clean up old revision from version history +chronicle.delete_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id="r1" +) +``` + +Create a new connector revision snapshot: + +```python +# Get the current connector configuration +connector = chronicle.get_integration_connector( + integration_name="MyIntegration", + connector_id="c1" +) + +# Create a revision without comment +new_revision = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector +) + +# Create a revision with descriptive comment +new_revision = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector, + comment="Stable version before adding new field mapping" +) + +print(f"Created revision: {new_revision.get('name')}") +``` + +Rollback a connector to a previous revision: + +```python +# Revert to a known good configuration +rolled_back = chronicle.rollback_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id="r1" +) + +print(f"Rolled back to revision: {rolled_back.get('name')}") +print(f"Connector script restored") +``` + +Example workflow: Safe connector updates with revisions: + +```python +# 1. Get current connector +connector = chronicle.get_integration_connector( + integration_name="MyIntegration", + connector_id="c1" +) + +# 2. Create backup revision before changes +backup = chronicle.create_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + connector=connector, + comment="Backup before timeout increase" +) +print(f"Backup created: {backup.get('name')}") + +# 3. Update the connector +updated_connector = chronicle.update_integration_connector( + integration_name="MyIntegration", + connector_id="c1", + timeout_seconds=600, + description="Increased timeout for large data pulls" +) + +# 4. Test the updated connector +test_result = chronicle.execute_integration_connector_test( + integration_name="MyIntegration", + connector=updated_connector +) + +# 5. If test fails, rollback to the backup +if not test_result.get("outputMessage"): + print("Test failed, rolling back...") + chronicle.rollback_integration_connector_revision( + integration_name="MyIntegration", + connector_id="c1", + revision_id=backup.get("name").split("/")[-1] + ) + print("Rollback complete") +else: + print("Test passed, changes applied successfully") +``` + +### Connector Context Properties + +List all context properties for a specific connector: + +```python +# Get all context properties for a connector +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1" +) +for prop in context_properties.get("contextProperties", []): + print(f"Key: {prop.get('key')}, Value: {prop.get('value')}") + +# Get all context properties as a list +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter context properties +context_properties = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + filter_string='key = "last_run_time"', + order_by="key" +) +``` + +Get a specific context property: + +```python +property_value = chronicle.get_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last_run_time" +) +print(f"Value: {property_value.get('value')}") +``` + +Create a new context property: + +```python +# Create context property with auto-generated key +new_property = chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-03-09T10:00:00Z" +) + +# Create context property with custom key +new_property = chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-03-09T10:00:00Z", + key="last-sync-time" +) +print(f"Created property: {new_property.get('name')}") +``` + +Update an existing context property: + +```python +# Update property value +updated_property = chronicle.update_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-sync-time", + value="2026-03-09T11:00:00Z" +) +print(f"Updated value: {updated_property.get('value')}") +``` + +Delete a context property: + +```python +chronicle.delete_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-sync-time" +) +``` + +Delete all context properties: + +```python +# Clear all properties for a connector +chronicle.delete_all_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1" +) + +# Clear all properties for a specific context ID +chronicle.delete_all_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + context_id="my-context" +) +``` + +Example workflow: Track connector state with context properties: + +```python +# 1. Check if we have a last run time stored +try: + last_run = chronicle.get_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-run-time" + ) + print(f"Last run: {last_run.get('value')}") +except APIError: + print("No previous run time found") + # Create initial property + chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="2026-01-01T00:00:00Z", + key="last-run-time" + ) + +# 2. Run the connector and process data +# ... connector execution logic ... + +# 3. Update the last run time after successful execution +from datetime import datetime +current_time = datetime.utcnow().isoformat() + "Z" +chronicle.update_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + context_property_id="last-run-time", + value=current_time +) + +# 4. Store additional context like record count +chronicle.create_connector_context_property( + integration_name="MyIntegration", + connector_id="c1", + value="1500", + key="records-processed" +) + +# 5. List all context to see connector state +all_context = chronicle.list_connector_context_properties( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) +for prop in all_context: + print(f"{prop.get('key')}: {prop.get('value')}") +``` + +### Connector Instance Logs + +List all execution logs for a connector instance: + +```python +# Get all logs for a connector instance +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +for log in logs.get("logs", []): + print(f"Log ID: {log.get('name')}, Severity: {log.get('severity')}") + print(f"Timestamp: {log.get('timestamp')}") + print(f"Message: {log.get('message')}") + +# Get all logs as a list +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True +) + +# Filter logs by severity +logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + filter_string='severity = "ERROR"', + order_by="timestamp desc" +) +``` + +Get a specific log entry: + +```python +log_entry = chronicle.get_connector_instance_log( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log123" +) +print(f"Severity: {log_entry.get('severity')}") +print(f"Timestamp: {log_entry.get('timestamp')}") +print(f"Message: {log_entry.get('message')}") +``` + +Monitor connector execution and troubleshooting: + +```python +# Get recent logs for monitoring +recent_logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + order_by="timestamp desc", + page_size=10, + as_list=True +) + +# Check for errors +for log in recent_logs: + if log.get("severity") in ["ERROR", "CRITICAL"]: + print(f"Error at {log.get('timestamp')}") + log_details = chronicle.get_connector_instance_log( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + log_id=log.get("name").split("/")[-1] + ) + print(f"Error message: {log_details.get('message')}") +``` + +Analyze connector performance and reliability: + +```python +# Get all logs to calculate error rate +all_logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True +) + +errors = sum(1 for log in all_logs if log.get("severity") in ["ERROR", "CRITICAL"]) +warnings = sum(1 for log in all_logs if log.get("severity") == "WARNING") +total = len(all_logs) + +if total > 0: + error_rate = (errors / total) * 100 + print(f"Error Rate: {error_rate:.2f}%") + print(f"Total Logs: {total}") + print(f"Errors: {errors}, Warnings: {warnings}") +``` + +### Connector Instances + +List all connector instances for a specific connector: + +```python +# Get all instances for a connector +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1" +) +for instance in instances.get("connectorInstances", []): + print(f"Instance: {instance.get('displayName')}, Enabled: {instance.get('enabled')}") + +# Get all instances as a list +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + as_list=True +) + +# Filter instances +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + filter_string='enabled = true', + order_by="displayName" +) +``` + +Get a specific connector instance: + +```python +instance = chronicle.get_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +print(f"Display Name: {instance.get('displayName')}") +print(f"Environment: {instance.get('environment')}") +print(f"Interval: {instance.get('intervalSeconds')} seconds") +``` + +Create a new connector instance: + +```python +from secops.chronicle.models import ConnectorInstanceParameter + +# Create basic connector instance +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="production", + display_name="Production Instance", + interval_seconds=3600, # Run every hour + timeout_seconds=300, # 5 minute timeout + enabled=True +) + +# Create instance with parameters +param = ConnectorInstanceParameter() +param.value = "my-api-key" + +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="production", + display_name="Production Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Main production connector instance", + parameters=[param], + enabled=True +) +print(f"Created instance: {new_instance.get('name')}") +``` + +Update an existing connector instance: + +```python +# Update display name +updated_instance = chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated Production Instance" +) + +# Update multiple fields including parameters +param = ConnectorInstanceParameter() +param.value = "new-api-key" + +updated_instance = chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated Instance", + interval_seconds=7200, # Change to every 2 hours + parameters=[param], + enabled=True +) +``` + +Delete a connector instance: + +```python +chronicle.delete_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +``` + +Refresh instance with latest connector definition: + +```python +# Fetch latest definition from marketplace +refreshed_instance = chronicle.get_connector_instance_latest_definition( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) +print(f"Updated to latest definition") +``` + +Enable/disable logs collection for debugging: + +```python +# Enable logs collection +result = chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True +) +print(f"Logs enabled until: {result.get('loggingEnabledUntilUnixMs')}") + +# Disable logs collection +chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + enabled=False +) +``` + +Run a connector instance on demand for testing: + +```python +# Get the current instance configuration +instance = chronicle.get_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1" +) + +# Run on demand to test configuration +test_result = chronicle.run_connector_instance_on_demand( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=instance +) + +if test_result.get("success"): + print("Test execution successful!") + print(f"Debug output: {test_result.get('debugOutput')}") +else: + print("Test execution failed") + print(f"Error: {test_result.get('debugOutput')}") +``` + +Example workflow: Deploy and test a new connector instance: + +```python +from secops.chronicle.models import ConnectorInstanceParameter + +# 1. Create a new connector instance +param = ConnectorInstanceParameter() +param.value = "test-api-key" + +new_instance = chronicle.create_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + environment="development", + display_name="Dev Test Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Development testing instance", + parameters=[param], + enabled=False # Start disabled for testing +) + +instance_id = new_instance.get("name").split("/")[-1] +print(f"Created instance: {instance_id}") + +# 2. Enable logs collection for debugging +chronicle.set_connector_instance_logs_collection( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + enabled=True +) + +# 3. Run on demand to test +test_result = chronicle.run_connector_instance_on_demand( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + connector_instance=new_instance +) + +# 4. Check test results +if test_result.get("success"): + print("✓ Test passed - enabling instance") + # Enable the instance for scheduled runs + chronicle.update_connector_instance( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + enabled=True + ) +else: + print("✗ Test failed - reviewing logs") + # Get logs to debug the issue + logs = chronicle.list_connector_instance_logs( + integration_name="MyIntegration", + connector_id="c1", + connector_instance_id=instance_id, + filter_string='severity = "ERROR"', + as_list=True + ) + for log in logs: + print(f"Error: {log.get('message')}") + +# 5. Monitor execution after enabling +instances = chronicle.list_connector_instances( + integration_name="MyIntegration", + connector_id="c1", + filter_string=f'name = "{new_instance.get("name")}"', + as_list=True +) +if instances: + print(f"Instance status: Enabled={instances[0].get('enabled')}") +``` + ## Rule Management The SDK provides comprehensive support for managing Chronicle detection rules: diff --git a/api_module_mapping.md b/api_module_mapping.md index bcfa632d..3d006a93 100644 --- a/api_module_mapping.md +++ b/api_module_mapping.md @@ -7,10 +7,643 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/ ## Implementation Statistics - **v1:** 17 endpoints implemented -- **v1alpha:** 113 endpoints implemented +- **v1beta:** 88 endpoints implemented +- **v1alpha:** 203 endpoints implemented ## Endpoint Mapping +| REST Resource | Version | secops-wrapper module | CLI Command | +|--------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| +| dataAccessLabels.create | v1 | | | +| dataAccessLabels.delete | v1 | | | +| dataAccessLabels.get | v1 | | | +| dataAccessLabels.list | v1 | | | +| dataAccessLabels.patch | v1 | | | +| dataAccessScopes.create | v1 | | | +| dataAccessScopes.delete | v1 | | | +| dataAccessScopes.get | v1 | | | +| dataAccessScopes.list | v1 | | | +| dataAccessScopes.patch | v1 | | | +| get | v1 | | | +| operations.cancel | v1 | | | +| operations.delete | v1 | | | +| operations.get | v1 | | | +| operations.list | v1 | | | +| referenceLists.create | v1 | chronicle.reference_list.create_reference_list | secops reference-list create | +| referenceLists.get | v1 | chronicle.reference_list.get_reference_list | secops reference-list get | +| referenceLists.list | v1 | chronicle.reference_list.list_reference_lists | secops reference-list list | +| referenceLists.patch | v1 | chronicle.reference_list.update_reference_list | secops reference-list update | +| rules.create | v1 | chronicle.rule.create_rule | secops rule create | +| rules.delete | v1 | chronicle.rule.delete_rule | secops rule delete | +| rules.deployments.list | v1 | | | +| rules.get | v1 | chronicle.rule.get_rule | secops rule get | +| rules.getDeployment | v1 | | | +| rules.list | v1 | chronicle.rule.list_rules | secops rule list | +| rules.listRevisions | v1 | | | +| rules.patch | v1 | chronicle.rule.update_rule | secops rule update | +| rules.retrohunts.create | v1 | chronicle.rule_retrohunt.create_retrohunt | secops rule-retrohunt create | +| rules.retrohunts.get | v1 | chronicle.rule_retrohunt.get_retrohunt | secops rule-retrohunt get | +| rules.retrohunts.list | v1 | chronicle.rule_retrohunt.list_retrohunts | secops rule-retrohunt list | +| rules.updateDeployment | v1 | chronicle.rule.enable_rule | secops rule enable | +| watchlists.create | v1 | chronicle.watchlist.create_watchlist | secops watchlist create | +| watchlists.delete | v1 | chronicle.watchlist.delete_watchlist | secops watchlist delete | +| watchlists.get | v1 | chronicle.watchlist.get_watchlist | secops watchlist get | +| watchlists.list | v1 | chronicle.watchlist.list_watchlists | secops watchlist list | +| watchlists.patch | v1 | chronicle.watchlist.update_watchlist | secops watchlist update | +| dataAccessLabels.create | v1beta | | | +| dataAccessLabels.delete | v1beta | | | +| dataAccessLabels.get | v1beta | | | +| dataAccessLabels.list | v1beta | | | +| dataAccessLabels.patch | v1beta | | | +| dataAccessScopes.create | v1beta | | | +| dataAccessScopes.delete | v1beta | | | +| dataAccessScopes.get | v1beta | | | +| dataAccessScopes.list | v1beta | | | +| dataAccessScopes.patch | v1beta | | | +| get | v1beta | | | +| integrations.create | v1beta | | | +| integrations.delete | v1beta | chronicle.integration.integrations.delete_integration | secops integration integrations delete | +| integrations.download | v1beta | chronicle.integration.integrations.download_integration | secops integration integrations download | +| integrations.downloadDependency | v1beta | chronicle.integration.integrations.download_integration_dependency | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1beta | chronicle.integration.integrations.export_integration_items | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1beta | chronicle.integration.integrations.get_integration_affected_items | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1beta | chronicle.integration.integrations.get_agent_integrations | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1beta | chronicle.integration.integrations.get_integration_diff | secops integration integrations get-diff | +| integrations.fetchDependencies | v1beta | chronicle.integration.integrations.get_integration_dependencies | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1beta | chronicle.integration.integrations.get_integration_restricted_agents | secops integration integrations get-restricted-agents | +| integrations.get | v1beta | chronicle.integration.integrations.get_integration | secops integration integrations get | +| integrations.getFetchProductionDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1beta | chronicle.integration.integrations.get_integration_diff(diff_type=DiffType.STAGING) | secops integration integrations get-diff | +| integrations.import | v1beta | | | +| integrations.importIntegrationDependency | v1beta | | | +| integrations.importIntegrationItems | v1beta | | | +| integrations.list | v1beta | chronicle.integration.integrations.list_integrations | secops integration integrations list | +| integrations.patch | v1beta | | | +| integrations.pushToProduction | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1beta | chronicle.integration.integrations.transition_integration(target_mode=TargetMode.STAGING) | secops integration integrations transition | +| integrations.updateCustomIntegration | v1beta | | | +| integrations.upload | v1beta | | | +| integrations.actions.create | v1beta | chronicle.integration.actions.create_integration_action | secops integration actions create | +| integrations.actions.delete | v1beta | chronicle.integration.actions.delete_integration_action | secops integration actions delete | +| integrations.actions.executeTest | v1beta | chronicle.integration.actions.execute_integration_action_test | secops integration actions test | +| integrations.actions.fetchActionsByEnvironment | v1beta | chronicle.integration.actions.get_integration_actions_by_environment | | +| integrations.actions.fetchTemplate | v1beta | chronicle.integration.actions.get_integration_action_template | secops integration actions template | +| integrations.actions.get | v1beta | chronicle.integration.actions.get_integration_action | secops integration actions get | +| integrations.actions.list | v1beta | chronicle.integration.actions.list_integration_actions | secops integration actions list | +| integrations.actions.patch | v1beta | chronicle.integration.actions.update_integration_action | secops integration actions update | +| integrations.actions.revisions.create | v1beta | chronicle.integration.action_revisions.create_integration_action_revision | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1beta | chronicle.integration.action_revisions.delete_integration_action_revision | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1beta | chronicle.integration.action_revisions.list_integration_action_revisions | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1beta | chronicle.integration.action_revisions.rollback_integration_action_revision | secops integration action-revisions rollback | +| integrations.connectors.create | v1beta | chronicle.integration.connectors.create_integration_connector | secops integration connectors create | +| integrations.connectors.delete | v1beta | chronicle.integration.connectors.delete_integration_connector | secops integration connectors delete | +| integrations.connectors.executeTest | v1beta | chronicle.integration.connectors.execute_integration_connector_test | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1beta | chronicle.integration.connectors.get_integration_connector_template | secops integration connectors template | +| integrations.connectors.get | v1beta | chronicle.integration.connectors.get_integration_connector | secops integration connectors get | +| integrations.connectors.list | v1beta | chronicle.integration.connectors.list_integration_connectors | secops integration connectors list | +| integrations.connectors.patch | v1beta | chronicle.integration.connectors.update_integration_connector | secops integration connectors update | +| integrations.connectors.revisions.create | v1beta | chronicle.integration.connector_revisions.create_integration_connector_revision | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1beta | chronicle.integration.connector_revisions.delete_integration_connector_revision | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1beta | chronicle.integration.connector_revisions.list_integration_connector_revisions | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1beta | chronicle.integration.connector_revisions.rollback_integration_connector_revision | secops integration connector-revisions rollback| +| integrations.connectors.contextProperties.clearAll | v1beta | chronicle.integration.connector_context_properties.delete_all_connector_context_properties | secops integration connector-context-properties delete-all | +| integrations.connectors.contextProperties.create | v1beta | chronicle.integration.connector_context_properties.create_connector_context_property | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1beta | chronicle.integration.connector_context_properties.delete_connector_context_property | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1beta | chronicle.integration.connector_context_properties.get_connector_context_property | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1beta | chronicle.integration.connector_context_properties.list_connector_context_properties | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1beta | chronicle.integration.connector_context_properties.update_connector_context_property | secops integration connector-context-properties update | +| integrations.connectors.connectorInstances.logs.get | v1beta | chronicle.integration.connector_instance_logs.get_connector_instance_log | secops integration connector-instance-logs get | +| integrations.connectors.connectorInstances.logs.list | v1beta | chronicle.integration.connector_instance_logs.list_connector_instance_logs | secops integration connector-instance-logs list| +| integrations.connectors.connectorInstances.create | v1beta | chronicle.integration.connector_instances.create_connector_instance | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1beta | chronicle.integration.connector_instances.delete_connector_instance | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1beta | chronicle.integration.connector_instances.get_connector_instance_latest_definition | secops integration connector-instances get-latest-definition | +| integrations.connectors.connectorInstances.get | v1beta | chronicle.integration.connector_instances.get_connector_instance | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1beta | chronicle.integration.connector_instances.list_connector_instances | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1beta | chronicle.integration.connector_instances.update_connector_instance | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1beta | chronicle.integration.connector_instances.run_connector_instance_on_demand | secops integration connector-instances run-on-demand | +| integrations.connectors.connectorInstances.setLogsCollection | v1beta | chronicle.integration.connector_instances.set_connector_instance_logs_collection | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1beta | chronicle.integration.integration_instances.create_integration_instance | secops integration instances create | +| integrations.integrationInstances.delete | v1beta | chronicle.integration.integration_instances.delete_integration_instance | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1beta | chronicle.integration.integration_instances.execute_integration_instance_test | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1beta | chronicle.integration.integration_instances.get_integration_instance_affected_items | secops integration instances get-affected-items| +| integrations.integrationInstances.fetchDefaultInstance | v1beta | chronicle.integration.integration_instances.get_default_integration_instance | secops integration instances get-default | +| integrations.integrationInstances.get | v1beta | chronicle.integration.integration_instances.get_integration_instance | secops integration instances get | +| integrations.integrationInstances.list | v1beta | chronicle.integration.integration_instances.list_integration_instances | secops integration instances list | +| integrations.integrationInstances.patch | v1beta | chronicle.integration.integration_instances.update_integration_instance | secops integration instances update | +| integrations.jobs.create | v1beta | chronicle.integration.jobs.create_integration_job | secops integration jobs create | +| integrations.jobs.delete | v1beta | chronicle.integration.jobs.delete_integration_job | secops integration jobs delete | +| integrations.jobs.executeTest | v1beta | chronicle.integration.jobs.execute_integration_job_test | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1beta | chronicle.integration.jobs.get_integration_job_template | secops integration jobs template | +| integrations.jobs.get | v1beta | chronicle.integration.jobs.get_integration_job | secops integration jobs get | +| integrations.jobs.list | v1beta | chronicle.integration.jobs.list_integration_jobs | secops integration jobs list | +| integrations.jobs.patch | v1beta | chronicle.integration.jobs.update_integration_job | secops integration jobs update | +| integrations.managers.create | v1beta | chronicle.integration.managers.create_integration_manager | secops integration managers create | +| integrations.managers.delete | v1beta | chronicle.integration.managers.delete_integration_manager | secops integration managers delete | +| integrations.managers.fetchTemplate | v1beta | chronicle.integration.managers.get_integration_manager_template | secops integration managers template | +| integrations.managers.get | v1beta | chronicle.integration.managers.get_integration_manager | secops integration managers get | +| integrations.managers.list | v1beta | chronicle.integration.managers.list_integration_managers | secops integration managers list | +| integrations.managers.patch | v1beta | chronicle.integration.managers.update_integration_manager | secops integration managers update | +| integrations.managers.revisions.create | v1beta | chronicle.integration.manager_revisions.create_integration_manager_revision | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1beta | chronicle.integration.manager_revisions.delete_integration_manager_revision | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1beta | chronicle.integration.manager_revisions.get_integration_manager_revision | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1beta | chronicle.integration.manager_revisions.list_integration_manager_revisions | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1beta | chronicle.integration.manager_revisions.rollback_integration_manager_revision | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1beta | chronicle.integration.job_revisions.create_integration_job_revision | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1beta | chronicle.integration.job_revisions.delete_integration_job_revision | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1beta | chronicle.integration.job_revisions.list_integration_job_revisions | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1beta | chronicle.integration.job_revisions.rollback_integration_job_revision | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1beta | chronicle.integration.job_instances.create_integration_job_instance | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1beta | chronicle.integration.job_instances.delete_integration_job_instance | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1beta | chronicle.integration.job_instances.get_integration_job_instance | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1beta | chronicle.integration.job_instances.list_integration_job_instances | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1beta | chronicle.integration.job_instances.update_integration_job_instance | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1beta | chronicle.integration.job_instances.run_integration_job_instance_on_demand | secops integration job-instances run-on-demand | +| integrations.jobs.contextProperties.clearAll | v1beta | chronicle.integration.job_context_properties.delete_all_job_context_properties | secops integration job-context-properties delete-all | +| integrations.jobs.contextProperties.create | v1beta | chronicle.integration.job_context_properties.create_job_context_property | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1beta | chronicle.integration.job_context_properties.delete_job_context_property | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1beta | chronicle.integration.job_context_properties.get_job_context_property | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1beta | chronicle.integration.job_context_properties.list_job_context_properties | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1beta | chronicle.integration.job_context_properties.update_job_context_property | secops integration job-context-properties update | +| integrations.jobs.jobInstances.logs.get | v1beta | chronicle.integration.job_instance_logs.get_job_instance_log | secops integration job-instance-logs get | +| integrations.jobs.jobInstances.logs.list | v1beta | chronicle.integration.job_instance_logs.list_job_instance_logs | secops integration job-instance-logs list | +| marketplaceIntegrations.get | v1beta | chronicle.marketplace_integrations.get_marketplace_integration | secops integration marketplace get | +| marketplaceIntegrations.getDiff | v1beta | chronicle.marketplace_integrations.get_marketplace_integration_diff | secops integration marketplace diff | +| marketplaceIntegrations.install | v1beta | chronicle.marketplace_integrations.install_marketplace_integration | secops integration marketplace install | +| marketplaceIntegrations.list | v1beta | chronicle.marketplace_integrations.list_marketplace_integrations | secops integration marketplace list | +| marketplaceIntegrations.uninstall | v1beta | chronicle.marketplace_integrations.uninstall_marketplace_integration | secops integration marketplace uninstall | +| operations.cancel | v1beta | | | +| operations.delete | v1beta | | | +| operations.get | v1beta | | | +| operations.list | v1beta | | | +| referenceLists.create | v1beta | | | +| referenceLists.get | v1beta | | | +| referenceLists.list | v1beta | | | +| referenceLists.patch | v1beta | | | +| rules.create | v1beta | | | +| rules.delete | v1beta | | | +| rules.deployments.list | v1beta | | | +| rules.get | v1beta | | | +| rules.getDeployment | v1beta | | | +| rules.list | v1beta | | | +| rules.listRevisions | v1beta | | | +| rules.patch | v1beta | | | +| rules.retrohunts.create | v1beta | | | +| rules.retrohunts.get | v1beta | | | +| rules.retrohunts.list | v1beta | | | +| rules.updateDeployment | v1beta | | | +| watchlists.create | v1beta | | | +| watchlists.delete | v1beta | | | +| watchlists.get | v1beta | | | +| watchlists.list | v1beta | | | +| watchlists.patch | v1beta | | | +| analytics.entities.analyticValues.list | v1alpha | | | +| analytics.list | v1alpha | | | +| batchValidateWatchlistEntities | v1alpha | | | +| bigQueryAccess.provide | v1alpha | | | +| bigQueryExport.provision | v1alpha | | | +| cases.countPriorities | v1alpha | | | +| contentHub.featuredContentRules.list | v1alpha | chronicle.featured_content_rules.list_featured_content_rules | secops featured-content-rules list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.batchUpdate | v1alpha | chronicle.rule_set.batch_update_curated_rule_set_deployments | | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.patch | v1alpha | chronicle.rule_set.update_curated_rule_set_deployment | secops curated-rule rule-set-deployment update | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.list | v1alpha | chronicle.rule_set.list_curated_rule_set_deployments | secops curated-rule rule-set-deployment list | +| curatedRuleSetCategories.curatedRuleSets.curatedRuleSetDeployments.get | v1alpha | chronicle.rule_set.get_curated_rule_set_deployment
chronicle.rule_set.get_curated_rule_set_deployment_by_name | secops curated-rule rule-set-deployment get | +| curatedRuleSetCategories.curatedRuleSets.get | v1alpha | chronicle.rule_set.get_curated_rule_set | secops curated-rule rule-set get | +| curatedRuleSetCategories.curatedRuleSets.list | v1alpha | chronicle.rule_set.list_curated_rule_sets | secops curated-rule rule-set list | +| curatedRuleSetCategories.get | v1alpha | chronicle.rule_set.get_curated_rule_set_category | secops curated-rule rule-set-category get | +| curatedRuleSetCategories.list | v1alpha | chronicle.rule_set.list_curated_rule_set_categories | secops curated-rule rule-set-category list | +| curatedRules.get | v1alpha | chronicle.rule_set.get_curated_rule
chronicle.rule_set.get_curated_rule_by_name | secops curated-rule rule get | +| curatedRules.list | v1alpha | chronicle.rule_set.list_curated_rules | secops curated-rule rule list | +| dashboardCharts.batchGet | v1alpha | | | +| dashboardCharts.get | v1alpha | chronicle.dashboard.get_chart | secops dashboard get-chart | +| dashboardQueries.execute | v1alpha | chronicle.dashboard_query.execute_query | secops dashboard-query execute | +| dashboardQueries.get | v1alpha | chronicle.dashboard_query.get_execute_query | secops dashboard-query get | +| dashboards.copy | v1alpha | | | +| dashboards.create | v1alpha | | | +| dashboards.delete | v1alpha | | | +| dashboards.get | v1alpha | | | +| dashboards.list | v1alpha | | | +| dataAccessLabels.create | v1alpha | | | +| dataAccessLabels.delete | v1alpha | | | +| dataAccessLabels.get | v1alpha | | | +| dataAccessLabels.list | v1alpha | | | +| dataAccessLabels.patch | v1alpha | | | +| dataAccessScopes.create | v1alpha | | | +| dataAccessScopes.delete | v1alpha | | | +| dataAccessScopes.get | v1alpha | | | +| dataAccessScopes.list | v1alpha | | | +| dataAccessScopes.patch | v1alpha | | | +| dataExports.cancel | v1alpha | chronicle.data_export.cancel_data_export | secops export cancel | +| dataExports.create | v1alpha | chronicle.data_export.create_data_export | secops export create | +| dataExports.fetchavailablelogtypes | v1alpha | chronicle.data_export.fetch_available_log_types | secops export log-types | +| dataExports.get | v1alpha | chronicle.data_export.get_data_export | secops export status | +| dataExports.list | v1alpha | chronicle.data_export.list_data_export | secops export list | +| dataExports.patch | v1alpha | chronicle.data_export.update_data_export | secops export update | +| dataTableOperationErrors.get | v1alpha | | | +| dataTables.create | v1alpha | chronicle.data_table.create_data_table | secops data-table create | +| dataTables.dataTableRows.bulkCreate | v1alpha | chronicle.data_table.create_data_table_rows | secops data-table add-rows | +| dataTables.dataTableRows.bulkCreateAsync | v1alpha | | | +| dataTables.dataTableRows.bulkGet | v1alpha | | | +| dataTables.dataTableRows.bulkReplace | v1alpha | chronicle.data_table.replace_data_table_rows | secops data-table replace-rows | +| dataTables.dataTableRows.bulkReplaceAsync | v1alpha | | | +| dataTables.dataTableRows.bulkUpdate | v1alpha | chronicle.data_table.update_data_table_rows | secops data-table update-rows | +| dataTables.dataTableRows.bulkUpdateAsync | v1alpha | | | +| dataTables.dataTableRows.create | v1alpha | | | +| dataTables.dataTableRows.delete | v1alpha | chronicle.data_table.delete_data_table_rows | secops data-table delete-rows | +| dataTables.dataTableRows.get | v1alpha | | | +| dataTables.dataTableRows.list | v1alpha | chronicle.data_table.list_data_table_rows | secops data-table list-rows | +| dataTables.dataTableRows.patch | v1alpha | | | +| dataTables.delete | v1alpha | chronicle.data_table.delete_data_table | secops data-table delete | +| dataTables.get | v1alpha | chronicle.data_table.get_data_table | secops data-table get | +| dataTables.list | v1alpha | chronicle.data_table.list_data_tables | secops data-table list | +| dataTables.patch | v1alpha | | | +| dataTables.upload | v1alpha | | | +| dataTaps.create | v1alpha | | | +| dataTaps.delete | v1alpha | | | +| dataTaps.get | v1alpha | | | +| dataTaps.list | v1alpha | | | +| dataTaps.patch | v1alpha | | | +| delete | v1alpha | | | +| enrichmentControls.create | v1alpha | | | +| enrichmentControls.delete | v1alpha | | | +| enrichmentControls.get | v1alpha | | | +| enrichmentControls.list | v1alpha | | | +| entities.get | v1alpha | | | +| entities.import | v1alpha | chronicle.log_ingest.import_entities | secops entity import | +| entities.modifyEntityRiskScore | v1alpha | | | +| entities.queryEntityRiskScoreModifications | v1alpha | | | +| entityRiskScores.query | v1alpha | | | +| errorNotificationConfigs.create | v1alpha | | | +| errorNotificationConfigs.delete | v1alpha | | | +| errorNotificationConfigs.get | v1alpha | | | +| errorNotificationConfigs.list | v1alpha | | | +| errorNotificationConfigs.patch | v1alpha | | | +| events.batchGet | v1alpha | | | +| events.get | v1alpha | | | +| events.import | v1alpha | chronicle.log_ingest.ingest_udm | secops log ingest-udm | +| extractSyslog | v1alpha | | | +| federationGroups.create | v1alpha | | | +| federationGroups.delete | v1alpha | | | +| federationGroups.get | v1alpha | | | +| federationGroups.list | v1alpha | | | +| federationGroups.patch | v1alpha | | | +| feedPacks.get | v1alpha | | | +| feedPacks.list | v1alpha | | | +| feedServiceAccounts.fetchServiceAccountForCustomer | v1alpha | | | +| feedSourceTypeSchemas.list | v1alpha | | | +| feedSourceTypeSchemas.logTypeSchemas.list | v1alpha | | | +| feeds.create | v1alpha | chronicle.feeds.create_feed | secops feed create | +| feeds.delete | v1alpha | chronicle.feeds.delete_feed | secops feed delete | +| feeds.disable | v1alpha | chronicle.feeds.disable_feed | secops feed disable | +| feeds.enable | v1alpha | chronicle.feeds.enable_feed | secops feed enable | +| feeds.generateSecret | v1alpha | chronicle.feeds.generate_secret | secops feed secret | +| feeds.get | v1alpha | chronicle.feeds.get_feed | secops feed get | +| feeds.importPushLogs | v1alpha | | | +| feeds.list | v1alpha | chronicle.feeds.list_feeds | secops feed list | +| feeds.patch | v1alpha | chronicle.feeds.update_feed | secops feed update | +| feeds.scheduleTransfer | v1alpha | | | +| fetchFederationAccess | v1alpha | | | +| findEntity | v1alpha | | | +| findEntityAlerts | v1alpha | | | +| findRelatedEntities | v1alpha | | | +| findUdmFieldValues | v1alpha | | | +| findingsGraph.exploreNode | v1alpha | | | +| findingsGraph.initializeGraph | v1alpha | | | +| findingsRefinements.computeFindingsRefinementActivity | v1alpha | chronicle.rule_exclusion.compute_rule_exclusion_activity | secops rule-exclusion compute-activity | +| findingsRefinements.create | v1alpha | chronicle.rule_exclusion.create_rule_exclusion | secops rule-exclusion create | +| findingsRefinements.get | v1alpha | chronicle.rule_exclusion.get_rule_exclusion | secops rule-exclusion get | +| findingsRefinements.getDeployment | v1alpha | chronicle.rule_exclusion.get_rule_exclusion_deployment | secops rule-exclusion get-deployment | +| findingsRefinements.list | v1alpha | chronicle.rule_exclusion.list_rule_exclusions | secops rule-exclusion list | +| findingsRefinements.patch | v1alpha | chronicle.rule_exclusion.patch_rule_exclusion | secops rule-exclusion update | +| findingsRefinements.updateDeployment | v1alpha | chronicle.rule_exclusion.update_rule_exclusion_deployment | secops rule-exclusion update-deployment | +| forwarders.collectors.create | v1alpha | | | +| forwarders.collectors.delete | v1alpha | | | +| forwarders.collectors.get | v1alpha | | | +| forwarders.collectors.list | v1alpha | | | +| forwarders.collectors.patch | v1alpha | | | +| forwarders.create | v1alpha | chronicle.log_ingest.create_forwarder | secops forwarder create | +| forwarders.delete | v1alpha | chronicle.log_ingest.delete_forwarder | secops forwarder delete | +| forwarders.generateForwarderFiles | v1alpha | | | +| forwarders.get | v1alpha | chronicle.log_ingest.get_forwarder | secops forwarder get | +| forwarders.importStatsEvents | v1alpha | | | +| forwarders.list | v1alpha | chronicle.log_ingest.list_forwarder | secops forwarder list | +| forwarders.patch | v1alpha | chronicle.log_ingest.update_forwarder | secops forwarder update | +| generateCollectionAgentAuth | v1alpha | | | +| generateSoarAuthJwt | v1alpha | | | +| generateUdmKeyValueMappings | v1alpha | | | +| generateWorkspaceConnectionToken | v1alpha | | | +| get | v1alpha | | | +| getBigQueryExport | v1alpha | | | +| getMultitenantDirectory | v1alpha | | | +| getRiskConfig | v1alpha | | | +| ingestionLogLabels.get | v1alpha | | | +| ingestionLogLabels.list | v1alpha | | | +| ingestionLogNamespaces.get | v1alpha | | | +| ingestionLogNamespaces.list | v1alpha | | | +| integrations.create | v1alpha | | | +| integrations.delete | v1alpha | chronicle.integration.integrations.delete_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations delete | +| integrations.download | v1alpha | chronicle.integration.integrations.download_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations download | +| integrations.downloadDependency | v1alpha | chronicle.integration.integrations.download_integration_dependency(api_version=APIVersion.V1ALPHA) | secops integration integrations download-dependency | +| integrations.exportIntegrationItems | v1alpha | chronicle.integration.integrations.export_integration_items(api_version=APIVersion.V1ALPHA) | secops integration integrations export-items | +| integrations.fetchAffectedItems | v1alpha | chronicle.integration.integrations.get_integration_affected_items(api_version=APIVersion.V1ALPHA) | secops integration integrations get-affected-items | +| integrations.fetchAgentIntegrations | v1alpha | chronicle.integration.integrations.get_agent_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations get-agent | +| integrations.fetchCommercialDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA) | secops integration integrations get-diff | +| integrations.fetchDependencies | v1alpha | chronicle.integration.integrations.get_integration_dependencies(api_version=APIVersion.V1ALPHA) | secops integration integrations get-dependencies | +| integrations.fetchRestrictedAgents | v1alpha | chronicle.integration.integrations.get_integration_restricted_agents(api_version=APIVersion.V1ALPHA) | secops integration integrations get-restricted-agents | +| integrations.get | v1alpha | chronicle.integration.integrations.get_integration(api_version=APIVersion.V1ALPHA) | secops integration integrations get | +| integrations.getFetchProductionDiff | v1alpha | chronicle.integration.integrations.get_integration_diff(api_version=APIVersion.V1ALPHA, diff_type=DiffType.PRODUCTION) | secops integration integrations get-diff | +| integrations.getFetchStagingDiff | v1alpha | chronicle.integration.integrations.get_integration_diffapi_version=APIVersion.V1ALPHA, (diff_type=DiffType.STAGING) | secops integration integrations get-diff | +| integrations.import | v1alpha | | | +| integrations.importIntegrationDependency | v1alpha | | | +| integrations.importIntegrationItems | v1alpha | | | +| integrations.list | v1alpha | chronicle.integration.integrations.list_integrations(api_version=APIVersion.V1ALPHA) | secops integration integrations list | +| integrations.patch | v1alpha | | | +| integrations.pushToProduction | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.PRODUCTION) | secops integration integrations transition | +| integrations.pushToStaging | v1alpha | chronicle.integration.integrations.transition_integration(api_version=APIVersion.V1ALPHA, target_mode=TargetMode.STAGING) | secops integration integrations transition | +| integrations.updateCustomIntegration | v1alpha | | | +| integrations.upload | v1alpha | | | +| integrations.actions.create | v1alpha | chronicle.integration.actions.create_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions create | +| integrations.actions.delete | v1alpha | chronicle.integration.actions.delete_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions delete | +| integrations.actions.executeTest | v1alpha | chronicle.integration.actions.execute_integration_action_test(api_version=APIVersion.V1ALPHA) | secops integration actions test | +| integrations.actions.fetchActionsByEnvironment | v1alpha | chronicle.integration.actions.get_integration_actions_by_environment(api_version=APIVersion.V1ALPHA) | | +| integrations.actions.fetchTemplate | v1alpha | chronicle.integration.actions.get_integration_action_template(api_version=APIVersion.V1ALPHA) | secops integration actions template | +| integrations.actions.get | v1alpha | chronicle.integration.actions.get_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions get | +| integrations.actions.list | v1alpha | chronicle.integration.actions.list_integration_actions(api_version=APIVersion.V1ALPHA) | secops integration actions list | +| integrations.actions.patch | v1alpha | chronicle.integration.actions.update_integration_action(api_version=APIVersion.V1ALPHA) | secops integration actions update | +| integrations.actions.revisions.create | v1alpha | chronicle.integration.action_revisions.create_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions create | +| integrations.actions.revisions.delete | v1alpha | chronicle.integration.action_revisions.delete_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions delete | +| integrations.actions.revisions.list | v1alpha | chronicle.integration.action_revisions.list_integration_action_revisions(api_version=APIVersion.V1ALPHA) | secops integration action-revisions list | +| integrations.actions.revisions.rollback | v1alpha | chronicle.integration.action_revisions.rollback_integration_action_revision(api_version=APIVersion.V1ALPHA) | secops integration action-revisions rollback | +| integrations.connectors.create | v1alpha | chronicle.integration.connectors.create_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors create | +| integrations.connectors.delete | v1alpha | chronicle.integration.connectors.delete_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors delete | +| integrations.connectors.executeTest | v1alpha | chronicle.integration.connectors.execute_integration_connector_test(api_version=APIVersion.V1ALPHA) | secops integration connectors test | +| integrations.connectors.fetchTemplate | v1alpha | chronicle.integration.connectors.get_integration_connector_template(api_version=APIVersion.V1ALPHA) | secops integration connectors template | +| integrations.connectors.get | v1alpha | chronicle.integration.connectors.get_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors get | +| integrations.connectors.list | v1alpha | chronicle.integration.connectors.list_integration_connectors(api_version=APIVersion.V1ALPHA) | secops integration connectors list | +| integrations.connectors.patch | v1alpha | chronicle.integration.connectors.update_integration_connector(api_version=APIVersion.V1ALPHA) | secops integration connectors update | +| integrations.connectors.revisions.create | v1alpha | chronicle.integration.connector_revisions.create_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions create | +| integrations.connectors.revisions.delete | v1alpha | chronicle.integration.connector_revisions.delete_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions delete | +| integrations.connectors.revisions.list | v1alpha | chronicle.integration.connector_revisions.list_integration_connector_revisions(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions list | +| integrations.connectors.revisions.rollback | v1alpha | chronicle.integration.connector_revisions.rollback_integration_connector_revision(api_version=APIVersion.V1ALPHA) | secops integration connector-revisions rollback| +| integrations.connectors.contextProperties.clearAll | v1alpha | chronicle.integration.connector_context_properties.delete_all_connector_context_properties(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties delete-all | +| integrations.connectors.contextProperties.create | v1alpha | chronicle.integration.connector_context_properties.create_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties create | +| integrations.connectors.contextProperties.delete | v1alpha | chronicle.integration.connector_context_properties.delete_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties delete | +| integrations.connectors.contextProperties.get | v1alpha | chronicle.integration.connector_context_properties.get_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties get | +| integrations.connectors.contextProperties.list | v1alpha | chronicle.integration.connector_context_properties.list_connector_context_properties(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties list | +| integrations.connectors.contextProperties.patch | v1alpha | chronicle.integration.connector_context_properties.update_connector_context_property(api_version=APIVersion.V1ALPHA) | secops integration connector-context-properties update | +| integrations.connectors.connectorInstances.logs.get | v1alpha | chronicle.integration.connector_instance_logs.get_connector_instance_log(api_version=APIVersion.V1ALPHA) | secops integration connector-instance-logs get | +| integrations.connectors.connectorInstances.logs.list | v1alpha | chronicle.integration.connector_instance_logs.list_connector_instance_logs(api_version=APIVersion.V1ALPHA) | secops integration connector-instance-logs list| +| integrations.connectors.connectorInstances.create | v1alpha | chronicle.integration.connector_instances.create_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances create | +| integrations.connectors.connectorInstances.delete | v1alpha | chronicle.integration.connector_instances.delete_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances delete | +| integrations.connectors.connectorInstances.fetchLatestDefinition | v1alpha | chronicle.integration.connector_instances.get_connector_instance_latest_definition(api_version=APIVersion.V1ALPHA) | secops integration connector-instances get-latest-definition | +| integrations.connectors.connectorInstances.get | v1alpha | chronicle.integration.connector_instances.get_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances get | +| integrations.connectors.connectorInstances.list | v1alpha | chronicle.integration.connector_instances.list_connector_instances(api_version=APIVersion.V1ALPHA) | secops integration connector-instances list | +| integrations.connectors.connectorInstances.patch | v1alpha | chronicle.integration.connector_instances.update_connector_instance(api_version=APIVersion.V1ALPHA) | secops integration connector-instances update | +| integrations.connectors.connectorInstances.runOnDemand | v1alpha | chronicle.integration.connector_instances.run_connector_instance_on_demand(api_version=APIVersion.V1ALPHA) | secops integration connector-instances run-on-demand | +| integrations.connectors.connectorInstances.setLogsCollection | v1alpha | chronicle.integration.connector_instances.set_connector_instance_logs_collection(api_version=APIVersion.V1ALPHA) | secops integration connector-instances set-logs-collection | +| integrations.integrationInstances.create | v1alpha | chronicle.integration.integration_instances.create_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances create | +| integrations.integrationInstances.delete | v1alpha | chronicle.integration.integration_instances.delete_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances delete | +| integrations.integrationInstances.executeTest | v1alpha | chronicle.integration.integration_instances.execute_integration_instance_test(api_version=APIVersion.V1ALPHA) | secops integration instances test | +| integrations.integrationInstances.fetchAffectedItems | v1alpha | chronicle.integration.integration_instances.get_integration_instance_affected_items(api_version=APIVersion.V1ALPHA) | secops integration instances get-affected-items| +| integrations.integrationInstances.fetchDefaultInstance | v1alpha | chronicle.integration.integration_instances.get_default_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get-default | +| integrations.integrationInstances.get | v1alpha | chronicle.integration.integration_instances.get_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances get | +| integrations.integrationInstances.list | v1alpha | chronicle.integration.integration_instances.list_integration_instances(api_version=APIVersion.V1ALPHA) | secops integration instances list | +| integrations.integrationInstances.patch | v1alpha | chronicle.integration.integration_instances.update_integration_instance(api_version=APIVersion.V1ALPHA) | secops integration instances update | +| integrations.transformers.create | v1alpha | chronicle.integration.transformers.create_integration_transformer | secops integration transformers create | +| integrations.transformers.delete | v1alpha | chronicle.integration.transformers.delete_integration_transformer | secops integration transformers delete | +| integrations.transformers.executeTest | v1alpha | chronicle.integration.transformers.execute_integration_transformer_test | secops integration transformers test | +| integrations.transformers.fetchTemplate | v1alpha | chronicle.integration.transformers.get_integration_transformer_template | secops integration transformers template | +| integrations.transformers.get | v1alpha | chronicle.integration.transformers.get_integration_transformer | secops integration transformers get | +| integrations.transformers.list | v1alpha | chronicle.integration.transformers.list_integration_transformers | secops integration transformers list | +| integrations.transformers.patch | v1alpha | chronicle.integration.transformers.update_integration_transformer | secops integration transformers update | +| integrations.transformers.revisions.create | v1alpha | chronicle.integration.transformer_revisions.create_integration_transformer_revision | secops integration transformer-revisions create| +| integrations.transformers.revisions.delete | v1alpha | chronicle.integration.transformer_revisions.delete_integration_transformer_revision | secops integration transformer-revisions delete| +| integrations.transformers.revisions.list | v1alpha | chronicle.integration.transformer_revisions.list_integration_transformer_revisions | secops integration transformer-revisions list | +| integrations.transformers.revisions.rollback | v1alpha | chronicle.integration.transformer_revisions.rollback_integration_transformer_revision | secops integration transformer-revisions rollback| +| integrations.logicalOperators.create | v1alpha | chronicle.integration.logical_operators.create_integration_logical_operator | secops integration logical-operators create | +| integrations.logicalOperators.delete | v1alpha | chronicle.integration.logical_operators.delete_integration_logical_operator | secops integration logical-operators delete | +| integrations.logicalOperators.executeTest | v1alpha | chronicle.integration.logical_operators.execute_integration_logical_operator_test | secops integration logical-operators test | +| integrations.logicalOperators.fetchTemplate | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator_template | secops integration logical-operators template | +| integrations.logicalOperators.get | v1alpha | chronicle.integration.logical_operators.get_integration_logical_operator | secops integration logical-operators get | +| integrations.logicalOperators.list | v1alpha | chronicle.integration.logical_operators.list_integration_logical_operators | secops integration logical-operators list | +| integrations.logicalOperators.patch | v1alpha | chronicle.integration.logical_operators.update_integration_logical_operator | secops integration logical-operators update | +| integrations.logicalOperators.revisions.create | v1alpha | chronicle.integration.logical_operator_revisions.create_integration_logical_operator_revision | secops integration logical-operator-revisions create | +| integrations.logicalOperators.revisions.delete | v1alpha | chronicle.integration.logical_operator_revisions.delete_integration_logical_operator_revision | secops integration logical-operator-revisions delete | +| integrations.logicalOperators.revisions.list | v1alpha | chronicle.integration.logical_operator_revisions.list_integration_logical_operator_revisions | secops integration logical-operator-revisions list | +| integrations.logicalOperators.revisions.rollback | v1alpha | chronicle.integration.logical_operator_revisions.rollback_integration_logical_operator_revision | secops integration logical-operator-revisions rollback | +| integrations.jobs.create | v1alpha | chronicle.integration.jobs.create_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs create | +| integrations.jobs.delete | v1alpha | chronicle.integration.jobs.delete_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs delete | +| integrations.jobs.executeTest | v1alpha | chronicle.integration.jobs.execute_integration_job_test(api_version=APIVersion.V1ALPHA) | secops integration jobs test | +| integrations.jobs.fetchTemplate | v1alpha | chronicle.integration.jobs.get_integration_job_template(api_version=APIVersion.V1ALPHA) | secops integration jobs template | +| integrations.jobs.get | v1alpha | chronicle.integration.jobs.get_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs get | +| integrations.jobs.list | v1alpha | chronicle.integration.jobs.list_integration_jobs(api_version=APIVersion.V1ALPHA) | secops integration jobs list | +| integrations.jobs.patch | v1alpha | chronicle.integration.jobs.update_integration_job(api_version=APIVersion.V1ALPHA) | secops integration jobs update | +| integrations.managers.create | v1alpha | chronicle.integration.managers.create_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers create | +| integrations.managers.delete | v1alpha | chronicle.integration.managers.delete_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers delete | +| integrations.managers.fetchTemplate | v1alpha | chronicle.integration.managers.get_integration_manager_template(api_version=APIVersion.V1ALPHA) | secops integration managers template | +| integrations.managers.get | v1alpha | chronicle.integration.managers.get_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers get | +| integrations.managers.list | v1alpha | chronicle.integration.managers.list_integration_managers(api_version=APIVersion.V1ALPHA) | secops integration managers list | +| integrations.managers.patch | v1alpha | chronicle.integration.managers.update_integration_manager(api_version=APIVersion.V1ALPHA) | secops integration managers update | +| integrations.managers.revisions.create | v1alpha | chronicle.integration.manager_revisions.create_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions create | +| integrations.managers.revisions.delete | v1alpha | chronicle.integration.manager_revisions.delete_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions delete | +| integrations.managers.revisions.get | v1alpha | chronicle.integration.manager_revisions.get_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions get | +| integrations.managers.revisions.list | v1alpha | chronicle.integration.manager_revisions.list_integration_manager_revisions(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions list | +| integrations.managers.revisions.rollback | v1alpha | chronicle.integration.manager_revisions.rollback_integration_manager_revision(api_version=APIVersion.V1ALPHA) | secops integration manager-revisions rollback | +| integrations.jobs.revisions.create | v1alpha | chronicle.integration.job_revisions.create_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions create | +| integrations.jobs.revisions.delete | v1alpha | chronicle.integration.job_revisions.delete_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions delete | +| integrations.jobs.revisions.list | v1alpha | chronicle.integration.job_revisions.list_integration_job_revisions(api_version=APIVersion.V1ALPHA) | secops integration job-revisions list | +| integrations.jobs.revisions.rollback | v1alpha | chronicle.integration.job_revisions.rollback_integration_job_revision(api_version=APIVersion.V1ALPHA) | secops integration job-revisions rollback | +| integrations.jobs.jobInstances.create | v1alpha | chronicle.integration.job_instances.create_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances create | +| integrations.jobs.jobInstances.delete | v1alpha | chronicle.integration.job_instances.delete_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances delete | +| integrations.jobs.jobInstances.get | v1alpha | chronicle.integration.job_instances.get_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances get | +| integrations.jobs.jobInstances.list | v1alpha | chronicle.integration.job_instances.list_integration_job_instances(api_version=APIVersion.V1ALPHA) | secops integration job-instances list | +| integrations.jobs.jobInstances.patch | v1alpha | chronicle.integration.job_instances.update_integration_job_instance(api_version=APIVersion.V1ALPHA) | secops integration job-instances update | +| integrations.jobs.jobInstances.runOnDemand | v1alpha | chronicle.integration.job_instances.run_integration_job_instance_on_demand(api_version=APIVersion.V1ALPHA) | secops integration job-instances run-on-demand | +| integrations.jobs.contextProperties.clearAll | v1alpha | chronicle.integration.job_context_properties.delete_all_job_context_properties(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties delete-all | +| integrations.jobs.contextProperties.create | v1alpha | chronicle.integration.job_context_properties.create_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties create | +| integrations.jobs.contextProperties.delete | v1alpha | chronicle.integration.job_context_properties.delete_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties delete | +| integrations.jobs.contextProperties.get | v1alpha | chronicle.integration.job_context_properties.get_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties get | +| integrations.jobs.contextProperties.list | v1alpha | chronicle.integration.job_context_properties.list_job_context_properties(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties list | +| integrations.jobs.contextProperties.patch | v1alpha | chronicle.integration.job_context_properties.update_job_context_property(api_version=APIVersion.V1ALPHA) | secops integration job-context-properties update | +| integrations.jobs.jobInstances.logs.get | v1alpha | chronicle.integration.job_instance_logs.get_job_instance_log(api_version=APIVersion.V1ALPHA) | secops integration job-instance-logs get | +| integrations.jobs.jobInstances.logs.list | v1alpha | chronicle.integration.job_instance_logs.list_job_instance_logs(api_version=APIVersion.V1ALPHA) | secops integration job-instance-logs list | +| investigations.fetchAssociated | v1alpha | chronicle.investigations.fetch_associated_investigations | secops investigation fetch-associated | +| investigations.get | v1alpha | chronicle.investigations.get_investigation | secops investigation get | +| investigations.list | v1alpha | chronicle.investigations.list_investigations | secops investigation list | +| investigations.trigger | v1alpha | chronicle.investigations.trigger_investigation | secops investigation trigger | +| iocs.batchGet | v1alpha | | | +| iocs.findFirstAndLastSeen | v1alpha | | | +| iocs.get | v1alpha | | | +| iocs.getIocState | v1alpha | | | +| iocs.searchCuratedDetectionsForIoc | v1alpha | | | +| iocs.updateIocState | v1alpha | | | +| legacy.legacyBatchGetCases | v1alpha | chronicle.case.get_cases_from_list | secops case | +| legacy.legacyBatchGetCollections | v1alpha | | | +| legacy.legacyCreateOrUpdateCase | v1alpha | | | +| legacy.legacyCreateSoarAlert | v1alpha | | | +| legacy.legacyFetchAlertsView | v1alpha | chronicle.alert.get_alerts | secops alert | +| legacy.legacyFetchUdmSearchCsv | v1alpha | chronicle.udm_search.fetch_udm_search_csv | secops search --csv | +| legacy.legacyFetchUdmSearchView | v1alpha | chronicle.udm_search.fetch_udm_search_view | secops udm-search-view | +| legacy.legacyFindAssetEvents | v1alpha | | | +| legacy.legacyFindRawLogs | v1alpha | | | +| legacy.legacyFindUdmEvents | v1alpha | | | +| legacy.legacyGetAlert | v1alpha | chronicle.rule_alert.get_alert | | +| legacy.legacyGetCuratedRulesTrends | v1alpha | | | +| legacy.legacyGetDetection | v1alpha | | | +| legacy.legacyGetEventForDetection | v1alpha | | | +| legacy.legacyGetRuleCounts | v1alpha | | | +| legacy.legacyGetRulesTrends | v1alpha | | | +| legacy.legacyListCases | v1alpha | chronicle.case.get_cases | secops case --ids | +| legacy.legacyRunTestRule | v1alpha | chronicle.rule.run_rule_test | secops rule validate | +| legacy.legacySearchArtifactEvents | v1alpha | | | +| legacy.legacySearchArtifactIoCDetails | v1alpha | | | +| legacy.legacySearchAssetEvents | v1alpha | | | +| legacy.legacySearchCuratedDetections | v1alpha | | | +| legacy.legacySearchCustomerStats | v1alpha | | | +| legacy.legacySearchDetections | v1alpha | chronicle.rule_detection.list_detections | | +| legacy.legacySearchDomainsRecentlyRegistered | v1alpha | | | +| legacy.legacySearchDomainsTimingStats | v1alpha | | | +| legacy.legacySearchEnterpriseWideAlerts | v1alpha | | | +| legacy.legacySearchEnterpriseWideIoCs | v1alpha | chronicle.ioc.list_iocs | secops iocs | +| legacy.legacySearchFindings | v1alpha | | | +| legacy.legacySearchIngestionStats | v1alpha | | | +| legacy.legacySearchIoCInsights | v1alpha | | | +| legacy.legacySearchRawLogs | v1alpha | | | +| legacy.legacySearchRuleDetectionCountBuckets | v1alpha | | | +| legacy.legacySearchRuleDetectionEvents | v1alpha | | | +| legacy.legacySearchRuleResults | v1alpha | | | +| legacy.legacySearchRulesAlerts | v1alpha | chronicle.rule_alert.search_rule_alerts | | +| legacy.legacySearchUserEvents | v1alpha | | | +| legacy.legacyStreamDetectionAlerts | v1alpha | | | +| legacy.legacyTestRuleStreaming | v1alpha | | | +| legacy.legacyUpdateAlert | v1alpha | chronicle.rule_alert.update_alert | | +| listAllFindingsRefinementDeployments | v1alpha | | | +| logProcessingPipelines.associateStreams | v1alpha | chronicle.log_processing_pipelines.associate_streams | secops log-processing associate-streams | +| logProcessingPipelines.create | v1alpha | chronicle.log_processing_pipelines.create_log_processing_pipeline | secops log-processing create | +| logProcessingPipelines.delete | v1alpha | chronicle.log_processing_pipelines.delete_log_processing_pipeline | secops log-processing delete | +| logProcessingPipelines.dissociateStreams | v1alpha | chronicle.log_processing_pipelines.dissociate_streams | secops log-processing dissociate-streams | +| logProcessingPipelines.fetchAssociatedPipeline | v1alpha | chronicle.log_processing_pipelines.fetch_associated_pipeline | secops log-processing fetch-associated | +| logProcessingPipelines.fetchSampleLogsByStreams | v1alpha | chronicle.log_processing_pipelines.fetch_sample_logs_by_streams | secops log-processing fetch-sample-logs | +| logProcessingPipelines.get | v1alpha | chronicle.log_processing_pipelines.get_log_processing_pipeline | secops log-processing get | +| logProcessingPipelines.list | v1alpha | chronicle.log_processing_pipelines.list_log_processing_pipelines | secops log-processing list | +| logProcessingPipelines.patch | v1alpha | chronicle.log_processing_pipelines.update_log_processing_pipeline | secops log-processing update | +| logProcessingPipelines.testPipeline | v1alpha | chronicle.log_processing_pipelines.test_pipeline | secops log-processing test | +| logTypes.create | v1alpha | | | +| logTypes.generateEventTypesSuggestions | v1alpha | | | +| logTypes.get | v1alpha | | | +| logTypes.getLogTypeSetting | v1alpha | | | +| logTypes.legacySubmitParserExtension | v1alpha | | | +| logTypes.list | v1alpha | | | +| logTypes.logs.export | v1alpha | | | +| logTypes.logs.get | v1alpha | | | +| logTypes.logs.import | v1alpha | chronicle.log_ingest.ingest_log | secops log ingest | +| logTypes.logs.list | v1alpha | | | +| logTypes.parserExtensions.activate | v1alpha | chronicle.parser_extension.activate_parser_extension | secops parser-extension activate | +| logTypes.parserExtensions.create | v1alpha | chronicle.parser_extension.create_parser_extension | secops parser-extension create | +| logTypes.parserExtensions.delete | v1alpha | chronicle.parser_extension.delete_parser_extension | secops parser-extension delete | +| logTypes.parserExtensions.extensionValidationReports.get | v1alpha | | | +| logTypes.parserExtensions.extensionValidationReports.list | v1alpha | | | +| logTypes.parserExtensions.extensionValidationReports.validationErrors.list | v1alpha | | | +| logTypes.parserExtensions.get | v1alpha | chronicle.parser_extension.get_parser_extension | secops parser-extension get | +| logTypes.parserExtensions.list | v1alpha | chronicle.parser_extension.list_parser_extensions | secops parser-extension list | +| logTypes.parserExtensions.validationReports.get | v1alpha | | | +| logTypes.parserExtensions.validationReports.parsingErrors.list | v1alpha | | | +| logTypes.parsers.activate | v1alpha | chronicle.parser.activate_parser | secops parser activate | +| logTypes.parsers.activateReleaseCandidateParser | v1alpha | chronicle.parser.activate_release_candidate | secops parser activate-rc | +| logTypes.parsers.copy | v1alpha | chronicle.parser.copy_parser | secops parser copy | +| logTypes.parsers.create | v1alpha | chronicle.parser.create_parser | secops parser create | +| logTypes.parsers.deactivate | v1alpha | chronicle.parser.deactivate_parser | secops parser deactivate | +| logTypes.parsers.delete | v1alpha | chronicle.parser.delete_parser | secops parser delete | +| logTypes.parsers.get | v1alpha | chronicle.parser.get_parser | secops parser get | +| logTypes.parsers.list | v1alpha | chronicle.parser.list_parsers | secops parser list | +| logTypes.parsers.validationReports.get | v1alpha | | | +| logTypes.parsers.validationReports.parsingErrors.list | v1alpha | | | +| logTypes.patch | v1alpha | | | +| logTypes.runParser | v1alpha | chronicle.parser.run_parser | secops parser run | +| logTypes.updateLogTypeSetting | v1alpha | | | +| logs.classify | v1alpha | chronicle.log_types.classify_logs | secops log classify | +| marketplaceIntegrations.get | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace get | +| marketplaceIntegrations.getDiff | v1alpha | chronicle.marketplace_integrations.get_marketplace_integration_diff(api_version=APIVersion.V1ALPHA) | secops integration marketplace diff | +| marketplaceIntegrations.install | v1alpha | chronicle.marketplace_integrations.install_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace install | +| marketplaceIntegrations.list | v1alpha | chronicle.marketplace_integrations.list_marketplace_integrations(api_version=APIVersion.V1ALPHA) | secops integration marketplace list | +| marketplaceIntegrations.uninstall | v1alpha | chronicle.marketplace_integrations.uninstall_marketplace_integration(api_version=APIVersion.V1ALPHA) | secops integration marketplace uninstall | +| nativeDashboards.addChart | v1alpha | chronicle.dashboard.add_chart | secops dashboard add-chart | +| nativeDashboards.create | v1alpha | chronicle.dashboard.create_dashboard | secops dashboard create | +| nativeDashboards.delete | v1alpha | chronicle.dashboard.delete_dashboard | secops dashboard delete | +| nativeDashboards.duplicate | v1alpha | chronicle.dashboard.duplicate_dashboard | secops dashboard duplicate | +| nativeDashboards.duplicateChart | v1alpha | | | +| nativeDashboards.editChart | v1alpha | chronicle.dashboard.edit_chart | secops dashboard edit-chart | +| nativeDashboards.export | v1alpha | chronicle.dashboard.export_dashboard | secops dashboard export | +| nativeDashboards.get | v1alpha | chronicle.dashboard.get_dashboard | secops dashboard get | +| nativeDashboards.import | v1alpha | chronicle.dashboard.import_dashboard | secops dashboard import | +| nativeDashboards.list | v1alpha | chronicle.dashboard.list_dashboards | secops dashboard list | +| nativeDashboards.patch | v1alpha | chronicle.dashboard.update_dashboard | secops dashboard update | +| nativeDashboards.removeChart | v1alpha | chronicle.dashboard.remove_chart | secops dashboard remove-chart | +| operations.cancel | v1alpha | | | +| operations.delete | v1alpha | | | +| operations.get | v1alpha | | | +| operations.list | v1alpha | | | +| operations.streamSearch | v1alpha | | | +| queryProductSourceStats | v1alpha | | | +| referenceLists.create | v1alpha | | | +| referenceLists.get | v1alpha | | | +| referenceLists.list | v1alpha | | | +| referenceLists.patch | v1alpha | | | +| report | v1alpha | | | +| ruleExecutionErrors.list | v1alpha | chronicle.rule_detection.list_errors | | +| rules.create | v1alpha | | | +| rules.delete | v1alpha | | | +| rules.deployments.list | v1alpha | | | +| rules.get | v1alpha | | | +| rules.getDeployment | v1alpha | | | +| rules.list | v1alpha | | | +| rules.listRevisions | v1alpha | | | +| rules.patch | v1alpha | | | +| rules.retrohunts.create | v1alpha | | | +| rules.retrohunts.get | v1alpha | | | +| rules.retrohunts.list | v1alpha | | | +| rules.updateDeployment | v1alpha | | | +| searchEntities | v1alpha | | | +| searchRawLogs | v1alpha | | | +| summarizeEntitiesFromQuery | v1alpha | chronicle.entity.summarize_entity | secops entity | +| summarizeEntity | v1alpha | chronicle.entity.summarize_entity | | +| testFindingsRefinement | v1alpha | | | +| translateUdmQuery | v1alpha | chronicle.nl_search.translate_nl_to_udm | | +| translateYlRule | v1alpha | | | +| udmSearch | v1alpha | chronicle.search.search_udm | secops search | +| undelete | v1alpha | | | +| updateBigQueryExport | v1alpha | | | +| updateRiskConfig | v1alpha | | | +| users.clearConversationHistory | v1alpha | | | +| users.conversations.create | v1alpha | chronicle.gemini.create_conversation | | +| users.conversations.delete | v1alpha | | | +| users.conversations.get | v1alpha | | | +| users.conversations.list | v1alpha | | | +| users.conversations.messages.create | v1alpha | chronicle.gemini.query_gemini | secops gemini | +| users.conversations.messages.delete | v1alpha | | | +| users.conversations.messages.get | v1alpha | | | +| users.conversations.messages.list | v1alpha | | | +| users.conversations.messages.patch | v1alpha | | | +| users.conversations.patch | v1alpha | | | +| users.getPreferenceSet | v1alpha | chronicle.gemini.opt_in_to_gemini | secops gemini --opt-in | +| users.searchQueries.create | v1alpha | | | +| users.searchQueries.delete | v1alpha | | | +| users.searchQueries.get | v1alpha | | | +| users.searchQueries.list | v1alpha | | | +| users.searchQueries.patch | v1alpha | | | +| users.updatePreferenceSet | v1alpha | | | +| validateQuery | v1alpha | chronicle.validate.validate_query | | +| verifyReferenceList | v1alpha | | | +| verifyRuleText | v1alpha | chronicle.rule_validation.validate_rule | secops rule validate | +| watchlists.create | v1alpha | | | +| watchlists.delete | v1alpha | | | +| watchlists.entities.add | v1alpha | | | +| watchlists.entities.batchAdd | v1alpha | | | +| watchlists.entities.batchRemove | v1alpha | | | +| watchlists.entities.remove | v1alpha | | | +| watchlists.get | v1alpha | | | +| watchlists.list | v1alpha | | | +| watchlists.listEntities | v1alpha | | | +| watchlists.patch | v1alpha | | | | REST Resource | Version | secops-wrapper module | CLI Command | |--------------------------------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------| | dataAccessLabels.create | v1 | | | diff --git a/src/secops/chronicle/__init__.py b/src/secops/chronicle/__init__.py index 15320ac9..d7488872 100644 --- a/src/secops/chronicle/__init__.py +++ b/src/secops/chronicle/__init__.py @@ -98,27 +98,43 @@ search_log_types, ) from secops.chronicle.models import ( + AdvancedConfig, AlertCount, AlertState, Case, CaseList, + DailyScheduleDetails, DataExport, DataExportStage, DataExportStatus, + Date, + DayOfWeek, DetectionType, + DiffType, Entity, EntityMetadata, EntityMetrics, EntitySummary, FileMetadataAndProperties, InputInterval, + IntegrationJobInstanceParameter, + IntegrationParam, + IntegrationParamType, + IntegrationType, ListBasis, + MonthlyScheduleDetails, + OneTimeScheduleDetails, PrevalenceData, + PythonVersion, + ScheduleType, SoarPlatformInfo, + TargetMode, TileType, TimeInterval, Timeline, TimelineBucket, + TimeOfDay, + WeeklyScheduleDetails, WidgetMetadata, ) from secops.chronicle.nl_search import translate_nl_to_udm @@ -198,6 +214,43 @@ create_watchlist, update_watchlist, ) +from secops.chronicle.integration.connectors import ( + list_integration_connectors, + get_integration_connector, + delete_integration_connector, + create_integration_connector, + update_integration_connector, + execute_integration_connector_test, + get_integration_connector_template, +) +from secops.chronicle.integration.connector_revisions import ( + list_integration_connector_revisions, + delete_integration_connector_revision, + create_integration_connector_revision, + rollback_integration_connector_revision, +) +from secops.chronicle.integration.connector_context_properties import ( + list_connector_context_properties, + get_connector_context_property, + delete_connector_context_property, + create_connector_context_property, + update_connector_context_property, + delete_all_connector_context_properties, +) +from secops.chronicle.integration.connector_instance_logs import ( + list_connector_instance_logs, + get_connector_instance_log, +) +from secops.chronicle.integration.connector_instances import ( + list_connector_instances, + get_connector_instance, + delete_connector_instance, + create_connector_instance, + update_connector_instance, + get_connector_instance_latest_definition, + set_connector_instance_logs_collection, + run_connector_instance_on_demand, +) __all__ = [ # Client @@ -315,21 +368,31 @@ "execute_query", "get_execute_query", # Models + "AdvancedConfig", + "AlertCount", + "AlertState", + "Case", + "CaseList", + "DailyScheduleDetails", + "Date", + "DayOfWeek", "Entity", "EntityMetadata", "EntityMetrics", + "EntitySummary", + "FileMetadataAndProperties", + "IntegrationJobInstanceParameter", + "MonthlyScheduleDetails", + "OneTimeScheduleDetails", + "PrevalenceData", + "ScheduleType", + "SoarPlatformInfo", "TimeInterval", - "TimelineBucket", "Timeline", + "TimelineBucket", + "TimeOfDay", + "WeeklyScheduleDetails", "WidgetMetadata", - "EntitySummary", - "AlertCount", - "AlertState", - "Case", - "SoarPlatformInfo", - "CaseList", - "PrevalenceData", - "FileMetadataAndProperties", "ValidationResult", "GeminiResponse", "Block", @@ -367,4 +430,37 @@ "delete_watchlist", "create_watchlist", "update_watchlist", + # Integration Connectors + "list_integration_connectors", + "get_integration_connector", + "delete_integration_connector", + "create_integration_connector", + "update_integration_connector", + "execute_integration_connector_test", + "get_integration_connector_template", + # Integration Connector Revisions + "list_integration_connector_revisions", + "delete_integration_connector_revision", + "create_integration_connector_revision", + "rollback_integration_connector_revision", + # Connector Context Properties + "list_connector_context_properties", + "get_connector_context_property", + "delete_connector_context_property", + "create_connector_context_property", + "update_connector_context_property", + "delete_all_connector_context_properties", + # Connector Instance Logs + "list_connector_instance_logs", + "get_connector_instance_log", + # Connector Instances + "list_connector_instances", + "get_connector_instance", + "delete_connector_instance", + "create_connector_instance", + "update_connector_instance", + "get_connector_instance_latest_definition", + "set_connector_instance_logs_collection", + "run_connector_instance_on_demand", + ] diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 9b892272..987ab2a1 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -13,6 +13,7 @@ # limitations under the License. # """Chronicle API client.""" + import ipaddress import re from collections.abc import Iterator @@ -22,246 +23,226 @@ from google.auth.transport import requests as google_auth_requests +# pylint: disable=line-too-long from secops import auth as secops_auth from secops.auth import RetryConfig from secops.chronicle.alert import get_alerts as _get_alerts from secops.chronicle.case import get_cases_from_list -from secops.chronicle.dashboard import DashboardAccessType, DashboardView -from secops.chronicle.dashboard import add_chart as _add_chart -from secops.chronicle.dashboard import create_dashboard as _create_dashboard -from secops.chronicle.dashboard import delete_dashboard as _delete_dashboard from secops.chronicle.dashboard import ( + DashboardAccessType, + DashboardView, + add_chart as _add_chart, + create_dashboard as _create_dashboard, + delete_dashboard as _delete_dashboard, duplicate_dashboard as _duplicate_dashboard, + edit_chart as _edit_chart, + export_dashboard as _export_dashboard, + get_chart as _get_chart, + get_dashboard as _get_dashboard, + import_dashboard as _import_dashboard, + list_dashboards as _list_dashboards, + remove_chart as _remove_chart, + update_dashboard as _update_dashboard, ) -from secops.chronicle.dashboard import edit_chart as _edit_chart -from secops.chronicle.dashboard import export_dashboard as _export_dashboard -from secops.chronicle.dashboard import get_chart as _get_chart -from secops.chronicle.dashboard import get_dashboard as _get_dashboard -from secops.chronicle.dashboard import import_dashboard as _import_dashboard -from secops.chronicle.dashboard import list_dashboards as _list_dashboards -from secops.chronicle.dashboard import remove_chart as _remove_chart -from secops.chronicle.dashboard import update_dashboard as _update_dashboard from secops.chronicle.dashboard_query import ( execute_query as _execute_dashboard_query, -) -from secops.chronicle.dashboard_query import ( get_execute_query as _get_execute_query, ) from secops.chronicle.data_export import ( cancel_data_export as _cancel_data_export, -) -from secops.chronicle.data_export import ( create_data_export as _create_data_export, -) -from secops.chronicle.data_export import ( fetch_available_log_types as _fetch_available_log_types, -) -from secops.chronicle.data_export import get_data_export as _get_data_export -from secops.chronicle.data_export import list_data_export as _list_data_export -from secops.chronicle.data_export import ( + get_data_export as _get_data_export, + list_data_export as _list_data_export, update_data_export as _update_data_export, ) -from secops.chronicle.data_table import DataTableColumnType -from secops.chronicle.data_table import create_data_table as _create_data_table from secops.chronicle.data_table import ( + DataTableColumnType, + create_data_table as _create_data_table, create_data_table_rows as _create_data_table_rows, -) -from secops.chronicle.data_table import delete_data_table as _delete_data_table -from secops.chronicle.data_table import ( + delete_data_table as _delete_data_table, delete_data_table_rows as _delete_data_table_rows, -) -from secops.chronicle.data_table import get_data_table as _get_data_table -from secops.chronicle.data_table import ( + get_data_table as _get_data_table, list_data_table_rows as _list_data_table_rows, -) -from secops.chronicle.data_table import list_data_tables as _list_data_tables -from secops.chronicle.data_table import ( + list_data_tables as _list_data_tables, replace_data_table_rows as _replace_data_table_rows, -) -from secops.chronicle.data_table import update_data_table as _update_data_table -from secops.chronicle.data_table import ( + update_data_table as _update_data_table, update_data_table_rows as _update_data_table_rows, ) -from secops.chronicle.entity import _detect_value_type_for_query -from secops.chronicle.entity import summarize_entity as _summarize_entity -from secops.chronicle.feeds import CreateFeedModel, UpdateFeedModel -from secops.chronicle.feeds import create_feed as _create_feed -from secops.chronicle.feeds import delete_feed as _delete_feed -from secops.chronicle.feeds import disable_feed as _disable_feed -from secops.chronicle.feeds import enable_feed as _enable_feed -from secops.chronicle.feeds import generate_secret as _generate_secret -from secops.chronicle.feeds import get_feed as _get_feed -from secops.chronicle.feeds import list_feeds as _list_feeds -from secops.chronicle.feeds import update_feed as _update_feed -from secops.chronicle.gemini import GeminiResponse -from secops.chronicle.gemini import opt_in_to_gemini as _opt_in_to_gemini -from secops.chronicle.gemini import query_gemini as _query_gemini -from secops.chronicle.ioc import list_iocs as _list_iocs -from secops.chronicle.investigations import ( - fetch_associated_investigations as _fetch_associated_investigations, +from secops.chronicle.entity import ( + _detect_value_type_for_query, + summarize_entity as _summarize_entity, ) -from secops.chronicle.investigations import ( - get_investigation as _get_investigation, +from secops.chronicle.featured_content_rules import ( + list_featured_content_rules as _list_featured_content_rules, ) -from secops.chronicle.investigations import ( - list_investigations as _list_investigations, +from secops.chronicle.feeds import ( + CreateFeedModel, + UpdateFeedModel, + create_feed as _create_feed, + delete_feed as _delete_feed, + disable_feed as _disable_feed, + enable_feed as _enable_feed, + generate_secret as _generate_secret, + get_feed as _get_feed, + list_feeds as _list_feeds, + update_feed as _update_feed, +) +from secops.chronicle.gemini import ( + GeminiResponse, + opt_in_to_gemini as _opt_in_to_gemini, + query_gemini as _query_gemini, ) from secops.chronicle.investigations import ( + fetch_associated_investigations as _fetch_associated_investigations, + get_investigation as _get_investigation, + list_investigations as _list_investigations, trigger_investigation as _trigger_investigation, ) -from secops.chronicle.log_ingest import create_forwarder as _create_forwarder -from secops.chronicle.log_ingest import delete_forwarder as _delete_forwarder -from secops.chronicle.log_ingest import get_forwarder as _get_forwarder +from secops.chronicle.ioc import list_iocs as _list_iocs from secops.chronicle.log_ingest import ( + create_forwarder as _create_forwarder, + delete_forwarder as _delete_forwarder, + get_forwarder as _get_forwarder, get_or_create_forwarder as _get_or_create_forwarder, + import_entities as _import_entities, + ingest_log as _ingest_log, + ingest_udm as _ingest_udm, + list_forwarders as _list_forwarders, + update_forwarder as _update_forwarder, ) -from secops.chronicle.log_ingest import import_entities as _import_entities -from secops.chronicle.log_ingest import ingest_log as _ingest_log -from secops.chronicle.log_ingest import ingest_udm as _ingest_udm -from secops.chronicle.log_ingest import list_forwarders as _list_forwarders -from secops.chronicle.log_ingest import update_forwarder as _update_forwarder -from secops.chronicle.log_types import classify_logs as _classify_logs -from secops.chronicle.log_types import get_all_log_types as _get_all_log_types -from secops.chronicle.log_types import ( - get_log_type_description as _get_log_type_description, -) -from secops.chronicle.log_types import is_valid_log_type as _is_valid_log_type -from secops.chronicle.log_types import search_log_types as _search_log_types from secops.chronicle.log_processing_pipelines import ( associate_streams as _associate_streams, -) -from secops.chronicle.log_processing_pipelines import ( create_log_processing_pipeline as _create_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( delete_log_processing_pipeline as _delete_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( dissociate_streams as _dissociate_streams, -) -from secops.chronicle.log_processing_pipelines import ( fetch_associated_pipeline as _fetch_associated_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( fetch_sample_logs_by_streams as _fetch_sample_logs_by_streams, -) -from secops.chronicle.log_processing_pipelines import ( get_log_processing_pipeline as _get_log_processing_pipeline, -) -from secops.chronicle.log_processing_pipelines import ( list_log_processing_pipelines as _list_log_processing_pipelines, -) -from secops.chronicle.log_processing_pipelines import ( + test_pipeline as _test_pipeline, update_log_processing_pipeline as _update_log_processing_pipeline, ) -from secops.chronicle.log_processing_pipelines import ( - test_pipeline as _test_pipeline, +from secops.chronicle.log_types import ( + classify_logs as _classify_logs, + get_all_log_types as _get_all_log_types, + get_log_type_description as _get_log_type_description, + is_valid_log_type as _is_valid_log_type, + search_log_types as _search_log_types, +) +from secops.chronicle.integration.connectors import ( + create_integration_connector as _create_integration_connector, + delete_integration_connector as _delete_integration_connector, + execute_integration_connector_test as _execute_integration_connector_test, + get_integration_connector as _get_integration_connector, + get_integration_connector_template as _get_integration_connector_template, + list_integration_connectors as _list_integration_connectors, + update_integration_connector as _update_integration_connector, +) +from secops.chronicle.integration.connector_revisions import ( + create_integration_connector_revision as _create_integration_connector_revision, + delete_integration_connector_revision as _delete_integration_connector_revision, + list_integration_connector_revisions as _list_integration_connector_revisions, + rollback_integration_connector_revision as _rollback_integration_connector_revision, +) +from secops.chronicle.integration.connector_context_properties import ( + create_connector_context_property as _create_connector_context_property, + delete_all_connector_context_properties as _delete_all_connector_context_properties, + delete_connector_context_property as _delete_connector_context_property, + get_connector_context_property as _get_connector_context_property, + list_connector_context_properties as _list_connector_context_properties, + update_connector_context_property as _update_connector_context_property, +) +from secops.chronicle.integration.connector_instance_logs import ( + get_connector_instance_log as _get_connector_instance_log, + list_connector_instance_logs as _list_connector_instance_logs, +) +from secops.chronicle.integration.connector_instances import ( + create_connector_instance as _create_connector_instance, + delete_connector_instance as _delete_connector_instance, + get_connector_instance as _get_connector_instance, + get_connector_instance_latest_definition as _get_connector_instance_latest_definition, + list_connector_instances as _list_connector_instances, + run_connector_instance_on_demand as _run_connector_instance_on_demand, + set_connector_instance_logs_collection as _set_connector_instance_logs_collection, + update_connector_instance as _update_connector_instance, ) from secops.chronicle.models import ( APIVersion, CaseList, + ConnectorParameter, + ConnectorRule, DashboardChart, DashboardQuery, EntitySummary, InputInterval, TileType, + ConnectorInstanceParameter, +) +from secops.chronicle.nl_search import ( + nl_search as _nl_search, + translate_nl_to_udm, ) -from secops.chronicle.nl_search import nl_search as _nl_search -from secops.chronicle.nl_search import translate_nl_to_udm -from secops.chronicle.parser import activate_parser as _activate_parser from secops.chronicle.parser import ( + activate_parser as _activate_parser, activate_release_candidate_parser as _activate_release_candidate_parser, + copy_parser as _copy_parser, + create_parser as _create_parser, + deactivate_parser as _deactivate_parser, + delete_parser as _delete_parser, + get_parser as _get_parser, + list_parsers as _list_parsers, + run_parser as _run_parser, ) -from secops.chronicle.parser import copy_parser as _copy_parser -from secops.chronicle.parser import create_parser as _create_parser -from secops.chronicle.parser import deactivate_parser as _deactivate_parser -from secops.chronicle.parser import delete_parser as _delete_parser -from secops.chronicle.parser import get_parser as _get_parser -from secops.chronicle.parser import list_parsers as _list_parsers -from secops.chronicle.parser import run_parser as _run_parser -from secops.chronicle.parser_extension import ParserExtensionConfig from secops.chronicle.parser_extension import ( + ParserExtensionConfig, activate_parser_extension as _activate_parser_extension, -) -from secops.chronicle.parser_extension import ( create_parser_extension as _create_parser_extension, -) -from secops.chronicle.parser_extension import ( delete_parser_extension as _delete_parser_extension, -) -from secops.chronicle.parser_extension import ( get_parser_extension as _get_parser_extension, -) -from secops.chronicle.parser_extension import ( list_parser_extensions as _list_parser_extensions, ) from secops.chronicle.reference_list import ( ReferenceListSyntaxType, ReferenceListView, -) -from secops.chronicle.reference_list import ( create_reference_list as _create_reference_list, -) -from secops.chronicle.reference_list import ( get_reference_list as _get_reference_list, -) -from secops.chronicle.reference_list import ( list_reference_lists as _list_reference_lists, -) -from secops.chronicle.reference_list import ( update_reference_list as _update_reference_list, ) - -# Import rule functions -from secops.chronicle.rule import create_rule as _create_rule -from secops.chronicle.rule import delete_rule as _delete_rule -from secops.chronicle.rule import enable_rule as _enable_rule -from secops.chronicle.rule import get_rule as _get_rule -from secops.chronicle.rule import get_rule_deployment as _get_rule_deployment from secops.chronicle.rule import ( + create_rule as _create_rule, + delete_rule as _delete_rule, + enable_rule as _enable_rule, + get_rule as _get_rule, + get_rule_deployment as _get_rule_deployment, list_rule_deployments as _list_rule_deployments, -) -from secops.chronicle.rule import list_rules as _list_rules -from secops.chronicle.rule import run_rule_test -from secops.chronicle.rule import search_rules as _search_rules -from secops.chronicle.rule import set_rule_alerting as _set_rule_alerting -from secops.chronicle.rule import update_rule as _update_rule -from secops.chronicle.rule import ( + list_rules as _list_rules, + run_rule_test, + search_rules as _search_rules, + set_rule_alerting as _set_rule_alerting, + update_rule as _update_rule, update_rule_deployment as _update_rule_deployment, ) from secops.chronicle.rule_alert import ( bulk_update_alerts as _bulk_update_alerts, -) -from secops.chronicle.rule_alert import get_alert as _get_alert -from secops.chronicle.rule_alert import ( + get_alert as _get_alert, search_rule_alerts as _search_rule_alerts, + update_alert as _update_alert, +) +from secops.chronicle.rule_detection import ( + list_detections as _list_detections, + list_errors as _list_errors, ) -from secops.chronicle.rule_alert import update_alert as _update_alert -from secops.chronicle.rule_detection import list_detections as _list_detections -from secops.chronicle.rule_detection import list_errors as _list_errors from secops.chronicle.rule_exclusion import ( RuleExclusionType, UpdateRuleDeployment, -) -from secops.chronicle.rule_exclusion import ( compute_rule_exclusion_activity as _compute_rule_exclusion_activity, -) -from secops.chronicle.rule_exclusion import ( create_rule_exclusion as _create_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( get_rule_exclusion as _get_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( get_rule_exclusion_deployment as _get_rule_exclusion_deployment, -) -from secops.chronicle.rule_exclusion import ( list_rule_exclusions as _list_rule_exclusions, -) -from secops.chronicle.rule_exclusion import ( patch_rule_exclusion as _patch_rule_exclusion, -) -from secops.chronicle.rule_exclusion import ( update_rule_exclusion_deployment as _update_rule_exclusion_deployment, ) from secops.chronicle.rule_retrohunt import ( @@ -270,72 +251,45 @@ list_retrohunts as _list_retrohunts, ) from secops.chronicle.rule_set import ( - batch_update_curated_rule_set_deployments as _batch_update_curated_rule_set_deployments, # pylint: disable=line-too-long -) -from secops.chronicle.rule_set import get_curated_rule as _get_curated_rule -from secops.chronicle.rule_set import ( + batch_update_curated_rule_set_deployments as _batch_update_curated_rule_set_deployments, + get_curated_rule as _get_curated_rule, get_curated_rule_by_name as _get_curated_rule_by_name, -) -from secops.chronicle.rule_set import ( get_curated_rule_set as _get_curated_rule_set, -) -from secops.chronicle.rule_set import ( get_curated_rule_set_category as _get_curated_rule_set_category, -) -from secops.chronicle.rule_set import ( get_curated_rule_set_deployment as _get_curated_rule_set_deployment, -) -from secops.chronicle.rule_set import ( - get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, # pylint: disable=line-too-long -) -from secops.chronicle.rule_set import ( + get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, list_curated_rule_set_categories as _list_curated_rule_set_categories, -) -from secops.chronicle.rule_set import ( list_curated_rule_set_deployments as _list_curated_rule_set_deployments, -) -from secops.chronicle.rule_set import ( list_curated_rule_sets as _list_curated_rule_sets, -) -from secops.chronicle.rule_set import list_curated_rules as _list_curated_rules -from secops.chronicle.rule_set import ( + list_curated_rules as _list_curated_rules, search_curated_detections as _search_curated_detections, -) -from secops.chronicle.rule_set import ( update_curated_rule_set_deployment as _update_curated_rule_set_deployment, ) -from secops.chronicle.featured_content_rules import ( - list_featured_content_rules as _list_featured_content_rules, -) from secops.chronicle.rule_validation import validate_rule as _validate_rule from secops.chronicle.search import search_udm as _search_udm from secops.chronicle.log_search import search_raw_logs as _search_raw_logs from secops.chronicle.stats import get_stats as _get_stats -from secops.chronicle.udm_mapping import RowLogFormat from secops.chronicle.udm_mapping import ( + RowLogFormat, generate_udm_key_value_mappings as _generate_udm_key_value_mappings, ) - -# Import functions from the new modules from secops.chronicle.udm_search import ( fetch_udm_search_csv as _fetch_udm_search_csv, -) -from secops.chronicle.udm_search import ( fetch_udm_search_view as _fetch_udm_search_view, -) -from secops.chronicle.udm_search import ( find_udm_field_values as _find_udm_field_values, ) from secops.chronicle.validate import validate_query as _validate_query from secops.chronicle.watchlist import ( - list_watchlists as _list_watchlists, - get_watchlist as _get_watchlist, - delete_watchlist as _delete_watchlist, create_watchlist as _create_watchlist, + delete_watchlist as _delete_watchlist, + get_watchlist as _get_watchlist, + list_watchlists as _list_watchlists, update_watchlist as _update_watchlist, ) from secops.exceptions import SecOpsError +# pylint: enable=line-too-long + class ValueType(Enum): """Chronicle API value types.""" @@ -761,6 +715,1223 @@ def update_watchlist( update_mask, ) + # ------------------------------------------------------------------------- + # Integration Connector methods + # ------------------------------------------------------------------------- + + def list_integration_connectors( + self, + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all connectors defined for a specific integration. + + Args: + integration_name: Name of the integration to list connectors + for. + page_size: Maximum number of connectors to return. Defaults + to 50, maximum is 1000. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter connectors. + order_by: Field to sort the connectors by. + exclude_staging: Whether to exclude staging connectors from + the response. By default, staging connectors are included. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of connectors instead of a + dict with connectors list and nextPageToken. + + Returns: + If as_list is True: List of connectors. + If as_list is False: Dict with connectors list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_connectors( + self, + integration_name, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + exclude_staging=exclude_staging, + api_version=api_version, + as_list=as_list, + ) + + def get_integration_connector( + self, + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single connector for a given integration. + + Use this method to retrieve the Python script, configuration parameters, + and field mapping logic for a specific connector. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationConnector. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_connector( + self, + integration_name, + connector_id, + api_version=api_version, + ) + + def delete_integration_connector( + self, + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific custom connector from a given integration. + + Only custom connectors can be deleted; commercial connectors are + immutable. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_connector( + self, + integration_name, + connector_id, + api_version=api_version, + ) + + def create_integration_connector( + self, + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + product_field_name: str, + event_field_name: str, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new custom connector for a given integration. + + Use this method to define how to fetch and parse alerts from a + unique or unofficial data source. Each connector must have a + unique display name and a functional Python script. + + Args: + integration_name: Name of the integration to create the + connector for. + display_name: Connector's display name. Required. + script: Connector's Python script. Required. + timeout_seconds: Timeout in seconds for a single script run. + Required. + enabled: Whether the connector is enabled or disabled. + Required. + product_field_name: Field name used to determine the device + product. Required. + event_field_name: Field name used to determine the event + name (sub-type). Required. + description: Connector's description. Optional. + parameters: List of ConnectorParameter instances or dicts. + Optional. + rules: List of ConnectorRule instances or dicts. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created IntegrationConnector + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_connector( + self, + integration_name, + display_name, + script, + timeout_seconds, + enabled, + product_field_name, + event_field_name, + description=description, + parameters=parameters, + rules=rules, + api_version=api_version, + ) + + def update_integration_connector( + self, + integration_name: str, + connector_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing custom connector for a given integration. + + Only custom connectors can be updated; commercial connectors are + immutable. + + Args: + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to update. + display_name: Connector's display name. + script: Connector's Python script. + timeout_seconds: Timeout in seconds for a single script run. + enabled: Whether the connector is enabled or disabled. + product_field_name: Field name used to determine the device product. + event_field_name: Field name used to determine the event name + (sub-type). + description: Connector's description. + parameters: List of ConnectorParameter instances or dicts. + rules: List of ConnectorRule instances or dicts. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + return _update_integration_connector( + self, + integration_name, + connector_id, + display_name=display_name, + script=script, + timeout_seconds=timeout_seconds, + enabled=enabled, + product_field_name=product_field_name, + event_field_name=event_field_name, + description=description, + parameters=parameters, + rules=rules, + update_mask=update_mask, + api_version=api_version, + ) + + def execute_integration_connector_test( + self, + integration_name: str, + connector: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Execute a test run of a connector's Python script. + + Use this method to verify data fetching logic, authentication, + and parsing logic before enabling the connector for production + ingestion. The full connector object is required as the test + can be run without saving the connector first. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector: Dict containing the IntegrationConnector to test. + agent_identifier: Agent identifier for remote testing. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the test execution results with the + following fields: + - outputMessage: Human-readable output message set by + the script. + - debugOutputMessage: The script debug output. + - resultJson: The result JSON if it exists (optional). + + Raises: + APIError: If the API request fails. + """ + return _execute_integration_connector_test( + self, + integration_name, + connector, + agent_identifier=agent_identifier, + api_version=api_version, + ) + + def get_integration_connector_template( + self, + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Retrieve a default Python script template for a new + integration connector. + + Use this method to rapidly initialize the development of a new + connector. + + Args: + integration_name: Name of the integration to fetch the + template for. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the IntegrationConnector template. + + Raises: + APIError: If the API request fails. + """ + return _get_integration_connector_template( + self, + integration_name, + api_version=api_version, + ) + + # ------------------------------------------------------------------------- + # Integration Connector Revisions methods + # ------------------------------------------------------------------------- + + def list_integration_connector_revisions( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration connector. + + Use this method to browse the version history and identify + potential rollback targets. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of revisions instead of a + dict with revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_integration_connector_revisions( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def delete_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific revision for a given integration + connector. + + Use this method to clean up old or incorrect snapshots from the + version history. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_integration_connector_revision( + self, + integration_name, + connector_id, + revision_id, + api_version=api_version, + ) + + def create_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + connector: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new revision snapshot of the current integration + connector. + + Use this method to save a stable configuration before making + experimental changes. Only custom connectors can be versioned. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create a revision for. + connector: Dict containing the IntegrationConnector to + snapshot. + comment: Comment describing the revision. Maximum 400 + characters. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ConnectorRevision + resource. + + Raises: + APIError: If the API request fails. + """ + return _create_integration_connector_revision( + self, + integration_name, + connector_id, + connector, + comment=comment, + api_version=api_version, + ) + + def rollback_integration_connector_revision( + self, + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Revert the current connector definition to a previously + saved revision. + + Use this method to quickly revert to a known good configuration + if an investigation or update is unsuccessful. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the ConnectorRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return _rollback_integration_connector_revision( + self, + integration_name, + connector_id, + revision_id, + api_version=api_version, + ) + + # ------------------------------------------------------------------------- + # Connector Context Properties methods + # ------------------------------------------------------------------------- + + def list_connector_context_properties( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration + connector. + + Use this method to discover all custom data points associated + with a connector. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list context + properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter context + properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of context properties + instead of a dict with context properties list and + nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_context_properties( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single context property for a specific integration + connector. + + Use this method to retrieve the value of a specific key within + a connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + api_version=api_version, + ) + + def delete_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific context property for a given integration + connector. + + Use this method to remove a custom data point that is no longer + relevant to the connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + api_version=api_version, + ) + + def create_connector_context_property( + self, + integration_name: str, + connector_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new context property for a specific integration + connector. + + Use this method to attach custom data to a connector's context. + Property keys must be unique within their context. Key values + must be 4-63 characters and match /[a-z][0-9]-/. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create the context + property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 + characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _create_connector_context_property( + self, + integration_name, + connector_id, + value, + key=key, + api_version=api_version, + ) + + def update_connector_context_property( + self, + integration_name: str, + connector_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing context property for a given integration + connector. + + Use this method to modify the value of a previously saved key. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the context property + belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only + "value" is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + return _update_connector_context_property( + self, + integration_name, + connector_id, + context_property_id, + value, + update_mask=update_mask, + api_version=api_version, + ) + + def delete_all_connector_context_properties( + self, + integration_name: str, + connector_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete all context properties for a specific integration + connector. + + Use this method to quickly clear all supplemental data from a + connector's context. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to clear context + properties from. + context_id: The context ID to remove context properties + from. Must be 4-63 characters and match /[a-z][0-9]-/. + Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_all_connector_context_properties( + self, + integration_name, + connector_id, + context_id=context_id, + api_version=api_version, + ) + + # ------------------------------------------------------------------------- + # Connector Instance Logs methods + # ------------------------------------------------------------------------- + + def list_connector_instance_logs( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all logs for a specific connector instance. + + Use this method to browse the execution history and diagnostic + output of a connector. Supports filtering and pagination to + efficiently navigate large volumes of log data. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to list + logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of logs instead of a dict + with logs list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_instance_logs( + self, + integration_name, + connector_id, + connector_instance_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_instance_log( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single log entry for a specific connector instance. + + Use this method to retrieve a specific log entry from a + connector instance's execution, including its message, + timestamp, and severity level. Useful for auditing and detailed + troubleshooting of a specific connector run. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance the log + belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ConnectorLog. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance_log( + self, + integration_name, + connector_id, + connector_instance_id, + log_id, + api_version=api_version, + ) + + # ------------------------------------------------------------------------- + # Connector Instance methods + # ------------------------------------------------------------------------- + + def list_connector_instances( + self, + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, + ) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration connector. + + Use this method to discover all configured instances of a + connector. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to list instances for. + page_size: Maximum number of instances to return. + page_token: Page token from a previous call to retrieve the + next page. + filter_string: Filter expression to filter instances. + order_by: Field to sort the instances by. + api_version: API version to use for the request. Default is + V1BETA. + as_list: If True, return a list of instances instead of a + dict with instances list and nextPageToken. + + Returns: + If as_list is True: List of connector instances. + If as_list is False: Dict with connector instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + return _list_connector_instances( + self, + integration_name, + connector_id, + page_size=page_size, + page_token=page_token, + filter_string=filter_string, + order_by=order_by, + api_version=api_version, + as_list=as_list, + ) + + def get_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Get a single connector instance by ID. + + Use this method to retrieve the configuration of a specific + connector instance, including its parameters, schedule, and + runtime settings. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + retrieve. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing details of the specified ConnectorInstance. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def delete_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> None: + """Delete a specific connector instance. + + Use this method to permanently remove a connector instance and + its configuration. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + delete. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + return _delete_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def create_connector_instance( + self, + integration_name: str, + connector_id: str, + environment: str, + display_name: str, + interval_seconds: int, + timeout_seconds: int, + description: str | None = None, + parameters: list[ConnectorInstanceParameter | dict] | None = None, + agent: str | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + connector_instance_id: str | None = None, + enabled: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Create a new connector instance. + + Use this method to configure a new instance of a connector with + specific parameters and schedule settings. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector to create an instance for. + environment: Environment for the instance (e.g., + "production"). + display_name: Display name for the instance. Required. + interval_seconds: Interval in seconds for recurring + execution. Required. + timeout_seconds: Timeout in seconds for execution. Required. + description: Description of the instance. Optional. + parameters: List of parameters for the instance. Optional. + agent: Agent identifier for remote execution. Optional. + allow_list: List of allowed IP addresses. Optional. + product_field_name: Product field name. Optional. + event_field_name: Event field name. Optional. + integration_version: Integration version. Optional. + version: Version. Optional. + logging_enabled_until_unix_ms: Logging enabled until + timestamp. Optional. + connector_instance_id: Custom ID for the instance. Optional. + enabled: Whether the instance is enabled. Optional. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the newly created ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _create_connector_instance( + self, + integration_name, + connector_id, + environment, + display_name, + interval_seconds, + timeout_seconds, + description=description, + parameters=parameters, + agent=agent, + allow_list=allow_list, + product_field_name=product_field_name, + event_field_name=event_field_name, + integration_version=integration_version, + version=version, + logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, + connector_instance_id=connector_instance_id, + enabled=enabled, + api_version=api_version, + ) + + def update_connector_instance( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + display_name: str | None = None, + description: str | None = None, + interval_seconds: int | None = None, + timeout_seconds: int | None = None, + parameters: list[ConnectorInstanceParameter | dict] | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + enabled: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Update an existing connector instance. + + Use this method to modify the configuration, parameters, or + schedule of a connector instance. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + update. + display_name: Display name for the instance. Optional. + description: Description of the instance. Optional. + interval_seconds: Interval in seconds for recurring + execution. Optional. + timeout_seconds: Timeout in seconds for execution. Optional. + parameters: List of parameters for the instance. Optional. + agent: Agent identifier for remote execution. Optional. + allow_list: List of allowed IP addresses. Optional. + product_field_name: Product field name. Optional. + event_field_name: Event field name. Optional. + integration_version: Integration version. Optional. + version: Version. Optional. + logging_enabled_until_unix_ms: Logging enabled until + timestamp. Optional. + enabled: Whether the instance is enabled. Optional. + update_mask: Comma-separated list of fields to update. If + omitted, all provided fields will be updated. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return _update_connector_instance( + self, + integration_name, + connector_id, + connector_instance_id, + display_name=display_name, + description=description, + interval_seconds=interval_seconds, + timeout_seconds=timeout_seconds, + parameters=parameters, + allow_list=allow_list, + product_field_name=product_field_name, + event_field_name=event_field_name, + integration_version=integration_version, + version=version, + logging_enabled_until_unix_ms=logging_enabled_until_unix_ms, + enabled=enabled, + update_mask=update_mask, + api_version=api_version, + ) + + def get_connector_instance_latest_definition( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Fetch the latest definition for a connector instance. + + Use this method to refresh a connector instance with the latest + connector definition from the marketplace. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + refresh. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the refreshed ConnectorInstance with latest + definition. + + Raises: + APIError: If the API request fails. + """ + return _get_connector_instance_latest_definition( + self, + integration_name, + connector_id, + connector_instance_id, + api_version=api_version, + ) + + def set_connector_instance_logs_collection( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + enabled: bool, + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Enable or disable logs collection for a connector instance. + + Use this method to control whether execution logs are collected + for a specific connector instance. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to + configure. + enabled: Whether to enable or disable logs collection. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the updated logs collection status. + + Raises: + APIError: If the API request fails. + """ + return _set_connector_instance_logs_collection( + self, + integration_name, + connector_id, + connector_instance_id, + enabled, + api_version=api_version, + ) + + def run_connector_instance_on_demand( + self, + integration_name: str, + connector_id: str, + connector_instance_id: str, + connector_instance: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1BETA, + ) -> dict[str, Any]: + """Run a connector instance on demand for testing. + + Use this method to execute a connector instance immediately + without waiting for its scheduled run. Useful for testing + configuration changes. + + Args: + integration_name: Name of the integration the connector + belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to run. + connector_instance: The connector instance configuration to + test. Should include parameters and other settings. + api_version: API version to use for the request. Default is + V1BETA. + + Returns: + Dict containing the execution result, including success + status and debug output. + + Raises: + APIError: If the API request fails. + """ + return _run_connector_instance_on_demand( + self, + integration_name, + connector_id, + connector_instance_id, + connector_instance, + api_version=api_version, + ) + def get_stats( self, query: str, diff --git a/src/secops/chronicle/entity.py b/src/secops/chronicle/entity.py index 429d4393..84e5060a 100644 --- a/src/secops/chronicle/entity.py +++ b/src/secops/chronicle/entity.py @@ -15,6 +15,7 @@ """ Provides entity search, analysis and summarization functionality for Chronicle. """ + import ipaddress import re from datetime import datetime diff --git a/src/secops/chronicle/feeds.py b/src/secops/chronicle/feeds.py index b9ed7f22..8030b753 100644 --- a/src/secops/chronicle/feeds.py +++ b/src/secops/chronicle/feeds.py @@ -15,6 +15,7 @@ """ Provides ingestion feed management functionality for Chronicle. """ + import json import os import sys diff --git a/src/secops/chronicle/gemini.py b/src/secops/chronicle/gemini.py index abed52cb..eee42374 100644 --- a/src/secops/chronicle/gemini.py +++ b/src/secops/chronicle/gemini.py @@ -16,6 +16,7 @@ Provides access to Chronicle's Gemini conversational AI interface. """ + import re from typing import Any diff --git a/src/secops/chronicle/integration/__init__.py b/src/secops/chronicle/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/chronicle/integration/connector_context_properties.py b/src/secops/chronicle/integration/connector_context_properties.py new file mode 100644 index 00000000..24e59f66 --- /dev/null +++ b/src/secops/chronicle/integration/connector_context_properties.py @@ -0,0 +1,299 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Integration connector context properties functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_context_properties( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all context properties for a specific integration connector. + + Use this method to discover all custom data points associated with a + connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list context properties for. + page_size: Maximum number of context properties to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter context properties. + order_by: Field to sort the context properties by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of context properties instead of a + dict with context properties list and nextPageToken. + + Returns: + If as_list is True: List of context properties. + If as_list is False: Dict with context properties list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties" + ), + items_key="contextProperties", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single context property for a specific integration connector. + + Use this method to retrieve the value of a specific key within a + connector's context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ContextProperty. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def delete_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific context property for a given integration connector. + + Use this method to remove a custom data point that is no longer relevant + to the connector's context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + ) + + +def create_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + value: str, + key: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new context property for a specific integration connector. + + Use this method to attach custom data to a connector's context. Property + keys must be unique within their context. Key values must be 4-63 + characters and match /[a-z][0-9]-/. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create the context property for. + value: The property value. Required. + key: The context property ID to use. Must be 4-63 characters and + match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body = {"value": value} + + if key is not None: + body["key"] = key + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties" + ), + api_version=api_version, + json=body, + ) + + +def update_connector_context_property( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_property_id: str, + value: str, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing context property for a given integration connector. + + Use this method to modify the value of a previously saved key. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the context property belongs to. + context_property_id: ID of the context property to update. + value: The new property value. Required. + update_mask: Comma-separated list of fields to update. Only "value" + is supported. If omitted, defaults to "value". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated ContextProperty resource. + + Raises: + APIError: If the API request fails. + """ + body, params = build_patch_body( + field_map=[ + ("value", "value", value), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties/{context_property_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def delete_all_connector_context_properties( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + context_id: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete all context properties for a specific integration connector. + + Use this method to quickly clear all supplemental data from a connector's + context. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to clear context properties from. + context_id: The context ID to remove context properties from. Must be + 4-63 characters and match /[a-z][0-9]-/. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + body = {} + + if context_id is not None: + body["contextId"] = context_id + + chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/contextProperties:clearAll" + ), + api_version=api_version, + json=body, + ) diff --git a/src/secops/chronicle/integration/connector_instance_logs.py b/src/secops/chronicle/integration/connector_instance_logs.py new file mode 100644 index 00000000..0be7bd25 --- /dev/null +++ b/src/secops/chronicle/integration/connector_instance_logs.py @@ -0,0 +1,130 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Integration connector instance logs functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_instance_logs( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all logs for a specific connector instance. + + Use this method to browse the execution history and diagnostic output of + a connector. Supports filtering and pagination to efficiently navigate + large volumes of log data. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to list logs for. + page_size: Maximum number of logs to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter logs. + order_by: Field to sort the logs by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of logs instead of a dict with logs + list and nextPageToken. + + Returns: + If as_list is True: List of logs. + If as_list is False: Dict with logs list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}/logs" + ), + items_key="logs", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_instance_log( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + log_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single log entry for a specific connector instance. + + Use this method to retrieve a specific log entry from a connector + instance's execution, including its message, timestamp, and severity + level. Useful for auditing and detailed troubleshooting of a specific + connector run. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance the log belongs to. + log_id: ID of the log entry to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ConnectorLog. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}/logs/{log_id}" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/connector_instances.py b/src/secops/chronicle/integration/connector_instances.py new file mode 100644 index 00000000..c6b563cc --- /dev/null +++ b/src/secops/chronicle/integration/connector_instances.py @@ -0,0 +1,489 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Integration connector instances functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + ConnectorInstanceParameter, +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_connector_instances( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all instances for a specific integration connector. + + Use this method to discover all configured instances of a connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list instances for. + page_size: Maximum number of connector instances to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter connector instances. + order_by: Field to sort the connector instances by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of connector instances instead of a + dict with connector instances list and nextPageToken. + + Returns: + If as_list is True: List of connector instances. + If as_list is False: Dict with connector instances list and + nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances" + ), + items_key="connectorInstances", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single instance for a specific integration connector. + + Use this method to retrieve the configuration and status of a specific + connector instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified ConnectorInstance. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + ) + + +def delete_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific connector instance. + + Use this method to permanently remove a data ingestion stream. For remote + connectors, the associated agent must be live and have no pending packages. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + ) + + +def create_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + environment: str, + display_name: str, + interval_seconds: int, + timeout_seconds: int, + description: str | None = None, + agent: str | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, + connector_instance_id: str | None = None, + enabled: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new connector instance for a specific integration connector. + + Use this method to establish a new data ingestion stream from a security + product. Note that agent and remote cannot be patched after creation. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create an instance for. + environment: Connector instance environment. Cannot be patched for + remote connectors. Required. + display_name: Connector instance display name. Required. + interval_seconds: Connector instance execution interval in seconds. + Required. + timeout_seconds: Timeout of a single Python script run. Required. + description: Connector instance description. Optional. + agent: Agent identifier for a remote connector instance. Cannot be + patched after creation. Optional. + allow_list: Connector instance allow list. Optional. + product_field_name: Connector's device product field. Optional. + event_field_name: Connector's event name field. Optional. + integration_version: The integration version. Optional. + version: The connector instance version. Optional. + logging_enabled_until_unix_ms: Timeout when log collecting will be + disabled. Optional. + parameters: List of ConnectorInstanceParameter instances or dicts. + Optional. + connector_instance_id: The connector instance id. Optional. + enabled: Whether the connector instance is enabled. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body = { + "environment": environment, + "displayName": display_name, + "intervalSeconds": interval_seconds, + "timeoutSeconds": timeout_seconds, + "description": description, + "agent": agent, + "allowList": allow_list, + "productFieldName": product_field_name, + "eventFieldName": event_field_name, + "integrationVersion": integration_version, + "version": version, + "loggingEnabledUntilUnixMs": logging_enabled_until_unix_ms, + "parameters": resolved_parameters, + "id": connector_instance_id, + "enabled": enabled, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances" + ), + api_version=api_version, + json=body, + ) + + +def update_connector_instance( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + display_name: str | None = None, + description: str | None = None, + interval_seconds: int | None = None, + timeout_seconds: int | None = None, + allow_list: list[str] | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + integration_version: str | None = None, + version: str | None = None, + logging_enabled_until_unix_ms: str | None = None, + parameters: list[dict[str, Any] | ConnectorInstanceParameter] | None = None, + enabled: bool | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing connector instance. + + Use this method to enable or disable a connector, change its display + name, or adjust its ingestion parameters. Note that agent, remote, and + environment cannot be patched after creation. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to update. + display_name: Connector instance display name. + description: Connector instance description. + interval_seconds: Connector instance execution interval in seconds. + timeout_seconds: Timeout of a single Python script run. + allow_list: Connector instance allow list. + product_field_name: Connector's device product field. + event_field_name: Connector's event name field. + integration_version: The integration version. Required on patch if + provided. + version: The connector instance version. + logging_enabled_until_unix_ms: Timeout when log collecting will be + disabled. + parameters: List of ConnectorInstanceParameter instances or dicts. + enabled: Whether the connector instance is enabled. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,intervalSeconds". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorInstanceParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("description", "description", description), + ("intervalSeconds", "intervalSeconds", interval_seconds), + ("timeoutSeconds", "timeoutSeconds", timeout_seconds), + ("allowList", "allowList", allow_list), + ("productFieldName", "productFieldName", product_field_name), + ("eventFieldName", "eventFieldName", event_field_name), + ("integrationVersion", "integrationVersion", integration_version), + ("version", "version", version), + ( + "loggingEnabledUntilUnixMs", + "loggingEnabledUntilUnixMs", + logging_enabled_until_unix_ms, + ), + ("parameters", "parameters", resolved_parameters), + ("id", "id", connector_instance_id), + ("enabled", "enabled", enabled), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def get_connector_instance_latest_definition( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Refresh a connector instance with the latest definition. + + Use this method to discover new parameters or updated scripts for an + existing connector instance. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to refresh. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the refreshed ConnectorInstance resource. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:fetchLatestDefinition" + ), + api_version=api_version, + ) + + +def set_connector_instance_logs_collection( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + enabled: bool, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Enable or disable debug log collection for a connector instance. + + When enabled is set to True, existing logs are cleared and a new + collection period is started. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to configure. + enabled: Whether logs collection is enabled for the connector + instance. Required. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the log enable expiration time in unix ms. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:setLogsCollection" + ), + api_version=api_version, + json={"enabled": enabled}, + ) + + +def run_connector_instance_on_demand( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector_instance_id: str, + connector_instance: dict[str, Any], + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Trigger an immediate, single execution of a connector instance. + + Use this method for testing configuration changes or manually + force-starting a data ingestion cycle. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the instance belongs to. + connector_instance_id: ID of the connector instance to run. + connector_instance: Dict containing the ConnectorInstance with + values to use for the run. Required. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the run results with the following fields: + - debugOutput: The execution debug output message. + - success: True if the execution was successful. + - sampleCases: List of alerts produced by the connector run. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/connectorInstances/" + f"{connector_instance_id}:runOnDemand" + ), + api_version=api_version, + json={"connectorInstance": connector_instance}, + ) diff --git a/src/secops/chronicle/integration/connector_revisions.py b/src/secops/chronicle/integration/connector_revisions.py new file mode 100644 index 00000000..a5908864 --- /dev/null +++ b/src/secops/chronicle/integration/connector_revisions.py @@ -0,0 +1,202 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Integration connector revisions functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import APIVersion +from secops.chronicle.utils.format_utils import format_resource_id +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_connector_revisions( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all revisions for a specific integration connector. + + Use this method to browse the version history and identify potential + rollback targets. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to list revisions for. + page_size: Maximum number of revisions to return. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter revisions. + order_by: Field to sort the revisions by. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of revisions instead of a dict with + revisions list and nextPageToken. + + Returns: + If as_list is True: List of revisions. + If as_list is False: Dict with revisions list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions" + ), + items_key="revisions", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def delete_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific revision for a given integration connector. + + Use this method to clean up old or incorrect snapshots from the version + history. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector the revision belongs to. + revision_id: ID of the revision to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions/{revision_id}" + ), + api_version=api_version, + ) + + +def create_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + connector: dict[str, Any], + comment: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new revision snapshot of the current integration connector. + + Use this method to save a stable configuration before making experimental + changes. Only custom connectors can be versioned. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to create a revision for. + connector: Dict containing the IntegrationConnector to snapshot. + comment: Comment describing the revision. Maximum 400 characters. + Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created ConnectorRevision resource. + + Raises: + APIError: If the API request fails. + """ + body = {"connector": connector} + + if comment is not None: + body["comment"] = comment + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions" + ), + api_version=api_version, + json=body, + ) + + +def rollback_integration_connector_revision( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + revision_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Revert the current connector definition to a previously saved revision. + + Use this method to quickly revert to a known good configuration if an + investigation or update is unsuccessful. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to rollback. + revision_id: ID of the revision to rollback to. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the ConnectorRevision rolled back to. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}/revisions/{revision_id}:rollback" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/integration/connectors.py b/src/secops/chronicle/integration/connectors.py new file mode 100644 index 00000000..b2c0ccd1 --- /dev/null +++ b/src/secops/chronicle/integration/connectors.py @@ -0,0 +1,405 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Integration connectors functionality for Chronicle.""" + +from typing import Any, TYPE_CHECKING + +from secops.chronicle.models import ( + APIVersion, + ConnectorParameter, + ConnectorRule, +) +from secops.chronicle.utils.format_utils import ( + format_resource_id, + build_patch_body, +) +from secops.chronicle.utils.request_utils import ( + chronicle_paginated_request, + chronicle_request, +) + +if TYPE_CHECKING: + from secops.chronicle.client import ChronicleClient + + +def list_integration_connectors( + client: "ChronicleClient", + integration_name: str, + page_size: int | None = None, + page_token: str | None = None, + filter_string: str | None = None, + order_by: str | None = None, + exclude_staging: bool | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, + as_list: bool = False, +) -> dict[str, Any] | list[dict[str, Any]]: + """List all connectors defined for a specific integration. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to list connectors for. + page_size: Maximum number of connectors to return. Defaults to 50, + maximum is 1000. + page_token: Page token from a previous call to retrieve the next page. + filter_string: Filter expression to filter connectors. + order_by: Field to sort the connectors by. + exclude_staging: Whether to exclude staging connectors from the + response. By default, staging connectors are included. + api_version: API version to use for the request. Default is V1BETA. + as_list: If True, return a list of connectors instead of a dict with + connectors list and nextPageToken. + + Returns: + If as_list is True: List of connectors. + If as_list is False: Dict with connectors list and nextPageToken. + + Raises: + APIError: If the API request fails. + """ + extra_params = { + "filter": filter_string, + "orderBy": order_by, + "excludeStaging": exclude_staging, + } + + # Remove keys with None values + extra_params = {k: v for k, v in extra_params.items() if v is not None} + + return chronicle_paginated_request( + client, + api_version=api_version, + path=f"integrations/{format_resource_id(integration_name)}/connectors", + items_key="connectors", + page_size=page_size, + page_token=page_token, + extra_params=extra_params, + as_list=as_list, + ) + + +def get_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Get a single connector for a given integration. + + Use this method to retrieve the Python script, configuration parameters, + and field mapping logic for a specific connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to retrieve. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing details of the specified IntegrationConnector. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + ) + + +def delete_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> None: + """Delete a specific custom connector from a given integration. + + Only custom connectors can be deleted; commercial connectors are + immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to delete. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + None + + Raises: + APIError: If the API request fails. + """ + chronicle_request( + client, + method="DELETE", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + ) + + +def create_integration_connector( + client: "ChronicleClient", + integration_name: str, + display_name: str, + script: str, + timeout_seconds: int, + enabled: bool, + product_field_name: str, + event_field_name: str, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Create a new custom connector for a given integration. + + Use this method to define how to fetch and parse alerts from a unique or + unofficial data source. Each connector must have a unique display name + and a functional Python script. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to create the connector for. + display_name: Connector's display name. Required. + script: Connector's Python script. Required. + timeout_seconds: Timeout in seconds for a single script run. Required. + enabled: Whether the connector is enabled or disabled. Required. + product_field_name: Field name used to determine the device product. + Required. + event_field_name: Field name used to determine the event name + (sub-type). Required. + description: Connector's description. Optional. + parameters: List of ConnectorParameter instances or dicts. Optional. + rules: List of ConnectorRule instances or dicts. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the newly created IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + resolved_rules = ( + [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] + if rules is not None + else None + ) + + body = { + "displayName": display_name, + "script": script, + "timeoutSeconds": timeout_seconds, + "enabled": enabled, + "productFieldName": product_field_name, + "eventFieldName": event_field_name, + "description": description, + "parameters": resolved_parameters, + "rules": resolved_rules, + } + + # Remove keys with None values + body = {k: v for k, v in body.items() if v is not None} + + return chronicle_request( + client, + method="POST", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors" + ), + api_version=api_version, + json=body, + ) + + +def update_integration_connector( + client: "ChronicleClient", + integration_name: str, + connector_id: str, + display_name: str | None = None, + script: str | None = None, + timeout_seconds: int | None = None, + enabled: bool | None = None, + product_field_name: str | None = None, + event_field_name: str | None = None, + description: str | None = None, + parameters: list[dict[str, Any] | ConnectorParameter] | None = None, + rules: list[dict[str, Any] | ConnectorRule] | None = None, + update_mask: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Update an existing custom connector for a given integration. + + Only custom connectors can be updated; commercial connectors are + immutable. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector_id: ID of the connector to update. + display_name: Connector's display name. + script: Connector's Python script. + timeout_seconds: Timeout in seconds for a single script run. + enabled: Whether the connector is enabled or disabled. + product_field_name: Field name used to determine the device product. + event_field_name: Field name used to determine the event name + (sub-type). + description: Connector's description. + parameters: List of ConnectorParameter instances or dicts. + rules: List of ConnectorRule instances or dicts. + update_mask: Comma-separated list of fields to update. If omitted, + the mask is auto-generated from whichever fields are provided. + Example: "displayName,script". + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the updated IntegrationConnector resource. + + Raises: + APIError: If the API request fails. + """ + resolved_parameters = ( + [ + p.to_dict() if isinstance(p, ConnectorParameter) else p + for p in parameters + ] + if parameters is not None + else None + ) + resolved_rules = ( + [r.to_dict() if isinstance(r, ConnectorRule) else r for r in rules] + if rules is not None + else None + ) + + body, params = build_patch_body( + field_map=[ + ("displayName", "displayName", display_name), + ("script", "script", script), + ("timeoutSeconds", "timeoutSeconds", timeout_seconds), + ("enabled", "enabled", enabled), + ("productFieldName", "productFieldName", product_field_name), + ("eventFieldName", "eventFieldName", event_field_name), + ("description", "description", description), + ("parameters", "parameters", resolved_parameters), + ("rules", "rules", resolved_rules), + ], + update_mask=update_mask, + ) + + return chronicle_request( + client, + method="PATCH", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors/{connector_id}" + ), + api_version=api_version, + json=body, + params=params, + ) + + +def execute_integration_connector_test( + client: "ChronicleClient", + integration_name: str, + connector: dict[str, Any], + agent_identifier: str | None = None, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Execute a test run of a connector's Python script. + + Use this method to verify data fetching logic, authentication, and parsing + logic before enabling the connector for production ingestion. The full + connector object is required as the test can be run without saving the + connector first. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration the connector belongs to. + connector: Dict containing the IntegrationConnector to test. + agent_identifier: Agent identifier for remote testing. Optional. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the test execution results with the following fields: + - outputMessage: Human-readable output message set by the script. + - debugOutputMessage: The script debug output. + - resultJson: The result JSON if it exists (optional). + + Raises: + APIError: If the API request fails. + """ + body = {"connector": connector} + + if agent_identifier is not None: + body["agentIdentifier"] = agent_identifier + + return chronicle_request( + client, + method="POST", + endpoint_path=f"integrations/{format_resource_id(integration_name)}" + f"/connectors:executeTest", + api_version=api_version, + json=body, + ) + + +def get_integration_connector_template( + client: "ChronicleClient", + integration_name: str, + api_version: APIVersion | None = APIVersion.V1BETA, +) -> dict[str, Any]: + """Retrieve a default Python script template for a + new integration connector. + + Use this method to rapidly initialize the development of a new connector. + + Args: + client: ChronicleClient instance. + integration_name: Name of the integration to fetch the template for. + api_version: API version to use for the request. Default is V1BETA. + + Returns: + Dict containing the IntegrationConnector template. + + Raises: + APIError: If the API request fails. + """ + return chronicle_request( + client, + method="GET", + endpoint_path=( + f"integrations/{format_resource_id(integration_name)}/" + f"connectors:fetchTemplate" + ), + api_version=api_version, + ) diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index 0074bc53..06d81da9 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -13,6 +13,7 @@ # limitations under the License. # """Data models for Chronicle API responses.""" + import json import sys from dataclasses import asdict, dataclass, field @@ -73,6 +74,686 @@ class DetectionType(StrEnum): CASE = "DETECTION_TYPE_CASE" +class PythonVersion(str, Enum): + """Python version for compatibility checks.""" + + UNSPECIFIED = "PYTHON_VERSION_UNSPECIFIED" + PYTHON_2_7 = "V2_7" + PYTHON_3_7 = "V3_7" + PYTHON_3_11 = "V3_11" + + +class DiffType(str, Enum): + """Type of diff to retrieve.""" + + COMMERCIAL = "Commercial" + PRODUCTION = "Production" + STAGING = "Staging" + + +class TargetMode(str, Enum): + """Target mode for integration transition.""" + + PRODUCTION = "Production" + STAGING = "Staging" + + +class IntegrationType(str, Enum): + """Type of integration.""" + + UNSPECIFIED = "INTEGRATION_TYPE_UNSPECIFIED" + RESPONSE = "RESPONSE" + EXTENSION = "EXTENSION" + + +class IntegrationParamType(str, Enum): + """Type of integration parameter.""" + + PARAM_TYPE_UNSPECIFIED = "PARAM_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + + +@dataclass +class IntegrationParam: + """A parameter definition for a Chronicle SOAR integration. + + Attributes: + display_name: Human-readable label shown in the UI. + property_name: The programmatic key used in code/config. + type: The data type of the parameter (see IntegrationParamType). + description: Optional. Explanation of what the parameter is for. + mandatory: Whether the parameter must be supplied. Defaults to False. + default_value: Optional. Pre-filled value shown in the UI. + """ + + display_name: str + property_name: str + type: IntegrationParamType + mandatory: bool + description: str | None = None + default_value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "propertyName": self.property_name, + "type": str(self.type.value), + "mandatory": self.mandatory, + } + if self.description is not None: + data["description"] = self.description + if self.default_value is not None: + data["defaultValue"] = self.default_value + return data + + +class ActionParamType(str, Enum): + """Action parameter types for Chronicle SOAR integration actions.""" + + STRING = "STRING" + BOOLEAN = "BOOLEAN" + WFS_REPOSITORY = "WFS_REPOSITORY" + USER_REPOSITORY = "USER_REPOSITORY" + STAGES_REPOSITORY = "STAGES_REPOSITORY" + CLOSE_CASE_REASON_REPOSITORY = "CLOSE_CASE_REASON_REPOSITORY" + CLOSE_CASE_ROOT_CAUSE_REPOSITORY = "CLOSE_CASE_ROOT_CAUSE_REPOSITORY" + PRIORITIES_REPOSITORY = "PRIORITIES_REPOSITORY" + EMAIL_CONTENT = "EMAIL_CONTENT" + CONTENT = "CONTENT" + PASSWORD = "PASSWORD" + ENTITY_TYPE = "ENTITY_TYPE" + MULTI_VALUES = "MULTI_VALUES" + LIST = "LIST" + CODE = "CODE" + MULTIPLE_CHOICE_PARAMETER = "MULTIPLE_CHOICE_PARAMETER" + + +class ActionType(str, Enum): + """Action types for Chronicle SOAR integration actions.""" + + UNSPECIFIED = "ACTION_TYPE_UNSPECIFIED" + STANDARD = "STANDARD" + AI_AGENT = "AI_AGENT" + + +@dataclass +class ActionParameter: + """A parameter definition for a Chronicle SOAR integration action. + + Attributes: + display_name: The parameter's display name. Maximum 150 characters. + type: The parameter's type. + description: The parameter's description. Maximum 150 characters. + mandatory: Whether the parameter is mandatory. + default_value: The default value of the parameter. + Maximum 150 characters. + optional_values: Parameter's optional values. Maximum 50 items. + """ + + display_name: str + type: ActionParamType + description: str + mandatory: bool + default_value: str | None = None + optional_values: list[str] | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "type": str(self.type.value), + "description": self.description, + "mandatory": self.mandatory, + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.optional_values is not None: + data["optionalValues"] = self.optional_values + return data + + +class ParamType(str, Enum): + """Parameter types for Chronicle SOAR integration functions.""" + + UNSPECIFIED = "PARAM_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + NUMERICAL_VALUES = "NUMERICAL_VALUES" + + +class ConnectorParamMode(str, Enum): + """Parameter modes for Chronicle SOAR integration connectors.""" + + UNSPECIFIED = "PARAM_MODE_UNSPECIFIED" + REGULAR = "REGULAR" + CONNECTIVITY = "CONNECTIVITY" + SCRIPT = "SCRIPT" + + +class ConnectorRuleType(str, Enum): + """Rule types for Chronicle SOAR integration connectors.""" + + UNSPECIFIED = "RULE_TYPE_UNSPECIFIED" + ALLOW_LIST = "ALLOW_LIST" + BLOCK_LIST = "BLOCK_LIST" + + +@dataclass +class ConnectorParameter: + """A parameter definition for a Chronicle SOAR integration connector. + + Attributes: + display_name: The parameter's display name. + type: The parameter's type. + mode: The parameter's mode. + mandatory: Whether the parameter is mandatory for configuring a + connector instance. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + description: The parameter's description. + advanced: The parameter's advanced flag. + """ + + display_name: str + type: ParamType + mode: ConnectorParamMode + mandatory: bool + default_value: str | None = None + description: str | None = None + advanced: bool | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "type": str(self.type.value), + "mode": str(self.mode.value), + "mandatory": self.mandatory, + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.description is not None: + data["description"] = self.description + if self.advanced is not None: + data["advanced"] = self.advanced + return data + + +@dataclass +class IntegrationJobInstanceParameter: + """A parameter instance for a Chronicle SOAR integration job instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring a job instance. + + Attributes: + value: The value of the parameter. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + +class ScheduleType(str, Enum): + """Schedule types for Chronicle SOAR integration job + instance advanced config.""" + + UNSPECIFIED = "SCHEDULE_TYPE_UNSPECIFIED" + ONCE = "ONCE" + DAILY = "DAILY" + WEEKLY = "WEEKLY" + MONTHLY = "MONTHLY" + + +class DayOfWeek(str, Enum): + """Days of the week for Chronicle SOAR weekly schedule details.""" + + UNSPECIFIED = "DAY_OF_WEEK_UNSPECIFIED" + MONDAY = "MONDAY" + TUESDAY = "TUESDAY" + WEDNESDAY = "WEDNESDAY" + THURSDAY = "THURSDAY" + FRIDAY = "FRIDAY" + SATURDAY = "SATURDAY" + SUNDAY = "SUNDAY" + + +@dataclass +class Date: + """A calendar date for Chronicle SOAR schedule details. + + Attributes: + year: The year. + month: The month of the year (1-12). + day: The day of the month (1-31). + """ + + year: int + month: int + day: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return {"year": self.year, "month": self.month, "day": self.day} + + +@dataclass +class TimeOfDay: + """A time of day for Chronicle SOAR schedule details. + + Attributes: + hours: The hour of the day (0-23). + minutes: The minute of the hour (0-59). + seconds: The second of the minute (0-59). + nanos: The nanoseconds of the second (0-999999999). + """ + + hours: int + minutes: int + seconds: int = 0 + nanos: int = 0 + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "hours": self.hours, + "minutes": self.minutes, + "seconds": self.seconds, + "nanos": self.nanos, + } + + +@dataclass +class OneTimeScheduleDetails: + """One-time schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The date to run the job. + time: The time to run the job. + """ + + start_date: Date + time: TimeOfDay + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "time": self.time.to_dict(), + } + + +@dataclass +class DailyScheduleDetails: + """Daily schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + time: The time to run the job. + interval: The day interval. + """ + + start_date: Date + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class WeeklyScheduleDetails: + """Weekly schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + days: The days of the week to run the job. + time: The time to run the job. + interval: The week interval. + """ + + start_date: Date + days: list[DayOfWeek] + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "days": [d.value for d in self.days], + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class MonthlyScheduleDetails: + """Monthly schedule details for a Chronicle SOAR job instance. + + Attributes: + start_date: The start date. + day: The day of the month to run the job. + time: The time to run the job. + interval: The month interval. + """ + + start_date: Date + day: int + time: TimeOfDay + interval: int + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "startDate": self.start_date.to_dict(), + "day": self.day, + "time": self.time.to_dict(), + "interval": self.interval, + } + + +@dataclass +class AdvancedConfig: + """Advanced scheduling configuration for a Chronicle SOAR job instance. + + Exactly one of the schedule detail fields should be provided, corresponding + to the schedule_type. + + Attributes: + time_zone: The zone id. + schedule_type: The schedule type. + one_time_schedule: One-time schedule details. Use with ONCE. + daily_schedule: Daily schedule details. Use with DAILY. + weekly_schedule: Weekly schedule details. Use with WEEKLY. + monthly_schedule: Monthly schedule details. Use with MONTHLY. + """ + + time_zone: str + schedule_type: ScheduleType + one_time_schedule: OneTimeScheduleDetails | None = None + daily_schedule: DailyScheduleDetails | None = None + weekly_schedule: WeeklyScheduleDetails | None = None + monthly_schedule: MonthlyScheduleDetails | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "timeZone": self.time_zone, + "scheduleType": str(self.schedule_type.value), + } + if self.one_time_schedule is not None: + data["oneTimeSchedule"] = self.one_time_schedule.to_dict() + if self.daily_schedule is not None: + data["dailySchedule"] = self.daily_schedule.to_dict() + if self.weekly_schedule is not None: + data["weeklySchedule"] = self.weekly_schedule.to_dict() + if self.monthly_schedule is not None: + data["monthlySchedule"] = self.monthly_schedule.to_dict() + return data + + +@dataclass +class JobParameter: + """A parameter definition for a Chronicle SOAR integration job. + + Attributes: + id: The parameter's id. + display_name: The parameter's display name. + description: The parameter's description. + mandatory: Whether the parameter is mandatory. + type: The parameter's type. + default_value: The default value of the parameter. + """ + + id: int + display_name: str + description: str + mandatory: bool + type: ParamType + default_value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "id": self.id, + "displayName": self.display_name, + "description": self.description, + "mandatory": self.mandatory, + "type": str(self.type.value), + } + if self.default_value is not None: + data["defaultValue"] = self.default_value + return data + + +class IntegrationParameterType(str, Enum): + """Parameter types for Chronicle SOAR integration instances.""" + + UNSPECIFIED = "INTEGRATION_PARAMETER_TYPE_UNSPECIFIED" + BOOLEAN = "BOOLEAN" + INT = "INT" + STRING = "STRING" + PASSWORD = "PASSWORD" + IP = "IP" + IP_OR_HOST = "IP_OR_HOST" + URL = "URL" + DOMAIN = "DOMAIN" + EMAIL = "EMAIL" + VALUES_LIST = "VALUES_LIST" + VALUES_AS_SEMICOLON_SEPARATED_STRING = ( + "VALUES_AS_SEMICOLON_SEPARATED_STRING" + ) + MULTI_VALUES_SELECTION = "MULTI_VALUES_SELECTION" + SCRIPT = "SCRIPT" + FILTER_LIST = "FILTER_LIST" + + +@dataclass +class IntegrationInstanceParameter: + """A parameter instance for a Chronicle SOAR integration instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring an integration instance. + + Attributes: + value: The parameter's value. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + +class ConnectorConnectivityStatus(str, Enum): + """Connectivity status for Chronicle SOAR connector instances.""" + + LIVE = "LIVE" + NOT_LIVE = "NOT_LIVE" + + +@dataclass +class ConnectorInstanceParameter: + """A parameter instance for a Chronicle SOAR connector instance. + + Note: Most fields are output-only and will be populated by the API. + Only value needs to be provided when configuring a connector instance. + + Attributes: + value: The value of the parameter. + """ + + value: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = {} + if self.value is not None: + data["value"] = self.value + return data + + +class TransformerType(str, Enum): + """Transformer types for Chronicle SOAR integration transformers.""" + + UNSPECIFIED = "TRANSFORMER_TYPE_UNSPECIFIED" + BUILT_IN = "BUILT_IN" + CUSTOM = "CUSTOM" + + +@dataclass +class TransformerDefinitionParameter: + """A parameter definition for a Chronicle SOAR transformer definition. + + Attributes: + display_name: The parameter's display name. May contain letters, + numbers, and underscores. Maximum 150 characters. + mandatory: Whether the parameter is mandatory for configuring a + transformer instance. + id: The parameter's id. Server-generated on creation; must be + provided when updating an existing parameter. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + description: The parameter's description. Maximum 2050 characters. + """ + + display_name: str + mandatory: bool + id: str | None = None + default_value: str | None = None + description: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "mandatory": self.mandatory, + } + if self.id is not None: + data["id"] = self.id + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.description is not None: + data["description"] = self.description + return data + + +class LogicalOperatorType(str, Enum): + """Logical operator types for Chronicle SOAR + integration logical operators.""" + + UNSPECIFIED = "LOGICAL_OPERATOR_TYPE_UNSPECIFIED" + BUILT_IN = "BUILT_IN" + CUSTOM = "CUSTOM" + + +@dataclass +class IntegrationLogicalOperatorParameter: + """A parameter definition for a Chronicle SOAR logical operator. + + Attributes: + display_name: The parameter's display name. May contain letters, + numbers, and underscores. Maximum 150 characters. + mandatory: Whether the parameter is mandatory for configuring a + logical operator instance. + id: The parameter's id. Server-generated on creation; must be + provided when updating an existing parameter. + default_value: The default value of the parameter. Required for + boolean and mandatory parameters. + order: The parameter's order in the parameters list. + description: The parameter's description. Maximum 2050 characters. + """ + + display_name: str + mandatory: bool + id: str | None = None + default_value: str | None = None + order: int | None = None + description: str | None = None + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + data: dict = { + "displayName": self.display_name, + "mandatory": self.mandatory, + } + if self.id is not None: + data["id"] = self.id + if self.default_value is not None: + data["defaultValue"] = self.default_value + if self.order is not None: + data["order"] = self.order + if self.description is not None: + data["description"] = self.description + return data + + +@dataclass +class ConnectorRule: + """A rule definition for a Chronicle SOAR integration connector. + + Attributes: + display_name: Connector's rule data name. + type: Connector's rule data type. + """ + + display_name: str + type: ConnectorRuleType + + def to_dict(self) -> dict: + """Serialize to the dict shape expected by the Chronicle API.""" + return { + "displayName": self.display_name, + "type": str(self.type.value), + } + + @dataclass class TimeInterval: """Time interval with start and end times.""" diff --git a/src/secops/chronicle/stats.py b/src/secops/chronicle/stats.py index 99b46309..42e31aba 100644 --- a/src/secops/chronicle/stats.py +++ b/src/secops/chronicle/stats.py @@ -13,6 +13,7 @@ # limitations under the License. # """Statistics functionality for Chronicle searches.""" + from datetime import datetime from typing import Any, TYPE_CHECKING diff --git a/src/secops/chronicle/utils/format_utils.py b/src/secops/chronicle/utils/format_utils.py index b6567528..126ae503 100644 --- a/src/secops/chronicle/utils/format_utils.py +++ b/src/secops/chronicle/utils/format_utils.py @@ -65,3 +65,34 @@ def parse_json_list( except ValueError as e: raise APIError(f"Invalid {field_name} JSON") from e return value + + +# pylint: disable=line-too-long +def build_patch_body( + field_map: list[tuple[str, str, Any]], + update_mask: str | None = None, +) -> tuple[dict[str, Any], dict[str, Any] | None]: + """Build a request body and params dict for a PATCH request. + + Args: + field_map: List of (api_key, mask_key, value) tuples for + each optional field. + update_mask: Explicit update mask. If provided, + overrides the auto-generated mask. + + Returns: + Tuple of (body, params) where params contains the updateMask or is None. + """ + body = { + api_key: value for api_key, _, value in field_map if value is not None + } + mask_fields = [ + mask_key for _, mask_key, value in field_map if value is not None + ] + + resolved_mask = update_mask or ( + ",".join(mask_fields) if mask_fields else None + ) + params = {"updateMask": resolved_mask} if resolved_mask else None + + return body, params diff --git a/src/secops/chronicle/utils/request_utils.py b/src/secops/chronicle/utils/request_utils.py index 43f2d885..c3b2cd8a 100644 --- a/src/secops/chronicle/utils/request_utils.py +++ b/src/secops/chronicle/utils/request_utils.py @@ -14,7 +14,7 @@ # """Helper functions for Chronicle.""" -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import requests from google.auth.exceptions import GoogleAuthError @@ -297,3 +297,66 @@ def chronicle_request( ) return data + + +def chronicle_request_bytes( + client: "ChronicleClient", + method: str, + endpoint_path: str, + *, + api_version: str = APIVersion.V1, + params: Optional[dict[str, Any]] = None, + headers: Optional[dict[str, Any]] = None, + expected_status: int | set[int] | tuple[int, ...] | list[int] = 200, + error_message: str | None = None, + timeout: int | None = None, +) -> bytes: + base = f"{client.base_url(api_version)}/{client.instance_id}" + + if endpoint_path.startswith(":"): + url = f"{base}{endpoint_path}" + else: + url = f'{base}/{endpoint_path.lstrip("/")}' + + try: + response = client.session.request( + method=method, + url=url, + params=params, + headers=headers, + timeout=timeout, + stream=True, + ) + except GoogleAuthError as exc: + base_msg = error_message or "Google authentication failed" + raise APIError(f"{base_msg}: authentication_error={exc}") from exc + except requests.RequestException as exc: + base_msg = error_message or "API request failed" + raise APIError( + f"{base_msg}: method={method}, url={url}, " + f"request_error={exc.__class__.__name__}, detail={exc}" + ) from exc + + if isinstance(expected_status, (set, tuple, list)): + status_ok = response.status_code in expected_status + else: + status_ok = response.status_code == expected_status + + if not status_ok: + # try json for detail, else preview text + try: + data = response.json() + raise APIError( + f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"status={response.status_code}, response={data}" + ) from None + except ValueError: + preview = _safe_body_preview( + getattr(response, "text", ""), limit=MAX_BODY_CHARS + ) + raise APIError( + f"{error_message or 'API request failed'}: method={method}, url={url}, " + f"status={response.status_code}, response_text={preview}" + ) from None + + return response.content diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index 4c483656..65b787f2 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -39,6 +39,9 @@ from secops.cli.commands.udm_search import setup_udm_search_view_command from secops.cli.commands.watchlist import setup_watchlist_command from secops.cli.commands.rule_retrohunt import setup_rule_retrohunt_command +from secops.cli.commands.integration.integration_client import ( + setup_integrations_command, +) from secops.cli.utils.common_args import add_chronicle_args, add_common_args from secops.cli.utils.config_utils import load_config from secops.exceptions import AuthenticationError, SecOpsError @@ -189,6 +192,7 @@ def build_parser() -> argparse.ArgumentParser: setup_dashboard_query_command(subparsers) setup_watchlist_command(subparsers) setup_rule_retrohunt_command(subparsers) + setup_integrations_command(subparsers) return parser diff --git a/src/secops/cli/commands/__init__.py b/src/secops/cli/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/cli/commands/integration/__init__.py b/src/secops/cli/commands/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/secops/cli/commands/integration/connector_context_properties.py b/src/secops/cli/commands/integration/connector_context_properties.py new file mode 100644 index 00000000..46b2d936 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_context_properties.py @@ -0,0 +1,375 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Google SecOps CLI connector context properties commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_context_properties_command(subparsers): + """Setup connector context properties command""" + properties_parser = subparsers.add_parser( + "connector-context-properties", + help="Manage connector context properties", + ) + lvl1 = properties_parser.add_subparsers( + dest="connector_context_properties_command", + help="Connector context properties command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List connector context properties" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + list_parser.add_argument( + "--context-id", + type=str, + help="Context ID to filter properties", + dest="context_id", + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing properties", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing properties", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_connector_context_properties_list_command, + ) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector context property" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + get_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to get", + dest="property_id", + required=True, + ) + get_parser.set_defaults( + func=handle_connector_context_properties_get_command + ) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete a connector context property" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + delete_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to delete", + dest="property_id", + required=True, + ) + delete_parser.set_defaults( + func=handle_connector_context_properties_delete_command + ) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new connector context property" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--context-id", + type=str, + help="Context ID for the property", + dest="context_id", + required=True, + ) + create_parser.add_argument( + "--key", + type=str, + help="Key for the property", + dest="key", + required=True, + ) + create_parser.add_argument( + "--value", + type=str, + help="Value for the property", + dest="value", + required=True, + ) + create_parser.set_defaults( + func=handle_connector_context_properties_create_command + ) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update a connector context property" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--context-id", + type=str, + help="Context ID of the property", + dest="context_id", + required=True, + ) + update_parser.add_argument( + "--property-id", + type=str, + help="ID of the property to update", + dest="property_id", + required=True, + ) + update_parser.add_argument( + "--value", + type=str, + help="New value for the property", + dest="value", + required=True, + ) + update_parser.set_defaults( + func=handle_connector_context_properties_update_command + ) + + # clear-all command + clear_parser = lvl1.add_parser( + "clear-all", help="Delete all connector context properties" + ) + clear_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + clear_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + clear_parser.add_argument( + "--context-id", + type=str, + help="Context ID to clear all properties for", + dest="context_id", + required=True, + ) + clear_parser.set_defaults( + func=handle_connector_context_properties_clear_command + ) + + +def handle_connector_context_properties_list_command(args, chronicle): + """Handle connector context properties list command""" + try: + out = chronicle.list_connector_context_properties( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error listing connector context properties: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_get_command(args, chronicle): + """Handle connector context property get command""" + try: + out = chronicle.get_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector context property: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_context_properties_delete_command(args, chronicle): + """Handle connector context property delete command""" + try: + chronicle.delete_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + ) + print( + f"Connector context property " + f"{args.property_id} deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error deleting connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_create_command(args, chronicle): + """Handle connector context property create command""" + try: + out = chronicle.create_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + key=args.key, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error creating connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_update_command(args, chronicle): + """Handle connector context property update command""" + try: + out = chronicle.update_connector_context_property( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + context_property_id=args.property_id, + value=args.value, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error updating connector context property: {e}", file=sys.stderr + ) + sys.exit(1) + + +def handle_connector_context_properties_clear_command(args, chronicle): + """Handle clear all connector context properties command""" + try: + chronicle.delete_all_connector_context_properties( + integration_name=args.integration_name, + connector_id=args.connector_id, + context_id=args.context_id, + ) + print( + f"All connector context properties for context " + f"{args.context_id} cleared successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error clearing connector context properties: {e}", file=sys.stderr + ) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instance_logs.py b/src/secops/cli/commands/integration/connector_instance_logs.py new file mode 100644 index 00000000..b67e35f2 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_instance_logs.py @@ -0,0 +1,142 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Google SecOps CLI connector instance logs commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_instance_logs_command(subparsers): + """Setup connector instance logs command""" + logs_parser = subparsers.add_parser( + "connector-instance-logs", + help="View connector instance logs", + ) + lvl1 = logs_parser.add_subparsers( + dest="connector_instance_logs_command", + help="Connector instance logs command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List connector instance logs") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + list_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing logs", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing logs", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_instance_logs_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector instance log" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + get_parser.add_argument( + "--log-id", + type=str, + help="ID of the log to get", + dest="log_id", + required=True, + ) + get_parser.set_defaults(func=handle_connector_instance_logs_get_command) + + +def handle_connector_instance_logs_list_command(args, chronicle): + """Handle connector instance logs list command""" + try: + out = chronicle.list_connector_instance_logs( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector instance logs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instance_logs_get_command(args, chronicle): + """Handle connector instance log get command""" + try: + out = chronicle.get_connector_instance_log( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + connector_instance_log_id=args.log_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector instance log: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_instances.py b/src/secops/cli/commands/integration/connector_instances.py new file mode 100644 index 00000000..df68bfde --- /dev/null +++ b/src/secops/cli/commands/integration/connector_instances.py @@ -0,0 +1,473 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Google SecOps CLI connector instances commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_instances_command(subparsers): + """Setup connector instances command""" + instances_parser = subparsers.add_parser( + "connector-instances", + help="Manage connector instances", + ) + lvl1 = instances_parser.add_subparsers( + dest="connector_instances_command", + help="Connector instances command", + ) + + # list command + list_parser = lvl1.add_parser("list", help="List connector instances") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing instances", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing instances", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_instances_list_command) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get a specific connector instance" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + get_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to get", + dest="connector_instance_id", + required=True, + ) + get_parser.set_defaults(func=handle_connector_instances_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete a connector instance" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to delete", + dest="connector_instance_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connector_instances_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new connector instance" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--environment", + type=str, + help="Environment for the connector instance", + dest="environment", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the connector instance", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--interval-seconds", + type=int, + help="Interval in seconds for connector execution", + dest="interval_seconds", + ) + create_parser.add_argument( + "--timeout-seconds", + type=int, + help="Timeout in seconds for connector execution", + dest="timeout_seconds", + ) + create_parser.add_argument( + "--enabled", + action="store_true", + help="Enable the connector instance", + dest="enabled", + ) + create_parser.set_defaults(func=handle_connector_instances_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update a connector instance" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to update", + dest="connector_instance_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the connector instance", + dest="display_name", + ) + update_parser.add_argument( + "--interval-seconds", + type=int, + help="New interval in seconds for connector execution", + dest="interval_seconds", + ) + update_parser.add_argument( + "--timeout-seconds", + type=int, + help="New timeout in seconds for connector execution", + dest="timeout_seconds", + ) + update_parser.add_argument( + "--enabled", + type=str, + choices=["true", "false"], + help="Enable or disable the connector instance", + dest="enabled", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_connector_instances_update_command) + + # fetch-latest command + fetch_parser = lvl1.add_parser( + "fetch-latest", + help="Get the latest definition of a connector instance", + ) + fetch_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + fetch_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + fetch_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + fetch_parser.set_defaults( + func=handle_connector_instances_fetch_latest_command + ) + + # set-logs command + logs_parser = lvl1.add_parser( + "set-logs", + help="Enable or disable log collection for a connector instance", + ) + logs_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + logs_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + logs_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance", + dest="connector_instance_id", + required=True, + ) + logs_parser.add_argument( + "--enabled", + type=str, + choices=["true", "false"], + help="Enable or disable log collection", + dest="enabled", + required=True, + ) + logs_parser.set_defaults(func=handle_connector_instances_set_logs_command) + + # run-ondemand command + run_parser = lvl1.add_parser( + "run-ondemand", + help="Run a connector instance on demand", + ) + run_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + run_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + run_parser.add_argument( + "--connector-instance-id", + type=str, + help="ID of the connector instance to run", + dest="connector_instance_id", + required=True, + ) + run_parser.set_defaults( + func=handle_connector_instances_run_ondemand_command + ) + + +def handle_connector_instances_list_command(args, chronicle): + """Handle connector instances list command""" + try: + out = chronicle.list_connector_instances( + integration_name=args.integration_name, + connector_id=args.connector_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector instances: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_get_command(args, chronicle): + """Handle connector instance get command""" + try: + out = chronicle.get_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_delete_command(args, chronicle): + """Handle connector instance delete command""" + try: + chronicle.delete_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + print( + f"Connector instance {args.connector_instance_id}" + f" deleted successfully" + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_create_command(args, chronicle): + """Handle connector instance create command""" + try: + out = chronicle.create_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + environment=args.environment, + display_name=args.display_name, + interval_seconds=args.interval_seconds, + timeout_seconds=args.timeout_seconds, + enabled=args.enabled, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_update_command(args, chronicle): + """Handle connector instance update command""" + try: + # Convert enabled string to boolean if provided + enabled = None + if args.enabled: + enabled = args.enabled.lower() == "true" + + out = chronicle.update_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + display_name=args.display_name, + interval_seconds=args.interval_seconds, + timeout_seconds=args.timeout_seconds, + enabled=enabled, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_fetch_latest_command(args, chronicle): + """Handle fetch latest connector instance definition command""" + try: + out = chronicle.get_connector_instance_latest_definition( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error fetching latest connector instance: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_set_logs_command(args, chronicle): + """Handle set connector instance logs collection command""" + try: + enabled = args.enabled.lower() == "true" + out = chronicle.set_connector_instance_logs_collection( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + enabled=enabled, + ) + status = "enabled" if enabled else "disabled" + print(f"Log collection {status} for connector instance successfully") + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error setting connector instance logs: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_instances_run_ondemand_command(args, chronicle): + """Handle run connector instance on demand command""" + try: + # Get the connector instance first + connector_instance = chronicle.get_connector_instance( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + ) + out = chronicle.run_connector_instance_on_demand( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector_instance_id=args.connector_instance_id, + connector_instance=connector_instance, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Error running connector instance on demand: {e}", file=sys.stderr + ) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connector_revisions.py b/src/secops/cli/commands/integration/connector_revisions.py new file mode 100644 index 00000000..779888c9 --- /dev/null +++ b/src/secops/cli/commands/integration/connector_revisions.py @@ -0,0 +1,217 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Google SecOps CLI integration connector revisions commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connector_revisions_command(subparsers): + """Setup integration connector revisions command""" + revisions_parser = subparsers.add_parser( + "connector-revisions", + help="Manage integration connector revisions", + ) + lvl1 = revisions_parser.add_subparsers( + dest="connector_revisions_command", + help="Integration connector revisions command", + ) + + # list command + list_parser = lvl1.add_parser( + "list", help="List integration connector revisions" + ) + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + list_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing revisions", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing revisions", + dest="order_by", + ) + list_parser.set_defaults(func=handle_connector_revisions_list_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration connector revision" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + delete_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to delete", + dest="revision_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connector_revisions_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration connector revision" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + create_parser.add_argument( + "--comment", + type=str, + help="Comment describing the revision", + dest="comment", + ) + create_parser.set_defaults(func=handle_connector_revisions_create_command) + + # rollback command + rollback_parser = lvl1.add_parser( + "rollback", help="Rollback connector to a previous revision" + ) + rollback_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + rollback_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector", + dest="connector_id", + required=True, + ) + rollback_parser.add_argument( + "--revision-id", + type=str, + help="ID of the revision to rollback to", + dest="revision_id", + required=True, + ) + rollback_parser.set_defaults( + func=handle_connector_revisions_rollback_command, + ) + + +def handle_connector_revisions_list_command(args, chronicle): + """Handle integration connector revisions list command""" + try: + out = chronicle.list_integration_connector_revisions( + integration_name=args.integration_name, + connector_id=args.connector_id, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing connector revisions: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_delete_command(args, chronicle): + """Handle integration connector revision delete command""" + try: + chronicle.delete_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + revision_id=args.revision_id, + ) + print(f"Connector revision {args.revision_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting connector revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_create_command(args, chronicle): + """Handle integration connector revision create command""" + try: + # Get the current connector to create a revision + connector = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + out = chronicle.create_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector=connector, + comment=args.comment, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating connector revision: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connector_revisions_rollback_command(args, chronicle): + """Handle integration connector revision rollback command""" + try: + out = chronicle.rollback_integration_connector_revision( + integration_name=args.integration_name, + connector_id=args.connector_id, + revision_id=args.revision_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error rolling back connector revision: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/connectors.py b/src/secops/cli/commands/integration/connectors.py new file mode 100644 index 00000000..fe8e03ef --- /dev/null +++ b/src/secops/cli/commands/integration/connectors.py @@ -0,0 +1,325 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Google SecOps CLI integration connectors commands""" + +import sys + +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.common_args import ( + add_pagination_args, + add_as_list_arg, +) + + +def setup_connectors_command(subparsers): + """Setup integration connectors command""" + connectors_parser = subparsers.add_parser( + "connectors", + help="Manage integration connectors", + ) + lvl1 = connectors_parser.add_subparsers( + dest="connectors_command", help="Integration connectors command" + ) + + # list command + list_parser = lvl1.add_parser("list", help="List integration connectors") + list_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + add_pagination_args(list_parser) + add_as_list_arg(list_parser) + list_parser.add_argument( + "--filter-string", + type=str, + help="Filter string for listing connectors", + dest="filter_string", + ) + list_parser.add_argument( + "--order-by", + type=str, + help="Order by string for listing connectors", + dest="order_by", + ) + list_parser.set_defaults( + func=handle_connectors_list_command, + ) + + # get command + get_parser = lvl1.add_parser( + "get", help="Get integration connector details" + ) + get_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + get_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to get", + dest="connector_id", + required=True, + ) + get_parser.set_defaults(func=handle_connectors_get_command) + + # delete command + delete_parser = lvl1.add_parser( + "delete", help="Delete an integration connector" + ) + delete_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + delete_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to delete", + dest="connector_id", + required=True, + ) + delete_parser.set_defaults(func=handle_connectors_delete_command) + + # create command + create_parser = lvl1.add_parser( + "create", help="Create a new integration connector" + ) + create_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + create_parser.add_argument( + "--display-name", + type=str, + help="Display name for the connector", + dest="display_name", + required=True, + ) + create_parser.add_argument( + "--code", + type=str, + help="Python code for the connector", + dest="code", + required=True, + ) + create_parser.add_argument( + "--description", + type=str, + help="Description of the connector", + dest="description", + ) + create_parser.add_argument( + "--connector-id", + type=str, + help="Custom ID for the connector", + dest="connector_id", + ) + create_parser.set_defaults(func=handle_connectors_create_command) + + # update command + update_parser = lvl1.add_parser( + "update", help="Update an integration connector" + ) + update_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + update_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to update", + dest="connector_id", + required=True, + ) + update_parser.add_argument( + "--display-name", + type=str, + help="New display name for the connector", + dest="display_name", + ) + update_parser.add_argument( + "--code", + type=str, + help="New Python code for the connector", + dest="code", + ) + update_parser.add_argument( + "--description", + type=str, + help="New description for the connector", + dest="description", + ) + update_parser.add_argument( + "--update-mask", + type=str, + help="Comma-separated list of fields to update", + dest="update_mask", + ) + update_parser.set_defaults(func=handle_connectors_update_command) + + # test command + test_parser = lvl1.add_parser( + "test", help="Execute an integration connector test" + ) + test_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + test_parser.add_argument( + "--connector-id", + type=str, + help="ID of the connector to test", + dest="connector_id", + required=True, + ) + test_parser.set_defaults(func=handle_connectors_test_command) + + # template command + template_parser = lvl1.add_parser( + "template", + help="Get a template for creating a connector", + ) + template_parser.add_argument( + "--integration-name", + type=str, + help="Name of the integration", + dest="integration_name", + required=True, + ) + template_parser.set_defaults(func=handle_connectors_template_command) + + +def handle_connectors_list_command(args, chronicle): + """Handle integration connectors list command""" + try: + out = chronicle.list_integration_connectors( + integration_name=args.integration_name, + page_size=args.page_size, + page_token=args.page_token, + filter_string=args.filter_string, + order_by=args.order_by, + as_list=args.as_list, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error listing integration connectors: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_get_command(args, chronicle): + """Handle integration connector get command""" + try: + out = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_delete_command(args, chronicle): + """Handle integration connector delete command""" + try: + chronicle.delete_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + print(f"Connector {args.connector_id} deleted successfully") + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error deleting integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_create_command(args, chronicle): + """Handle integration connector create command""" + try: + out = chronicle.create_integration_connector( + integration_name=args.integration_name, + display_name=args.display_name, + code=args.code, + description=args.description, + connector_id=args.connector_id, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error creating integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_update_command(args, chronicle): + """Handle integration connector update command""" + try: + out = chronicle.update_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + display_name=args.display_name, + code=args.code, + description=args.description, + update_mask=args.update_mask, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error updating integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_test_command(args, chronicle): + """Handle integration connector test command""" + try: + # First get the connector to test + connector = chronicle.get_integration_connector( + integration_name=args.integration_name, + connector_id=args.connector_id, + ) + out = chronicle.execute_integration_connector_test( + integration_name=args.integration_name, + connector_id=args.connector_id, + connector=connector, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error testing integration connector: {e}", file=sys.stderr) + sys.exit(1) + + +def handle_connectors_template_command(args, chronicle): + """Handle get connector template command""" + try: + out = chronicle.get_integration_connector_template( + integration_name=args.integration_name, + ) + output_formatter(out, getattr(args, "output", "json")) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error getting connector template: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/secops/cli/commands/integration/integration_client.py b/src/secops/cli/commands/integration/integration_client.py new file mode 100644 index 00000000..ab068590 --- /dev/null +++ b/src/secops/cli/commands/integration/integration_client.py @@ -0,0 +1,42 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Top level arguments for integration commands""" + +from secops.cli.commands.integration import ( + connectors, + connector_revisions, + connector_context_properties, + connector_instance_logs, + connector_instances, +) + + +def setup_integrations_command(subparsers): + """Setup integration command""" + integrations_parser = subparsers.add_parser( + "integration", help="Manage SecOps integrations" + ) + lvl1 = integrations_parser.add_subparsers( + dest="integrations_command", help="Integrations command" + ) + + # Setup all subcommands under `integration` + connectors.setup_connectors_command(lvl1) + connector_revisions.setup_connector_revisions_command(lvl1) + connector_context_properties.setup_connector_context_properties_command( + lvl1 + ) + connector_instance_logs.setup_connector_instance_logs_command(lvl1) + connector_instances.setup_connector_instances_command(lvl1) diff --git a/tests/chronicle/integration/__init__.py b/tests/chronicle/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/chronicle/integration/test_connector_context_properties.py b/tests/chronicle/integration/test_connector_context_properties.py new file mode 100644 index 00000000..33941087 --- /dev/null +++ b/tests/chronicle/integration/test_connector_context_properties.py @@ -0,0 +1,561 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for Chronicle integration connector context properties functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_context_properties import ( + list_connector_context_properties, + get_connector_context_property, + delete_connector_context_property, + create_connector_context_property, + update_connector_context_property, + delete_all_connector_context_properties, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_context_properties tests -- + + +def test_list_connector_context_properties_success(chronicle_client): + """Test list_connector_context_properties delegates to paginated request.""" + expected = { + "contextProperties": [{"key": "prop1"}, {"key": "prop2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_context_properties.format_resource_id", + return_value="My Integration", + ): + result = list_connector_context_properties( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/contextProperties" in kwargs["path"] + assert kwargs["items_key"] == "contextProperties" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_context_properties_default_args(chronicle_client): + """Test list_connector_context_properties with default args.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_connector_context_properties_with_filters(chronicle_client): + """Test list_connector_context_properties with filter and order_by.""" + expected = {"contextProperties": [{"key": "prop1"}]} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='key = "prop1"', + order_by="key", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'key = "prop1"', + "orderBy": "key", + } + + +def test_list_connector_context_properties_as_list(chronicle_client): + """Test list_connector_context_properties returns list when as_list=True.""" + expected = [{"key": "prop1"}, {"key": "prop2"}] + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_context_properties_error(chronicle_client): + """Test list_connector_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + side_effect=APIError("Failed to list context properties"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list context properties" in str(exc_info.value) + + +# -- get_connector_context_property tests -- + + +def test_get_connector_context_property_success(chronicle_client): + """Test get_connector_context_property issues GET request.""" + expected = { + "name": "contextProperties/prop1", + "key": "prop1", + "value": "test-value", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_context_property_error(chronicle_client): + """Test get_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to get context property"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + assert "Failed to get context property" in str(exc_info.value) + + +# -- delete_connector_context_property tests -- + + +def test_delete_connector_context_property_success(chronicle_client): + """Test delete_connector_context_property issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_connector_context_property_error(chronicle_client): + """Test delete_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to delete context property"), + ): + with pytest.raises(APIError) as exc_info: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + ) + assert "Failed to delete context property" in str(exc_info.value) + + +# -- create_connector_context_property tests -- + + +def test_create_connector_context_property_required_fields_only(chronicle_client): + """Test create_connector_context_property with required fields only.""" + expected = {"name": "contextProperties/new", "value": "test-value"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors/c1/contextProperties", + api_version=APIVersion.V1BETA, + json={"value": "test-value"}, + ) + + +def test_create_connector_context_property_with_key(chronicle_client): + """Test create_connector_context_property includes key when provided.""" + expected = {"name": "contextProperties/custom-key", "value": "test-value"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + key="custom-key", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["value"] == "test-value" + assert kwargs["json"]["key"] == "custom-key" + + +def test_create_connector_context_property_error(chronicle_client): + """Test create_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to create context property"), + ): + with pytest.raises(APIError) as exc_info: + create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + ) + assert "Failed to create context property" in str(exc_info.value) + + +# -- update_connector_context_property tests -- + + +def test_update_connector_context_property_success(chronicle_client): + """Test update_connector_context_property updates value.""" + expected = { + "name": "contextProperties/prop1", + "value": "updated-value", + } + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "PATCH" + assert "connectors/c1/contextProperties/prop1" in kwargs["endpoint_path"] + assert kwargs["json"]["value"] == "updated-value" + assert kwargs["params"]["updateMask"] == "value" + + +def test_update_connector_context_property_with_custom_mask(chronicle_client): + """Test update_connector_context_property with custom update_mask.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + update_mask="value", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["params"]["updateMask"] == "value" + + +def test_update_connector_context_property_error(chronicle_client): + """Test update_connector_context_property raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to update context property"), + ): + with pytest.raises(APIError) as exc_info: + update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + ) + assert "Failed to update context property" in str(exc_info.value) + + +# -- delete_all_connector_context_properties tests -- + + +def test_delete_all_connector_context_properties_success(chronicle_client): + """Test delete_all_connector_context_properties issues POST request.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/contextProperties:clearAll" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + assert kwargs["json"] == {} + + +def test_delete_all_connector_context_properties_with_context_id(chronicle_client): + """Test delete_all_connector_context_properties with context_id.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_id="my-context", + ) + + _, kwargs = mock_request.call_args + assert kwargs["json"]["contextId"] == "my-context" + + +def test_delete_all_connector_context_properties_error(chronicle_client): + """Test delete_all_connector_context_properties raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + side_effect=APIError("Failed to clear context properties"), + ): + with pytest.raises(APIError) as exc_info: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to clear context properties" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_context_properties_custom_api_version(chronicle_client): + """Test list_connector_context_properties with custom API version.""" + expected = {"contextProperties": []} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_context_property_custom_api_version(chronicle_client): + """Test get_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_connector_context_property_custom_api_version(chronicle_client): + """Test delete_connector_context_property with custom API version.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_create_connector_context_property_custom_api_version(chronicle_client): + """Test create_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/new"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + value="test-value", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_update_connector_context_property_custom_api_version(chronicle_client): + """Test update_connector_context_property with custom API version.""" + expected = {"name": "contextProperties/prop1"} + + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_context_property( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + context_property_id="prop1", + value="updated-value", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_all_connector_context_properties_custom_api_version(chronicle_client): + """Test delete_all_connector_context_properties with custom API version.""" + with patch( + "secops.chronicle.integration.connector_context_properties.chronicle_request", + return_value=None, + ) as mock_request: + delete_all_connector_context_properties( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + diff --git a/tests/chronicle/integration/test_connector_instance_logs.py b/tests/chronicle/integration/test_connector_instance_logs.py new file mode 100644 index 00000000..873264fc --- /dev/null +++ b/tests/chronicle/integration/test_connector_instance_logs.py @@ -0,0 +1,256 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for Chronicle integration connector instance logs functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_instance_logs import ( + list_connector_instance_logs, + get_connector_instance_log, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_instance_logs tests -- + + +def test_list_connector_instance_logs_success(chronicle_client): + """Test list_connector_instance_logs delegates to paginated request.""" + expected = { + "logs": [{"name": "log1"}, {"name": "log2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_instance_logs.format_resource_id", + return_value="My Integration", + ): + result = list_connector_instance_logs( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + connector_instance_id="ci1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/connectorInstances/ci1/logs" in kwargs["path"] + assert kwargs["items_key"] == "logs" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_instance_logs_default_args(chronicle_client): + """Test list_connector_instance_logs with default args.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + +def test_list_connector_instance_logs_with_filters(chronicle_client): + """Test list_connector_instance_logs with filter and order_by.""" + expected = {"logs": [{"name": "log1"}]} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + filter_string='severity = "ERROR"', + order_by="timestamp desc", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'severity = "ERROR"', + "orderBy": "timestamp desc", + } + + +def test_list_connector_instance_logs_as_list(chronicle_client): + """Test list_connector_instance_logs returns list when as_list=True.""" + expected = [{"name": "log1"}, {"name": "log2"}] + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_instance_logs_error(chronicle_client): + """Test list_connector_instance_logs raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + side_effect=APIError("Failed to list connector instance logs"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to list connector instance logs" in str(exc_info.value) + + +# -- get_connector_instance_log tests -- + + +def test_get_connector_instance_log_success(chronicle_client): + """Test get_connector_instance_log issues GET request.""" + expected = { + "name": "logs/log1", + "message": "Test log message", + "severity": "INFO", + "timestamp": "2026-03-09T10:00:00Z", + } + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/connectorInstances/ci1/logs/log1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_log_error(chronicle_client): + """Test get_connector_instance_log raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + side_effect=APIError("Failed to get connector instance log"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + ) + assert "Failed to get connector instance log" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_instance_logs_custom_api_version(chronicle_client): + """Test list_connector_instance_logs with custom API version.""" + expected = {"logs": []} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instance_logs( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_log_custom_api_version(chronicle_client): + """Test get_connector_instance_log with custom API version.""" + expected = {"name": "logs/log1"} + + with patch( + "secops.chronicle.integration.connector_instance_logs.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_log( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + log_id="log1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + diff --git a/tests/chronicle/integration/test_connector_instances.py b/tests/chronicle/integration/test_connector_instances.py new file mode 100644 index 00000000..25bf3abe --- /dev/null +++ b/tests/chronicle/integration/test_connector_instances.py @@ -0,0 +1,845 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for Chronicle integration connector instances functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + ConnectorInstanceParameter, +) +from secops.chronicle.integration.connector_instances import ( + list_connector_instances, + get_connector_instance, + delete_connector_instance, + create_connector_instance, + update_connector_instance, + get_connector_instance_latest_definition, + set_connector_instance_logs_collection, + run_connector_instance_on_demand, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_connector_instances tests -- + + +def test_list_connector_instances_success(chronicle_client): + """Test list_connector_instances delegates to chronicle_paginated_request.""" + expected = { + "connectorInstances": [{"name": "ci1"}, {"name": "ci2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_instances.format_resource_id", + return_value="My Integration", + ): + result = list_connector_instances( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/connectorInstances" in kwargs["path"] + assert kwargs["items_key"] == "connectorInstances" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_connector_instances_default_args(chronicle_client): + """Test list_connector_instances with default args.""" + expected = {"connectorInstances": []} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ): + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_connector_instances_with_filters(chronicle_client): + """Test list_connector_instances with filter and order_by.""" + expected = {"connectorInstances": [{"name": "ci1"}]} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='enabled = true', + order_by="displayName", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'enabled = true', + "orderBy": "displayName", + } + + +def test_list_connector_instances_as_list(chronicle_client): + """Test list_connector_instances returns list when as_list=True.""" + expected = [{"name": "ci1"}, {"name": "ci2"}] + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_connector_instances_error(chronicle_client): + """Test list_connector_instances raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + side_effect=APIError("Failed to list connector instances"), + ): + with pytest.raises(APIError) as exc_info: + list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list connector instances" in str(exc_info.value) + + +# -- get_connector_instance tests -- + + +def test_get_connector_instance_success(chronicle_client): + """Test get_connector_instance issues GET request.""" + expected = { + "name": "connectorInstances/ci1", + "displayName": "Test Instance", + "enabled": True, + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_error(chronicle_client): + """Test get_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to get connector instance"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to get connector instance" in str(exc_info.value) + + +# -- delete_connector_instance tests -- + + +def test_delete_connector_instance_success(chronicle_client): + """Test delete_connector_instance issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_connector_instance_error(chronicle_client): + """Test delete_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to delete connector instance"), + ): + with pytest.raises(APIError) as exc_info: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to delete connector instance" in str(exc_info.value) + + +# -- create_connector_instance tests -- + + +def test_create_connector_instance_required_fields_only(chronicle_client): + """Test create_connector_instance with required fields only.""" + expected = {"name": "connectorInstances/new", "displayName": "New Instance"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + ) + + assert result == expected + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/connectorInstances" in kwargs["endpoint_path"] + assert kwargs["json"]["environment"] == "production" + assert kwargs["json"]["displayName"] == "New Instance" + assert kwargs["json"]["intervalSeconds"] == 3600 + assert kwargs["json"]["timeoutSeconds"] == 300 + + +def test_create_connector_instance_with_optional_fields(chronicle_client): + """Test create_connector_instance includes optional fields when provided.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + description="Test description", + agent="agent-123", + allow_list=["192.168.1.0/24"], + product_field_name="product", + event_field_name="event", + integration_version="1.0.0", + version="2.0.0", + logging_enabled_until_unix_ms="1234567890000", + connector_instance_id="custom-id", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test description" + assert kwargs["json"]["agent"] == "agent-123" + assert kwargs["json"]["allowList"] == ["192.168.1.0/24"] + assert kwargs["json"]["productFieldName"] == "product" + assert kwargs["json"]["eventFieldName"] == "event" + assert kwargs["json"]["integrationVersion"] == "1.0.0" + assert kwargs["json"]["version"] == "2.0.0" + assert kwargs["json"]["loggingEnabledUntilUnixMs"] == "1234567890000" + assert kwargs["json"]["id"] == "custom-id" + assert kwargs["json"]["enabled"] is True + + +def test_create_connector_instance_with_parameters(chronicle_client): + """Test create_connector_instance with ConnectorInstanceParameter objects.""" + expected = {"name": "connectorInstances/new"} + + param = ConnectorInstanceParameter() + param.value = "secret-key" + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert len(kwargs["json"]["parameters"]) == 1 + assert kwargs["json"]["parameters"][0]["value"] == "secret-key" + + +def test_create_connector_instance_with_dict_parameters(chronicle_client): + """Test create_connector_instance with dict parameters.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + parameters=[{"displayName": "API Key", "value": "secret-key"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["parameters"][0]["displayName"] == "API Key" + + +def test_create_connector_instance_error(chronicle_client): + """Test create_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to create connector instance"), + ): + with pytest.raises(APIError) as exc_info: + create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + ) + assert "Failed to create connector instance" in str(exc_info.value) + + +# -- update_connector_instance tests -- + + +def test_update_connector_instance_success(chronicle_client): + """Test update_connector_instance updates fields.""" + expected = {"name": "connectorInstances/ci1", "displayName": "Updated"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "PATCH" + assert "connectors/c1/connectorInstances/ci1" in kwargs["endpoint_path"] + assert kwargs["json"]["displayName"] == "Updated" + assert kwargs["json"]["enabled"] is True + # Check that update mask contains the expected fields + assert "displayName" in kwargs["params"]["updateMask"] + assert "enabled" in kwargs["params"]["updateMask"] + + +def test_update_connector_instance_with_custom_mask(chronicle_client): + """Test update_connector_instance with custom update_mask.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + update_mask="displayName", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["params"]["updateMask"] == "displayName" + + +def test_update_connector_instance_with_parameters(chronicle_client): + """Test update_connector_instance with parameters.""" + expected = {"name": "connectorInstances/ci1"} + + param = ConnectorInstanceParameter() + param.value = "new-key" + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert len(kwargs["json"]["parameters"]) == 1 + assert kwargs["json"]["parameters"][0]["value"] == "new-key" + + +def test_update_connector_instance_error(chronicle_client): + """Test update_connector_instance raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to update connector instance"), + ): + with pytest.raises(APIError) as exc_info: + update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + ) + assert "Failed to update connector instance" in str(exc_info.value) + + +# -- get_connector_instance_latest_definition tests -- + + +def test_get_connector_instance_latest_definition_success(chronicle_client): + """Test get_connector_instance_latest_definition issues GET request.""" + expected = { + "name": "connectorInstances/ci1", + "displayName": "Refreshed Instance", + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "GET" + assert "connectorInstances/ci1:fetchLatestDefinition" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_get_connector_instance_latest_definition_error(chronicle_client): + """Test get_connector_instance_latest_definition raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to fetch latest definition"), + ): + with pytest.raises(APIError) as exc_info: + get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + ) + assert "Failed to fetch latest definition" in str(exc_info.value) + + +# -- set_connector_instance_logs_collection tests -- + + +def test_set_connector_instance_logs_collection_enable(chronicle_client): + """Test set_connector_instance_logs_collection enables logs.""" + expected = {"loggingEnabledUntilUnixMs": "1234567890000"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectorInstances/ci1:setLogsCollection" in kwargs["endpoint_path"] + assert kwargs["json"]["enabled"] is True + + +def test_set_connector_instance_logs_collection_disable(chronicle_client): + """Test set_connector_instance_logs_collection disables logs.""" + expected = {} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=False, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["enabled"] is False + + +def test_set_connector_instance_logs_collection_error(chronicle_client): + """Test set_connector_instance_logs_collection raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to set logs collection"), + ): + with pytest.raises(APIError) as exc_info: + set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + ) + assert "Failed to set logs collection" in str(exc_info.value) + + +# -- run_connector_instance_on_demand tests -- + + +def test_run_connector_instance_on_demand_success(chronicle_client): + """Test run_connector_instance_on_demand triggers execution.""" + expected = { + "debugOutput": "Execution completed", + "success": True, + "sampleCases": [], + } + + connector_instance = { + "name": "connectorInstances/ci1", + "displayName": "Test Instance", + "parameters": [{"displayName": "param1", "value": "value1"}], + } + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectorInstances/ci1:runOnDemand" in kwargs["endpoint_path"] + assert kwargs["json"]["connectorInstance"] == connector_instance + + +def test_run_connector_instance_on_demand_error(chronicle_client): + """Test run_connector_instance_on_demand raises APIError on failure.""" + connector_instance = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + side_effect=APIError("Failed to run connector instance"), + ): + with pytest.raises(APIError) as exc_info: + run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + ) + assert "Failed to run connector instance" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_connector_instances_custom_api_version(chronicle_client): + """Test list_connector_instances with custom API version.""" + expected = {"connectorInstances": []} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_connector_instances( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_custom_api_version(chronicle_client): + """Test get_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_connector_instance_custom_api_version(chronicle_client): + """Test delete_connector_instance with custom API version.""" + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=None, + ) as mock_request: + delete_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_create_connector_instance_custom_api_version(chronicle_client): + """Test create_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/new"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + environment="production", + display_name="New Instance", + interval_seconds=3600, + timeout_seconds=300, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_update_connector_instance_custom_api_version(chronicle_client): + """Test update_connector_instance with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_connector_instance( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + display_name="Updated", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_get_connector_instance_latest_definition_custom_api_version(chronicle_client): + """Test get_connector_instance_latest_definition with custom API version.""" + expected = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_connector_instance_latest_definition( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_set_connector_instance_logs_collection_custom_api_version(chronicle_client): + """Test set_connector_instance_logs_collection with custom API version.""" + expected = {} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = set_connector_instance_logs_collection( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + enabled=True, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_run_connector_instance_on_demand_custom_api_version(chronicle_client): + """Test run_connector_instance_on_demand with custom API version.""" + expected = {"success": True} + connector_instance = {"name": "connectorInstances/ci1"} + + with patch( + "secops.chronicle.integration.connector_instances.chronicle_request", + return_value=expected, + ) as mock_request: + result = run_connector_instance_on_demand( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector_instance_id="ci1", + connector_instance=connector_instance, + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + + + diff --git a/tests/chronicle/integration/test_connector_revisions.py b/tests/chronicle/integration/test_connector_revisions.py new file mode 100644 index 00000000..7b214bcb --- /dev/null +++ b/tests/chronicle/integration/test_connector_revisions.py @@ -0,0 +1,385 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for Chronicle marketplace integration connector revisions functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import APIVersion +from secops.chronicle.integration.connector_revisions import ( + list_integration_connector_revisions, + delete_integration_connector_revision, + create_integration_connector_revision, + rollback_integration_connector_revision, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_connector_revisions tests -- + + +def test_list_integration_connector_revisions_success(chronicle_client): + """Test list_integration_connector_revisions delegates to chronicle_paginated_request.""" + expected = { + "revisions": [{"name": "r1"}, {"name": "r2"}], + "nextPageToken": "t", + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connector_revisions.format_resource_id", + return_value="My Integration", + ): + result = list_integration_connector_revisions( + chronicle_client, + integration_name="My Integration", + connector_id="c1", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert "connectors/c1/revisions" in kwargs["path"] + assert kwargs["items_key"] == "revisions" + assert kwargs["page_size"] == 10 + assert kwargs["page_token"] == "next-token" + + +def test_list_integration_connector_revisions_default_args(chronicle_client): + """Test list_integration_connector_revisions with default args.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + +def test_list_integration_connector_revisions_with_filters(chronicle_client): + """Test list_integration_connector_revisions with filter and order_by.""" + expected = {"revisions": [{"name": "r1"}]} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + filter_string='version = "1.0"', + order_by="createTime", + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": 'version = "1.0"', + "orderBy": "createTime", + } + + +def test_list_integration_connector_revisions_as_list(chronicle_client): + """Test list_integration_connector_revisions returns list when as_list=True.""" + expected = [{"name": "r1"}, {"name": "r2"}] + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_connector_revisions_error(chronicle_client): + """Test list_integration_connector_revisions raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + side_effect=APIError("Failed to list connector revisions"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to list connector revisions" in str(exc_info.value) + + +# -- delete_integration_connector_revision tests -- + + +def test_delete_integration_connector_revision_success(chronicle_client): + """Test delete_integration_connector_revision issues DELETE request.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "DELETE" + assert "connectors/c1/revisions/r1" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_delete_integration_connector_revision_error(chronicle_client): + """Test delete_integration_connector_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to delete connector revision"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + assert "Failed to delete connector revision" in str(exc_info.value) + + +# -- create_integration_connector_revision tests -- + + +def test_create_integration_connector_revision_required_fields_only( + chronicle_client, +): + """Test create_integration_connector_revision with required fields only.""" + expected = { + "name": "revisions/new", + "connector": {"displayName": "My Connector"}, + } + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path=( + "integrations/test-integration/connectors/c1/revisions" + ), + api_version=APIVersion.V1BETA, + json={"connector": connector_dict}, + ) + + +def test_create_integration_connector_revision_with_comment(chronicle_client): + """Test create_integration_connector_revision includes comment when provided.""" + expected = {"name": "revisions/new"} + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + comment="Backup before major update", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["comment"] == "Backup before major update" + assert kwargs["json"]["connector"] == connector_dict + + +def test_create_integration_connector_revision_error(chronicle_client): + """Test create_integration_connector_revision raises APIError on failure.""" + connector_dict = { + "displayName": "My Connector", + "script": "print('hello')", + "version": 1, + "enabled": True, + "custom": True, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to create connector revision"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + connector=connector_dict, + ) + assert "Failed to create connector revision" in str(exc_info.value) + + +# -- rollback_integration_connector_revision tests -- + + +def test_rollback_integration_connector_revision_success(chronicle_client): + """Test rollback_integration_connector_revision issues POST request.""" + expected = { + "name": "revisions/r1", + "connector": { + "displayName": "My Connector", + "script": "print('hello')", + }, + } + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=expected, + ) as mock_request: + result = rollback_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["method"] == "POST" + assert "connectors/c1/revisions/r1:rollback" in kwargs["endpoint_path"] + assert kwargs["api_version"] == APIVersion.V1BETA + + +def test_rollback_integration_connector_revision_error(chronicle_client): + """Test rollback_integration_connector_revision raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + side_effect=APIError("Failed to rollback connector revision"), + ): + with pytest.raises(APIError) as exc_info: + rollback_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + ) + assert "Failed to rollback connector revision" in str(exc_info.value) + + +# -- API version tests -- + + +def test_list_integration_connector_revisions_custom_api_version( + chronicle_client, +): + """Test list_integration_connector_revisions with custom API version.""" + expected = {"revisions": []} + + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connector_revisions( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + api_version=APIVersion.V1ALPHA, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + + +def test_delete_integration_connector_revision_custom_api_version( + chronicle_client, +): + """Test delete_integration_connector_revision with custom API version.""" + with patch( + "secops.chronicle.integration.connector_revisions.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector_revision( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + revision_id="r1", + api_version=APIVersion.V1ALPHA, + ) + + _, kwargs = mock_request.call_args + assert kwargs["api_version"] == APIVersion.V1ALPHA + diff --git a/tests/chronicle/integration/test_connectors.py b/tests/chronicle/integration/test_connectors.py new file mode 100644 index 00000000..3aca859a --- /dev/null +++ b/tests/chronicle/integration/test_connectors.py @@ -0,0 +1,665 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for Chronicle marketplace integration connectors functions.""" + +from unittest.mock import Mock, patch + +import pytest + +from secops.chronicle.client import ChronicleClient +from secops.chronicle.models import ( + APIVersion, + ConnectorParameter, + ParamType, + ConnectorParamMode, + ConnectorRule, + ConnectorRuleType, +) +from secops.chronicle.integration.connectors import ( + list_integration_connectors, + get_integration_connector, + delete_integration_connector, + create_integration_connector, + update_integration_connector, + execute_integration_connector_test, + get_integration_connector_template, +) +from secops.exceptions import APIError + + +@pytest.fixture +def chronicle_client(): + """Create a Chronicle client for testing.""" + with patch("secops.auth.SecOpsAuth") as mock_auth: + mock_session = Mock() + mock_session.headers = {} + mock_auth.return_value.session = mock_session + return ChronicleClient( + customer_id="test-customer", + project_id="test-project", + default_api_version=APIVersion.V1BETA, + ) + + +# -- list_integration_connectors tests -- + + +def test_list_integration_connectors_success(chronicle_client): + """Test list_integration_connectors delegates to chronicle_paginated_request.""" + expected = {"connectors": [{"name": "c1"}, {"name": "c2"}], "nextPageToken": "t"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated, patch( + "secops.chronicle.integration.connectors.format_resource_id", + return_value="My Integration", + ): + result = list_integration_connectors( + chronicle_client, + integration_name="My Integration", + page_size=10, + page_token="next-token", + ) + + assert result == expected + + mock_paginated.assert_called_once_with( + chronicle_client, + api_version=APIVersion.V1BETA, + path="integrations/My Integration/connectors", + items_key="connectors", + page_size=10, + page_token="next-token", + extra_params={}, + as_list=False, + ) + + +def test_list_integration_connectors_default_args(chronicle_client): + """Test list_integration_connectors with default args.""" + expected = {"connectors": []} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + +def test_list_integration_connectors_with_filters(chronicle_client): + """Test list_integration_connectors with filter and order_by.""" + expected = {"connectors": [{"name": "c1"}]} + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + filter_string="enabled=true", + order_by="displayName", + exclude_staging=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["extra_params"] == { + "filter": "enabled=true", + "orderBy": "displayName", + "excludeStaging": True, + } + + +def test_list_integration_connectors_as_list(chronicle_client): + """Test list_integration_connectors returns list when as_list=True.""" + expected = [{"name": "c1"}, {"name": "c2"}] + + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + return_value=expected, + ) as mock_paginated: + result = list_integration_connectors( + chronicle_client, + integration_name="test-integration", + as_list=True, + ) + + assert result == expected + + _, kwargs = mock_paginated.call_args + assert kwargs["as_list"] is True + + +def test_list_integration_connectors_error(chronicle_client): + """Test list_integration_connectors raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_paginated_request", + side_effect=APIError("Failed to list integration connectors"), + ): + with pytest.raises(APIError) as exc_info: + list_integration_connectors( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to list integration connectors" in str(exc_info.value) + + +# -- get_integration_connector tests -- + + +def test_get_integration_connector_success(chronicle_client): + """Test get_integration_connector issues GET request.""" + expected = { + "name": "connectors/c1", + "displayName": "My Connector", + "script": "print('hello')", + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_connector_error(chronicle_client): + """Test get_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to get integration connector"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to get integration connector" in str(exc_info.value) + + +# -- delete_integration_connector tests -- + + +def test_delete_integration_connector_success(chronicle_client): + """Test delete_integration_connector issues DELETE request.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=None, + ) as mock_request: + delete_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + + mock_request.assert_called_once_with( + chronicle_client, + method="DELETE", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + ) + + +def test_delete_integration_connector_error(chronicle_client): + """Test delete_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to delete integration connector"), + ): + with pytest.raises(APIError) as exc_info: + delete_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + ) + assert "Failed to delete integration connector" in str(exc_info.value) + + +# -- create_integration_connector tests -- + + +def test_create_integration_connector_required_fields_only(chronicle_client): + """Test create_integration_connector sends only required fields when optionals omitted.""" + expected = {"name": "connectors/new", "displayName": "My Connector"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors", + api_version=APIVersion.V1BETA, + json={ + "displayName": "My Connector", + "script": "print('hi')", + "timeoutSeconds": 300, + "enabled": True, + "productFieldName": "product", + "eventFieldName": "event", + }, + ) + + +def test_create_integration_connector_with_optional_fields(chronicle_client): + """Test create_integration_connector includes optional fields when provided.""" + expected = {"name": "connectors/new"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + description="Test connector", + parameters=[{"name": "p1", "type": "STRING"}], + rules=[{"name": "r1", "type": "MAPPING"}], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["description"] == "Test connector" + assert kwargs["json"]["parameters"] == [{"name": "p1", "type": "STRING"}] + assert kwargs["json"]["rules"] == [{"name": "r1", "type": "MAPPING"}] + + +def test_create_integration_connector_with_dataclass_parameters(chronicle_client): + """Test create_integration_connector converts ConnectorParameter dataclasses.""" + expected = {"name": "connectors/new"} + + param = ConnectorParameter( + display_name="API Key", + type=ParamType.STRING, + mode=ConnectorParamMode.REGULAR, + mandatory=True, + description="API key for authentication", + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["displayName"] == "API Key" + assert params_sent[0]["type"] == "STRING" + + +def test_create_integration_connector_with_dataclass_rules(chronicle_client): + """Test create_integration_connector converts ConnectorRule dataclasses.""" + expected = {"name": "connectors/new"} + + rule = ConnectorRule( + display_name="Mapping Rule", + type=ConnectorRuleType.ALLOW_LIST, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + rules=[rule], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + rules_sent = kwargs["json"]["rules"] + assert len(rules_sent) == 1 + assert rules_sent[0]["displayName"] == "Mapping Rule" + assert rules_sent[0]["type"] == "ALLOW_LIST" + + +def test_create_integration_connector_error(chronicle_client): + """Test create_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to create integration connector"), + ): + with pytest.raises(APIError) as exc_info: + create_integration_connector( + chronicle_client, + integration_name="test-integration", + display_name="My Connector", + script="print('hi')", + timeout_seconds=300, + enabled=True, + product_field_name="product", + event_field_name="event", + ) + assert "Failed to create integration connector" in str(exc_info.value) + + +# -- update_integration_connector tests -- + + +def test_update_integration_connector_with_explicit_update_mask(chronicle_client): + """Test update_integration_connector passes through explicit update_mask.""" + expected = {"name": "connectors/c1", "displayName": "New Name"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + display_name="New Name", + update_mask="displayName", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="PATCH", + endpoint_path="integrations/test-integration/connectors/c1", + api_version=APIVersion.V1BETA, + json={"displayName": "New Name"}, + params={"updateMask": "displayName"}, + ) + + +def test_update_integration_connector_auto_update_mask(chronicle_client): + """Test update_integration_connector auto-generates updateMask based on fields.""" + expected = {"name": "connectors/c1"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + enabled=False, + timeout_seconds=600, + ) + + assert result == expected + + assert mock_request.call_count == 1 + _, kwargs = mock_request.call_args + + assert kwargs["method"] == "PATCH" + assert kwargs["endpoint_path"] == "integrations/test-integration/connectors/c1" + assert kwargs["api_version"] == APIVersion.V1BETA + + assert kwargs["json"] == {"enabled": False, "timeoutSeconds": 600} + + update_mask = kwargs["params"]["updateMask"] + assert set(update_mask.split(",")) == {"enabled", "timeoutSeconds"} + + +def test_update_integration_connector_with_parameters(chronicle_client): + """Test update_integration_connector with parameters field.""" + expected = {"name": "connectors/c1"} + + param = ConnectorParameter( + display_name="Auth Token", + type=ParamType.STRING, + mode=ConnectorParamMode.REGULAR, + mandatory=True, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + parameters=[param], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + params_sent = kwargs["json"]["parameters"] + assert len(params_sent) == 1 + assert params_sent[0]["displayName"] == "Auth Token" + + +def test_update_integration_connector_with_rules(chronicle_client): + """Test update_integration_connector with rules field.""" + expected = {"name": "connectors/c1"} + + rule = ConnectorRule( + display_name="Filter Rule", + type=ConnectorRuleType.BLOCK_LIST, + ) + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + rules=[rule], + ) + + assert result == expected + + _, kwargs = mock_request.call_args + rules_sent = kwargs["json"]["rules"] + assert len(rules_sent) == 1 + assert rules_sent[0]["displayName"] == "Filter Rule" + + +def test_update_integration_connector_error(chronicle_client): + """Test update_integration_connector raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to update integration connector"), + ): + with pytest.raises(APIError) as exc_info: + update_integration_connector( + chronicle_client, + integration_name="test-integration", + connector_id="c1", + display_name="New Name", + ) + assert "Failed to update integration connector" in str(exc_info.value) + + +# -- execute_integration_connector_test tests -- + + +def test_execute_integration_connector_test_success(chronicle_client): + """Test execute_integration_connector_test sends POST request with connector.""" + expected = { + "outputMessage": "Success", + "debugOutputMessage": "Debug info", + "resultJson": {"status": "ok"}, + } + + connector = { + "displayName": "Test Connector", + "script": "print('test')", + "enabled": True, + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector=connector, + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="POST", + endpoint_path="integrations/test-integration/connectors:executeTest", + api_version=APIVersion.V1BETA, + json={"connector": connector}, + ) + + +def test_execute_integration_connector_test_with_agent_identifier(chronicle_client): + """Test execute_integration_connector_test includes agent_identifier when provided.""" + expected = {"outputMessage": "Success"} + + connector = {"displayName": "Test", "script": "print('test')"} + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector=connector, + agent_identifier="agent-123", + ) + + assert result == expected + + _, kwargs = mock_request.call_args + assert kwargs["json"]["agentIdentifier"] == "agent-123" + + +def test_execute_integration_connector_test_error(chronicle_client): + """Test execute_integration_connector_test raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to execute connector test"), + ): + with pytest.raises(APIError) as exc_info: + execute_integration_connector_test( + chronicle_client, + integration_name="test-integration", + connector={"displayName": "Test"}, + ) + assert "Failed to execute connector test" in str(exc_info.value) + + +# -- get_integration_connector_template tests -- + + +def test_get_integration_connector_template_success(chronicle_client): + """Test get_integration_connector_template issues GET request.""" + expected = { + "script": "# Template script\nprint('hello')", + "displayName": "Template Connector", + } + + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + return_value=expected, + ) as mock_request: + result = get_integration_connector_template( + chronicle_client, + integration_name="test-integration", + ) + + assert result == expected + + mock_request.assert_called_once_with( + chronicle_client, + method="GET", + endpoint_path="integrations/test-integration/connectors:fetchTemplate", + api_version=APIVersion.V1BETA, + ) + + +def test_get_integration_connector_template_error(chronicle_client): + """Test get_integration_connector_template raises APIError on failure.""" + with patch( + "secops.chronicle.integration.connectors.chronicle_request", + side_effect=APIError("Failed to get connector template"), + ): + with pytest.raises(APIError) as exc_info: + get_integration_connector_template( + chronicle_client, + integration_name="test-integration", + ) + assert "Failed to get connector template" in str(exc_info.value) + diff --git a/tests/chronicle/utils/test_format_utils.py b/tests/chronicle/utils/test_format_utils.py index c71bda40..5610c2da 100644 --- a/tests/chronicle/utils/test_format_utils.py +++ b/tests/chronicle/utils/test_format_utils.py @@ -18,6 +18,7 @@ import pytest from secops.chronicle.utils.format_utils import ( + build_patch_body, format_resource_id, parse_json_list, ) @@ -98,3 +99,107 @@ def test_parse_json_list_handles_empty_json_array() -> None: def test_parse_json_list_handles_empty_list_input() -> None: result = parse_json_list([], "filters") assert result == [] + + +def test_build_patch_body_all_fields_set_builds_body_and_mask() -> None: + # All three fields provided — body and mask should include all of them + body, params = build_patch_body([ + ("displayName", "display_name", "My Rule"), + ("enabled", "enabled", True), + ("severity", "severity", "HIGH"), + ]) + + assert body == {"displayName": "My Rule", "enabled": True, "severity": "HIGH"} + assert params == {"updateMask": "display_name,enabled,severity"} + + +def test_build_patch_body_partial_fields_omits_none_values() -> None: + # Only non-None values should appear in body and mask + body, params = build_patch_body([ + ("displayName", "display_name", "New Name"), + ("enabled", "enabled", None), + ("severity", "severity", None), + ]) + + assert body == {"displayName": "New Name"} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_no_fields_set_returns_empty_body_and_none_params() -> None: + # All values are None — body should be empty and params should be None + body, params = build_patch_body([ + ("displayName", "display_name", None), + ("enabled", "enabled", None), + ]) + + assert body == {} + assert params is None + + +def test_build_patch_body_empty_field_map_returns_empty_body_and_none_params() -> None: + # Empty field_map — nothing to build + body, params = build_patch_body([]) + + assert body == {} + assert params is None + + +def test_build_patch_body_explicit_update_mask_overrides_auto_generated() -> None: + # An explicit update_mask should always win, even when fields are set + body, params = build_patch_body( + [ + ("displayName", "display_name", "Name"), + ("enabled", "enabled", True), + ], + update_mask="display_name", + ) + + assert body == {"displayName": "Name", "enabled": True} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_explicit_update_mask_with_no_fields_set_still_applies() -> None: + # Explicit mask should appear even when all values are None (caller's intent) + body, params = build_patch_body( + [ + ("displayName", "display_name", None), + ], + update_mask="display_name", + ) + + assert body == {} + assert params == {"updateMask": "display_name"} + + +def test_build_patch_body_false_and_zero_are_not_treated_as_none() -> None: + # False-like but non-None values (False, 0, "") should be included in the body + body, params = build_patch_body([ + ("enabled", "enabled", False), + ("count", "count", 0), + ("label", "label", ""), + ]) + + assert body == {"enabled": False, "count": 0, "label": ""} + assert params == {"updateMask": "enabled,count,label"} + + +def test_build_patch_body_single_field_produces_single_entry_mask() -> None: + body, params = build_patch_body([ + ("severity", "severity", "LOW"), + ]) + + assert body == {"severity": "LOW"} + assert params == {"updateMask": "severity"} + + +def test_build_patch_body_mask_order_matches_field_map_order() -> None: + # The mask field order should mirror the order of field_map entries + body, params = build_patch_body([ + ("z", "z_key", "z_val"), + ("a", "a_key", "a_val"), + ("m", "m_key", "m_val"), + ]) + + assert params == {"updateMask": "z_key,a_key,m_key"} + assert list(body.keys()) == ["z", "a", "m"] + diff --git a/tests/chronicle/utils/test_request_utils.py b/tests/chronicle/utils/test_request_utils.py index 6f8687a2..c4e8b5b9 100644 --- a/tests/chronicle/utils/test_request_utils.py +++ b/tests/chronicle/utils/test_request_utils.py @@ -26,6 +26,7 @@ from secops.chronicle.utils.request_utils import ( DEFAULT_PAGE_SIZE, chronicle_request, + chronicle_request_bytes, chronicle_paginated_request, ) from secops.exceptions import APIError @@ -655,3 +656,181 @@ def test_chronicle_request_non_json_error_body_is_truncated(client: Mock) -> Non assert "status=500" in msg # Should not include the full 5000 chars, should include truncation marker assert "truncated" in msg + + +# --------------------------------------------------------------------------- +# chronicle_request_bytes() tests +# --------------------------------------------------------------------------- + +def test_chronicle_request_bytes_success_returns_content_and_stream_true(client: Mock) -> None: + resp = _mock_response(status_code=200, json_value={"ignored": True}) + resp.content = b"PK\x03\x04...zip-bytes..." # ZIP magic prefix in real life + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="integrations/foo:export", + api_version=APIVersion.V1BETA, + params={"alt": "media"}, + headers={"Accept": "application/zip"}, + ) + + assert out == b"PK\x03\x04...zip-bytes..." + + client.base_url.assert_called_once_with(APIVersion.V1BETA) + client.session.request.assert_called_once_with( + method="GET", + url="https://example.test/chronicle/instances/instance-1/integrations/foo:export", + params={"alt": "media"}, + headers={"Accept": "application/zip"}, + timeout=None, + stream=True, + ) + + +def test_chronicle_request_bytes_builds_url_for_rpc_colon_prefix(client: Mock) -> None: + resp = _mock_response(status_code=200, json_value={"ok": True}) + resp.content = b"binary" + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="POST", + endpoint_path=":exportSomething", + api_version=APIVersion.V1ALPHA, + ) + + assert out == b"binary" + + _, kwargs = client.session.request.call_args + assert kwargs["url"] == "https://example.test/chronicle/instances/instance-1:exportSomething" + assert kwargs["stream"] is True + + +def test_chronicle_request_bytes_accepts_multiple_expected_statuses_set(client: Mock) -> None: + resp = _mock_response(status_code=204, json_value=None) + resp.content = b"" + client.session.request.return_value = resp + + out = chronicle_request_bytes( + client=client, + method="DELETE", + endpoint_path="something", + api_version=APIVersion.V1ALPHA, + expected_status={200, 204}, + ) + + assert out == b"" + + +def test_chronicle_request_bytes_status_mismatch_with_json_includes_json(client: Mock) -> None: + resp = _mock_response(status_code=400, json_value={"error": "bad"}) + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"API request failed: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/curatedRules" + r", status=400, response={'error': 'bad'}", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + +def test_chronicle_request_bytes_status_mismatch_non_json_includes_text(client: Mock) -> None: + resp = _mock_response(status_code=500, json_raises=True, text="boom") + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"API request failed: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/curatedRules, " + r"status=500, response_text=boom", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + +def test_chronicle_request_bytes_custom_error_message_used(client: Mock) -> None: + resp = _mock_response(status_code=404, json_value={"message": "not found"}) + resp.content = b"" + client.session.request.return_value = resp + + with pytest.raises( + APIError, + match=r"Failed to download export: method=GET, " + r"url=https://example\.test/chronicle/instances/instance-1/integrations/foo:export, " + r"status=404, response={'message': 'not found'}", + ): + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="integrations/foo:export", + api_version=APIVersion.V1BETA, + error_message="Failed to download export", + ) + + +def test_chronicle_request_bytes_wraps_requests_exception(client: Mock) -> None: + client.session.request.side_effect = requests.RequestException("no route to host") + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "API request failed" in msg + assert "method=GET" in msg + assert "url=https://example.test/chronicle/instances/instance-1/curatedRules" in msg + assert "request_error=RequestException" in msg + + +def test_chronicle_request_bytes_wraps_google_auth_error(client: Mock) -> None: + client.session.request.side_effect = GoogleAuthError("invalid_grant") + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "Google authentication failed" in msg + assert "authentication_error=" in msg + + +def test_chronicle_request_bytes_non_json_error_body_is_truncated(client: Mock) -> None: + long_text = "x" * 5000 + resp = _mock_response(status_code=500, json_raises=True, text=long_text) + resp.content = b"" + resp.headers = {"Content-Type": "text/plain"} + client.session.request.return_value = resp + + with pytest.raises(APIError) as exc_info: + chronicle_request_bytes( + client=client, + method="GET", + endpoint_path="curatedRules", + api_version=APIVersion.V1ALPHA, + ) + + msg = str(exc_info.value) + assert "status=500" in msg + assert "truncated" in msg \ No newline at end of file