IBMSecurityGuardium
Collect events from IBM Guardium Data Security Center.
Analytics & SIEM · IBM Guardium
Details
| ID | IBMSecurityGuardium |
|---|---|
| Provider | IBM |
| Category | Analytics & SIEM |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
README
IBM Security Guardium is a comprehensive data security platform that provides visibility and protection for sensitive data across databases, data warehouses, big data platforms, and cloud environments.
This integration enables the collection of security events from IBM Guardium Data Security Center.
Configure IBM Security Guardium in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | The URL of your IBM Guardium instance. The default value is an example - replace it with your specific instance URL. | True |
| API Key | The API Key for authentication | True |
| API Secret | The API Secret for authentication | True |
| Report ID | The ID of the report to fetch events from. | True |
| Fetch events | Whether to automatically fetch events. | False |
| Maximum number of events to fetch | Maximum number of events to fetch per run. Default is 10000. Recommended maximum is 10000. | False |
| Timestamp Field Name | The display name of the header in the report that contains the timestamp field, e.g., “Date created (local time)”. Note: This field name varies between different reports. |
False (Required when Fetch events is enabled) |
| Trust any certificate (not secure) | Trust any certificate (not secure). | False |
| Use system proxy settings | Use system proxy settings. | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
ibm-guardium-get-events
Manual command to fetch and display events.
Base Command
ibm-guardium-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | If true, the command creates events; otherwise, it only displays them. Possible values are: true, false. Default is false. | Required |
| timestamp_field | The name of the field in the event data that contains the timestamp. Note: This field name varies between different reports. If not provided, uses the value from integration configuration. |
Optional (Required when should_push_events is true) |
| limit | Maximum number of results to return. Maximum allowed is 1000. Default is 50. | Optional |
| start_time | Start time for fetching events. Supports ISO format (“2023-01-01T00:00:00”) or natural language (“7 days ago”, “yesterday”, “1 week ago”). Defaults to 1 hour ago if not provided. | Optional |
| end_time | End time for fetching events. Supports ISO format (“2023-01-01T23:59:59”) or natural language (“2 hours ago”, “now”). If not provided, defaults to now. | Optional |
Context Output
There is no context output for this command.
Command Example
!ibm-guardium-get-events limit=50 start_time="2024-01-01T00:00:00" end_time="2024-01-01T23:59:59" should_push_events=true
Human Readable Output
IBM Guardium Events
Client IP Database User Source Program Server IP Service Name Database Name Session Start Time 10.0.0.1 admin SQLClient 10.0.0.100 PROD_DB customers 2024-01-01 10:30:00 10.0.0.2 user1 AppServer 10.0.0.100 PROD_DB orders 2024-01-01 10:31:15
Additional Information
Note: The integration fetches events from the last 12 hours by default on first run. This accounts for IBM Guardium’s event indexing delays and ensures events are captured even when indexed with significant delays.
Configuration parameters
url— Server URL (required)credentials— API Key (required)report_id— Report ID (required)isFetchEvents— Fetch eventsmax_fetch— Maximum number of events to fetchtimestamp_field— Timestamp Field Nameinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
ibm-guardium-get-eventsManual command to fetch and display events.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * import urllib3 import hashlib import traceback from typing import Any # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ API_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" # Format for dates sent in API requests VENDOR = "IBM" PRODUCT = "Guardium_dsc" DEFAULT_MAX_FETCH = 10000 MAX_BATCH_SIZE = 1000 """ CLIENT CLASS """ class Client(BaseClient): def __init__(self, base_url: str, auth: tuple, verify: bool, proxy: bool): super().__init__(base_url=base_url, verify=verify, proxy=proxy, auth=auth) def run_report(self, report_id: str, fetch_size: int, offset: int, from_date: str, to_date: str) -> dict[str, Any]: """ Run a report to fetch events from IBM Guardium. Args: report_id: The ID of the report to run fetch_size: Number of events to fetch offset: Offset for pagination from_date: Start date in ISO format to_date: End date in ISO format Returns: API response containing report data and pagination metadata """ payload = { "fetch_size": fetch_size, "offset": offset, "report_id": report_id, "runtime_parameter_list": [ { "key": "QUERY_FROM_DATE", "runtime_parameter_type": "DATE", "operator_type": "GREATER_THAN_OR_EQUAL", "value": from_date, }, {"key": "QUERY_TO_DATE", "runtime_parameter_type": "DATE", "operator_type": "LESS_THAN", "value": to_date}, ], "without_limit": True, } demisto.debug(f"Running report {report_id} with payload: {payload}") response = self._http_request(method="POST", url_suffix="/api/v3/reports/run", json_data=payload, resp_type="text") # The API returns multiple JSON objects separated by newlines # First line: report data with report_layout and data array # Second line (optional): pagination metadata with limit_reached, total_number_of_rows, final_result # If the second line doesn't exist, the metadata is included in the first line lines = response.strip().split("\n") demisto.debug(f"Received {len(lines)} JSON response line(s)") if not lines: raise DemistoException("Empty response from API") # Parse first line containing report data try: parsed_response = json.loads(lines[0]) demisto.debug(f"Parsed first line successfully. Keys: {list(parsed_response.keys())}") # Validate expected structure in first line if "result" not in parsed_response: raise DemistoException("First line missing expected 'result' key") result_data = parsed_response.get("result", {}) if "report_layout" not in result_data or "data" not in result_data: demisto.debug(f"Warning: First line result keys: {list(result_data.keys())}") except json.JSONDecodeError as e: demisto.debug(f"Failed to parse JSON. Full response text: {response}") raise DemistoException(f"Failed to parse first line as JSON: {e}") # Parse second line if present and merge pagination metadata if len(lines) > 1: try: pagination_data = json.loads(lines[1]) demisto.debug(f"Parsed second line successfully. Keys: {list(pagination_data.keys())}") # Merge pagination metadata into the main response if "result" in pagination_data: parsed_response.setdefault("result", {}).update(pagination_data["result"]) demisto.debug(f"Merged pagination metadata: {pagination_data['result']}") else: demisto.debug(f"Warning: Second line missing 'result' key. Available keys: {list(pagination_data.keys())}") except json.JSONDecodeError as e: demisto.debug(f"Failed to parse pagination JSON. Full response text: {response}") raise DemistoException(f"Failed to parse pagination data (second line): {e}") return parsed_response """ HELPER FUNCTIONS """ def extract_field_mapping(response: dict[str, Any]) -> dict[str, str]: """ Extract field mapping from the API response's report_headers. Uses the user-friendly field_name.nls_value. Args: response: API response containing report_layout with report_headers Returns: Dictionary mapping sequence number to field display name (e.g., {"1": "Date created (local time)", "2": "Performed by"}) Raises: DemistoException: If field mapping extraction fails """ try: headers = response["result"]["report_layout"]["report_headers"] field_mapping = {} for header in headers: sequence = str(header["sequence"]) field_name = header.get("field_name", {}).get("nls_value") field_mapping[sequence] = field_name demisto.debug(f"Extracted field mapping: {field_mapping}") return field_mapping except (KeyError, TypeError) as e: raise DemistoException(f"Failed to extract field mapping from API response: {e}") def get_event_hash(event: dict[str, Any]) -> str: """ Generate a SHA-256 hash for an event. Args: event: Event dictionary to hash Returns: First 16 characters of SHA-256 hash """ event_str = json.dumps(event, sort_keys=True) return hashlib.sha256(event_str.encode()).hexdigest()[:16] def map_event(raw_event: dict[str, Any], field_mapping: dict[str, str]) -> dict[str, Any]: """ Map raw event fields to readable names. Args: raw_event: Raw event data with numbered fields field_mapping: Mapping from field IDs to field names Returns: Event with readable field names """ return {field_mapping.get(key, key): value for key, value in raw_event.items()} def validate_timestamp_field(timestamp_field: str | None, field_mapping: dict[str, str], is_fetch_flow: bool = True) -> None: """ Validate that the timestamp field exists in the report headers. Args: timestamp_field: Name of the timestamp field to validate field_mapping: Dictionary mapping field IDs to field names from report headers is_fetch_flow: True if called from fetch-events, False if called from get-events with should_push_events Raises: DemistoException: If timestamp field is empty/None or not found in available fields """ if not timestamp_field: if is_fetch_flow: raise DemistoException("Timestamp Field Name is required when using fetch events.") else: raise DemistoException("Timestamp Field Name is required when should_push_events=true.") available_fields = list(field_mapping.values()) if timestamp_field not in available_fields: raise DemistoException( f"Timestamp field '{timestamp_field}' not found in this report's headers. " f"Available fields in this report: {', '.join(available_fields)}" ) demisto.debug(f"Validated timestamp field '{timestamp_field}' exists in report headers") def send_events_to_xsiam_with_time(events: list[dict[str, Any]], timestamp_field: str) -> None: """ Add _time field to events and send them to XSIAM. The _time field is required by XSIAM for event ingestion. If timestamp field is missing, uses None as fallback. Args: events: List of events to send to XSIAM timestamp_field: Name of the timestamp field to use for _time (required) """ if not events: demisto.debug("No events to send to XSIAM") return demisto.debug(f"Preparing to send {len(events)} events to XSIAM with timestamp field: {timestamp_field}") # Add _time field to each event based on the timestamp field for event in events: if timestamp_field in event: event["_time"] = event[timestamp_field] else: # Log error and use None as fallback to avoid failing the entire fetch demisto.error( f"Timestamp field '{timestamp_field}' not found in event. Using None as fallback. " f"Event keys: {list(event.keys())}" ) event["_time"] = None # Send events to XSIAM send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) demisto.debug(f"Successfully sent {len(events)} events to XSIAM") def deduplicate_events(events: list[dict[str, Any]], last_run: dict[str, Any], timestamp_field: str) -> list[dict[str, Any]]: """ Deduplicate events based on the ignore list from last run. Events are in descending order (newest first). Args: events: List of events ordered by timestamp descending (newest first) last_run: Dictionary containing last_fetch_time and fetched_event_hashes timestamp_field: Name of the timestamp field to use (required) Returns: List of deduplicated events in descending order """ if not events: return events last_fetch_time = last_run.get("last_fetch_time") fetched_event_hashes = set(last_run.get("fetched_event_hashes", [])) demisto.debug( f"Deduplicating {len(events)} events using timestamp field '{timestamp_field}'. " f"Last fetch time: {last_fetch_time}, Ignore list size: {len(fetched_event_hashes)}" ) deduplicated: list[dict[str, Any]] = [] # Iterate from the end (oldest first) for idx in range(len(events) - 1, -1, -1): event = events[idx] event_time = event.get(timestamp_field) # If timestamp is greater than last_fetch_time, add this event and all remaining (newer) events if event_time and last_fetch_time and event_time > last_fetch_time: deduplicated = events[: idx + 1] demisto.debug( f"Found event at index {idx} with timestamp {event_time} > last_fetch_time {last_fetch_time}, " f"added {idx + 1} newer events without duplicate check" ) break # If event has the same timestamp as last_fetch_time, check if it's a duplicate if event_time == last_fetch_time: event_hash = get_event_hash(event) if event_hash in fetched_event_hashes: demisto.debug(f"Skipping duplicate event with hash {event_hash} at time {event_time}") continue deduplicated.insert(0, event) # Insert at beginning to maintain descending order demisto.debug( f"Deduplication complete. Original: {len(events)}, Deduplicated: {len(deduplicated)}, " f"Filtered: {len(events) - len(deduplicated)}" ) return deduplicated def build_ignore_list(events: list[dict[str, Any]], timestamp_field: str) -> set[str]: """ Build ignore list of event hashes with the same timestamp as the most recent event. Events are expected in descending order (newest first). Args: events: List of events ordered by timestamp descending (newest first) timestamp_field: Name of the timestamp field to use (required) Returns: Set of event hashes for events with the same timestamp as the most recent (first) event """ ignore_set: set[str] = set() if not events: return ignore_set # Most recent event is the first one (descending order) most_recent_time = events[0].get(timestamp_field) if not most_recent_time: raise DemistoException(f"Timestamp field '{timestamp_field}' not found in first event") # Iterate from the beginning to collect all events with the same timestamp as the most recent for event in events: event_time = event.get(timestamp_field) if event_time == most_recent_time: ignore_set.add(get_event_hash(event)) else: break demisto.debug(f"Created ignore list with {len(ignore_set)} event hashes at timestamp {most_recent_time}") return ignore_set """ COMMAND FUNCTIONS """ def test_module_command(client: Client, report_id: str, is_fetch: bool, timestamp_field: str | None) -> str: """ Test API connectivity and authentication. If fetch is enabled, also validates that the timestamp field exists in the response. Args: client: IBM Guardium client report_id: Report ID to test with is_fetch: Whether fetch events is enabled timestamp_field: Name of the timestamp field (required if is_fetch is True) Returns: 'ok' if successful, error message otherwise """ try: now = datetime.utcnow() from_date = (now - timedelta(hours=5)).strftime(API_DATE_FORMAT) to_date = now.strftime(API_DATE_FORMAT) demisto.debug(f"Testing connectivity: fetching 1 event from {from_date} to {to_date}") response = client.run_report(report_id, fetch_size=1, offset=0, from_date=from_date, to_date=to_date) # If fetch is enabled, validate timestamp field exists in response headers if is_fetch: # Extract field mapping (headers) and validate timestamp field field_mapping = extract_field_mapping(response) validate_timestamp_field(timestamp_field, field_mapping, is_fetch_flow=True) return "ok" except DemistoException as e: error_str = str(e) demisto.debug(f"Test module failed with DemistoException: {error_str}\nTraceback: {traceback.format_exc()}") if "Forbidden" in error_str or "Unauthorized" in error_str or "401" in error_str or "403" in error_str: return "Authorization Error: Please verify that the API Key and Secret are correctly configured." elif "ConnectionError" in error_str or "Name does not resolve" in error_str or "Failed to resolve" in error_str: return "Connection Error: Unable to connect to the server. Please verify the Server URL is correct and accessible." elif "timeout" in error_str.lower() or "timed out" in error_str.lower(): return "Connection Error: Request timed out. Please verify the Server URL and network connectivity." else: return f"Error: {error_str}" except Exception as e: demisto.debug(f"Test module failed with unexpected exception: {str(e)}\nTraceback: {traceback.format_exc()}") return f"Unexpected error during connection test: {str(e)}" def fetch_events_command( client: Client, report_id: str, max_fetch: int, last_run: dict[str, Any], timestamp_field: str ) -> tuple[list[dict[str, Any]], dict[str, Any]]: """ Fetch events from IBM Guardium with deduplication. The API returns events in descending order (newest first). Args: client: IBM Guardium client report_id: Report ID to fetch events from max_fetch: Maximum number of events to fetch last_run: Last run context with last_fetch_time and fetched_event_hashes timestamp_field: Name of the timestamp field to use for deduplication and _time Returns: Tuple of (deduplicated events list in descending order, next run context) """ demisto.debug(f"Starting fetch_events with last_run: {last_run}, max_fetch: {max_fetch}") last_fetch_time_str = last_run.get("last_fetch_time") # Determine fetch time range now = datetime.utcnow() if last_fetch_time_str: # Use dateparser to handle various timestamp formats flexibly last_fetch_time = dateparser.parse(last_fetch_time_str) if not last_fetch_time: raise DemistoException(f"Failed to parse last_fetch_time: {last_fetch_time_str}") else: # Default to 12 hours ago for first fetch to account for IBM Guardium's event indexing delays. # Events may be indexed with significant delays. Without a sufficient lookback period, # the integration may never retrieve any events, as the fetch window would always be ahead of the indexed events. last_fetch_time = now - timedelta(hours=12) demisto.debug("No last_fetch_time found, using default: 12 hours ago") from_date = last_fetch_time.strftime(API_DATE_FORMAT) to_date = now.strftime(API_DATE_FORMAT) demisto.debug(f"Fetching events from {from_date} to {to_date}") events: list[dict[str, Any]] = [] offset = 0 field_mapping: dict[str, str] = {} final_result = False while len(events) < max_fetch and not final_result: remaining = max_fetch - len(events) batch_size = min(MAX_BATCH_SIZE, remaining) demisto.debug(f"Fetching batch with offset={offset}, batch_size={batch_size}, remaining={remaining}") response = client.run_report( report_id=report_id, fetch_size=batch_size, offset=offset, from_date=from_date, to_date=to_date ) # Extract field mapping only on first batch (offset == 0) if offset == 0: field_mapping = extract_field_mapping(response) # Validate timestamp field exists in report headers validate_timestamp_field(timestamp_field, field_mapping, is_fetch_flow=True) raw_events = response.get("result", {}).get("data", []) demisto.debug(f"Raw events count: {len(raw_events)}") if not raw_events: demisto.debug("No events returned in batch, stopping fetch loop") break # Process events for raw_event in raw_events: event_data = raw_event["results"] mapped_event = map_event(event_data, field_mapping) events.append(mapped_event) if len(events) >= max_fetch: demisto.debug(f"Reached max_fetch limit of {max_fetch}, stopping event collection") break # Check for final_result flag in the response result_data = response.get("result", {}) final_result = result_data.get("final_result") limit_reached = result_data.get("limit_reached") demisto.debug( f"Batch complete: fetched {len(raw_events)} events, total so far: {len(events)}, " f"final_result={final_result}, limit_reached={limit_reached}" ) # Stop if we've reached the final result if final_result: demisto.debug("Received final_result=true, stopping pagination") break # Stop if limit not reached (no more data available) if not limit_reached: demisto.debug("limit_reached is false or absent, stopping pagination") break # Update offset for next iteration offset += len(raw_events) # Deduplicate events (events are in descending order - newest first) deduplicated_events = deduplicate_events(events, last_run, timestamp_field) new_ignore_set = build_ignore_list(deduplicated_events, timestamp_field) # Update last_run with the most recent event (first event in descending order) if deduplicated_events: most_recent_event = deduplicated_events[0] # First event is the newest if timestamp_field in most_recent_event: next_run = {"last_fetch_time": most_recent_event[timestamp_field], "fetched_event_hashes": list(new_ignore_set)} else: raise DemistoException(f"Timestamp field '{timestamp_field}' not found in most recent event") else: next_run = last_run demisto.debug(f"No events after deduplication, keeping last_run unchanged: {next_run}") demisto.debug( f"Fetch completed. Total events: {len(events)}, Deduplicated: {len(deduplicated_events)}. " f"Next run: {next_run}" ) return deduplicated_events, next_run def get_events_command( client: Client, report_id: str, args: dict[str, Any], timestamp_field: str ) -> tuple[list[dict[str, Any]], CommandResults, str | None]: """ Manual command to fetch events within a specified time range. Args: client: IBM Guardium client report_id: Report ID to fetch events from args: Command arguments (limit, start_time, end_time, should_push_events, timestamp_field) timestamp_field: Timestamp field from integration configuration Returns: Tuple of (events list, CommandResults, timestamp_field to use for XSIAM or None) """ demisto.debug(f"Executing get_events_command with args: {args}") limit = arg_to_number(args.get("limit", 50)) or 50 # Validate limit does not exceed 1000 if limit > 1000: raise DemistoException("The limit parameter cannot exceed 1000. Please use a smaller value.") now = datetime.utcnow() if args.get("start_time"): start_time = dateparser.parse(args["start_time"]) if not start_time: raise DemistoException(f"Invalid start_time format: {args['start_time']}") else: start_time = now - timedelta(hours=1) if args.get("end_time"): end_time = dateparser.parse(args["end_time"]) if not end_time: raise DemistoException(f"Invalid end_time format: {args['end_time']}") else: end_time = now from_date = start_time.strftime(API_DATE_FORMAT) to_date = end_time.strftime(API_DATE_FORMAT) demisto.debug(f"Getting events from {from_date} to {to_date} with limit {limit}") response = client.run_report(report_id=report_id, fetch_size=limit, offset=0, from_date=from_date, to_date=to_date) field_mapping = extract_field_mapping(response) raw_events = response.get("result", {}).get("data", []) events = [map_event(raw_event["results"], field_mapping) for raw_event in raw_events] demisto.debug(f"Got {len(events)} events") headers = list(field_mapping.values()) if field_mapping else [] # Determine timestamp field for XSIAM if should_push_events is true timestamp_field_result = None if argToBoolean(args.get("should_push_events", False)): # Get timestamp_field from command argument or fall back to config parameter cmd_timestamp_field = args.get("timestamp_field") or timestamp_field # Validate timestamp field (checks for None/empty and existence in headers) validate_timestamp_field(cmd_timestamp_field, field_mapping, is_fetch_flow=False) timestamp_field_result = cmd_timestamp_field return ( events, CommandResults( readable_output=tableToMarkdown("IBM Guardium Events", events, headers=headers, removeNull=True), ), timestamp_field_result, ) """ MAIN FUNCTION """ def main() -> None: params = demisto.params() args = demisto.args() base_url = params.get("url", "").rstrip("/") credentials = params.get("credentials", {}) api_key = credentials.get("identifier") api_secret = credentials.get("password") report_id = str(params.get("report_id", "")) verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) is_fetch = params.get("isFetchEvents", False) timestamp_field = params.get("timestamp_field", "") try: client = Client(base_url=base_url, auth=(api_key, api_secret), verify=verify_certificate, proxy=proxy) command = demisto.command() demisto.debug(f"Executing command: {command}") if command == "test-module": return_results(test_module_command(client, report_id, is_fetch, timestamp_field)) elif command == "fetch-events": max_fetch = arg_to_number(params.get("max_fetch")) or DEFAULT_MAX_FETCH last_run = demisto.getLastRun() events, next_run = fetch_events_command(client, report_id, max_fetch, last_run, timestamp_field) send_events_to_xsiam_with_time(events, timestamp_field) demisto.setLastRun(next_run) demisto.debug(f"Successfully sent {len(events)} events to XSIAM and set last run to: {next_run}") elif command == "ibm-guardium-get-events": events, results, timestamp_field_result = get_events_command(client, report_id, args, timestamp_field) if argToBoolean(args.get("should_push_events", False)) and timestamp_field_result: send_events_to_xsiam_with_time(events, timestamp_field_result) demisto.debug(f"Successfully sent {len(events)} events to XSIAM") return_results(f"Sent {len(events)} events to XSIAM") return_results(results) except Exception as e: error_msg = f"Failed to execute {demisto.command()} command.\nError: {str(e)}\nTraceback: {traceback.format_exc()}" return_error(error_msg) if __name__ in ("__main__", "__builtin__", "builtins"): main()