BitSight Event Collector
Use this integration to fetch BitSight findings as events in XSIAM.
Analytics & SIEM · Bitsight
Details
| ID | BitSight Event Collector |
|---|---|
| Provider | BitSight |
| Category | Analytics & SIEM |
| From Version | 6.9.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Use this integration to fetch BitSight security findings as events in Cortex XSIAM. This is important for organizations that wish to integrate BitSight programmatically into their security operations.
When configured as a fetching integration, it will continuously fetch new findings starting from the current day. The manual bitsight-get-events command fetches findings from the last 2 days (48 hours).
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure BitSight Event Collector in Cortex
| Parameter | Description | Required | Default Value |
|---|---|---|---|
| Server URL | REST API Endpoint of BitSight server. | True | https://api.bitsighttech.com |
| API Key | BitSight API token. | True | - |
| Company’s GUID | Optional. If provided, findings for this company and its subsidiaries will be collected. If omitted, the collector attempts to use myCompany.guid. |
False | - |
| Trust any certificate (not secure) | False | - | |
| Use system proxy settings | False | - | |
| Max events per fetch | Maximum number of findings to fetch at a time. | False | 1000 |
| Events Fetch Interval | Interval between fetch operations. | False | 5 |
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.
bitsight-get-events
Gets findings as events from BitSight Event Collector. The command fetches findings from the last 2 days (48 hours).
Base Command
bitsight-get-events
Input
| Argument Name | Description | Required | Default Value |
|---|---|---|---|
| limit | The number of events to return. | Optional | 5 |
| guid | Override the Company GUID for this command only. | Optional | - |
| should_push_events | Set this argument to True in order to create events, otherwise the command will only display them. Possible values are: true, false. | Required | false |
Context Output
There is no context output for this command.
Notes
API Date Format Limitation: The BitSight API returns first_seen timestamps in date-only format (YYYY-MM-DD) without hours or minutes. The integration converts these to full timestamps in the _time field for XSIAM compatibility. This means all events will show timestamps like 2025-08-30T00:00:00 (midnight UTC) regardless of when during that day the finding was actually discovered.
Configuration parameters
base_url— Server URL (required)credentials— API Key (required)guid— Company's GUIDinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsmax_fetch— Max events per fetchisFetchEvents— Fetch eventseventFetchInterval— Events Fetch Interval
Commands (1)
-
bitsight-get-eventsGets BitSight findings events from the last 2 days (48 hours).
from datetime import datetime, timedelta, UTC from typing import Any import traceback import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 import requests import urllib3 # Disable insecure warnings urllib3.disable_warnings() VENDOR = "Bitsight" PRODUCT = "Bitsight" BITSIGHT_DATE_FORMAT = "%Y-%m-%d" DEFAULT_MAX_FETCH = 1000 GET_EVENTS_LOOKBACK_DAYS = 2 # Bitsight headers per existing integration CALLING_PLATFORM_VERSION = "XSIAM" CONNECTOR_NAME_VERSION = f"Bitsight - {get_pack_version() or '1.0.0'}" class Client(BaseClient): """Client to interact with Bitsight API.""" def get_companies_guid(self) -> dict[str, Any]: """Retrieve the companies metadata for the authenticated API key. Returns: dict[str, Any]: The response payload from Bitsight's `/v1/companies` API, containing information about the authorized company (e.g., `myCompany`) and related metadata. """ return self._http_request(method="GET", url_suffix="v1/companies") def get_company_findings( self, guid: str, first_seen_gte: str, last_seen_lte: str, limit: int, offset: int, ) -> dict[str, Any]: """Get company findings for given guid and time range. Args: guid (str): guid of the company whose findings need to be retrieved first_seen_gte (str): first seen date (YYYY-MM-DD) of the findings last_seen_lte (str): last seen date (YYYY-MM-DD) of the findings limit (int): maximum number of findings to return offset (int): offset to begin returning findings from Returns: dict[str, Any]: findings response """ params = { "first_seen_gte": first_seen_gte, "last_seen_lte": last_seen_lte, "unsampled": "true", "expand": "attributed_companies", "limit": limit, "offset": offset, } return self._http_request(method="GET", url_suffix=f"v1/companies/{encode_string_results(guid)}/findings", params=params) def to_bitsight_date(ts: int) -> str: """Convert a UNIX timestamp (seconds) to Bitsight date format (YYYY-MM-DD). Args: ts (int): UNIX timestamp in seconds. Returns: str: Date string formatted as YYYY-MM-DD (UTC). """ return datetime.fromtimestamp(ts, tz=UTC).strftime(BITSIGHT_DATE_FORMAT) def findings_to_events(findings: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[str]]: """Transform Bitsight findings into XSIAM event objects. - Preserves original finding fields. - Sets the XSIAM `_time` field using the finding's `first_seen` (YYYY-MM-DD) when available. - Collects findings without dates for later error handling. Args: findings (list[dict[str, Any]]): A list of Bitsight findings as returned from the API. Returns: tuple[list[dict[str, Any]], list[str]]: A tuple containing: - A list of event dictionaries suitable for pushing to XSIAM - A list of finding IDs that lack date fields (for error handling after offset update) """ events: list[dict[str, Any]] = [] missing_date_findings: list[str] = [] for f in findings: event = dict(f) # Set XSIAM time field to first_seen first_seen = f.get("first_seen") or f.get("firstSeen") if first_seen: # Bitsight returns YYYY-MM-DD for first_seen try: first_seen_dt = datetime.strptime(first_seen, BITSIGHT_DATE_FORMAT) event["_time"] = first_seen_dt.strftime("%Y-%m-%dT%H:%M:%S") except Exception as e: demisto.debug(f"Failed to parse first_seen date '{first_seen}' for finding {f.get('id', 'unknown')}: {e}") event["_time"] = first_seen else: # Track findings missing dates for later error handling finding_id = f.get("id", "unknown") missing_date_findings.append(finding_id) events.append(event) return events, missing_date_findings def fetch_events( client: Client, guid: str, max_fetch: int, last_run: dict[str, Any], lookback_days: int | None = None, ) -> tuple[list[dict[str, Any]], dict[str, Any], list[str]]: """Fetch Bitsight findings as events using offset pagination from a fixed starting date. This function pages through Bitsight findings for the specified company starting from a fixed date (first_fetch) and uses offset pagination to avoid duplicates. Since the API only supports date-level precision, we start from current date for scheduled fetches or use lookback_days for command calls. Args: client (Client): Initialized API client. guid (str): Company GUID to collect findings for. max_fetch (int): Maximum number of findings to request for this page. last_run (dict[str, Any]): State from the previous run (expects keys `first_fetch`, `offset`). lookback_days (int | None): Days to look back for initial fetch. If None, uses current date. Returns: tuple[list[dict[str, Any]], dict[str, Any], list[str]]: A tuple containing: - list of events - updated last_run dictionary with incremented offset - list of finding IDs that lack date fields (for error handling by caller) """ if "offset" in last_run: offset = last_run["offset"] first_fetch_date = last_run["first_fetch"] else: # Initial fetch - start from current date or lookback offset = 0 current_time = datetime.now() if lookback_days: lookback_time = current_time - timedelta(days=lookback_days) first_fetch_date = to_bitsight_date(int(lookback_time.timestamp())) else: first_fetch_date = to_bitsight_date(int(current_time.timestamp())) # Always use same starting date, current date as end first_seen_gte = first_fetch_date last_seen_lte = to_bitsight_date(int(datetime.now().timestamp())) res = client.get_company_findings( guid, first_seen_gte=first_seen_gte, last_seen_lte=last_seen_lte, limit=max_fetch, offset=offset ) findings = res.get("results", []) events, missing_date_findings = findings_to_events(findings) # Update last_run with incremented offset (matches Performance Management pattern) # Note: Although the API returns pagination links (next/previous) in rare cases of very large amounts of data, # we ignore them since our offset-based approach will automatically fetch remaining data in subsequent calls # See: https://help.bitsighttech.com/hc/en-us/articles/360050111794-Pagination new_offset = offset + len(events) new_last_run: dict[str, Any] = { "first_fetch": first_fetch_date, "offset": new_offset, } return events, new_last_run, missing_date_findings def bitsight_get_events_command(client: Client, guid: str, limit: int, should_push: bool) -> CommandResults: """Command implementation for `bitsight-get-events`. Executes a one-off retrieval of findings for the last 2 days (48 hours) and optionally pushes them to XSIAM when `should_push_events=true`. Args: client (Client): Initialized API client. guid (str): Resolved company GUID to collect findings for. limit (int): Max events to fetch in this invocation. should_push (bool): When true, pushes events to XSIAM; otherwise returns them as output only. Returns: CommandResults: CommandResults object with table output (when not pushing) or a summary message. """ events, _, missing_date_findings = fetch_events( client, guid=guid, max_fetch=int(limit), last_run={}, lookback_days=GET_EVENTS_LOOKBACK_DAYS ) title = "Bitsight Findings Events (pushed)" if should_push else "Bitsight Findings Events" if should_push: send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) if missing_date_findings: raise ValueError(f"No first_seen date found for findings: {', '.join(missing_date_findings)}") return CommandResults(readable_output=tableToMarkdown(title, events, removeNull=True)) def test_module(client: Client, guid: str | None) -> str: """Validate connection and optional GUID permissions. Performs a lightweight API call to ensure credentials are valid. If a GUID is provided in integration parameters, also validates that the findings endpoint for that GUID is reachable. Args: client (Client): Initialized API client. guid (str | None): Optional company GUID to validate findings access for. Returns: str: "ok" when validation succeeds; otherwise raises an error. """ # Validate credentials and, if guid provided, that findings endpoint is reachable client.get_companies_guid() if guid: # Use a simple 2-day lookback for testing connectivity current_time = datetime.now() lookback_time = current_time - timedelta(days=GET_EVENTS_LOOKBACK_DAYS) start_date = to_bitsight_date(int(lookback_time.timestamp())) end_date = to_bitsight_date(int(current_time.timestamp())) client.get_company_findings(guid, start_date, end_date, limit=1, offset=0) return "ok" """ HELPER FUNCTIONS """ def handle_api_error(e: DemistoException, context: str = "API operation") -> str: """Centralized error handling for DemistoException with user-friendly messages. Args: e: The DemistoException to handle context: Context description for the error message Returns: User-friendly error message string """ error_str = str(e) message = None if "401" in error_str or "Unauthorized" in error_str: message = "Authentication failed. Please verify your API key is correct and has proper permissions." elif "403" in error_str or "Forbidden" in error_str: message = "Access denied. Your API key does not have sufficient permissions to access BitSight data." elif "404" in error_str or "Not Found" in error_str: message = "Resource not found. Please verify the Company GUID is correct." # Return formatted error message if message: return f"{context} failed: {message} Original error: {error_str}" else: return f"{context} failed: {error_str}" def resolve_guid(client: Client, guid_from_args: str | None, guid_from_params: str | None) -> str: """Resolve the company GUID, preferring command arg, then integration param, then myCompany. Raises ValueError if no GUID could be determined. """ guid = guid_from_args or guid_from_params if guid: return guid companies = client.get_companies_guid() guid = (companies.get("my_company") or {}).get("guid") if not guid: raise ValueError("Company GUID is required. Provide it as an argument or in the integration parameters.") return guid def main(): # Extract command and parameters params = demisto.params() args = demisto.args() command = demisto.command() # Extract configuration variables base_url = params.get("base_url") or "https://api.bitsighttech.com" verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) api_key = params.get("credentials", {}).get("identifier", "") # Extract command-specific variables max_fetch = arg_to_number(params.get("max_fetch", DEFAULT_MAX_FETCH)) or DEFAULT_MAX_FETCH should_push = argToBoolean(args.get("should_push_events", "false")) if args.get("should_push_events") else False limit = arg_to_number(args.get("limit")) or 5 last_run = demisto.getLastRun() or {} demisto.debug(f"Command being called is {command}") # Create API client client = Client( base_url=base_url, verify=verify_certificate, proxy=proxy, ok_codes=[200], auth=requests.auth.HTTPBasicAuth(api_key, ""), headers={ "X-BITSIGHT-CALLING-PLATFORM_VERSION": CALLING_PLATFORM_VERSION, "X-BITSIGHT-CONNECTOR-NAME-VERSION": CONNECTOR_NAME_VERSION, }, ) try: guid = resolve_guid(client, args.get("guid"), params.get("guid")) if command == "test-module": return_results(test_module(client, params.get("guid"))) elif command == "fetch-events": events, new_last_run, missing_date_findings = fetch_events(client, guid=guid, max_fetch=max_fetch, last_run=last_run) demisto.setLastRun(new_last_run) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) demisto.debug(f"Fetched and pushed {len(events)} events") if missing_date_findings: raise ValueError(f"No first_seen date found for findings: {', '.join(missing_date_findings)}") return elif command == "bitsight-get-events": return_results(bitsight_get_events_command(client, guid, limit, should_push)) else: raise NotImplementedError(f"Command {command} is not implemented") except DemistoException as e: demisto.error(traceback.format_exc()) return_error(handle_api_error(e, f"{command} command")) except Exception as e: demisto.error(traceback.format_exc()) return_error(f"Failed to execute {command} command. Error: {e}") if __name__ in ["__main__", "builtin", "builtins"]: main()