GitLab Event Collector
Analytics & SIEM · GitLab
Details
| ID | GitLab Event Collector |
|---|---|
| Provider | GitLab Inc |
| Category | Analytics & SIEM |
| From Version | 6.8.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | Agentix XSIAM |
README
An event collector for GitLab audit events using Gitlab’s API.
Audit events API documentation
Prerequisites
To retrieve audit events using the API, you must authenticate yourself as an Administrator.
You must use Personal access tokens:
Create a Personal Access Token
- In the upper-right corner, select your avatar.
- Select Edit profile.
- On the left sidebar, select Personal access tokens.
- Select Add new token.
- In Token name, enter a name for the token.
- Optional. In Token description, enter a description for the token.
- In Expiration date, enter an expiration date for the token.
- The token expires on that date at midnight UTC. A token with the expiration date of 2024-01-01 expires at 00:00:00 UTC on 2024-01-01.
- If you do not enter an expiration date, the expiration date is automatically set to 365 days later than the current date.
- By default, this date can be a maximum of 365 days later than the current date. In GitLab 17.6 or later, you can extend this limit to 400 days.
- Select the desired scopes (see PAT scopes).
- Select Create personal access token.
Configure Gitlab Event Collector in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | True | |
| API Key | The personal access token created above with Administrator authorization. | True |
| Fetch Instance Audit Events | When checked, the fetch mechanism will fetch events from the audit_events endpoint. That endpoint requires Administrator authorization. See Audit Events API documentation for more details. | |
| Groups IDs | False | |
| Projects IDS | False | |
| First fetch timestamp (<number> <time unit>, for example, 12 hours, 7 days, 3 months, 1 year) | True | |
| The maximum number of events per fetch for each event type | Each fetch will bring the `limit` number of events for each type (audits, groups and projects) and each group/project ID. For example, if `limit` is set to 500 and groups/projects IDs are given as well, then the fetch will bring 500 audit events and 500 group/project events for each group/project ID. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute the following command from the Cortex XSOAR 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.
gitlab-get-events
Manual command to fetch events and display them.
Base Command
gitlab-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | Set this argument to True in order to create events, otherwise the command will only display them. Default is False. | True |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (required)api_key— (required)fetch_instance_audit_events— Fetch Instance Audit Eventsgroup_ids— Groups IDsproject_ids— Projects IDSafter— First fetch timestamp (<number> <time unit>, for example, 12 hours, 7 days, 3 months, 1 year) (required)limit— The maximum number of events per fetch for each event typeinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
gitlab-get-eventsManual command to fetch events and display them.
import demistomock as demisto from CommonServerPython import * """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" VENDOR = "gitlab" PRODUCT = "gitlab" DEFAULT_LIMIT = 500 """ HELPER FUNCTIONS """ def arg_to_strtime(value: Any) -> Optional[str]: if datetime_obj := arg_to_datetime(value): return datetime_obj.strftime(DATE_FORMAT) return None def prepare_query_params(params: dict, last_run: dict = {}) -> str: """ Parses the given inputs into GitLab Events API expected query params URL. """ if next_url := last_run.get("next_url"): return next_url return f"pagination=keyset&created_after={arg_to_strtime(params.get('after'))}&per_page=100" def reformat_details(audit_events: list, groups_and_projects_events: list) -> tuple[list, list]: for event in groups_and_projects_events: if "details" in event: for action in ["add", "change", "remove"]: if action in event["details"]: event["details"]["action"] = f'{action}_{event["details"][action]}' event["details"]["action_type"] = action event["details"]["action_category"] = event["details"][action] break for event in audit_events + groups_and_projects_events: event["_time"] = event["created_at"] return audit_events, groups_and_projects_events """ CLIENT CLASS """ class Client(BaseClient): def get_events_request(self, query_params_url: str | None, url_suffix: str = "/audit_events") -> tuple: """ Sends request to get events from GitLab. """ response = self._http_request(method="GET", url_suffix=f"{url_suffix}?{query_params_url}", resp_type="response") events = response.json() next_url = (response.links or {}).get("next", {}).get("url") demisto.debug(f"Successfully got {len(events)} events.") return events, next_url def handle_pagination_first_batch(self, query_params_url: str | None, last_run: dict, url_suffix: str) -> tuple: """ Makes the first events API call in the current fetch run. If `first_id` exists in the lastRun obj, finds it in the response and returns only the subsequent events (that weren't collected yet). """ events, next_url = self.get_events_request(query_params_url, url_suffix) if last_run.get("first_id"): for idx, event in enumerate(events): if event.get("id") == last_run["first_id"]: events = events[idx:] break last_run.pop("first_id", None) # removing to make sure it won't be used in future runs return events, next_url def fetch_events( self, query_params_url: str, last_run: dict, user_defined_params: dict, url_suffix: str = "/audit_events" ) -> List[dict]: """ Aggregates events using cursor-based pagination, until one of the following occurs: 1. Encounters an event that was already fetched in a previous run / reaches the end of the pagination. In both cases, clears the cursor from the lastRun obj, updates `last_id` to know where to stop in the next runs and returns the aggregated logs. 2. Reaches the user-defined limit (parameter). In this case, stores the last used URL suffix and the id of the next event to collect (`first_id`) and returns the events that have been accumulated so far. 3. Reaches a rate limit. In this case, stores the last URL suffix used in the lastRun obj and returns the events that have been accumulated so far. """ aggregated_events: List[dict] = [] user_defined_limit = arg_to_number(user_defined_params.get("limit")) or DEFAULT_LIMIT current_query_params_url: str | None = query_params_url try: events, next_url = self.handle_pagination_first_batch(current_query_params_url, last_run, url_suffix) while events: for event in events: if event.get("id") == last_run.get("last_id"): demisto.debug("Encountered an event that was already fetched - stopping.") current_query_params_url = None if aggregated_events: last_run.update({"last_id": aggregated_events[0].get("id")}) break if len(aggregated_events) == user_defined_limit: demisto.debug(f"Reached the user-defined limit ({user_defined_limit}) - stopping.") last_run["first_id"] = event.get("id") break aggregated_events.append(event) else: # Finished iterating through all events in this batch if next_url: demisto.debug("Using the given next_url from the last API call to execute the next call.") current_query_params_url = next_url.split("?", 1)[1] events, next_url = self.get_events_request(current_query_params_url, url_suffix) continue else: current_query_params_url = None demisto.debug("Finished iterating through all events in this fetch run for the current event type.") break except DemistoException as e: if not e.res or e.res.status_code != 429: raise e demisto.debug("Reached API rate limit, storing last used cursor.") if not last_run.get("last_id") and aggregated_events: last_run["last_id"] = aggregated_events[0].get("id") last_run["next_url"] = current_query_params_url return aggregated_events """ COMMAND FUNCTIONS """ def test_module_command(client: Client, params: dict, events_types_ids: dict) -> str: """ Tests connection to GitLab. Tests also given groups / projects ids that are exists. Args: client (Client): The client implementing the API to GitLab. params (dict): The configuration parameters. events_collection_management (dict): The event types ids collection. Returns: (str) 'ok' if success. """ fetch_events_command(client, params, {}, events_types_ids) return "ok" def get_events_command(client: Client, args: dict) -> tuple[list, CommandResults]: """ Gets log events from GitLab. Args: client (Client): the client implementing the API to GitLab. args (dict): the command arguments. Returns: (list) the events retrieved from the API call. (CommandResults) the CommandResults object holding the collected events information. """ query_params_url = prepare_query_params(args) events, _ = client.get_events_request(query_params_url) results = CommandResults( raw_response=events, readable_output=tableToMarkdown("GitLab Events", events, date_fields=["created_at"], removeNull=True), ) return events, results def fetch_events_command(client: Client, params: dict, last_run: dict, event_type_management: dict) -> tuple[list, list, dict]: """ Collects log events from GitLab using pagination. Args: client (Client): the client implementing the API to GitLab. params (dict): the instance configuration parameters. last_run (dict): the lastRun object, holding information from the previous run. event_type_management (dict): The groups and projects Ids to fetch events for as well as whether to fetch instance events. Returns: (list) the audit events retrieved from the API call. (list) the groups and projects events retrieved from the API call. (dict) the updated lastRun object. """ should_fetch_instance_events = event_type_management.get("instance_events") audit_events = [] if should_fetch_instance_events: query_params_url = prepare_query_params(params, last_run.get("audit_events", {})) audit_events.extend(client.fetch_events(query_params_url, last_run.get("audit_events", {}), params)) demisto.debug(f"Aggregated audits events: {len(audit_events)}") group_and_project_events = [] for event_type in ["groups", "projects"]: for obj_id in event_type_management.get(f"{event_type}_ids", []): query_params_url = prepare_query_params(params, last_run.get(event_type, {})) events = client.fetch_events( query_params_url, last_run.get(event_type, {}), params, url_suffix=f"/{event_type}/{obj_id}/audit_events" ) group_and_project_events.extend(events) demisto.debug(f"Aggregated group and project events: {len(group_and_project_events)}") return audit_events, group_and_project_events, last_run """ MAIN FUNCTION """ def main() -> None: command = demisto.command() params = demisto.params() args = demisto.args() demisto.debug(f"Command being called is {command}") try: client = Client( base_url=f"{params['url']}/api/v4", verify=not params.get("insecure", False), proxy=params.get("proxy", False), headers={"PRIVATE-TOKEN": params.get("api_key", {}).get("password")}, ) events_collection_management = { "instance_events": argToBoolean(params.get("fetch_instance_audit_events", True)), "groups_ids": argToList(params.get("group_ids", "")), "projects_ids": argToList(params.get("project_ids", "")), } if command == "test-module": return_results(test_module_command(client, params, events_collection_management)) elif command == "gitlab-get-events": events, results = get_events_command(client, params) return_results(results) if argToBoolean(args.get("should_push_events", "false")): send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) elif command == "fetch-events": last_run = demisto.getLastRun() if not last_run: last_run = {"groups": {}, "projects": {}, "audit_events": {}} audit_events, group_and_project_events, last_run = fetch_events_command( client, params, last_run, events_collection_management ) audit_events, group_and_project_events = reformat_details(audit_events, group_and_project_events) send_events_to_xsiam(audit_events + group_and_project_events, vendor=VENDOR, product=PRODUCT) demisto.setLastRun(last_run) except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()